Přeskočit obsah

Doky, karty, stavový řádek a motiv

Plochy rozhraní, do kterých může zásuvný modul přispívat, nad rámec nabídek. Všechny se řídí týmž vzorem: popište, co chcete, daty neutrálními vůči backendu, předejte hostiteli továrnu a podržte si vrácený token RAII.

Ukotvitelné panely

ctx.docks() přidává ukotvitelný postranní panel. Veřejná plocha záměrně předává widget jako neprůhledný void* — hostitel postavený na Qt jej přetypuje na QWidget*; modul v Pythonu místo toho staví tentýž panel deklarativně (viz reference ctx.docks v Pythonu).

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

Hostitel panel vytváří líně (při prvním zobrazení) a může jej po obnovení rozvržení vytvořit znovu — stav panelu proto držte mimo widget, nebo jej v create() znovu sestavte. DockToken panel při svém zániku odebere.

Pixely, nebo podíl okna — proportionalSize

0 (výchozí hodnota) je obyčejné chování: panel si drží velikost v pixelech a změna velikosti okna se ho nedotkne. To chce nástrojový panel — přehled dokumentu nebo seznam výsledků je při jedné šířce stejně užitečný, ať okno dělá cokoli. Hodnota z intervalu (0, 1) naproti tomu vyhlašuje výchozí podíl hlavního okna: jeho šířky v oblastech Left/Right, jeho výšky v Top/Bottom. To chce náhled — okno maximalizujte a vykreslená stránka se s ním rozšíří.

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

Je to obyčejné číslo právě proto, aby stejný kontrakt mohl naplnit i jiný backend než v C++ (Pythonový backend jej přijímá jako proportional_size). Co kolem toho hostitel zaručuje:

  • Vy dodáváte jen výchozí hodnotu. Ve chvíli, kdy uživatel přetáhne oddělovač panelu, hostitel přijme podíl, který si zvolil, zapamatuje si jej spolu s rozvržením okna a vaše hodnota se na ten panel už nikdy nevztahuje.
  • Hodnoty mimo rozsah se omezí, nikoli odmítnou — podíl se drží v rozumném pásu a jak váš panel, tak editor si podrží minimální rozměr, takže ani jeden nelze sklapnout. Negativní nebo nekonečná hodnota se čte jako 0.
  • Vítězí vlastní minimum vašeho panelu. Hostitel nikdy nežádá od rozvržení méně, než na co lze widget stlačit; panel se na té hranici zastaví, aniž by podíl zapomněl, a vrátí se k němu, jakmile se okno opět rozšíří. Panel, který má být proporcionální, by se proto měl umět přeskládat — lišta, která své skupiny skládá pod sebe místo toho, aby vyžadovala jedinou řadu, nechává panel zužitelný.
  • Plovoucí a skryté panely se přeskakují. Plovoucí panel je vlastním oknem a drží si svou velikost; skrytému se velikost přidělí, až se odkryje, a znovu zadokovanému, až je zpět.
  • Panely v kartách sdílejí jedno místo, takže jej řídí panel vpředu — ten, na který se uživatel dívá, je ten, jehož podíl něco znamená. Dva panely vedle sebe na jedné ose sdílejí jediný rozpočet a škálují se společně se zachováním svého poměru.

Panel zaregistrovaný s initiallyVisible = false se později odkryje pomocí setPanelVisible — obvykle z příkazu, který jej »otevírá«. Zobrazení panel zároveň zvedne nad doky, s nimiž sdílí oblast, takže skutečně skončí před uživatelem. Volání je idempotentní (tutéž hodnotu opakujte, jak chcete) a je tichou prázdnou operací u tokenu, který už nežije, takže není co sledovat a není třeba sahat po žádném QDockWidget:

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

isPanelVisible odpovídá, zda je panel v rozvržení okna otevřený — stav, který zapisuje setPanelVisible a který přepíná přepínač nebo zavírací tlačítko uživatele. Neznamená to »jsou jeho pixely na obrazovce«: panel schovaný jako karta za jiným je otevřený, panel zaregistrovaný jako skrytý a nikdy nezobrazený není, a není jím ani token, který už nežije. Dotaz panel nikdy nevytvoří. Obě volání dohromady umožňují modulu zobrazit své panely po dobu úlohy a potom vrátit přesně to, co měl uživatel:

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

Panely, které patří k sobě — tabGroup

Panely, které uvádějí stejnou neprázdnou tabGroup a stejnou preferredArea, sdílejí jeden dok jako karty, místo aby ležely vedle sebe — pět panelů ladicího programu se změní v jeden pruh karet u dolního okraje okna. Dejte identifikátoru jmenný prostor jako identifikátoru panelu ("myplugin/panels"); stejný identifikátor ve dvou oblastech vytvoří dva zásobníky a modul se může záměrně připojit ke skupině jiného modulu. Karty se objevují v pořadí registrace; první otevřený panel je vpředu, dokud uživatel nebo setPanelVisible nepřenese dopředu jiný.

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

