Przejdź do treści

Polecenia i menu

Polecenia

ICommandRegistry to jedyny kanał wykonywania każdej akcji dostępnej dla użytkownika — pozycji menu, przycisków paska narzędzi, skrótów i (w przyszłości) skryptów Pythona.

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 zwraca CommandToken (RAII). Jego zniszczenie usuwa polecenie, jego pozycje menu i jego skrót. Trzymaj każdy token jako pole składowe, aby cała wtyczka sprzątała się automatycznie w shutdown().

Wymagania wobec edytora

requiresEditor / requiresWritableEditor mówią hostowi, czego polecenie potrzebuje od aktywnego dokumentu, dzięki czemu host może je wyszarzyć zamiast pozwolić użytkownikowi wywołać coś, co po cichu nic nie robi.

Flaga Ustaw ją, gdy polecenie… Wyszarza się na…
requiresEditor czyta bufor, zaznaczenie lub kursor albo umieszcza znaczniki (Zakładki, Mark, Znajdź, Przejdź do definicji) karcie, która w ogóle nie zawiera dokumentu — karcie porównania lub widoku logu, gdzie activeDocument() zwraca 0
requiresWritableEditor zmienia tekst (Komentarz, Zmiana wielkości liter, Edytor kolumnowy, Zwiększ wcięcie, Linie…, Formatuj dokument, Zmień nazwę symbolu) powyższym, oraz dokumencie tylko do odczytu: pliku śledzonym przez tail -f, sesji --viewer lub --readonly

Flaga „writable” implikuje tę drugą, więc deskryptor może ustawić tylko silniejszą z nich. Obie domyślnie mają wartość false — „zawsze dostępne”, co jest właściwą odpowiedzią dla poleceń otwierających okno dialogowe, katalog lub panel.

Wyszarzenie obejmuje pozycję menu, paletę poleceń i skrót. Może wyłącznie odbierać dostępność: pozycja, którą Twoja wtyczka sama wyłączyła przez IMenuRegistry::setItemEnabled, pozostaje wyłączona niezależnie od aktywnego dokumentu.

Owiń deskryptor w miejscu wywołania:

using MTE::plugin::needsWritableEditor;

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

W Pythonie przekaż argumenty nazwane:

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

Skróty per platforma

shortcut to pojedynczy przenośny akord klawiszowy. Qt samo mapuje modyfikator "Ctrl" na natywny dla platformy — ⌘ Command na macOS — więc "Ctrl+F" jest tam ⌘F bez dodatkowej pracy. Więcej potrzeba tylko wtedy, gdy to automatyczne mapowanie trafiłoby w akord zarezerwowany przez system (np. "Ctrl+Q" → ⌘Q = Zakończ na macOS).

W takich przypadkach ustaw platformShortcuts, kluczowane przez platform_id. Host zwraca wpis pasujący do bieżącej platformy albo wraca do shortcut, gdy platformy brakuje (lub jest zmapowana na pusty string). Bez #ifdef, bez typów Qt — same dane — więc powiązanie pozostaje niezależne od backendu, a nowa platforma to jeden nowy klucz:

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

Zarezerwowane akordy ⌘ na macOS, z którymi warto zestawić pojedynczy Ctrl+<key>: ⌘Q (Zakończ), ⌘W (Zamknij), ⌘H (Ukryj), ⌘M (Minimalizuj), ⌘, (Preferencje), ⌘Spacja (Spotlight). Dla akordu Ctrl+Alt+<key> (⌥⌘ na macOS) omijaj też ⌥⌘H (Ukryj pozostałe), ⌥⌘M (Minimalizuj wszystkie), ⌥⌘D (Dock) i ⌥⌘Esc (Wymuś zakończenie).

Dwie dodatkowe zasady chronią wpisywanie tekstu:

  • Nigdy gołe Alt+<letter> jako akord przenośny. W Windows to przestrzeń mnemoników menu, a na macOS ⌥+litera składa znaki diakrytyczne (⌥U to martwy klawisz umlautu, ⌥C wpisuje ç), więc skrót by je połykał — takiej komendzie daj override {"macos", "Meta+Alt+<key>"} (⌃⌥).
  • Ctrl+Alt+<key> to w Windows AltGr+<key>. Układy narodowe wpisują tam litery (polski: ś ł ą ć ę ń ó ź ż €; niemiecki: @ € µ). Host utrzymuje ich wpisywanie — prawy Alt dający drukowalny znak trafia jako tekst, lewy Ctrl+Alt uruchamia komendę — więc takie akordy są dozwolone, ale wybieraj litery, które na popularnych układach nic nie wpisują (B D F G H I J K P R T V W Y), i nigdy Ctrl+Alt+<digit>.

