Przejdź do treści

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.

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

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.

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

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;
};
  • area to Margin przy kliknięciu w dowolnej kolumnie na lewo od tekstu (numery wierszy, kolumna zakładek, kolumna zwijania), a w pozostałych przypadkach Text; menu otwarte z klawiatury ma zawsze Text.
  • line to kliknięty wiersz (wiersz kursora przy menu z klawiatury), więc nie musisz sam przeliczać position.
  • position to 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 tam position to przesunięcie początku wiersza line — 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.