Zum Inhalt

Docks, Tabs, Statusleiste & Theme

Die UI-Flächen, zu denen ein Plugin über Menüs hinaus beitragen kann. Alle folgen demselben Muster: Beschreiben Sie Ihr Anliegen in Backend-neutralen Daten, übergeben Sie dem Host eine Factory und behalten Sie das zurückgegebene RAII-Token.

Dock-Panels

ctx.docks() fügt ein andockbares Seitenpanel hinzu. Die öffentliche Oberfläche übergibt das Widget bewusst als opakes void* — der Qt-gestützte Host castet es zu QWidget*; ein Python-Plugin baut dasselbe Panel stattdessen deklarativ (siehe die Python-Referenz zu ctx.docks).

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

Der Host instanziiert das Panel lazy (bei der ersten Anzeige) und kann es nach einem Layout-Reset erneut instanziieren — halten Sie den Zustand pro Panel außerhalb des Widgets oder bauen Sie ihn in create() neu auf. Das DockToken entfernt das Panel bei seiner Zerstörung.

Pixel oder ein Anteil des Fensters — proportionalSize

0 (der Standard) ist das schlichte Verhalten: das Panel hält eine Pixelgröße, und eine Fenstergrößenänderung lässt es in Ruhe. Das will ein Werkzeug-Panel — eine Gliederung oder eine Ergebnisliste ist bei einer Breite gleich nützlich, was das Fenster auch tut. Ein Wert in (0, 1) erklärt stattdessen einen Standardanteil am Hauptfenster: seiner Breite in den Bereichen Left/Right, seiner Höhe in Top/Bottom. Das will eine Vorschau — maximieren Sie das Fenster, und die gerenderte Seite wird mit ihm breiter.

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

Es ist genau darum eine einfache Zahl, damit ein Backend außerhalb von C++ denselben Vertrag erfüllen kann (das Python-Backend nimmt sie als proportional_size). Was der Host darum herum garantiert:

  • Sie liefern nur den Standard. Sobald der Nutzer den Trenner des Panels zieht, übernimmt der Host den von ihm gewählten Anteil, merkt ihn mit dem Fensterlayout, und Ihr Wert gilt für dieses Panel nie wieder.
  • Werte außerhalb des Bereichs werden geklemmt, nicht abgelehnt — der Anteil wird in einem sinnvollen Band gehalten, und sowohl Ihr Panel als auch der Editor behalten eine Mindestausdehnung, sodass keines zusammengeklappt werden kann. Ein negativer oder nicht endlicher Wert liest sich als 0.
  • Das eigene Minimum Ihres Panels gewinnt. Der Host verlangt vom Layout nie weniger, als das Widget zusammengedrückt werden kann; das Panel hält an dieser Untergrenze, ohne den Anteil zu vergessen, und kehrt zu ihm zurück, wenn das Fenster wieder breiter wird. Ein Panel, das proportional sein soll, sollte sich daher umbrechen können — eine Werkzeugleiste, die ihre Gruppen stapelt statt eine Reihe zu verlangen, hält das Panel schmaler machbar.
  • Freischwebende und verborgene Panels werden übersprungen. Ein freischwebendes Panel ist sein eigenes Fenster und behält seine Größe; ein verborgenes wird dimensioniert, wenn es gezeigt wird, ein wieder angedocktes, wenn es zurück ist.
  • Zu Tabs zusammengefasste Panels teilen einen Platz, das Panel im Vordergrund bestimmt ihn also — jenes, das der Nutzer ansieht, ist das, dessen Anteil etwas bedeutet. Zwei Panels nebeneinander auf einer Achse teilen ein einziges Budget und werden gemeinsam skaliert, wobei ihr Verhältnis erhalten bleibt.

Ein mit initiallyVisible = false registriertes Panel zeigen Sie später mit setPanelVisible — typischerweise aus dem Befehl, der es „öffnet". Anzeigen hebt das Panel auch über die Docks, mit denen es sich einen Bereich teilt, sodass es wirklich vor dem Nutzer landet. Der Aufruf ist idempotent (denselben Wert beliebig wiederholen) und ein stilles No-op für ein Token, das nicht mehr lebt — nichts zu verfolgen und kein QDockWidget, nach dem man greifen müsste:

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

isPanelVisible beantwortet, ob ein Panel im Fensterlayout geöffnet ist — der Zustand, den setPanelVisible schreibt und den der Umschalter oder die Schließen-Schaltfläche des Benutzers umlegt. Es heißt nicht „sind seine Pixel auf dem Bildschirm“: Ein Panel, das als Tab hinter einem anderen liegt, ist geöffnet; ein verborgen registriertes und nie gezeigtes Panel ist es nicht, ebenso wenig ein Token, das nicht mehr lebt. Die Abfrage erzeugt das Panel nie. Zusammen erlauben die beiden Aufrufe einem Plugin, seine Panels für die Dauer einer Aufgabe zu zeigen und danach genau das wiederherzustellen, was der Benutzer hatte:

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

Panels, die zusammengehören — tabGroup

Panels, die dieselbe nicht-leere tabGroup und dieselbe preferredArea angeben, teilen sich ein Dock als Tabs, statt nebeneinander zu liegen — fünf Debugger-Panels werden zu einer Tab-Leiste am unteren Fensterrand. Geben Sie der ID einen Namensraum wie einer Panel-ID ("myplugin/panels"); dieselbe ID in zwei Bereichen ergibt zwei Stapel, und ein Plugin darf absichtlich der Gruppe eines anderen Plugins beitreten. Tabs erscheinen in Registrierungsreihenfolge; das erste geöffnete Panel liegt vorn, bis der Benutzer oder setPanelVisible ein anderes nach vorn holt.

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

