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 usesCtrl+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).
Menús¶
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;
};
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.
title— el menú de primer nivel; coincide con el primer segmento separado por/de tumenuPath. Dale un mnemónico de teclado con&("Lo&g Analyzer") y usa la misma escritura en cadamenuPath: 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 detitle(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.