Gå till innehållet

Kommandon och menyer

Kommandon

ICommandRegistry är den enda exekveringstratten för varje åtgärd en användare kan anropa — menyposter, verktygsfältsknappar, kortkommandon och (senare) 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 returnerar en CommandToken (RAII). Att förstöra den tar bort kommandot, dess menyposter och dess kortkommando. Håll varje symbol som medlem så att hela insticksmodulen rivs ned automatiskt i shutdown().

Krav på redigeraren

requiresEditor / requiresWritableEditor talar om för värden vad kommandot behöver från det aktiva dokumentet, så att den kan gråtona kommandot i stället för att låta användaren anropa något som tyst inte gör något.

Flagga Sätt den när kommandot… Den gråtonas vid…
requiresEditor läser bufferten, markeringen eller markören, eller placerar markörer (Bokmärken, Markera, Sök, Gå till definitionen) en flik som inte rymmer något dokument alls — en flik med en jämförelse eller en loggvy, där activeDocument() returnerar 0
requiresWritableEditor ändrar texten (Kommentar, Ändra skiftläge, Kolumnredigerare, Öka indraget, Rader…, Formatera dokumentet, Byt namn på symbolen) ovanstående och ett skrivskyddat dokument: en fil som följs med tail -f, en session med --viewer eller --readonly

Skrivflaggan medför den andra, så en beskrivning får sätta bara den starkare. Båda är false som standard — «alltid tillgängligt», rätt svar för kommandon som öppnar en dialogruta, en mapp eller en panel.

Gråtoningen når menyposten, kommandopaletten och kortkommandot. Den kan bara ta bort tillgänglighet: en post som din insticksmodul själv inaktiverat genom IMenuRegistry::setItemEnabled förblir inaktiverad oavsett vilket dokument som är aktivt.

Kapsla in beskrivningen på anropsplatsen:

using MTE::plugin::needsWritableEditor;

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

I Python skickar du med de namngivna argumenten:

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

Kortkommandon per plattform

shortcut är en enda portabel tangentkombination. Qt kopplar redan modifieraren "Ctrl" till plattformens inhemska — ⌘ Command på macOS — så "Ctrl+F" är ⌘F där utan extra arbete. Du behöver mer bara när den automatiska kopplingen skulle hamna på en kombination som operativsystemet reserverat (till exempel "Ctrl+Q" → ⌘Q = Avsluta på macOS).

För de fallen anger du platformShortcuts, med platform_id som nyckel. Värden returnerar den post som matchar den körande plattformen, eller faller tillbaka på shortcut när en plattform saknas (eller kopplats till en tom sträng). Ingen #ifdef, inga Qt-typer — bara data — så att kopplingen förblir backend-oberoende, och en ny plattform är en enda ny nyckel:

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

Reserverade ⌘-kombinationer i macOS att stämma av ett enskilt Ctrl+<key> mot: ⌘Q (Avsluta), ⌘W (Stäng), ⌘H (Göm), ⌘M (Minimera), ⌘, (Inställningar), ⌘Mellanslag (Spotlight). För en kombination Ctrl+Alt+<key> (⌥⌘ på macOS) undvik också ⌥⌘H (Göm övriga), ⌥⌘M (Minimera alla), ⌥⌘D (Dock) och ⌥⌘Esc (Tvångsavsluta).

Två regler till håller kortkommandon ur vägen för skrivandet:

  • Aldrig ett ensamt Alt+<letter> som portabel kombination. På Windows är det menymnemonikernas område, och på macOS sätter ⌥+bokstav samman accenttecken (⌥U är den döda tangenten för trema, ⌥C skriver ç), så kortkommandot skulle svälja dem — ge ett sådant kommando en åsidosättning {"macos", "Meta+Alt+<key>"} (⌃⌥).
  • Ctrl+Alt+<key> är AltGr+<key> på Windows. Nationella layouter skriver bokstäver där (polska: ś ł ą ć ę ń ó ź ż €; tyska: @ € µ). Värden håller dem skrivbara — en tryckning på höger Alt som ger ett utskrivbart tecken levereras som text, en tryckning på vänster Ctrl+Alt kör kommandot — så sådana kombinationer är tillåtna, men föredra bokstäver som inte skriver något i de vanliga layouterna (B D F G H I J K P R T V W Y) och använd aldrig Ctrl+Alt+<digit>.

