Add Acme-style left+middle mouse chord = Cut

Left-button chords only (right button stays the radial). With the left
button held while selecting, a middle-button tap cuts the current selection
and is consumed, so it does not also trigger a primary-selection paste;
middle-click without the left held still pastes as usual.

Implemented in the shared application event filter: track the left button
held inside the active view, and on a middle press while held trigger
edit_cut. The filter is now installed unconditionally (it serves both the
radial gesture and the chord). All 5 ctest suites green.
This commit is contained in:
Levi Neely 2026-10-07 17:11:29 +02:00
parent dca2514023
commit d117aebb7f
3 changed files with 41 additions and 8 deletions

View File

@ -128,6 +128,16 @@ Therefore the palette is replaced, not extended, and the replacement is built on
- Not yet done: free-form stroke gestures (only button-press gesture so far);
user-file config loading (built-in default only); frecency.
### Mouse chording (Acme-style) — DONE (first increment)
- Left+middle chord = **Cut**: with the left button held while selecting
(button 1 anchor), a tap of the middle button (button 2) cuts the current
selection. Driven by the same application event filter — it tracks the left
button held inside the view and, on a middle press while held, triggers
`edit_cut` and consumes the event. Middle-click alone (no left held) still
does the usual primary-selection paste.
- Right-button chords are intentionally **not** ported — the right button is the
radial gesture. (Revisit later.)
### M5 — Command vocabulary + project model + switchers — NEXT
- `:`-verb pack via `KTextEditor::Command`: `sort`, `align`, `json`, `b64`,
`uuid`, `case`, `pipe <shell>`, etc. These populate M-x and the radial.

View File

@ -67,6 +67,11 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
}
setupRadials();
// One application-wide filter serves both the mouse-button radial gesture
// and the Acme-style left+middle chord, catching presses no matter which
// internal child of the view actually receives them.
qApp->installEventFilter(this);
}
OllieView::~OllieView()
@ -284,7 +289,6 @@ void OllieView::setupRadials()
m_radialDefs = RadialConfig::builtinDefault();
QWidget *w = m_mainWindow->window();
bool wantMouseGestures = false;
for (int i = 0; i < m_radialDefs.size(); ++i) {
const RadialDef &def = m_radialDefs.at(i);
@ -312,16 +316,9 @@ void OllieView::setupRadials()
}
if (btn != Qt::NoButton) {
m_buttonToRadial.insert(static_cast<int>(btn), i);
wantMouseGestures = true;
}
}
}
// A single application-wide filter catches the gesture no matter which
// internal child of the view actually receives the press.
if (wantMouseGestures) {
qApp->installEventFilter(this);
}
}
bool OllieView::eventFilter(QObject *obj, QEvent *event)
@ -350,6 +347,31 @@ bool OllieView::eventFilter(QObject *obj, QEvent *event)
}
}
// Acme-style left+middle chord = Cut. Button 1 (left) held while selecting
// is the anchor; a tap of button 2 (middle) cuts the current selection.
// Right-button chords are intentionally not ported (RMB is the radial).
// Middle-click alone (no left held) still does the usual primary paste.
if (event->type() == QEvent::MouseButtonRelease) {
if (static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton) {
m_leftDownInView = false;
}
} else if (event->type() == QEvent::MouseButtonPress) {
auto *mev = static_cast<QMouseEvent *>(event);
KTextEditor::View *v = m_mainWindow->activeView();
auto *w = qobject_cast<QWidget *>(obj);
const bool inView = v && w && v->isAncestorOf(w);
if (mev->button() == Qt::MiddleButton && m_leftDownInView && inView) {
if (QAction *cut = findActionByName(QStringLiteral("edit_cut"))) {
cut->trigger();
}
return true; // consume: this is the chord, not a primary paste
}
if (mev->button() == Qt::LeftButton && inView) {
m_leftDownInView = true; // anchor set; selection proceeds normally
}
}
// Suppress the context menu that would otherwise follow an RMB gesture.
if (event->type() == QEvent::ContextMenu
&& m_buttonToRadial.contains(static_cast<int>(Qt::RightButton))) {

View File

@ -89,6 +89,7 @@ private:
QList<RadialDef> m_radialDefs;
QHash<int, int> m_buttonToRadial; // Qt::MouseButton (int) -> def index
bool m_gestureActive = false; // an RMB drag gesture is in progress
bool m_leftDownInView = false; // LMB held inside the view (Acme chord anchor)
QHash<QString, QPointer<QAction>> m_actionsById;
};