Ga naar inhoud

Dokken, tabbladen, statusbalk en thema

De interfacevlakken waaraan een plug-in kan bijdragen, buiten de menu's om. Ze volgen alle hetzelfde patroon: beschrijf wat u wilt in backend-neutrale gegevens, geef de host een fabriek, en houd het teruggegeven RAII-token vast.

Dokpanelen

ctx.docks() voegt een dokbaar zijpaneel toe. Het publieke vlak geeft de widget met opzet door als een ondoorzichtige void* — de op Qt gebouwde host zet die om naar QWidget*; een Python-plug-in bouwt hetzelfde paneel in plaats daarvan declaratief op (zie de Python-naslag voor 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));

De host maakt het paneel lui aan (bij de eerste weergave) en kan het na het terugzetten van een indeling opnieuw aanmaken — houd de staat per paneel buiten de widget of bouw hem opnieuw op in create(). Het DockToken verwijdert het paneel wanneer het vernietigd wordt.

Pixels of een aandeel van het venster — proportionalSize

0 (de standaard) is het gewone gedrag: het paneel houdt een grootte in pixels, en het venster van grootte veranderen laat het met rust. Dat is wat een gereedschapspaneel wil — een overzicht of een resultatenlijst is bij één breedte even nuttig, wat het venster ook doet. Een waarde in (0, 1) verklaart in plaats daarvan een standaardaandeel van het hoofdvenster: van de breedte in de gebieden Left/Right, van de hoogte in Top/Bottom. Dat is wat een voorbeeld wil — maximaliseer het venster en de weergegeven pagina wordt breder met het venster mee.

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

Het is juist daarom een eenvoudig getal, zodat een backend buiten C++ hetzelfde contract kan nakomen (de Python-backend neemt het als proportional_size). Wat de host eromheen garandeert:

  • U levert alleen de standaard. Op het moment dat de gebruiker de scheiding van het paneel versleept, neemt de host het aandeel over dat hij koos, onthoudt het bij de vensterindeling, en uw waarde geldt nooit meer voor dat paneel.
  • Waarden buiten het bereik worden afgekapt, niet geweigerd — het aandeel blijft binnen een verstandige band, en zowel uw paneel als de editor houden een minimale omvang, zodat geen van beide kan worden dichtgeklapt. Een negatieve of niet-eindige waarde leest als 0.
  • Het eigen minimum van uw paneel wint. De host vraagt de indeling nooit om minder dan waarin de widget geperst kan worden; het paneel stopt op die bodem zonder het aandeel te vergeten, en gaat ernaar terug wanneer het venster weer breder wordt. Een paneel dat proportioneel bedoeld is, moet dus kunnen herschikken — een werkbalk die haar groepen stapelt in plaats van één rij te eisen, houdt het paneel smaller te maken.
  • Zwevende en verborgen panelen worden overgeslagen. Een zwevend paneel is zijn eigen venster en houdt zijn grootte; een verborgen paneel krijgt zijn grootte wanneer het tevoorschijn komt, een weer aangekoppeld paneel wanneer het terug is.
  • Panelen in tabbladen delen één plaats, dus het paneel op de voorgrond bepaalt die — dat wat de gebruiker bekijkt, is het paneel waarvan het aandeel iets betekent. Twee panelen naast elkaar op één as delen één budget en worden samen geschaald, met behoud van hun verhouding.

Een paneel dat met initiallyVisible = false geregistreerd is wordt later met setPanelVisible tevoorschijn gehaald — doorgaans vanuit de opdracht die het «opent». Tonen brengt het paneel ook boven de dokken waarmee het een gebied deelt, zodat het werkelijk voor de gebruiker eindigt. De aanroep is idempotent (herhaal dezelfde waarde gerust) en is een stille niet-bewerking voor een token dat niet meer leeft, dus er valt niets te volgen en er is geen QDockWidget om naar te grijpen:

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

isPanelVisible beantwoordt of een paneel in de vensterindeling open is — de staat die setPanelVisible schrijft en die de schakelaar of de sluitknop van de gebruiker omzet. Het betekent niet «staan zijn pixels op het scherm»: een paneel dat als tabblad achter een ander zit, is open; een paneel dat verborgen is geregistreerd en nooit getoond, is het niet, en een token dat niet meer leeft evenmin. Vragen maakt het paneel nooit aan. Samen laten de twee aanroepen een plug-in zijn panelen tonen zolang een taak duurt en daarna precies terugzetten wat de gebruiker had:

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

Panelen die bij elkaar horen — tabGroup

Panelen die dezelfde niet-lege tabGroup en dezelfde preferredArea opgeven, delen één dok als tabbladen in plaats van naast elkaar te staan — vijf debuggerpanelen worden één tabstrook onder in het venster. Geef de id een naamruimte zoals een paneel-id ("myplugin/panels"); dezelfde id in twee gebieden vormt twee stapels, en een plug-in mag bewust bij de groep van een andere plug-in aansluiten. Tabbladen verschijnen in volgorde van registratie; het eerste open paneel ligt vooraan totdat de gebruiker of setPanelVisible een ander naar voren haalt.

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

