Skip to content

[KBM] Manual selection of keys#46377

Open
niels9001 wants to merge 6 commits intomainfrom
niels9001/kbm-manual-key-selection
Open

[KBM] Manual selection of keys#46377
niels9001 wants to merge 6 commits intomainfrom
niels9001/kbm-manual-key-selection

Conversation

@niels9001
Copy link
Copy Markdown
Collaborator

@niels9001 niels9001 commented Mar 21, 2026

Summary of the Pull Request

This PR introduces the following changes:

  • The ability to enter a key or shortcut, and then manual edit it via a dropdown.
  • A new KBM action called 'Disable key or shortcut', which allows you to... disable a key or shortcut :).

GIF:
KBM-Disbble

image image image

PR Checklist

  • Communication: I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected
  • Tests: Added/updated and all pass
  • Localization: All end-user-facing strings can be localized
  • Dev docs: Added/updated
  • New binaries: Added on the required places
  • Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed Description of the Pull Request / Additional comments

Validation Steps Performed

@niels9001 niels9001 marked this pull request as draft March 21, 2026 17:53
@niels9001 niels9001 marked this pull request as ready for review March 21, 2026 21:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the Keyboard Manager Editor UI to support manual key selection/editing via dropdowns and introduces a new “Disable key or shortcut” action, while also making the editor more resilient when the native KBM wrapper DLL isn’t available.

Changes:

  • Added a new “Disable key or shortcut” action type and UI section for displaying disabled mappings.
  • Introduced key dropdown UI (with new interop API) to manually adjust recorded trigger/action keys.
  • Hardened startup/keyboard-hook paths to tolerate missing native KBM DLL (standalone/read-only scenarios).

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/modules/keyboardmanager/KeyboardManagerEditorUI/Styles/Colors.xaml Minor cleanup in resource dictionary.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Styles/Button.xaml Adds DropDownButton styles used by key-selection UI.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Strings/en-US/Resources.resw Adds localized strings for the new Disable action and labels.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/SettingsManager.cs Makes native service optional; supports standalone/read-only mode.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml.cs Adds disabled mappings list handling + save/toggle logic for Disable action.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml Adds a new “Disabled” section to the consolidated mappings view.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardMappingService.cs Adds managed wrapper to fetch key lists from native code.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardManagerInterop.cs Adds P/Invoke + struct for native key list retrieval.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyNameEntry.cs New record type for key list entries.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/KeyboardHookHelper.cs Makes hook/service initialization more resilient when native components are missing.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml.cs Adds dropdown event handling and new Disable action support in the unified editor.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml Replaces KeyVisual with KeyDropDownButton and adds Disable action UI.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyDropDownButton.xaml.cs New control backing logic: key list flyout + caching + selection events.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyDropDownButton.xaml New dropdown-based key visual UI.
src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyChangedEventArgs.cs New event args type for dropdown key changes.

Comment on lines +160 to +163
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.Disable);
UnifiedMappingControl.SetAppSpecific(!disabledMapping.IsAllApps, disabledMapping.AppName);
RemappingDialog.Title = "Edit remapping";
await ShowRemappingDialog();
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemappingDialog.Title is being set with a hard-coded, non-localizable string. Since the dialog already has x:Uid, the title should come from Resources.resw (or a ResourceLoader) so it can be localized like other UI text.

Copilot uses AI. Check for mistakes.
Comment on lines +460 to +470
private bool SaveDisableMapping(List<string> triggerKeys)
{
// VK_DISABLED = 0x100 (256) — target "256" tells the engine to suppress the key
const string vkDisabledCode = "256";
bool isAppSpecific = UnifiedMappingControl.GetIsAppSpecific();
string appName = UnifiedMappingControl.GetAppName();

string originalKeysString = string.Join(
";",
triggerKeys.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(System.Globalization.CultureInfo.InvariantCulture)));

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SaveDisableMapping does not validate the trigger keys (e.g., modifier-only shortcuts, illegal shortcuts like Win+L, or duplicates) before saving. Because Disable is a new ActionType and ValidateMapping currently doesn’t cover it, this path can persist invalid/unsupported mappings (including 0 key codes if a dropdown provides a “None” option). Consider adding a dedicated validation step for Disable (reuse the same original-keys checks as ValidateKeyMapping without requiring remapped keys).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SaveDisableMapping does not validate the trigger keys (e.g., modifier-only shortcuts, illegal shortcuts like Win+L, or duplicates) before saving. [...]

