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:
parent
1b56cca1a2
commit
bfbb32a95a
10
docs/PLAN.md
10
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`,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue