Vai al contenuto

Pannelli, schede, barra di stato e tema

Le superfici dell'interfaccia a cui un plugin può contribuire, oltre ai menu. Tutte seguono lo stesso schema: descrivi ciò che vuoi con dati neutri rispetto al backend, consegna all'host una fabbrica, conserva il token RAII restituito.

Pannelli ancorabili

ctx.docks() aggiunge un pannello laterale ancorabile. La superficie pubblica passa deliberatamente il widget come un void* opaco: l'host basato su Qt lo converte in QWidget*; un plugin Python costruisce invece lo stesso pannello in modo dichiarativo (vedi la documentazione di ctx.docks per 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));

L'host istanzia il pannello in modo pigro (alla prima visualizzazione) e può ricrearlo dopo un azzeramento della disposizione: tieni lo stato di ogni pannello fuori dal widget oppure ricostruiscilo in create(). Il DockToken rimuove il pannello quando viene distrutto.

Pixel o una quota della finestra — proportionalSize

0 (il valore predefinito) è il comportamento semplice: il pannello mantiene una dimensione in pixel, e ridimensionare la finestra non lo tocca. È ciò che vuole un pannello strumento: una struttura o un elenco di risultati è utile allo stesso modo a una certa larghezza qualunque cosa faccia la finestra. Un valore in (0, 1) dichiara invece una quota predefinita della finestra principale: della sua larghezza nelle aree Left/Right, della sua altezza in Top/Bottom. È ciò che vuole un'anteprima: ingrandisci la finestra e la pagina resa si allarga con essa.

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

È un semplice numero proprio perché un backend diverso dal C++ possa adempiere allo stesso contratto (il backend Python lo riceve come proportional_size). Ciò che l'host garantisce attorno:

  • Tu fornisci soltanto il valore predefinito. Nel momento in cui l'utente trascina il separatore del pannello, l'host adotta la quota che ha scelto, la ricorda con la disposizione della finestra, e il tuo valore non si applica mai più a quel pannello.
  • I valori fuori intervallo vengono limitati, non rifiutati: la quota è tenuta entro una fascia sensata, e sia il tuo pannello sia l'editor conservano un'estensione minima, così nessuno dei due può essere ridotto a nulla. Un valore negativo o non finito si legge come 0.
  • Vince il minimo del tuo pannello. L'host non chiede mai alla disposizione meno di quanto il widget possa essere compresso; il pannello si ferma a quel limite senza dimenticare la quota, e vi ritorna quando la finestra viene allargata di nuovo. Un pannello pensato per essere proporzionale dovrebbe quindi poter riorganizzarsi: una barra degli strumenti che impila i suoi gruppi invece di pretendere una sola riga lascia il pannello restringibile.
  • I pannelli mobili e nascosti vengono saltati. Un pannello mobile è una finestra a sé e mantiene la propria dimensione; uno nascosto viene dimensionato quando è mostrato, uno riagganciato quando è tornato al suo posto.
  • I pannelli a schede condividono un unico posto, quindi lo governa quello in primo piano: quello che l'utente guarda è quello la cui quota significa qualcosa. Due pannelli affiancati su uno stesso asse condividono un solo budget e vengono scalati insieme, conservandone il rapporto.

Un pannello registrato con initiallyVisible = false viene mostrato in seguito con setPanelVisible — di norma dal comando che lo «apre». Mostrarlo lo porta anche davanti ai pannelli con cui condivide un'area, così finisce davvero davanti all'utente. La chiamata è idempotente (ripeti pure lo stesso valore) ed è un'operazione nulla e silenziosa per un token non più vivo, quindi non c'è nulla da tracciare e nessun QDockWidget da andare a cercare:

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

isPanelVisible risponde se un pannello è aperto nel layout della finestra: lo stato che setPanelVisible scrive e che l'interruttore o il pulsante di chiusura dell'utente commuta. Non significa «i suoi pixel sono sullo schermo»: un pannello in una scheda dietro un altro è aperto, un pannello registrato nascosto e mai mostrato non lo è, e nemmeno un token non più vivo. Chiedere non crea mai il pannello. Insieme, le due chiamate permettono a un plugin di mostrare i propri pannelli per la durata di un'attività e poi di rimettere esattamente ciò che l'utente aveva:

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

Pannelli che vanno insieme — tabGroup

I pannelli che indicano lo stesso tabGroup non vuoto e la stessa preferredArea condividono un unico pannello ancorato, a schede, invece di stare uno accanto all'altro: cinque pannelli di un debugger diventano un'unica striscia di schede in fondo alla finestra. Dai all'id uno spazio dei nomi come a un id di pannello ("myplugin/panels"); lo stesso id in due aree forma due pile, e un plugin può unirsi di proposito al gruppo di un altro plugin. Le schede compaiono in ordine di registrazione; il primo pannello aperto resta davanti finché l'utente o setPanelVisible non ne porta in primo piano un altro.

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

