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 nigdyCtrl+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).
Menu¶
Ś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;
};
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:
title— menu najwyższego poziomu; odpowiada pierwszemu segmentowi TwojejmenuPathrozdzielanej/. Nadaj mu mnemonik klawiaturowy przez&("Lo&g Analyzer") i używaj tej samej pisowni w każdejmenuPath: 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ątrztitle(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.