Omtilldelning av användaren

De deklarerade shortcut / platformShortcuts är standardvärden. Användaren kan tilldela om eller lossa varje kommando i kärnans tilldelning av kortkommandon (Inställningar ▸ Kortkommandon), och konflikter löses på värdsidan: dubbletter inom samma omfattning samexisterar aldrig — förloraren stängs av för sessionen och rapporteras. Inget av detta kräver kod i insticksmodulen för vanliga menybundna kommandon: värden äger deras QAction och kopplar om den under körning.

scope — kommandon i fönster som en insticksmodul äger

Kommandon vars kombination bor i en widget som insticksmodulen äger (ett verktygsfönsters verktygsfält snarare än huvudmenyn) deklarerar scope = command_scope::kPluginWindow. Värden kopplar då aldrig kombinationen själv; den listar bara kommandot i tilldelningen och löser användarens val. Insticksmodulen tillämpar kopplingen på sina egna widgetar:

  • fråga ctx.commands().effectiveShortcut(id) när du bygger widgetarna, och
  • prenumerera på events::CommandShortcutChanged { commandId, sequence } och tillämpa på nytt när ett av dess id:n ändras ("" = nu lossad).

Konfliktdomänen är global i v1 — en pluginWindow-kombination vägrar krocka med en programkombination. Fungerande exempel: verktygsfältet i Logganalysens loggvy (loganalyzer.logView.nextError F8 / .prevError Shift+F8 / .goToTime Ctrl+G).

Menyer

Menysökvägar använder / som avskiljare; värden skapar mellanliggande undermenyer på begäran:

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 är RAII precis som CommandToken.

Kryssbara menyposter

En menypost som representerar ett bestående på/av-tillstånd kan bära ett synligt kryss. setItemChecked tar postens TokenId — använd symbolens icke-förstörande åtkomst id() (release() skulle ge upp ägandet):

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örsta anropet gör posten kryssbar; senare anrop vänder bara tillståndet. En okänd symbol är en tyst nulloperation, så ett fördröjt återanrop som utlöses efter avslutet är ofarligt.

Gråtona menyposter

Ett kommando som behöver ett villkor — en mapp att stänga, en stängd flik att återställa — bör gråtona sin menypost medan villkoret saknas i stället för att erbjuda en post som tyst inte gör något. setItemEnabled tar postens TokenId, precis 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 inaktiverad post förblir synlig men kan inte utlösas, varken med ett klick eller med sitt kortkommando. Anropet är idempotent — att upprepa samma värde är väntat, så du behöver aldrig minnas det tidigare tillståndet — och en okänd symbol är en tyst nulloperation, som ovan. Poster börjar aktiverade, så en post som inte är tillgänglig vid start behöver ett anrop direkt efter addItem.

Menyplacering (plugin.json)

Var en insticksmoduls toppnivåmeny sitter är datadrivet genom vektorn menus i plugin.json — värden hårdkodar aldrig insticksmodulers menynamn:

"menus": [
  { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
  • title — toppnivåmenyn; stämmer med det första /-avskilda segmentet i din menuPath. Ge den en tangentbordsgenväg med & ("Lo&g Analyzer") och använd samma stavning i varje menuPath: på Windows öppnar Alt+bokstav menyn, och varje titel i raden behöver en egen bokstav, i varje språk — välj en som är fri bland Fil, Redigera, Sök, Teckenkodning, Jämför, Visa, Språk, Insticksmoduler, Fönster, Om och Hjälp, och låt varje översättning välja sin egen. Matchningen av menyer bortser från markören.
  • barPriority — vågrät position i menyraden (lägre = längre till vänster). Kärnmenyerna är fasta, med 100 emellan: Fil 100, Redigera 200, Visa 300, Språk 400, Jämför 500, Fönster 600 (Om 1000, alltid längst till höger); insticksmodulerna fyller luckorna (Sök 250, Teckenkodning 350, Logganalys 540, Insticksmoduler 570).
  • itemPriority — lodrät rang för den här insticksmodulens block inom title (lägre = högre upp; standard 1000). Poster inom en insticksmoduls block behåller den ordning de läggs till i koden.

Note

Eftersom plugin.json är inbäddad som Qt-metadata för insticksmodulen får ändringar i den effekt först efter en ombyggnad.