Preskočiť na obsah

Príkazy a ponuky

Príkazy

ICommandRegistry je jediným lievikom na vykonanie každej akcie vyvolateľnej používateľom — položiek ponuky, tlačidiel panela nástrojov, skratiek a (neskôr) skriptov v Pythone.

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 vracia CommandToken (RAII). Jeho zničením sa odstráni príkaz, jeho položky ponuky aj jeho skratka. Každý token si držte ako člen, aby sa celý zásuvný modul v shutdown() zrušil automaticky.

Požiadavky na editor

requiresEditor a requiresWritableEditor hovoria hostiteľovi, čo príkaz od aktívneho dokumentu potrebuje, aby ho mohol zosivieť namiesto toho, aby používateľ vyvolal niečo, čo potichu neurobí nič.

Príznak Nastavte ho, keď príkaz… Zosivie pri…
requiresEditor číta buffer, výber alebo kurzor, prípadne umiestňuje značky (Záložky, Označiť, Nájsť, Prejsť na definíciu) karte, ktorá nehostí žiadny dokument — karte porovnania alebo zobrazenia logu, kde activeDocument() vracia 0
requiresWritableEditor mení text (Komentár, Previesť veľkosť písmen, Stĺpcový editor, Zväčšiť odsadenie, Riadky…, Formátovať dokument, Premenovať symbol) tom istom a zároveň pri dokumente iba na čítanie: pri súbore sledovanom cez tail -f, pri relácii s --viewer alebo --readonly

Príznak zapisovateľnosti zahŕňa ten druhý, takže deskriptor môže nastaviť iba ten silnejší. Obidva sú predvolene false — „vždy dostupné“, čo je správna odpoveď pre príkazy, ktoré otvárajú dialóg, priečinok alebo panel.

Zosivenie zasiahne položku ponuky, paletu príkazov aj skratku. Dostupnosť môže vždy iba ubrať: položka, ktorú váš zásuvný modul sám vypol cez IMenuRegistry::setItemEnabled, ostane vypnutá, nech je aktívny dokument akýkoľvek.

Deskriptor obaľte na mieste volania:

using MTE::plugin::needsWritableEditor;

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

V Pythone odovzdajte kľúčové argumenty:

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

Skratky pre jednotlivé platformy

shortcut je jediná prenosná kombinácia klávesov. Qt už modifikátor "Ctrl" mapuje na natívny modifikátor platformy — na macOS na ⌘ Command — takže "Ctrl+F" je tam ⌘F bez akejkoľvek práce navyše. Viac potrebujete iba vtedy, keď by toto automatické mapovanie pristálo na kombinácii, ktorú si vyhradzuje operačný systém (napr. "Ctrl+Q" → ⌘Q = Ukončiť na macOS).

Pre tieto prípady nastavte platformShortcuts s kľúčmi z platform_id. Hostiteľ vráti položku zodpovedajúcu bežiacej platforme, prípadne sa vráti k shortcut, keď platforma chýba (alebo je priradená k prázdnemu reťazcu). Žiadne #ifdef, žiadne typy z Qt — iba údaje — takže priradenie ostáva nezávislé od backendu a nová platforma je iba novým kľúčom:

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

Vyhradené kombinácie s ⌘ na macOS, oproti ktorým si treba overiť jednoduché Ctrl+<key>: ⌘Q (Ukončiť), ⌘W (Zavrieť), ⌘H (Skryť), ⌘M (Minimalizovať), ⌘, (Predvoľby), ⌘Space (Spotlight). Pri kombinácii Ctrl+Alt+<key> (⌥⌘ na macOS) sa vyhnite aj ⌥⌘H (Skryť ostatné), ⌥⌘M (Minimalizovať všetko), ⌥⌘D (Dock) a ⌥⌘Esc (Vynútiť ukončenie).

Ďalšie dve pravidlá držia skratky mimo cesty pri písaní:

  • Nikdy nie samotné Alt+<letter> ako prenosná kombinácia. Vo Windows je to priestor mnemoník ponuky a na macOS ⌥+písmeno skladá písmená s diakritikou (⌥U je mŕtvy kláves pre prehlásku, ⌥C napíše ç), takže by ich skratka pohltila — takému príkazu dajte prekrytie {"macos", "Meta+Alt+<key>"} (⌃⌥).
  • Ctrl+Alt+<key> je vo Windows AltGr+<key>. Národné rozloženia tam píšu písmená (poľské: ś ł ą ć ę ń ó ź ż €; nemecké: @ € µ). Hostiteľ ich ponecháva napísateľné — stlačenie pravého Altu, ktoré vytvorí tlačiteľný znak, sa doručí ako text, stlačenie ľavého Ctrl+Alt spustí príkaz — takže sú takéto kombinácie povolené, ale uprednostnite písmená, ktoré na bežných rozloženiach nepíšu nič (B D F G H I J K P R T V W Y), a nikdy nepoužívajte Ctrl+Alt+<digit>.

Premapovanie používateľom