De groep is alleen de standaardindeling. Een door de gebruiker opgeslagen indeling gaat voor: een paneel dat de gebruiker uit de stapel, naar een ander gebied of in een zwevend venster heeft gesleept, blijft daar na een herstart. Een paneel dat de opgeslagen indeling nog niet kent — nieuw in deze versie van je plug-in — sluit zich bij zijn groep aan.

Volwaardige tabbladsoorten

Wanneer een functie een eigen soort tabblad verdient (vergelijkingsresultaten, een logweergave, een hexweergave) in plaats van een tekstbuffer of een zijpaneel, registreert u een tabbladsoort met 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 */});

De host plaatst uw IDocumentTab in de tabbladbalk naast de editortabbladen en leidt opslaan en sluiten ernaartoe. openTab() levert een greep DocumentTabId op (0 wanneer er niets geopend is — onbekende soort, fabriek die niets opleverde); id's zijn stabiel zolang het tabblad leeft en worden binnen één uitvoering nooit hergebruikt, zodat een verouderde greep als gesloten gelezen wordt en niet als het tabblad van iemand anders. Houd hem vast om het tabblad daarna te volgen, of laat hem vallen voor een openen-en-vergeten:

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

Het patroon «openen of tonen» is activateTab() wanneer isTabOpen() zegt dat de greep nog leeft, en anders openTab(). Tabbladen die een niet-lege sessionParams() opleveren worden door de sessiebewaring vastgelegd en bij het herstellen via openTab() afgespeeld; ontbreekt de bezittende plug-in op het moment van herstellen, dan rolt de host de verweesde items in één bericht samen in plaats van ze te laten vallen. Voorbeelden van eigen makelij: Vergelijken en de Logboekanalyse.

De vergelijkingsweergave

ctx.compare() opent het vergelijkingsvenster met twee deelvensters van de editor voor elk paar bronnen — bestandspaden, geopende documenten of tekst in het geheugen:

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

Altijd veilig aan te roepen: een host zonder vergelijkingsweergave levert een niet-bewerking die false oplevert.

Statusbalk

Twee onafhankelijke kanalen — vluchtige meldingen en blijvende segmenten die de plug-in bezit:

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

Een melding met een hogere ernstgraad vervangt die op het scherm meteen; een met een lagere graad kan even tegengehouden worden, zodat een Error niet op slag door een routinematige Info weggevaagd wordt. Een segment verversen overschrijft nooit een melding, en omgekeerd.

Contextmenu van de editor

Anders dan IMenuRegistry — statische items op de menubalk die aan opdrachten gebonden zijn — zijn items van het contextmenu dynamisch: bij elke rechtsklik in het tekstgebied — of in de marge ernaast — vraagt de host aan elke geregistreerde leverancier wat er op de aangeklikte plek van toepassing is, en voegt hij de antwoorden onder de ingebouwde bewerkacties toe (Ongedaan maken/Opnieuw, Knippen/Kopiëren/Plakken, Alles selecteren). Het verzoek zegt waar de klik was:

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 is Margin bij een klik in een willekeurige kolom links van de tekst (regelnummers, de bladwijzerkolom, de vouwkolom) en anders Text; een menu dat via het toetsenbord is geopend, is altijd Text.
  • line is de aangeklikte regel (de regel van de cursor bij een toetsenbordmenu), dus je hoeft position nooit zelf om te rekenen.
  • position is de byte-offset onder de klik in de tekst (de cursor bij een toetsenbordmenu). Onder een klik in de marge staat geen teken, dus daar is position de offset van het begin van line — geldig, maar lees er nooit een kolom uit af.

Bepaal per gebied wat je aanbiedt: items op regelniveau (Add Conditional Breakpoint…, Revert Hunk) horen in de marge, items op woordniveau in de tekst. De spellingcontrole biedt bijvoorbeeld niets aan bij een klik in de marge.

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

Leveranciers draaien op de interfacethread terwijl de gebruiker op het menu wacht — antwoord vanuit staat in het geheugen, blokkeer nooit. Itemtitels zijn dynamische tekenreeksen die de plug-in zelf vertaalt. De spellingcontrole gebruikt dit voor haar items met suggesties, Aan woordenboek toevoegen en Negeren in deze sessie.

Themapalet

Plug-ins die eigen kleuren schilderen registreren benoemde categorieën met standaarden voor licht en donker en lossen die bij het tekenen naar RGB op — schrijf kleuren nooit hard in de code:

// 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 levert de overschrijving van het thema op wanneer die er is, anders de geregistreerde standaard voor de actieve variant, en 0x000000 voor een niet geregistreerde id (behandel dat als «gebruik mijn eigen terugvalwaarde»).

Elke geregistreerde categorie verschijnt automatisch in de Thema-editor, en overschrijvingen van de gebruiker gaan heen en weer in het themabestand onder pluginColors. Teken opnieuw bij events::ThemeChanged.