Ga naar inhoud

Opdrachten en menu's

Opdrachten

ICommandRegistry is de enige trechter voor de uitvoering van elke actie die een gebruiker kan aanroepen — menu-items, werkbalkknoppen, sneltoetsen en (later) Python-scripts.

namespace platform_id {              // keys for platformShortcuts
    inline constexpr std::string_view kWindows = "windows";
    inline constexpr std::string_view kMacOS   = "macos";
    inline constexpr std::string_view kLinux   = "linux";
}

namespace command_scope {            // values for CommandDescriptor::scope
    inline constexpr std::string_view kApplication  = "application";
    inline constexpr std::string_view kPluginWindow = "pluginWindow";
}

struct CommandDescriptor {
    std::string id;          // "helloworld.sayHi"
    std::string title;       // "Say Hi"
    std::string shortcut;    // portable default: "Ctrl+Alt+H"
    std::string category;    // "Hello"
    std::string scope;       // "" / "application" | "pluginWindow"
    std::map<std::string, std::string> platformShortcuts; // optional per-platform
    bool requiresEditor         = false;  // needs an active document
    bool requiresWritableEditor = false;  // ...and changes its text
};

// Convenience wrappers, so an existing aggregate literal only has to be
// wrapped rather than rewritten:
CommandDescriptor needsEditor(CommandDescriptor);          // requiresEditor
CommandDescriptor needsWritableEditor(CommandDescriptor);  // + writable

using CommandHandler = std::function<void()>;

class ICommandRegistry {
public:
    virtual CommandToken registerCommand(CommandDescriptor, CommandHandler) = 0;
    virtual void         invoke(std::string_view commandId) = 0;
    virtual bool         exists(std::string_view commandId) const = 0;
    // The user-resolved sequence in effect right now ("" = unbound),
    // NOT the declared default. See "User remapping" below.
    virtual std::string  effectiveShortcut(std::string_view commandId) const = 0;
};

registerCommand levert een CommandToken (RAII) op. Hem vernietigen verwijdert de opdracht, haar menu-items en haar sneltoets. Houd elk token als lid vast, zodat de hele plug-in in shutdown() automatisch afgebroken wordt.

Eisen aan de editor

requiresEditor / requiresWritableEditor vertellen de host wat de opdracht van het actieve document nodig heeft, zodat hij de opdracht grijs kan maken in plaats van de gebruiker iets te laten aanroepen dat stilzwijgend niets doet.

Vlag Zet hem wanneer de opdracht… Hij wordt grijs bij…
requiresEditor de buffer, de selectie of de cursor leest, of markeringen plaatst (Bladwijzers, Markeren, Zoeken, Ga naar definitie) een tabblad dat helemaal geen document herbergt — een tabblad met een vergelijking of een logweergave, waar activeDocument() 0 oplevert
requiresWritableEditor de tekst wijzigt (Commentaar, Hoofdlettergebruik omzetten, Kolomeditor, Inspringing vergroten, Regels…, Document opmaken, Symboolnaam wijzigen) het bovenstaande en een alleen-lezen document: een bestand dat met tail -f gevolgd wordt, een sessie met --viewer of --readonly

De schrijfvlag impliceert de andere, dus een beschrijving mag alleen de sterkste van de twee zetten. Beide staan standaard op false — «altijd beschikbaar», het juiste antwoord voor opdrachten die een venster, een map of een paneel openen.

Het grijs maken bereikt het menu-item, het opdrachtenpalet en de sneltoets. Het kan alleen beschikbaarheid wegnemen: een item dat uw plug-in zelf via IMenuRegistry::setItemEnabled uitgeschakeld heeft blijft uitgeschakeld, wat het actieve document ook is.

Verpak de beschrijving op de plek van de aanroep:

using MTE::plugin::needsWritableEditor;

m_toggleComment = ctx.commands().registerCommand(
    needsWritableEditor({ "comment.toggle", "Toggle Comment",
                          "Ctrl+Q", "Comment" }),
    [this] { onToggleComment(); });

In Python geeft u de benoemde argumenten mee:

@ctx.commands.command(id="wc.upper", title="Upper Case",
                      requires_writable_editor=True)
def _upper():
    ...

Sneltoetsen per platform

