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 nieCtrl+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üs¶
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;
};
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.
Menüeinträge ausgrauen¶
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.
Menüplatzierung (plugin.json)¶
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:
title— das Top-Level-Menü; entspricht dem ersten mit/getrennten Segment IhresmenuPath. Geben Sie ihm mit&einen Tastatur-Mnemonic ("Lo&g Analyzer") und verwenden Sie dieselbe Schreibweise in jedemmenuPath: 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 vontitle(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.