Radial: fix held-drag hover and wrong-slice selection

The opening RMB press leaves an implicit mouse grab on the editor view, so
move/release events during the hold were delivered there, never to the
popup's grabMouse(). Hover froze and the release resolved at a stale
position (always landing on 'More').

Drive the whole gesture from the plugin's application event filter instead,
which reliably sees those events: on press show the radial and set
m_gestureActive; forward each MouseMove to driveHoverGlobal() and the
MouseButtonRelease to driveReleaseGlobal(), both in global coordinates
mapped locally by the widget. The widget's own mouse handlers are gated off
during this external drive (m_externalDrive) to avoid double-handling, and
the manual grabMouse()/releaseMouse() is removed. Key-triggered radials keep
using the widget's local click handling. All 5 ctest suites green.
This commit is contained in:
Levi Neely 2026-10-07 17:00:15 +02:00
parent 9ecfd379c2
commit 2e006b34d9
5 changed files with 88 additions and 36 deletions

View File

@ -98,12 +98,14 @@ Therefore the palette is replaced, not extended, and the replacement is built on
default) so multiple radials can be bound to different triggers. 8 tests.
- Wired into the plugin: `OllieView::setupRadials()` registers a key trigger
(default **Alt+R**) and a **mouse-button gesture** (default **right mouse
button**) per configured radial. The RMB gesture is caught by an application
event filter gated to the active view; it pops the radial at the press point
and suppresses the normal context menu (`MouseButtonPress` + `ContextMenu`
consumed) — an intentional, configured trade-off. The radial grabs the mouse
on popup so an in-progress RMB drag is tracked and the release selects.
Key-triggered radials pop at the caret (`View::cursorPositionCoordinates()`).
button**) per configured radial. Because the opening RMB press leaves an
implicit grab on the editor view, the whole gesture (hover + release) is
driven from the plugin's application event filter in global coordinates
(`driveHoverGlobal`/`driveReleaseGlobal`), not from a popup mouse grab — this
fixes stale hover and wrong-slice selection. The press and the following
`ContextMenu` are consumed, so the context menu is suppressed and the caret
does not move. Key-triggered radials pop at the caret
(`View::cursorPositionCoordinates()`) and use the widget's own click handling.
- A slice may reference any Kate action by objectName, resolved via
`findActionByName`; the special `ollie_command_palette` slice opens the palette.
- Honours the agreed model: radials are triggerable by keys (weapon-wheel style)

View File

@ -305,6 +305,30 @@ void OllieView::setupRadials()
bool OllieView::eventFilter(QObject *obj, QEvent *event)
{
// While a drag gesture is active, drive the radial directly from the raw
// events: the opening press leaves an implicit grab on the editor view, so
// the live move/release events are delivered there, and this application
// filter is the one place that reliably sees them.
if (m_gestureActive) {
switch (event->type()) {
case QEvent::MouseMove:
if (m_radial) {
m_radial->driveHoverGlobal(
static_cast<QMouseEvent *>(event)->globalPosition().toPoint());
}
return true;
case QEvent::MouseButtonRelease:
m_gestureActive = false;
if (m_radial) {
m_radial->driveReleaseGlobal(
static_cast<QMouseEvent *>(event)->globalPosition().toPoint());
}
return true;
default:
break;
}
}
// Suppress the context menu that would otherwise follow an RMB gesture.
if (event->type() == QEvent::ContextMenu
&& m_buttonToRadial.contains(static_cast<int>(Qt::RightButton))) {
@ -332,9 +356,11 @@ bool OllieView::eventFilter(QObject *obj, QEvent *event)
return QObject::eventFilter(obj, event);
}
// Pop the radial at the press position and consume the event so the normal
// context menu does not appear (an intentional, configured trade-off).
// Pop the radial at the press position and begin the drag gesture. The
// press is consumed so the context menu does not appear and the caret is
// not moved (an intentional, configured trade-off).
showRadialAt(m_buttonToRadial.value(button), me->globalPosition().toPoint());
m_gestureActive = true;
return true;
}

View File

@ -88,6 +88,7 @@ private:
RadialMenu *m_radial = nullptr;
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
QHash<QString, QPointer<QAction>> m_actionsById;
};

View File