Zmiana skrótów przez użytkownika

Zadeklarowane shortcut / platformShortcuts to wartości domyślne. Użytkownik może przemapować lub odpiąć każde polecenie w rdzennym mapperze skrótów (Preferencje ▸ Skróty), a konflikty rozstrzyga host: duplikaty w tym samym zakresie nigdy nie współistnieją — przegrany jest zawieszany na czas sesji i raportowany. Nic z tego nie wymaga kodu wtyczki dla zwykłych poleceń powiązanych z menu: host jest właścicielem ich QAction i przepina skrót na żywo.

scope — polecenia w oknach należących do wtyczki

Polecenia, których akord żyje w widżecie należącym do wtyczki (pasek narzędzi okna narzędziowego, a nie główne menu), deklarują scope = command_scope::kPluginWindow. Host wtedy nigdy sam nie wiąże akordu; jedynie wymienia polecenie w mapperze i rozstrzyga wybór użytkownika. Wtyczka sama stosuje powiązanie do własnych widżetów:

  • odpytaj ctx.commands().effectiveShortcut(id) podczas budowania widżetów, oraz
  • zasubskrybuj events::CommandShortcutChanged { commandId, sequence } i zastosuj ponownie, gdy zmieni się któreś z jej id ("" = teraz niezwiązane).

Domena konfliktów w v1 jest globalna — akord pluginWindow nie może kolidować z akordem application. Działający przykład: pasek narzędzi Log View w Log Analyzerze (loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime Ctrl+G).

Ścieżki menu używają / jako separatora; host tworzy pośrednie podmenu na żądanie:

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 jest RAII tak samo jak CommandToken.

Pozycje menu z zaznaczeniem

Pozycja menu reprezentująca trwały stan włącz/wyłącz może nosić widoczny znacznik zaznaczenia. setItemChecked przyjmuje TokenId pozycji — użyj nieniszczącego akcesora id() tokenu (release() oddałoby własność):

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

Pierwsze wywołanie zamienia pozycję w zaznaczalną; kolejne tylko przełączają stan. Nieznany token to ciche no-op, więc opóźniony callback odpalony po shutdownie jest nieszkodliwy.

Wygaszanie pozycji menu

Polecenie wymagające spełnionego warunku — otwartego folderu do zamknięcia, zamkniętej karty do przywrócenia — powinno wygasić swoją pozycję menu, dopóki ten warunek nie jest spełniony, zamiast oferować wpis, który po cichu nic nie robi. setItemEnabled przyjmuje TokenId pozycji, dokładnie jak 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());

Wygaszona pozycja pozostaje widoczna, ale nie da się jej uruchomić — ani kliknięciem, ani skrótem klawiszowym. Wywołanie jest idempotentne — powtarzanie tej samej wartości jest oczekiwane, więc nigdy nie musisz pamiętać poprzedniego stanu — a nieznany token to ciche no-op, jak wyżej. Pozycje startują jako aktywne, więc pozycja niedostępna zaraz po starcie wymaga jednego wywołania tuż po addItem.

Umiejscowienie menu (plugin.json)

To, gdzie znajdzie się menu najwyższego poziomu wtyczki, jest sterowane danymi przez tablicę menus w plugin.json — host nigdy nie hardkoduje nazw menu wtyczek:

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — menu najwyższego poziomu; odpowiada pierwszemu segmentowi Twojej menuPath rozdzielanej /. Nadaj mu mnemonik klawiaturowy przez & ("Lo&g Analyzer") i używaj tej samej pisowni w każdej menuPath: w Windows Alt+litera otwiera menu, a każdy tytuł na pasku potrzebuje własnej litery, w każdym języku — wybierz wolną spośród Plik, Edycja, Szukaj, Kodowanie, Porównaj, Widok, Język, Wtyczki, Okno, O programie i Pomoc, a każdemu tłumaczeniu pozwól wybrać własną. Dopasowywanie menu ignoruje znacznik.
  • barPriority — pozycja pozioma na pasku menu (niższa = bardziej w lewo). Menu rdzenia są stałe, rozstawione co 100: Plik 100, Edycja 200, Widok 300, Język 400, Porównaj 500, Okno 600 (O programie 1000, zawsze skrajnie po prawej); wtyczki wypełniają luki (Szukaj 250, Kodowanie 350, Analizator logów 540, Wtyczki 570).
  • itemPriority — pionowa ranga bloku tej wtyczki wewnątrz title (niższa = wyżej; domyślnie 1000). Pozycje w obrębie bloku jednej wtyczki zachowują kolejność dodawania w kodzie.

Note

Ponieważ plugin.json jest osadzany jako metadane wtyczki Qt, zmiany w nim zaczynają obowiązywać dopiero po przebudowaniu.