Commands And Shortcuts - AngryCarrot789/MemoryEngine360 GitHub Wiki

Command System

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

Example command

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}'"
        );
    }
}

Shortcut System

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.

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, and each group is separated by a '/' character.

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.

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

Final stuff

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

⚠️ **GitHub.com Fallback** ⚠️