-
Notifications
You must be signed in to change notification settings - Fork 4
Commands And Shortcuts
This system is inspired by IntelliJ IDEA's action system. The CommandManager contains registered commands. You access commands via a string key. To execute a command you provide contextual data.
Note
It's recommended to use the command system instead of AsyncRelayCommand (or any other type of relay command), so that the same functionality can be triggered by buttons, menus, context menus and keyboard/mouse shortcuts seamlessly. The command system also provides info about the shortcut or the menu that caused the command to be executed.
If you still need ICommand, then consider using DataManagerCommandWrapper
The command system also manages a "local" global context stack, using AsyncLocal, to associate the current IContextData with the async call frame. This is accessible via CommandManager.LocalContextManager.
This is useful for commands that show windows/dialogs; rather than query the current window manually, the dialog service (e.g IUserInputDialogService and IMessageDialogService) can access the current command context and query the active window. This is how it can be done:
IWindow? parentWindow = WindowContextUtils.GetUsefulWindow();
if (parentWindow == null) {
return null; // invalid dialog result
}
IWindow window = parentWindow.WindowManager.CreateWindow(new WindowBuilder() {
...
Parent = parentWindow,
});You can register custom commands in your plugin's RegisterCommands method, like so:
public override void RegisterCommands(CommandManager manager) {
manager.Register(
"commands.sequencer.RenameSequenceCommand",
new RenameSequenceCommand()
);
}
public class RenameSequenceCommand : Command {
// This method is used mostly for visual feedback.
// Anything that isn't `Valid` marks the control as disabled.
// When used for a menu item, `Invalid` hides the item.
protected override Executability CanExecuteCore(CommandEventArgs e) {
if (!ITaskSequenceManagerUI.DataKey.TryGetContext(e.ContextData, out ITaskSequenceManagerUI? manager))
return Executability.Invalid; // Command was executed outside of the task sequencer scope
// We can execute when there's a single selected task sequence (since it supports multi-selection)
return manager.PrimarySelectedSequence != null ? Executability.Valid : Executability.ValidButCannotExecute;
}
protected override async Task ExecuteCommandAsync(CommandEventArgs e) {
if (!ITaskSequenceManagerUI.DataKey.TryGetContext(e.ContextData, out ITaskSequenceManagerUI? manager))
return;
ITaskSequenceEntryUI? sequenceUI = manager.PrimarySelectedSequence;
if (sequenceUI == null)
return;
SingleUserInputInfo info = new SingleUserInputInfo("Rename sequence", null, "New Name", sequenceUI.TaskSequence.DisplayName);
if (await IUserInputDialogService.Instance.ShowInputDialogAsync(info) == true) {
sequenceUI.TaskSequence.DisplayName = info.Text;
}
}
}The shortcut system is used to execute commands via the CommandManager when shortcuts are pressed (key and/or mouse shortcuts).
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; the forward slash (/) character is the group separator.
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 (unless they are marked as global shortcuts).
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, then CTRL+SHIFT+X, then B, then Q, then WheelUp, and then finally ALT+E to activate the shortcut)
This shortcut for example would fire the command "commands.sequencer.RenameSequenceCommand" when CTRL+R is pressed twice. You can remove either of the KeyStrokes 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>It will activate all of them until one does something. Typically the first one found is the first to be activated (which is the case when its Command ID is set and a command exists with that ID)
Keymap.xml contains the shortcuts
-
Home
- Connect to a console
- Scanning Options
- Scan results & Saved Address Table
- Remote Commands
- Memory Dump
- Tools
- Preferences/App Settings
-
API
- Making a custom connection
- Busy Tokens
- Models, ViewStates, MVP & Binding
- Plugins
- Config Pages
- Brushes and Icons
- Data Manager, Context Data and Data Keys
- Commands and Shortcuts
- Context Menus
- Windows and Dialogs