Skip to content

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.