From 91a0e2d260e8338e92899827998cee9ce068c4a9 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 7 Oct 2026 15:59:05 +0200 Subject: [PATCH] M3: loadable KTextEditor plugin with M-x command palette - OlliePlugin (KTextEditor::Plugin) + OllieView, built as olliepalette.so via kcoreaddons_add_plugin, installed under kf6/ktexteditor. - M-x bound to Alt+X (single modifier, adjacent keys on a compact layout, ApplicationShortcut so it is always live). - On trigger, aggregates every enabled/visible QAction from the window's guiFactory()->clients() action collections (keyed by objectName) and shows them in PaletteWidget; the chosen row triggers the QAction. The action's current shortcut is surfaced in the group column. - Enable position-independent code for linking the static palette lib into the shared plugin module. - Plugin metadata verified valid via KPluginMetaData. Frecency wiring deferred (model supports it; plugin passes 0). All three ctest suites remain green. --- CMakeLists.txt | 2 + README.md | 21 ++++- docs/PLAN.md | 20 +++-- src/CMakeLists.txt | 1 + src/plugin/CMakeLists.txt | 25 ++++++ src/plugin/ollieplugin.cpp | 159 ++++++++++++++++++++++++++++++++++++ src/plugin/ollieplugin.h | 67 +++++++++++++++ src/plugin/ollieplugin.json | 14 ++++ 8 files changed, 302 insertions(+), 7 deletions(-) create mode 100644 src/plugin/CMakeLists.txt create mode 100644 src/plugin/ollieplugin.cpp create mode 100644 src/plugin/ollieplugin.h create mode 100644 src/plugin/ollieplugin.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bdea73..9a375dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,8 @@ include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS Core Gui Widgets) find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Test) diff --git a/README.md b/README.md index dd0328a..c9d027d 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,20 @@ QT_QPA_PLATFORM=offscreen ./build/bin/test_fuzzyranker # run the matcher tests ctest --test-dir build # or via ctest ``` +## Install the plugin into Kate + +The plugin builds to `build/bin/kf6/ktexteditor/olliepalette.so`. Drop it where +Kate scans for KTextEditor plugins (a user path on `QT_PLUGIN_PATH`): + +```sh +install -D build/bin/kf6/ktexteditor/olliepalette.so \ + ~/.local/lib/x86_64-linux-gnu/qt6/plugins/kf6/ktexteditor/olliepalette.so +``` + +Then in Kate: Settings → Configure Kate → Plugins → enable +**Ollie Command Palette**. Press **Alt+X** (M-x) to open the palette; type to +filter, Enter or click to run. + ## Status See [docs/PLAN.md](docs/PLAN.md) for the full roadmap. @@ -61,7 +75,9 @@ See [docs/PLAN.md](docs/PLAN.md) for the full roadmap. - **Milestone 2 — custom palette widget: DONE.** `PaletteModel` + `PaletteWidget` (frameless popup, live filtering, highlighted matches, keyboard + mouse activation). 10 model unit tests, all green. -- Milestone 3 — KTextEditor plugin + M-x + action registry: next. +- Milestone 3 — KTextEditor plugin + M-x: **DONE.** Loadable `olliepalette.so`; + `Alt+X` opens the palette over every action in the window's GUI clients. +- Milestone 4 — radial caret menu (mouse door): next. ## Layout @@ -75,6 +91,9 @@ src/ palettemodel.{h,cpp} ranked, filterable list model (headless) palettewidget.{h,cpp} frameless command-palette popup test_palettemodel.cpp QTest suite + plugin/ + ollieplugin.{h,cpp} KTextEditor::Plugin + M-x action registry + ollieplugin.json plugin metadata docs/ PLAN.md goal, constraints, roadmap ``` diff --git a/docs/PLAN.md b/docs/PLAN.md index 72f8cf2..56b45d7 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -61,13 +61,21 @@ Therefore the palette is replaced, not extended, and the replacement is built on - `src/palette/test_palettemodel.cpp`: 10 QTest cases, all green (filtering, orderless, typo, best-first ordering, frecency tie-break, highlight ranges). -### M3 — KTextEditor plugin + M-x + action registry — NEXT -- `KTextEditor::Plugin` skeleton producing a loadable `.so`. -- Central action registry aggregating Kate built-in actions + plugin actions. -- `M-x` (`Alt+x`) opens the M2 palette over the registry. -- Frecency ranking: usage history feeds a score bonus. +### M3 — KTextEditor plugin + M-x + action registry — DONE +- `src/plugin/ollieplugin.{h,cpp}` + `ollieplugin.json`: a loadable + `KTextEditor::Plugin` (`olliepalette.so`) built with `kcoreaddons_add_plugin`. +- `OllieView` (one per MainWindow) registers the **M-x** action, `Alt+X`, a + single modifier with the keys adjacent on a compact layout; `ApplicationShortcut` + context so it is always live. +- On trigger it aggregates every enabled, visible `QAction` from the window's + `guiFactory()->clients()` action collections, keyed by stable `objectName`, + and feeds them to `PaletteWidget`. The action's current shortcut is shown in + the group column. Accepting a row triggers the underlying `QAction`. +- Installs to `kf6/ktexteditor`; metadata verified valid via `KPluginMetaData`. +- Not yet wired: frecency (the model supports a bonus, but the plugin currently + passes 0 for every action — usage tracking is deferred). -### M4 — Radial caret menu (mouse door) +### M4 — Radial caret menu (mouse door) — NEXT - Mouse-triggered radial menu positioned at the caret (`View::cursorToCoordinate`), flick-to-select by angle. - Slices invoke registry actions; one slice opens the palette / command line, so diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4c9d8b1..77ccd88 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(fuzzy) add_subdirectory(palette) +add_subdirectory(plugin) diff --git a/src/plugin/CMakeLists.txt b/src/plugin/CMakeLists.txt new file mode 100644 index 0000000..d16ae40 --- /dev/null +++ b/src/plugin/CMakeLists.txt @@ -0,0 +1,25 @@ +find_package(KF6 ${KF_MIN_VERSION} COMPONENTS + TextEditor + XmlGui + CoreAddons + I18n +) + +if(NOT KF6TextEditor_FOUND) + message(STATUS "KF6::TextEditor not found — skipping the Kate plugin target") + return() +endif() + +kcoreaddons_add_plugin(olliepalette + SOURCES ollieplugin.cpp ollieplugin.h + INSTALL_NAMESPACE "kf6/ktexteditor" +) + +target_link_libraries(olliepalette PRIVATE + palette + KF6::TextEditor + KF6::XmlGui + KF6::CoreAddons + KF6::I18n + Qt6::Widgets +) diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp new file mode 100644 index 0000000..cba929d --- /dev/null +++ b/src/plugin/ollieplugin.cpp @@ -0,0 +1,159 @@ +/* + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +#include "ollieplugin.h" + +#include "palettewidget.h" + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace katecustom +{ + +// --------------------------------------------------------------------------- +// OlliePlugin +// --------------------------------------------------------------------------- + +OlliePlugin::OlliePlugin(QObject *parent, const QVariantList &args) + : KTextEditor::Plugin(parent) +{ + Q_UNUSED(args); +} + +OlliePlugin::~OlliePlugin() = default; + +QObject *OlliePlugin::createView(KTextEditor::MainWindow *mainWindow) +{ + return new OllieView(this, mainWindow); +} + +// --------------------------------------------------------------------------- +// OllieView +// --------------------------------------------------------------------------- + +OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow) + : QObject(plugin) + , m_mainWindow(mainWindow) +{ + // The one keyboard door: Alt+X (M-x). A single modifier, no chord, and the + // keys sit together on a compact layout. + auto *openAction = new QAction(i18n("Command Palette (M-x)"), this); + openAction->setObjectName(QStringLiteral("ollie_command_palette")); + openAction->setShortcut(QKeySequence(Qt::ALT | Qt::Key_X)); + openAction->setShortcutContext(Qt::ApplicationShortcut); + connect(openAction, &QAction::triggered, this, &OllieView::showCommandPalette); + + // Attach the shortcut to the window so it is always live. + if (QWidget *w = m_mainWindow->window()) { + w->addAction(openAction); + } +} + +OllieView::~OllieView() +{ + delete m_palette; +} + +void OllieView::collectActions(QHash> &out) const +{ + KXMLGUIFactory *factory = m_mainWindow->guiFactory(); + if (!factory) { + return; + } + + const QList clients = factory->clients(); + for (KXMLGUIClient *client : clients) { + KActionCollection *coll = client->actionCollection(); + if (!coll) { + continue; + } + const QList actions = coll->actions(); + for (QAction *action : actions) { + if (!action || action->isSeparator()) { + continue; + } + // Only offer things the user could actually invoke now. + if (!action->isEnabled() || !action->isVisible()) { + continue; + } + const QString label = action->text().remove(QLatin1Char('&')).trimmed(); + if (label.isEmpty()) { + continue; + } + // Prefer the stable objectName as id; fall back to the label. + QString id = action->objectName(); + if (id.isEmpty()) { + id = label; + } + if (!out.contains(id)) { + out.insert(id, action); + } + } + } +} + +void OllieView::showCommandPalette() +{ + if (!m_palette) { + m_palette = new PaletteWidget(m_mainWindow->window()); + connect(m_palette, &PaletteWidget::activatedId, this, &OllieView::runAction); + } + + m_actionsById.clear(); + collectActions(m_actionsById); + + QList items; + items.reserve(m_actionsById.size()); + for (auto it = m_actionsById.cbegin(); it != m_actionsById.cend(); ++it) { + QAction *action = it.value(); + if (!action) { + continue; + } + PaletteItem item; + item.id = it.key(); + item.label = action->text().remove(QLatin1Char('&')).trimmed(); + // Show the current shortcut as the group column hint, when present. + const QString sc = action->shortcut().toString(QKeySequence::NativeText); + item.group = sc; + items.push_back(item); + } + + m_palette->setItems(items); + + // Center the palette near the top of the active window. + if (QWidget *w = m_mainWindow->window()) { + const QSize sz = m_palette->size(); + const QPoint topLeft = w->mapToGlobal(QPoint((w->width() - sz.width()) / 2, + w->height() / 6)); + m_palette->move(topLeft); + } + + m_palette->activate(); +} + +void OllieView::runAction(const QString &actionId) +{ + QPointer action = m_actionsById.value(actionId); + if (action) { + action->trigger(); + } +} + +} // namespace katecustom + +// K_PLUGIN_FACTORY generates the factory and the moc-level registration. The +// matching .moc include is required in this translation unit. +K_PLUGIN_FACTORY_WITH_JSON(OlliePluginFactory, "ollieplugin.json", + registerPlugin();) + +#include "ollieplugin.moc" diff --git a/src/plugin/ollieplugin.h b/src/plugin/ollieplugin.h new file mode 100644 index 0000000..06684e1 --- /dev/null +++ b/src/plugin/ollieplugin.h @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: LGPL-2.0-or-later + * + * The Ollie command-palette plugin for Kate / KTextEditor. + * + * OlliePlugin is the KTextEditor::Plugin entry point. For every MainWindow the + * host creates an OllieView, which registers the "Command Palette (M-x)" action + * (Alt+X) and, when triggered, gathers every QAction currently exposed by the + * window's GUI clients into a PaletteWidget for orderless fuzzy selection. + */ +#ifndef KATECUSTOM_OLLIEPLUGIN_H +#define KATECUSTOM_OLLIEPLUGIN_H + +#include + +#include +#include +#include + +class QAction; + +namespace KTextEditor +{ +class MainWindow; +} + +namespace katecustom +{ + +class PaletteWidget; + +class OlliePlugin : public KTextEditor::Plugin +{ + Q_OBJECT + +public: + explicit OlliePlugin(QObject *parent, const QVariantList &args = {}); + ~OlliePlugin() override; + + QObject *createView(KTextEditor::MainWindow *mainWindow) override; +}; + +class OllieView : public QObject +{ + Q_OBJECT + +public: + OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow); + ~OllieView() override; + +private Q_SLOTS: + void showCommandPalette(); + void runAction(const QString &actionId); + +private: + // Collect every unique, enabled, visible QAction reachable from the + // window's GUI factory clients. Keyed by a stable id for lookup on accept. + void collectActions(QHash> &out) const; + + KTextEditor::MainWindow *m_mainWindow; + PaletteWidget *m_palette = nullptr; + QHash> m_actionsById; +}; + +} // namespace katecustom + +#endif diff --git a/src/plugin/ollieplugin.json b/src/plugin/ollieplugin.json new file mode 100644 index 0000000..433ec52 --- /dev/null +++ b/src/plugin/ollieplugin.json @@ -0,0 +1,14 @@ +{ + "KPlugin": { + "Authors": [ + { + "Name": "Levi Neely" + } + ], + "Description": "Menu-free, mouse-first command access: an orderless fuzzy command palette (M-x).", + "Icon": "code-context", + "License": "LGPL-2.0-or-later", + "Name": "Ollie Command Palette", + "Version": "0.1.0" + } +}