Commandes et menus¶
Commandes¶
ICommandRegistry est l'entonnoir d'exécution unique de toute action
invocable par l'utilisateur — éléments de menu, boutons de barre d'outils,
raccourcis et (plus tard) scripts 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 renvoie un CommandToken (RAII). Sa destruction supprime la
commande, ses entrées de menu et son raccourci. Conservez chaque jeton comme
membre afin que tout le plugin se démonte automatiquement dans shutdown().
Exigences vis-à-vis de l'éditeur¶
requiresEditor / requiresWritableEditor indiquent à l'hôte ce dont la
commande a besoin du document actif, afin qu'il puisse la griser au lieu de
laisser l'utilisateur invoquer quelque chose qui ne fait silencieusement rien.
| Indicateur | À définir quand la commande… | Elle est grisée sur… |
|---|---|---|
requiresEditor |
lit le tampon, la sélection ou le curseur, ou pose des marqueurs (Signets, Mark, Rechercher, Aller à la définition) | un onglet sans aucun document — un onglet de comparaison ou de vue de journal, où activeDocument() renvoie 0 |
requiresWritableEditor |
modifie le texte (Commentaire, Casse, Éditeur de colonnes, Augmenter l'indentation, Lignes…, Formater le document, Renommer le symbole) | ce qui précède et un document en lecture seule : un fichier suivi par tail -f, une session --viewer ou --readonly |
L'indicateur « writable » implique l'autre : un descripteur peut donc ne définir
que le plus fort. Les deux valent false par défaut — « toujours disponible »,
la bonne réponse pour les commandes qui ouvrent une boîte de dialogue, un
dossier ou un panneau.
Le grisage touche l'entrée de menu, la palette de commandes et le raccourci. Il
ne peut que retirer de la disponibilité : une entrée que votre extension a
elle-même désactivée via IMenuRegistry::setItemEnabled reste désactivée quel
que soit le document actif.
Enveloppez le descripteur à l'appel :
using MTE::plugin::needsWritableEditor;
m_toggleComment = ctx.commands().registerCommand(
needsWritableEditor({ "comment.toggle", "Toggle Comment",
"Ctrl+Q", "Comment" }),
[this] { onToggleComment(); });
En Python, passez les arguments nommés :
@ctx.commands.command(id="wc.upper", title="Upper Case",
requires_writable_editor=True)
def _upper():
...
Raccourcis par plateforme¶
shortcut est une combinaison portable unique. Qt fait déjà correspondre le
modificateur "Ctrl" au modificateur natif de la plateforme — ⌘ Command
sur macOS — donc "Ctrl+F" y devient ⌘F sans effort supplémentaire. Vous
n'avez besoin de plus que lorsque ce mappage automatique tomberait sur une
combinaison réservée par l'OS (p. ex. "Ctrl+Q" → ⌘Q = Quitter sur macOS).
Dans ces cas-là, renseignez platformShortcuts, indexé par platform_id.
L'hôte renvoie l'entrée correspondant à la plateforme en cours, ou se rabat
sur shortcut quand une plateforme est absente (ou associée à une chaîne
vide). Pas de #ifdef, pas de types Qt — uniquement des données — de sorte
que la liaison reste indépendante du backend, et une nouvelle plateforme n'est
qu'une nouvelle clé :
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";
Combinaisons ⌘ réservées sur macOS à vérifier face à un simple Ctrl+<key> :
⌘Q (Quitter), ⌘W (Fermer), ⌘H (Masquer), ⌘M (Réduire), ⌘, (Préférences),
⌘Espace (Spotlight). Pour un raccourci Ctrl+Alt+<key> (⌥⌘ sur macOS),
évitez aussi ⌥⌘H (Masquer les autres), ⌥⌘M (Tout réduire), ⌥⌘D (Dock) et
⌥⌘Esc (Forcer à quitter).
Deux règles supplémentaires protègent la saisie de texte :
- Jamais un simple
Alt+<letter>comme raccourci portable. Sous Windows, c'est l'espace des mnémoniques de menu, et sur macOS ⌥+lettre compose des caractères accentués (⌥U est la touche morte du tréma, ⌥C saisit ç) ; le raccourci les avalerait — donnez à une telle commande un remplacement{"macos", "Meta+Alt+<key>"}(⌃⌥). Ctrl+Alt+<key>est AltGr+<key>sous Windows. Les dispositions nationales y saisissent des caractères (français : € @ ; polonais : ś ł ą ć ę ń ó ź ż €). L'hôte les garde saisissables — un Alt droit produisant un caractère imprimable est transmis comme texte, un Ctrl+Alt gauche exécute la commande — de tels raccourcis sont donc permis, mais préférez des lettres qui ne saisissent rien sur les dispositions courantes (B D F G H I J K P R T V W Y), et jamaisCtrl+Alt+<digit>.
Remappage par l'utilisateur¶
Les shortcut / platformShortcuts déclarés sont des valeurs par défaut.
L'utilisateur peut réaffecter ou délier chaque commande dans le mappeur de
raccourcis du cœur (Préférences ▸ Raccourcis), et les conflits sont résolus
côté hôte : des doublons de même portée ne coexistent jamais — le perdant est
suspendu pour la session et signalé. Rien de tout cela ne demande de code de
plugin pour les commandes ordinaires liées à un menu : l'hôte possède leur
QAction et la relie à chaud.
scope — commandes dans des fenêtres possédées par le plugin¶
Les commandes dont la combinaison vit dans un widget que le plugin possède
(la barre d'outils d'une fenêtre outil plutôt que le menu principal) déclarent
scope = command_scope::kPluginWindow. L'hôte ne lie alors jamais la
combinaison lui-même ; il se contente de lister la commande dans le mappeur et
de résoudre le choix de l'utilisateur. Le plugin applique la liaison à ses
propres widgets :
- interrogez
ctx.commands().effectiveShortcut(id)lors de la construction des widgets, et - abonnez-vous à
events::CommandShortcutChanged { commandId, sequence }et réappliquez quand l'un de vos ids change (""= désormais non lié).
Le domaine de conflit est global en v1 — une combinaison pluginWindow refuse
d'entrer en collision avec une combinaison application. Exemple fonctionnel :
la barre d'outils Log View du Log Analyzer
(loganalyzer.logView.nextError F8 / .prevError Shift+F8 /
.goToTime Ctrl+G).
Menus¶
Les chemins de menu utilisent / comme séparateur ; l'hôte crée les
sous-menus intermédiaires à la demande :
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 est RAII, comme CommandToken.
Éléments de menu cochables¶
Un élément de menu représentant un état actif/inactif persistant peut porter
une coche visible. setItemChecked prend le TokenId de l'élément — utilisez
l'accesseur non destructif id() du jeton (release() abandonnerait la
propriété) :
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);
Le premier appel promeut l'élément en élément cochable ; les appels suivants ne font que basculer l'état. Un jeton inconnu est un no-op silencieux, donc un callback retardé qui se déclenche après l'arrêt est sans danger.
Griser les éléments de menu¶
Une commande soumise à une condition préalable — un dossier à fermer, un
onglet fermé à rouvrir — devrait griser son élément de menu tant que cette
condition n'est pas remplie, plutôt que d'offrir une entrée qui ne fait
silencieusement rien. setItemEnabled prend le TokenId de l'élément,
exactement comme 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());
Un élément désactivé reste visible mais ne peut pas être déclenché, ni par un
clic ni par son raccourci clavier. L'appel est idempotent — répéter la même
valeur est attendu, vous n'avez donc jamais à mémoriser l'état précédent — et
un jeton inconnu est un no-op silencieux, comme ci-dessus. Les éléments
démarrent activés : un élément indisponible au démarrage demande donc un appel
juste après addItem.
Placement des menus (plugin.json)¶
L'emplacement du menu de premier niveau d'un plugin est piloté par les
données via le tableau menus de plugin.json — l'hôte ne code jamais en
dur les noms de menus des plugins :
title— le menu de premier niveau ; correspond au premier segment (séparé par/) de votremenuPath. Donnez-lui un mnémonique clavier avec&("Lo&g Analyzer") et employez la même orthographe dans chaquemenuPath: sous Windows, Alt+lettre ouvre le menu, et chaque titre de la barre a besoin d'une lettre qui lui est propre, dans chaque langue — choisissez-en une qui soit libre parmi Fichier, Édition, Recherche, Encodage, Comparer, Affichage, Langage, Extensions, Fenêtre, À propos et Aide, et laissez chaque traduction choisir la sienne. La correspondance des menus ignore le marqueur.barPriority— position horizontale dans la barre de menus (plus bas = plus à gauche). Les menus du cœur sont fixes, espacés de 100 : Fichier 100, Édition 200, Affichage 300, Langage 400, Comparer 500, Fenêtre 600 (À propos 1000, toujours tout à droite) ; les plugins comblent les intervalles (Recherche 250, Encodage 350, Analyseur de journaux 540, Extensions 570).itemPriority— rang vertical du bloc de ce plugin au sein detitle(plus bas = plus haut ; 1000 par défaut). Les éléments à l'intérieur du bloc d'un même plugin gardent l'ordre dans lequel ils sont ajoutés dans le code.
Note
Comme plugin.json est embarqué en tant que métadonnées de plugin Qt, ses
modifications ne prennent effet qu'après une recompilation.