diff --git a/docs/PLAN.md b/docs/PLAN.md index f383eb6..44884bc 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -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) diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp index c26f8a3..203d04b 100644 --- a/src/plugin/ollieplugin.cpp +++ b/src/plugin/ollieplugin.cpp @@ -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(event)->globalPosition().toPoint()); + } + return true; + case QEvent::MouseButtonRelease: + m_gestureActive = false; + if (m_radial) { + m_radial->driveReleaseGlobal( + static_cast(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(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; } diff --git a/src/plugin/ollieplugin.h b/src/plugin/ollieplugin.h index cef9c66..29c90fd 100644 --- a/src/plugin/ollieplugin.h +++ b/src/plugin/ollieplugin.h @@ -88,6 +88,7 @@ private: RadialMenu *m_radial = nullptr; QList m_radialDefs; QHash m_buttonToRadial; // Qt::MouseButton (int) -> def index + bool m_gestureActive = false; // an RMB drag gesture is in progress QHash> m_actionsById; }; diff --git a/src/radial/radialmenu.cpp b/src/radial/radialmenu.cpp index 210aa14..54149ab 100644 --- a/src/radial/radialmenu.cpp +++ b/src/radial/radialmenu.cpp @@ -40,10 +40,11 @@ void RadialMenu::popup(const RadialNode &root, const QPoint &globalCenter) m_model = std::make_unique(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(); } diff --git a/src/radial/radialmenu.h b/src/radial/radialmenu.h index 72388a1..a29c1db 100644 --- a/src/radial/radialmenu.h +++ b/src/radial/radialmenu.h @@ -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 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;