Přeskočit obsah

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žívejte Ctrl+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;
};
m_menu = ctx.menus().addItem("File/Export/As HTML...", "myplugin.exportHtml");

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á:

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — nabídka nejvyšší úrovně; odpovídá prvnímu segmentu vaší menuPath oddě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í.