Bug 668953 - Support two-finger horizontal swipe on OS X Lion. r=bgirard
authorSteven Michaud <smichaud@pobox.com>
Thu, 11 Aug 2011 12:42:23 -0500
changeset 74253 420c7a55b4a084a6c132662de25265ed9013f949
parent 74252 90719558b5e226593af33d47e65fa770df203bc2
child 74254 ffb2a6be641a6adf0b462666925d65ff84420433
push id2
push userbsmedberg@mozilla.com
push dateFri, 19 Aug 2011 14:38:13 +0000
reviewersbgirard
bugs668953
milestone8.0a1
Bug 668953 - Support two-finger horizontal swipe on OS X Lion. r=bgirard
content/events/src/nsEventStateManager.cpp
widget/public/nsGUIEvent.h
widget/src/cocoa/nsChildView.h
widget/src/cocoa/nsChildView.mm
--- a/content/events/src/nsEventStateManager.cpp
+++ b/content/events/src/nsEventStateManager.cpp
@@ -136,20 +136,16 @@
 #include "mozilla/Services.h"
 #include "mozAutoDocUpdate.h"
 #include "nsHTMLLabelElement.h"
 
 #include "mozilla/Preferences.h"
 
 #ifdef XP_MACOSX
 #import <ApplicationServices/ApplicationServices.h>
-
-#ifdef MOZ_WIDGET_COCOA
-#include "nsCocoaFeatures.h"
-#endif 
 #endif
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 //#define DEBUG_DOCSHELL_FOCUS
 
 #define NS_USER_INTERACTION_INTERVAL 5000 // ms
@@ -372,30 +368,25 @@ public:
   // Be careful, UpdateTransaction may fire a DOM event, therefore, the target
   // frame might be destroyed in the event handler.
   static PRBool UpdateTransaction(PRInt32 aNumLines,
                                   PRBool aScrollHorizontal);
   static void EndTransaction();
   static void OnEvent(nsEvent* aEvent);
   static void Shutdown();
   static PRUint32 GetTimeoutTime();
-  static PRUint32 GetGestureTimeoutTime();
   static PRInt32 AccelerateWheelDelta(PRInt32 aScrollLines,
                    PRBool aIsHorizontal, PRBool aAllowScrollSpeedOverride,
                    nsIScrollableFrame::ScrollUnit *aScrollQuantity,
                    PRBool aLimitToMaxOnePageScroll = PR_TRUE);
   static PRBool IsAccelerationEnabled();
 
   enum {
     kScrollSeriesTimeout = 80
   };
-#ifdef MOZ_WIDGET_COCOA
-  static PRBool GetGestureTriggered();
-  static void SetGestureTriggered();
-#endif
 protected:
   static nsIntPoint GetScreenPoint(nsGUIEvent* aEvent);
   static void OnFailToScrollTarget();
   static void OnTimeout(nsITimer *aTimer, void *aClosure);
   static void SetTimeout();
   static PRUint32 GetIgnoreMoveDelayTime();
   static PRInt32 GetAccelerationStart();
   static PRInt32 GetAccelerationFactor();
@@ -406,29 +397,23 @@ protected:
                    PRBool aIsHorizontal,
                    nsIScrollableFrame::ScrollUnit *aScrollQuantity);
 
   static nsWeakFrame sTargetFrame;
   static PRUint32    sTime;        // in milliseconds
   static PRUint32    sMouseMoved;  // in milliseconds
   static nsITimer*   sTimer;
   static PRInt32     sScrollSeriesCounter;
-#ifdef MOZ_WIDGET_COCOA
-  static PRUint32    sGestureTriggered; // in milliseconds
-#endif
 };
 
 nsWeakFrame nsMouseWheelTransaction::sTargetFrame(nsnull);
 PRUint32    nsMouseWheelTransaction::sTime        = 0;
 PRUint32    nsMouseWheelTransaction::sMouseMoved  = 0;
 nsITimer*   nsMouseWheelTransaction::sTimer       = nsnull;
 PRInt32     nsMouseWheelTransaction::sScrollSeriesCounter = 0;
