Bug 668953 - Support two-finger horizontal swipe on OS X Lion. r=bgirard

This commit is contained in:
Steven Michaud 2011-08-11 12:42:23 -05:00
parent 58c0c20c7a
commit 21ffbbde09
4 changed files with 197 additions and 58 deletions

View File

@ -141,10 +141,6 @@
#ifdef XP_MACOSX #ifdef XP_MACOSX
#import <ApplicationServices/ApplicationServices.h> #import <ApplicationServices/ApplicationServices.h>
#ifdef MOZ_WIDGET_COCOA
#include "nsCocoaFeatures.h"
#endif
#endif #endif
using namespace mozilla; using namespace mozilla;
@ -377,7 +373,6 @@ public:
static void OnEvent(nsEvent* aEvent); static void OnEvent(nsEvent* aEvent);
static void Shutdown(); static void Shutdown();
static PRUint32 GetTimeoutTime(); static PRUint32 GetTimeoutTime();
static PRUint32 GetGestureTimeoutTime();
static PRInt32 AccelerateWheelDelta(PRInt32 aScrollLines, static PRInt32 AccelerateWheelDelta(PRInt32 aScrollLines,
PRBool aIsHorizontal, PRBool aAllowScrollSpeedOverride, PRBool aIsHorizontal, PRBool aAllowScrollSpeedOverride,
nsIScrollableFrame::ScrollUnit *aScrollQuantity, nsIScrollableFrame::ScrollUnit *aScrollQuantity,
@ -387,10 +382,6 @@ public:
enum { enum {
kScrollSeriesTimeout = 80 kScrollSeriesTimeout = 80
}; };
#ifdef MOZ_WIDGET_COCOA
static PRBool GetGestureTriggered();
static void SetGestureTriggered();
#endif
protected: protected:
static nsIntPoint GetScreenPoint(nsGUIEvent* aEvent); static nsIntPoint GetScreenPoint(nsGUIEvent* aEvent);
static void OnFailToScrollTarget(); static void OnFailToScrollTarget();
@ -411,9 +402,6 @@ protected:
static PRUint32 sMouseMoved; // in milliseconds static PRUint32 sMouseMoved; // in milliseconds
static nsITimer* sTimer; static nsITimer* sTimer;
static PRInt32 sScrollSeriesCounter; static PRInt32 sScrollSeriesCounter;
#ifdef MOZ_WIDGET_COCOA
static PRUint32 sGestureTriggered; // in milliseconds
#endif
}; };
nsWeakFrame nsMouseWheelTransaction::sTargetFrame(nsnull); nsWeakFrame nsMouseWheelTransaction::sTargetFrame(nsnull);
@ -421,9 +409,6 @@ PRUint32 nsMouseWheelTransaction::sTime = 0;
PRUint32 nsMouseWheelTransaction::sMouseMoved = 0; PRUint32 nsMouseWheelTransaction::sMouseMoved = 0;
nsITimer* nsMouseWheelTransaction::sTimer = nsnull; nsITimer* nsMouseWheelTransaction::sTimer = nsnull;
PRInt32 nsMouseWheelTransaction::sScrollSeriesCounter = 0; PRInt32 nsMouseWheelTransaction::sScrollSeriesCounter = 0;
#ifdef MOZ_WIDGET_COCOA
PRUint32 nsMouseWheelTransaction::sGestureTriggered = 0;
#endif
static PRBool static PRBool
OutOfTime(PRUint32 aBaseTime, PRUint32 aThreshold) OutOfTime(PRUint32 aBaseTime, PRUint32 aThreshold)
@ -502,29 +487,8 @@ nsMouseWheelTransaction::EndTransaction()
sTimer->Cancel(); sTimer->Cancel();
sTargetFrame = nsnull; sTargetFrame = nsnull;
sScrollSeriesCounter = 0; 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 void
nsMouseWheelTransaction::OnEvent(nsEvent* aEvent) nsMouseWheelTransaction::OnEvent(nsEvent* aEvent)
{ {
@ -670,12 +634,6 @@ nsMouseWheelTransaction::GetScreenPoint(nsGUIEvent* aEvent)
return aEvent->refPoint + aEvent->widget->WidgetToScreenOffset(); return aEvent->refPoint + aEvent->widget->WidgetToScreenOffset();
} }
PRUint32
nsMouseWheelTransaction::GetGestureTimeoutTime()
{
return Preferences::GetUint("mousewheel.transaction.gesturetimeout", 300);
}
PRUint32 PRUint32
nsMouseWheelTransaction::GetTimeoutTime() nsMouseWheelTransaction::GetTimeoutTime()
{ {
@ -2839,22 +2797,6 @@ nsEventStateManager::DoScrollText(nsIFrame* aTargetFrame,
} }
} }
#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 (!passToParent && frameToScroll) {
if (aScrollQuantity == nsIScrollableFrame::LINES) { if (aScrollQuantity == nsIScrollableFrame::LINES) {
// When this is called for querying the scroll target information, // When this is called for querying the scroll target information,

View File

@ -1560,6 +1560,13 @@ public:
{ {
} }
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 PRUint32 direction; // See nsIDOMSimpleGestureEvent for values
PRFloat64 delta; // Delta for magnify and rotate events PRFloat64 delta; // Delta for magnify and rotate events
}; };

