Panneaux, onglets, barre d'état et thème¶
Les surfaces d'interface auxquelles un plugin peut contribuer, au-delà des menus. Toutes suivent le même schéma : décrivez ce que vous voulez sous forme de données neutres vis-à-vis du backend, confiez une fabrique à l'hôte, conservez le jeton RAII renvoyé.
Panneaux ancrables¶
ctx.docks() ajoute un panneau latéral ancrable. La surface publique passe
délibérément le widget sous forme de void* opaque — l'hôte basé sur Qt le
convertit en QWidget* ; un plugin Python construit le même panneau de façon
déclarative (voir la référence Python ctx.docks).
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'hôte instancie le panneau paresseusement (au premier affichage) et peut
le réinstancier après une réinitialisation de la disposition — conservez
l'état propre au panneau hors du widget ou reconstruisez-le dans create().
Le DockToken supprime le panneau à sa destruction.
Des pixels ou une part de la fenêtre — proportionalSize¶
0 (le défaut) est le comportement simple : le panneau conserve une taille en pixels, et
un redimensionnement de la fenêtre le laisse tranquille. C'est ce que veut un panneau
outil — un plan de document ou une liste de résultats est aussi utile à une largeur
donnée quoi que fasse la fenêtre. Une valeur dans (0, 1) déclare au contraire une part
par défaut de la fenêtre principale : de sa largeur dans les zones Left/Right, de sa
hauteur dans Top/Bottom. C'est ce que veut un aperçu : maximisez la fenêtre et la
page rendue s'élargit avec elle.
C'est un simple nombre précisément pour qu'un backend autre que C++ puisse remplir le même
contrat (le backend Python le prend comme
proportional_size). Ce que l'hôte garantit
autour :
- Vous ne fournissez que le défaut. Dès que l'utilisateur fait glisser le séparateur du panneau, l'hôte adopte la part qu'il a choisie, la mémorise avec la disposition de la fenêtre, et votre valeur ne s'applique plus jamais à ce panneau.
- Les valeurs hors plage sont bornées, pas rejetées — la part est maintenue dans une
fourchette raisonnable, et votre panneau comme l'éditeur gardent une étendue minimale, si
bien qu'aucun des deux ne peut être réduit à rien. Une valeur négative ou non finie se
lit comme
0. - Le minimum propre de votre panneau l'emporte. L'hôte ne demande jamais à la disposition moins que ce à quoi le widget peut être comprimé ; le panneau s'arrête à ce plancher sans oublier la part, et y revient quand la fenêtre s'élargit de nouveau. Un panneau destiné à être proportionnel doit donc pouvoir se réorganiser — une barre d'outils qui empile ses groupes au lieu d'exiger une seule rangée reste rétrécissable.
- Les panneaux flottants et masqués sont ignorés. Un panneau flottant est sa propre fenêtre et garde sa taille ; un panneau masqué est dimensionné quand il est révélé, un panneau réancré quand il est de retour.
- Les panneaux en onglets partagent un seul emplacement : c'est donc celui au premier plan qui le pilote — celui que l'utilisateur regarde est celui dont la part signifie quelque chose. Deux panneaux côte à côte sur un même axe partagent un budget unique et sont mis à l'échelle ensemble, en conservant leur rapport.
Un panneau enregistré avec initiallyVisible = false est révélé plus tard
avec setPanelVisible — typiquement depuis la commande qui « l'ouvre ».
L'afficher le remonte aussi au-dessus des docks qui partagent sa zone, pour
qu'il arrive vraiment sous les yeux de l'utilisateur. L'appel est idempotent
(répétez la même valeur librement) et un no-op silencieux pour un jeton qui
ne vit plus — rien à suivre et aucun QDockWidget à aller chercher :
ctx.docks().setPanelVisible(m_dock.id(), true); // reveal + raise
ctx.docks().setPanelVisible(m_dock.id(), false); // hide, keep registered
isPanelVisible indique si un panneau est ouvert dans la disposition de la
fenêtre — l'état qu'écrit setPanelVisible et que bascule l'interrupteur ou le
bouton de fermeture de l'utilisateur. Ce n'est pas « ses pixels sont-ils à
l'écran » : un panneau en onglet derrière un autre est ouvert, un panneau
enregistré masqué et jamais affiché ne l'est pas, pas plus qu'un jeton qui
n'est plus vivant. La question ne crée jamais le panneau. Ensemble, les deux
appels permettent à un plugin d'afficher ses panneaux le temps d'une tâche,
puis de remettre exactement ce que l'utilisateur avait :
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);
Des panneaux qui vont ensemble — tabGroup¶
Les panneaux qui indiquent le même tabGroup non vide et la même
preferredArea partagent un seul panneau ancré, sous forme d'onglets, au
lieu de se placer côte à côte — cinq panneaux de débogueur deviennent une
seule bande d'onglets en bas de la fenêtre. Préfixez l'identifiant comme un
identifiant de panneau ("myplugin/panels") ; le même identifiant dans deux
zones forme deux piles, et un plugin peut rejoindre volontairement le groupe
d'un autre plugin. Les onglets apparaissent dans l'ordre d'enregistrement ; le
premier panneau ouvert est au premier plan jusqu'à ce que l'utilisateur ou
setPanelVisible en fasse passer un autre devant.
Le groupe n'est que la disposition par défaut. Une disposition enregistrée par l'utilisateur l'emporte : un panneau que l'utilisateur a sorti de la pile, déplacé vers une autre zone ou dans une fenêtre flottante y reste d'un redémarrage à l'autre. Un panneau que la disposition enregistrée ne connaît pas encore — nouveau dans cette version de votre plugin — rejoint son groupe.
Types d'onglets de première classe¶
Quand une fonctionnalité mérite son propre type d'onglet (résultats de diff,
vue de log, visionneuse hexadécimale) plutôt qu'un tampon texte ou un panneau
latéral, enregistrez un type d'onglet auprès de 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'hôte place votre IDocumentTab dans la barre d'onglets aux côtés des
onglets d'édition et lui achemine les opérations d'enregistrement/fermeture.
openTab() renvoie un handle DocumentTabId (0 si rien ne s'est ouvert —
type inconnu, fabrique ayant renvoyé null) ; les identifiants sont stables
pour la vie de l'onglet et jamais réutilisés au cours d'une exécution, si
bien qu'un handle périmé se lit comme « fermé » et jamais comme l'onglet de
quelqu'un d'autre. Gardez-le pour suivre l'onglet ensuite, ou ignorez-le pour
une ouverture « lancer et oublier » :
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'idiome « ouvrir ou révéler » est activateTab() quand isTabOpen() dit
que le handle est encore vivant, sinon openTab().
Les onglets qui renvoient un sessionParams() non vide sont capturés par la
persistance de session et rejoués via openTab() à la restauration ; si le
plugin propriétaire est absent au moment de la restauration, l'hôte regroupe
les entrées orphelines dans un seul message au lieu de les abandonner.
Exemples internes : Diff Compare et le Log Analyzer.
La vue de comparaison¶
ctx.compare() ouvre la fenêtre de comparaison à deux volets de l'éditeur
pour n'importe quelle paire de sources — chemins de fichiers, documents
ouverts ou texte en mémoire :
CompareSource left{...}, right{...};
bool shown = ctx.compare().openCompare(left, right, CompareOptions{});
Toujours sûr à appeler : un hôte sans vue de comparaison fournit un no-op qui
renvoie false.
Barre d'état¶
Deux canaux indépendants — notifications transitoires et segments persistants possédés par le 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
Une notification de sévérité supérieure remplace immédiatement celle à
l'écran ; une notification de sévérité inférieure peut être brièvement
retenue afin qu'une Error ne soit pas aussitôt effacée par une Info de
routine. Rafraîchir un segment n'écrase jamais une notification, et
inversement.
Menu contextuel de l'éditeur¶
Contrairement à IMenuRegistry — des entrées statiques de la barre de menus
liées à des commandes — les éléments du menu contextuel sont dynamiques :
à chaque clic droit dans la zone de texte — ou dans la marge à côté —, l'hôte
demande à chaque fournisseur enregistré ce qui s'applique à l'endroit cliqué
et ajoute les réponses sous les actions d'édition intégrées
(Annuler/Rétablir, Couper/Copier/Coller, Tout sélectionner). La requête
indique où a eu lieu le 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;
};
areavautMarginpour un clic dans n'importe quelle colonne à gauche du texte (numéros de ligne, colonne des signets, colonne de repli) etTextsinon ; un menu ouvert au clavier est toujoursText.lineest la ligne cliquée (la ligne du curseur pour un menu au clavier) : vous n'avez donc jamais à convertirpositionvous-même.positionest le décalage en octets sous le clic dans le texte (le curseur pour un menu au clavier). Un clic dans la marge n'a aucun caractère au-dessous de lui ;positiony est donc le décalage du début deline— valide, mais n'en déduisez jamais une colonne.
Décidez par zone de ce que vous proposez : les entrées de niveau ligne (Add Conditional Breakpoint…, Revert Hunk) relèvent de la marge, celles de niveau mot du texte. Le correcteur orthographique, par exemple, ne propose rien pour un clic dans la marge.
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
Les fournisseurs s'exécutent sur le thread UI pendant que l'utilisateur attend le menu — répondez à partir d'un état en mémoire, ne bloquez jamais. Les titres des éléments sont des chaînes dynamiques que le plugin traduit lui-même. Le correcteur orthographique s'en sert pour ses suggestions et ses entrées Ajouter au dictionnaire / Ignorer pour cette session.
Palette de thème¶
Les plugins qui peignent des couleurs personnalisées enregistrent des catégories nommées avec des valeurs par défaut claire/sombre et les résolvent en RGB au moment du dessin — ne codez jamais les couleurs en dur :
// 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 renvoie la surcharge du thème quand elle existe, sinon la
valeur par défaut enregistrée pour la variante active, et 0x000000 pour un
id non enregistré (traitez cela comme « utiliser mon propre repli »).
Chaque catégorie enregistrée apparaît automatiquement dans l'éditeur de
thèmes, et les surcharges de l'utilisateur font l'aller-retour dans le fichier
de thème sous pluginColors. Repeignez sur events::ThemeChanged.