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:
parent
9ecfd379c2
commit
2e006b34d9
14
docs/PLAN.md
14
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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue