Painéis, abas, barra de status e tema¶
As superfícies de interface às quais um plugin pode contribuir, além dos menus. Todas seguem o mesmo padrão: descreva o que você quer com dados neutros em relação ao backend, entregue ao host uma fábrica e guarde o token RAII devolvido.
Painéis acopláveis¶
ctx.docks() acrescenta um painel lateral acoplável. A superfície pública passa
deliberadamente o widget como um void* opaco — o host baseado em Qt o converte em
QWidget*; um plugin Python constrói o mesmo painel de forma declarativa (veja
a referência de ctx.docks em Python).
class IDockPanel {
public:
void* nativeWidget(); // your QWidget*, host takes ownership
std::string title() const;
};
class IDockPanelFactory {
public:
std::unique_ptr<IDockPanel> create();
};
struct DockPanelDescriptor {
std::string id; // "myplugin.outline"
std::string defaultTitle;
DockArea preferredArea = DockArea::Right; // Left/Right/Top/Bottom
bool initiallyVisible = true; // false = created hidden
double proportionalSize = 0.0; // 0 = pixel-sized
std::string tabGroup; // "" = a dock of its own
};
m_dock = ctx.docks().addPanel(descriptor, std::move(factory));
O host instancia o painel de forma preguiçosa (na primeira exibição) e pode
reinstanciá-lo depois de uma redefinição de layout — mantenha o estado próprio do
painel fora do widget, ou reconstrua-o em create(). O DockToken remove o painel
ao ser destruído.
Pixels ou uma fração da janela — proportionalSize¶
0 (o padrão) é o comportamento simples: o painel mantém um tamanho em pixels e
redimensionar a janela não o toca. É o que um painel de ferramenta quer — uma estrutura
de tópicos ou uma lista de resultados é igualmente útil com uma largura só, faça a janela o
que fizer. Um valor em (0, 1), em vez disso, declara uma fração padrão da janela
principal: da largura dela nas áreas Left/Right, da altura em Top/Bottom. É o que uma
visualização quer — maximize a janela e a página renderizada se alarga com ela.
É um número simples justamente para que um backend fora do C++ possa cumprir o mesmo
contrato (o backend Python o recebe como
proportional_size). O que o host garante em volta:
- Você fornece apenas o padrão. No momento em que o usuário arrasta o separador do painel, o host adota a fração que ele escolheu, lembra dela junto com o layout da janela, e o seu valor nunca mais se aplica àquele painel.
- Valores fora do intervalo são limitados, não rejeitados — a fração é mantida dentro de
uma faixa sensata, e tanto o seu painel quanto o editor conservam uma extensão mínima,
então nenhum dos dois pode ser colapsado. Um valor negativo ou não finito é lido como
0. - O mínimo do seu próprio painel vence. O host nunca pede ao layout menos do que o widget pode ser comprimido; o painel para nesse piso sem esquecer a fração, e volta a ela quando a janela é alargada de novo. Um painel que pretende ser proporcional deve, por isso, conseguir se reorganizar — uma barra de ferramentas que empilha seus grupos em vez de exigir uma única linha mantém o painel estreitável.
- Painéis flutuantes e ocultos são ignorados. Um painel flutuante é a própria janela dele e mantém o tamanho; um oculto recebe tamanho quando é revelado, e um reancorado quando volta.
- Painéis em abas compartilham uma única vaga, então quem a governa é o painel à frente — aquele que o usuário está olhando é aquele cuja fração significa algo. Dois painéis lado a lado em um mesmo eixo compartilham um único orçamento e são escalados juntos, mantendo a proporção entre eles.
Um painel registrado com initiallyVisible = false é revelado depois com
setPanelVisible — normalmente a partir do comando que o "abre". Exibi-lo também
o eleva acima dos painéis com que ele compartilha a área, então ele realmente
termina na frente do usuário. A chamada é idempotente (repita o mesmo valor à
vontade) e não faz nada em silêncio para um token que já não está vivo, então não há
nada a rastrear e nenhum QDockWidget a que recorrer:
ctx.docks().setPanelVisible(m_dock.id(), true); // reveal + raise
ctx.docks().setPanelVisible(m_dock.id(), false); // hide, keep registered
isPanelVisible responde se um painel está aberto no layout da janela — o
estado que setPanelVisible grava e que o botão de alternância ou de fechar
do usuário inverte. Não quer dizer "os pixels dele estão na tela": um painel em
uma aba atrás de outro está aberto; um painel registrado oculto e nunca
mostrado não está, e nem um token que não está mais vivo. Perguntar nunca cria
o painel. Juntas, as duas chamadas permitem que um plugin mostre seus painéis
durante uma tarefa e depois devolva exatamente o que o usuário tinha:
const bool wasOpen = ctx.docks().isPanelVisible(m_dock.id());
ctx.docks().setPanelVisible(m_dock.id(), true);
// ... when the task ends:
ctx.docks().setPanelVisible(m_dock.id(), wasOpen);
Painéis que andam juntos — tabGroup¶
Painéis que informam o mesmo tabGroup não vazio e a mesma preferredArea
compartilham um único painel acoplável em abas em vez de ficarem lado a
lado — cinco painéis de um depurador viram uma só faixa de abas na parte de
baixo da janela. Dê ao id um namespace como a um id de painel
("myplugin/panels"); o mesmo id em duas áreas forma duas pilhas, e um plugin
pode entrar de propósito no grupo de outro plugin. As abas aparecem na ordem
de registro; o primeiro painel aberto fica na frente até que o usuário ou
setPanelVisible traga outro para a frente.
O grupo é só a disposição padrão. Um layout salvo pelo usuário tem prioridade: um painel que o usuário arrastou para fora da pilha, para outra área ou para uma janela flutuante fica lá entre reinicializações. Um painel que o layout salvo ainda não conhece — novo nesta versão do seu plugin — entra no seu grupo.
Tipos de aba de primeira classe¶
Quando um recurso merece o próprio tipo de aba (resultados de diff, uma visualização
de log, um visualizador hexadecimal) em vez de um buffer de texto ou um painel
lateral, registre um tipo de aba com ctx.documents():
struct DocumentTabTypeDescriptor {
std::string typeId; // "log-view" -- lowercase-with-hyphens
std::string displayName; // localized type name
DefaultPlacement defaultPlacement; // ActiveGroup / NewGroupRight / NewGroupBottom
};
m_type = ctx.documents().registerType(descriptor, std::move(factory));
DocumentTabId tab = ctx.documents().openTab("log-view", {/* params */});
O host posiciona seu IDocumentTab na faixa de abas ao lado das abas do editor e
encaminha salvar/fechar para ele. openTab() devolve um identificador
DocumentTabId (0 quando nada foi aberto — tipo desconhecido, resultado nulo da
fábrica); os ids são estáveis durante a vida da aba e nunca reutilizados dentro de
uma execução, então um identificador obsoleto é lido como fechado e não como a aba de
outra pessoa. Guarde-o para acompanhar a aba depois, ou descarte-o para uma abertura
sem acompanhamento:
bool open = ctx.documents().isTabOpen(tab); // false once the user closed it
ctx.documents().activateTab(tab); // raise + focus (no-op if closed)
bool closed = ctx.documents().closeTab(tab); // honours canClose(); false on veto
O idioma de "abrir ou revelar" é activateTab() quando isTabOpen() diz que o
identificador ainda está vivo, senão openTab(). Abas que devolvem um
sessionParams() não vazio são capturadas pela persistência de sessão e reproduzidas
via openTab() na restauração; se o plugin dono estiver ausente na hora da
restauração, o host reúne as entradas órfãs em uma única mensagem em vez de
descartá-las. Exemplos oficiais: Comparação de diferenças e o Analisador de logs.
A visualização de comparação¶
ctx.compare() abre a janela de comparação de dois painéis do editor para qualquer
par de fontes — caminhos de arquivo, documentos abertos ou texto em memória:
CompareSource left{...}, right{...};
bool shown = ctx.compare().openCompare(left, right, CompareOptions{});
Sempre seguro chamar: um host sem visualização de comparação fornece uma
implementação vazia que devolve false.
Barra de status¶
Dois canais independentes — notificações passageiras e segmentos persistentes dos quais o plugin é dono:
// Transient: replaces/queues by severity; do NOT use for live state.
ctx.statusBar().showMessage("Wrapped", 2000, StatusLevel::Info);
// Persistent segment: an always-on indicator you own and refresh.
m_segment = ctx.statusBar().addSegment("wordcount.total");
m_segment->setText("2,431 words");
m_segment->setVisible(false); // hide without destroying
// dropping the unique_ptr removes the segment
Uma notificação de severidade mais alta substitui de imediato a que está na tela;
uma de severidade mais baixa pode ser segurada por um instante, para que um Error
não seja apagado na hora por um Info rotineiro. Atualizar um segmento nunca
atropela uma notificação, nem o contrário.
Menu de contexto do editor¶
Diferente do IMenuRegistry — entradas estáticas da barra de menus ligadas a
comandos —, os itens de menu de contexto são dinâmicos: a cada clique com o
botão direito na área de texto — ou na margem ao lado dela —, o host pergunta a
cada provedor registrado o que se aplica no lugar clicado e acrescenta as
respostas abaixo das ações de edição integradas (Desfazer/Refazer,
Recortar/Copiar/Colar, Selecionar tudo). A solicitação diz onde foi o clique:
enum class ContextMenuArea : std::int32_t { Text = 0, Margin = 1 };
struct ContextMenuRequest {
DocumentId doc;
std::int64_t position; // byte offset; the start of `line` for Margin
std::int64_t line; // 0-based
ContextMenuArea area;
};
areaéMarginpara um clique em qualquer coluna à esquerda do texto (números de linha, a coluna de marcadores, a coluna de dobra) eTextnos demais casos; um menu aberto pelo teclado é sempreText.lineé a linha clicada (a linha do cursor para um menu de teclado), então você nunca precisa converterpositionpor conta própria.positioné o deslocamento em bytes sob o clique no texto (o cursor para um menu de teclado). Um clique na margem não tem nenhum caractere embaixo, então alipositioné o deslocamento do início deline— válido, mas nunca leia uma coluna a partir dele.
Decida por área o que você oferece: entradas no nível da linha (Add Conditional Breakpoint…, Revert Hunk) pertencem à margem; as do nível da palavra, ao texto. O corretor ortográfico, por exemplo, não oferece nada para um clique na margem.
class MyProvider final : public plugin::IContextMenuProvider
{
std::vector<plugin::ContextMenuItem>
contextMenuItems(const plugin::ContextMenuRequest& req) override
{
// Word-level entries: text clicks only. Empty vector = decline.
if (req.area != plugin::ContextMenuArea::Text) { return {}; }
if (!appliesAt(req.doc, req.position)) { return {}; }
return {
{ "Do the thing", [this, req] { doTheThing(req); } },
{ "", {} }, // empty title = separator
{ "Another action", [this] { another(); } },
};
}
};
m_token = ctx.contextMenu().registerProvider(m_provider); // RAII token
Os provedores rodam na thread de interface enquanto o usuário espera o menu — responda a partir de estado em memória, nunca bloqueie. Os títulos dos itens são cadeias dinâmicas que o próprio plugin traduz. O verificador ortográfico usa isso para as entradas de sugestões / Adicionar ao dicionário / Ignorar nesta sessão.
Paleta do tema¶
Plugins que pintam cores próprias registram categorias nomeadas com padrões para claro/escuro e as resolvem em RGB na hora de pintar — nunca fixe cores no código:
// ThemeCategory: {id, displayLabel, defaultLight, defaultDark}.
// The id is namespaced "<plugin.id>/<role>" by convention.
m_cat = ctx.themePalette().registerCategory(
{"com.example.myplugin/match", "Match highlight",
/*light*/ 0xFFF3B0, /*dark*/ 0x8A6D00});
std::uint32_t rgb = ctx.themePalette().resolveColor("com.example.myplugin/match");
bool themed = ctx.themePalette().hasOverride("com.example.myplugin/match");
std::string variant = ctx.themePalette().activeVariant(); // "light" / "dark" / ""
resolveColor devolve a substituição do tema quando ela existe, senão o padrão
registrado para a variante ativa, e 0x000000 para um id não registrado (trate isso
como "use minha própria reserva").
Toda categoria registrada aparece automaticamente no Editor de temas, e as
substituições do usuário vão e voltam no arquivo de tema sob pluginColors.
Repinte ao receber events::ThemeChanged.