shortcut is één draagbare toetscombinatie. Qt koppelt de modificatietoets "Ctrl" al aan de inheemse van het platform — ⌘ Command op macOS — dus "Ctrl+F" is daar zonder extra werk ⌘F. U hebt alleen meer nodig wanneer die automatische koppeling op een combinatie zou uitkomen die het besturingssysteem zich voorbehoudt (bijvoorbeeld "Ctrl+Q" → ⌘Q = Afsluiten op macOS).

Stel voor die gevallen platformShortcuts in, met platform_id als sleutel. De host levert het item op dat bij het draaiende platform past, of valt terug op shortcut wanneer een platform ontbreekt (of aan een lege tekenreeks gekoppeld is). Geen #ifdef, geen Qt-typen — alleen gegevens — zodat de koppeling backend-onafhankelijk blijft, en een nieuw platform één nieuwe sleutel is:

MTE::plugin::CommandDescriptor desc;
desc.id       = "comment.toggle";
desc.title    = "Toggle Single Line Comment";
desc.shortcut = "Ctrl+Q";                       // Windows / Linux → Ctrl+Q
desc.platformShortcuts = {                       // macOS → ⌃Q (the portable
    { std::string(MTE::plugin::platform_id::kMacOS), "Meta+Q" } }; // "Meta" = ⌃
desc.category = "Comment";

Gereserveerde ⌘-combinaties van macOS om één Ctrl+<key> tegen te houden: ⌘Q (Afsluiten), ⌘W (Sluiten), ⌘H (Verbergen), ⌘M (Minimaliseren), ⌘, (Voorkeuren), ⌘Spatie (Spotlight). Vermijd bij een combinatie Ctrl+Alt+<key> (⌥⌘ op macOS) ook ⌥⌘H (Andere verbergen), ⌥⌘M (Alles minimaliseren), ⌥⌘D (Dock) en ⌥⌘Esc (Geforceerd stoppen).

Nog twee regels houden sneltoetsen uit de weg van het typen:

  • Nooit een kale Alt+<letter> als draagbare combinatie. Op Windows is dat de ruimte van de menumnemonieken, en op macOS stelt ⌥+letter accenttekens samen (⌥U is de dode toets voor de trema, ⌥C typt ç), zodat de sneltoets die zou opslokken — geef zo'n opdracht een overschrijving {"macos", "Meta+Alt+<key>"} (⌃⌥).
  • Ctrl+Alt+<key> is AltGr+<key> op Windows. Nationale indelingen typen daar letters (Pools: ś ł ą ć ę ń ó ź ż €; Duits: @ € µ). De host houdt ze typbaar — een druk op de rechter Alt die een afdrukbaar teken oplevert wordt als tekst afgeleverd, een druk op de linker Ctrl+Alt voert de opdracht uit — dus zulke combinaties zijn toegestaan, maar geef de voorkeur aan letters die op de gangbare indelingen niets typen (B D F G H I J K P R T V W Y) en gebruik nooit Ctrl+Alt+<digit>.

Opnieuw toewijzen door de gebruiker

De verklaarde shortcut / platformShortcuts zijn standaarden. De gebruiker kan elke opdracht opnieuw toewijzen of loskoppelen in de sneltoetstoewijzing van de kern (Voorkeuren ▸ Sneltoetsen), en conflicten worden aan de kant van de host opgelost: duplicaten binnen hetzelfde bereik bestaan nooit naast elkaar — de verliezer wordt voor de sessie geschorst en gemeld. Niets hiervan vergt code in de plug-in voor gewone menu-gebonden opdrachten: de host bezit hun QAction en wijst die live opnieuw toe.

scope — opdrachten in vensters die een plug-in bezit

Opdrachten waarvan de toetscombinatie in een widget woont die de plug-in bezit (de werkbalk van een gereedschapsvenster in plaats van het hoofdmenu) verklaren scope = command_scope::kPluginWindow. De host koppelt de combinatie dan nooit zelf; hij somt de opdracht alleen in de toewijzing op en lost de keuze van de gebruiker op. De plug-in past de koppeling op haar eigen widgets toe:

  • vraag ctx.commands().effectiveShortcut(id) op terwijl u de widgets bouwt, en
  • schrijf u in op events::CommandShortcutChanged { commandId, sequence } en pas opnieuw toe wanneer een van haar id's verandert ("" = nu losgekoppeld).

Het conflictdomein is in v1 globaal — een combinatie in pluginWindow weigert met een combinatie van de toepassing te botsen. Werkend voorbeeld: de werkbalk van de Logboekweergave van de Logboekanalyse (loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime Ctrl+G).

