1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 /*
7  * nsWinGesture - Touch input handling for tablet displays.
8  */
9 
10 #include "nscore.h"
11 #include "nsWinGesture.h"
12 #include "nsUXThemeData.h"
13 #include "nsIDOMWheelEvent.h"
14 #include "mozilla/Logging.h"
15 #include "mozilla/MouseEvents.h"
16 #include "mozilla/Preferences.h"
17 #include "mozilla/TouchEvents.h"
18 #include "mozilla/dom/SimpleGestureEventBinding.h"
19 
20 #include <cmath>
21 
22 using namespace mozilla;
23 using namespace mozilla::widget;
24 
25 extern mozilla::LazyLogModule gWindowsLog;
26 
27 static bool gEnableSingleFingerPanEvents = false;
28 
nsWinGesture()29 nsWinGesture::nsWinGesture()
30     : mPanActive(false),
31       mFeedbackActive(false),
32       mXAxisFeedback(false),
33       mYAxisFeedback(false),
34       mPanInertiaActive(false) {
35   (void)InitLibrary();
36   mPixelScrollOverflow = 0;
37 }
38 
39 /* Load and shutdown */
40 
InitLibrary()41 bool nsWinGesture::InitLibrary() {
42   // Check to see if we want single finger gesture input. Only do this once
43   // for the app so we don't have to look it up on every window create.
44   gEnableSingleFingerPanEvents =
45       Preferences::GetBool("gestures.enable_single_finger_input", false);
46 
47   return true;
48 }
49 
50 #define GCOUNT 5
51 
SetWinGestureSupport(HWND hWnd,WidgetGestureNotifyEvent::PanDirection aDirection)52 bool nsWinGesture::SetWinGestureSupport(
53     HWND hWnd, WidgetGestureNotifyEvent::PanDirection aDirection) {
54   GESTURECONFIG config[GCOUNT];
55 
56   memset(&config, 0, sizeof(config));
57 
58   config[0].dwID = GID_ZOOM;
59   config[0].dwWant = GC_ZOOM;
60   config[0].dwBlock = 0;
61 
62   config[1].dwID = GID_ROTATE;
63   config[1].dwWant = GC_ROTATE;
64   config[1].dwBlock = 0;
65 
66   config[2].dwID = GID_PAN;
67   config[2].dwWant = GC_PAN | GC_PAN_WITH_INERTIA | GC_PAN_WITH_GUTTER;
68   config[2].dwBlock = GC_PAN_WITH_SINGLE_FINGER_VERTICALLY |
69                       GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY;
70 
71   if (gEnableSingleFingerPanEvents) {
72     if (aDirection == WidgetGestureNotifyEvent::ePanVertical ||
73         aDirection == WidgetGestureNotifyEvent::ePanBoth) {
74       config[2].dwWant |= GC_PAN_WITH_SINGLE_FINGER_VERTICALLY;
75       config[2].dwBlock -= GC_PAN_WITH_SINGLE_FINGER_VERTICALLY;
76     }
77 
78     if (aDirection == WidgetGestureNotifyEvent::ePanHorizontal ||
79         aDirection == WidgetGestureNotifyEvent::ePanBoth) {
80       config[2].dwWant |= GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY;
81       config[2].dwBlock -= GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY;
82     }
83   }
84 
85   config[3].dwWant = GC_TWOFINGERTAP;
86   config[3].dwID = GID_TWOFINGERTAP;
87   config[3].dwBlock = 0;
88 
89   config[4].dwWant = GC_PRESSANDTAP;
90   config[4].dwID = GID_PRESSANDTAP;
91   config[4].dwBlock = 0;
92 
93   return SetGestureConfig(hWnd, 0, GCOUNT, (PGESTURECONFIG)&config,
94                           sizeof(GESTURECONFIG));
95 }
96 
97 /* Helpers */
98 
IsPanEvent(LPARAM lParam)99 bool nsWinGesture::IsPanEvent(LPARAM lParam) {
100   GESTUREINFO gi;
101 
102   ZeroMemory(&gi, sizeof(GESTUREINFO));
103   gi.cbSize = sizeof(GESTUREINFO);
104 
105   BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi);
106   if (!result) return false;
107 
108   if (gi.dwID == GID_PAN) return true;
109 
110   return false;
111 }
112 
113 /* Gesture event processing */
114 
ProcessGestureMessage(HWND hWnd,WPARAM wParam,LPARAM lParam,WidgetSimpleGestureEvent & evt)115 bool nsWinGesture::ProcessGestureMessage(HWND hWnd, WPARAM wParam,
116                                          LPARAM lParam,
117                                          WidgetSimpleGestureEvent& evt) {
118   GESTUREINFO gi;
119 
120   ZeroMemory(&gi, sizeof(GESTUREINFO));
121   gi.cbSize = sizeof(GESTUREINFO);
122 
123   BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi);
124   if (!result) return false;
125 
126   // The coordinates of this event
127   nsPointWin coord;
128   coord = gi.ptsLocation;
129   coord.ScreenToClient(hWnd);
130 
131   evt.mRefPoint = LayoutDeviceIntPoint(coord.x, coord.y);
132 
133   // Multiple gesture can occur at the same time so gesture state
134   // info can't be shared.
135   switch (gi.dwID) {
136     case GID_BEGIN:
137     case GID_END:
138       // These should always fall through to DefWndProc
139       return false;
140       break;
141 
142     case GID_ZOOM: {
143       if (gi.dwFlags & GF_BEGIN) {
144         // Send a zoom start event
145 
146         // The low 32 bits are the distance in pixels.
147         mZoomIntermediate = (float)gi.ullArguments;
148 
149         evt.mMessage = eMagnifyGestureStart;
150         evt.mDelta = 0.0;
151       } else if (gi.dwFlags & GF_END) {
152         // Send a zoom end event, the delta is the change
153         // in touch points.
154         evt.mMessage = eMagnifyGesture;
155         // (positive for a "zoom in")
156         evt.mDelta = -1.0 * (mZoomIntermediate - (float)gi.ullArguments);
157         mZoomIntermediate = (float)gi.ullArguments;
158       } else {
159         // Send a zoom intermediate event, the delta is the change
160         // in touch points.
161         evt.mMessage = eMagnifyGestureUpdate;
162         // (positive for a "zoom in")
163         evt.mDelta = -1.0 * (mZoomIntermediate - (float)gi.ullArguments);
164         mZoomIntermediate = (float)gi.ullArguments;
165       }
166     } break;
167 
168     case GID_ROTATE: {
169       // Send a rotate start event
170       double radians = 0.0;
171 
172       // On GF_BEGIN, ullArguments contains the absolute rotation at the
173       // start of the gesture. In later events it contains the offset from
174       // the start angle.
175       if (gi.ullArguments != 0)
176         radians = GID_ROTATE_ANGLE_FROM_ARGUMENT(gi.ullArguments);
177 
178       double degrees = -1 * radians * (180 / M_PI);
179 
180       if (gi.dwFlags & GF_BEGIN) {
181         // At some point we should pass the initial angle in
182         // along with delta. It's useful.
183         degrees = mRotateIntermediate = 0.0;
184       }
185 
186       evt.mDirection = 0;
187       evt.mDelta = degrees - mRotateIntermediate;
188       mRotateIntermediate = degrees;
189 
190       if (evt.mDelta > 0) {
191         evt.mDirection =
192             dom::SimpleGestureEventBinding::ROTATION_COUNTERCLOCKWISE;
193       } else if (evt.mDelta < 0) {
194         evt.mDirection = dom::SimpleGestureEventBinding::ROTATION_CLOCKWISE;
195       }
196 
197       if (gi.dwFlags & GF_BEGIN) {
198         evt.mMessage = eRotateGestureStart;
199       } else if (gi.dwFlags & GF_END) {
200         evt.mMessage = eRotateGesture;
201       } else {
202         evt.mMessage = eRotateGestureUpdate;
203       }
204     } break;
205 
206     case GID_TWOFINGERTAP:
207       // Normally maps to "restore" from whatever you may have recently changed.
208       // A simple double click.
209       evt.mMessage = eTapGesture;
210       evt.mClickCount = 1;
211       break;
212 
213     case GID_PRESSANDTAP:
214       // Two finger right click. Defaults to right click if it falls through.
215       evt.mMessage = ePressTapGesture;
216       evt.mClickCount = 1;
217       break;
218   }
219 
220   return true;
221 }
222 
ProcessPanMessage(HWND hWnd,WPARAM wParam,LPARAM lParam)223 bool nsWinGesture::ProcessPanMessage(HWND hWnd, WPARAM wParam, LPARAM lParam) {
224   GESTUREINFO gi;
225 
226   ZeroMemory(&gi, sizeof(GESTUREINFO));
227   gi.cbSize = sizeof(GESTUREINFO);
228 
229   BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi);
230   if (!result) return false;
231 
232   // The coordinates of this event
233   nsPointWin coord;
234   coord = mPanRefPoint = gi.ptsLocation;
235   // We want screen coordinates in our local offsets as client coordinates will
236   // change when feedback is taking place. Gui events though require client
237   // coordinates.
238   mPanRefPoint.ScreenToClient(hWnd);
239 
240   switch (gi.dwID) {
241     case GID_BEGIN:
242     case GID_END:
243       // These should always fall through to DefWndProc
244       return false;
245       break;
246 
247     // Setup pixel scroll events for both axis
248     case GID_PAN: {
249       if (gi.dwFlags & GF_BEGIN) {
250         mPanIntermediate = coord;
251         mPixelScrollDelta = 0;
252         mPanActive = true;
253         mPanInertiaActive = false;
254       } else {
255 #ifdef DBG_jimm
256         int32_t deltaX = mPanIntermediate.x - coord.x;
257         int32_t deltaY = mPanIntermediate.y - coord.y;
258         MOZ_LOG(gWindowsLog, LogLevel::Info,
259                 ("coordX=%d coordY=%d deltaX=%d deltaY=%d x:%d y:%d\n", coord.x,
260                  coord.y, deltaX, deltaY, mXAxisFeedback, mYAxisFeedback));
261 #endif
262 
263         mPixelScrollDelta.x = mPanIntermediate.x - coord.x;
264         mPixelScrollDelta.y = mPanIntermediate.y - coord.y;
265         mPanIntermediate = coord;
266 
267         if (gi.dwFlags & GF_INERTIA) mPanInertiaActive = true;
268 
269         if (gi.dwFlags & GF_END) {
270           mPanActive = false;
271           mPanInertiaActive = false;
272           PanFeedbackFinalize(hWnd, true);
273         }
274       }
275     } break;
276   }
277   return true;
278 }
279 
TestTransition(int32_t a,int32_t b)280 inline bool TestTransition(int32_t a, int32_t b) {
281   // If a is zero, overflow is zero, implying the cursor has moved back to the
282   // start position. If b is zero, cached overscroll is zero, implying feedback
283   // just begun.
284   if (a == 0 || b == 0) return true;
285   // Test for different signs.
286   return (a < 0) == (b < 0);
287 }
288 
UpdatePanFeedbackX(HWND hWnd,int32_t scrollOverflow,bool & endFeedback)289 void nsWinGesture::UpdatePanFeedbackX(HWND hWnd, int32_t scrollOverflow,
290                                       bool& endFeedback) {
291   // If scroll overflow was returned indicating we panned past the bounds of
292   // the scrollable view port, start feeback.
293   if (scrollOverflow != 0) {
294     if (!mFeedbackActive) {
295       BeginPanningFeedback(hWnd);
296       mFeedbackActive = true;
297     }
298     endFeedback = false;
299     mXAxisFeedback = true;
300     return;
301   }
302 
303   if (mXAxisFeedback) {
304     int32_t newOverflow = mPixelScrollOverflow.x - mPixelScrollDelta.x;
305 
306     // Detect a reverse transition past the starting drag point. This tells us
307     // the user has panned all the way back so we can stop providing feedback
308     // for this axis.
309     if (!TestTransition(newOverflow, mPixelScrollOverflow.x) ||
310         newOverflow == 0)
311       return;
312 
313     // Cache the total over scroll in pixels.
314     mPixelScrollOverflow.x = newOverflow;
315     endFeedback = false;
316   }
317 }
318 
UpdatePanFeedbackY(HWND hWnd,int32_t scrollOverflow,bool & endFeedback)319 void nsWinGesture::UpdatePanFeedbackY(HWND hWnd, int32_t scrollOverflow,
320                                       bool& endFeedback) {
321   // If scroll overflow was returned indicating we panned past the bounds of
322   // the scrollable view port, start feeback.
323   if (scrollOverflow != 0) {
324     if (!mFeedbackActive) {
325       BeginPanningFeedback(hWnd);
326       mFeedbackActive = true;
327     }
328     endFeedback = false;
329     mYAxisFeedback = true;
330     return;
331   }
332 
333   if (mYAxisFeedback) {
334     int32_t newOverflow = mPixelScrollOverflow.y - mPixelScrollDelta.y;
335 
336     // Detect a reverse transition past the starting drag point. This tells us
337     // the user has panned all the way back so we can stop providing feedback
338     // for this axis.
339     if (!TestTransition(newOverflow, mPixelScrollOverflow.y) ||
340         newOverflow == 0)
341       return;
342 
343     // Cache the total over scroll in pixels.
344     mPixelScrollOverflow.y = newOverflow;
345     endFeedback = false;
346   }
347 }
348 
PanFeedbackFinalize(HWND hWnd,bool endFeedback)349 void nsWinGesture::PanFeedbackFinalize(HWND hWnd, bool endFeedback) {
350   if (!mFeedbackActive) return;
351 
352   if (endFeedback) {
353     mFeedbackActive = false;
354     mXAxisFeedback = false;
355     mYAxisFeedback = false;
356     mPixelScrollOverflow = 0;
357     EndPanningFeedback(hWnd, TRUE);
358     return;
359   }
360 
361   UpdatePanningFeedback(hWnd, mPixelScrollOverflow.x, mPixelScrollOverflow.y,
362                         mPanInertiaActive);
363 }
364 
PanDeltaToPixelScroll(WidgetWheelEvent & aWheelEvent)365 bool nsWinGesture::PanDeltaToPixelScroll(WidgetWheelEvent& aWheelEvent) {
366   aWheelEvent.mDeltaX = aWheelEvent.mDeltaY = aWheelEvent.mDeltaZ = 0.0;
367   aWheelEvent.mLineOrPageDeltaX = aWheelEvent.mLineOrPageDeltaY = 0;
368 
369   aWheelEvent.mRefPoint = LayoutDeviceIntPoint(mPanRefPoint.x, mPanRefPoint.y);
370   aWheelEvent.mDeltaMode = nsIDOMWheelEvent::DOM_DELTA_PIXEL;
371   aWheelEvent.mScrollType = WidgetWheelEvent::SCROLL_SYNCHRONOUSLY;
372   aWheelEvent.mIsNoLineOrPageDelta = true;
373 
374   aWheelEvent.mOverflowDeltaX = 0.0;
375   aWheelEvent.mOverflowDeltaY = 0.0;
376 
377   // Don't scroll the view if we are currently at a bounds, or, if we are
378   // panning back from a max feedback position. This keeps the original drag
379   // point constant.
380   if (!mXAxisFeedback) {
381     aWheelEvent.mDeltaX = mPixelScrollDelta.x;
382   }
383   if (!mYAxisFeedback) {
384     aWheelEvent.mDeltaY = mPixelScrollDelta.y;
385   }
386 
387   return (aWheelEvent.mDeltaX != 0 || aWheelEvent.mDeltaY != 0);
388 }
389