Il gruppo è solo la disposizione predefinita. Un layout salvato dall'utente ha la precedenza: un pannello che l'utente ha trascinato fuori dalla pila, in un'altra area o in una finestra mobile resta lì da un riavvio all'altro. Un pannello che il layout salvato non conosce ancora — nuovo in questa versione del tuo plugin — si unisce al suo gruppo.

Tipi di scheda di primo livello

Quando una funzionalità merita un proprio tipo di scheda (risultati di un diff, una vista di log, un visualizzatore esadecimale) anziché un buffer di testo o un pannello laterale, registra un tipo di scheda con 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 */});

L'host colloca il tuo IDocumentTab nella striscia delle schede accanto alle schede dell'editor e vi instrada salvataggio e chiusura. openTab() restituisce un handle DocumentTabId (0 quando non si è aperto nulla — tipo sconosciuto, risultato nullo della fabbrica); gli id sono stabili per tutta la vita della scheda e non vengono mai riusati all'interno di un'esecuzione, quindi un handle obsoleto viene letto come «chiuso» e non come la scheda di qualcun altro. Conservalo per seguire la scheda in seguito, oppure scartalo per un'apertura mordi-e-fuggi:

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

L'idioma «apri o mostra» è activateTab() quando isTabOpen() dice che l'handle è ancora vivo, altrimenti openTab(). Le schede che restituiscono un sessionParams() non vuoto vengono catturate dalla persistenza di sessione e riprodotte tramite openTab() al ripristino; se al momento del ripristino il plugin proprietario manca, l'host raccoglie le voci orfane in un unico messaggio invece di scartarle. Esempi di prima parte: Confronto diff e l'Analizzatore di log.

La vista di confronto

ctx.compare() apre la finestra di confronto a due riquadri dell'editor per qualsiasi coppia di sorgenti — percorsi di file, documenti aperti o testo in memoria:

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

È sempre sicuro chiamarla: un host senza vista di confronto fornisce un'operazione nulla che restituisce false.

Barra di stato

Due canali indipendenti: notifiche temporanee e segmenti persistenti posseduti dal plugin:

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

Una notifica di gravità più alta sostituisce immediatamente quella sullo schermo; una di gravità più bassa può essere trattenuta brevemente, così che un Error non venga cancellato all'istante da un Info di routine. Aggiornare un segmento non calpesta mai una notifica, e viceversa.

Menu contestuale dell'editor

A differenza di IMenuRegistry — voci statiche della barra dei menu legate a comandi — le voci del menu contestuale sono dinamiche: a ogni clic destro nell'area di testo — o nel margine accanto — l'host chiede a ciascun fornitore registrato che cosa si applica al punto cliccato e accoda le risposte sotto le azioni di modifica integrate (Annulla/Ripristina, Taglia/Copia/Incolla, Seleziona tutto). La richiesta dice dove è avvenuto il clic:

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 è Margin per un clic in una qualsiasi colonna a sinistra del testo (numeri di riga, la colonna dei segnalibri, la colonna di piegatura) e Text negli altri casi; un menu aperto dalla tastiera è sempre Text.
  • line è la riga cliccata (la riga del cursore per un menu da tastiera), quindi non devi mai convertire position da solo.
  • position è l'offset in byte sotto il clic nel testo (il cursore per un menu da tastiera). Sotto un clic nel margine non c'è alcun carattere, quindi lì position è l'offset dell'inizio di line: valido, ma non ricavarne mai una colonna.

Decidi per area che cosa offrire: le voci a livello di riga (Add Conditional Breakpoint…, Revert Hunk) appartengono al margine, quelle a livello di parola al testo. Il controllo ortografico, per esempio, non offre nulla per un clic nel margine.

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

I fornitori girano sul thread dell'interfaccia mentre l'utente aspetta il menu: rispondi da stato in memoria, non bloccare mai. I titoli delle voci sono stringhe dinamiche che il plugin traduce da sé. Il correttore ortografico usa questo meccanismo per le proprie voci di suggerimento / Aggiungi al dizionario / Ignora per questa sessione.

Tavolozza del tema

I plugin che disegnano colori propri registrano categorie con nome con valori predefiniti chiari/scuri e li risolvono in RGB al momento del disegno: non scrivere mai i colori nel codice:

// 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 restituisce la sostituzione del tema quando c'è, altrimenti il valore predefinito registrato per la variante attiva, e 0x000000 per un id non registrato (interpretalo come «usa il mio ripiego»).

Ogni categoria registrata compare automaticamente nell'Editor dei temi, e le sostituzioni dell'utente fanno andata e ritorno nel file del tema sotto pluginColors. Ridisegna su events::ThemeChanged.