-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdwellmanager.cpp
More file actions
228 lines (202 loc) · 8 KB
/
Copy pathdwellmanager.cpp
File metadata and controls
228 lines (202 loc) · 8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#include "dwellmanager.h"
#include "clickinjector.h"
#include <QCursor>
#include <cmath>
DwellManager::DwellManager(QObject* parent)
: QObject(parent)
{
m_pollTimer.setInterval(50); // 20 Hz
connect(&m_pollTimer, &QTimer::timeout, this, &DwellManager::onPoll);
m_pollTimer.start();
}
DwellManager::~DwellManager()
{
// Safety net only. MainWindow releases from aboutToQuit, while the event
// loop and the platform injector are still healthy; by the time we are
// destroyed this is normally a no-op. It stays because a drag surviving
// the process would leave the button held system-wide.
releaseHeldButton();
}
void DwellManager::releaseHeldButton()
{
if (!m_dragActive) return;
// When the Down fired, fireSelected() already flipped m_clickType to the
// paired Up, so injecting it now is exactly the release. Restore the Down
// type afterwards (as the 10-second safety release below does) to leave the
// state machine ready for the next drag.
m_clickFn(m_clickType, m_cursorPosFn(), m_modifiers);
m_clickType = (m_clickType == ClickType::LeftUp) ? ClickType::LeftDown
: ClickType::RightDown;
m_dragActive = false;
}
void DwellManager::arm(ClickType type, int modifiers)
{
// If a drag Down was fired but not yet released, release it now before
// switching to a different action — otherwise the button stays held.
releaseHeldButton();
m_clickType = type;
m_modifiers = modifiers;
m_armed = true;
m_waiting = false;
m_hovering = false;
m_dragActive = false;
m_anchorPos = m_cursorPosFn();
m_lastPos = m_anchorPos;
m_hoverStartMs = m_nowFn();
}
void DwellManager::disarm()
{
// If a drag Down was fired but not released, release it to avoid stuck buttons.
releaseHeldButton();
m_armed = false;
m_waiting = false;
m_hovering = false;
emit dwellProgress(0.0f);
}
void DwellManager::fireNow()
{
if (!m_armed) return;
const QPoint cur = m_cursorPosFn();
fireSelected(cur);
// In audio-trigger mode there is no movement-based re-arm: each audio cue
// should be able to fire again right away. A drag still alternates
// Down/Up across cues (m_dragActive stays set), so only clear the
// movement-wait state that fireSelected leaves behind for ordinary clicks.
if (!m_dragActive) {
m_waiting = false;
m_hovering = false;
m_anchorPos = cur;
m_hoverStartMs = m_nowFn();
}
}
void DwellManager::onPoll()
{
if (!m_armed) return;
// In audio-trigger mode the cursor-stillness countdown is disabled; the
// action fires only when fireNow() is called from the audio cue.
if (m_audioMode) return;
QPoint cur = m_cursorPosFn();
auto dist = [](QPoint a, QPoint b) -> double {
double dx = a.x() - b.x();
double dy = a.y() - b.y();
return std::sqrt(dx*dx + dy*dy);
};
if (m_waiting) {
// Exit waiting when the cursor moves far enough away from the fire point,
// OR after one full dwell period — whichever comes first. The timeout
// handles Wayland compositor pointer grabs (e.g. right-click context menus)
// that freeze XQueryPointer so movement is never detected.
qint64 waitedMs = m_nowFn() - m_waitStartMs;
bool movedAway = dist(cur, m_anchorPos) > m_sensitivityPx * 2;
bool timedOut = waitedMs >= m_dwellMs;
if (movedAway || timedOut) {
m_waiting = false;
m_hovering = false;
m_anchorPos = cur;
m_lastPos = cur;
m_hoverStartMs = m_nowFn();
if (timedOut && !movedAway && !m_repeatOnDwell) {
// Cursor position appeared frozen throughout the entire wait —
// most likely the pointer is outside the XWayland session on
// Wayland, so XQueryPointer returns stale data and movement
// cannot be detected. Disarm to prevent repeated unintended
// clicks; the user re-arms by hovering over a button again.
// In repeat mode the user explicitly wants continuous firing,
// so we let waiting exit normally and allow the next dwell.
m_armed = false;
emit dwellProgress(0.0f);
}
}
return;
}
// Safety release: if a drag button has been held for more than 10 seconds
// (e.g. XQueryPointer glitches keep resetting the hover countdown), force
// the Up event so the user is never permanently locked out.
if (m_dragActive) {
qint64 heldMs = m_nowFn() - m_dragStartMs;
if (heldMs >= 10000) {
m_clickFn(m_clickType, cur, m_modifiers);
m_clickType = (m_clickType == ClickType::LeftUp) ? ClickType::LeftDown : ClickType::RightDown;
m_dragActive = false;
m_waiting = true;
m_hovering = false;
m_waitStartMs = m_nowFn();
emit dwellProgress(0.0f);
return;
}
}
if (dist(cur, m_anchorPos) > m_sensitivityPx) {
// Cursor moved — reset hover countdown
m_anchorPos = cur;
m_lastPos = cur;
m_hovering = false;
m_hoverStartMs = m_nowFn();
emit dwellProgress(0.0f);
return;
}
// Cursor is still within sensitivity radius
if (!m_hovering) {
m_hovering = true;
m_hoverStartMs = m_nowFn();
}
qint64 elapsed = m_nowFn() - m_hoverStartMs;
float frac = static_cast<float>(elapsed) / static_cast<float>(m_dwellMs);
frac = qBound(0.0f, frac, 1.0f);
emit dwellProgress(frac);
if (elapsed >= m_dwellMs)
fireSelected(cur);
}
void DwellManager::fireSelected(QPoint cur)
{
emit dwellAboutToFire(cur, m_clickType);
bool isScroll = (m_clickType == ClickType::ScrollUp ||
m_clickType == ClickType::ScrollDown ||
m_clickType == ClickType::ScrollLeft ||
m_clickType == ClickType::ScrollRight);
int reps = isScroll ? m_scrollRepeat : 1;
for (int i = 0; i < reps; ++i)
m_clickFn(m_clickType, cur, m_modifiers);
emit dwellFired(cur, m_clickType);
// For drag (Down) events: skip the movement-wait entirely and go straight
// into the next hover countdown for the paired Up event. This is critical
// on Linux where the OS button grab can freeze the cursor position, making
// movement-based re-arm impossible. The user drags to a destination and
// dwells to release; if the cursor IS frozen the countdown still fires Up.
if (m_clickType == ClickType::LeftDown) {
m_clickType = ClickType::LeftUp;
m_dragActive = true;
m_dragStartMs = m_nowFn();
m_anchorPos = cur;
m_lastPos = cur;
m_hovering = false;
emit dwellProgress(0.0f);
return;
}
if (m_clickType == ClickType::RightDown) {
m_clickType = ClickType::RightUp;
m_dragActive = true;
m_dragStartMs = m_nowFn();
m_anchorPos = cur;
m_lastPos = cur;
m_hovering = false;
emit dwellProgress(0.0f);
return;
}
// For Up events (completing a drag), restore the original Down type.
if (m_clickType == ClickType::LeftUp && m_dragActive) {
m_clickType = ClickType::LeftDown;
m_dragActive = false;
} else if (m_clickType == ClickType::RightUp && m_dragActive) {
m_clickType = ClickType::RightDown;
m_dragActive = false;
}
// Wait for cursor to move before allowing next dwell (prevents an
// immediate double-fire at the same location). In one-shot mode the
// timeout branch in the waiting block disarms to avoid repeat firing;
// in repeat mode it lets the next dwell proceed normally.
m_anchorPos = cur;
m_waiting = true;
m_hovering = false;
m_waitStartMs = m_nowFn();
emit dwellProgress(0.0f);
}