Docks, tabs, status bar & theme¶
The UI surfaces a plugin can contribute to, beyond menus. All of them follow the same pattern: describe what you want in backend-neutral data, hand the host a factory, keep the returned RAII token.
Dock panels¶
ctx.docks() adds a dockable side panel. The public surface deliberately
passes the widget as an opaque void* — the Qt-backed host casts it to
QWidget*; a Python plugin builds the same panel declaratively instead
(see the Python ctx.docks reference).
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
};
m_dock = ctx.docks().addPanel(descriptor, std::move(factory));
The host instantiates the panel lazily (on first display) and may
re-instantiate it after a layout reset — keep per-panel state outside the
widget or rebuild it in create(). The DockToken removes the panel on
destruction.
First-class tab types¶
When a feature deserves its own kind of tab (diff results, a log view, a
hex viewer) rather than a text buffer or a side panel, register a tab type
with 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));
ctx.documents().openTab("log-view", {/* params */});
The host places your IDocumentTab in the tab strip alongside editor tabs
and routes save/close to it. Tabs that return non-empty
sessionParams() are captured by session persistence and replayed via
openTab() on restore; if the owning plugin is missing at restore time,
the host rolls the orphaned entries into a single message instead of
dropping them. First-party examples: Diff Compare and the Log Analyzer.
The compare view¶
ctx.compare() opens the editor's two-pane compare window for any pair of
sources — file paths, open documents, or in-memory text:
CompareSource left{...}, right{...};
bool shown = ctx.compare().openCompare(left, right, CompareOptions{});
Always safe to call: a host without a compare view supplies a no-op that
returns false.
Status bar¶
Two independent channels — transient notifications and persistent, plugin-owned segments:
// 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
A higher-severity notification replaces the one on screen immediately; a
lower-severity one may be held back briefly so an Error isn't instantly
wiped by routine Info. Refreshing a segment never clobbers a
notification and vice versa.
Editor context menu¶
Unlike IMenuRegistry — static menu-bar entries bound to commands —
context-menu items are dynamic: on every right-click in the text area
the host asks each registered provider what applies at the clicked
position and appends the answers below the built-in edit actions
(Undo/Redo, Cut/Copy/Paste, Select All):
class MyProvider final : public plugin::IContextMenuProvider
{
std::vector<plugin::ContextMenuItem>
contextMenuItems(const plugin::ContextMenuRequest& req) override
{
// req.doc + req.position (byte offset under the click; the caret
// for keyboard-triggered menus). Empty vector = decline.
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
Providers run on the UI thread while the user waits for the menu — answer from in-memory state, never block. Item titles are dynamic strings the plugin translates itself. The spell checker uses this for its suggestions / Add to Dictionary / Ignore for This Session entries.
Theme palette¶
Plugins that paint custom colours register named categories with light/dark defaults and resolve them to RGB at paint time — never hard-code colours:
// 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 returns the theme's override when present, else the
registered default for the active variant, and 0x000000 for an
unregistered id (treat that as "use my own fallback").
Every registered category automatically appears in the Theme Editor, and
user overrides round-trip in the theme file under pluginColors. Repaint
on events::ThemeChanged.