I would argue that modifier-only shortcuts should also be customizable and deactivatable.

While these could lead to unintended side effects and shortcuts like Win+L are even more critical, these are also changes that could arise in legitimate use cases.

With modifier keys, I could imagine that entire key combinations might be swapped for ergonomic reasons. Other special/protected keys might've already been duplicated on customized keyboards.

There even seem to be hardening projects for kiosk mode that, for example, remove shortcuts and keys (such as the Win key).

Such changes (e.g.,
deactivating a modifier key) should already be possible with other programs. Win+L (and even the entire locking part) can be bypassed at the user level with a registry tweak like DisableLockWorkstation.

I therefore suggest not introducing these validations and restrictions and, if they already exist, removing them.
But that would likely be a different/new issue, so for now I’ll just ask you to not implement this suggestion by Copilot.

———

Regardless of the result,
thanks a lot for this fantastic PR!
It really does cover all the features.

Comment on lines +792 to +812
private void EnableDisabledMapping(Remapping remapping)
{
string originalKeysString = string.Join(
";",
remapping.Shortcut.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(System.Globalization.CultureInfo.InvariantCulture)));

if (remapping.Shortcut.Count == 1)
{
int originalKey = _mappingService!.GetKeyCodeFromName(remapping.Shortcut[0]);
if (originalKey != 0)
{
_mappingService.AddSingleKeyMapping(originalKey, 0x100);
}
}
else
{
_mappingService!.AddShortcutMapping(
originalKeysString,
"256",
!remapping.IsAllApps ? remapping.AppName : string.Empty);
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VK_DISABLED sentinel is hard-coded in multiple forms ("256" and 0x100) across SaveDisableMapping/LoadRemappings/EnableDisabledMapping. This duplication makes it easy for the behavior to drift; please centralize it into a single constant (string + int as needed) and reuse it everywhere you detect or apply a disabled mapping.

Copilot uses AI. Check for mistakes.
Comment on lines +298 to +323
dropDown.KeyChanged += TriggerKeyDropDown_KeyChanged;
dropDown.Unloaded += (s, _) =>
{
if (s is KeyDropDownButton dd)
{
dd.KeyChanged -= TriggerKeyDropDown_KeyChanged;
}
};
}
}

private void ActionKeyDropDown_Loaded(object sender, RoutedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
dropDown.KeyChanged += ActionKeyDropDown_KeyChanged;
dropDown.Unloaded += (s, _) =>
{
if (s is KeyDropDownButton dd)
{
dd.KeyChanged -= ActionKeyDropDown_KeyChanged;
}
};
}
}

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TriggerKeyDropDown_Loaded / ActionKeyDropDown_Loaded subscribe to KeyChanged on every Loaded and also attach a new anonymous Unloaded handler each time. In WinUI, Loaded/Unloaded can fire repeatedly for recycled visuals, which can lead to duplicate subscriptions and accumulating Unloaded handlers. Prefer subscribing once (e.g., inside KeyDropDownButton) or detach-before-attach and use a named Unloaded handler you can add/remove.

