diff --git a/docs/PLAN.md b/docs/PLAN.md index 2926f1b..ae5a922 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -164,11 +164,11 @@ Therefore the palette is replaced, not extended, and the replacement is built on non-alphanumerics then eats the alphanumeric run; isalnum excludes `_`). - Implemented directly against the View/Document API (`cursorPosition`, `setCursorPosition`, `removeText`, `characterAt`, `lineLength`). -- Shortcut conflicts (e.g. `Ctrl+A` = Select All, `Ctrl+W` = Close) are resolved - by stripping our sequences from every other GUI-client action - (`clearConflictingShortcuts`), which also clears their `KActionCollection` - defaults. The sweep runs at setup, deferred once via the event loop (after the - GUI factory merges all clients), and again on every `viewCreated`. +- Bound via the application **event filter**, not QActions: QActions for + `Ctrl+A/W/…` collide with Kate's own (Select All, Close) and Qt reports + ambiguous shortcuts. Instead the filter accepts the `ShortcutOverride` for + these combos (so Kate's shortcut never fires) and acts on the `KeyPress`, + consuming it — no registered shortcut, so no ambiguity, no per-view sweeping. ### M5 — Command vocabulary + project model + switchers — NEXT - `:`-verb pack via `KTextEditor::Command`: `sort`, `align`, `json`, `b64`, diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp index bcffd85..430cda2 100644 --- a/src/plugin/ollieplugin.cpp +++ b/src/plugin/ollieplugin.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -160,7 +161,6 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow) } setupRadials(); - setupAcmeKeys(); // One application-wide filter serves both the mouse-button radial gesture // and the Acme-style left+middle chord, catching presses no matter which @@ -493,6 +493,31 @@ bool OllieView::selectBracketPairAt(KTextEditor::View *view, const QPoint &globa bool OllieView::eventFilter(QObject *obj, QEvent *event) { + // Acme line-editing keys. Handle them here instead of via QActions to avoid + // ambiguous-shortcut clashes with Kate's own Ctrl+A/Ctrl+W/etc. On + // ShortcutOverride we accept the event so Kate's registered shortcut does + // not fire; the matching KeyPress is then delivered and we act on it. + if (event->type() == QEvent::ShortcutOverride || event->type() == QEvent::KeyPress) { + auto *ke = static_cast(event); + const bool isAcme = + (ke->modifiers() & Qt::KeyboardModifierMask) == Qt::ControlModifier + && (ke->key() == Qt::Key_A || ke->key() == Qt::Key_E || ke->key() == Qt::Key_H + || ke->key() == Qt::Key_U || ke->key() == Qt::Key_W); + if (isAcme) { + KTextEditor::View *v = m_mainWindow->activeView(); + auto *w = qobject_cast(obj); + const bool inView = v && w && v->isAncestorOf(w); + if (inView) { + if (event->type() == QEvent::ShortcutOverride) { + event->accept(); // deliver as a normal keypress, not a shortcut + return true; + } + dispatchAcmeKey(ke); + return true; // consume the keypress + } + } + } + // Bracket-pair selection: double-clicking next to a bracket selects the // text between the matching pair (multi-line). Falls through to Kate's // default double-click (word select) when no bracket is adjacent. The @@ -670,6 +695,12 @@ void OllieView::onRadialActivated(const QString &actionId) // --------------------------------------------------------------------------- // Acme-style line-editing keys // +// Handled directly in the application event filter rather than via QActions: +// registering QActions for Ctrl+A/W/… would collide with Kate's own actions +// (Select All, Close, …) and Qt would report ambiguous shortcuts. Instead we +// accept the ShortcutOverride for these combos (so Kate's shortcut does not +// fire) and act on the KeyPress, consuming it. +// // Semantics ported from plan9port acme text.c (textbswidth / Ktype): // ^A move to beginning of line (stop at the preceding newline) // ^E move to end of line (stop at the next newline) @@ -679,84 +710,18 @@ void OllieView::onRadialActivated(const QString &actionId) // alphanumeric run (Acme uses C isalnum, which excludes '_') // --------------------------------------------------------------------------- -void OllieView::setupAcmeKeys() +bool OllieView::dispatchAcmeKey(QKeyEvent *ke) { - QWidget *w = m_mainWindow->window(); - - struct Binding { - const char *name; - QKeySequence seq; - void (OllieView::*slot)(); - }; - const Binding bindings[] = { - {"ollie_acme_bol", QKeySequence(Qt::CTRL | Qt::Key_A), &OllieView::acmeBeginningOfLine}, - {"ollie_acme_eol", QKeySequence(Qt::CTRL | Qt::Key_E), &OllieView::acmeEndOfLine}, - {"ollie_acme_erase_char", QKeySequence(Qt::CTRL | Qt::Key_H), &OllieView::acmeEraseChar}, - {"ollie_acme_erase_line", QKeySequence(Qt::CTRL | Qt::Key_U), &OllieView::acmeEraseLine}, - {"ollie_acme_erase_word", QKeySequence(Qt::CTRL | Qt::Key_W), &OllieView::acmeEraseWord}, - }; - - for (const Binding &b : bindings) { - auto *action = new QAction(this); - action->setObjectName(QString::fromLatin1(b.name)); - action->setShortcut(b.seq); - // Override the Kate/KDE defaults (Select All, Close, …) inside the app. - action->setShortcutContext(Qt::ApplicationShortcut); - connect(action, &QAction::triggered, this, b.slot); - if (w) { - w->addAction(action); - } - m_acmeSequences.push_back(b.seq); - m_acmeActions.push_back(action); + if ((ke->modifiers() & Qt::KeyboardModifierMask) != Qt::ControlModifier) { + return false; } - - // Remove our sequences from any pre-existing Kate/KDE action so Qt does not - // report an ambiguous shortcut. New views bring their own copies of these - // actions, so re-run the sweep whenever a view is created. Also defer one - // sweep to the event loop, after the GUI factory has merged all clients. - clearConflictingShortcuts(); - QMetaObject::invokeMethod(this, &OllieView::clearConflictingShortcuts, - Qt::QueuedConnection); - connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, - [this](KTextEditor::View *) { clearConflictingShortcuts(); }); -} - -void OllieView::clearConflictingShortcuts() -{ - KXMLGUIFactory *factory = m_mainWindow->guiFactory(); - if (!factory) { - return; - } - - // Build the quick-lookup set of sequences we own. - const QList clients = factory->clients(); - for (KXMLGUIClient *client : clients) { - KActionCollection *coll = client->actionCollection(); - if (!coll) { - continue; - } - const QList actions = coll->actions(); - for (QAction *a : actions) { - if (!a || m_acmeActions.contains(a)) { - continue; // never strip our own actions - } - const QList scs = a->shortcuts(); - if (scs.isEmpty()) { - continue; - } - QList kept; - kept.reserve(scs.size()); - for (const QKeySequence &s : scs) { - if (!m_acmeSequences.contains(s)) { - kept.push_back(s); - } - } - if (kept.size() != scs.size()) { - a->setShortcuts(kept); - // Also clear any KActionCollection default so it is not restored. - KActionCollection::setDefaultShortcuts(a, kept); - } - } + switch (ke->key()) { + case Qt::Key_A: acmeBeginningOfLine(); return true; + case Qt::Key_E: acmeEndOfLine(); return true; + case Qt::Key_H: acmeEraseChar(); return true; + case Qt::Key_U: acmeEraseLine(); return true; + case Qt::Key_W: acmeEraseWord(); return true; + default: return false; } } diff --git a/src/plugin/ollieplugin.h b/src/plugin/ollieplugin.h index 86b18a4..a134fe8 100644 --- a/src/plugin/ollieplugin.h +++ b/src/plugin/ollieplugin.h @@ -19,6 +19,7 @@ #include class QAction; +class QKeyEvent; namespace KTextEditor { @@ -81,16 +82,15 @@ private: void showRadialAt(int defIndex, const QPoint &globalPos); // centre on a point void onRadialActivated(const QString &actionId); - // Acme-style line-editing keys, bound to override the Kate/KDE defaults. - void setupAcmeKeys(); - // Strip our Acme shortcuts from any other action that also claims them, to - // avoid Qt "ambiguous shortcut" warnings. Safe to call repeatedly. - void clearConflictingShortcuts(); + // Acme-style line-editing keys, handled in the event filter (no QActions, + // so there is nothing for Qt to report as an ambiguous shortcut). void acmeBeginningOfLine(); // C-a void acmeEndOfLine(); // C-e void acmeEraseChar(); // C-h void acmeEraseLine(); // C-u void acmeEraseWord(); // C-w + // Dispatch an Acme key combo. Returns true if it was one of ours. + bool dispatchAcmeKey(QKeyEvent *ke); // Catches configured mouse-button gestures inside the active view. bool eventFilter(QObject *obj, QEvent *event) override; @@ -108,8 +108,6 @@ private: bool m_leftDownInView = false; // LMB held inside the view (Acme chord anchor) bool m_swallowMiddleRelease = false; // swallow the release paired with a consumed middle press bool m_swallowRightRelease = false; // swallow the release paired with a left+right paste chord - QList m_acmeSequences; // shortcuts we own (for conflict clearing) - QList> m_acmeActions; // our Acme actions (never cleared) QHash> m_actionsById; };