Saltar a contenido

Comandos y menús

Comandos

ICommandRegistry es el embudo único de ejecución de todas las acciones que puede invocar el usuario: elementos de menú, botones de barra de herramientas, atajos y (más adelante) scripts de 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 devuelve un CommandToken (RAII). Destruirlo elimina el comando, sus entradas de menú y su atajo. Guarda todos los testigos como miembros para que el plugin entero se desmonte automáticamente en shutdown().

Requisitos del editor

requiresEditor / requiresWritableEditor le dicen al anfitrión qué necesita el comando del documento activo, de modo que pueda atenuarlo en lugar de dejar que el usuario invoque algo que no hace nada en silencio.

Indicador Actívalo cuando el comando… Se atenúa en…
requiresEditor lee el búfer, la selección o el cursor, o coloca marcadores (Marcadores, Marcar, Buscar, Ir a la definición) una pestaña que no aloja ningún documento en absoluto: una pestaña de comparación de diferencias o de vista de registro, donde activeDocument() devuelve 0
requiresWritableEditor cambia el texto (Comentario, Cambiar mayúsculas y minúsculas, Editor de columnas, Aumentar sangría, Líneas…, Formatear el documento, Renombrar símbolo) lo anterior y además un documento de solo lectura: un archivo seguido con tail -f, una sesión --viewer o --readonly

El indicador de escritura implica al otro, así que un descriptor puede poner solo el más fuerte. Ambos son false de forma predeterminada —«siempre disponible»—, que es la respuesta correcta para los comandos que abren un diálogo, una carpeta o un panel.

La atenuación llega al elemento de menú, a la paleta de comandos y al atajo. Solo puede quitar disponibilidad: un elemento que tu propio plugin haya desactivado mediante IMenuRegistry::setItemEnabled sigue desactivado sea cual sea el documento activo.

Envuelve el descriptor en el punto de llamada:

using MTE::plugin::needsWritableEditor;

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

En Python, pasa los argumentos con nombre:

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

Atajos por plataforma

shortcut es una única combinación portátil. Qt ya asigna el modificador "Ctrl" al nativo de la plataforma —⌘ Command en macOS—, así que "Ctrl+F" es ⌘F allí sin trabajo adicional. Solo necesitas más cuando esa asignación automática caería sobre una combinación que el sistema operativo reserva (p. ej. "Ctrl+Q" → ⌘Q = Salir en macOS).

Para esos casos define platformShortcuts, con las claves de platform_id. El anfitrión devuelve la entrada que coincide con la plataforma en ejecución, o recurre a shortcut cuando falta una plataforma (o está asignada a una cadena vacía). Sin #ifdef, sin tipos de Qt: solo datos, de modo que la asignación sigue siendo independiente del backend y una plataforma nueva es una clave nueva:

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

Combinaciones ⌘ reservadas de macOS con las que conviene cotejar un simple Ctrl+<key>: ⌘Q (Salir), ⌘W (Cerrar), ⌘H (Ocultar), ⌘M (Minimizar), ⌘, (Preferencias), ⌘Espacio (Spotlight). Para una combinación Ctrl+Alt+<key> (⌥⌘ en macOS) evita además ⌥⌘H (Ocultar los demás), ⌥⌘M (Minimizar todo), ⌥⌘D (Dock) y ⌥⌘Esc (Forzar salida).

Dos reglas más mantienen los atajos apartados de la escritura:

  • Nunca un Alt+<letter> a secas como combinación portátil. En Windows ese es el espacio de los mnemónicos de menú, y en macOS ⌥+letra compone caracteres acentuados (⌥U es la tecla muerta de la diéresis, ⌥C escribe ç), así que el atajo se los tragaría; dale a un comando así una anulación {"macos", "Meta+Alt+<key>"} (⌃⌥).
  • Ctrl+Alt+<key> es AltGr+<key> en Windows. Las distribuciones nacionales escriben letras ahí (polaco: ś ł ą ć ę ń ó ź ż €; alemán: @ € µ). El anfitrión hace que sigan siendo escribibles —una pulsación de Alt derecho que produce un carácter imprimible se entrega como texto, y una pulsación de Ctrl+Alt izquierdos ejecuta el comando—, así que esas combinaciones están permitidas; pero prefiere letras que no escriban nada en las distribuciones habituales (B D F G H I J K P R T V W Y) y nunca uses Ctrl+Alt+<digit>.

