JOGL v2.6.0-rc-20250706
JOGL, High-Performance Graphics Binding for Java™ (public API).
DoubleTapScrollGesture.java
Go to the documentation of this file.
1/**
2 * Copyright 2013 JogAmp Community. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without modification, are
5 * permitted provided that the following conditions are met:
6 *
7 * 1. Redistributions of source code must retain the above copyright notice, this list of
8 * conditions and the following disclaimer.
9 *
10 * 2. Redistributions in binary form must reproduce the above copyright notice, this list
11 * of conditions and the following disclaimer in the documentation and/or other materials
12 * provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
15 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
16 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
20 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
21 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
22 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 *
24 * The views and conclusions contained in the software and documentation are those of the
25 * authors and should not be interpreted as representing official policies, either expressed
26 * or implied, of JogAmp Community.
27 */
28package com.jogamp.newt.event;
29
30import com.jogamp.common.util.PropertyAccess;
31
32import jogamp.newt.Debug;
33
34/**
35 * 2 pointer scroll/rotate gesture handler processing {@link MouseEvent}s
36 * while producing {@link MouseEvent#EVENT_MOUSE_WHEEL_MOVED} events if gesture is completed.
37 * <p>
38 * Criteria related to parameters:
39 * <pre>
40 * - doubleTapSlop (scaled in pixels):
41 * - Max 2 finger distance to start 'scroll' mode
42 * - Max. distance diff of current 2-pointer middle and initiated 2-pointer middle.
43 *
44 * - touchSlop (scaled in pixels):
45 * - Min. movement w/ 2 pointer within ScaledDoubleTapSlop starting 'scroll' mode
46 *
47 * - Avoid computation if not within gesture, especially for MOVE/DRAG
48 *
49 * - Only allow gesture to start with PRESS
50 *
51 * - Leave gesture completely with RELEASE of both/all fingers, or dist-diff exceeds doubleTapSlop
52 *
53 * - Tolerate temporary lift 1 of 2 pointer
54 *
55 * - Always validate pointer-id
56 * </pre>
57 * </p>
58 * Implementation uses a n-state to get detect gesture:
59 * <p>
60 * <table border="1">
61 * <tr><th>from</th> <th>to</th> <th>action</th></tr>
62 * <tr><td>NONE</td> <td>1PRESS</td> <td>1-pointer-pressed</td></tr>
63 * <tr><td>1PRESS</td> <td>2PRESS_T</td> <td>2-pointer-pressed within doubleTapSlope</td></tr>
64 * <tr><td>2PRESS_T</td> <td>SCROLL</td> <td>2-pointer dragged, dist-diff within doubleTapSlop and scrollLen >= scrollSlop</td></tr>
65 * <tr><td>2PRESS_C</td> <td>SCROLL</td> <td>2-pointer dragged, dist-diff within doubleTapSlop</td></tr>
66 * <tr><td>SCROLL</td> <td>SCROLL</td> <td>2-pointer dragged, dist-diff within doubleTapSlop</td></tr>
67 * </table>
68 * State ST_2PRESS_C merely exist to pick up gesture after one pointer has been lost temporarily.
69 * </p>
70 * <p>
71 * {@link #isWithinGesture()} returns gestureState >= 2PRESS_C
72 * </p>
73 */
74public class DoubleTapScrollGesture implements GestureHandler {
75 /** Scroll threshold in pixels (fallback), defaults to 16 pixels. Can be overriden by integer property <code>newt.event.scroll_slop_pixel</code>.*/
76 public static final int SCROLL_SLOP_PIXEL;
77 /** Two pointer 'double tap' slop in pixels (fallback), defaults to 104 pixels. Can be overriden by integer property <code>newt.event.double_tap_slop_pixel</code>.*/
78 public static final int DOUBLE_TAP_SLOP_PIXEL;
79
80 /** Scroll threshold in millimeter, defaults to 3 mm. Can be overriden by integer property <code>newt.event.scroll_slop_mm</code>.*/
81 public static final float SCROLL_SLOP_MM;
82 /** Two pointer 'double tap' slop in millimeter, defaults to 20 mm. Can be overriden by integer property <code>newt.event.double_tap_slop_mm</code>.*/
83 public static final float DOUBLE_TAP_SLOP_MM;
84
85 static {
86 Debug.initSingleton();
87
88 SCROLL_SLOP_PIXEL = PropertyAccess.getIntProperty("newt.event.scroll_slop_pixel", true, 16);
89 DOUBLE_TAP_SLOP_PIXEL = PropertyAccess.getIntProperty("newt.event.double_tap_slop_pixel", true, 104);
90 SCROLL_SLOP_MM = PropertyAccess.getIntProperty("newt.event.scroll_slop_mm", true, 3);
91 DOUBLE_TAP_SLOP_MM = PropertyAccess.getIntProperty("newt.event.double_tap_slop_mm", true, 20);
92 }
93
94 private static final int ST_NONE = 0;
95 private static final int ST_1PRESS = 1;
96 private static final int ST_2PRESS_T = 2;
97 private static final int ST_2PRESS_C = 3;
98 private static final int ST_SCROLL = 4;
99
100 private final int scrollSlop, scrollSlopSquare, doubleTapSlop, doubleTapSlopSquare;
101 private final float[] scrollDistance = new float[] { 0f, 0f };
102 private final int[] pIds = new int[] { -1, -1 };
103 /** See class docu */
104 private int gestureState;
105 private int sqStartDist;
106 private int lastX, lastY;
107 private int pointerDownCount;
108 private MouseEvent hitGestureEvent;
109
110 private static final int getSquareDistance(final float x1, final float y1, final float x2, final float y2) {
111 final int deltaX = (int) x1 - (int) x2;
112 final int deltaY = (int) y1 - (int) y2;
113 return deltaX * deltaX + deltaY * deltaY;
114 }
115
116 private int gesturePointers(final MouseEvent e, final int excludeIndex) {
117 int j = 0;
118 for(int i=e.getPointerCount()-1; i>=0; i--) {
119 if( excludeIndex != i ) {
120 final int id = e.getPointerId(i);
121 if( pIds[0] == id || pIds[1] == id ) {
122 j++;
123 }
124 }
125 }
126 return j;
127 }
128
129 /**
130 * scaledScrollSlop < scaledDoubleTapSlop
131 * @param scaledScrollSlop Distance a pointer can wander before we think the user is scrolling in <i>pixels</i>.
132 * @param scaledDoubleTapSlop Distance in <i>pixels</i> between the first touch and second touch to still be considered a double tap.
133 */
134 public DoubleTapScrollGesture(final int scaledScrollSlop, final int scaledDoubleTapSlop) {
135 scrollSlop = scaledScrollSlop;
136 scrollSlopSquare = scaledScrollSlop * scaledScrollSlop;
137 doubleTapSlop = scaledDoubleTapSlop;
138 doubleTapSlopSquare = scaledDoubleTapSlop * scaledDoubleTapSlop;
139 pointerDownCount = 0;
140 clear(true);
141 if(DEBUG) {
142 System.err.println("DoubleTapScroll scrollSlop (scaled) "+scrollSlop);
143 System.err.println("DoubleTapScroll doubleTapSlop (scaled) "+doubleTapSlop);
144 }
145 }
146
147 @Override
148 public String toString() {
149 return "DoubleTapScroll[state "+gestureState+", in "+isWithinGesture()+", has "+(null!=hitGestureEvent)+", pc "+pointerDownCount+"]";
150 }
151
152 @Override
153 public void clear(final boolean clearStarted) {
154 scrollDistance[0] = 0f;
155 scrollDistance[1] = 0f;
156 hitGestureEvent = null;
157 if( clearStarted ) {
158 gestureState = ST_NONE;
159 sqStartDist = 0;
160 pIds[0] = -1;
161 pIds[1] = -1;
162 lastX = 0;
163 lastY = 0;
164 }
165 }
166
167 @Override
168 public boolean isWithinGesture() {
169 return ST_2PRESS_C <= gestureState;
170 }
171
172 @Override
173 public boolean hasGesture() {
174 return null != hitGestureEvent;
175 }
176
177 @Override
179 if( null != hitGestureEvent ) {
180 final MouseEvent ge = hitGestureEvent;
181 int modifiers = ge.getModifiers();
182 final float[] rotationXYZ = ge.getRotation();
183 rotationXYZ[0] = scrollDistance[0] / scrollSlop;
184 rotationXYZ[1] = scrollDistance[1] / scrollSlop;
185 if( rotationXYZ[0]*rotationXYZ[0] > rotationXYZ[1]*rotationXYZ[1] ) {
186 // Horizontal scroll -> SHIFT
187 modifiers |= com.jogamp.newt.event.InputEvent.SHIFT_MASK;
188 }
189 return new MouseEvent(MouseEvent.EVENT_MOUSE_WHEEL_MOVED, ge.getSource(), ge.getWhen(), modifiers,
191 ge.getAllX(), ge.getAllY(), ge.getAllPressures(), ge.getMaxPressure(),
192 ge.getButton(), ge.getClickCount(), rotationXYZ, scrollSlop);
193 }
194 return null;
195 }
196
197 public final float[] getScrollDistanceXY() {
198 return scrollDistance;
199 }
200
201 @Override
202 public boolean process(final InputEvent in) {
203 if( null != hitGestureEvent || !(in instanceof MouseEvent) ) {
204 return true;
205 }
206 final MouseEvent pe = (MouseEvent)in;
207 if( pe.getPointerType(0).getPointerClass() != MouseEvent.PointerClass.Onscreen ) {
208 return false;
209 }
210 pointerDownCount = pe.getPointerCount();
211 final int eventType = pe.getEventType();
212 final int x0 = pe.getX(0);
213 final int y0 = pe.getY(0);
214 switch ( eventType ) {
216 int gPtr = 0;
217 if( ST_NONE == gestureState && 1 == pointerDownCount ) {
218 pIds[0] = pe.getPointerId(0);
219 pIds[1] = -1;
220 gestureState = ST_1PRESS;
221 } else if( ST_NONE < gestureState && 2 == pointerDownCount && 1 == gesturePointers(pe, 0) /* w/o pressed pointer */ ) {
222 final int x1 = pe.getX(1);
223 final int y1 = pe.getY(1);
224 final int xm = (x0+x1)/2;
225 final int ym = (y0+y1)/2;
226
227 if( ST_1PRESS == gestureState ) {
228 final int sqDist = getSquareDistance(x0, y0, x1, y1);
229 final boolean isDistWithinDoubleTapSlop = sqDist < doubleTapSlopSquare;
230 if( isDistWithinDoubleTapSlop ) {
231 // very first 2-finger touch-down
232 gPtr = 2;
233 pIds[0] = pe.getPointerId(0);
234 pIds[1] = pe.getPointerId(1);
235 lastX = xm;
236 lastY = ym;
237 sqStartDist = sqDist;
238 gestureState = ST_2PRESS_T;
239 }
240 if(DEBUG) {
241 final int dist = (int)Math.round(Math.sqrt(sqDist));
242 System.err.println(this+".pressed.1: dist "+dist+", gPtr "+gPtr+", distWithin2DTSlop "+isDistWithinDoubleTapSlop+", last "+lastX+"/"+lastY+", "+pe);
243 }
244 } else if( ST_2PRESS_C == gestureState ) { // pick up gesture after temp loosing one pointer
245 gPtr = gesturePointers(pe, -1);
246 if( 2 == gPtr ) {
247 // same pointers re-touch-down
248 lastX = xm;
249 lastY = ym;
250 } else {
251 // other 2 pointers .. should rarely happen!
252 clear(true);
253 }
254 }
255 }
256 if(DEBUG) {
257 System.err.println(this+".pressed: gPtr "+gPtr+", this "+lastX+"/"+lastY+", "+pe);
258 }
259 } break;
260
262 pointerDownCount--; // lifted
263 final int gPtr = gesturePointers(pe, 0); // w/o lifted pointer
264 if ( 1 == gPtr ) {
265 // tolerate lifting 1 of 2 gesture pointers temporary
266 gestureState = ST_2PRESS_C;
267 } else if( 0 == gPtr ) {
268 // all lifted
269 clear(true);
270 }
271 if(DEBUG) {
272 System.err.println(this+".released: gPtr "+gPtr+", "+pe);
273 }
274 } break;
275
277 if( 2 == pointerDownCount && ST_1PRESS < gestureState ) {
278 final int gPtr = gesturePointers(pe, -1);
279 if( 2 == gPtr ) {
280 // same pointers
281 final int x1 = pe.getX(1);
282 final int y1 = pe.getY(1);
283 final int xm = (x0+x1)/2;
284 final int ym = (y0+y1)/2;
285 final int sqDist = getSquareDistance(x0, y0, x1, y1);
286 final boolean isDistDiffWithinDoubleTapSlop = Math.abs(sqDist - sqStartDist) <= doubleTapSlopSquare;
287 if( isDistDiffWithinDoubleTapSlop ) {
288 switch( gestureState ) {
289 case ST_2PRESS_T: {
290 final int sqScrollLen = getSquareDistance(lastX, lastY, xm, ym);
291 if( sqScrollLen > scrollSlopSquare ) { // min. scrolling threshold reached
292 gestureState = ST_SCROLL;
293 }
294 } break;
295
296 case ST_2PRESS_C:
297 gestureState = ST_SCROLL;
298 break;
299
300 case ST_SCROLL:
301 scrollDistance[0] = lastX - xm;
302 scrollDistance[1] = lastY - ym;
303 hitGestureEvent = pe;
304 break;
305 }
306 if(DEBUG) {
307 final boolean isDistWithinDoubleTapSlop = sqDist < doubleTapSlopSquare;
308 final int dist = (int)Math.round(Math.sqrt(sqDist));
309 final int sqScrollLen = getSquareDistance(lastX, lastY, xm, ym);
310 final int scrollLen = (int)Math.round(Math.sqrt(sqScrollLen));
311 System.err.println(this+".dragged.1: pDist "+dist+", scrollLen "+scrollLen+", gPtr "+gPtr+" ["+pIds[0]+", "+pIds[1]+"]"+
312 ", diffDistWithinTapSlop "+isDistDiffWithinDoubleTapSlop+
313 ", distWithin2DTSlop "+isDistWithinDoubleTapSlop+
314 ", this "+xm+"/"+ym+", last "+lastX+"/"+lastY+", d "+scrollDistance[0]+"/"+scrollDistance[1]);
315 }
316 } else {
317 // distance too big ..
318 if(DEBUG) {
319 final boolean isDistWithinDoubleTapSlop = sqDist < doubleTapSlopSquare;
320 final int dist = (int)Math.round(Math.sqrt(sqDist));
321 final int startDist = (int)Math.round(Math.sqrt(sqStartDist));
322 System.err.println(this+".dragged.X1: pDist "+dist+", distStart "+startDist+", gPtr "+gPtr+" ["+pIds[0]+", "+pIds[1]+"]"+
323 ", diffDistWithinTapSlop "+isDistDiffWithinDoubleTapSlop+
324 ", distWithin2DTSlop "+isDistWithinDoubleTapSlop+
325 ", this "+xm+"/"+ym+", last "+lastX+"/"+lastY+", d "+scrollDistance[0]+"/"+scrollDistance[1]);
326 }
327 clear(true);
328 }
329 if( ST_2PRESS_T < gestureState ) {
330 // state ST_2PRESS_T waits for min scroll threshold !
331 lastX = xm;
332 lastY = ym;
333 }
334 } else {
335 // other 2 pointers .. should rarely happen!
336 if(DEBUG) {
337 System.err.println(this+".dragged.X2: gPtr "+gPtr+" ["+pIds[0]+", "+pIds[1]+"]"+
338 ", last "+lastX+"/"+lastY+", d "+scrollDistance[0]+"/"+scrollDistance[1]);
339 }
340 clear(true);
341 }
342 }
343 } break;
344
345 default:
346 }
347 return null != hitGestureEvent;
348 }
349}
2 pointer scroll/rotate gesture handler processing MouseEvents while producing MouseEvent#EVENT_MOUSE...
boolean hasGesture()
Returns true if a previous process(InputEvent) command produced a gesture, which has not been cleared...
static final float SCROLL_SLOP_MM
Scroll threshold in millimeter, defaults to 3 mm.
InputEvent getGestureEvent()
Returns the corresponding InputEvent for the gesture as detected by a previous process(InputEvent),...
boolean isWithinGesture()
Returns true if within a gesture as detected by a previous process(InputEvent) command,...
static final float DOUBLE_TAP_SLOP_MM
Two pointer 'double tap' slop in millimeter, defaults to 20 mm.
boolean process(final InputEvent in)
Process the given InputEvent and returns true if it produced the gesture.
static final int DOUBLE_TAP_SLOP_PIXEL
Two pointer 'double tap' slop in pixels (fallback), defaults to 104 pixels.
DoubleTapScrollGesture(final int scaledScrollSlop, final int scaledDoubleTapSlop)
scaledScrollSlop < scaledDoubleTapSlop
static final int SCROLL_SLOP_PIXEL
Scroll threshold in pixels (fallback), defaults to 16 pixels.
void clear(final boolean clearStarted)
Clears state of handler, i.e.
final int getModifiers()
Return the modifier bits of this event, e.g.
Pointer event of type PointerType.
Definition: MouseEvent.java:74
final short[] getAllPointerIDs()
See details for multiple-pointer events.
final PointerType getPointerType(final int index)
See details for multiple-pointer events.
final int getPointerCount()
See details for multiple-pointer events.
final PointerType[] getAllPointerTypes()
See details for multiple-pointer events.
static final short EVENT_MOUSE_PRESSED
final short getButton()
Returns the button number, e.g.
final short getPointerId(final int index)
Return the pointer id for the given index or -1 if index not available.
static final short EVENT_MOUSE_WHEEL_MOVED
final float getMaxPressure()
Returns the maximum pressure known for the input device generating this event.
final float[] getAllPressures()
See details for multiple-pointer events.
final int[] getAllX()
See details for multiple-pointer events.
final int getY()
See details for multiple-pointer events.
final int[] getAllY()
See details for multiple-pointer events.
static final short EVENT_MOUSE_DRAGGED
static final short EVENT_MOUSE_RELEASED
final float[] getRotation()
Returns a 3-component float array filled with the values of the rotational axis in the following orde...
final int getX()
See details for multiple-pointer events.
final short getEventType()
Returns the event type of this event.
Definition: NEWTEvent.java:72
final long getWhen()
Returns the timestamp, in milliseconds, of this event.
Definition: NEWTEvent.java:77
Generic gesture handler interface designed to allow pass-through filtering of InputEvents.