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.
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.
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;
};
areaesMarginpara un clic en cualquier columna a la izquierda del texto (números de línea, la columna de marcadores, la columna de plegado) yTexten los demás casos; un menú abierto desde el teclado es siempreText.linees la línea en la que se hizo clic (la línea del cursor para un menú de teclado), así que nunca tienes que convertirpositionpor tu cuenta.positiones 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ípositiones el desplazamiento del inicio deline: 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.