Pular para conteúdo

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.

desc.proportionalSize = 0.40;   // open at 40 % of the window

É 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.

desc.preferredArea = DockArea::Bottom;
desc.tabGroup      = "myplugin/panels";

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 é Margin para um clique em qualquer coluna à esquerda do texto (números de linha, a coluna de marcadores, a coluna de dobra) e Text nos demais casos; um menu aberto pelo teclado é sempre Text.
  • line é a linha clicada (a linha do cursor para um menu de teclado), então você nunca precisa converter position por 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 ali position é o deslocamento do início de line — 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.