Menupaden gebruiken / als scheiding; de host maakt tussenliggende submenu's op verzoek aan:

class IMenuRegistry {
public:
    virtual MenuToken addItem(std::string menuPath, std::string commandId) = 0;
    virtual MenuToken addSeparator(std::string menuPath) = 0;
    virtual void      setItemChecked(TokenId itemToken, bool checked) = 0;
    virtual void      setItemEnabled(TokenId itemToken, bool enabled) = 0;
};
m_menu = ctx.menus().addItem("File/Export/As HTML...", "myplugin.exportHtml");

MenuToken is RAII, net als CommandToken.

Aanvinkbare menu-items

Een menu-item dat een blijvende aan/uit-staat vertegenwoordigt kan een zichtbaar vinkje dragen. setItemChecked neemt de TokenId van het item — gebruik de niet-vernietigende toegang id() van het token (release() zou het eigendom opgeven):

m_toggle = ctx.menus().addItem("View/Auto-open preview", "myplugin.toggle");

// Seed the mark from the persisted setting so the menu matches reality
// before the user first opens it:
ctx.menus().setItemChecked(m_toggle.id(),
                           ctx.settings().getBool("autoOpen", true));

// Inside the command handler, after flipping the setting:
ctx.menus().setItemChecked(m_toggle.id(), nowEnabled);

De eerste aanroep maakt het item aanvinkbaar; latere aanroepen kantelen alleen de staat. Een onbekend token is een stille niet-bewerking, dus een vertraagde callback die na het afsluiten scherpstelt is onschadelijk.

Een opdracht die een voorwaarde nodig heeft — een map om te sluiten, een gesloten tabblad om te herstellen — hoort haar menu-item grijs te maken zolang die voorwaarde ontbreekt, in plaats van een item aan te bieden dat stilletjes niets doet. setItemEnabled neemt de TokenId van het item, net als setItemChecked:

m_closeItem = ctx.menus().addItem("File/Close Folder", "myplugin.closeFolder");

// Nothing to close yet:
ctx.menus().setItemEnabled(m_closeItem.id(), false);

// ... and from every place that changes the state the command needs:
ctx.menus().setItemEnabled(m_closeItem.id(), !folder.empty());

Een uitgeschakeld item blijft zichtbaar maar kan niet in gang gezet worden, niet met een klik en niet met zijn sneltoets. De aanroep is idempotent — dezelfde waarde herhalen wordt verwacht, zodat u de vorige staat nooit hoeft te onthouden — en een onbekend token is een stille niet-bewerking, zoals hierboven. Items beginnen ingeschakeld, dus een item dat bij het starten niet beschikbaar is heeft één aanroep vlak na addItem nodig.

Plaatsing in het menu (plugin.json)

Waar het menu van het hoogste niveau van een plug-in zit is gegevensgestuurd via de array menus in plugin.json — de host legt de namen van plug-inmenu's nooit in code vast:

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — het menu van het hoogste niveau; komt overeen met het eerste door / gescheiden segment van uw menuPath. Geef het met & een toetsenbordmnemoniek ("Lo&g Analyzer") en gebruik dezelfde spelling in elk menuPath: op Windows opent Alt+letter het menu, en elke balktitel heeft een eigen letter nodig, in elke taal — kies er een die vrij is onder Bestand, Bewerken, Zoeken, Tekencodering, Vergelijken, Beeld, Taal, Plug-ins, Venster, Over en Help, en laat elke vertaling haar eigen letter kiezen. Het vergelijken van menu's negeert de markering.
  • barPriority — horizontale positie op de menubalk (lager = verder naar links). De kernmenu's staan vast, met 100 ertussen: Bestand 100, Bewerken 200, Beeld 300, Taal 400, Vergelijken 500, Venster 600 (Over 1000, altijd helemaal rechts); plug-ins vullen de gaten (Zoeken 250, Tekencodering 350, Logboekanalyse 540, Plug-ins 570).
  • itemPriority — de verticale rang van het blok van deze plug-in binnen title (lager = hoger; standaard 1000). Items binnen het blok van één plug-in behouden de volgorde waarin ze in de code worden toegevoegd.

Note

Omdat plugin.json als metagegevens van een Qt-plug-in ingebed is, worden wijzigingen eraan pas na een herbouw van kracht.