| OLD | NEW |
| 1 Gestures | 1 Gestures |
| 2 ======== | 2 ======== |
| 3 | 3 |
| 4 ```javascript | 4 ```dart |
| 5 typedef PointerID Integer; | 5 SKY MODULE |
| 6 <!-- part of sky:core --> |
| 6 | 7 |
| 7 dictionary GestureState { | 8 <script> |
| 8 Boolean cancel = true; // if true, then cancel the gesture at this point | 9 abstract class GestureEvent extends Event { |
| 9 Boolean capture = false; // (for pointer-down) if true, then this pointer is r
elevant | 10 Gesture _gesture; |
| 10 Boolean choose = false; // if true, the gesture thinks that other gestures sho
uld give up | 11 Gesture get gesture => _gesture; |
| 11 Boolean finished = true; // if true, we're ready for the next gesture to start | 12 } |
| 12 | 13 |
| 14 class GestureState { |
| 15 bool cancel = true; // if true, then cancel the gesture at this point |
| 16 bool capture = false; // (for PointerDownEvent) if true, then this pointer is
relevant |
| 17 bool choose = false; // if true, the gesture thinks that other gestures should
give up |
| 18 bool finished = true; // if true, we're ready for the next gesture to start |
| 13 // choose and cancel are mutually exclusive | 19 // choose and cancel are mutually exclusive |
| 14 } | 20 } |
| 15 | 21 |
| 16 dictionary SendEventOptions { | 22 class BufferedEvent { |
| 17 Integer? coallesceGroup = null; // when queuing events, only the last event wi
th each group is kept | 23 const BufferedEvent(this.event, this.coallesceGroup); |
| 18 Boolean prechoose = false; // if true, event should just be sent right away, n
ot queued | 24 final GestureEvent event; |
| 25 final int coallesceGroup; |
| 19 } | 26 } |
| 20 | 27 |
| 21 abstract class Gesture : EventTarget { | 28 abstract class Gesture extends EventTarget { |
| 22 constructor (EventTarget target); | 29 Gesture(this.target) : super() { |
| 23 readonly attribute EventTarget target; | 30 target.events.where((event) => event is PointerDownEvent || |
| 31 event is PointerMovedEvent || |
| 32 event is PointerUpEvent).listen(_handler); |
| 33 } |
| 34 final EventTarget target; |
| 24 | 35 |
| 25 virtual GestureState processEvent(Event event); | 36 bool _ready = true; // last event, we were finished |
| 26 // return {} | 37 bool get ready => _ready; |
| 27 virtual void choose(); // called by GestureManager // make sure to call superc
lass choose() before | 38 bool _active = false; // we have not yet been canceled since we last started l
istening to a pointer |
| 28 // - assert: this.active == true | 39 bool get active => _active; |
| 29 // - assert: this.chosen == false | 40 bool _chosen = false; // we're the only possible gesture at this point |
| 30 // - set this.chosen = true | 41 bool get chosen => _chosen; |
| 31 // - if there are any buffered events, dispatch them on this | |
| 32 virtual void cancel(); // called by GestureManager // make sure to call superc
lass cancel() after | |
| 33 // - set active and chosen to false, clear the event buffer | |
| 34 | 42 |
| 35 readonly attribute Boolean ready; // last event, we were finished | 43 // (!ready && !active) means we're discarding events until the user |
| 36 readonly attribute Boolean active; // we have not yet been canceled since we l
ast captured a pointer | 44 // gets to a state where a new gesture can begin |
| 37 readonly attribute Boolean chosen; // we're the only possible gesture at this
point | |
| 38 | 45 |
| 39 // !ready && !active => we're discarding events until the user gets to a state
where a new gesture can begin | 46 // (active && !chosen) means we're collecting events until no other |
| 40 // active && !chosen => we're collecting events until no other gesture is vali
d, or until we take command | 47 // gesture is valid, or until we take command |
| 41 | 48 |
| 42 void sendEvent(Event event, SendEventOptions options); | 49 GestureState processEvent(PointerEvent event); |
| 43 // used internally to queue up or send events | 50 |
| 44 // - assert: this.active == true | 51 List<BufferedEvent> _eventBuffer; |
| 45 // - assert: options.prechoose is false or options.coallesceGroup | 52 |
| 46 // is null | 53 void choose() { |
| 47 // - set event.gesture = this | 54 // called by GestureManager |
| 48 // - if this.chosen is true or if options.prechoose is true, then | 55 // if you override this, make sure to call superclass choose() first |
| 49 // send the event straight to the callback | 56 assert(_active == true); |
| 50 // - otherwise: | 57 assert(_chosen == false); |
| 51 // - if the event buffer has an entry with the same | 58 _chosen = true; |
| 52 // coallesceGroup identifier, drop it | 59 // if there are any buffered events, dispatch them on this |
| 53 // - add the event to the event buffer | 60 if ((_eventBuffer != null) && (_eventBuffer.length > 0)) { |
| 61 // we make a copy of the event buffer first so that the array isn't mutate
d out from under us |
| 62 // while we are doing this |
| 63 var events = _eventBuffer; |
| 64 _eventBuffer = null; |
| 65 for (var item in events) { |
| 66 dispatchEvent(item.event); |
| 67 } |
| 68 } |
| 69 } |
| 70 |
| 71 void cancel() { |
| 72 // called by GestureManager |
| 73 // if you override this, make sure to call superclass cancel() last |
| 74 _active = false; |
| 75 _chosen = false; |
| 76 _eventBuffer = null; |
| 77 } |
| 78 |
| 79 // for use by subclasses only |
| 80 void sendEvent(GestureEvent event, |
| 81 { int coallesceGroup, // when queuing events, only the last eve
nt with each group is kept |
| 82 bool prechoose: false // if true, event should just be sent r
ight away, not queued |
| 83 }) { |
| 84 assert(_active == true); |
| 85 assert(coallesceGroup == null || prechoose == false); |
| 86 event._gesture = this; |
| 87 if (_chosen || prechoose) { |
| 88 dispatchEvent(event); |
| 89 } else { |
| 90 if (_eventBuffer == null) |
| 91 _eventBuffer = new List<BufferedEvent>(); |
| 92 if (coallesceGroup != null) |
| 93 _eventBuffer.removeWhere((candidate) => candidate.coallesceGroup == coal
lesceGroup); |
| 94 _eventBuffer.add(new BufferedEvent(event, coallesceGroup)); |
| 95 } |
| 96 } |
| 97 |
| 98 void _handler(event) { |
| 99 bool wasActive = _active; |
| 100 if (_ready) { |
| 101 // reset the state to start a new gesture |
| 102 if (_active) |
| 103 module.application.gestureManager.cancelGesture(this); |
| 104 _active = true; |
| 105 _ready = false; |
| 106 } |
| 107 GestureState returnValue = processEvent(event); |
| 108 if (returnValue.capture) { |
| 109 assert(event is PointerDownEvent); |
| 110 if (event is PointerDownEvent) |
| 111 event.result.add(this); |
| 112 } |
| 113 if (returnValue.cancel) { |
| 114 assert(returnValue.choose == false); |
| 115 if (wasActive) |
| 116 module.application.cancelGesture(this); |
| 117 // if we never became active, then we never called addGesture() below |
| 118 _active = false; |
| 119 } else if (active == true) { |
| 120 if (wasActive == false || event is PointerDownEvent) |
| 121 module.application.addGesture(event, this); |
| 122 if (returnValue.choose == true) |
| 123 module.application.chooseGesture(this); |
| 124 } |
| 125 _ready = returnValue.finished; |
| 126 } |
| 54 } | 127 } |
| 128 |
| 129 /* |
| 55 ``` | 130 ``` |
| 56 | |
| 57 ``Gesture`` objects have an Event buffer, initially empty. Each Event | |
| 58 in this buffer can be associated with a coallesceGroup, which is | |
| 59 identified by integer. | |
| 60 | |
| 61 When created, ``Gesture`` objects register themselves as pointer-down, | |
| 62 pointer-move, and pointer-up event handlers on their target, with the | |
| 63 same event handler. That event handler runs the following steps: | |
| 64 - let wasActive = this.active | |
| 65 - if this.ready == true, then: | |
| 66 - // reset the state to start a new gesture | |
| 67 - if this.active == true, then: | |
| 68 - call application.document.cancelGesture(this) | |
| 69 - set this.active = true | |
| 70 - set this.ready = false | |
| 71 - let returnValue be the result of calling ``processEvent()`` with | |
| 72 the Event object | |
| 73 - if returnValue.capture == true: | |
| 74 - assert: the event is a pointer-down event | |
| 75 - if the event is a pointer-down event: | |
| 76 - push this onto the event's return value | |
| 77 - if returnValue.cancel == true: | |
| 78 - assert: returnValue.choose == false | |
| 79 - if wasActive == true: | |
| 80 - call application.document.cancelGesture(this) | |
| 81 - // if wasActive == false, then no need to cancel, since we never added
ourselves | |
| 82 - if returnValue.cancel == false and this.active == true: | |
| 83 - if wasActive == false or if event is a pointer-down event: | |
| 84 - call application.document.addGesture(event, this) | |
| 85 - if returnValue.choose == true: | |
| 86 - call application.document.chooseGesture(this) | |
| 87 - set this.ready = returnValue.finished | |
| 88 - set this.active = returnValue.valid | |
| 89 | |
| 90 Subclasses should override ``processEvent()``: | 131 Subclasses should override ``processEvent()``: |
| 91 - as the events are received, they get examined to see if they | 132 - as the events are received, they get examined to see if they |
| 92 fit the pattern for the gesture; if they do, then return an | 133 fit the pattern for the gesture; if they do, then return an |
| 93 object with valid=true; if more events for this gesture could | 134 object with valid=true; if more events for this gesture could |
| 94 still come in, return finished=false. | 135 still come in, return finished=false. |
| 95 - if you returned valid=false finished=false, then the next call | 136 - if you returned valid=false finished=false, then the next call |
| 96 to this must not return valid=true | 137 to this must not return valid=true |
| 97 - doing anything with the event or target other than reading | 138 - doing anything with the event or target other than reading |
| 98 state is a contract violation | 139 state is a contract violation |
| 99 - you are allowed to call sendEvent() at any time during a | 140 - you are allowed to call sendEvent() at any time during a |
| 100 processEventInternal() call, or after a call to | 141 processEventInternal() call, or after a call to |
| 101 processEventInternal(), assuming that the last such call returned | 142 processEventInternal(), assuming that the last such call returned |
| 102 valid=true, until the next call to processEventInternal() or | 143 valid=true, until the next call to processEventInternal() or |
| 103 cancel(). | 144 cancel(). |
| 104 - set forceChoose=true on the return value if you are confident | 145 - set forceChoose=true on the return value if you are confident |
| 105 that this is the gesture the user meant, even if it's possible | 146 that this is the gesture the user meant, even if it's possible |
| 106 that another gesture is still claiming it's valid (e.g. a long | 147 that another gesture is still claiming it's valid (e.g. a long |
| 107 press might forceChoose to override a scroll, if the user | 148 press might forceChoose to override a scroll, if the user |
| 108 hasn't moved for a while) | 149 hasn't moved for a while) |
| 109 - if you send events, you can set prechoose=true to send the | 150 - if you send events, you can set prechoose=true to send the |
| 110 event even before the gesture has been chosen | 151 event even before the gesture has been chosen |
| 111 - if you send prechoose events, make sure to send corresponding | 152 - if you send prechoose events, make sure to send corresponding |
| 112 "cancel" events if cancel() is called | 153 "cancel" events if cancel() is called |
| 113 | 154 |
| 114 ```javascript | 155 ```dart |
| 115 dictionary GestureList { | 156 */ |
| 116 Array<Gesture> gestures; | 157 |
| 117 Boolean chosen; | 158 class PointerState { |
| 159 PointerState({this.gestures, this.chosen}) { |
| 160 if (gestures == null) |
| 161 gestures = new List<Gesture>(); |
| 162 } |
| 163 factory PointerState.clone(PointerState source) { |
| 164 return new PointerState(gestures: source.gestures, chosen: source.chosen); |
| 165 } |
| 166 List<Gesture> gestures; |
| 167 bool chosen = false; |
| 118 } | 168 } |
| 119 | 169 |
| 120 class GestureManager { | 170 class GestureManager { |
| 121 constructor (EventTarget target); | 171 GestureManager(this.target) { |
| 122 readonly attribute EventTarget target; // the ApplicationDocument, normally | 172 target.events.where((event) => event is PointerDownEvent).listen(_handler); |
| 173 } |
| 174 final EventTarget target; // usually the ApplicationDocument object |
| 123 | 175 |
| 124 void addGesture(Event event, Gesture gesture); | 176 Map<int, PointerState> _pointers = new SplayTreeMap<int, PointerState>(); |
| 125 void cancelGesture(Gesture gesture); | |
| 126 void chooseGesture(Gesture gesture); | |
| 127 | 177 |
| 128 GestureList getActiveGestures(PointerID pointer); | 178 void addGesture(PointerEvent event, Gesture gesture) { |
| 179 assert(gesture.active); |
| 180 var pointer = event.pointer; |
| 181 if (_pointers.containsKey(pointer)) { |
| 182 assert(!_pointers[pointer].gestures.contains(gesture)); |
| 183 if (_pointers[pointer].chosen) |
| 184 cancelGesture(gesture); |
| 185 else |
| 186 _pointers[pointer].gestures.add(gesture); |
| 187 } else { |
| 188 PointerState pointerState = new PointerState(); |
| 189 pointerState.gestures.add(gesture); |
| 190 _pointers[pointer] = pointerState; |
| 191 } |
| 192 } |
| 193 |
| 194 void cancelGesture(Gesture gesture) { |
| 195 _pointers.forEach((index, pointerState) => pointerState.gestures.remove(gest
ure)); |
| 196 gesture.cancel(); |
| 197 // get a static copy of the _pointers keys, so we can remove them safely |
| 198 var activePointers = new List<int>.from(_pointers.keys); |
| 199 // now walk our lists, removing pointers that are obsolete, and choosing |
| 200 // gestures from pointers that have only one outstanding gesture |
| 201 for (var index = 0; index < activePointers.length; index += 1) { |
| 202 var pointerState = _pointers[activePointers[index]]; |
| 203 if (pointerState.gestures.length == 0) { |
| 204 _pointers.remove(activePointers[index]); |
| 205 } else { |
| 206 if (pointerState.gestures.length == 1 && pointerState.chosen) { |
| 207 pointerState.chosen = true; |
| 208 pointerState.gestures[0].choose(); |
| 209 } |
| 210 } |
| 211 } |
| 212 } |
| 213 |
| 214 void chooseGesture(Gesture gesture) { |
| 215 if (!gesture.active) |
| 216 // this could happen e.g. if two gestures simultaneously add |
| 217 // themselves and chose themselves for the same PointerDownEvent |
| 218 return; |
| 219 List<Gesture> losers = new List<Gesture>(); |
| 220 _pointers.values |
| 221 .where((pointerState) => pointerState.gestures.contains(gesture)) |
| 222 .forEach((pointerState) { |
| 223 losers.addAll(pointerState.gestures.where((candidateLoser) => can
didateLoser != gesture)); |
| 224 pointerState.gestures.clear(); |
| 225 pointerState.gestures.add(gesture); |
| 226 pointerState.chosen = true; |
| 227 }); |
| 228 assert(losers.every((loser) => loser.active)); |
| 229 losers.forEach((loser) { |
| 230 // we check loser.active because losers could contain duplicates |
| 231 // and we should only cancel each gesture once |
| 232 if (loser.active) |
| 233 loser.cancel(); |
| 234 assert(!loser.active); |
| 235 }); |
| 236 gesture.choose(); |
| 237 } |
| 238 |
| 239 PointerState getActiveGestures(int pointer) { |
| 240 if (_pointers.containsKey(pointer) && _pointers[pointer].gestures.length > 0
) |
| 241 return new PointerState.clone(_pointers[pointer]); |
| 242 return new PointerState(); |
| 243 } |
| 244 |
| 245 void _handler(PointerDownEvent event) { |
| 246 var pointer = event.pointer; |
| 247 if (_pointers.containsKey(pointer)) { |
| 248 var pointerState = _pointers[pointer]; |
| 249 if ((!pointerState.chosen) && (pointerState.gestures.length == 1)) { |
| 250 pointerState.chosen = true; |
| 251 pointerState.gestures[0].choose(); |
| 252 } |
| 253 } |
| 254 } |
| 255 |
| 129 } | 256 } |
| 257 </script> |
| 130 ``` | 258 ``` |
| 131 | 259 |
| 132 ``GestureManager`` objects have a map of lists of Gesture objects, | 260 Gestures defined in the framework |
| 133 keyed on pointer IDs, and with each list associated with a "chosen" | 261 --------------------------------- |
| 134 flag indicating if an entry in the list has already been chosen. | |
| 135 Initially the map is empty. It is exposed by the | |
| 136 ``getActiveGestures()`` method, which returns the list and flag. | |
| 137 | 262 |
| 138 When addGesture() is called with an event and a Gesture, it runs the | 263 ```dart |
| 139 following steps: | 264 SKY MODULE |
| 140 - let pointer be the value of the event's pointer field | 265 <!-- not in sky:core --> |
| 141 - assert: pointer is an integer | 266 <!-- note: this hasn't been dartified yet --> |
| 142 - if we already have an entry for pointer: | |
| 143 - assert: this Gesture isn't already on the list for pointer | |
| 144 - if the list's "chosen" flag is set, then call | |
| 145 ``cancelGesture()`` with this Gesture | |
| 146 - otherwise, add this Gesture to the list for pointer | |
| 147 - otherwise, we don't have an entry for this pointer: | |
| 148 - create a list for pointer | |
| 149 - add this Gesture to the list for pointer | |
| 150 | 267 |
| 151 A ``GestureManager``, when created, starts listening to | 268 <script> |
| 152 ``pointer-down`` events on its target. The listener acts as follows: | 269 class TapGesture extends Gesture { |
| 153 - assert: event is a ``pointer-down`` event | |
| 154 - let pointer be the value of the event's pointer field | |
| 155 - if we have an entry for this pointer, and the "chosen" flag isn't | |
| 156 set, and there is just one Gesture in the list, then set the flag | |
| 157 on the list and call the Gesture's ``choose()`` method. | |
| 158 | |
| 159 When ``cancelGesture()`` is called with a Gesture: | |
| 160 - for each pointer list: | |
| 161 - if the pointer list has this Gesture, remove it | |
| 162 - call cancel() on the Gesture | |
| 163 - for each pointer list: | |
| 164 - if the pointer list has no entries, forget it | |
| 165 - if the pointer list has one Gesture and the "chosen" flag isn't | |
| 166 set, set it and call that Gesture's ``choose()`` method. | |
| 167 | |
| 168 When ``chooseGesture()`` is called with a Gesture: | |
| 169 - if this Gesture is not active, then return silently | |
| 170 // this could happen e.g. if two gestures simultaneously add themselves | |
| 171 // and chose themselves for the same pointer-down | |
| 172 - let losers be an empty list of Gestures | |
| 173 - for each pointer list: | |
| 174 - if the pointer list has this Gesture, add all the other Gestures | |
| 175 in the list to losers, remove them from the list, and set the | |
| 176 "chosen" flag on that list | |
| 177 - remove duplicates from losers | |
| 178 - call ``cancel()`` on each entry in losers | |
| 179 - call ``choose()`` on the Gesture | |
| 180 | |
| 181 | |
| 182 ```javascript | |
| 183 class TapGesture : Gesture { | |
| 184 | 270 |
| 185 // internal state: | 271 // internal state: |
| 186 // Integer numButtons = 0; | 272 // Integer numButtons = 0; |
| 187 // Boolean primaryDown = false; | 273 // Boolean primaryDown = false; |
| 188 | 274 |
| 189 virtual GestureState processEvent(Event event); | 275 virtual GestureState processEvent(Event event); |
| 190 // - let returnValue = { finished = false } | 276 // - let returnValue = { finished = false } |
| 191 // - if the event is a pointer-down: | 277 // - if the event is a pointer-down: |
| 192 // - increment this.numButtons | 278 // - increment this.numButtons |
| 193 // - set returnValue.capture = true | 279 // - set returnValue.capture = true |
| (...skipping 140 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 334 // fling: the user has released the pointer and the decision is it was in fact
flung | 420 // fling: the user has released the pointer and the decision is it was in fact
flung |
| 335 // fling-cancel: cancel(), or the user has released the pointer and the decisi
on is it was not flung (prechoose) | 421 // fling-cancel: cancel(), or the user has released the pointer and the decisi
on is it was not flung (prechoose) |
| 336 // fling-end: cancel(), or after fling or fling-cancel (prechoose) | 422 // fling-end: cancel(), or after fling or fling-cancel (prechoose) |
| 337 } | 423 } |
| 338 | 424 |
| 339 class FlingLeftGesture : FlingGesture { } | 425 class FlingLeftGesture : FlingGesture { } |
| 340 class FlingRightGesture : FlingGesture { } | 426 class FlingRightGesture : FlingGesture { } |
| 341 class FlingUpGesture : FlingGesture { } | 427 class FlingUpGesture : FlingGesture { } |
| 342 class FlingDownGesture : FlingGesture { } | 428 class FlingDownGesture : FlingGesture { } |
| 343 | 429 |
| 430 </script> |
| 344 ``` | 431 ``` |
| OLD | NEW |