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 maiCtrl+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).
Menu¶
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;
};
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:
title— il menu di primo livello; corrisponde al primo segmento separato da/del tuomenuPath. Dagli un mnemonico da tastiera con&("Lo&g Analyzer") e usa la stessa grafia in ognimenuPath: 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 dentrotitle(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.