Gå til innhold

Kommandoer og menyer

Kommandoer

ICommandRegistry er den ene trakten hver handling en bruker kan utløse, går gjennom — menyposter, verktøylinjeknapper, hurtigtaster og (senere) Python-skript.

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 gir et CommandToken tilbake (RAII). Ødelegger man det, fjernes kommandoen, menypostene dens og hurtigtasten dens. Behold hvert symbol som et medlem, så hele programtillegget rives ned av seg selv i shutdown().

Krav til redigereren

requiresEditor / requiresWritableEditor forteller verten hva kommandoen trenger fra det aktive dokumentet, så den kan nedtone kommandoen i stedet for å la brukeren utløse noe som i stillhet ikke gjør noe.

Flagg Sett det når kommandoen … Den nedtones ved …
requiresEditor leser bufferen, merkingen eller markøren, eller setter markører (Bokmerker, Merk, Søk, Gå til definisjonen) en fane som slett ikke rommer et dokument — en sammenlignings- eller loggvisningsfane, der activeDocument() gir 0
requiresWritableEditor endrer teksten (Kommentar, Endre store/små bokstaver, Kolonneeditor, Øk innrykket, Linjer …, Formater dokumentet, Gi symbolet nytt navn) det ovenstående og et skrivebeskyttet dokument: en fil som følges med tail -f, eller en --viewer- eller --readonly-økt

Skriveflagget innebærer det andre, så en beskrivelse kan gjerne bare sette det sterkeste av dem. Begge er false som standard — «alltid tilgjengelig», det riktige svaret for kommandoer som åpner et vindu, en mappe eller et panel.

Nedtoningen når menyposten, kommandopaletten og hurtigtasten. Den kan bare ta bort tilgjengelighet: en post programtillegget ditt selv har slått av med IMenuRegistry::setItemEnabled, blir værende av, uansett hva det aktive dokumentet er.

Pakk beskrivelsen inn på kallstedet:

using MTE::plugin::needsWritableEditor;

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

I Python gir du nøkkelordargumentene med:

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

Hurtigtaster per plattform

shortcut er én flyttbar tastekombinasjon. Qt oversetter alt "Ctrl"-modifikatoren til plattformens egen — ⌘ Command på macOS — så "Ctrl+F" er ⌘F der uten mer arbeid. Du trenger bare mer når den automatiske oversettelsen ville landet på en kombinasjon operativsystemet har tatt (for eksempel "Ctrl+Q" → ⌘Q = Avslutt på macOS).

Sett i de tilfellene platformShortcuts, med platform_id som nøkkel. Verten gir den posten tilbake som passer til plattformen som kjører, eller faller tilbake på shortcut når en plattform ikke er nevnt (eller er satt til en tom streng). Ingen #ifdef, ingen Qt-typer — bare data — så bindingen blir værende backend-uavhengig, og en ny plattform er én ny nøkkel:

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

Reserverte ⌘-kombinasjoner på macOS som en enkelt Ctrl+<key> bør holdes opp mot: ⌘Q (Avslutt), ⌘W (Lukk), ⌘H (Skjul), ⌘M (Minimer), ⌘, (Innstillinger), ⌘Mellomrom (Spotlight). Ved en Ctrl+Alt+<key>-kombinasjon (⌥⌘ på macOS) bør du dessuten unngå ⌥⌘H (Skjul andre), ⌥⌘M (Minimer alle), ⌥⌘D (Dock) og ⌥⌘Esc (Tving avslutning).

To regler til holder hurtigtastene unna skrivingen:

  • Aldri en bar Alt+<letter> som den flyttbare kombinasjonen. På Windows er det menyenes bokstavområde, og på macOS setter ⌥ + bokstav sammen tegn med aksenter (⌥U er tremataksenten, ⌥C skriver ç), så hurtigtasten ville slukt dem — gi en slik kommando en {"macos", "Meta+Alt+<key>"}-overstyring (⌃⌥).
  • Ctrl+Alt+<key> er AltGr+<key> på Windows. Nasjonale tastaturoppsett skriver bokstaver der (polsk: ś ł ą ć ę ń ó ź ż €; tysk: @ € µ). Verten holder dem skrivbare — et trykk på høyre Alt som gir et tegn som kan skrives ut, leveres som tekst, mens et trykk med venstre Ctrl+Alt kjører kommandoen — så slike kombinasjoner er tillatt, men foretrekk bokstaver som ikke skriver noe på de vanlige oppsettene (B D F G H I J K P R T V W Y), og bruk aldri Ctrl+Alt+<digit>.

