Gå til indholdet

Paneler, faneblade, statuslinje og tema

De flader i brugerfladen, et udvidelsesmodul kan bidrage til ud over menuerne. De følger alle det samme mønster: beskriv det, du vil have, som backend-neutrale data, giv værten en fabrik, og behold det RAII-symbol, du får tilbage.

Dokpaneler

ctx.docks() tilføjer et sidepanel, der kan dokkes. Den offentlige flade sender med vilje kontrollen videre som en uigennemsigtig void* — den Qt-baserede vært omformer den til en QWidget*; et Python-udvidelsesmodul bygger i stedet det samme panel deklarativt (se Python-dokumentationen for 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ærten danner panelet dovent (ved første visning) og kan danne det igen efter en nulstilling af layoutet — hold tilstand pr. panel uden for kontrollen, eller byg den op igen i create(). DockToken fjerner panelet, når det ødelægges.

Pixels eller en andel af vinduet — proportionalSize

0 (standardvalget) er den enkle opførsel: panelet holder en størrelse i pixels, og det at ændre vinduets størrelse lader det være. Det er, hvad et værktøjspanel vil — en disposition eller en resultatliste er lige nyttig ved én bredde, uanset hvad vinduet gør. En værdi i (0, 1) erklærer i stedet en standardandel af hovedvinduet: af bredden i områderne Left/Right, af højden i Top/Bottom. Det er, hvad en forhåndsvisning vil — maksimér vinduet, og den gengivne side bliver bredere med det.

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

Det er netop derfor et simpelt tal, så en anden backend end C++ kan indfri den samme kontrakt (Python-backenden tager det som proportional_size). Dette garanterer værten omkring det:

  • Du angiver kun standarden. I det øjeblik brugeren trækker panelets deler, tager værten den andel, de valgte, i brug, husker den sammen med vinduets layout, og din værdi gælder aldrig mere for det panel.
  • Værdier uden for intervallet klemmes, ikke afvises — andelen holdes inden for et fornuftigt bånd, og både dit panel og editoren beholder en mindste udstrækning, så ingen af dem kan klappes sammen. En negativ eller ikke-endelig værdi læses som 0.
  • Panelets eget minimum vinder. Værten beder aldrig layoutet om mindre, end kontrollen kan presses ind i; panelet stopper ved den grænse uden at glemme andelen og går tilbage til den, når vinduet bliver bredere igen. Et panel, der er tænkt som proportionalt, bør derfor kunne flyde om — en værktøjslinje, der stabler sine grupper i stedet for at kræve én række, gør, at panelet stadig kan gøres smallere.
  • Flydende og skjulte paneler springes over. Et flydende panel er sit eget vindue og beholder sin størrelse; et skjult får størrelse, når det kommer frem, og et genfastgjort, når det er tilbage.
  • Paneler i faneblade deler én plads, så det er panelet forrest, der styrer den — det, som brugeren ser på, er det, hvis andel betyder noget. To paneler ved siden af hinanden på samme akse deler ét budget og skaleres sammen, med forholdet bevaret.

Et panel, der er registreret med initiallyVisible = false, kommer frem senere med setPanelVisible — typisk fra den kommando, der »åbner« det. At vise det løfter også panelet op over de paneler, det deler område med, så det virkelig ender foran brugeren. Kaldet kan gentages (gentag den samme værdi frit) og gør i stilhed ingenting for et symbol, der ikke længere lever, så der er intet at holde styr på og ingen QDockWidget at række ud efter:

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

isPanelVisible svarer på, om et panel er åbent i vinduets layout — den tilstand, som setPanelVisible skriver, og som brugerens kontakt eller lukkeknap slår om. Det betyder ikke »er dets pixels på skærmen«: et panel, der ligger som faneblad bag et andet, er åbent; et panel, der blev registreret skjult og aldrig vist, er det ikke, og det er et token, der ikke længere lever, heller ikke. At spørge opretter aldrig panelet. Sammen lader de to kald et udvidelsesmodul vise sine paneler, mens en opgave står på, og bagefter sætte præcis det tilbage, som brugeren havde:

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, der hører sammen — tabGroup

Paneler, der angiver den samme ikke-tomme tabGroup og den samme preferredArea, deler én dok som faneblade i stedet for at ligge side om side — fem debuggerpaneler bliver til én fanebladsstribe nederst i vinduet. Giv id'et et navnerum som et panel-id ("myplugin/panels"); det samme id i to områder giver to stakke, og et modul må med vilje slutte sig til et andet moduls gruppe. Fanebladene vises i registreringsrækkefølge; det første åbne panel ligger forrest, indtil brugeren eller setPanelVisible henter et andet frem.

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

Gruppen er kun standardopstillingen. Et layout, som brugeren har gemt, går forud: et panel, som brugeren har trukket ud af stakken, til et andet område eller til et flydende vindue, bliver der på tværs af genstarter. Et panel, som det gemte layout endnu ikke kender — nyt i denne version af dit modul — slutter sig til sin gruppe.

Fuldgyldige fanebladstyper

Når en funktion fortjener sin egen slags faneblad (forskelsresultater, en logvisning, en hex-fremviser) snarere end en tekstbuffer eller et sidepanel, så registrér en fanebladstype 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ærten placerer dit IDocumentTab i fanebladsrækken ved siden af editorfanebladene og sender gemning og lukning videre til det. openTab() giver et DocumentTabId-håndtag tilbage (0, når intet blev åbnet — ukendt type, tomt fabriksresultat); id'er er stabile fanebladets liv ud og genbruges aldrig inden for en kørsel, så et forældet håndtag læses som lukket og ikke som en andens faneblad. Behold det for at følge fanebladet bagefter, eller kassér det for en åbning, du ikke tænker mere over:

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

Idiomet »åbn eller vis frem« er activateTab(), når isTabOpen() siger, at håndtaget stadig lever, og ellers openTab(). Faneblade, der giver et ikke-tomt sessionParams() tilbage, opfanges af sessionsbevaringen og spilles af igen via openTab() ved gendannelsen; er det ejende udvidelsesmodul væk ved gendannelsen, samler værten de forældreløse poster i én besked i stedet for at lade dem falde bort. Egetudviklede eksempler: Sammenlign og Loganalyse.

Sammenligningsvisningen

ctx.compare() åbner editorens sammenligningsvindue med to ruder for et vilkårligt par af kilder — filstier, åbne dokumenter eller tekst i hukommelsen:

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

Det er altid trygt at kalde: en vært uden en sammenligningsvisning leverer en tom udgave, der giver false tilbage.

Statuslinje

To uafhængige kanaler — forbigående meddelelser og vedvarende afsnit, som udvidelsesmodulet ejer:

// 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 meddelelse med højere alvorsgrad erstatter den, der er på skærmen, med det samme; en med lavere alvorsgrad kan blive holdt tilbage et øjeblik, så en Error ikke øjeblikkeligt viskes ud af en rutinemæssig Info. At opdatere et afsnit ødelægger aldrig en meddelelse, og omvendt.

Editorens kontekstmenu

I modsætning til IMenuRegistry — faste poster på menulinjen, der er bundet til kommandoer — er kontekstmenupunkter dynamiske: ved hvert højreklik i tekstområdet — eller i margenen ved siden af — spørger værten hver registreret leverandør, hvad der gælder der, hvor der blev klikket, og føjer svarene til under de indbyggede redigeringshandlinger (Fortryd og Gentag, Klip, Kopiér og Sæt ind, Markér alt). Forespørgslen fortæller, hvor klikket 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 er Margin ved et klik i en hvilken som helst kolonne til venstre for teksten (linjenumre, bogmærkekolonnen, foldekolonnen) og ellers Text; en menu, der åbnes fra tastaturet, er altid Text.
  • line er den linje, der blev klikket på (markørens linje for en tastaturmenu), så du aldrig selv skal omregne position.
  • position er byteforskydningen under klikket i teksten (markøren for en tastaturmenu). Et klik i margenen har intet tegn under sig, så dér er position forskydningen til starten af line — gyldig, men aflæs aldrig en kolonne af den.

Beslut pr. område, hvad du tilbyder: punkter på linjeniveau (Add Conditional Breakpoint…, Revert Hunk) hører til i margenen, punkter på ordniveau i teksten. Stavekontrollen tilbyder for eksempel intet ved et klik i margenen.

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

Leverandører kører på brugerfladetråden, mens brugeren venter på menuen — svar ud fra tilstand i hukommelsen, og bloker aldrig. Punkternes titler er dynamiske tekststrenge, som udvidelsesmodulet selv oversætter. Stavekontrollen bruger dette til sine forslag og til punkterne Føj til ordbogen og Ignorér i denne session.

Temapaletten

Udvidelsesmoduler, der tegner egne farver, registrerer navngivne kategorier med lyse og mørke standardværdier og oversætter dem til RGB på tegnetidspunktet — hardkod aldrig farver:

// 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 giver temaets tilsidesættelse tilbage, når der er en, ellers den registrerede standard for den aktive variant, og 0x000000 for et uregistreret id (behandl det som »brug min egen reserve«).

Hver registreret kategori kommer af sig selv frem i Temaeditoren, og brugerens tilsidesættelser gemmes og læses igen i temafilen under pluginColors. Tegn på ny ved events::ThemeChanged.