Reasignación por parte del usuario

El shortcut y los platformShortcuts declarados son valores predeterminados. El usuario puede reasignar o desasignar todos los comandos en la Asignación de atajos del núcleo (Preferencias ▸ Atajos de teclado), y los conflictos se resuelven del lado del anfitrión: dos comandos del mismo ámbito nunca coexisten con la misma combinación —el perdedor queda suspendido durante la sesión y se informa de ello—. Nada de esto requiere código del plugin para los comandos normales ligados a menús: el anfitrión es el dueño de su QAction y lo reasigna en vivo.

scope — comandos en ventanas propiedad del plugin

Los comandos cuya combinación vive en un widget propiedad del plugin (la barra de herramientas de una ventana de utilidad, en vez del menú principal) declaran scope = command_scope::kPluginWindow. El anfitrión no asigna entonces la combinación por sí mismo; solo lista el comando en la herramienta de asignación y resuelve la elección del usuario. El plugin aplica la asignación a sus propios widgets:

  • consulta ctx.commands().effectiveShortcut(id) al construir los widgets, y
  • suscríbete a events::CommandShortcutChanged { commandId, sequence } y vuelve a aplicarla cuando cambie uno de sus identificadores ("" = ahora sin asignar).

El dominio de conflictos es global en la v1: una combinación de ventana de plugin se niega a chocar con una combinación de aplicación. Ejemplo práctico: la barra de herramientas de la Vista de registro del Analizador de registros (loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime Ctrl+G).

Las rutas de menú usan / como separador; el anfitrión crea los submenús intermedios bajo demanda:

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 es RAII, igual que CommandToken.

Elementos de menú marcables

Un elemento de menú que representa un estado persistente de encendido/apagado puede llevar una marca de verificación visible. setItemChecked toma el TokenId del elemento; usa el accesor no destructivo id() del testigo (release() cedería la propiedad):

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 primera llamada promueve el elemento a marcable; las llamadas posteriores solo cambian el estado. Un testigo desconocido no hace nada en silencio, así que una retrollamada retrasada que se dispare después del cierre es inofensiva.

Atenuar elementos de menú

Un comando que necesita una condición previa —una carpeta que cerrar, una pestaña cerrada que restaurar— debería atenuar su elemento de menú mientras esa condición falte, en lugar de ofrecer una entrada que no hace nada en silencio. setItemEnabled toma el TokenId del elemento, exactamente igual que 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 elemento desactivado sigue visible pero no se puede activar, ni con un clic ni con su atajo de teclado. La llamada es idempotente —repetir el mismo valor es esperable, así que nunca tienes que recordar el estado anterior— y un testigo desconocido no hace nada en silencio, como arriba. Los elementos empiezan activados, así que un elemento que no está disponible al arrancar necesita una llamada justo después de addItem.

Colocación de los menús (plugin.json)

Dónde se sitúa el menú de primer nivel de un plugin viene dado por datos, a través de la matriz menus de plugin.json: el anfitrión nunca codifica a mano los nombres de menú de los plugins.

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — el menú de primer nivel; coincide con el primer segmento separado por / de tu menuPath. Dale un mnemónico de teclado con & ("Lo&g Analyzer") y usa la misma escritura en cada menuPath: en Windows, Alt+letra abre el menú, y cada título de la barra necesita una letra propia, en cada idioma; elige una que esté libre entre Archivo, Editar, Buscar, Codificación, Comparar, Ver, Lenguaje, Complementos, Ventana, Acerca de y Ayuda, y deja que cada traducción escoja la suya. La coincidencia de menús ignora el marcador.
  • barPriority — posición horizontal en la barra de menús (menor = más a la izquierda). Los menús del núcleo son fijos y están separados de 100 en 100: Archivo 100, Editar 200, Ver 300, Lenguaje 400, Comparar 500, Ventana 600 (Acerca de 1000, siempre a la derecha del todo); los plugins llenan los huecos (Buscar 250, Codificación 350, Analizador de registros 540, Complementos 570).
  • itemPriority — rango vertical del bloque de este plugin dentro de title (menor = más arriba; 1000 de forma predeterminada). Los elementos dentro del bloque de un mismo plugin conservan el orden en que se añaden en el código.

Note

Como plugin.json va incrustado como metadatos de plugin de Qt, las ediciones que le hagas solo surten efecto tras recompilar.