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(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)
|
||||
|
||||
|
|
|
|||
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
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
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,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
add_subdirectory(fuzzy)
|
||||
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