Saltar a contenido

Paneles, pestañas, barra de estado y tema

Las superficies de interfaz a las que puede contribuir un plugin, más allá de los menús. Todas siguen el mismo patrón: describe lo que quieres con datos neutrales respecto al backend, entrega al anfitrión una fábrica y conserva el testigo RAII que te devuelve.

Paneles acoplables

ctx.docks() añade un panel lateral acoplable. La superficie pública pasa a propósito el widget como un void* opaco: el anfitrión basado en Qt lo convierte a QWidget*; un plugin de Python construye el mismo panel de forma declarativa (consulta la referencia de ctx.docks en 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));

El anfitrión instancia el panel de forma perezosa (en su primera visualización) y puede volver a instanciarlo tras un restablecimiento del diseño: guarda el estado propio del panel fuera del widget, o reconstrúyelo en create(). El DockToken elimina el panel al destruirse.

Píxeles o una porción de la ventana — proportionalSize

0 (el valor predeterminado) es el comportamiento llano: el panel conserva un tamaño en píxeles y redimensionar la ventana no lo toca. Eso es lo que quiere un panel de herramienta: un esquema o una lista de resultados es igual de útil con un ancho dado haga lo que haga la ventana. Un valor en (0, 1) declara en cambio una porción predeterminada de la ventana principal: de su ancho en las áreas Left/Right, de su alto en Top/Bottom. Eso es lo que quiere una vista previa: maximiza la ventana y la página dibujada se ensancha con ella.

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

Es un número simple precisamente para que un backend que no sea C++ pueda cumplir el mismo contrato (el backend de Python lo toma como proportional_size). Lo que el anfitrión garantiza alrededor:

  • Tú solo aportas el valor predeterminado. En el momento en que el usuario arrastra el separador del panel, el anfitrión adopta la porción que ha elegido, la recuerda con el diseño de la ventana, y tu valor no vuelve a aplicarse nunca a ese panel.
  • Los valores fuera de rango se acotan, no se rechazan: la porción se mantiene dentro de una banda sensata, y tanto tu panel como el editor conservan una extensión mínima, así que ninguno puede colapsarse. Un valor negativo o no finito se lee como 0.
  • El mínimo propio de tu panel manda. El anfitrión nunca pide al diseño menos de lo que el widget puede comprimirse; el panel se detiene en ese suelo sin olvidar la porción, y vuelve a ella cuando la ventana se ensancha de nuevo. Un panel pensado para ser proporcional debería, por tanto, poder reorganizarse: una barra de herramientas que apila sus grupos en lugar de exigir una sola fila mantiene el panel estrechable.
  • Los paneles flotantes y ocultos se omiten. Un panel flotante es su propia ventana y conserva su tamaño; uno oculto recibe su tamaño cuando se revela, y uno vuelto a acoplar cuando regresa.
  • Los paneles en pestañas comparten un único hueco, así que lo gobierna el panel que está delante: el que el usuario mira es aquel cuya porción significa algo. Dos paneles uno al lado del otro en un mismo eje comparten un solo presupuesto y se escalan juntos, conservando su proporción.

Un panel registrado con initiallyVisible = false se revela después con setPanelVisible, normalmente desde el comando que lo «abre». Mostrarlo lo levanta además por encima de los paneles acoplables con los que comparte área, de modo que acabe realmente delante del usuario. La llamada es idempotente (repite el mismo valor cuantas veces quieras) y no hace nada en silencio con un testigo que ya no está vivo, así que no hay nada que rastrear ni ningún QDockWidget al que recurrir:

ctx.docks().setPanelVisible(m_dock.id(), true);    // reveal + raise
ctx.docks().setPanelVisible(m_dock.id(), false);   // hide, keep registered

isPanelVisible responde si un panel está abierto en la disposición de la ventana: el estado que escribe setPanelVisible y que cambia el interruptor o el botón de cierre del usuario. No significa «están sus píxeles en pantalla»: un panel en una pestaña detrás de otro está abierto; un panel registrado oculto y nunca mostrado no lo está, como tampoco un token que ya no está vivo. Preguntar nunca crea el panel. Juntas, las dos llamadas permiten a un plugin mostrar sus paneles mientras dura una tarea y después devolver exactamente lo que tenía el usuario:

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);

Paneles que van juntos: tabGroup

Los paneles que indican el mismo tabGroup no vacío y la misma preferredArea comparten un único panel acoplable como pestañas en lugar de colocarse uno al lado de otro: cinco paneles de un depurador se convierten en una sola tira de pestañas en la parte inferior de la ventana. Pon al identificador un espacio de nombres como a un identificador de panel ("myplugin/panels"); el mismo identificador en dos áreas forma dos pilas, y un plugin puede unirse a propósito al grupo de otro plugin. Las pestañas aparecen en orden de registro; el primer panel abierto queda delante hasta que el usuario o setPanelVisible traigan otro al frente.

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

