diff --git a/docs/PLAN.md b/docs/PLAN.md index bbed2bb..f383eb6 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -88,8 +88,12 @@ Therefore the palette is replaced, not extended, and the replacement is built on dead zone, far-flick snaps by angle, descend/ascend navigation. 14 tests. - `src/radial/radialmenu.{h,cpp}`: frameless translucent `QWidget`; paints the current ring (wedges, labels, branch ▸, central "back" hub), highlights the - slice under the pointer, release/click activates a leaf, descends a branch, or - ascends/cancels from the dead zone; Esc ascends/cancels. + slice under the pointer. Two interaction modes from one widget: a **drag + gesture** (opened with a button held — drag out and release to select; a + release that never left the dead zone switches to click mode and keeps the + menu open) and a **discrete click mode** (click a slice to select, click + outside the ring to cancel). Esc ascends/cancels. Opens centred on the given + point without moving the caret. - `src/radial/radialconfig.{h,cpp}`: JSON → radial tree parser (+ built-in default) so multiple radials can be bound to different triggers. 8 tests. - Wired into the plugin: `OllieView::setupRadials()` registers a key trigger diff --git a/src/radial/radialmenu.cpp b/src/radial/radialmenu.cpp index 36bf8ec..210aa14 100644 --- a/src/radial/radialmenu.cpp +++ b/src/radial/radialmenu.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -38,6 +39,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. + m_armed = (QApplication::mouseButtons() != Qt::NoButton); // 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 @@ -149,17 +155,54 @@ void RadialMenu::mouseMoveEvent(QMouseEvent *event) } } -void RadialMenu::mouseReleaseEvent(QMouseEvent *event) +void RadialMenu::mousePressEvent(QMouseEvent *event) { - handleActivationAt(event->position()); + // Accept presses so a Qt::Popup does not treat an in-widget press as an + // outside click and dismiss itself. Selection happens on release. + event->accept(); } -void RadialMenu::handleActivationAt(const QPointF &localPos) +void RadialMenu::mouseReleaseEvent(QMouseEvent *event) { if (!m_model) { return; } - const int slice = m_model->sliceAt(localPos, currentLayout()); + const QPointF pos = event->position(); + + 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); + return; + } + + // 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); +} + +void RadialMenu::handleActivationAt(const QPointF &localPos, bool allowFarFlick) +{ + if (!m_model) { + return; + } + int slice = m_model->sliceAt(localPos, currentLayout()); + if (!allowFarFlick && slice != RadialModel::InvalidSlice) { + const double dist = QLineF(m_center, localPos).length(); + if (dist > m_outerRadius) { + slice = RadialModel::InvalidSlice; // outside the ring => cancel/ascend + } + } + QString id; switch (m_model->resolve(slice, id)) { case RadialModel::Hit::Activate: diff --git a/src/radial/radialmenu.h b/src/radial/radialmenu.h index 64838f9..72388a1 100644 --- a/src/radial/radialmenu.h +++ b/src/radial/radialmenu.h @@ -3,11 +3,14 @@ * * RadialMenu — a frameless, multi-level radial (pie) menu widget. * - * Shows the current ring of a RadialModel centred on a screen point. The slice - * under the pointer is highlighted; releasing over it activates a leaf (emits - * activatedId) or descends into a branch. Releasing in the central dead zone - * ascends one level, or cancels at the root. Works both as a gesture - * (press-drag-release) and as discrete clicks. + * Shows the current ring of a RadialModel centred on a screen point. Two + * interaction modes are supported from a single widget: + * - Drag gesture: opened with a mouse button held (the RMB gesture), the user + * drags toward a slice and releases to select; a release that never left the + * central dead zone instead switches to click mode (the menu stays open). + * - Click mode: discrete hover + click. A click on a slice selects, a click + * outside the ring cancels, the central hub goes back one level or cancels. + * Esc ascends one level, or cancels at the root. */ #ifndef KATECUSTOM_RADIALMENU_H #define KATECUSTOM_RADIALMENU_H @@ -41,19 +44,24 @@ Q_SIGNALS: protected: void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void keyPressEvent(QKeyEvent *event) override; private: RadialLayout currentLayout() const; - void handleActivationAt(const QPointF &localPos); + // allowFarFlick: when true, a point beyond the outer radius still selects + // the slice in that direction (confident drag-flick). When false, such a + // point is treated as "outside" and cancels/ascends (discrete click mode). + void handleActivationAt(const QPointF &localPos, bool allowFarFlick); void finishActivated(const QString &id); void finishCancelled(); 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 double m_innerRadius = 36.0; double m_outerRadius = 130.0;