Die Gruppe ist nur die Standardanordnung. Ein vom Benutzer gespeichertes Layout hat Vorrang: Ein Panel, das der Benutzer aus dem Stapel, in einen anderen Bereich oder in ein schwebendes Fenster gezogen hat, bleibt über Neustarts hinweg dort. Ein Panel, das das gespeicherte Layout noch nicht kennt — neu in dieser Version Ihres Plugins —, tritt seiner Gruppe bei.

Vollwertige Tab-Typen

Wenn ein Feature eine eigene Art von Tab verdient (Diff-Ergebnisse, eine Log-Ansicht, ein Hex-Viewer) statt eines Textpuffers oder eines Seitenpanels, registrieren Sie einen Tab-Typ mit 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 */});

Der Host platziert Ihren IDocumentTab in der Tab-Leiste neben den Editor-Tabs und leitet Speichern/Schließen an ihn weiter. openTab() liefert ein DocumentTabId-Handle (0, wenn nichts geöffnet wurde — unbekannter Typ, Fabrik gab null zurück); Ids sind für die Lebensdauer des Tabs stabil und werden innerhalb eines Laufs nie wiederverwendet, sodass ein veraltetes Handle als „geschlossen" gelesen wird und nie den Tab eines anderen adressiert. Behalten Sie es, um dem Tab danach zu folgen, oder verwerfen Sie es bei einem Fire-and-forget-Öffnen:

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

Das Idiom „öffnen oder zeigen" ist activateTab(), wenn isTabOpen() das Handle noch als lebendig meldet, sonst openTab(). Tabs, die ein nicht-leeres sessionParams() zurückgeben, werden von der Sitzungspersistenz erfasst und beim Wiederherstellen über openTab() wieder abgespielt; fehlt das besitzende Plugin zum Zeitpunkt der Wiederherstellung, fasst der Host die verwaisten Einträge in einer einzigen Meldung zusammen, statt sie zu verwerfen. First-Party-Beispiele: Diff Compare und der Log Analyzer.

Die Vergleichsansicht

ctx.compare() öffnet das zweigeteilte Vergleichsfenster des Editors für ein beliebiges Quellenpaar — Dateipfade, offene Dokumente oder Text im Speicher:

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

Der Aufruf ist immer sicher: Ein Host ohne Vergleichsansicht liefert ein No-op, das false zurückgibt.

Statusleiste

Zwei unabhängige Kanäle — flüchtige Benachrichtigungen und persistente, Plugin-eigene Segmente:

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

Eine Benachrichtigung höherer Dringlichkeit ersetzt die angezeigte sofort; eine mit niedrigerer Dringlichkeit kann kurz zurückgehalten werden, damit ein Error nicht sofort von routinemäßigem Info überschrieben wird. Das Aktualisieren eines Segments überschreibt niemals eine Benachrichtigung und umgekehrt.

Editor-Kontextmenü

Anders als IMenuRegistry — statische Menüleisten-Einträge, die an Commands gebunden sind — sind Kontextmenü-Einträge dynamisch: Bei jedem Rechtsklick in den Textbereich — oder in den Rand daneben — fragt der Host jeden registrierten Provider, was an der angeklickten Stelle zutrifft, und hängt die Antworten unter die eingebauten Bearbeitungsaktionen (Undo/Redo, Ausschneiden/Kopieren/Einfügen, Alles auswählen). Die Anfrage sagt, wo der Klick war:

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 ist Margin bei einem Klick in eine beliebige Spalte links vom Text (Zeilennummern, die Lesezeichenspalte, die Faltspalte) und sonst Text; ein über die Tastatur geöffnetes Menü ist immer Text.
  • line ist die angeklickte Zeile (bei einem Tastaturmenü die Zeile der Einfügemarke), Sie müssen position also nie selbst umrechnen.
  • position ist der Byte-Offset unter dem Klick im Text (bei einem Tastaturmenü die Einfügemarke). Unter einem Klick in den Rand liegt kein Zeichen, dort ist position daher der Offset des Anfangs von line — gültig, aber lesen Sie nie eine Spalte daraus ab.

Entscheiden Sie pro Bereich, was Sie anbieten: zeilenbezogene Einträge (Add Conditional Breakpoint…, Revert Hunk) gehören in den Rand, wortbezogene in den Text. Die Rechtschreibprüfung zum Beispiel bietet bei einem Klick in den Rand nichts an.

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

Provider laufen auf dem UI-Thread, während der Benutzer auf das Menü wartet — antworten Sie aus dem Zustand im Speicher, blockieren Sie niemals. Die Eintragstitel sind dynamische Strings, die das Plugin selbst übersetzt. Die Rechtschreibprüfung nutzt dies für ihre Vorschläge / Zum Wörterbuch hinzufügen / Für diese Sitzung ignorieren-Einträge.

Theme-Palette

Plugins, die eigene Farben zeichnen, registrieren benannte Kategorien mit Hell/Dunkel-Standardwerten und lösen sie zur Zeichenzeit zu RGB auf — Farben niemals fest codieren:

// 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 liefert die Überschreibung des Themes, falls vorhanden, sonst den registrierten Standard der aktiven Variante, und 0x000000 für eine nicht registrierte Id (behandeln Sie das als „eigenen Fallback verwenden“).

Jede registrierte Kategorie erscheint automatisch im Theme-Editor, und Benutzer-Überschreibungen werden in der Theme-Datei unter pluginColors verlustfrei gespeichert. Zeichnen Sie bei events::ThemeChanged neu.