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
};
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().
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).
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;
};
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.
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.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, Log Analyzer 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.