Deklarované shortcut a platformShortcuts sú predvoľbami. Používateľ môže každý príkaz premapovať alebo mu priradenie zrušiť v Mapovaní skratiek v jadre (Predvoľby ▸ Skratky) a konflikty sa riešia na strane hostiteľa: duplicity v tom istom rozsahu nikdy neexistujú súbežne — porazený sa pozastaví na danú reláciu a ohlási sa. Pri bežných príkazoch viazaných na ponuku si to nevyžaduje žiadny kód zásuvného modulu: hostiteľ vlastní ich QAction a premapuje ho naživo.

scope — príkazy v oknách vlastnených zásuvným modulom

Príkazy, ktorých kombinácia klávesov žije vo widgete vlastnenom zásuvným modulom (v paneli nástrojov nástrojového okna, a nie v hlavnej ponuke), deklarujú scope = command_scope::kPluginWindow. Hostiteľ potom kombináciu sám nikdy nepriradí; príkaz iba uvedie v mapovaní a vyhodnotí voľbu používateľa. Priradenie na vlastné widgety uplatní zásuvný modul:

  • pri zostavovaní widgetov sa spýtajte ctx.commands().effectiveShortcut(id) a
  • prihláste sa na events::CommandShortcutChanged { commandId, sequence } a pri zmene niektorého z vašich id priradenie uplatnite znova ("" = teraz bez priradenia).

Doména konfliktov je vo verzii 1 globálna — kombinácia typu pluginWindow odmietne kolidovať s kombináciou na úrovni aplikácie. Fungujúcim príkladom je panel nástrojov Zobrazenia logu v Analyzátore logov (loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime Ctrl+G).

Ponuky

Cesty v ponukách používajú ako oddeľovač /; hostiteľ medziľahlé podponuky vytvára na požiadanie:

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 rovnako ako CommandToken.

Začiarkavacie položky ponuky

Položka ponuky, ktorá predstavuje trvalý stav zapnuté/vypnuté, môže niesť viditeľnú značku začiarknutia. setItemChecked prijíma TokenId položky — použite nedeštruktívny prístupový prvok tokenu id() (release() by sa vzdal vlastníctva):

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

Prvé volanie povýši položku na začiarkavaciu; ďalšie volania už iba prepínajú stav. Neznámy token je tichou prázdnou operáciou, takže oneskorené zavolanie po ukončení nič nepokazí.

Zosivenie položiek ponuky

Príkaz, ktorý potrebuje splnenú podmienku — priečinok na zatvorenie, zatvorenú kartu na obnovenie — by mal svoju položku ponuky zosivieť, kým podmienka nie je splnená, namiesto toho, aby ponúkal položku, ktorá potichu neurobí nič. setItemEnabled prijíma TokenId položky presne tak ako 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 ostáva viditeľná, ale nedá sa vyvolať ani kliknutím, ani jej klávesovou skratkou. Volanie je idempotentné — opakovanie tej istej hodnoty sa očakáva, takže si predchádzajúci stav nikdy nemusíte pamätať — a neznámy token je tichou prázdnou operáciou, ako vyššie. Položky sú na začiatku zapnuté, takže položka, ktorá nie je dostupná pri spustení, potrebuje hneď po addItem jedno volanie.

Umiestnenie v ponuke (plugin.json)

To, kde sa hlavná ponuka zásuvného modulu nachádza, sa riadi údajmi cez pole menus v súbore plugin.json — hostiteľ názvy ponúk zásuvných modulov nikdy nemá napevno v kóde:

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — hlavná ponuka; zodpovedá prvému segmentu vašej menuPath oddelenému znakom /. Dajte jej klávesový mnemonik znakom & ("Lo&g Analyzer") a používajte rovnaký zápis v každej menuPath: vo Windowse otvára ponuku Alt+písmeno a každý titulok na paneli potrebuje vlastné písmeno, v každom jazyku — vyberte voľné spomedzi Súbor, Úpravy, Hľadať, Kódovanie, Porovnať, Zobraziť, Jazyk, Zásuvné moduly, Okno, O aplikácii a Pomocník a každému prekladu dovoľte vybrať si vlastné. Porovnávanie ponúk značku ignoruje.
  • barPriority — vodorovná poloha na paneli ponuky (nižšie = viac vľavo). Ponuky jadra sú pevné, rozostúpené po 100: Súbor 100, Úpravy 200, Zobraziť 300, Jazyk 400, Porovnať 500, Okno 600 (O aplikácii 1000, vždy úplne vpravo); zásuvné moduly vypĺňajú medzery (Hľadať 250, Kódovanie 350, Analyzátor logov 540, Zásuvné moduly 570).
  • itemPriority — zvislé poradie bloku tohto zásuvného modulu v rámci title (nižšie = vyššie; predvolene 1000). Položky v rámci bloku jedného modulu si ponechávajú poradie, v akom sú pridané v kóde.

Note

Keďže je plugin.json vložený ako metaúdaje zásuvného modulu Qt, jeho úpravy sa prejavia až po opätovnom zostavení.