Příkazy a nabídky¶
Příkazy¶
ICommandRegistry je jediným trychtýřem pro provádění každé akce vyvolatelné uživatelem —
položek nabídek, tlačítek lišty, zkratek a (později) skriptů v Pythonu.
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 vrací CommandToken (RAII). Jeho zničením se odebere příkaz, jeho položky
nabídek i jeho zkratka. Každý token držte jako člen, aby se celý modul ve shutdown() strhl
automaticky.
Požadavky na editor¶
requiresEditor / requiresWritableEditor říkají hostiteli, co příkaz od aktivního dokumentu
potřebuje, aby jej mohl zašednout, místo aby uživateli dovolil vyvolat něco, co mlčky nic
neudělá.
| Příznak | Nastavte jej, když příkaz… | Zašedne u… |
|---|---|---|
requiresEditor |
čte buffer, výběr či kurzor, nebo umísťuje značky (Záložky, Označit, Najít, Přejít na definici) | karty, která nehostí vůbec žádný dokument — karty Porovnání nebo Zobrazení protokolu, kde activeDocument() vrací 0 |
requiresWritableEditor |
mění text (Komentář, Převod velikosti písmen, Sloupcový editor, Zvětšit odsazení, Řádky…, Formátovat dokument, Přejmenovat symbol) | výše uvedeného a dokumentu jen ke čtení: souboru sledovaného pomocí tail -f, relace --viewer nebo --readonly |
Příznak zapisovatelnosti ten druhý implikuje, takže deskriptor smí nastavit jen ten silnější. Oba
jsou ve výchozím stavu false — »vždy dostupné«, což je správná odpověď u příkazů, které otevírají
okno, složku nebo panel.
Zašednutí dosáhne na položku nabídky, paletu příkazů i zkratku. Může dostupnost vždy jen odebrat:
položka, kterou váš modul sám vypnul přes IMenuRegistry::setItemEnabled, zůstane vypnutá bez
ohledu na to, jaký je aktivní dokument.
Deskriptor obalte v místě volání:
using MTE::plugin::needsWritableEditor;
m_toggleComment = ctx.commands().registerCommand(
needsWritableEditor({ "comment.toggle", "Toggle Comment",
"Ctrl+Q", "Comment" }),
[this] { onToggleComment(); });
V Pythonu předejte pojmenované argumenty:
@ctx.commands.command(id="wc.upper", title="Upper Case",
requires_writable_editor=True)
def _upper():
...
Zkratky podle platformy¶
shortcut je jediná přenositelná kombinace. Qt už mapuje modifikátor "Ctrl" na ten nativní pro
danou platformu — na macOS ⌘ Command — takže "Ctrl+F" je tam ⌘F bez další práce. Víc
potřebujete, jen když by toto automatické mapování dosedlo na kombinaci, kterou si vyhrazuje
operační systém (například "Ctrl+Q" → ⌘Q = Ukončit na macOS).
Pro tyto případy nastavte platformShortcuts s klíči podle platform_id. Hostitel vrátí položku
odpovídající běžící platformě, nebo se vrátí k shortcut, když platforma chybí (či je mapována na
prázdný řetězec). Žádné #ifdef, žádné typy Qt — jen data — takže vazba zůstává nezávislá na
backendu a nová platforma znamená jeden nový klíč:
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";
Vyhrazené kombinace s ⌘ na macOS, proti kterým je třeba jediné Ctrl+<key> ověřit: ⌘Q
(Ukončit), ⌘W (Zavřít), ⌘H (Skrýt), ⌘M (Minimalizovat), ⌘, (Předvolby), ⌘Mezerník (Spotlight). U
kombinace Ctrl+Alt+<key> (na macOS ⌥⌘) se navíc vyhněte ⌥⌘H (Skrýt ostatní), ⌥⌘M
(Minimalizovat vše), ⌥⌘D (Dock) a ⌥⌘Esc (Vynutit ukončení).
Dvě další pravidla drží zkratky stranou od psaní:
- Nikdy holé
Alt+<letter>jako přenositelnou kombinaci. Ve Windows je to prostor nabídkových mnemotechnických kláves a na macOS ⌥+písmeno skládá znaky s diakritikou (⌥U je mrtvá klávesa pro přehlásku, ⌥C píše ç), takže by je zkratka spolkla — takovému příkazu dejte přebití{"macos", "Meta+Alt+<key>"}(⌃⌥). Ctrl+Alt+<key>je ve Windows AltGr+<key>. Národní rozložení tam píšou písmena (polsky: ś ł ą ć ę ń ó ź ż €; německy: @ € µ). Hostitel je nechává zapsatelné — stisk pravého Altu, který vytvoří tisknutelný znak, se doručí jako text, stisk levého Ctrl+Alt spustí příkaz — takže takové kombinace povolené jsou, ale dávejte přednost písmenům, která na běžných rozloženích nic nepíšou (B D F G H I J K P R T V W Y), a nikdy nepoužívejteCtrl+Alt+<digit>.
Přemapování uživatelem¶
Deklarované shortcut / platformShortcuts jsou výchozí hodnoty. Uživatel může každý příkaz
přiřadit znovu nebo jeho vazbu zrušit v Mapování klávesových zkratek v jádře (Předvolby ▸
Klávesové zkratky) a konflikty se řeší na straně hostitele: duplicity v témže rozsahu nikdy
nekoexistují — poražený je pro danou relaci pozastaven a ohlášen. U běžných příkazů vázaných na
nabídku k tomu není potřeba žádný kód modulu: jejich QAction vlastní hostitel a přiřazuje ji
znovu za běhu.
scope — příkazy v oknech vlastněných modulem¶
Příkazy, jejichž kombinace bydlí ve widgetu, který vlastní modul (lišta nástrojového okna místo
hlavní nabídky), deklarují scope = command_scope::kPluginWindow. Hostitel pak kombinaci sám
nikdy neváže; pouze příkaz vypíše v mapování a vyhodnotí volbu uživatele. Modul vazbu uplatní na
své vlastní widgety:
- při stavbě widgetů se zeptejte
ctx.commands().effectiveShortcut(id)a - odebírejte
events::CommandShortcutChanged { commandId, sequence }a při změně některého ze svých id ji uplatněte znovu (""= nyní bez vazby).
Doména konfliktů je ve verzi 1 globální — kombinace pluginWindow odmítne kolidovat s kombinací
aplikace. Funkční příklad: lišta Zobrazení protokolu v Analyzátoru protokolů
(loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime Ctrl+G).
Nabídky¶
Cesty v nabídkách používají jako oddělovač /; hostitel mezilehlé podnabídky vytváří na
vyžádání:
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 je RAII stejně jako CommandToken.
Zaškrtávatelné položky nabídek¶
Položka nabídky představující trvalý stav zapnuto/vypnuto může nést viditelné zaškrtnutí.
setItemChecked bere TokenId položky — použijte nedestruktivní přístupovou metodu id() tokenu
(release() by se vzdal vlastnictví):
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);
První volání povýší položku na zaškrtávatelnou; další volání už jen stav přehazují. Neznámý token je tichá prázdná operace, takže je zpožděné zavolání po ukončení neškodné.
Zašednutí položek nabídek¶
Příkaz, který potřebuje splněnou podmínku — složku k zavření, zavřenou kartu k obnovení — by měl
svou položku nabídky zašednout, dokud podmínka chybí, místo aby nabízel položku, která tiše nic
neudělá. setItemEnabled bere TokenId položky, přesně jako 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());
Vypnutá položka zůstává viditelná, ale nelze ji spustit, ani kliknutím, ani klávesovou zkratkou.
Volání je idempotentní — opakování téže hodnoty se očekává, takže si nikdy nemusíte pamatovat
předchozí stav — a neznámý token je tichá prázdná operace, jako výše. Položky začínají zapnuté,
takže položka, která při spuštění není dostupná, potřebuje jedno volání hned po addItem.
Umístění v nabídce (plugin.json)¶
Kde sedí nabídka modulu na nejvyšší úrovni, určují data prostřednictvím pole menus v
plugin.json — hostitel názvy nabídek modulů nikdy nezadrátovává:
title— nabídka nejvyšší úrovně; odpovídá prvnímu segmentu vašímenuPathoddělenému/. Dejte jí klávesový mnemonik znakem&("Lo&g Analyzer") a používejte stejný zápis v každémenuPath: ve Windows otevírá nabídku Alt+písmeno a každý titulek na liště potřebuje vlastní písmeno, v každém jazyce — vyberte volné mezi Soubor, Úpravy, Hledat, Kódování, Porovnat, Zobrazit, Jazyk, Zásuvné moduly, Okno, O aplikaci a Nápověda a každému překladu dovolte vybrat si vlastní. Porovnávání nabídek značku ignoruje.barPriority— vodorovná pozice v nabídkové liště (nižší = dál vlevo). Nabídky jádra jsou pevné, rozestupy po 100: Soubor 100, Úpravy 200, Zobrazit 300, Jazyk 400, Porovnat 500, Okno 600 (O aplikaci 1000, vždy zcela vpravo); moduly vyplňují mezery (Hledat 250, Kódování 350, Analyzátor protokolů 540, Zásuvné moduly 570).itemPriority— svislé pořadí bloku tohoto modulu uvnitřtitle(nižší = výš; výchozí 1000). Položky uvnitř bloku jednoho modulu si drží pořadí, v jakém jsou přidány v kódu.
Note
Protože je plugin.json vložen jako metadata modulu Qt, projeví se jeho úpravy až po novém
sestavení.