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.
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.
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;
};
areaerMarginved et klik i en hvilken som helst kolonne til venstre for teksten (linjenumre, bogmærkekolonnen, foldekolonnen) og ellersText; en menu, der åbnes fra tastaturet, er altidText.lineer den linje, der blev klikket på (markørens linje for en tastaturmenu), så du aldrig selv skal omregneposition.positioner byteforskydningen under klikket i teksten (markøren for en tastaturmenu). Et klik i margenen har intet tegn under sig, så dér erpositionforskydningen til starten afline— 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.