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.
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.
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;
};
areaistMarginbei einem Klick in eine beliebige Spalte links vom Text (Zeilennummern, die Lesezeichenspalte, die Faltspalte) und sonstText; ein über die Tastatur geöffnetes Menü ist immerText.lineist die angeklickte Zeile (bei einem Tastaturmenü die Zeile der Einfügemarke), Sie müssenpositionalso nie selbst umrechnen.positionist der Byte-Offset unter dem Klick im Text (bei einem Tastaturmenü die Einfügemarke). Unter einem Klick in den Rand liegt kein Zeichen, dort istpositiondaher der Offset des Anfangs vonline— 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.