Commands And Shortcuts - AngryCarrot789/MemoryEngine360 GitHub Wiki
This system is inspired by IntelliJ IDEA's action system, where you have a CommandManager which contains all registered commands.
You access commands via a string key, and then execute the command by passing in contextual data (stored in a IContextData
), which contains the method bool TryGetContext(string key, out object? value)
.
We don't use the key in a raw fashion like this, instead, we use the DataKey<T>
type, which encapsulates a string key and also a type, which helps prevent issues when objects are the wrong type.
The DataManager
class manages the context data for all UI components (scroll down to the bottom for more info)
This means that when you for example press F2 while focused on the root window, there won't be much contextual information.
But if you click it when focused on say a saved address table entry, there's a lot of context information there (window, memory engine, address table manager, address table entry); It's context-sensitive, duh
You can register custom commands in your plugin's RegisterCommands
method, like so:
public override void RegisterCommands(CommandManager manager) {
manager.Register(
"myplugin.commands.editor.ShowCompTimlineName",
new ShowCompTimlineNameCommand()
);
}
private class ShowCompTimlineNameCommand : Command {
protected override Executability CanExecuteCore(CommandEventArgs e) {
if (!DataKeys.TimelineKey.TryGetContext(e.ContextData, out Timeline? timeline)) {
// Normally, MenuItem will be invisible, or button will be disabled.
// This is not guaranteed though.
return Executability.Invalid;
}
return timeline is CompositionTimeline
? Executability.Valid // Control clickable
: Executability.ValidButCannotExecute; // Control disabled
}
protected override async Task ExecuteAsync(CommandEventArgs e) {
// DataKey<T> contains a TryGetContext which delegates to the `IContextData`'s method,
// and does type checking too
if (!Timeline.DataKey.TryGetContext(e.ContextData, out Timeline timeline))
return;
if (!(timeline is CompositionTimeline composition))
return;
await IMessageDialogService.Instance.ShowMessage(
"hello", $"My resource = '{composition.Resource.DisplayName}'"
);
}
}
The shortcut system replaces Avalonia's built-in shortcut system, since it does not easily support changing the shortcuts, nor does WPF's shortcut system.
There is a singleton ShortcutManager instance. Each window whose UIInputManager.FocusPath
attached property value is set will have a ShortcutProcessor (which is created by the manager) and that handles the input for that specific window. The manager stores a root ShortcutGroup, forming a hierarchy of groups and shortcuts.
The class ShortcutGroup
is a group of shortcuts and GroupedShortcut
is a shortcut within a group, it is named this way to differentiate from IShortcut
. They all have their own identifier Name
, unique relative to their parent, which forms a FullPath
for each object in the tree. This is exactly like a file system, and each group is separated by a '/' character.
Parts of the UI would have their UIInputManager.FocusPath
value set to the full path of ideally a ShortcutGroup that is specific to that part of the UI.
For example, MemoryEngine360's Address Table's FocusPath is set to "MemEngineWindow/SavedAddressList"
, which means only shortcuts in that group can be applied. Any shortcut outside that group are not checked.
A shortcut could be activated with a single keystroke (e.g. S or CTRL+X), or by a long chain or sequential input strokes (LMB click, CTRL+SHIFT+X, B, Q, WheelUp, ALT+E to finally activate a shortcut)
This shortcut for example would fire the command "commands.sequencer.RenameSequenceCommand"
when CTRL+R
is pressed twice. You can remove either of the KeyStroke
s to make it single-press. Key and Mouse shortcuts can be mixed in a shortcut, but you probably don't want to do this since it'd be weird for the user to use.
<Shortcut Name="RenameSequence" CommandId="commands.sequencer.RenameSequenceCommand">
<KeyStroke Mods="CTRL" Key="R"/>
<KeyStroke Mods="CTRL" Key="R"/>
</Shortcut>
Long story short, the shortcut system figures out a list of shortcuts to "activate" based on the current global focus path (which is gets from the FocusPath
of the currently focused element), and activates all shortcuts until one of them does something (e.g. executes a command).
Keymap.xml contains the shortcuts