Pular para conteúdo

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 use Ctrl+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).

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

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:

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — o menu de primeiro nível; corresponde ao primeiro segmento separado por / do seu menuPath. Dê a ele um mnemônico de teclado com & ("Lo&g Analyzer") e use a mesma grafia em cada menuPath: 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 de title (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.