Radial: add window/split radial on the middle mouse button

Second built-in radial 'window' bound to MiddleButton: Split Vertical/
Horizontal, Close Split, Close Others, Toggle Orientation, and a Focus
sub-branch (go_left/right/up/down/next/prev_split_view). Leaf ids are
Kate view-manager action objectNames, resolved via findActionByName.

Route a plain middle press (no left held) to the radial dispatch instead
of unconditionally consuming it: the left+middle Cut chord still fires
(left held), and the X11 middle-click primary paste is still suppressed
on release in both paths. Clear m_swallowMiddleRelease when a gesture
consumes the release.

Example config + builtinHasWindowRadialOnMiddleButton test updated.
This commit is contained in:
Levi Neely 2026-10-08 11:53:19 +02:00
parent 5597dae15d
commit afedc88ac9
5 changed files with 102 additions and 4 deletions

View File

@ -135,6 +135,14 @@ Therefore the palette is replaced, not extended, and the replacement is built on
and by mouse buttons/gestures; the config carries both a key `trigger` and a
mouse `button` per radial.
- Not yet done: free-form stroke gestures (only button-press gesture so far).
- Built-in radials: an **edit** radial (Alt+R / right button) of in-place
actions, and a **window** radial (**middle button**) for split management
(`view_split_vert|horiz`, `view_close_current_space|others`,
`view_split_toggle`, and a "Focus" sub-branch of `go_*_split_view`). A plain
middle press (no left button held) routes to this radial; the Acme chords are
all **left+something**, so a middle press that is part of a chord
(left held) still cuts and never opens the radial. The X11 middle-click
primary paste is suppressed on release either way.
- User-file radial config — DONE. `RadialConfig::load()` reads
`$XDG_CONFIG_HOME/olliepalette/radials.json` when present and non-empty,
otherwise returns `builtinDefault()`; a present-but-broken file also falls

View File

@ -25,6 +25,28 @@
]
}
]
},
{
"name": "window",
"button": "MiddleButton",
"slices": [
{ "label": "Split Vertical", "action": "view_split_vert" },
{ "label": "Split Horizontal", "action": "view_split_horiz" },
{ "label": "Close Split", "action": "view_close_current_space" },
{ "label": "Close Others", "action": "view_close_others" },
{ "label": "Toggle Orientation", "action": "view_split_toggle" },
{
"label": "Focus",
"slices": [
{ "label": "Left", "action": "go_left_split_view" },
{ "label": "Right", "action": "go_right_split_view" },
{ "label": "Up", "action": "go_upward_split_view" },
{ "label": "Down", "action": "go_downward_split_view" },
{ "label": "Next", "action": "go_next_split_view" },
{ "label": "Previous", "action": "go_prev_split_view" }
]
}
]
}
]
}

View File

@ -982,6 +982,7 @@ bool OllieView::eventFilter(QObject *obj, QEvent *event)
return true;
case QEvent::MouseButtonRelease:
m_gestureActive = false;
m_swallowMiddleRelease = false; // the gesture consumed this release
if (m_radial) {
m_radial->driveReleaseGlobal(
static_cast<QMouseEvent *>(event)->globalPosition().toPoint());
@ -1030,14 +1031,23 @@ bool OllieView::eventFilter(QObject *obj, QEvent *event)
if (mev->button() == Qt::MiddleButton && inView) {
if (m_leftDownInView) {
// left + middle = Cut the selection (Acme chord). Consume both
// press and release; never a radial.
if (QAction *cut = findActionByName(QStringLiteral("edit_cut"))) {
cut->trigger();
}
m_swallowMiddleRelease = true;
return true;
}
// Consume press (and its release) to suppress middle-click primary
// paste entirely inside the editor.
// A plain middle press (no chord): always suppress the X11 primary
// paste that fires on release. If the middle button is bound to a
// radial, fall through to the radial dispatch below; otherwise
// consume the press here too.
m_swallowMiddleRelease = true;
return true;
if (!m_buttonToRadial.contains(static_cast<int>(Qt::MiddleButton))) {
return true;
}
// else: do not return — let the radial-trigger dispatch handle it.
}
if (mev->button() == Qt::LeftButton && inView) {
m_leftDownInView = true; // anchor set; selection proceeds normally

View File

@ -171,7 +171,37 @@ QList<RadialDef> RadialConfig::builtinDefault()
more,
};
return {edit};
// A second radial for window/split management, triggered by the middle
// mouse button. Split ops are inherently about window layout, not the caret,
// so a dedicated radial keeps them off the keyboard and out of the edit
// radial. MiddleButton is safe: every Acme chord is left+something, and a
// plain middle press (no left held) is routed here, not to a chord. Leaf ids
// are Kate's view-manager action objectNames.
RadialDef window;
window.name = QStringLiteral("window");
window.mouseButton = QStringLiteral("MiddleButton");
RadialNode focus;
focus.label = QStringLiteral("Focus");
focus.children = {
leaf(QStringLiteral("Left"), QStringLiteral("go_left_split_view")),
leaf(QStringLiteral("Right"), QStringLiteral("go_right_split_view")),
leaf(QStringLiteral("Up"), QStringLiteral("go_upward_split_view")),
leaf(QStringLiteral("Down"), QStringLiteral("go_downward_split_view")),
leaf(QStringLiteral("Next"), QStringLiteral("go_next_split_view")),
leaf(QStringLiteral("Previous"), QStringLiteral("go_prev_split_view")),
};
window.root.children = {
leaf(QStringLiteral("Split Vertical"), QStringLiteral("view_split_vert")),
leaf(QStringLiteral("Split Horizontal"), QStringLiteral("view_split_horiz")),
leaf(QStringLiteral("Close Split"), QStringLiteral("view_close_current_space")),
leaf(QStringLiteral("Close Others"), QStringLiteral("view_close_others")),
leaf(QStringLiteral("Toggle Orientation"), QStringLiteral("view_split_toggle")),
focus,
};
return {edit, window};
}
} // namespace katecustom

View File

@ -129,6 +129,34 @@ private Q_SLOTS:
QVERIFY(hasBranch);
}
void builtinHasWindowRadialOnMiddleButton()
{
const QList<RadialDef> defs = RadialConfig::builtinDefault();
QCOMPARE(defs.size(), 2);
// Find the window radial (second def), triggered by the middle button.
const RadialDef *window = nullptr;
for (const RadialDef &d : defs) {
if (d.name == QStringLiteral("window")) {
window = &d;
}
}
QVERIFY(window);
QCOMPARE(window->mouseButton, QStringLiteral("MiddleButton"));
// It must expose the core split ops by their Kate action objectNames.
QStringList ids;
for (const RadialNode &n : window->root.children) {
ids << n.actionId;
for (const RadialNode &c : n.children) {
ids << c.actionId;
}
}
QVERIFY(ids.contains(QStringLiteral("view_split_vert")));
QVERIFY(ids.contains(QStringLiteral("view_split_horiz")));
QVERIFY(ids.contains(QStringLiteral("view_close_current_space")));
QVERIFY(ids.contains(QStringLiteral("go_left_split_view")));
}
void builtinDefaultHasLauncherSlices()
{
// The "More" branch exposes the Go-to-File / Go-to-Symbol switchers and