Suggested change
dropDown.KeyChanged += TriggerKeyDropDown_KeyChanged;
dropDown.Unloaded += (s, _) =>
{
if (s is KeyDropDownButton dd)
{
dd.KeyChanged -= TriggerKeyDropDown_KeyChanged;
}
};
}
}
private void ActionKeyDropDown_Loaded(object sender, RoutedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
dropDown.KeyChanged += ActionKeyDropDown_KeyChanged;
dropDown.Unloaded += (s, _) =>
{
if (s is KeyDropDownButton dd)
{
dd.KeyChanged -= ActionKeyDropDown_KeyChanged;
}
};
}
}
// Ensure we do not accumulate multiple subscriptions when Loaded fires repeatedly.
dropDown.KeyChanged -= TriggerKeyDropDown_KeyChanged;
dropDown.KeyChanged += TriggerKeyDropDown_KeyChanged;
// Use a named Unloaded handler so we can detach it and avoid accumulating handlers.
dropDown.Unloaded -= TriggerKeyDropDown_Unloaded;
dropDown.Unloaded += TriggerKeyDropDown_Unloaded;
}
}
private void TriggerKeyDropDown_Unloaded(object sender, RoutedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
dropDown.KeyChanged -= TriggerKeyDropDown_KeyChanged;
dropDown.Unloaded -= TriggerKeyDropDown_Unloaded;
}
}
private void ActionKeyDropDown_Loaded(object sender, RoutedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
// Ensure we do not accumulate multiple subscriptions when Loaded fires repeatedly.
dropDown.KeyChanged -= ActionKeyDropDown_KeyChanged;
dropDown.KeyChanged += ActionKeyDropDown_KeyChanged;
// Use a named Unloaded handler so we can detach it and avoid accumulating handlers.
dropDown.Unloaded -= ActionKeyDropDown_Unloaded;
dropDown.Unloaded += ActionKeyDropDown_Unloaded;
}
}
private void ActionKeyDropDown_Unloaded(object sender, RoutedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
dropDown.KeyChanged -= ActionKeyDropDown_KeyChanged;
dropDown.Unloaded -= ActionKeyDropDown_Unloaded;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +324 to +346
private void TriggerKeyDropDown_KeyChanged(object? sender, KeyChangedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
int index = GetDropDownIndex(TriggerKeys, dropDown);
if (index >= 0 && index < _triggerKeys.Count)
{
_triggerKeys[index] = e.NewKeyName;
HandleAutoGrowShrink(_triggerKeys, index, e.NewKeyCode);
}
}
}

