Comandos e menus¶
Comandos¶
ICommandRegistry é o funil único de execução de toda ação que o usuário pode
invocar — itens de menu, botões de barra de ferramentas, atalhos e (mais adiante)
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 devolve um CommandToken (RAII). Destruí-lo remove o comando, as
entradas de menu dele e o atalho. Guarde todos os tokens como membros, para que o
plugin inteiro se desmonte automaticamente no shutdown().
Requisitos do editor¶
requiresEditor / requiresWritableEditor dizem ao host o que o comando precisa
do documento ativo, para que ele possa esmaecer o comando em vez de deixar o
usuário invocar algo que silenciosamente não faz nada.
| Sinalizador | Ative quando o comando… | Ele fica esmaecido em… |
|---|---|---|
requiresEditor |
lê o buffer, a seleção ou o cursor, ou posiciona marcadores (Marcadores, Marcar, Localizar, Ir para a definição) | uma aba que não hospeda documento algum — uma aba de comparação de diferenças ou de visualização de log, onde activeDocument() devolve 0 |
requiresWritableEditor |
altera o texto (Comentário, Converter caixa, Editor de colunas, Aumentar recuo, Linhas…, Formatar documento, Renomear símbolo) | o acima e, além disso, um documento somente leitura: um arquivo acompanhado com tail -f, uma sessão --viewer ou --readonly |
O sinalizador de escrita implica o outro, então um descritor pode definir apenas o
mais forte. Os dois são false por padrão — "sempre disponível", a resposta certa
para comandos que abrem uma caixa de diálogo, uma pasta ou um painel.
O esmaecimento alcança o item de menu, a paleta de comandos e o atalho. Ele só pode
tirar disponibilidade: um item que o seu plugin desativou por meio de
IMenuRegistry::setItemEnabled continua desativado, seja qual for o documento
ativo.
Envolva o descritor no ponto de chamada:
using MTE::plugin::needsWritableEditor;
m_toggleComment = ctx.commands().registerCommand(
needsWritableEditor({ "comment.toggle", "Toggle Comment",
"Ctrl+Q", "Comment" }),
[this] { onToggleComment(); });
Em Python, passe os argumentos nomeados:
@ctx.commands.command(id="wc.upper", title="Upper Case",
requires_writable_editor=True)
def _upper():
...
Atalhos por plataforma¶
shortcut é uma única combinação portátil. O Qt já mapeia o modificador "Ctrl"
para o nativo da plataforma — ⌘ Command no macOS — então "Ctrl+F" é ⌘F lá sem
trabalho extra. Você só precisa de mais quando aquele mapeamento automático cairia
sobre uma combinação que o sistema operacional reserva (por exemplo, "Ctrl+Q" →
⌘Q = Sair no macOS).
Para esses casos, defina platformShortcuts, com as chaves de platform_id. O
host devolve a entrada correspondente à plataforma em execução, ou recorre a
shortcut quando uma plataforma está ausente (ou mapeada para uma cadeia vazia).
Sem #ifdef, sem tipos do Qt — apenas dados — então a associação continua
independente de backend, e uma plataforma nova é uma chave nova:
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";
Combinações ⌘ reservadas do macOS para conferir contra um simples
Ctrl+<key>: ⌘Q (Sair), ⌘W (Fechar), ⌘H (Ocultar), ⌘M (Minimizar), ⌘,
(Preferências), ⌘Espaço (Spotlight). Para uma combinação Ctrl+Alt+<key> (⌥⌘ no
macOS), evite também ⌥⌘H (Ocultar os outros), ⌥⌘M (Minimizar tudo), ⌥⌘D (Dock) e
⌥⌘Esc (Forçar encerramento).
Mais duas regras mantêm os atalhos fora do caminho da digitação:
- Nunca um
Alt+<letter>sozinho como combinação portátil. No Windows esse é o espaço dos mnemônicos de menu, e no macOS ⌥+letra compõe caracteres acentuados (⌥U é a tecla morta do trema, ⌥C digita ç), então o atalho os engoliria — dê a um comando desses uma substituição{"macos", "Meta+Alt+<key>"}(⌃⌥). Ctrl+Alt+<key>é AltGr+<key>no Windows. Layouts nacionais digitam letras ali (polonês: ś ł ą ć ę ń ó ź ż €; alemão: @ € µ). O host os mantém digitáveis — uma pressão do Alt direito que produz um caractere imprimível é entregue como texto, uma pressão do Ctrl+Alt esquerdo executa o comando — então essas combinações são permitidas, mas prefira letras que não digitam nada nos layouts comuns (B D F G H I J K P R T V W Y) e nunca useCtrl+Alt+<digit>.
Remapeamento pelo usuário¶
O shortcut e os platformShortcuts declarados são padrões. O usuário pode
remapear ou desvincular todo comando no Mapeamento de atalhos do núcleo
(Preferências ▸ Atalhos), e os conflitos são resolvidos do lado do host:
duplicatas no mesmo escopo nunca coexistem — o perdedor é suspenso durante a sessão
e reportado. Nada disso exige código de plugin para comandos normais ligados a
menus: o host é dono do QAction deles e o remapeia ao vivo.
scope — comandos em janelas do plugin¶
Comandos cuja combinação vive em um widget do qual o plugin é dono (a barra de
ferramentas de uma janela de ferramenta, em vez do menu principal) declaram
scope = command_scope::kPluginWindow. O host então nunca associa a combinação por
conta própria; ele apenas lista o comando no mapeador e resolve a escolha do
usuário. O plugin aplica a associação aos próprios widgets:
- consulte
ctx.commands().effectiveShortcut(id)ao construir os widgets, e - assine
events::CommandShortcutChanged { commandId, sequence }e reaplique quando um dos ids dele mudar (""= agora sem associação).
O domínio de conflito é global na v1 — uma combinação de janela de plugin se recusa
a colidir com uma combinação de aplicativo. Exemplo prático: a barra de ferramentas
da Visualização de log do Analisador de logs
(loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime
Ctrl+G).
Menus¶
Os caminhos de menu usam / como separador; o host cria os submenus intermediários
sob 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 é RAII, como CommandToken.
Itens de menu marcáveis¶
Um item de menu que representa um estado persistente de liga/desliga pode carregar
uma marca de verificação visível. setItemChecked recebe o TokenId do item — use
o acessor não destrutivo id() do token (release() abriria mão da posse):
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);
A primeira chamada promove o item a marcável; as chamadas seguintes apenas mudam o estado. Um token desconhecido não faz nada em silêncio, então uma retrochamada atrasada que dispare depois do encerramento é inofensiva.
Esmaecendo itens de menu¶
Um comando que precisa de uma condição prévia — uma pasta para fechar, uma aba
fechada para restaurar — deve esmaecer o item de menu dele enquanto aquela condição
falta, em vez de oferecer uma entrada que silenciosamente não faz nada.
setItemEnabled recebe o TokenId do item, exatamente como 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());
Um item desativado continua visível, mas não pode ser acionado, nem por clique nem
pelo atalho de teclado. A chamada é idempotente — repetir o mesmo valor é esperado,
então você nunca precisa lembrar o estado anterior — e um token desconhecido não faz
nada em silêncio, como acima. Os itens começam habilitados, então um item
indisponível na inicialização precisa de uma chamada logo depois de addItem.
Posicionamento dos menus (plugin.json)¶
Onde o menu de primeiro nível de um plugin fica é definido por dados, pelo array
menus no plugin.json — o host nunca fixa nomes de menu de plugins no código:
title— o menu de primeiro nível; corresponde ao primeiro segmento separado por/do seumenuPath. Dê a ele um mnemônico de teclado com&("Lo&g Analyzer") e use a mesma grafia em cadamenuPath: no Windows, Alt+letra abre o menu, e cada título da barra precisa de uma letra própria, em cada idioma — escolha uma que esteja livre entre Arquivo, Editar, Pesquisar, Codificação, Comparar, Exibir, Linguagem, Plug-ins, Janela, Sobre e Ajuda, e deixe cada tradução escolher a sua. A correspondência de menus ignora o marcador.barPriority— posição horizontal na barra de menus (menor = mais à esquerda). Os menus do núcleo são fixos, espaçados de 100 em 100: Arquivo 100, Editar 200, Exibir 300, Linguagem 400, Comparar 500, Janela 600 (Sobre 1000, sempre bem à direita); os plugins preenchem as lacunas (Pesquisar 250, Codificação 350, Analisador de logs 540, Plug-ins 570).itemPriority— classificação vertical do bloco deste plugin dentro detitle(menor = mais acima; 1000 por padrão). Os itens dentro do bloco de um plugin mantêm a ordem em que são acrescentados no código.
Note
Como o plugin.json é embutido como metadados de plugin do Qt, edições nele só
passam a valer depois de uma recompilação.