Doky, karty, stavový řádek a motiv¶
Plochy rozhraní, do kterých může zásuvný modul přispívat, nad rámec nabídek. Všechny se řídí týmž vzorem: popište, co chcete, daty neutrálními vůči backendu, předejte hostiteli továrnu a podržte si vrácený token RAII.
Ukotvitelné panely¶
ctx.docks() přidává ukotvitelný postranní panel. Veřejná plocha záměrně předává widget jako
neprůhledný void* — hostitel postavený na Qt jej přetypuje na QWidget*; modul v Pythonu místo
toho staví tentýž panel deklarativně (viz
reference ctx.docks v Pythonu).
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));
Hostitel panel vytváří líně (při prvním zobrazení) a může jej po obnovení rozvržení vytvořit
znovu — stav panelu proto držte mimo widget, nebo jej v create() znovu sestavte. DockToken
panel při svém zániku odebere.
Pixely, nebo podíl okna — proportionalSize¶
0 (výchozí hodnota) je obyčejné chování: panel si drží velikost v pixelech a změna velikosti okna
se ho nedotkne. To chce nástrojový panel — přehled dokumentu nebo seznam výsledků je při jedné
šířce stejně užitečný, ať okno dělá cokoli. Hodnota z intervalu (0, 1) naproti tomu vyhlašuje
výchozí podíl hlavního okna: jeho šířky v oblastech Left/Right, jeho výšky v Top/Bottom. To
chce náhled — okno maximalizujte a vykreslená stránka se s ním rozšíří.
Je to obyčejné číslo právě proto, aby stejný kontrakt mohl naplnit i jiný backend než v C++
(Pythonový backend jej přijímá jako
proportional_size). Co kolem toho hostitel zaručuje:
- Vy dodáváte jen výchozí hodnotu. Ve chvíli, kdy uživatel přetáhne oddělovač panelu, hostitel přijme podíl, který si zvolil, zapamatuje si jej spolu s rozvržením okna a vaše hodnota se na ten panel už nikdy nevztahuje.
- Hodnoty mimo rozsah se omezí, nikoli odmítnou — podíl se drží v rozumném pásu a jak váš panel,
tak editor si podrží minimální rozměr, takže ani jeden nelze sklapnout. Negativní nebo nekonečná
hodnota se čte jako
0. - Vítězí vlastní minimum vašeho panelu. Hostitel nikdy nežádá od rozvržení méně, než na co lze widget stlačit; panel se na té hranici zastaví, aniž by podíl zapomněl, a vrátí se k němu, jakmile se okno opět rozšíří. Panel, který má být proporcionální, by se proto měl umět přeskládat — lišta, která své skupiny skládá pod sebe místo toho, aby vyžadovala jedinou řadu, nechává panel zužitelný.
- Plovoucí a skryté panely se přeskakují. Plovoucí panel je vlastním oknem a drží si svou velikost; skrytému se velikost přidělí, až se odkryje, a znovu zadokovanému, až je zpět.
- Panely v kartách sdílejí jedno místo, takže jej řídí panel vpředu — ten, na který se uživatel dívá, je ten, jehož podíl něco znamená. Dva panely vedle sebe na jedné ose sdílejí jediný rozpočet a škálují se společně se zachováním svého poměru.
Panel zaregistrovaný s initiallyVisible = false se později odkryje pomocí setPanelVisible —
obvykle z příkazu, který jej »otevírá«. Zobrazení panel zároveň zvedne nad doky, s nimiž sdílí
oblast, takže skutečně skončí před uživatelem. Volání je idempotentní (tutéž hodnotu opakujte,
jak chcete) a je tichou prázdnou operací u tokenu, který už nežije, takže není co sledovat a není
třeba sahat po žádném QDockWidget:
ctx.docks().setPanelVisible(m_dock.id(), true); // reveal + raise
ctx.docks().setPanelVisible(m_dock.id(), false); // hide, keep registered
isPanelVisible odpovídá, zda je panel v rozvržení okna otevřený — stav,
který zapisuje setPanelVisible a který přepíná přepínač nebo zavírací
tlačítko uživatele. Neznamená to »jsou jeho pixely na obrazovce«: panel
schovaný jako karta za jiným je otevřený, panel zaregistrovaný jako skrytý a
nikdy nezobrazený není, a není jím ani token, který už nežije. Dotaz panel
nikdy nevytvoří. Obě volání dohromady umožňují modulu zobrazit své panely po
dobu úlohy a potom vrátit přesně to, co měl uživatel:
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, které patří k sobě — tabGroup¶
Panely, které uvádějí stejnou neprázdnou tabGroup a stejnou
preferredArea, sdílejí jeden dok jako karty, místo aby ležely vedle sebe
— pět panelů ladicího programu se změní v jeden pruh karet u dolního okraje
okna. Dejte identifikátoru jmenný prostor jako identifikátoru panelu
("myplugin/panels"); stejný identifikátor ve dvou oblastech vytvoří dva
zásobníky a modul se může záměrně připojit ke skupině jiného modulu. Karty se
objevují v pořadí registrace; první otevřený panel je vpředu, dokud uživatel
nebo setPanelVisible nepřenese dopředu jiný.
Skupina je jen výchozí uspořádání. Rozvržení uložené uživatelem má přednost: panel, který uživatel vytáhl ze zásobníku, přesunul do jiné oblasti nebo do plovoucího okna, tam zůstane i po restartu. Panel, který uložené rozvržení ještě nezná — nový v této verzi vašeho modulu —, se připojí ke své skupině.
Plnohodnotné typy karet¶
Když si funkce zaslouží vlastní druh karty (výsledky porovnání, zobrazení protokolu, prohlížeč v
šestnáctkové soustavě) spíš než textový buffer či postranní panel, zaregistrujte typ karty pomocí
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 */});
Hostitel váš IDocumentTab umístí do lišty karet po boku karet editoru a směruje na něj ukládání
a zavírání. openTab() vrací popisovač DocumentTabId (0, když se nic neotevřelo — neznámý
typ, nulový výsledek továrny); id jsou po dobu života karty stabilní a v rámci běhu se nikdy
nepoužijí znovu, takže se zastaralý popisovač čte jako zavřený, nikoli jako cizí karta. Podržte si
jej, chcete-li kartu dál sledovat, nebo jej zahoďte pro otevření »spusť a zapomeň«:
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
Idiomem »otevřít nebo odkrýt« je activateTab(), když isTabOpen() říká, že popisovač stále
žije, jinak openTab(). Karty, které vracejí neprázdné sessionParams(), zachycuje uchovávání
relace a při obnovení se přehrají přes openTab(); pokud vlastnící modul v době obnovení chybí,
sbalí hostitel osiřelé položky do jediné zprávy, místo aby je zahodil. Vlastní příklady:
Porovnání souborů a Analyzátor protokolů.
Zobrazení porovnání¶
ctx.compare() otevírá dvoupanelové okno porovnání v editoru pro libovolnou dvojici zdrojů —
cesty k souborům, otevřené dokumenty, nebo text v paměti:
CompareSource left{...}, right{...};
bool shown = ctx.compare().openCompare(left, right, CompareOptions{});
Volat jej je vždy bezpečné: hostitel bez zobrazení porovnání dodá prázdnou implementaci, která
vrací false.
Stavový řádek¶
Dva nezávislé kanály — pomíjivá oznámení a trvalé části vlastněné modulem:
// 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
Oznámení s vyšší závažností to na obrazovce ihned nahradí; oznámení s nižší závažností může být
krátce zadrženo, aby Error okamžitě nesmazalo rutinní Info. Obnovení části nikdy nepřebije
oznámení a naopak.
Místní nabídka editoru¶
Na rozdíl od IMenuRegistry — statických položek nabídkové lišty vázaných na příkazy — jsou
položky místní nabídky dynamické: při každém kliknutí pravým tlačítkem v
textové oblasti — nebo v okraji vedle ní — se hostitel zeptá každého
zaregistrovaného poskytovatele, co platí na místě kliknutí, a odpovědi připojí
pod vestavěné editační akce (Zpět/Znovu, Vyjmout/Kopírovat/Vložit, Vybrat
vše). Požadavek říká, kde ke kliknutí 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;
};
areajeMarginpři kliknutí do libovolného sloupce nalevo od textu (čísla řádků, sloupec záložek, sloupec sbalování) a jinakText; nabídka otevřená z klávesnice je vždyText.lineje řádek, na který se kliklo (u nabídky z klávesnice řádek kurzoru), takžepositionnikdy nemusíte převádět sami.positionje posun v bajtech pod kliknutím v textu (u nabídky z klávesnice kurzor). Pod kliknutím do okraje není žádný znak, takže tam jepositionposun začátku řádkuline— platný, ale nikdy z něj nečtěte sloupec.
Rozhodněte pro každou oblast zvlášť, co nabízíte: položky na úrovni řádku (Add Conditional Breakpoint…, Revert Hunk) patří do okraje, položky na úrovni slova do textu. Kontrola pravopisu například při kliknutí do okraje nenabízí nic.
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
Poskytovatelé běží v uživatelském vlákně, zatímco uživatel na nabídku čeká — odpovídejte ze stavu v paměti, nikdy neblokujte. Názvy položek jsou dynamické řetězce, které si modul překládá sám. Kontrola pravopisu to používá pro své návrhy a položky Přidat do slovníku / Ignorovat v této relaci.
Paleta motivu¶
Moduly, které vykreslují vlastní barvy, registrují pojmenované kategorie s výchozími hodnotami pro světlou a tmavou variantu a vyhodnocují je na RGB až při vykreslování — barvy nikdy nezadrátovávejte:
// 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 vrací přebití z motivu, je-li přítomné, jinak zaregistrovanou výchozí hodnotu pro
aktivní variantu, a 0x000000 u nezaregistrovaného id (to berte jako »použij mou vlastní záložní
hodnotu«).
Každá zaregistrovaná kategorie se automaticky objeví v Editoru motivů a přebití od uživatele se
obousměrně přenášejí v souboru motivu pod pluginColors. Překreslujte při
events::ThemeChanged.