1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 
  9 /**
 10   @class
 11 
 12   An SC.Gesture analyzes SC.Touch objects and determines if they are part
 13   of a gesture. If they are, SC.Gestures keep the views that own them up-to-date
 14   as that gesture progresses, informing it when it starts, when some aspect of
 15   it changes, when it ends, and—for convenience—when it is considered to have
 16   been "triggered".
 17 
 18   Gestures can call the following methods on their views:
 19 
 20   - [gestureName](args...): called when the gesture has occurred. This is
 21     useful for event-style gestures, where you aren't interested in when it starts or
 22     ends, but just that it has occurred. SC.SwipeGesture triggers this after the
 23     swipe has moved a minimum amount—40px by default.
 24 
 25   - [gestureName]Start(args...): called when the gesture is first recognized.
 26     For instance, a swipe gesture may be recognized after the finger has moved a
 27     minimum distance in a horizontal.
 28 
 29   - [gestureName]Changed(args...): called when some property of the gesture
 30     has changed. For instance, this may be called continuously as the user swipes as
 31     the swipe's distance changes.
 32 
 33   - [gestureName]Cancelled(args...): called when a gesture, for one reason
 34     or another, is no longer recognized. For instance, a horizontal swipe gesture
 35     could cancel if the user moves too far in a vertical direction.
 36 
 37   - [gestureName]End(args...): called when a gesture ends. A swipe would end
 38     when the user lifts their finger.
 39 
 40   Gesture Lifecycle
 41   ------------------------
 42   Gestures start receiving events when their view—usually mixing in SC.Gesturable—tells it
 43   about activities with "unassigned" touches. "Unassigned" touches are touches that have
 44   not _yet_ been assigned to a gesture.
 45 
 46   The touch becomes "assigned" when the gesture's touchIsInGesture method returns YES.
 47   When a touch is assigned to a gesture, the gesture becomes the touch's touch responder;
 48   this means that it will receive a touchStart event (to which it must return YES), and
 49   then, all further touch events will be sent _directly_ to the gesture—the gesture's view
 50   will not receive them at all.
 51 
 52   At any point, the gesture may tell the view that it has started, ended, or changed. In
 53   addition, the gesture may tell the view it has been "triggered." A gesture is not
 54   necessarily "triggered" when it starts and ends; for instance, a swipe gesture might
 55   only be triggered if the swipe moves more than a specified amount. The ability to track
 56   when the gesture has been triggered allows views to easily handle the gesture as its own
 57   event, rather than as the individual events that are part of it.
 58 
 59   If, at some point, the gesture must release the touch back (perhaps the gesture had _thought_
 60   the touch was a part of it, but turned out to be incorrect), the release(touch) method releases
 61   it back to the view.
 62 
 63   Exclusivity
 64   ---------------------------------
 65   The concept described above gives the gestures a way to be either exclusive or inclusive as-needed:
 66   they can choose to take exclusive control of a touch if they think it is theirs, but if they are
 67   not sure, they can wait and see.
 68 
 69   Status Object
 70   ---------------------------------
 71   It is a common need to track some data related to the touch, but without modifying the touch itself.
 72   SC.Gesture is able to keep track of simple hashes for you, mapping them to the SC.Touch object,
 73   so that you can maintain some state related to the touch.
 74 
 75   For instance, you could set status.failed in touchesDragged, if a touch that you previously
 76   thought may have been part of the gesture turned out not to be, and then check for
 77   status.failed in touchIsInGesture, returning NO if present. This would cause the touch
 78   to never be considered for your gesture again.
 79 
 80   touchIsInGesture is called with the status hash provided in the second argument. You may look
 81   up the status hash for a touch at any time by calling this.statusForTouch(touch).
 82 
 83 
 84   Implementing a Gesture
 85   ---------------------------------
 86   To write a gesture, you would generally implement the following methods:
 87 
 88   - touchIsInGesture: Return YES when the touch is—or is likely enough to be that you
 89     want your gesture to have exclusive control over the touch. You usually do not
 90     perform much gesture logic here—instead, you save it for touchStart, which will
 91     get called after you return YES from this method.
 92 
 93   - touchStart: Return YES to accept control of the touch. If you do not return YES,
 94     your gesture will not receive touchesDragged nor touchEnd events. At this point,
 95     you may (or may not) wish to tell the view that the gesture has started by using the
 96     start(args...) method.
 97 
 98   - touchesDragged: Use this as you would use it in an SC.View to track the touches
 99     assigned to the gesture. At this point, you might want to tell the view that the
100     gesture has updated by using the change(args...) method.
101 
102   - touchEnd: Again, use this like you would in an SC.View to track when touches
103     assigned to the gesture have ended. This is also a potential time to alert the view
104     that the gesture has ended, by using the end(args...) method. Further, this may
105     also be the time to "trigger" the gesture.
106 
107 */
108 SC.Gesture = SC.Object.extend({
109 
110   /** @private Tracks when the gesture is active or not. */
111   _sc_isActive: false,
112 
113   /**
114     Whether to receive touch events for each distinct touch (rather than only the first touch start
115     and last touch end).
116 
117     @type Boolean
118     @default false
119     @see SC.View#acceptsMultitouch
120   */
121   acceptsMultitouch: false,
122 
123   /**
124     The gesture's name. When calling events on the owning SC.View, this name will
125     be prefixed to the methods. For instance, if the method to be called is
126     'Start', and the gesture's name is 'swipe', SC.Gesture will call 'swipeStart'.
127 
128     @type String
129     @default "gesture"
130   */
131   name: "gesture",
132 
133   /**
134     Return YES to take exclusive control over the touch. In addition to the
135     SC.Touch object you may take control of, you are also provided a "status"
136     hash, which is unique for both the gesture instance and the touch instance,
137     which you may use for your own purposes.
138 
139     @param {SC.Touch} touch The touch.
140     @param {Object} status A unique status hash for the given touch.
141     @returns {Boolean} true if the gesture should claim the touch; false to leave it unclaimed.
142   */
143   touchIsInGesture: function(touch, status) {
144     return NO;
145   },
146 
147   /**
148     After you return YES from touchIsInGesture (or otherwise 'take' a touch, perhaps
149     using the 'take' method), touchStart will be called.
150 
151     This is where you do any logic needed now that the touch is part of the gesture.
152     For instance, you could inform the view that the gesture has started by calling
153     this.start().
154 
155     NOTE: SC.Gesture is just like SC.View in that it has an acceptsMultitouch property.
156     If NO (the default), the gesture will only receive touchStart for the first touch
157     assigned to it, and only receive touchEnd for the last touch that ends.
158 
159     @param {SC.Touch} touch The touch that started.
160     @returns {Boolean} true if the gesture should respond to the touch; false otherwise (this should always return true)
161     @see SC.ResponderProtocol#touchStart
162   */
163   touchStart: function(touch) {
164     return true;
165   },
166 
167   /**
168     Called when a touch assigned to the gesture ends.
169 
170     If there are no remaining touches on the gesture, you may want to call end() to
171     notify the view that the gesture has ended (if you haven't ended the gesture
172     already).
173 
174     NOTE: SC.Gesture is just like SC.View in that it has an acceptsMultitouch property.
175     If NO (the default), the gesture will only receive touchStart for the first touch
176     assigned to it, and only receive touchEnd for the last touch that ends.
177 
178     @name touchEnd
179     @function
180     @param {SC.Touch} touch The touch that ended.
181   */
182 
183   /**
184     Starts the gesture (marking it as "active"), and notifies the view.
185 
186     You can pass any number of arguments to start. They will, along with
187     the gesture instance itself, will be passed to the appropriate gesture
188     event on the SC.View.
189   */
190   start: function() {
191     if (!this._sc_isActive) {
192       this._sc_isActive = true;
193 
194       // var argumentsLength = arguments.length,
195       //     args = new Array(argumentsLength + 1);
196 
197       // // Unshift this to the front of arguments.
198       // args[0] = this;
199       // for (var i = 0, len = argumentsLength; i < len; i++) { args[i + 1] = arguments[i]; }
200 
201       // var act = this.name + "Start";
202       // if (this.view[act]) this.view[act].apply(this.view, args);
203 
204       // Fast arguments access. Don't materialize the `arguments` object, it is costly.
205       var argumentsLength = arguments.length,
206           args = new Array(argumentsLength);
207 
208       for (var i = 0, len = argumentsLength; i < len; i++) { args[i] = arguments[i]; }
209 
210       var act = this.name + "Start";
211       if (this.view[act]) this.view[act].apply(this.view, args);
212     }
213   },
214 
215   /**
216     Ends the gesture, if it is active (marking it as not active), and notifies
217     the view.
218 
219     You may pass any number of arguments to end(). They, along with your gesture
220     instance itself, will be passed to the appropriate gesture event on the SC.View.
221   */
222   end: function() {
223     if (this._sc_isActive) {
224       this._sc_isActive = false;
225 
226       // var argumentsLength = arguments.length,
227       //     args = new Array(argumentsLength + 1);
228 
229       // // Unshift this to the front of arguments.
230       // args[0] = this;
231       // for (var i = 0, len = argumentsLength; i < len; i++) { args[i + 1] = arguments[i]; }
232 
233       // var act = this.name + "End";
234       // if (this.view[act]) this.view[act].apply(this.view, args);
235 
236       // Fast arguments access. Don't materialize the `arguments` object, it is costly.
237       var argumentsLength = arguments.length,
238           args = new Array(argumentsLength);
239 
240       for (var i = 0, len = argumentsLength; i < len; i++) { args[i] = arguments[i]; }
241 
242       var act = this.name + "End";
243       if (this.view[act]) this.view[act].apply(this.view, args);
244     }
245   },
246 
247   /**
248     If the gesture is active, notifies the view that the gesture has
249     changed.
250 
251     The gesture, along with any arguments to change(), will be passed to
252     the appropriate method on the SC.View.
253   */
254   change: function() {
255     if (this._sc_isActive) {
256       // var argumentsLength = arguments.length,
257       //     args = new Array(argumentsLength + 1);
258 
259       // // Unshift this to the front of arguments.
260       // args[0] = this;
261       // for (var i = 0, len = argumentsLength; i < len; i++) { args[i + 1] = arguments[i]; }
262 
263       // var act = this.name + "Changed";
264       // if (this.view[act]) this.view[act].apply(this.view, args);
265 
266       // Fast arguments access. Don't materialize the `arguments` object, it is costly.
267       var argumentsLength = arguments.length,
268           args = new Array(argumentsLength);
269 
270       for (var i = 0, len = argumentsLength; i < len; i++) { args[i] = arguments[i]; }
271 
272       var act = this.name + "Changed";
273       if (this.view[act]) this.view[act].apply(this.view, args);
274     }
275   },
276 
277   /**
278     Cancels the gesture, if it is active, and notifies the view that the
279     gesture has been cancelled.
280 
281     Gestures are cancelled when they have ended, but any action that would
282     normally be appropriate due to their ending should not be performed.
283 
284     The gesture, along with any arguments to cancel(), will be passed to the
285     appropriate method on the SC.View.
286   */
287   cancel: function(){
288     if (this._sc_isActive) {
289       this._sc_isActive = false;
290 
291       // var argumentsLength = arguments.length,
292       //     args = new Array(argumentsLength + 1);
293 
294       // // Unshift this to the front of arguments.
295       // args[0] = this;
296       // for (var i = 0, len = argumentsLength; i < len; i++) { args[i + 1] = arguments[i]; }
297 
298       // var act = this.name + "Cancelled";
299       // if (this.view[act]) this.view[act].apply(this.view, args);
300 
301       // Fast arguments access. Don't materialize the `arguments` object, it is costly.
302       var argumentsLength = arguments.length,
303           args = new Array(argumentsLength);
304 
305       for (var i = 0, len = argumentsLength; i < len; i++) { args[i] = arguments[i]; }
306 
307       var act = this.name + "Cancelled";
308       if (this.view[act]) this.view[act].apply(this.view, args);
309     }
310   },
311 
312   /**
313     Triggers the gesture, notifying the view that the gesture has happened.
314 
315     You should trigger a gesture where it would be natural to say it has "happened";
316     for instance, if a touch moves a couple of pixels, you probably wouldn't say
317     a swipe has occurred—though you might say it has "begun." And you wouldn't necessarily
318     wait until the touch has ended either. Once the touch has moved a certain amount,
319     there has definitely been a swipe. By calling trigger() at this point, you will
320     tell the view that it has occurred.
321 
322     For SC.SwipeGesture, this allows a view to implement only swipe(), and then be
323     automatically notified whenever any swipe has occurred.
324   */
325   trigger: function() {
326     // Fast arguments access. Don't materialize the `arguments` object, it is costly.
327     var argumentsLength = arguments.length,
328         // args = new Array(argumentsLength + 1);
329         args = new Array(argumentsLength);
330 
331     // // Unshift this to the front of arguments.
332     // args[0] = this;
333     // for (var i = 0, len = argumentsLength; i < len; i++) { args[i + 1] = arguments[i]; }
334 
335     for (var i = 0, len = argumentsLength; i < len; i++) { args[i] = arguments[i]; }
336 
337     var act = this.name;
338     if (this.view[act]) this.view[act].apply(this.view, args);
339   },
340 
341   /**
342     Takes possession of a touch.
343 
344     This is called automatically when you return YES from touchIsInGesture.
345   */
346   // take: function(touch) {
347   //   if (!touch.isTaken) {
348   //     touch.isTaken = YES; // because even changing responder won't prevent it from being used this cycle.
349   //     if (SC.none(touch.touchResponder) || touch.touchResponder !== this) touch.makeTouchResponder(this, YES);
350   //   }
351   //   //@if(debug)
352   //   else {
353   //     SC.warn("Developer Warning: A gesture tried to take a touch that was already taken: %@".fmt(this));
354   //   }
355   //   //@endif
356   // },
357 
358   /**
359     Releases a touch back to its previous owner, which is usually the view. This allows
360     you to give back control of a touch that it turns out is not part of the gesture.
361 
362     This takes effect immediately, because you would usually call this from
363     touchesDragged or such.
364   */
365   // release: function(touch) {
366   //   if (touch.isTaken) {
367   //     touch.isTaken = NO;
368   //     if (touch.nextTouchResponder) touch.makeTouchResponder(touch.nextTouchResponder);
369   //   }
370   //   //@if(debug)
371   //   else {
372   //     SC.warn("Developer Warning: A gesture tried to release a touch that was not taken: %@".fmt(this));
373   //   }
374   //   //@endif
375   // },
376 
377   /**
378     Discards a touch, making its responder null. This makes the touch go away and never
379     come back—not to this gesture, nor to any other, nor to the view, nor to any other
380     view.
381   */
382   // discardTouch: function(touch) {
383   //   touch.isTaken = YES; // because even changing responder won't prevent it from being used this cycle.
384   //   touch.makeTouchResponder(null);
385   // },
386 
387   /**
388     Called by the view when a touch session has begun.
389 
390     You should override this method in your custom SC.Gesturable subclasses to set up any touch
391     session state. For example, you may want to track the initial touch start time in order to
392     decide how to react when or if additional touches start later.
393 
394     @param {SC.Touch} touch The touch that started the session.
395     @returns {void}
396     */
397   touchSessionStarted: function (touch) {
398   },
399 
400   /**
401     Called by the view when the touch session has ended.
402 
403     This will occur because all touches in the session have finished.
404 
405     You should override this method in your custom SC.Gesturable subclasses to clean up any state
406     variables used in the touch session.
407 
408     @returns {void}
409    */
410   touchSessionEnded: function () {
411   },
412 
413   /**
414     Called by the view when the touch session was cancelled.
415 
416     This will occur because this gesture returned `false` in any of `touchAddedToSession`,
417     `touchesMovedInSession`, `touchEndedInSession`, `touchCancelledInSession` to indicate that the
418     gesture is no longer interested in the session or because another gesture claimed the touch
419     session for itself, forcing all other gestures out (rare).
420 
421     You should override this method in your custom SC.Gesturable subclasses to clean up any state
422     variables used in the touch session.
423 
424     @returns {void}
425    */
426   touchSessionCancelled: function () {
427   },
428 
429   /**
430     @param {SC.Touch} touch The touch to be added to the session.
431     @param {Array} touchesInSession The touches already in the session.
432     @returns {Boolean} True if the gesture is still interested in the touch session; false to stop getting notified for any further touch changes in the touch session.
433     */
434   touchAddedToSession: function (touch, touchesInSession) {
435     return true; // Most gestures should theoretically be interested in a new touch session.
436   },
437 
438   /**
439     @param {SC.Touch} touch The touch to be removed from the session.
440     @param {Array} touchesInSession The touches still remaining in the session.
441     @returns {Boolean} True if the gesture is still interested in the touch session; false to stop getting notified for any further touch changes in the touch session.
442     */
443   touchCancelledInSession: function (touch, touchesInSession) {
444     return true;
445   },
446 
447   /**
448     @param {SC.Touch} touch The touch to be removed from the session.
449     @param {Array} touchesInSession The touches still remaining in the session.
450     @returns {Boolean} True if the gesture is still interested in the touch session; false to stop getting notified for any further touch changes in the touch session.
451     */
452   touchEndedInSession: function (touch, touchesInSession) {
453     return true;
454   },
455 
456   /**
457     @param {Array} touchesInSession The touches in the session.
458     @returns {Boolean} True if the gesture is still interested in the touch session; false to stop getting notified for any further touch changes in the touch session.
459     */
460   touchesMovedInSession: function (touchesInSession) {
461     return true;
462   }
463 
464 });
465