Gå till innehållet

Dockor, flikar, statusrad och tema

De gränssnittsytor en insticksmodul kan bidra till, utöver menyerna. Alla följer samma mönster: beskriv vad du vill ha i backend-neutrala data, ge värden en fabrik, och behåll den RAII-symbol du får tillbaka.

Dockpaneler

ctx.docks() lägger till en dockningsbar sidopanel. Den publika ytan skickar med avsikt widgeten som en ogenomskinlig void* — den Qt-baserade värden konverterar den till QWidget*; en Python-insticksmodul bygger i stället samma panel deklarativt (se Python-referensen för 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));

Värden instansierar panelen lat (vid första visningen) och kan skapa om den efter en återställning av layouten — håll tillståndet per panel utanför widgeten eller bygg upp det på nytt i create(). DockToken tar bort panelen när den förstörs.

Pixlar eller en andel av fönstret — proportionalSize

0 (standardvärdet) är det enkla beteendet: panelen håller en storlek i pixlar, och att ändra fönstrets storlek lämnar den i fred. Det är vad en verktygspanel vill — en dispositionsvy eller en resultatlista är lika användbar vid en bredd vad fönstret än gör. Ett värde i (0, 1) förklarar i stället en standardandel av huvudfönstret: av dess bredd i områdena Left/Right, av dess höjd i Top/Bottom. Det är vad en förhandsgranskning vill — maximera fönstret och den renderade sidan blir bredare med det.

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

Det är just därför ett enkelt tal, så att en backend utanför C++ kan uppfylla samma kontrakt (Python-backenden tar det som proportional_size). Vad värden garanterar omkring det:

  • Du anger bara standardvärdet. I det ögonblick användaren drar panelens avdelare tar värden över den andel de valde, kommer ihåg den med fönsterlayouten, och ditt värde gäller aldrig mer för den panelen.
  • Värden utanför intervallet klipps, inte avvisas — andelen hålls inom ett förnuftigt band, och både din panel och editorn behåller en minsta utsträckning, så ingen av dem kan fällas ihop. Ett negativt eller icke-ändligt värde läses som 0.
  • Panelens eget minimum vinner. Värden ber aldrig layouten om mindre än vad widgeten kan pressas in i; panelen stannar vid det golvet utan att glömma andelen, och återgår till den när fönstret breddas igen. En panel som är tänkt att vara proportionell bör därför kunna flöda om — ett verktygsfält som staplar sina grupper i stället för att kräva en enda rad gör panelen möjlig att göra smalare.
  • Flytande och dolda paneler hoppas över. En flytande panel är sitt eget fönster och behåller sin storlek; en dold får sin storlek när den tas fram, en åter dockad när den är tillbaka.
  • Paneler i flikar delar en enda plats, så det är panelen framtill som styr den — den användaren tittar på är den vars andel betyder något. Två paneler sida vid sida på samma axel delar en enda budget och skalas tillsammans, med kvoten bevarad.

En panel som registrerats med initiallyVisible = false tas fram senare med setPanelVisible — vanligtvis från det kommando som «öppnar» den. Att visa lyfter också panelen över de dockor den delar område med, så att den verkligen hamnar framför användaren. Anropet är idempotent (upprepa samma värde fritt) och är en tyst nulloperation för en symbol som inte längre lever, så det finns inget att hålla reda på och ingen QDockWidget att gripa efter:

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

isPanelVisible svarar på om en panel är öppen i fönsterlayouten — det tillstånd som setPanelVisible skriver och som användarens växlare eller stängningsknapp slår om. Det betyder inte «syns dess pixlar på skärmen»: en panel som ligger som flik bakom en annan är öppen, en panel som registrerats dold och aldrig visats är det inte, och inte heller en token som inte längre lever. Att fråga skapar aldrig panelen. Tillsammans låter de två anropen en insticksmodul visa sina paneler medan en uppgift pågår och sedan återställa exakt det användaren hade:

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

Paneler som hör ihop — tabGroup

Paneler som anger samma icke-tomma tabGroup och samma preferredArea delar en docka som flikar i stället för att ligga sida vid sida — fem felsökarpaneler blir en enda flikrad längst ned i fönstret. Ge id:t ett namnområde som ett panel-id ("myplugin/panels"); samma id i två områden ger två staplar, och en modul får avsiktligt gå med i en annan moduls grupp. Flikarna visas i registreringsordning; den första öppna panelen ligger överst tills användaren eller setPanelVisible lyfter fram en annan.

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

