Skip to content

Commands And Shortcuts

REghZy edited this page Sep 21, 2025 · 4 revisions

Command System

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,
});

Example command

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;
        }
    }
}

Shortcut System

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.

Classes and properties

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.

Making them work with the UI

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).

Chained 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>

What if multiple shortcuts exist with the same hotkey in the same scope?

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

Clone this wiki locally