El grupo es solo la disposición predeterminada. Una disposición guardada por el usuario tiene prioridad: un panel que el usuario sacó de la pila, llevó a otra área o a una ventana flotante se queda allí entre reinicios. Un panel que la disposición guardada aún no conoce —nuevo en esta versión de tu plugin— se une a su grupo.

Tipos de pestaña de primera clase

Cuando una función merece su propio tipo de pestaña (resultados de un diff, una vista de registro, un visor hexadecimal) en lugar de un búfer de texto o un panel lateral, registra un tipo de pestaña con 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 */});

El anfitrión coloca tu IDocumentTab en la tira de pestañas junto a las del editor y le encamina el guardado y el cierre. openTab() devuelve un identificador DocumentTabId (0 cuando no se abrió nada: tipo desconocido, resultado nulo de la fábrica); los identificadores son estables durante la vida de la pestaña y nunca se reutilizan dentro de una misma ejecución, así que un identificador obsoleto se lee como cerrado y no como la pestaña de otro. Consérvalo para seguir la pestaña después, o descártalo para una apertura sin seguimiento:

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

El idioma de «abrir o mostrar» es activateTab() cuando isTabOpen() dice que el identificador sigue vivo y, si no, openTab(). Las pestañas que devuelven un sessionParams() no vacío las captura la persistencia de sesión y se reproducen mediante openTab() al restaurar; si el plugin propietario no está presente en el momento de la restauración, el anfitrión agrupa las entradas huérfanas en un único mensaje en lugar de descartarlas. Ejemplos oficiales: Comparación de diferencias y el Analizador de registros.

La vista de comparación

ctx.compare() abre la ventana de comparación de dos paneles del editor para cualquier pareja de fuentes: rutas de archivo, documentos abiertos o texto en memoria.

CompareSource left{...}, right{...};
bool shown = ctx.compare().openCompare(left, right, CompareOptions{});

Siempre es seguro llamarla: un anfitrión sin vista de comparación suministra una implementación vacía que devuelve false.

Barra de estado

Dos canales independientes: notificaciones pasajeras y segmentos persistentes propiedad del plugin.

// 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

Una notificación de mayor gravedad sustituye de inmediato a la que hay en pantalla; una de menor gravedad puede retenerse un instante para que un Error no lo borre al momento un Info rutinario. Refrescar un segmento nunca pisa una notificación, ni al revés.

Menú contextual del editor

A diferencia de IMenuRegistry —entradas estáticas de la barra de menús ligadas a comandos—, los elementos del menú contextual son dinámicos: en cada clic derecho en el área de texto —o en el margen junto a ella—, el anfitrión pregunta a cada proveedor registrado qué corresponde en el lugar del clic y añade las respuestas bajo las acciones de edición integradas (Deshacer/Rehacer, Cortar/Copiar/Pegar, Seleccionar todo). La petición indica dónde fue el clic:

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 es Margin para un clic en cualquier columna a la izquierda del texto (números de línea, la columna de marcadores, la columna de plegado) y Text en los demás casos; un menú abierto desde el teclado es siempre Text.
  • line es la línea en la que se hizo clic (la línea del cursor para un menú de teclado), así que nunca tienes que convertir position por tu cuenta.
  • position es el desplazamiento en bytes bajo el clic en el texto (el cursor para un menú de teclado). Un clic en el margen no tiene ningún carácter debajo, así que allí position es el desplazamiento del inicio de line: válido, pero nunca leas una columna a partir de él.

Decide por área lo que ofreces: las entradas de nivel de línea (Add Conditional Breakpoint…, Revert Hunk) van en el margen; las de nivel de palabra, en el texto. El corrector ortográfico, por ejemplo, no ofrece nada para un clic en el margen.

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

Los proveedores se ejecutan en el hilo de interfaz mientras el usuario espera el menú: responde desde estado en memoria, nunca bloquees. Los títulos de los elementos son cadenas dinámicas que traduce el propio plugin. El corrector ortográfico usa esto para sus entradas de sugerencias, Añadir al diccionario e Ignorar en esta sesión.

Paleta del tema

Los plugins que pintan colores propios registran categorías con nombre con valores predeterminados para claro y oscuro y las resuelven a RGB en el momento de pintar; nunca codifiques los colores a mano:

// 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 devuelve la anulación del tema cuando existe, si no el valor predeterminado registrado para la variante activa, y 0x000000 para un identificador no registrado (interprétalo como «usa mi propio valor de reserva»).

Todas las categorías registradas aparecen automáticamente en el Editor de temas, y las anulaciones del usuario van y vuelven en el archivo de tema bajo pluginColors. Vuelve a pintar al recibir events::ThemeChanged.