Preskočiť na obsah

Panely, karty, stavový riadok a motív

Plochy rozhrania, do ktorých môže zásuvný modul prispieť nad rámec ponúk. Všetky sa riadia tým istým vzorom: opíšte, čo chcete, v údajoch nezávislých od backendu, odovzdajte hostiteľovi továreň a ponechajte si vrátený token RAII.

Ukotviteľné panely

ctx.docks() pridáva ukotviteľný bočný panel. Verejná plocha zámerne odovzdáva widget ako nepriehľadný void* — hostiteľ postavený na Qt ho pretypuje na QWidget*; zásuvný modul v Pythone namiesto toho zostaví ten istý panel deklaratívne (pozri referenciu ctx.docks pre Python).

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

Hostiteľ panel vytvára až v prípade potreby (pri prvom zobrazení) a po obnovení rozloženia ho môže vytvoriť znova — stav jednotlivých panelov preto držte mimo widgetu, prípadne ho v create() zostavte nanovo. DockToken panel pri zničení odstráni.

Pixely, alebo podiel okna — proportionalSize

0 (predvolená hodnota) je obyčajné správanie: panel si drží veľkosť v pixeloch a zmena veľkosti okna sa ho nedotkne. To chce nástrojový panel — prehľad dokumentu alebo zoznam výsledkov je pri jednej šírke rovnako užitočný, nech okno robí čokoľvek. Hodnota z intervalu (0, 1) naopak vyhlasuje predvolený podiel hlavného okna: jeho šírky v oblastiach Left/Right, jeho výšky v Top/Bottom. To chce náhľad — okno maximalizujte a vykreslená stránka sa s ním rozšíri.

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

Je to obyčajné číslo práve preto, aby rovnaký kontrakt mohol naplniť aj iný backend než v C++ (pythonový backend ho prijíma ako proportional_size). Čo okolo toho hostiteľ zaručuje:

  • Vy dodávate len predvolenú hodnotu. Vo chvíli, keď používateľ pretiahne oddeľovač panela, hostiteľ prijme podiel, ktorý si zvolil, zapamätá si ho spolu s rozložením okna a vaša hodnota sa na ten panel už nikdy nevzťahuje.
  • Hodnoty mimo rozsah sa obmedzia, nie odmietnu — podiel sa drží v rozumnom pásme a tak váš panel, ako aj editor si podržia minimálny rozmer, takže ani jeden nemožno zložiť. Negatívna alebo nekonečná hodnota sa číta ako 0.
  • Vyhráva vlastné minimum vášho panela. Hostiteľ nikdy nežiada od rozloženia menej, než na čo sa dá widget stlačiť; panel sa na tejto hranici zastaví bez toho, aby podiel zabudol, a vráti sa k nemu, len čo sa okno opäť rozšíri. Panel, ktorý má byť proporcionálny, by sa preto mal vedieť preskladať — panel nástrojov, ktorý svoje skupiny skladá pod seba namiesto toho, aby vyžadoval jediný riadok, ponecháva panel zúžiteľný.
  • Plávajúce a skryté panely sa preskakujú. Plávajúci panel je vlastným oknom a drží si svoju veľkosť; skrytému sa veľkosť pridelí, až keď sa zobrazí, a znova ukotvenému, až keď je späť.
  • Panely v kartách zdieľajú jedno miesto, takže ho riadi panel vpredu — ten, na ktorý sa používateľ pozerá, je ten, ktorého podiel niečo znamená. Dva panely vedľa seba na jednej osi zdieľajú jediný rozpočet a škálujú sa spoločne so zachovaním svojho pomeru.

Panel zaregistrovaný s initiallyVisible = false sa neskôr zobrazí pomocou setPanelVisible — zvyčajne z príkazu, ktorý ho „otvára“. Zobrazenie panel zároveň vynesie nad panely, s ktorými zdieľa oblasť, takže naozaj skončí pred používateľom. Volanie je idempotentné (tú istú hodnotu môžete pokojne opakovať) a pri tokene, ktorý už nie je živý, je tichou prázdnou operáciou, takže niet čo sledovať a netreba siahať po žiadnom QDockWidget:

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

isPanelVisible odpovedá, či je panel v rozložení okna otvorený — stav, ktorý zapisuje setPanelVisible a ktorý prepína prepínač alebo zatváracie tlačidlo používateľa. Neznamená to „sú jeho pixely na obrazovke“: panel schovaný ako karta za iným je otvorený, panel zaregistrovaný ako skrytý a nikdy nezobrazený nie je, a nie je ním ani token, ktorý už nežije. Otázka panel nikdy nevytvorí. Obe volania spolu umožňujú modulu zobraziť svoje panely počas úlohy a potom vrátiť presne to, čo mal používateľ:

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, ktoré patria k sebe — tabGroup

Panely, ktoré uvádzajú rovnakú neprázdnu tabGroup a rovnakú preferredArea, zdieľajú jeden dok ako karty, namiesto aby ležali vedľa seba — päť panelov ladiaceho programu sa zmení na jeden pás kariet pri dolnom okraji okna. Dajte identifikátoru menný priestor ako identifikátoru panela ("myplugin/panels"); rovnaký identifikátor v dvoch oblastiach vytvorí dva zásobníky a modul sa môže zámerne pripojiť ku skupine iného modulu. Karty sa zobrazujú v poradí registrácie; prvý otvorený panel je vpredu, kým používateľ alebo setPanelVisible neprenesie dopredu iný.

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

