Vai al contenuto

Comandi e menu

Comandi

ICommandRegistry è l'unico imbuto di esecuzione per ogni azione invocabile dall'utente: voci di menu, pulsanti della barra degli strumenti, scorciatoie e (in seguito) script Python.

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 restituisce un CommandToken (RAII). Distruggerlo rimuove il comando, le sue voci di menu e la sua scorciatoia. Conserva ogni token come membro, così l'intero plugin si smonta automaticamente in shutdown().

Requisiti sull'editor

requiresEditor / requiresWritableEditor dicono all'host che cosa serve al comando dal documento attivo, così che possa disattivarlo invece di lasciare che l'utente invochi qualcosa che silenziosamente non fa nulla.

Flag Impostalo quando il comando… Si disattiva in caso di…
requiresEditor legge il buffer, la selezione o il cursore, oppure colloca marcatori (Segnalibri, Segna, Trova, Vai alla definizione) una scheda che non ospita alcun documento — una scheda di Confronto diff o di Vista di log, dove activeDocument() restituisce 0
requiresWritableEditor modifica il testo (Commento, Converti maiuscole/minuscole, Editor di colonne, Aumenta il rientro, Righe…, Formatta documento, Rinomina simbolo) i casi qui sopra e un documento di sola lettura: un file seguito con tail -f, una sessione --viewer o --readonly

Il flag di scrittura implica l'altro, quindi un descrittore può impostare solo il più forte dei due. Entrambi valgono false per impostazione predefinita — «sempre disponibile», la risposta giusta per i comandi che aprono una finestra, una cartella o un pannello.

La disattivazione raggiunge la voce di menu, la tavolozza dei comandi e la scorciatoia. Può solo togliere disponibilità: una voce che il tuo plugin ha disabilitato da sé tramite IMenuRegistry::setItemEnabled resta disabilitata qualunque sia il documento attivo.

Avvolgi il descrittore nel punto di chiamata:

using MTE::plugin::needsWritableEditor;

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

In Python, passa gli argomenti nominati:

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

Scorciatoie per piattaforma

shortcut è un singolo accordo portabile. Qt mappa già il modificatore "Ctrl" su quello nativo della piattaforma — ⌘ Command su macOS — quindi "Ctrl+F" lì è ⌘F senza alcun lavoro aggiuntivo. Serve di più solo quando quella mappatura automatica finirebbe su un accordo riservato dal sistema operativo (per esempio "Ctrl+Q" → ⌘Q = Esci su macOS).

Per quei casi imposta platformShortcuts, indicizzato con platform_id. L'host restituisce la voce corrispondente alla piattaforma in esecuzione, oppure ripiega su shortcut quando una piattaforma è assente (o mappata su una stringa vuota). Nessun #ifdef, nessun tipo Qt — solo dati — così l'associazione resta indipendente dal backend e una nuova piattaforma è una sola chiave in più:

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

Accordi ⌘ riservati di macOS con cui confrontare un singolo Ctrl+<key>: ⌘Q (Esci), ⌘W (Chiudi), ⌘H (Nascondi), ⌘M (Riduci a icona), ⌘, (Preferenze), ⌘Spazio (Spotlight). Per un accordo Ctrl+Alt+<key> (⌥⌘ su macOS) evita anche ⌥⌘H (Nascondi gli altri), ⌥⌘M (Riduci tutto a icona), ⌥⌘D (Dock) e ⌥⌘Esc (Uscita forzata).

Altre due regole tengono le scorciatoie lontane dalla digitazione:

  • Mai un Alt+<letter> da solo come accordo portabile. Su Windows quello è lo spazio dei tasti mnemonici dei menu, e su macOS ⌥+lettera compone caratteri accentati (⌥U è il tasto morto della dieresi, ⌥C scrive ç), quindi la scorciatoia se li mangerebbe: dai a un comando simile una sostituzione {"macos", "Meta+Alt+<key>"} (⌃⌥).
  • Ctrl+Alt+<key> è AltGr+<key> su Windows. I layout nazionali ci scrivono delle lettere (polacco: ś ł ą ć ę ń ó ź ż €; tedesco: @ € µ). L'host le mantiene digitabili — una pressione dell'Alt destro che produce un carattere stampabile viene consegnata come testo, mentre una pressione di Ctrl+Alt sinistro esegue il comando — quindi tali accordi sono ammessi, ma preferisci le lettere che non scrivono nulla sui layout più diffusi (B D F G H I J K P R T V W Y) e mai Ctrl+Alt+<digit>.

