Acme keys: handle via ShortcutOverride/KeyPress, not QActions

Registering QActions for Ctrl+A/E/H/U/W kept colliding with Kate's own
actions (Select All, Close, …) — Qt reported ambiguous shortcuts and the
per-view shortcut-stripping sweep was fragile and incomplete.

Drop the QActions and the clearConflictingShortcuts machinery entirely.
Handle the keys in the application event filter: accept QEvent::ShortcutOverride
for our combos (so Kate's registered shortcut never fires) and act on the
following QEvent::KeyPress, consuming it. No shortcut is registered, so there
is nothing to be ambiguous with, and new views need no special handling.
All 5 ctest suites green.
This commit is contained in:
Levi Neely 2026-10-07 17:46:51 +02:00
parent 1b56cca1a2
commit bfbb32a95a
3 changed files with 52 additions and 89 deletions

View File

@ -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`,

View File

@ -21,6 +21,7 @@
#include <QApplication>
#include <QEvent>
#include <QHash>
#include <QKeyEvent>
#include <QKeySequence>
#include <QMouseEvent>
#include <QSet>
@ -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<QKeyEvent *>(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<QWidget *>(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<KXMLGUIClient *> clients = factory->clients();
for (KXMLGUIClient *client : clients) {
KActionCollection *coll = client->actionCollection();
if (!coll) {
continue;
}
const QList<QAction *> actions = coll->actions();
for (QAction *a : actions) {
if (!a || m_acmeActions.contains(a)) {
continue; // never strip our own actions
}
const QList<QKeySequence> scs = a->shortcuts();
if (scs.isEmpty()) {
continue;
}
QList<QKeySequence> 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;
}
}

View File

@ -19,6 +19,7 @@
#include <QList>
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<QKeySequence> m_acmeSequences; // shortcuts we own (for conflict clearing)
QList<QPointer<QAction>> m_acmeActions; // our Acme actions (never cleared)
QHash<QString, QPointer<QAction>> m_actionsById;
};