Skupina je jen výchozí uspořádání. Rozvržení uložené uživatelem má přednost: panel, který uživatel vytáhl ze zásobníku, přesunul do jiné oblasti nebo do plovoucího okna, tam zůstane i po restartu. Panel, který uložené rozvržení ještě nezná — nový v této verzi vašeho modulu —, se připojí ke své skupině.

Plnohodnotné typy karet

Když si funkce zaslouží vlastní druh karty (výsledky porovnání, zobrazení protokolu, prohlížeč v šestnáctkové soustavě) spíš než textový buffer či postranní panel, zaregistrujte typ karty pomocí 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 */});

Hostitel váš IDocumentTab umístí do lišty karet po boku karet editoru a směruje na něj ukládání a zavírání. openTab() vrací popisovač DocumentTabId (0, když se nic neotevřelo — neznámý typ, nulový výsledek továrny); id jsou po dobu života karty stabilní a v rámci běhu se nikdy nepoužijí znovu, takže se zastaralý popisovač čte jako zavřený, nikoli jako cizí karta. Podržte si jej, chcete-li kartu dál sledovat, nebo jej zahoďte pro otevření »spusť a zapomeň«:

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

Idiomem »otevřít nebo odkrýt« je activateTab(), když isTabOpen() říká, že popisovač stále žije, jinak openTab(). Karty, které vracejí neprázdné sessionParams(), zachycuje uchovávání relace a při obnovení se přehrají přes openTab(); pokud vlastnící modul v době obnovení chybí, sbalí hostitel osiřelé položky do jediné zprávy, místo aby je zahodil. Vlastní příklady: Porovnání souborů a Analyzátor protokolů.

Zobrazení porovnání

ctx.compare() otevírá dvoupanelové okno porovnání v editoru pro libovolnou dvojici zdrojů — cesty k souborům, otevřené dokumenty, nebo text v paměti:

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

Volat jej je vždy bezpečné: hostitel bez zobrazení porovnání dodá prázdnou implementaci, která vrací false.

Stavový řádek

Dva nezávislé kanály — pomíjivá oznámení a trvalé části vlastněné modulem:

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

Oznámení s vyšší závažností to na obrazovce ihned nahradí; oznámení s nižší závažností může být krátce zadrženo, aby Error okamžitě nesmazalo rutinní Info. Obnovení části nikdy nepřebije oznámení a naopak.

Místní nabídka editoru

Na rozdíl od IMenuRegistry — statických položek nabídkové lišty vázaných na příkazy — jsou položky místní nabídky dynamické: při každém kliknutí pravým tlačítkem v textové oblasti — nebo v okraji vedle ní — se hostitel zeptá každého zaregistrovaného poskytovatele, co platí na místě kliknutí, a odpovědi připojí pod vestavěné editační akce (Zpět/Znovu, Vyjmout/Kopírovat/Vložit, Vybrat vše). Požadavek říká, kde ke kliknutí došlo:

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 je Margin při kliknutí do libovolného sloupce nalevo od textu (čísla řádků, sloupec záložek, sloupec sbalování) a jinak Text; nabídka otevřená z klávesnice je vždy Text.
  • line je řádek, na který se kliklo (u nabídky z klávesnice řádek kurzoru), takže position nikdy nemusíte převádět sami.
  • position je posun v bajtech pod kliknutím v textu (u nabídky z klávesnice kurzor). Pod kliknutím do okraje není žádný znak, takže tam je position posun začátku řádku line — platný, ale nikdy z něj nečtěte sloupec.

Rozhodněte pro každou oblast zvlášť, co nabízíte: položky na úrovni řádku (Add Conditional Breakpoint…, Revert Hunk) patří do okraje, položky na úrovni slova do textu. Kontrola pravopisu například při kliknutí do okraje nenabízí nic.

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

Poskytovatelé běží v uživatelském vlákně, zatímco uživatel na nabídku čeká — odpovídejte ze stavu v paměti, nikdy neblokujte. Názvy položek jsou dynamické řetězce, které si modul překládá sám. Kontrola pravopisu to používá pro své návrhy a položky Přidat do slovníku / Ignorovat v této relaci.

Paleta motivu

Moduly, které vykreslují vlastní barvy, registrují pojmenované kategorie s výchozími hodnotami pro světlou a tmavou variantu a vyhodnocují je na RGB až při vykreslování — barvy nikdy nezadrátovávejte:

// 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 vrací přebití z motivu, je-li přítomné, jinak zaregistrovanou výchozí hodnotu pro aktivní variantu, a 0x000000 u nezaregistrovaného id (to berte jako »použij mou vlastní záložní hodnotu«).

Každá zaregistrovaná kategorie se automaticky objeví v Editoru motivů a přebití od uživatele se obousměrně přenášejí v souboru motivu pod pluginColors. Překreslujte při events::ThemeChanged.