Skupina je len predvolené usporiadanie. Rozloženie uložené používateľom má prednosť: panel, ktorý používateľ vytiahol zo zásobníka, presunul do inej oblasti alebo do plávajúceho okna, tam zostane aj po reštarte. Panel, ktorý uložené rozloženie ešte nepozná — nový v tejto verzii vášho modulu —, sa pripojí k svojej skupine.

Plnohodnotné typy kariet

Keď si funkcia zaslúži vlastný druh karty (výsledky porovnania, zobrazenie logu, prehliadač šestnástkových údajov) namiesto textového bufferu či bočného panela, zaregistrujte typ karty cez 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 */});

Hostiteľ umiestni vaše IDocumentTab do pásu kariet vedľa kariet editora a smeruje k nemu ukladanie a zatváranie. openTab() vracia popisovač DocumentTabId (0, keď sa nič neotvorilo — neznámy typ, prázdny výsledok továrne); id sú počas života karty stabilné a v rámci jedného behu sa nikdy nepoužijú znova, takže sa zastaraný popisovač číta ako zatvorený, a nie ako cudzia karta. Ponechajte si ho, ak chcete kartu ďalej sledovať, prípadne ho zahoďte pri otvorení bez ďalšej starosti:

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

Zvyklosťou „otvoriť alebo zobraziť“ je zavolať activateTab(), keď isTabOpen() hovorí, že je popisovač stále živý, a inak openTab(). Karty, ktoré vracajú neprázdne sessionParams(), zachytáva uchovávanie relácie a pri obnove ich prehrá cez openTab(); ak v čase obnovy chýba vlastniaci zásuvný modul, zhrnie hostiteľ osirené položky do jedinej správy namiesto toho, aby ich zahodil. Vlastné príklady: Porovnanie a Analyzátor logov.

Zobrazenie porovnania

ctx.compare() otvára dvojtablové okno porovnania editora pre ľubovoľnú dvojicu zdrojov — cesty k súborom, otvorené dokumenty alebo text v pamäti:

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

Volať sa dá vždy bezpečne: hostiteľ bez zobrazenia porovnania poskytne prázdnu implementáciu, ktorá vracia false.

Stavový riadok

Dva nezávislé kanály — dočasné upozornenia a trvalé segmenty vlastnené zásuvným modulom:

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

Upozornenie s vyššou závažnosťou to na obrazovke okamžite nahradí; upozornenie s nižšou závažnosťou sa môže krátko zadržať, aby Error hneď nezmazalo rutinné Info. Obnovenie segmentu nikdy neprebije upozornenie a naopak.

Kontextová ponuka editora

Na rozdiel od IMenuRegistry — statických položiek panela ponuky viazaných na príkazy — sú položky kontextovej ponuky dynamické: pri každom kliknutí pravým tlačidlom do textovej oblasti — alebo do okraja vedľa nej — sa hostiteľ spýta každého zaregistrovaného poskytovateľa, čo na mieste kliknutia platí, a odpovede pripojí pod vstavané akcie úprav (Späť/Znova, Vystrihnúť/Kopírovať/Prilepiť, Vybrať všetko). Požiadavka hovorí, kde ku kliknutiu 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 pri kliknutí do ľubovoľného stĺpca naľavo od textu (čísla riadkov, stĺpec záložiek, stĺpec zbaľovania) a inak Text; ponuka otvorená z klávesnice je vždy Text.
  • line je riadok, na ktorý sa kliklo (pri ponuke z klávesnice riadok kurzora), takže position nikdy nemusíte prevádzať sami.
  • position je posun v bajtoch pod kliknutím v texte (pri ponuke z klávesnice kurzor). Pod kliknutím do okraja nie je žiadny znak, takže tam je position posun začiatku riadka line — platný, ale nikdy z neho nečítajte stĺpec.

Rozhodnite pre každú oblasť zvlášť, čo ponúkate: položky na úrovni riadka (Add Conditional Breakpoint…, Revert Hunk) patria do okraja, položky na úrovni slova do textu. Kontrola pravopisu napríklad pri kliknutí do okraja neponúka nič.

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

Poskytovatelia bežia vo vlákne rozhrania, kým používateľ čaká na ponuku — odpovedajte zo stavu v pamäti a nikdy neblokujte. Názvy položiek sú dynamické reťazce, ktoré si zásuvný modul prekladá sám. Kontrola pravopisu to využíva pre svoje položky s návrhmi, Pridať do slovníka a Ignorovať v tejto relácii.

Paleta motívu

Zásuvné moduly, ktoré vykresľujú vlastné farby, registrujú pomenované kategórie so svetlými a tmavými predvoľbami a pri vykresľovaní ich vyhodnocujú na RGB — farby nikdy nepíšte natvrdo:

// 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 vracia prekrytie z motívu, ak existuje, inak zaregistrovanú predvoľbu pre aktívny variant a 0x000000 pri nezaregistrovanom id (berte to ako „použi moju vlastnú záložnú hodnotu“).

Každá zaregistrovaná kategória sa automaticky objaví v Editore motívov a prekrytia používateľa putujú tam a späť v súbore motívu pod kľúčom pluginColors. Prekresľujte pri events::ThemeChanged.