Aller au contenu

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

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;
};
m_menu = ctx.menus().addItem("File/Export/As HTML...", "myplugin.exportHtml");

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 :

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — le menu de premier niveau ; correspond au premier segment (séparé par /) de votre menuPath.
  • 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 de title (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.