Commands & menus¶
Commands¶
ICommandRegistry is the single execution funnel for every user-invocable action
— menu items, toolbar buttons, shortcuts, and (later) Python scripts.
namespace platform_id { // keys for platformShortcuts
inline constexpr std::string_view kWindows = "windows";
inline constexpr std::string_view kMacOS = "macos";
inline constexpr std::string_view kLinux = "linux";
}
namespace command_scope { // values for CommandDescriptor::scope
inline constexpr std::string_view kApplication = "application";
inline constexpr std::string_view kPluginWindow = "pluginWindow";
}
struct CommandDescriptor {
std::string id; // "helloworld.sayHi"
std::string title; // "Say Hi"
std::string shortcut; // portable default: "Ctrl+Alt+H"
std::string category; // "Hello"
std::string scope; // "" / "application" | "pluginWindow"
std::map<std::string, std::string> platformShortcuts; // optional per-platform
};
using CommandHandler = std::function<void()>;
class ICommandRegistry {
public:
virtual CommandToken registerCommand(CommandDescriptor, CommandHandler) = 0;
virtual void invoke(std::string_view commandId) = 0;
virtual bool exists(std::string_view commandId) const = 0;
// The user-resolved sequence in effect right now ("" = unbound),
// NOT the declared default. See "User remapping" below.
virtual std::string effectiveShortcut(std::string_view commandId) const = 0;
};
registerCommand returns a CommandToken (RAII). Destroying it removes the
command, its menu entries, and its shortcut. Hold every token as a member so the
whole plugin tears down automatically in shutdown().
Per-platform shortcuts¶
shortcut is a single portable chord. Qt already maps the "Ctrl" modifier to
the platform's native one — ⌘ Command on macOS — so "Ctrl+F" is ⌘F there
with no extra work. You only need more when that automatic mapping would land on
a chord the OS reserves (e.g. "Ctrl+Q" → ⌘Q = Quit on macOS).
For those cases set platformShortcuts, keyed by platform_id. The host returns
the entry matching the running platform, or falls back to shortcut when a
platform is absent (or mapped to an empty string). No #ifdef, no Qt types — just
data — so the binding stays backend-agnostic, and a new platform is one new key:
MTE::plugin::CommandDescriptor desc;
desc.id = "comment.toggle";
desc.title = "Toggle Single Line Comment";
desc.shortcut = "Ctrl+Q"; // Windows / Linux → Ctrl+Q
desc.platformShortcuts = { // macOS → ⌃Q (the portable
{ std::string(MTE::plugin::platform_id::kMacOS), "Meta+Q" } }; // "Meta" = ⌃
desc.category = "Comment";
Reserved macOS ⌘ chords to check a single Ctrl+<key> against: ⌘Q (Quit), ⌘W
(Close), ⌘H (Hide), ⌘M (Minimize), ⌘, (Preferences), ⌘Space (Spotlight).
User remapping¶
The declared shortcut / platformShortcuts are defaults. The user can
rebind or unbind every command in the core Shortcut Mapper (Preferences ▸
Shortcuts), and conflicts are resolved host-side: same-scope duplicates never
coexist — the loser is suspended for the session and reported. None of this
needs plugin code for ordinary menu-bound commands: the host owns their
QAction and rebinds it live.
scope — commands in plugin-owned windows¶
Commands whose chord lives in a widget the plugin owns (a tool window's
toolbar rather than the main menu) declare
scope = command_scope::kPluginWindow. The host then never binds the chord
itself; it only lists the command in the mapper and resolves the user's
choice. The plugin applies the binding to its own widgets:
- query
ctx.commands().effectiveShortcut(id)when building the widgets, and - subscribe to
events::CommandShortcutChanged { commandId, sequence }and re-apply when one of its ids changes (""= now unbound).
The conflict domain is global in v1 — a pluginWindow chord refuses to collide
with an application chord. Working example: the Log Analyzer's Log View
toolbar (loganalyzer.logView.nextError F8 / .prevError Shift+F8 /
.goToTime Ctrl+G).
Menus¶
Menu paths use / as the separator; the host creates intermediate submenus on
demand:
class IMenuRegistry {
public:
virtual MenuToken addItem(std::string menuPath, std::string commandId) = 0;
virtual MenuToken addSeparator(std::string menuPath) = 0;
virtual void setItemChecked(TokenId itemToken, bool checked) = 0;
};
MenuToken is RAII like CommandToken.
Checkable menu items¶
A menu item that represents a persistent on/off state can carry a visible
check mark. setItemChecked takes the item's TokenId — use the token's
non-destructive id() accessor (release() would give up ownership):
m_toggle = ctx.menus().addItem("View/Auto-open preview", "myplugin.toggle");
// Seed the mark from the persisted setting so the menu matches reality
// before the user first opens it:
ctx.menus().setItemChecked(m_toggle.id(),
ctx.settings().getBool("autoOpen", true));
// Inside the command handler, after flipping the setting:
ctx.menus().setItemChecked(m_toggle.id(), nowEnabled);
The first call promotes the item to checkable; subsequent calls just flip the state. An unknown token is a silent no-op, so a delayed callback that fires after shutdown is harmless.
Menu placement (plugin.json)¶
Where a plugin's top-level menu sits is data-driven via the menus array in
plugin.json — the host never hard-codes plugin menu names:
title— the top-level menu; matches the first/-separated segment of yourmenuPath.barPriority— horizontal position on the menu bar (lower = further left). Core menus are fixed, spaced 100 apart: File 100, Edit 200, View 300, Language 400, Compare 500, Window 600 (About 1000, always far right); plugins fill the gaps (Search 250, Encoding 350, Log Analyzer 540, Plugins 570).itemPriority— vertical rank of this plugin's block withintitle(lower = higher up; default 1000). Items inside one plugin's block keep the order they are added in code.
Note
Because plugin.json is embedded as Qt plugin metadata, edits to it take
effect only after a rebuild.