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.
This commit is contained in:
parent
617b526e3f
commit
91a0e2d260
|
|
@ -16,6 +16,8 @@ include(KDEInstallDirs)
|
||||||
include(KDECMakeSettings)
|
include(KDECMakeSettings)
|
||||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
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} REQUIRED COMPONENTS Core Gui Widgets)
|
||||||
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Test)
|
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Test)
|
||||||
|
|
||||||
|
|
|
||||||
21
README.md
21
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
|
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
|
## Status
|
||||||
|
|
||||||
See [docs/PLAN.md](docs/PLAN.md) for the full roadmap.
|
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` +
|
- **Milestone 2 — custom palette widget: DONE.** `PaletteModel` +
|
||||||
`PaletteWidget` (frameless popup, live filtering, highlighted matches,
|
`PaletteWidget` (frameless popup, live filtering, highlighted matches,
|
||||||
keyboard + mouse activation). 10 model unit tests, all green.
|
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
|
## Layout
|
||||||
|
|
||||||
|
|
@ -75,6 +91,9 @@ src/
|
||||||
palettemodel.{h,cpp} ranked, filterable list model (headless)
|
palettemodel.{h,cpp} ranked, filterable list model (headless)
|
||||||
palettewidget.{h,cpp} frameless command-palette popup
|
palettewidget.{h,cpp} frameless command-palette popup
|
||||||
test_palettemodel.cpp QTest suite
|
test_palettemodel.cpp QTest suite
|
||||||
|
plugin/
|
||||||
|
ollieplugin.{h,cpp} KTextEditor::Plugin + M-x action registry
|
||||||
|
ollieplugin.json plugin metadata
|
||||||
docs/
|
docs/
|
||||||
PLAN.md goal, constraints, roadmap
|
PLAN.md goal, constraints, roadmap
|
||||||
```
|
```
|
||||||
|
|
|
||||||
20
docs/PLAN.md
20
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,
|
- `src/palette/test_palettemodel.cpp`: 10 QTest cases, all green (filtering,
|
||||||
orderless, typo, best-first ordering, frecency tie-break, highlight ranges).
|
orderless, typo, best-first ordering, frecency tie-break, highlight ranges).
|
||||||
|
|
||||||
### M3 — KTextEditor plugin + M-x + action registry — NEXT
|
### M3 — KTextEditor plugin + M-x + action registry — DONE
|
||||||
- `KTextEditor::Plugin` skeleton producing a loadable `.so`.
|
- `src/plugin/ollieplugin.{h,cpp}` + `ollieplugin.json`: a loadable
|
||||||
- Central action registry aggregating Kate built-in actions + plugin actions.
|
`KTextEditor::Plugin` (`olliepalette.so`) built with `kcoreaddons_add_plugin`.
|
||||||
- `M-x` (`Alt+x`) opens the M2 palette over the registry.
|
- `OllieView` (one per MainWindow) registers the **M-x** action, `Alt+X`, a
|
||||||
- Frecency ranking: usage history feeds a score bonus.
|
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
|
- Mouse-triggered radial menu positioned at the caret
|
||||||
(`View::cursorToCoordinate`), flick-to-select by angle.
|
(`View::cursorToCoordinate`), flick-to-select by angle.
|
||||||
- Slices invoke registry actions; one slice opens the palette / command line, so
|
- Slices invoke registry actions; one slice opens the palette / command line, so
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
add_subdirectory(fuzzy)
|
add_subdirectory(fuzzy)
|
||||||
add_subdirectory(palette)
|
add_subdirectory(palette)
|
||||||
|
add_subdirectory(plugin)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
#include "ollieplugin.h"
|
||||||
|
|
||||||
|
#include "palettewidget.h"
|
||||||
|
|
||||||
|
#include <KTextEditor/MainWindow>
|
||||||
|
|
||||||
|
#include <KActionCollection>
|
||||||
|
#include <KXMLGUIClient>
|
||||||
|
#include <KXMLGUIFactory>
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <KPluginFactory>
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QKeySequence>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
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<QString, QPointer<QAction>> &out) const
|
||||||
|
{
|
||||||
|
KXMLGUIFactory *factory = m_mainWindow->guiFactory();
|
||||||
|
if (!factory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<KXMLGUIClient *> clients = factory->clients();
|
||||||
|
for (KXMLGUIClient *client : clients) {
|
||||||
|
KActionCollection *coll = client->actionCollection();
|
||||||
|
if (!coll) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QList<QAction *> 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<PaletteItem> 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<QAction> 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<katecustom::OlliePlugin>();)
|
||||||
|
|
||||||
|
#include "ollieplugin.moc"
|
||||||
|
|
@ -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 <KTextEditor/Plugin>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QHash>
|
||||||
|
|
||||||
|
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<QString, QPointer<QAction>> &out) const;
|
||||||
|
|
||||||
|
KTextEditor::MainWindow *m_mainWindow;
|
||||||
|
PaletteWidget *m_palette = nullptr;
|
||||||
|
QHash<QString, QPointer<QAction>> m_actionsById;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace katecustom
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue