1 /* Copyright 2020 Samuel Mannehed for Cendio AB
2 *
3 * This is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This software is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this software; if not, write to the Free Software
15 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
16 * USA.
17 */
18
19 #ifdef HAVE_CONFIG_H
20 #include <config.h>
21 #endif
22
23 #include <math.h>
24
25 #define XK_MISCELLANY
26 #include <rfb/keysymdef.h>
27 #include <rfb/Exception.h>
28 #include <rfb/LogWriter.h>
29
30 #include "i18n.h"
31 #include "Win32TouchHandler.h"
32
33 static rfb::LogWriter vlog("Win32TouchHandler");
34
35 static const DWORD MOUSEMOVE_FLAGS = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE |
36 MOUSEEVENTF_VIRTUALDESK;
37
38 static const unsigned SINGLE_PAN_THRESHOLD = 50;
39
Win32TouchHandler(HWND hWnd)40 Win32TouchHandler::Win32TouchHandler(HWND hWnd) :
41 hWnd(hWnd), gesturesConfigured(false), gestureActive(false),
42 ignoringGesture(false), fakeButtonMask(0)
43 {
44 // If window is registered as touch we can not receive gestures,
45 // this should not happen
46 if (IsTouchWindow(hWnd, NULL))
47 throw rfb::Exception(_("Window is registered for touch instead of gestures"));
48
49 // We will not receive any touch/gesture events if this service
50 // isn't running - Logging is enough
51 if (!GetSystemMetrics(SM_DIGITIZER))
52 vlog.debug("The 'Tablet PC Input' service is required for touch");
53
54 // When we have less than two touch points we won't receive all
55 // gesture events - Logging is enough
56 int supportedTouches = GetSystemMetrics(SM_MAXIMUMTOUCHES);
57 if (supportedTouches < 2)
58 vlog.debug("Two touch points required, system currently supports: %d",
59 supportedTouches);
60 }
61
processEvent(UINT Msg,WPARAM wParam,LPARAM lParam)62 bool Win32TouchHandler::processEvent(UINT Msg, WPARAM wParam, LPARAM lParam)
63 {
64 GESTUREINFO gi;
65
66 DWORD panWant = GC_PAN_WITH_SINGLE_FINGER_VERTICALLY |
67 GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY |
68 GC_PAN;
69 DWORD panBlock = GC_PAN_WITH_INERTIA | GC_PAN_WITH_GUTTER;
70
71 GESTURECONFIG gc[] = {{GID_ZOOM, GC_ZOOM, 0},
72 {GID_PAN, panWant, panBlock},
73 {GID_TWOFINGERTAP, GC_TWOFINGERTAP, 0}};
74
75 switch(Msg) {
76 case WM_GESTURENOTIFY:
77 if (gesturesConfigured)
78 return false;
79
80 if (!SetGestureConfig(hWnd, 0, 3, gc, sizeof(GESTURECONFIG))) {
81 vlog.error(_("Failed to set gesture configuration (error 0x%x)"),
82 (int)GetLastError());
83 }
84 gesturesConfigured = true;
85 // Windows expects all handler functions to always
86 // pass this message on, and not consume it
87 return false;
88 case WM_GESTURE:
89 ZeroMemory(&gi, sizeof(GESTUREINFO));
90 gi.cbSize = sizeof(GESTUREINFO);
91
92 if (!GetGestureInfo((HGESTUREINFO)lParam, &gi)) {
93 vlog.error(_("Failed to get gesture information (error 0x%x)"),
94 (int)GetLastError());
95 return true;
96 }
97
98 handleWin32GestureEvent(gi);
99
100 CloseGestureInfoHandle((HGESTUREINFO)lParam);
101 return true;
102 }
103
104 return false;
105 }
106
handleWin32GestureEvent(GESTUREINFO gi)107 void Win32TouchHandler::handleWin32GestureEvent(GESTUREINFO gi)
108 {
109 GestureEvent gev;
110 POINT pos;
111
112 if (gi.dwID == GID_BEGIN) {
113 // FLTK gets very confused if the cursor position is outside
114 // of the window when getting mouse events, so we start by
115 // moving the cursor to something proper.
116 // FIXME: Only do this when necessary?
117 // FIXME: There is some odd delay before Windows fully updates
118 // the state of the cursor position. By doing it here in
119 // GID_BEGIN we hope to do it early enough that we don't
120 // get any odd effects.
121 // FIXME: GF_BEGIN position can differ from GID_BEGIN pos.
122
123 SetCursorPos(gi.ptsLocation.x, gi.ptsLocation.y);
124 return;
125 } else if (gi.dwID == GID_END) {
126 gestureActive = false;
127 ignoringGesture = false;
128 return;
129 }
130
131 // The GID_BEGIN msg means that no fingers were previously touching,
132 // and a completely new set of gestures is beginning.
133 // The GF_BEGIN flag means a new type of gesture was detected. This
134 // flag can be set on a msg when changing between gestures within
135 // one set of touches.
136 //
137 // We don't support dynamically changing between gestures
138 // without lifting the finger(s).
139 if ((gi.dwFlags & GF_BEGIN) && gestureActive)
140 ignoringGesture = true;
141 if (ignoringGesture)
142 return;
143
144 if (gi.dwFlags & GF_BEGIN) {
145 gev.type = GestureBegin;
146 } else if (gi.dwFlags & GF_END) {
147 gev.type = GestureEnd;
148 } else {
149 gev.type = GestureUpdate;
150 }
151
152 // Convert to relative coordinates
153 pos.x = gi.ptsLocation.x;
154 pos.y = gi.ptsLocation.y;
155 ScreenToClient(gi.hwndTarget, &pos);
156 gev.eventX = pos.x;
157 gev.eventY = pos.y;
158
159 switch(gi.dwID) {
160
161 case GID_ZOOM:
162 gev.gesture = GesturePinch;
163 if (gi.dwFlags & GF_BEGIN) {
164 gestureStart.x = pos.x;
165 gestureStart.y = pos.y;
166 } else {
167 gev.eventX = gestureStart.x;
168 gev.eventY = gestureStart.y;
169 }
170 gev.magnitudeX = gi.ullArguments;
171 gev.magnitudeY = 0;
172 break;
173
174 case GID_PAN:
175 if (isSinglePan(gi)) {
176 if (gi.dwFlags & GF_BEGIN) {
177 gestureStart.x = pos.x;
178 gestureStart.y = pos.y;
179 startedSinglePan = false;
180
181 }
182
183 // FIXME: Add support for sending a OneFingerTap gesture here.
184 // When the movement was very small and we get a GF_END
185 // within a short time we should consider it a tap.
186
187 if (!startedSinglePan &&
188 ((unsigned)abs(gestureStart.x - pos.x) < SINGLE_PAN_THRESHOLD) &&
189 ((unsigned)abs(gestureStart.y - pos.y) < SINGLE_PAN_THRESHOLD))
190 return;
191
192 // Here we know we got a single pan!
193
194 // Change the first GestureUpdate to GestureBegin
195 // after we passed the threshold
196 if (!startedSinglePan) {
197 startedSinglePan = true;
198 gev.type = GestureBegin;
199 gev.eventX = gestureStart.x;
200 gev.eventY = gestureStart.y;
201 }
202
203 gev.gesture = GestureDrag;
204
205 } else {
206 if (gi.dwFlags & GF_BEGIN) {
207 gestureStart.x = pos.x;
208 gestureStart.y = pos.y;
209 gev.magnitudeX = 0;
210 gev.magnitudeY = 0;
211 } else {
212 gev.eventX = gestureStart.x;
213 gev.eventY = gestureStart.y;
214 gev.magnitudeX = pos.x - gestureStart.x;
215 gev.magnitudeY = pos.y - gestureStart.y;
216 }
217
218 gev.gesture = GestureTwoDrag;
219 }
220 break;
221
222 case GID_TWOFINGERTAP:
223 gev.gesture = GestureTwoTap;
224 break;
225
226 }
227
228 gestureActive = true;
229
230 BaseTouchHandler::handleGestureEvent(gev);
231
232 // Since we have a threshold for GestureDrag we need to generate
233 // a second event right away with the current position
234 if ((gev.type == GestureBegin) && (gev.gesture == GestureDrag)) {
235 gev.type = GestureUpdate;
236 gev.eventX = pos.x;
237 gev.eventY = pos.y;
238 BaseTouchHandler::handleGestureEvent(gev);
239 }
240
241 // FLTK tends to reset the cursor to the real position so we
242 // need to make sure that we update that position
243 if (gev.type == GestureEnd) {
244 POINT expectedPos;
245 POINT currentPos;
246
247 expectedPos = lastFakeMotionPos;
248 ClientToScreen(hWnd, &expectedPos);
249 GetCursorPos(¤tPos);
250
251 if ((expectedPos.x != currentPos.x) ||
252 (expectedPos.y != currentPos.y))
253 SetCursorPos(expectedPos.x, expectedPos.y);
254 }
255 }
256
isSinglePan(GESTUREINFO gi)257 bool Win32TouchHandler::isSinglePan(GESTUREINFO gi)
258 {
259 // To differentiate between a single and a double pan we can look
260 // at ullArguments. This shows the distance between the touch points,
261 // but in the case of single pan, it seems to show the monitor's
262 // origin value (this is not documented by microsoft). This origin
263 // value seems to be relative to the screen's position in a multi
264 // monitor setup. For example if the touch monitor is secondary and
265 // positioned to the left of the primary, the origin is negative.
266 //
267 // To use this we need to get the monitor's origin value and check
268 // if it is the same as ullArguments. If they match, we have a
269 // single pan.
270
271 POINT coordinates;
272 HMONITOR monitorHandler;
273 MONITORINFO mi;
274 LONG lowestX;
275
276 // Find the monitor with the touch event
277 coordinates.x = gi.ptsLocation.x;
278 coordinates.y = gi.ptsLocation.y;
279 monitorHandler = MonitorFromPoint(coordinates,
280 MONITOR_DEFAULTTOPRIMARY);
281
282 // Find the monitor's origin
283 ZeroMemory(&mi, sizeof(MONITORINFO));
284 mi.cbSize = sizeof(MONITORINFO);
285 GetMonitorInfo(monitorHandler, &mi);
286 lowestX = mi.rcMonitor.left;
287
288 return lowestX == (LONG)gi.ullArguments;
289 }
290
fakeMotionEvent(const GestureEvent origEvent)291 void Win32TouchHandler::fakeMotionEvent(const GestureEvent origEvent)
292 {
293 UINT Msg = WM_MOUSEMOVE;
294 WPARAM wParam = MAKEWPARAM(fakeButtonMask, 0);
295 LPARAM lParam = MAKELPARAM(origEvent.eventX, origEvent.eventY);
296
297 pushFakeEvent(Msg, wParam, lParam);
298 lastFakeMotionPos.x = origEvent.eventX;
299 lastFakeMotionPos.y = origEvent.eventY;
300 }
301
fakeButtonEvent(bool press,int button,const GestureEvent origEvent)302 void Win32TouchHandler::fakeButtonEvent(bool press, int button,
303 const GestureEvent origEvent)
304 {
305 UINT Msg;
306 WPARAM wParam;
307 LPARAM lParam;
308 int delta;
309
310 switch (button) {
311
312 case 1: // left mousebutton
313 if (press) {
314 Msg = WM_LBUTTONDOWN;
315 fakeButtonMask |= MK_LBUTTON;
316 } else {
317 Msg = WM_LBUTTONUP;
318 fakeButtonMask &= ~MK_LBUTTON;
319 }
320 break;
321 case 2: // middle mousebutton
322 if (press) {
323 Msg = WM_MBUTTONDOWN;
324 fakeButtonMask |= MK_MBUTTON;
325 } else {
326 Msg = WM_MBUTTONUP;
327 fakeButtonMask &= ~MK_MBUTTON;
328 }
329 break;
330 case 3: // right mousebutton
331 if (press) {
332 Msg = WM_RBUTTONDOWN;
333 fakeButtonMask |= MK_RBUTTON;
334 } else {
335 Msg = WM_RBUTTONUP;
336 fakeButtonMask &= ~MK_RBUTTON;
337 }
338 break;
339
340 case 4: // scroll up
341 Msg = WM_MOUSEWHEEL;
342 delta = WHEEL_DELTA;
343 break;
344 case 5: // scroll down
345 Msg = WM_MOUSEWHEEL;
346 delta = -WHEEL_DELTA;
347 break;
348 case 6: // scroll left
349 Msg = WM_MOUSEHWHEEL;
350 delta = -WHEEL_DELTA;
351 break;
352 case 7: // scroll right
353 Msg = WM_MOUSEHWHEEL;
354 delta = WHEEL_DELTA;
355 break;
356
357 default:
358 vlog.error(_("Invalid mouse button %d, must be a number between 1 and 7."),
359 button);
360 return;
361 }
362
363 if (1 <= button && button <= 3) {
364 wParam = MAKEWPARAM(fakeButtonMask, 0);
365
366 // Regular mouse events expect client coordinates
367 lParam = MAKELPARAM(origEvent.eventX, origEvent.eventY);
368 } else {
369 POINT pos;
370
371 // Only act on wheel press, not on release
372 if (!press)
373 return;
374
375 wParam = MAKEWPARAM(fakeButtonMask, delta);
376
377 // Wheel events require screen coordinates
378 pos.x = (LONG)origEvent.eventX;
379 pos.y = (LONG)origEvent.eventY;
380
381 ClientToScreen(hWnd, &pos);
382 lParam = MAKELPARAM(pos.x, pos.y);
383 }
384
385 pushFakeEvent(Msg, wParam, lParam);
386 }
387
fakeKeyEvent(bool press,int keysym,const GestureEvent origEvent)388 void Win32TouchHandler::fakeKeyEvent(bool press, int keysym,
389 const GestureEvent origEvent)
390 {
391 UINT Msg = press ? WM_KEYDOWN : WM_KEYUP;
392 WPARAM wParam;
393 LPARAM lParam;
394 int vKey;
395 int scanCode;
396 int previousKeyState = press ? 0 : 1;
397 int transitionState = press ? 0 : 1;
398
399 switch(keysym) {
400
401 case XK_Control_L:
402 vKey = VK_CONTROL;
403 scanCode = 0x1d;
404 if (press)
405 fakeButtonMask |= MK_CONTROL;
406 else
407 fakeButtonMask &= ~MK_CONTROL;
408 break;
409
410 // BaseTouchHandler will currently not send SHIFT but we keep it for
411 // completeness sake. This way we have coverage for all wParam's MK_-bits.
412 case XK_Shift_L:
413 vKey = VK_SHIFT;
414 scanCode = 0x2a;
415 if (press)
416 fakeButtonMask |= MK_SHIFT;
417 else
418 fakeButtonMask &= ~MK_SHIFT;
419 break;
420
421 default:
422 //FIXME: consider adding generic handling
423 vlog.error(_("Unhandled key 0x%x - can't generate keyboard event."),
424 keysym);
425 return;
426 }
427
428 wParam = MAKEWPARAM(vKey, 0);
429
430 scanCode <<= 0;
431 previousKeyState <<= 14;
432 transitionState <<= 15;
433 lParam = MAKELPARAM(1, // RepeatCount
434 (scanCode | previousKeyState | transitionState));
435
436 pushFakeEvent(Msg, wParam, lParam);
437 }
438
pushFakeEvent(UINT Msg,WPARAM wParam,LPARAM lParam)439 void Win32TouchHandler::pushFakeEvent(UINT Msg, WPARAM wParam, LPARAM lParam)
440 {
441 PostMessage(hWnd, Msg, wParam, lParam);
442 }
443