Zum Inhalt

Commands & Menüs

Commands

ICommandRegistry ist der einzige Ausführungstrichter für jede vom Benutzer auslösbare Aktion — Menüeinträge, Toolbar-Buttons, Shortcuts und (später) Python-Skripte.

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 liefert ein CommandToken (RAII) zurück. Wird es zerstört, werden das Command, seine Menüeinträge und sein Shortcut entfernt. Halten Sie jedes Token als Member, damit das gesamte Plugin in shutdown() automatisch abgebaut wird.

Plattformspezifische Shortcuts

shortcut ist ein einzelnes portables Tastenkürzel. Qt bildet den Modifikator "Ctrl" bereits auf den nativen der Plattform ab — ⌘ Command auf macOS —, sodass "Ctrl+F" dort ohne Zusatzaufwand ⌘F ist. Mehr brauchen Sie nur, wenn diese automatische Abbildung auf einem vom Betriebssystem reservierten Kürzel landen würde (z. B. "Ctrl+Q" → ⌘Q = Beenden auf macOS).

Setzen Sie für diese Fälle platformShortcuts, mit platform_id als Schlüssel. Der Host liefert den Eintrag der laufenden Plattform oder fällt auf shortcut zurück, wenn eine Plattform fehlt (oder auf einen leeren String abgebildet ist). Kein #ifdef, keine Qt-Typen — nur Daten —, sodass die Bindung Backend-agnostisch bleibt und eine neue Plattform nur ein neuer Schlüssel ist:

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

Reservierte macOS-⌘-Kürzel, gegen die ein einzelnes Ctrl+<key> zu prüfen ist: ⌘Q (Beenden), ⌘W (Schließen), ⌘H (Ausblenden), ⌘M (Im Dock ablegen), ⌘, (Einstellungen), ⌘Leertaste (Spotlight).

Benutzer-Remapping

Die deklarierten shortcut / platformShortcuts sind Standardwerte. Der Benutzer kann jedes Command im Shortcut-Mapper des Kerns (Einstellungen ▸ Shortcuts) neu belegen oder die Belegung aufheben; Konflikte werden hostseitig aufgelöst: Duplikate im selben Scope koexistieren nie — der Verlierer wird für die Sitzung ausgesetzt und gemeldet. Nichts davon erfordert Plugin-Code für gewöhnliche menügebundene Commands: Der Host besitzt deren QAction und bindet sie live neu.

scope — Commands in Plugin-eigenen Fenstern

Commands, deren Tastenkürzel in einem Widget lebt, das dem Plugin gehört (die Toolbar eines Werkzeugfensters statt des Hauptmenüs), deklarieren scope = command_scope::kPluginWindow. Der Host bindet das Kürzel dann niemals selbst; er listet das Command nur im Mapper auf und löst die Wahl des Benutzers auf. Das Plugin wendet die Belegung auf seine eigenen Widgets an:

  • Fragen Sie beim Aufbau der Widgets ctx.commands().effectiveShortcut(id) ab, und
  • abonnieren Sie events::CommandShortcutChanged { commandId, sequence } und wenden Sie die Belegung erneut an, wenn sich eine Ihrer Ids ändert ("" = jetzt unbelegt).

Die Konfliktdomäne ist in v1 global — ein pluginWindow-Kürzel darf nicht mit einem application-Kürzel kollidieren. Funktionierendes Beispiel: die Log-View-Toolbar des Log Analyzers (loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime Ctrl+G).

Menüpfade verwenden / als Trennzeichen; der Host erzeugt Zwischen-Untermenüs bei Bedarf:

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;
};
m_menu = ctx.menus().addItem("File/Export/As HTML...", "myplugin.exportHtml");

MenuToken ist RAII wie CommandToken.

Ankreuzbare Menüeinträge

Ein Menüeintrag, der einen persistenten Ein/Aus-Zustand darstellt, kann ein sichtbares Häkchen tragen. setItemChecked nimmt die TokenId des Eintrags — verwenden Sie den nicht-destruktiven id()-Accessor des Tokens (release() würde die Eigentümerschaft aufgeben):

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

Der erste Aufruf macht den Eintrag ankreuzbar; nachfolgende Aufrufe schalten nur den Zustand um. Ein unbekanntes Token ist ein stilles No-op, sodass ein verzögerter Callback, der nach dem Shutdown feuert, harmlos ist.

Wo das Top-Level-Menü eines Plugins sitzt, ist datengesteuert über das menus-Array in plugin.json — der Host codiert Plugin-Menünamen niemals fest:

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — das Top-Level-Menü; entspricht dem ersten mit / getrennten Segment Ihres menuPath.
  • barPriority — horizontale Position in der Menüleiste (niedriger = weiter links). Kernmenüs sind fest, im Abstand von 100: Datei 100, Bearbeiten 200, Ansicht 300, Language 400, Compare 500, Window 600 (About 1000, immer ganz rechts); Plugins füllen die Lücken (Suchen 250, Encoding 350, Log Analyzer 540, Plugins 570).
  • itemPriority — vertikaler Rang des Blocks dieses Plugins innerhalb von title (niedriger = weiter oben; Standard 1000). Einträge innerhalb des Blocks eines Plugins behalten die Reihenfolge, in der sie im Code hinzugefügt werden.

Note

Da plugin.json als Qt-Plugin-Metadaten eingebettet ist, wirken sich Änderungen daran erst nach einem Rebuild aus.