Gå til indholdet

Kommandoer og menuer

Kommandoer

ICommandRegistry er den ene tragt, som hver handling, brugeren kan udløse, går igennem — menupunkter, værktøjslinjeknapper, genvejstaster og (senere) 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 giver et CommandToken tilbage (RAII). Ødelægger man det, fjernes kommandoen, dens menupunkter og dens genvejstast. Behold hvert symbol som et medlem, så hele udvidelsesmodulet rives ned af sig selv i shutdown().

Krav til editoren

requiresEditor / requiresWritableEditor fortæller værten, hvad kommandoen har brug for fra det aktive dokument, så den kan nedtone kommandoen i stedet for at lade brugeren udløse noget, der i stilhed ikke gør noget.

Flag Sæt det, når kommandoen … Den nedtones ved …
requiresEditor læser bufferen, markeringen eller markøren, eller sætter markører (Bogmærker, Markér, Søg, Gå til definition) et faneblad, der slet ikke rummer et dokument — et sammenlignings- eller logvisningsfaneblad, hvor activeDocument() giver 0 tilbage
requiresWritableEditor ændrer teksten (Kommentar, Konvertér bogstaver, Kolonneeditor, Forøg indrykning, Linjer …, Formatér dokument, Omdøb symbol) det ovenstående og et skrivebeskyttet dokument: en fil, der følges med tail -f, eller en --viewer- eller --readonly-session

Skriveflaget indebærer det andet, så en beskrivelse må gerne kun sætte det stærkeste af dem. Begge er false som standard — »altid tilgængelig«, det rigtige svar for kommandoer, der åbner et vindue, en mappe eller et panel.

Nedtoningen når menupunktet, kommandopaletten og genvejstasten. Den kan kun fjerne tilgængelighed: et punkt, dit udvidelsesmodul selv har slået fra med IMenuRegistry::setItemEnabled, bliver ved med at være slået fra, uanset hvad det aktive dokument er.

Pak beskrivelsen ind på kaldstedet:

using MTE::plugin::needsWritableEditor;

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

I Python giver du nøgleordsargumenterne med:

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

Genvejstaster pr. platform

shortcut er én flytbar tastekombination. Qt oversætter allerede "Ctrl"-modifikatoren til platformens egen — ⌘ Command på macOS — så "Ctrl+F" er ⌘F der uden yderligere arbejde. Du har kun brug for mere, når den automatiske oversættelse ville lande på en kombination, styresystemet har taget (f.eks. "Ctrl+Q" → ⌘Q = Afslut på macOS).

Sæt i de tilfælde platformShortcuts med platform_id som nøgle. Værten giver den post tilbage, der passer til den kørende platform, eller falder tilbage på shortcut, når en platform ikke er nævnt (eller er sat til en tom streng). Ingen #ifdef, ingen Qt-typer — bare data — så bindingen bliver ved med at være backend-uafhængig, og en ny platform er én ny nøgle:

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

Reserverede ⌘-kombinationer på macOS, som en enkelt Ctrl+<key> bør holdes op imod: ⌘Q (Afslut), ⌘W (Luk), ⌘H (Skjul), ⌘M (Minimér), ⌘, (Indstillinger), ⌘Mellemrum (Spotlight). Ved en Ctrl+Alt+<key>-kombination (⌥⌘ på macOS) skal du desuden undgå ⌥⌘H (Skjul andre), ⌥⌘M (Minimér alle), ⌥⌘D (Dock) og ⌥⌘Esc (Tving til at afslutte).

To regler mere holder genvejstaster ude af vejen for skrivningen:

  • Aldrig en bar Alt+<letter> som den flytbare kombination. På Windows er det menuernes bogstavområde, og på macOS sætter ⌥ + bogstav accenttegn sammen (⌥U er omlydstasten, ⌥C skriver ç), så genvejstasten ville sluge dem — giv en sådan kommando en {"macos", "Meta+Alt+<key>"}-tilsidesættelse (⌃⌥).
  • Ctrl+Alt+<key> er AltGr+<key> på Windows. Nationale tastaturlayout skriver bogstaver der (polsk: ś ł ą ć ę ń ó ź ż €; tysk: @ € µ). Værten holder dem skrivbare — et tryk på højre Alt, der frembringer et tegn, der kan udskrives, leveres som tekst, mens et tryk på venstre Ctrl+Alt kører kommandoen — så sådanne kombinationer er tilladt, men foretræk bogstaver, der ikke skriver noget på de almindelige layout (B D F G H I J K P R T V W Y), og brug aldrig Ctrl+Alt+<digit>.