-#ifdef MOZ_WIDGET_COCOA
-PRUint32      nsMouseWheelTransaction::sGestureTriggered = 0;
-#endif
 
 static PRBool
 OutOfTime(PRUint32 aBaseTime, PRUint32 aThreshold)
 {
   PRUint32 now = PR_IntervalToMilliseconds(PR_IntervalNow());
   return (now - aBaseTime > aThreshold);
 }
 
@@ -497,39 +482,18 @@ nsMouseWheelTransaction::UpdateTransacti
 
 void
 nsMouseWheelTransaction::EndTransaction()
 {
   if (sTimer)
     sTimer->Cancel();
   sTargetFrame = nsnull;
   sScrollSeriesCounter = 0;
-#ifdef MOZ_WIDGET_COCOA
-  sGestureTriggered = 0;
-#endif
 }
 
-#ifdef MOZ_WIDGET_COCOA
-void
-nsMouseWheelTransaction::SetGestureTriggered() {
-  sGestureTriggered = PR_IntervalToMilliseconds(PR_IntervalNow());
-}
-
-PRBool
-nsMouseWheelTransaction::GetGestureTriggered() {
-  if (sGestureTriggered != 0 &&
-      OutOfTime(sGestureTriggered, GetGestureTimeoutTime())) {
-    // Start accepting new gestures
-    sGestureTriggered = 0;
-  }
-
-  return sGestureTriggered != 0;
-}
-#endif
-
 void
 nsMouseWheelTransaction::OnEvent(nsEvent* aEvent)
 {
   if (!sTargetFrame)
     return;
 
   if (OutOfTime(sTime, GetTimeoutTime())) {
     // Even if the scroll event which is handled after timeout, but onTimeout
@@ -666,22 +630,16 @@ nsIntPoint
 nsMouseWheelTransaction::GetScreenPoint(nsGUIEvent* aEvent)
 {
   NS_ASSERTION(aEvent, "aEvent is null");
   NS_ASSERTION(aEvent->widget, "aEvent-widget is null");
   return aEvent->refPoint + aEvent->widget->WidgetToScreenOffset();
 }
 
 PRUint32
-nsMouseWheelTransaction::GetGestureTimeoutTime()
-{
-  return Preferences::GetUint("mousewheel.transaction.gesturetimeout", 300);
-}
-
-PRUint32
 nsMouseWheelTransaction::GetTimeoutTime()
 {
   return Preferences::GetUint("mousewheel.transaction.timeout", 1500);
 }
 
 PRUint32
 nsMouseWheelTransaction::GetIgnoreMoveDelayTime()
 {
@@ -2834,32 +2792,16 @@ nsEventStateManager::DoScrollText(nsIFra
             passToParent = PR_TRUE;
             nsMouseWheelTransaction::EndTransaction();
           }
         }
       }
     }
   }
 
