Doki, karty, pasek stanu i motyw¶
Powierzchnie UI, do których wtyczka może kontrybuować poza menu. Wszystkie działają według tego samego wzorca: opisz, czego chcesz, danymi niezależnymi od backendu, przekaż hostowi fabrykę, zatrzymaj zwrócony token RAII.
Panele dokowane¶
ctx.docks() dodaje dokowany panel boczny. Publiczna powierzchnia celowo
przekazuje widżet jako nieprzezroczysty void* — host oparty na Qt rzutuje go
na QWidget*; wtyczka w Pythonie buduje ten sam panel deklaratywnie
(zobacz dokumentację ctx.docks w Pythonie).
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));
Host tworzy panel leniwie (przy pierwszym wyświetleniu) i może utworzyć go
ponownie po resecie układu — trzymaj stan panelu poza widżetem albo odbudowuj
go w create(). DockToken usuwa panel przy destrukcji.
Piksele albo udział w oknie — proportionalSize¶
0 (wartość domyślna) to zwykłe zachowanie: panel trzyma rozmiar w pikselach, a zmiana
rozmiaru okna go nie rusza. Tego chce panel narzędziowy — struktura dokumentu czy
lista wyników jest równie użyteczna przy jednej szerokości, niezależnie od tego, co robi
okno. Wartość z przedziału (0, 1) deklaruje natomiast domyślny udział w oknie
głównym: jego szerokości w obszarach Left/Right, jego wysokości w Top/Bottom. Tego
chce podgląd — zmaksymalizuj okno i wyrenderowana strona poszerzy się razem z nim.
To zwykła liczba właśnie dlatego, żeby ten sam kontrakt mógł wypełnić backend inny niż
C++ (backend Pythona przyjmuje ją jako
proportional_size). Co wokół niej gwarantuje
host:
- Ty podajesz tylko wartość domyślną. W chwili, gdy użytkownik przeciągnie separator panelu, host przyjmuje wybrany przez niego udział, pamięta go wraz z układem okna, a Twoja wartość nigdy więcej nie dotyczy tego panelu.
- Wartości spoza zakresu są przycinane, nie odrzucane — udział trzymany jest w
rozsądnym przedziale, a i panel, i edytor zachowują minimalny rozmiar, więc żadnego z
nich nie da się zwinąć. Wartość ujemna lub nieskończona czytana jest jako
0. - Minimum Twojego panelu wygrywa. Host nigdy nie żąda od układu mniej, niż widżet da się ścisnąć; panel zatrzymuje się na tej granicy, nie zapominając udziału, i wraca do niego, gdy okno znów się poszerzy. Panel, który ma być proporcjonalny, powinien więc umieć się przeformować — pasek narzędzi, który układa swoje grupy jedna pod drugą, zamiast domagać się jednego wiersza, pozwala panel zwężać.
- Panele pływające i ukryte są pomijane. Panel pływający jest własnym oknem i trzyma swój rozmiar; ukryty dostaje rozmiar, gdy zostanie odsłonięty, a zadokowany ponownie — gdy wróci.
- Panele w kartach dzielą jedno miejsce, więc steruje nim panel na wierzchu — ten, na który patrzy użytkownik, jest tym, którego udział cokolwiek znaczy. Dwa panele obok siebie na jednej osi dzielą jeden budżet i są skalowane razem, z zachowaniem proporcji.
Panel zarejestrowany z initiallyVisible = false odsłaniasz później przez
setPanelVisible — zwykle z polecenia, które go „otwiera". Pokazanie
podnosi też panel ponad doki dzielące z nim obszar, więc naprawdę trafia
przed oczy użytkownika. Wywołanie jest idempotentne (tę samą wartość możesz
powtarzać dowolnie) i cicho ignorowane dla tokenu, który już nie żyje — nic
nie trzeba śledzić i nie trzeba sięgać po QDockWidget:
ctx.docks().setPanelVisible(m_dock.id(), true); // reveal + raise
ctx.docks().setPanelVisible(m_dock.id(), false); // hide, keep registered
isPanelVisible mówi, czy panel jest otwarty w układzie okna — to ten
stan, który zapisuje setPanelVisible, a przełącza przełącznik użytkownika
lub przycisk zamknięcia. Nie chodzi o to, czy jego piksele są na ekranie:
panel schowany jako karta za innym jest otwarty, panel zarejestrowany jako
ukryty i nigdy niepokazany nie jest, podobnie jak token, który już nie żyje.
Samo pytanie nigdy nie tworzy panelu. Razem te dwa wywołania pozwalają
wtyczce pokazać swoje panele na czas zadania, a potem przywrócić dokładnie
to, co miał użytkownik:
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);
Panele, które należą do siebie — tabGroup¶
Panele, które podają ten sam niepusty tabGroup i ten sam preferredArea,
dzielą jeden dok jako karty zamiast leżeć obok siebie — pięć paneli
debuggera staje się jednym paskiem kart u dołu okna. Nadaj identyfikatorowi
przestrzeń nazw jak identyfikatorowi panelu ("myplugin/panels"); ten sam
identyfikator w dwóch obszarach tworzy dwa stosy, a wtyczka może celowo
dołączyć do grupy innej wtyczki. Karty pojawiają się w kolejności rejestracji;
pierwszy otwarty panel jest na wierzchu, dopóki użytkownik albo
setPanelVisible nie wysunie innego.
Grupa to tylko układ domyślny. Wygrywa układ zapisany przez użytkownika: panel, który użytkownik wyciągnął ze stosu, przeniósł do innego obszaru albo do pływającego okna, zostaje tam po ponownym uruchomieniu. Panel, którego zapisany układ jeszcze nie zna — nowy w tej wersji Twojej wtyczki — dołącza do swojej grupy.
Typy kart pierwszej klasy¶
Gdy funkcja zasługuje na własny rodzaj karty (wyniki diffa, widok logów,
podgląd heksadecymalny), a nie bufor tekstowy czy panel boczny, zarejestruj typ
karty przez 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 */});
Host umieszcza Twój IDocumentTab w pasku kart obok kart edytora i kieruje do
niego zapis/zamykanie. openTab() zwraca uchwyt DocumentTabId (0, gdy nic
się nie otworzyło — nieznany typ, fabryka zwróciła null); id są stałe przez
życie karty i nigdy nie są ponownie używane w ramach jednego uruchomienia,
więc nieaktualny uchwyt czyta się jako „zamknięta", a nie jako cudza karta.
Zachowaj go, żeby śledzić kartę dalej, albo odrzuć przy otwarciu typu
„odpal i zapomnij":
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
Idiom „otwórz albo pokaż" to activateTab(), gdy isTabOpen() mówi, że
uchwyt wciąż żyje, w przeciwnym razie openTab(). Karty zwracające niepuste sessionParams() są
przechwytywane przez utrwalanie sesji i odtwarzane przez openTab() przy
przywracaniu; jeśli w chwili przywracania brakuje wtyczki-właściciela, host
zbiera osierocone wpisy w jeden komunikat, zamiast je porzucać. Przykłady we
wtyczkach własnych projektu: Diff Compare i Log Analyzer.
Widok porównania¶
ctx.compare() otwiera dwupanelowe okno porównania edytora dla dowolnej pary
źródeł — ścieżek plików, otwartych dokumentów lub tekstu w pamięci:
CompareSource left{...}, right{...};
bool shown = ctx.compare().openCompare(left, right, CompareOptions{});
Zawsze bezpieczne do wywołania: host bez widoku porównania dostarcza no-op
zwracający false.
Pasek stanu¶
Dwa niezależne kanały — tymczasowe powiadomienia oraz trwałe segmenty należące do wtyczki:
// 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
Powiadomienie o wyższej wadze natychmiast zastępuje to widoczne na ekranie;
powiadomienie o niższej wadze może zostać na chwilę wstrzymane, aby Error nie
został od razu zmazany przez rutynowe Info. Odświeżanie segmentu nigdy nie
nadpisuje powiadomienia i odwrotnie.
Menu kontekstowe edytora¶
W odróżnieniu od IMenuRegistry — statycznych pozycji paska menu powiązanych z
poleceniami — pozycje menu kontekstowego są dynamiczne: przy każdym kliknięciu
prawym przyciskiem w obszarze tekstu — albo na marginesie obok niego — host pyta
każdego zarejestrowanego providera, co ma zastosowanie w klikniętym miejscu, i
dokleja odpowiedzi poniżej wbudowanych akcji edycji (Cofnij/Ponów,
Wytnij/Kopiuj/Wklej, Zaznacz wszystko). Żądanie mówi, gdzie nastąpiło
kliknięcie:
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;
};
areatoMarginprzy kliknięciu w dowolnej kolumnie na lewo od tekstu (numery wierszy, kolumna zakładek, kolumna zwijania), a w pozostałych przypadkachText; menu otwarte z klawiatury ma zawszeText.lineto kliknięty wiersz (wiersz kursora przy menu z klawiatury), więc nie musisz sam przeliczaćposition.positionto przesunięcie w bajtach pod kliknięciem w tekście (kursor przy menu z klawiatury). Pod kliknięciem na marginesie nie ma żadnego znaku, więc tampositionto przesunięcie początku wierszaline— poprawne, ale nie odczytuj z niego kolumny.
Decyduj osobno dla każdego obszaru, co oferujesz: pozycje dotyczące wiersza (Add Conditional Breakpoint…, Revert Hunk) należą do marginesu, a te dotyczące słowa — do tekstu. Sprawdzanie pisowni, na przykład, przy kliknięciu na marginesie nie oferuje niczego.
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
Providery działają w wątku UI, podczas gdy użytkownik czeka na menu — odpowiadaj ze stanu w pamięci, nigdy nie blokuj. Tytuły pozycji to dynamiczne stringi, które wtyczka tłumaczy samodzielnie. Sprawdzanie pisowni używa tego do swoich podpowiedzi / pozycji Dodaj do słownika / Ignoruj w tej sesji.
Paleta motywu¶
Wtyczki malujące własne kolory rejestrują nazwane kategorie z domyślnymi wartościami dla motywu jasnego/ciemnego i rozwiązują je do RGB w momencie malowania — nigdy nie hardkoduj kolorów:
// 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 zwraca nadpisanie z motywu, jeśli istnieje, w przeciwnym razie
zarejestrowaną wartość domyślną dla aktywnego wariantu, a 0x000000 dla
niezarejestrowanego id (traktuj to jako „użyj własnego fallbacku").
Każda zarejestrowana kategoria automatycznie pojawia się w edytorze motywów,
a nadpisania użytkownika wędrują w obie strony w pliku motywu pod
pluginColors. Przemaluj przy events::ThemeChanged.