Brugerens omtildeling

Den erklærede shortcut / platformShortcuts er standardværdier. Brugeren kan tildele hver kommando en anden tast eller slet ingen i kernens genvejstastoversigt (Indstillinger ▸ Genvejstaster), og konflikter løses hos værten: dubletter inden for samme omfang findes aldrig side om side — taberen sættes på pause sessionen ud, og der meldes om det. Intet af dette kræver kode i udvidelsesmodulet for almindelige menubundne kommandoer: værten ejer deres QAction og tildeler den en ny tast undervejs.

scope — kommandoer i vinduer, udvidelsesmodulet ejer

Kommandoer, hvis tastekombination hører hjemme i en kontrol, udvidelsesmodulet ejer (en værktøjslinje i et værktøjsvindue snarere end hovedmenuen), erklærer scope = command_scope::kPluginWindow. Værten binder så aldrig selv kombinationen; den opregner bare kommandoen i oversigten og afgør brugerens valg. Udvidelsesmodulet anvender selv bindingen på sine egne kontroller:

  • spørg ctx.commands().effectiveShortcut(id), når kontrollerne bygges, og
  • abonnér på events::CommandShortcutChanged { commandId, sequence }, og anvend den igen, når et af dets id'er ændrer sig ("" = nu uden tildeling).

Konfliktområdet er globalt i v1 — en pluginWindow-kombination nægter at støde sammen med en programkombination. Et fungerende eksempel: værktøjslinjen i Loganalysens logvisning (loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime Ctrl+G).

Menustier bruger / som skilletegn; værten opretter mellemliggende undermenuer efter behov:

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 er RAII ligesom CommandToken.

Afkrydselige menupunkter

Et menupunkt, der står for en vedvarende til/fra-tilstand, kan bære et synligt flueben. setItemChecked tager punktets TokenId — brug symbolets ikke-ødelæggende id()-tilgang (release() ville opgive ejerskabet):

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

Det første kald gør punktet afkrydseligt; de følgende kald vender bare tilstanden. Et ukendt symbol gør i stilhed ingenting, så et forsinket tilbagekald, der udløses efter nedlukningen, er harmløst.

At nedtone menupunkter

En kommando, der har brug for en forudsætning — en mappe at lukke, et lukket faneblad at gendanne — bør nedtone sit menupunkt, mens den forudsætning mangler, i stedet for at tilbyde en post, der i stilhed ikke gør noget. setItemEnabled tager punktets TokenId, præcis som 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());

Et frakoblet punkt bliver ved med at være synligt, men kan ikke udløses, hverken med et klik eller med sin genvejstast. Kaldet kan gentages — det er meningen, at den samme værdi gentages, så du aldrig behøver at huske den forrige tilstand — og et ukendt symbol gør i stilhed ingenting som ovenfor. Punkter begynder tilkoblede, så et punkt, der ikke er tilgængeligt ved opstart, kræver ét kald lige efter addItem.

Hvor et udvidelsesmoduls øverste menu sidder, er datastyret via menus-tabellen i plugin.json — værten hardkoder aldrig navne på modulmenuer:

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — den øverste menu; passer til det første /-adskilte led i din menuPath. Giv den en tastaturgenvej med & ("Lo&g Analyzer"), og brug den samme skrivemåde i hver menuPath: på Windows åbner Alt+bogstav menuen, og hver titel på linjen har brug for sit eget bogstav, i hvert sprog — vælg et, der er ledigt blandt Filer, Rediger, Søg, Tegnkodning, Sammenlign, Vis, Sprog, Udvidelsesmoduler, Vindue, Om og Hjælp, og lad hver oversættelse vælge sit eget. Menusammenligningen ser bort fra markøren.
  • barPriority — den vandrette placering på menulinjen (lavere = længere til venstre). Kernens menuer ligger fast med 100 imellem: Filer 100, Rediger 200, Vis 300, Sprog 400, Sammenlign 500, Vindue 600 (Om 1000, altid yderst til højre); udvidelsesmodulerne udfylder mellemrummene (Søg 250, Tegnkodning 350, Loganalyse 540, Udvidelsesmoduler 570).
  • itemPriority — den lodrette rang for dette moduls blok inde i title (lavere = længere oppe; 1000 som standard). Punkterne inden for ét moduls blok beholder den rækkefølge, de tilføjes i i koden.

Note

Fordi plugin.json indlejres som Qt-metadata for udvidelsesmodulet, får ændringer i den først virkning efter en ny bygning.