-#ifdef MOZ_WIDGET_COCOA
-  // On lion scroll will trigger back/forward at the edge of the page
-  if (isHorizontal && passToParent && nsCocoaFeatures::OnLionOrLater()) {
-    if (!nsMouseWheelTransaction::GetGestureTriggered()) {
-      if (numLines > 4 || numLines < -4) {
-        DoScrollHistory(-numLines);
-        nsMouseWheelTransaction::SetGestureTriggered();
-        return NS_OK;
-      }
-    } else {
-      // Extend the gesture in progress
-      nsMouseWheelTransaction::SetGestureTriggered();
-    }
-  }
-#endif
-
   if (!passToParent && frameToScroll) {
     if (aScrollQuantity == nsIScrollableFrame::LINES) {
       // When this is called for querying the scroll target information,
       // we shouldn't limit the scrolling amount to less one page.
       // Otherwise, we shouldn't scroll more one page at once.
       numLines =
         nsMouseWheelTransaction::AccelerateWheelDelta(numLines, isHorizontal,
                                                       aAllowScrollSpeedOverride,
--- a/widget/public/nsGUIEvent.h
+++ b/widget/public/nsGUIEvent.h
@@ -1555,16 +1555,23 @@ class nsSimpleGestureEvent : public nsMo
 public:
   nsSimpleGestureEvent(PRBool isTrusted, PRUint32 msg, nsIWidget* w,
                          PRUint32 directionArg, PRFloat64 deltaArg)
     : nsMouseEvent_base(isTrusted, msg, w, NS_SIMPLE_GESTURE_EVENT),
       direction(directionArg), delta(deltaArg)
   {
   }
 
+  nsSimpleGestureEvent(const nsSimpleGestureEvent& other)
+    : nsMouseEvent_base((other.flags & NS_EVENT_FLAG_TRUSTED) != 0,
+                        other.message, other.widget, NS_SIMPLE_GESTURE_EVENT),
+      direction(other.direction), delta(other.delta)
+  {
+  }
+
   PRUint32 direction;   // See nsIDOMSimpleGestureEvent for values
   PRFloat64 delta;      // Delta for magnify and rotate events
 };
 
 class nsTransitionEvent : public nsEvent
 {
 public:
   nsTransitionEvent(PRBool isTrusted, PRUint32 msg,
--- a/widget/src/cocoa/nsChildView.h
+++ b/widget/src/cocoa/nsChildView.h
@@ -163,16 +163,59 @@ extern "C" long TSMProcessRawKeyEvent(Ev
 @end
 
 // Undocumented scrollPhase flag that lets us discern between real scrolls and
 // automatically firing momentum scroll events.
 @interface NSEvent (ScrollPhase)
 - (long long)_scrollPhase;
 @end
 
+// The following section, required to support fluid swipe tracking on OS X 10.7
+// and up, contains defines/declarations that are only available on 10.7 and up.
+// [NSEvent trackSwipeEventWithOptions:...] also requires that the compiler
+// support "blocks"
+// (http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html)
+// -- which it does on 10.6 and up (using the 10.6 SDK or higher).
+#ifdef __LP64__
+enum {
+  NSEventPhaseNone        = 0,
+  NSEventPhaseBegan       = 0x1 << 0,
+  NSEventPhaseStationary  = 0x1 << 1,
+  NSEventPhaseChanged     = 0x1 << 2,
+  NSEventPhaseEnded       = 0x1 << 3,
+  NSEventPhaseCancelled   = 0x1 << 4,
+};
+typedef NSUInteger NSEventPhase;
+
+enum {
+  NSEventSwipeTrackingLockDirection = 0x1 << 0,
+  NSEventSwipeTrackingClampGestureAmount = 0x1 << 1
+};
+typedef NSUInteger NSEventSwipeTrackingOptions;
+
+enum {
+  NSEventGestureAxisNone = 0,
+  NSEventGestureAxisHorizontal,
+  NSEventGestureAxisVertical
+};
+typedef NSInteger NSEventGestureAxis;
+
+@interface NSEvent (FluidSwipeTracking)
++ (BOOL)isSwipeTrackingFromScrollEventsEnabled;
+- (BOOL)hasPreciseScrollingDeltas;
+- (CGFloat)scrollingDeltaX;
+- (CGFloat)scrollingDeltaY;
+- (NSEventPhase)phase;
+- (void)trackSwipeEventWithOptions:(NSEventSwipeTrackingOptions)options
+          dampenAmountThresholdMin:(CGFloat)minDampenThreshold
+                               max:(CGFloat)maxDampenThreshold
+                      usingHandler:(void (^)(CGFloat gestureAmount, NSEventPhase phase, BOOL isComplete, BOOL *stop))trackingHandler;
+@end
+#endif // #ifdef __LP64__
+
 @interface ChildView : NSView<
 #ifdef ACCESSIBILITY
                               mozAccessible,
 #endif
                               mozView, NSTextInput>
 {
 @private
   // the nsChildView that created the view. It retains this NSView, so
@@ -234,16 +277,21 @@ extern "C" long TSMProcessRawKeyEvent(Ev
     eGestureState_StartGesture,
     eGestureState_MagnifyGesture,
     eGestureState_RotateGesture
   } mGestureState;
   float mCumulativeMagnification;
   float mCumulativeRotation;
 
   BOOL mDidForceRefreshOpenGL;
+
+  // Support for fluid swipe tracking.
+#ifdef __LP64__
+  BOOL *mSwipeAnimationCancelled;
+#endif
 }
 
 // class initialization
 + (void)initialize;
 
 // these are sent to the first responder when the window key status changes
 - (void)viewsWindowDidBecomeKey;
 - (void)viewsWindowDidResignKey;
@@ -281,16 +329,22 @@ extern "C" long TSMProcessRawKeyEvent(Ev
 //
 // The prototypes were obtained from the following link:
 // http://cocoadex.com/2008/02/nsevent-modifications-swipe-ro.html
 - (void)swipeWithEvent:(NSEvent *)anEvent;
 - (void)beginGestureWithEvent:(NSEvent *)anEvent;
 - (void)magnifyWithEvent:(NSEvent *)anEvent;
 - (void)rotateWithEvent:(NSEvent *)anEvent;
 - (void)endGestureWithEvent:(NSEvent *)anEvent;
+
+// Support for fluid swipe tracking.
+#ifdef __LP64__
+- (void)maybeTrackScrollEventAsSwipe:(NSEvent *)anEvent
+                      scrollOverflow:(PRInt32)overflow;
+#endif
 @end
 
 class ChildViewMouseTracker {
 
 public:
 
   static void MouseMoved(NSEvent* aEvent);
   static void OnDestroyView(ChildView* aView);
--- a/widget/src/cocoa/nsChildView.mm
+++ b/widget/src/cocoa/nsChildView.mm
@@ -42,16 +42,17 @@
  * ***** END LICENSE BLOCK ***** */
 
 #ifdef MOZ_LOGGING
 #define FORCE_PR_LOG
 #endif
 #include "prlog.h"
 
 #include <unistd.h>
+#include <math.h>
  
 #include "nsChildView.h"
 #include "nsCocoaWindow.h"
 
 #include "nsObjCExceptions.h"
 #include "nsCOMPtr.h"
 #include "nsToolkit.h"
 #include "nsCRT.h"
@@ -2007,16 +2008,20 @@ NSEvent* gLastDragMouseDownEvent = nil;
 
     // We can't call forceRefreshOpenGL here because, in order to work around
     // the bug, it seems we need to have a draw already happening. Therefore,
     // we call it in drawRect:inContext:, when we know that a draw is in
     // progress.
     mDidForceRefreshOpenGL = NO;
 
     [self setFocusRingType:NSFocusRingTypeNone];
+
+#ifdef __LP64__
+    mSwipeAnimationCancelled = nil;
+#endif
   }
   
   // register for things we'll take from other applications
   PR_LOG(sCocoaLog, PR_LOG_ALWAYS, ("ChildView initWithFrame: registering drag types\n"));
   [self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType,
                                                           NSStringPboardType,
                                                           NSHTMLPboardType,
                                                           NSURLPboardType,
@@ -2982,16 +2987,136 @@ NSEvent* gLastDragMouseDownEvent = nil;
   // Clear the gestures state.
   mGestureState = eGestureState_None;
   mCumulativeMagnification = 0.0;
   mCumulativeRotation = 0.0;
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
+// Support fluid swipe tracking on OS X 10.7 and higher.  We must be careful
+// to only invoke this support on a horizontal two-finger gesture that really
+// is a swipe (and not a scroll) -- in other words, the app is responsible
+// for deciding which is which.  But once the decision is made, the OS tracks
+// the swipe until it has finished, and decides whether or not it succeeded.
+// A swipe has the same functionality as the Back and Forward buttons.  For
+// now swipe animation is unsupported (e.g. no bounces).  This method is
+// partly based on Apple sample code available at
+// http://developer.apple.com/library/mac/#releasenotes/Cocoa/AppKit.html
+// (under Fluid Swipe Tracking API).
+#ifdef __LP64__
+- (void)maybeTrackScrollEventAsSwipe:(NSEvent *)anEvent
+                      scrollOverflow:(PRInt32)overflow
+{
+  if (!nsToolkit::OnLionOrLater()) {
+    return;
+  }
+  // This method checks whether the AppleEnableSwipeNavigateWithScrolls global
+  // preference is set.  If it isn't, fluid swipe tracking is disabled, and a
+  // horizontal two-finger gesture is always a scroll (even in Safari).  This
+  // preference can't (currently) be set from the Preferences UI -- only using
+  // 'defaults write'.
+  if (![NSEvent isSwipeTrackingFromScrollEventsEnabled]) {
+    return;
+  }
+  if ([anEvent type] != NSScrollWheel) {
+    return;
+  }
+
+  // If a swipe is currently being tracked kill it -- it's been interrupted by
+  // another gesture or legacy scroll wheel event.
+  if (mSwipeAnimationCancelled && (*mSwipeAnimationCancelled == NO)) {
+    *mSwipeAnimationCancelled = YES;
+    mSwipeAnimationCancelled = nil;
+  }
+
+  // Only initiate tracking if the user has tried to scroll past the edge of
+  // the current page (as indicated by 'overflow' being non-zero).  Gecko only
+  // sets nsMouseScrollEvent.scrollOverflow when it's processing
+  // NS_MOUSE_PIXEL_SCROLL events (not NS_MOUSE_SCROLL events).
+  // nsMouseScrollEvent.scrollOverflow only indicates left or right overflow
+  // for horizontal NS_MOUSE_PIXEL_SCROLL events.
+  if (!overflow) {
+    return;
+  }
+  // Only initiate tracking for gestures that have just begun -- otherwise a
+  // scroll to one side of the page can have a swipe tacked on to it.
+  if ([anEvent phase] != NSEventPhaseBegan) {
+    return;
+  }
+  CGFloat deltaX, deltaY;
+  if ([anEvent hasPreciseScrollingDeltas]) {
+    deltaX = [anEvent scrollingDeltaX];
+    deltaY = [anEvent scrollingDeltaY];
+  } else {
+    deltaX = [anEvent deltaX];
+    deltaY = [anEvent deltaY];
+  }
+  // Only initiate tracking for events whose horizontal element is at least
+  // eight times larger than its vertical element.  This minimizes performance
+  // problems with vertical scrolls (by minimizing the possibility that they'll
+  // be misinterpreted as horizontal swipes), while still tolerating a small
+  // vertical element to a true horizontal swipe.  The number '8' was arrived
+  // at by trial and error.
+  if ((deltaX == 0) || (fabs(deltaX) <= fabs(deltaY) * 8)) {
+    return;
+  }
+
+  // geckoEvent must be initialized now (while anEvent is still available),
+  // but we also need to access it (and modify it) in the following "block"
+  // (the trackingHandler passed to [NSEvent trackSwipeEventWithOptions:...]).
+  // Normally we'd give it the '__block' keyword, but this makes the compiler
+  // crash :-(  Without the '__block' keyword, it becomes immutable from
+  // trackingHandler.  So trackingHandler must make a copy of it and modify
+  // that.
+  nsSimpleGestureEvent geckoEvent(PR_TRUE, NS_SIMPLE_GESTURE_SWIPE, mGeckoChild, 0, 0.0);
+  [self convertCocoaMouseEvent:anEvent toGeckoEvent:&geckoEvent];
+
+  __block BOOL animationCancelled = NO;
+  // At this point, anEvent is the first scroll wheel event in a two-finger
+  // horizontal gesture that we've decided to treat as a swipe.  When we call
+  // [NSEvent trackSwipeEventWithOptions:...], the OS interprets all
+  // subsequent scroll wheel events that are part of this gesture as a swipe,
+  // and stops sending them to us.  The OS calls the trackingHandler "block"
+  // multiple times, asynchronously (sometimes after [NSEvent
+  // maybeTrackScrollEventAsSwipe:...] has returned).  The OS determines when
+  // the gesture has finished, and whether or not it was "successful" -- this
+  // information is passed to trackingHandler.  We must be careful to only
+  // call [NSEvent maybeTrackScrollEventAsSwipe:...] on a "real" swipe --
+  // otherwise two-finger scrolling performance will suffer significantly.
+  [anEvent trackSwipeEventWithOptions:0
+             dampenAmountThresholdMin:-1
+                                  max:1
+                         usingHandler:^(CGFloat gestureAmount, NSEventPhase phase, BOOL isComplete, BOOL *stop) {
+      if (animationCancelled) {
+        *stop = YES;
+        return;
+      }
+      if (isComplete) {
+        if (gestureAmount) {
+          nsSimpleGestureEvent geckoEventCopy(geckoEvent);
+          if (gestureAmount > 0) {
+            geckoEventCopy.direction |= nsIDOMSimpleGestureEvent::DIRECTION_LEFT;
+          } else {
+            geckoEventCopy.direction |= nsIDOMSimpleGestureEvent::DIRECTION_RIGHT;
+          }
+          mGeckoChild->DispatchWindowEvent(geckoEventCopy);
+        }
+        mSwipeAnimationCancelled = nil;
+      }
+    }];
+
+  // We keep a pointer to the __block variable (animationCanceled) so we
+  // can cancel our block handler at any time.  Note: We must assign
+  // &animationCanceled after our block creation and copy -- its address
+  // isn't resolved until then!
+  mSwipeAnimationCancelled = &animationCancelled;
+}
+#endif // #ifdef __LP64__
+
 // Returning NO from this method only disallows ordering on mousedown - in order
 // to prevent it for mouseup too, we need to call [NSApp preventWindowOrdering]
 // when handling the mousedown event.
 - (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent*)aEvent
 {
   // Always using system-provided window ordering for normal windows.
   if (![[self window] isKindOfClass:[PopupWindow class]])
     return NO;
@@ -3665,16 +3790,27 @@ NSEvent* gLastDragMouseDownEvent = nil;
     nsMouseScrollEvent geckoEvent(PR_TRUE, NS_MOUSE_PIXEL_SCROLL, nsnull);
     [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
     geckoEvent.scrollFlags |= inAxis;
     if (isMomentumScroll)
       geckoEvent.scrollFlags |= nsMouseScrollEvent::kIsMomentum;
     geckoEvent.delta = NSToIntRound(scrollDeltaPixels);
     nsAutoRetainCocoaObject kungFuDeathGrip(self);
     mGeckoChild->DispatchWindowEvent(geckoEvent);
+#ifdef __LP64__
+    // scrollOverflow tells us when the user has tried to scroll past the edge
+    // of a page (in those cases it's non-zero).  Gecko only sets it when
+    // processing NS_MOUSE_PIXEL_SCROLL events (not MS_MOUSE_SCROLL events).
+    // It only means left/right overflow when Gecko is processing a horizontal
+    // event.
+    if (inAxis & nsMouseScrollEvent::kIsHorizontal) {
+      [self maybeTrackScrollEventAsSwipe:theEvent
+                          scrollOverflow:geckoEvent.scrollOverflow];
+    }
+#endif // #ifdef __LP64__
   }
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
 -(void)scrollWheel:(NSEvent*)theEvent
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;