@ -40,10 +40,11 @@ void RadialMenu::popup(const RadialNode &root, const QPoint &globalCenter)
m_model = std::make_unique<RadialModel>(root);
m_hoverSlice = RadialModel::InvalidSlice;
// If a mouse button is held as we open (the RMB gesture), this is a
// drag-gesture candidate: a release that drags out selects, a release in
// place switches to discrete click mode. Opened from a key (no button) we
// start directly in click mode.
// drag-gesture candidate driven externally by the plugin's event filter
// (the editor view holds the implicit grab, so live events arrive there,
// not here). Opened from a key (no button) we start in local click mode.
m_armed = (QApplication::mouseButtons() != Qt::NoButton);
m_externalDrive = m_armed;
// Size the widget to a square large enough for the outer ring plus labels,
// positioned so the ring centre lands on globalCenter — clamped to the
@ -66,9 +67,6 @@ void RadialMenu::popup(const RadialNode &root, const QPoint &globalCenter)
show();
raise();
setFocus();
// Grab the mouse so an in-progress button-drag (e.g. an RMB gesture that
// began on the editor) is tracked here and the release lands on us.
grabMouse();
update();
}
@ -145,8 +143,8 @@ void RadialMenu::paintEvent(QPaintEvent *)
void RadialMenu::mouseMoveEvent(QMouseEvent *event)
{
if (!m_model) {
return;
if (!m_model || m_externalDrive) {
return; // gesture moves are delivered to the view and driven externally
}
const int slice = m_model->sliceAt(event->position(), currentLayout());
if (slice != m_hoverSlice) {
@ -163,31 +161,48 @@ void RadialMenu::mousePressEvent(QMouseEvent *event)
}
void RadialMenu::mouseReleaseEvent(QMouseEvent *event)
{
if (!m_model || m_externalDrive) {
return; // gesture release is handled via driveReleaseGlobal()
}
// Discrete click mode: require the click to land on the ring; a click
// beyond the outer edge dismisses rather than snapping by angle.
handleActivationAt(event->position(), /*allowFarFlick=*/false);
}
void RadialMenu::driveHoverGlobal(const QPoint &global)
{
if (!m_model) {
return;
}
const QPointF pos = event->position();
const QPointF local = mapFromGlobal(global);
const int slice = m_model->sliceAt(local, currentLayout());
if (slice != m_hoverSlice) {
m_hoverSlice = slice;
update();
}
}
if (m_armed) {
// End of the initial hold from the opening gesture.
m_armed = false;
const int slice = m_model->sliceAt(pos, currentLayout());
if (slice == RadialModel::InvalidSlice) {
// Released in place without dragging out: treat the gesture as a
// click-to-open and stay open for discrete clicks.
m_hoverSlice = RadialModel::InvalidSlice;
update();
return;
}
// Dragged out to a slice: select immediately (far flicks allowed).
handleActivationAt(pos, /*allowFarFlick=*/true);
void RadialMenu::driveReleaseGlobal(const QPoint &global)
{
if (!m_model) {
return;
}
const QPointF local = mapFromGlobal(global);
const int slice = m_model->sliceAt(local, currentLayout());
// Discrete click mode: require the click to land on the ring; a click
// beyond the outer edge dismisses rather than snapping by angle.
handleActivationAt(pos, /*allowFarFlick=*/false);
// End of the opening drag. If the pointer never left the central dead zone
// it was a click-to-open: hand over to local click mode and stay open.
m_armed = false;
m_externalDrive = false;
if (slice == RadialModel::InvalidSlice) {
m_hoverSlice = RadialModel::InvalidSlice;
update();
return;
}
// Dragged out to a slice: select immediately (far flicks allowed). If this
// descends into a submenu the menu stays open in local click mode.
handleActivationAt(local, /*allowFarFlick=*/true);
}
void RadialMenu::handleActivationAt(const QPointF &localPos, bool allowFarFlick)
@ -241,14 +256,12 @@ void RadialMenu::keyPressEvent(QKeyEvent *event)
void RadialMenu::finishActivated(const QString &id)
{
releaseMouse();
hide();
Q_EMIT activatedId(id);
}
void RadialMenu::finishCancelled()
{
releaseMouse();
hide();
Q_EMIT cancelled();
}

View File

@ -38,6 +38,15 @@ public:
*/
void popup(const RadialNode &root, const QPoint &globalCenter);
/*!
* Drive the menu from an external gesture (the plugin's event filter), using
* global coordinates. Needed because the mouse-button press that opens the
* menu leaves an implicit grab on the editor view, so live move/release
* events are delivered there, not to this popup.
*/
void driveHoverGlobal(const QPoint &global);
void driveReleaseGlobal(const QPoint &global);
Q_SIGNALS:
void activatedId(const QString &id);
void cancelled();
@ -61,7 +70,8 @@ private:
std::unique_ptr<RadialModel> m_model;
QPointF m_center; // local widget coordinates of the ring centre
int m_hoverSlice = RadialModel::InvalidSlice;
bool m_armed = false; // opened with a button held => drag-gesture candidate
bool m_armed = false; // opened with a button held => drag candidate
bool m_externalDrive = false; // gesture driven by the plugin event filter
double m_innerRadius = 36.0;
double m_outerRadius = 130.0;