Brukerens omtildeling

Den erklærte shortcut / platformShortcuts er standardverdier. Brukeren kan gi hver kommando en ny binding eller ingen binding i kjernens hurtigtasttildeling (Innstillinger ▸ Hurtigtaster), og konflikter løses hos verten: dubletter innenfor samme omfang finnes aldri side om side — taperen settes på pause økten ut, og det meldes fra om det. Ingenting av dette krever kode i programtillegget for vanlige menybundne kommandoer: verten eier QAction-en deres og gir den en ny binding underveis.

scope — kommandoer i vinduer programtillegget eier

Kommandoer der kombinasjonen hører hjemme i en kontroll programtillegget eier (en verktøylinje i et verktøyvindu heller enn hovedmenyen), erklærer scope = command_scope::kPluginWindow. Verten binder da aldri kombinasjonen selv; den bare lister kommandoen i tildelingen og avgjør brukerens valg. Programtillegget tar selv bindingen i bruk på sine egne kontroller:

  • spør ctx.commands().effectiveShortcut(id) når kontrollene bygges, og
  • abonner på events::CommandShortcutChanged { commandId, sequence }, og ta den i bruk på nytt når en av id-ene endrer seg ("" = nå uten binding).

Konfliktområdet er globalt i v1 — en pluginWindow-kombinasjon nekter å kollidere med en programkombinasjon. Et fungerende eksempel: verktøylinjen i Logganalysens loggvisning (loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime Ctrl+G).

Menyer

Menystier bruker / som skilletegn; verten lager mellomliggende undermenyer ved 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 slik CommandToken er det.

Avkryssbare menyposter

En menypost som står for en varig av/på-tilstand, kan bære et synlig kryss. setItemChecked tar postens TokenId — bruk symbolets ikke-ødeleggende id()-tilgang (release() ville gi opp eierskapet):

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 kallet gjør posten avkryssbar; de følgende kallene snur bare tilstanden. Et ukjent symbol gjør i stillhet ingenting, så et forsinket tilbakekall som utløses etter nedstengningen, er harmløst.

Å nedtone menyposter

En kommando som trenger en forutsetning — en mappe å lukke, en lukket fane å gjenopprette — bør nedtone menyposten sin mens den forutsetningen mangler, i stedet for å tilby en post som i stillhet ikke gjør noe. setItemEnabled tar postens TokenId, nøyaktig 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());

En post som er slått av, blir værende synlig, men kan ikke utløses, verken med et klikk eller med hurtigtasten sin. Kallet kan gjentas — det er meningen at den samme verdien gjentas, så du aldri trenger å huske den forrige tilstanden — og et ukjent symbol gjør i stillhet ingenting, som ovenfor. Poster begynner påslått, så en post som ikke er tilgjengelig ved oppstart, trenger ett kall rett etter addItem.

Menyplassering (plugin.json)

Hvor et programtilleggs øverste meny sitter, er datastyrt via menus-tabellen i plugin.json — verten hardkoder aldri navn på programtilleggsmenyer:

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — den øverste menyen; passer til det første /-adskilte leddet i din menuPath. Gi den en tastatursnarvei med & ("Lo&g Analyzer"), og bruk den samme skrivemåten i hver menuPath: på Windows åpner Alt+bokstav menyen, og hver tittel på linjen trenger sin egen bokstav, i hvert språk — velg en som er ledig blant Fil, Rediger, Søk, Tegnkoding, Sammenlign, Vis, Språk, Programtillegg, Vindu, Om og Hjelp, og la hver oversettelse velge sin egen. Menysammenligningen ser bort fra markøren.
  • barPriority — den vannrette plasseringen på menylinjen (lavere = lenger til venstre). Kjernens menyer ligger fast med 100 imellom: Fil 100, Rediger 200, Vis 300, Språk 400, Sammenlign 500, Vindu 600 (Om 1000, alltid ytterst til høyre); programtilleggene fyller hullene (Søk 250, Tegnkoding 350, Logganalyse 540, Programtillegg 570).
  • itemPriority — den loddrette rangen for dette programtilleggets blokk inne i title (lavere = lenger opp; 1000 som standard). Postene innenfor ett programtilleggs blokk beholder den rekkefølgen de legges til i i koden.

Note

Fordi plugin.json legges inn som Qt-metadata for programtillegget, får endringer i den først virkning etter et nytt bygg.