View File

@ -168,6 +168,49 @@ extern "C" long TSMProcessRawKeyEvent(EventRef carbonEvent);
- (long long)_scrollPhase; - (long long)_scrollPhase;
@end @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< @interface ChildView : NSView<
#ifdef ACCESSIBILITY #ifdef ACCESSIBILITY
mozAccessible, mozAccessible,
@ -239,6 +282,11 @@ extern "C" long TSMProcessRawKeyEvent(EventRef carbonEvent);
float mCumulativeRotation; float mCumulativeRotation;
BOOL mDidForceRefreshOpenGL; BOOL mDidForceRefreshOpenGL;
// Support for fluid swipe tracking.
#ifdef __LP64__
BOOL *mSwipeAnimationCancelled;
#endif
} }
// class initialization // class initialization
@ -286,6 +334,12 @@ extern "C" long TSMProcessRawKeyEvent(EventRef carbonEvent);
- (void)magnifyWithEvent:(NSEvent *)anEvent; - (void)magnifyWithEvent:(NSEvent *)anEvent;
- (void)rotateWithEvent:(NSEvent *)anEvent; - (void)rotateWithEvent:(NSEvent *)anEvent;
- (void)endGestureWithEvent:(NSEvent *)anEvent; - (void)endGestureWithEvent:(NSEvent *)anEvent;
// Support for fluid swipe tracking.
#ifdef __LP64__
- (void)maybeTrackScrollEventAsSwipe:(NSEvent *)anEvent
scrollOverflow:(PRInt32)overflow;
#endif
@end @end
class ChildViewMouseTracker { class ChildViewMouseTracker {

View File

@ -47,6 +47,7 @@
#include "prlog.h" #include "prlog.h"
#include <unistd.h> #include <unistd.h>
#include <math.h>
#include "nsChildView.h" #include "nsChildView.h"
#include "nsCocoaWindow.h" #include "nsCocoaWindow.h"
@ -2012,6 +2013,10 @@ NSEvent* gLastDragMouseDownEvent = nil;
mDidForceRefreshOpenGL = NO; mDidForceRefreshOpenGL = NO;
[self setFocusRingType:NSFocusRingTypeNone]; [self setFocusRingType:NSFocusRingTypeNone];
#ifdef __LP64__
mSwipeAnimationCancelled = nil;
#endif
} }
// register for things we'll take from other applications // register for things we'll take from other applications
@ -2987,6 +2992,126 @@ NSEvent* gLastDragMouseDownEvent = nil;
NS_OBJC_END_TRY_ABORT_BLOCK; 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 // Returning NO from this method only disallows ordering on mousedown - in order
// to prevent it for mouseup too, we need to call [NSApp preventWindowOrdering] // to prevent it for mouseup too, we need to call [NSApp preventWindowOrdering]
// when handling the mousedown event. // when handling the mousedown event.
@ -3670,6 +3795,17 @@ NSEvent* gLastDragMouseDownEvent = nil;
geckoEvent.delta = NSToIntRound(scrollDeltaPixels); geckoEvent.delta = NSToIntRound(scrollDeltaPixels);
nsAutoRetainCocoaObject kungFuDeathGrip(self); nsAutoRetainCocoaObject kungFuDeathGrip(self);
mGeckoChild->DispatchWindowEvent(geckoEvent); 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; NS_OBJC_END_TRY_ABORT_BLOCK;