Gruppen är bara standardarrangemanget. En layout som användaren har sparat går före: en panel som användaren dragit ut ur stapeln, till ett annat område eller till ett flytande fönster stannar där mellan omstarter. En panel som den sparade layouten ännu inte känner till — ny i den här versionen av din modul — går med i sin grupp.

Fullvärdiga fliktyper

När en funktion förtjänar sin egen sorts flik (jämförelseresultat, en loggvy, en hexvisning) snarare än en textbuffert eller en sidopanel, registrerar du en fliktyp med 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 */});

Värden placerar din IDocumentTab i flikraden bredvid redigerarflikarna och leder sparande och stängning dit. openTab() returnerar ett handtag DocumentTabId (0 när inget öppnades — okänd typ, fabrik som inte gav något); id:n är stabila så länge fliken lever och återanvänds aldrig inom en körning, så att ett inaktuellt handtag läses som stängt snarare än som någon annans flik. Behåll det för att följa fliken efteråt, eller släng det för ett öppna-och-glöm:

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

Mönstret «öppna eller lyft fram» är activateTab() när isTabOpen() säger att handtaget fortfarande lever, annars openTab(). Flikar som returnerar en icke-tom sessionParams() fångas av sessionsbevarandet och spelas upp via openTab() vid återställning; saknas den ägande insticksmodulen vid återställningen rullar värden ihop de föräldralösa posterna i ett enda meddelande i stället för att släppa dem. Egenutvecklade exempel: Jämför och Logganalysen.

Jämförelsevyn

ctx.compare() öppnar redigerarens jämförelsefönster med två rutor för vilket par källor som helst — filsökvägar, öppna dokument, eller text i minnet:

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

Alltid säkert att anropa: en värd utan jämförelsevy tillhandahåller en nulloperation som returnerar false.

Statusrad

Två oberoende kanaler — flyktiga aviseringar och bestående segment som insticksmodulen äger:

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

En avisering med högre allvarlighetsgrad ersätter den på skärmen omedelbart; en med lägre grad kan hållas tillbaka en kort stund så att ett Error inte genast sopas bort av ett rutinmässigt Info. Att uppdatera ett segment skriver aldrig över en avisering, och tvärtom.

Redigerarens snabbmeny

Till skillnad från IMenuRegistry — statiska poster i menyraden bundna till kommandon — är snabbmenyns poster dynamiska: vid varje högerklick i textområdet — eller i marginalen bredvid det — frågar värden varje registrerad leverantör vad som gäller på den klickade platsen och lägger svaren under de inbyggda redigeringsåtgärderna (Ångra/Gör om, Klipp ut/Kopiera/Klistra in, Markera allt). Förfrågan talar om var klicket var:

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 är Margin vid ett klick i någon kolumn till vänster om texten (radnummer, bokmärkeskolumnen, vikningskolumnen) och annars Text; en meny som öppnas från tangentbordet är alltid Text.
  • line är den klickade raden (markörens rad för en tangentbordsmeny), så du behöver aldrig räkna om position själv.
  • position är byteförskjutningen under klicket i texten (markören för en tangentbordsmeny). Under ett klick i marginalen finns inget tecken, så där är position förskjutningen för början av line — giltig, men läs aldrig ut en kolumn ur den.

Bestäm per område vad du erbjuder: poster på radnivå (Add Conditional Breakpoint…, Revert Hunk) hör hemma i marginalen, poster på ordnivå i texten. Stavningskontrollen, till exempel, erbjuder ingenting vid ett klick i marginalen.

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

Leverantörer körs på gränssnittstråden medan användaren väntar på menyn — svara från tillstånd i minnet, blockera aldrig. Posttitlarna är dynamiska strängar som insticksmodulen själv översätter. Stavningskontrollen använder detta för sina poster med förslag, Lägg till i ordlistan och Ignorera under den här sessionen.

Temapalett

Insticksmoduler som målar egna färger registrerar namngivna kategorier med standardvärden för ljust och mörkt och löser dem till RGB vid ritningen — hårdkoda aldrig färger:

// 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 returnerar temats åsidosättning när en sådan finns, annars det registrerade standardvärdet för den aktiva varianten, och 0x000000 för ett oregistrerat id (behandla det som «använd mitt eget reservvärde»).

Varje registrerad kategori dyker automatiskt upp i Temaredigeraren, och användarens åsidosättningar går fram och tillbaka i temafilen under pluginColors. Rita om vid events::ThemeChanged.