Riassegnazione da parte dell'utente

Le shortcut / platformShortcuts dichiarate sono valori predefiniti. L'utente può riassegnare o annullare ogni comando nella Mappatura delle scorciatoie del nucleo (Preferenze ▸ Scorciatoie), e i conflitti vengono risolti lato host: due duplicati nello stesso ambito non coesistono mai — il perdente viene sospeso per la sessione e segnalato. Nulla di tutto questo richiede codice nel plugin per i normali comandi legati a un menu: l'host possiede la loro QAction e la riassegna al volo.

scope — comandi in finestre possedute dal plugin

I comandi il cui accordo vive in un widget posseduto dal plugin (la barra degli strumenti di una finestra strumenti anziché il menu principale) dichiarano scope = command_scope::kPluginWindow. L'host allora non associa mai l'accordo da sé; si limita a elencare il comando nella mappatura e a risolvere la scelta dell'utente. È il plugin ad applicare l'associazione ai propri widget:

  • interroga ctx.commands().effectiveShortcut(id) mentre costruisci i widget, e
  • sottoscrivi events::CommandShortcutChanged { commandId, sequence } e riapplica quando uno dei suoi id cambia ("" = ora non associato).

Nella v1 il dominio dei conflitti è globale: un accordo pluginWindow si rifiuta di collidere con un accordo dell'applicazione. Esempio funzionante: la barra degli strumenti della Vista di log dell'Analizzatore di log (loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime Ctrl+G).

I percorsi di menu usano / come separatore; l'host crea i sottomenu intermedi su richiesta:

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 è RAII come CommandToken.

Voci di menu spuntabili

Una voce di menu che rappresenta uno stato persistente acceso/spento può portare un segno di spunta visibile. setItemChecked prende il TokenId della voce — usa la funzione di accesso non distruttiva id() del token (release() rinuncerebbe alla proprietà):

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

La prima chiamata promuove la voce a spuntabile; le chiamate successive si limitano a ribaltare lo stato. Un token sconosciuto è un'operazione nulla e silenziosa, quindi una callback ritardata che scatta dopo lo spegnimento è innocua.

Disattivare le voci di menu

Un comando che ha bisogno di una precondizione — una cartella da chiudere, una scheda chiusa da ripristinare — dovrebbe disattivare la propria voce di menu finché quella precondizione manca, invece di offrire una voce che non fa nulla in silenzio. setItemEnabled prende il TokenId della voce, esattamente come 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());

Una voce disabilitata resta visibile ma non può essere attivata, né con un clic né con la sua scorciatoia da tastiera. La chiamata è idempotente — ripetere lo stesso valore è previsto, quindi non devi mai ricordare lo stato precedente — e un token sconosciuto è un'operazione nulla e silenziosa, come sopra. Le voci nascono abilitate, quindi una voce non disponibile all'avvio richiede una chiamata subito dopo addItem.

Collocazione nei menu (plugin.json)

Dove si colloca il menu di primo livello di un plugin è guidato dai dati tramite l'array menus in plugin.json: l'host non scrive mai nel codice i nomi dei menu dei plugin:

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — il menu di primo livello; corrisponde al primo segmento separato da / del tuo menuPath. Dagli un mnemonico da tastiera con & ("Lo&g Analyzer") e usa la stessa grafia in ogni menuPath: su Windows Alt+lettera apre il menu, e ogni titolo della barra ha bisogno di una lettera propria, in ogni lingua — scegline una libera tra File, Modifica, Cerca, Codifica, Confronta, Visualizza, Linguaggio, Estensioni, Finestra, Informazioni e Aiuto, e lascia che ogni traduzione scelga la sua. La corrispondenza dei menu ignora il marcatore.
  • barPriority — posizione orizzontale sulla barra dei menu (più basso = più a sinistra). I menu del nucleo sono fissi, distanziati di 100: File 100, Modifica 200, Visualizza 300, Linguaggio 400, Confronta 500, Finestra 600 (Informazioni 1000, sempre all'estrema destra); i plugin riempiono i vuoti (Cerca 250, Codifica 350, Analizzatore di log 540, Estensioni 570).
  • itemPriority — rango verticale del blocco di questo plugin dentro title (più basso = più in alto; predefinito 1000). Le voci dentro il blocco di un plugin mantengono l'ordine in cui vengono aggiunte nel codice.

Note

Poiché plugin.json è incorporato come metadato del plugin Qt, le modifiche ad esso hanno effetto solo dopo una ricompilazione.