private void ActionKeyDropDown_KeyChanged(object? sender, KeyChangedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
int index = GetDropDownIndex(ActionKeys, dropDown);
if (index >= 0 && index < _actionKeys.Count)
{
_actionKeys[index] = e.NewKeyName;
HandleAutoGrowShrink(_actionKeys, index, e.NewKeyCode);
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeyChanged handlers write the selected display name directly into _triggerKeys/_actionKeys without handling the special "None" entry (KeyCode=0) returned by LayoutMap when isShortcut=true. Since GetKeyCodeFromName("None") resolves to 0, this can later serialize invalid mappings ("0" in the key list) and bypass validation for Disable. Handle NewKeyCode==0 explicitly (e.g., treat as clearing/removing that slot), and consider updating app-specific enablement after trigger keys change.

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +104
var service = new KeyboardMappingService();
cached = service.GetKeyboardKeysList(isShortcut);
service.Dispose();
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetKeyList manually calls service.Dispose() only on the success path. If GetKeyboardKeysList throws after the service is constructed, the service won’t be disposed. Prefer a using/try-finally pattern so the native handle is reliably released even when exceptions occur.

Suggested change
var service = new KeyboardMappingService();
cached = service.GetKeyboardKeysList(isShortcut);
service.Dispose();
using (var service = new KeyboardMappingService())
{
cached = service.GetKeyboardKeysList(isShortcut);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 11 comments.

<value>Disable key or shortcut</value>
</data>
<data name="DisableDescription.Text" xml:space="preserve">
<value>This shortcut will be disabled.</value>
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DisableDescription.Text says "This shortcut will be disabled.", but the feature also supports disabling a single key. Consider changing the string to cover both cases (e.g., "This key or shortcut will be disabled.") so the UI remains accurate.

Suggested change
<value>This shortcut will be disabled.</value>
<value>This key or shortcut will be disabled.</value>

Copilot uses AI. Check for mistakes.
if (e.ClickedItem is KeyNameEntry entry)
{
string oldKeyName = KeyName;
KeyName = entry.DisplayName;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeyName is data-bound from the parent ItemsControl (e.g., KeyName="{Binding}" in UnifiedMappingControl). Setting KeyName directly here can break/override the binding and lead to the visual not tracking later source updates. Prefer not setting KeyName in the control and instead only raise KeyChanged (let the parent update the bound collection to drive the UI), including for reverting invalid selections.

Suggested change
KeyName = entry.DisplayName;

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +31
public static readonly DependencyProperty IsShortcutProperty =
DependencyProperty.Register(
nameof(IsShortcut),
typeof(bool),
typeof(KeyDropDownButton),
new PropertyMetadata(true));

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsShortcut defaults to true, which means the native key list includes a synthetic "None" entry with keycode 0 (see LayoutMap::GetKeyNameList for shortcut lists). If the user selects that entry, the UI will store/display "None" and later save paths can end up emitting keycode 0 in OriginalKeys (especially for multi-key mappings), producing an invalid mapping. Consider filtering out the 0/"None" entry for this UI, or treating selection of keycode 0 as clearing the slot (set empty string + trigger shrink logic) rather than a real key name.

Copilot uses AI. Check for mistakes.
Comment on lines +412 to +444
private static string? ValidateDropDownSelection(ObservableCollection<string> keys, int changedIndex, int newKeyCode, string newKeyName)
{
const int maxShortcutSize = 5;

// KeyType: 0=Win, 1=Ctrl, 2=Alt, 3=Shift, 4=Action
int newKeyType = KeyboardManagerInterop.GetKeyType(newKeyCode);
bool isModifier = newKeyType < 4;

// Rule: action key at position 0 in multi-key shortcut (shortcut must start with modifier)
if (!isModifier && changedIndex == 0 && keys.Count > 1)
{
return ResourceHelper.GetString("Warning_ShortcutStartWithModifier");
}

// Rule: no repeated modifier types
if (isModifier)
{
for (int i = 0; i < keys.Count; i++)
{
if (i == changedIndex)
{
continue;
}

int existingKeyCode = KeyboardManagerInterop.GetKeyCodeFromName(keys[i]);
int existingKeyType = KeyboardManagerInterop.GetKeyType(existingKeyCode);

if (existingKeyType == newKeyType)
{
return ResourceHelper.GetString("Warning_RepeatedModifier");
}
}
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ValidateDropDownSelection iterates over keys and calls KeyboardManagerInterop.GetKeyCodeFromName(keys[i]) even when entries are placeholders (string.Empty) added by HandleAutoGrowShrink. This can incorrectly trigger repeated-modifier warnings (and treats empty as keycode 0). Consider filtering to non-empty entries when validating (and use the count of non-empty keys for rules like "shortcut must start with a modifier").

Copilot uses AI. Check for mistakes.
Comment on lines +353 to +358
string? validationError = ValidateDropDownSelection(_actionKeys, index, e.NewKeyCode, e.NewKeyName);
if (validationError != null)
{
dropDown.KeyName = e.OldKeyName;
ShowNotificationTip(validationError);
return;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same binding concern in ActionKeyDropDown_KeyChanged: reverting by setting dropDown.KeyName = e.OldKeyName can override/break the KeyName="{Binding}" expression. Consider keeping the underlying _actionKeys[index] unchanged on validation failure and drive the UI back to the old value through the collection/binding instead.

Copilot uses AI. Check for mistakes.
Comment on lines 816 to 824
int index = actionType switch
{
ActionType.Text => 1,
ActionType.OpenUrl => 2,
ActionType.OpenApp => 3,
ActionType.MouseClick => 4,
ActionType.Disable => 4,
ActionType.MouseClick => 5,
_ => 0,
};
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetActionType relies on hard-coded ComboBox indices. This is brittle (and currently ActionType.MouseClick => 5 doesn't correspond to any visible ComboBoxItem since MouseClick is commented out in XAML). Consider selecting the item by matching the ComboBoxItem.Tag instead of by index, so adding/removing items in XAML can't desync the mapping.

Copilot uses AI. Check for mistakes.
Comment on lines 98 to 106
// Process all shortcut mappings (RunProgram, OpenUri, RemapShortcut, RemapText)
foreach (ShortcutKeyMapping mapping in _mappingService.GetShortcutMappings())
foreach (ShortcutKeyMapping mapping in _mappingService!.GetShortcutMappings())
{
AddShortcutMapping(settings, mapping);
}

// Process single key to key mappings
foreach (var mapping in _mappingService.GetSingleKeyMappings())
foreach (var mapping in _mappingService!.GetSingleKeyMappings())
{
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateSettingsFromKeyboardManagerService mixes _mappingService! and _mappingService access. Since _mappingService is now nullable, the later foreach (var mapping in _mappingService.GetKeyToTextMappings()) can raise nullable warnings (and potentially fail builds if warnings are treated as errors). Suggest using _mappingService! consistently in this method, or refactor to pass a non-null service instance into the method.

Copilot uses AI. Check for mistakes.
Logger.LogError("Failed to initialize KeyboardMappingService in MainPage page: " + ex.Message);
}

LoadAllMappings();
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoadAllMappings() now runs even if KeyboardMappingService fails to initialize (so _mappingService stays null). In that state, the UI will still populate from JSON, but interactive actions like toggling active state or deleting will early-return (and toggles can visually change without persisting). Consider explicitly switching the page into a read-only mode when _mappingService is null (e.g., disable toggle switches / delete buttons / save actions, or keep LoadAllMappings() inside the successful init path if standalone mode isn't intended for end users).

Suggested change
LoadAllMappings();
if (_mappingService != null)
{
LoadAllMappings();
}
else
{
MappingState = "Error";
}

Copilot uses AI. Check for mistakes.
Comment on lines +563 to +567
IsOn="{x:Bind IsActive}"
Style="{StaticResource RightAlignedCompactToggleSwitchStyle}"
Toggled="ToggleSwitch_Toggled" />
<Button
VerticalAlignment="Center"
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Disabled section’s ToggleSwitch is always enabled, but the toggle handler is a no-op when the mapping service isn't available. This can leave the UI showing a changed active state that wasn't applied. Bind IsEnabled (and ideally the delete button too) to a service-availability flag (e.g., SettingsManager.IsNativeServiceAvailable exposed via the page/viewmodel) so the section is clearly read-only when native interop isn't present.

Suggested change
IsOn="{x:Bind IsActive}"
Style="{StaticResource RightAlignedCompactToggleSwitchStyle}"
Toggled="ToggleSwitch_Toggled" />
<Button
VerticalAlignment="Center"
IsOn="{x:Bind IsActive}"
IsEnabled="{x:Bind IsNativeServiceAvailable, Mode=OneWay}"
Style="{StaticResource RightAlignedCompactToggleSwitchStyle}"
Toggled="ToggleSwitch_Toggled" />
<Button
VerticalAlignment="Center"
IsEnabled="{x:Bind IsNativeServiceAvailable, Mode=OneWay}"

Copilot uses AI. Check for mistakes.
Comment on lines +331 to +337
string? validationError = ValidateDropDownSelection(_triggerKeys, index, e.NewKeyCode, e.NewKeyName);
if (validationError != null)
{
// Revert the selection
dropDown.KeyName = e.OldKeyName;
ShowNotificationTip(validationError);
return;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On invalid dropdown selection you revert by setting dropDown.KeyName = e.OldKeyName, but KeyName is bound (KeyName="{Binding}") so directly setting it can override/break the binding and may not reliably revert the displayed value. Prefer reverting via the bound source (leave _triggerKeys[index] unchanged and refresh the dropdown selection from that), rather than writing to the bound DP.

Copilot uses AI. Check for mistakes.
@niels9001 niels9001 added the Product-Keyboard Shortcut Manager Issues regarding Keyboard Shortcut Manager label Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Product-Keyboard Shortcut Manager Issues regarding Keyboard Shortcut Manager

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Keyboard Manager] Cannot Disable Key / Shortcut using the new editor [v0.98.0]

3 participants