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
    bool requiresEditor         = false;  // needs an active document
    bool requiresWritableEditor = false;  // ...and changes its text
};

// Convenience wrappers, so an existing aggregate literal only has to be
// wrapped rather than rewritten:
CommandDescriptor needsEditor(CommandDescriptor);          // requiresEditor
CommandDescriptor needsWritableEditor(CommandDescriptor);  // + writable

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.

Anforderungen an den Editor

requiresEditor / requiresWritableEditor teilen dem Host mit, was das Command vom aktiven Dokument braucht, damit er es ausgrauen kann, statt den Benutzer etwas aufrufen zu lassen, das stillschweigend nichts tut.

Flag Setzen Sie es, wenn das Command… Es graut aus bei…
requiresEditor den Puffer, die Auswahl oder den Cursor liest oder Marker setzt (Lesezeichen, Mark, Suchen, Gehe zu Definition) einem Tab ganz ohne Dokument — einem Vergleichs- oder Log-Ansicht-Tab, wo activeDocument() 0 liefert
requiresWritableEditor den Text ändert (Kommentar, Groß-/Kleinschreibung, Spalteneditor, Einzug vergrößern, Zeilen…, Dokument formatieren, Symbol umbenennen) dem obigen und einem schreibgeschützten Dokument: einer mit tail -f verfolgten Datei, einer --viewer- oder --readonly-Sitzung

Das Writable-Flag impliziert das andere, ein Deskriptor darf also nur das stärkere setzen. Beide sind standardmäßig false — „immer verfügbar“, die richtige Antwort für Commands, die einen Dialog, einen Ordner oder ein Panel öffnen.

Das Ausgrauen erfasst den Menüeintrag, die Befehlspalette und den Shortcut. Es kann Verfügbarkeit nur wegnehmen: ein Eintrag, den Ihr Plugin selbst über IMenuRegistry::setItemEnabled deaktiviert hat, bleibt deaktiviert, ganz gleich welches Dokument aktiv ist.

Umschließen Sie den Deskriptor an der Aufrufstelle:

using MTE::plugin::needsWritableEditor;

m_toggleComment = ctx.commands().registerCommand(
    needsWritableEditor({ "comment.toggle", "Toggle Comment",
                          "Ctrl+Q", "Comment" }),
    [this] { onToggleComment(); });

In Python übergeben Sie die Schlüsselwortargumente:

@ctx.commands.command(id="wc.upper", title="Upper Case",
                      requires_writable_editor=True)
def _upper():
    ...

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). Bei einem Ctrl+Alt+<key>-Kürzel (⌥⌘ auf macOS) zusätzlich ⌥⌘H (Andere ausblenden), ⌥⌘M (Alle im Dock ablegen), ⌥⌘D (Dock) und ⌥⌘Esc (Sofort beenden) meiden.

Zwei weitere Regeln halten Kürzel vom Tippen fern:

  • Nie ein nacktes Alt+<letter> als portables Kürzel. Unter Windows ist das der Raum der Menü-Mnemonics, und auf macOS setzt ⌥+Buchstabe Sonderzeichen zusammen (⌥U ist die Umlaut-Tottaste, ⌥C tippt ç); das Kürzel würde sie schlucken — einem solchen Befehl ein Override {"macos", "Meta+Alt+<key>"} (⌃⌥) geben.
  • Ctrl+Alt+<key> ist unter Windows AltGr+<key>. Nationale Layouts tippen dort Zeichen (Deutsch: @ € µ; Polnisch: ś ł ą ć ę ń ó ź ż €). Der Host hält sie tippbar — ein rechtes Alt, das ein druckbares Zeichen erzeugt, wird als Text zugestellt, ein linkes Ctrl+Alt führt den Befehl aus — solche Kürzel sind also erlaubt, aber bevorzugt Buchstaben, die auf den gängigen Layouts nichts tippen (B D F G H I J K P R T V W Y), und nie Ctrl+Alt+<digit>.

Benutzer-Remapping

Die deklarierten shortcut / platformShortcuts sind Standardwerte. Der Benutzer kann jedes Command im Shortcut-Mapper des Kerns (Einstellungen ▸ Tastenkürzel) 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;
    virtual void      setItemEnabled(TokenId itemToken, bool enabled) = 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.

Ein Command mit einer Voraussetzung — ein zu schließender Ordner, ein wiederherstellbarer geschlossener Tab — sollte seinen Menüeintrag ausgrauen, solange diese Voraussetzung fehlt, statt einen Eintrag anzubieten, der still nichts tut. setItemEnabled nimmt die TokenId des Eintrags, genau wie setItemChecked:

m_closeItem = ctx.menus().addItem("File/Close Folder", "myplugin.closeFolder");

// Nothing to close yet:
ctx.menus().setItemEnabled(m_closeItem.id(), false);

// ... and from every place that changes the state the command needs:
ctx.menus().setItemEnabled(m_closeItem.id(), !folder.empty());

Ein deaktivierter Eintrag bleibt sichtbar, lässt sich aber nicht auslösen — weder per Klick noch über sein Tastenkürzel. Der Aufruf ist idempotent — denselben Wert zu wiederholen ist erwartet, Sie müssen sich den vorherigen Zustand also nie merken — und ein unbekanntes Token ist ein stilles No-op wie oben. Einträge starten aktiviert; ein Eintrag, der beim Start nicht verfügbar ist, braucht daher einen Aufruf direkt nach addItem.

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. Geben Sie ihm mit & einen Tastatur-Mnemonic ("Lo&g Analyzer") und verwenden Sie dieselbe Schreibweise in jedem menuPath: unter Windows öffnet Alt+Buchstabe das Menü, und jeder Leistentitel braucht einen eigenen Buchstaben, in jeder Sprache — wählen Sie einen, der unter Datei, Bearbeiten, Suchen, Kodierung, Vergleichen, Ansicht, Sprache, Erweiterungen, Fenster, Über und Hilfe frei ist, und lassen Sie jede Übersetzung ihren eigenen wählen. Der Menüabgleich ignoriert die Markierung.
  • barPriority — horizontale Position in der Menüleiste (niedriger = weiter links). Kernmenüs sind fest, im Abstand von 100: Datei 100, Bearbeiten 200, Ansicht 300, Sprache 400, Vergleichen 500, Fenster 600 (Über 1000, immer ganz rechts); Plugins füllen die Lücken (Suchen 250, Kodierung 350, Log-Analysator 540, Erweiterungen 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.