Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / renderer_host / chrome_render_widget_host_view_mac_history_swiper.mm
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "chrome/browser/renderer_host/chrome_render_widget_host_view_mac_history_swiper.h"
6
7 #include "chrome/browser/ui/browser.h"
8 #include "chrome/browser/ui/browser_commands.h"
9 #include "chrome/browser/ui/browser_finder.h"
10
11 #import "base/mac/sdk_forward_declarations.h"
12 #import "chrome/browser/ui/cocoa/history_overlay_controller.h"
13
14 // Once we call `[NSEvent trackSwipeEventWithOptions:]`, we cannot reliably
15 // expect NSTouch callbacks. We set this variable to YES and ignore NSTouch
16 // callbacks.
17 static BOOL forceMagicMouse = NO;
18
19 @implementation HistorySwiper
20 @synthesize delegate = delegate_;
21
22 - (id)initWithDelegate:(id<HistorySwiperDelegate>)delegate {
23   self = [super init];
24   if (self) {
25     // Gesture ids start at 0.
26     currentGestureId_ = 0;
27     // No gestures have been processed
28     lastProcessedGestureId_ = -1;
29     delegate_ = delegate;
30   }
31   return self;
32 }
33
34 - (void)dealloc {
35   [self endHistorySwipe];
36   [super dealloc];
37 }
38
39 - (BOOL)handleEvent:(NSEvent*)event {
40   if ([event type] == NSScrollWheel)
41     return [self maybeHandleHistorySwiping:event];
42
43   return NO;
44 }
45
46 - (void)gotUnhandledWheelEvent {
47   gotUnhandledWheelEvent_ = YES;
48 }
49
50 - (void)scrollOffsetPinnedToLeft:(BOOL)left toRight:(BOOL)right {
51   isPinnedLeft_ = left;
52   isPinnedRight_ = right;
53 }
54
55 - (void)setHasHorizontalScrollbar:(BOOL)hasHorizontalScrollbar {
56   hasHorizontalScrollbar_ = hasHorizontalScrollbar;
57 }
58
59 - (BOOL)canRubberbandLeft:(NSView*)view {
60   Browser* browser = chrome::FindBrowserWithWindow([view window]);
61   // If history swiping isn't possible, allow rubberbanding.
62   if (!browser)
63     return true;
64   if (!chrome::CanGoBack(browser))
65     return true;
66   // History swiping is possible. By default, disallow rubberbanding.  If the
67   // user has both started, and then cancelled history swiping for this
68   // gesture, allow rubberbanding.
69   return inGesture_ && historySwipeCancelled_;
70 }
71
72 - (BOOL)canRubberbandRight:(NSView*)view {
73   Browser* browser = chrome::FindBrowserWithWindow([view window]);
74   // If history swiping isn't possible, allow rubberbanding.
75   if (!browser)
76     return true;
77   if (!chrome::CanGoForward(browser))
78     return true;
79   // History swiping is possible. By default, disallow rubberbanding.  If the
80   // user has both started, and then cancelled history swiping for this
81   // gesture, allow rubberbanding.
82   return inGesture_ && historySwipeCancelled_;
83 }
84
85 // Is is theoretically possible for multiple simultaneous gestures to occur, if
86 // the user has multiple input devices. There will be 2 beginGesture events, but
87 // only 1 endGesture event. The unfinished gesture will continue to send
88 // touchesMoved events, but when the gesture finishes there is not endGesture
89 // callback. We ignore this case, because it is sufficiently unlikely to occur.
90 - (void)beginGestureWithEvent:(NSEvent*)event {
91   inGesture_ = YES;
92   ++currentGestureId_;
93   // Reset state pertaining to previous gestures.
94   historySwipeCancelled_ = NO;
95   gestureStartPointValid_ = NO;
96   gotUnhandledWheelEvent_ = NO;
97   receivedTouch_ = NO;
98   mouseScrollDelta_ = NSZeroSize;
99 }
100
101 - (void)endGestureWithEvent:(NSEvent*)event {
102   inGesture_ = NO;
103 }
104
105 // This method assumes that there is at least 1 touch in the event.
106 // The event must correpond to a valid gesture, or else
107 // [NSEvent touchesMatchingPhase:inView:] will fail.
108 - (NSPoint)averagePositionInEvent:(NSEvent*)event {
109   NSPoint position = NSMakePoint(0,0);
110   int pointCount = 0;
111   for (NSTouch* touch in
112        [event touchesMatchingPhase:NSTouchPhaseAny inView:nil]) {
113     position.x += touch.normalizedPosition.x;
114     position.y += touch.normalizedPosition.y;
115     ++pointCount;
116   }
117
118   if (pointCount > 1) {
119     position.x /= pointCount;
120     position.y /= pointCount;
121   }
122
123   return position;
124 }
125
126 - (void)updateGestureCurrentPointFromEvent:(NSEvent*)event {
127   // The points in an event are not valid unless the event is part of
128   // a gesture.
129   if (inGesture_) {
130     // Update the current point of the gesture.
131     gestureCurrentPoint_ = [self averagePositionInEvent:event];
132
133     // If the gesture doesn't have a start point, set one.
134     if (!gestureStartPointValid_) {
135       gestureStartPointValid_ = YES;
136       gestureStartPoint_ = gestureCurrentPoint_;
137     }
138   }
139 }
140
141 // Ideally, we'd set the gestureStartPoint_ here, but this method only gets
142 // called before the gesture begins, and the touches in an event are only
143 // available after the gesture begins.
144 - (void)touchesBeganWithEvent:(NSEvent*)event {
145   receivedTouch_ = YES;
146   // Do nothing.
147 }
148
149 - (void)touchesMovedWithEvent:(NSEvent*)event {
150   receivedTouch_ = YES;
151   if (![self shouldProcessEventForHistorySwiping:event])
152     return;
153
154   [self updateGestureCurrentPointFromEvent:event];
155
156   if (historyOverlay_) {
157     // Consider cancelling the history swipe gesture.
158     if ([self shouldCancelHorizontalSwipeWithCurrentPoint:gestureCurrentPoint_
159         startPoint:gestureStartPoint_]) {
160       [self cancelHistorySwipe];
161       return;
162     }
163
164     [self updateProgressBar];
165   }
166 }
167 - (void)touchesCancelledWithEvent:(NSEvent*)event {
168   receivedTouch_ = YES;
169   if (![self shouldProcessEventForHistorySwiping:event])
170     return;
171
172   if (historyOverlay_)
173     [self cancelHistorySwipe];
174 }
175 - (void)touchesEndedWithEvent:(NSEvent*)event {
176   receivedTouch_ = YES;
177   if (![self shouldProcessEventForHistorySwiping:event])
178     return;
179
180   [self updateGestureCurrentPointFromEvent:event];
181   if (historyOverlay_) {
182     BOOL finished = [self updateProgressBar];
183
184     // If the gesture was completed, perform a navigation.
185     if (finished) {
186       [self navigateBrowserInDirection:historySwipeDirection_];
187     }
188
189     // Remove the history overlay.
190     [self endHistorySwipe];
191   }
192 }
193
194 - (BOOL)shouldProcessEventForHistorySwiping:(NSEvent*)event {
195   // TODO(erikchen): what is the point of NSEventTypeSwipe and NSScrollWheel?
196   NSEventType type = [event type];
197   return type == NSEventTypeBeginGesture || type == NSEventTypeEndGesture ||
198       type == NSEventTypeGesture;
199 }
200
201 // Consider cancelling the horizontal swipe if the user was intending a
202 // vertical swipe.
203 - (BOOL)shouldCancelHorizontalSwipeWithCurrentPoint:(NSPoint)currentPoint
204     startPoint:(NSPoint)startPoint {
205   // There's been more vertical distance than horizontal distance.
206   CGFloat yDelta = fabs(currentPoint.y - startPoint.y);
207   CGFloat xDelta = fabs(currentPoint.x - startPoint.x);
208   BOOL moreVertThanHoriz = yDelta > xDelta && yDelta > 0.1;
209
210   // There's been a lot of vertical distance.
211   BOOL muchVert = yDelta > 0.32;
212
213   return moreVertThanHoriz || muchVert;
214 }
215
216 - (void)cancelHistorySwipe {
217   [self endHistorySwipe];
218   historySwipeCancelled_ = YES;
219 }
220
221 - (void)endHistorySwipe {
222   [historyOverlay_ dismiss];
223   [historyOverlay_ release];
224   historyOverlay_ = nil;
225 }
226
227 // Returns whether the progress bar has been 100% filled.
228 - (BOOL)updateProgressBar {
229   NSPoint currentPoint = gestureCurrentPoint_;
230   NSPoint startPoint = gestureStartPoint_;
231
232   float progress = 0;
233   BOOL finished = NO;
234
235   // This value was determined by experimentation.
236   CGFloat requiredSwipeDistance = 0.08;
237   progress = (currentPoint.x - startPoint.x) / requiredSwipeDistance;
238   // If the swipe is a backwards gesture, we need to invert progress.
239   if (historySwipeDirection_ == history_swiper::kBackwards)
240     progress *= -1;
241   // If the user has directions reversed, we need to invert progress.
242   if (historySwipeDirectionInverted_)
243     progress *= -1;
244
245   if (progress >= 1.0)
246     finished = YES;
247
248   // Progress can't be less than 0 or greater than 1.
249   progress = MAX(0.0, progress);
250   progress = MIN(1.0, progress);
251
252   [historyOverlay_ setProgress:progress finished:finished];
253
254   return finished;
255 }
256
257 - (BOOL)isEventDirectionInverted:(NSEvent*)event {
258   if ([event respondsToSelector:@selector(isDirectionInvertedFromDevice)])
259     return [event isDirectionInvertedFromDevice];
260   return NO;
261 }
262
263 // goForward indicates whether the user is starting a forward or backward
264 // history swipe.
265 // Creates and displays a history overlay controller.
266 // Responsible for cleaning up after itself when the gesture is finished.
267 // Responsible for starting a browser navigation if necessary.
268 // Does not prevent swipe events from propagating to other handlers.
269 - (void)beginHistorySwipeInDirection:
270         (history_swiper::NavigationDirection)direction
271                                event:(NSEvent*)event {
272   // We cannot make any assumptions about the current state of the
273   // historyOverlay_, since users may attempt to use multiple gesture input
274   // devices simultaneously, which confuses Cocoa.
275   [self endHistorySwipe];
276
277   HistoryOverlayController* historyOverlay = [[HistoryOverlayController alloc]
278       initForMode:(direction == history_swiper::kForwards)
279                      ? kHistoryOverlayModeForward
280                      : kHistoryOverlayModeBack];
281   [historyOverlay showPanelForView:[delegate_ viewThatWantsHistoryOverlay]];
282   historyOverlay_ = historyOverlay;
283
284   // Record whether the user was swiping forwards or backwards.
285   historySwipeDirection_ = direction;
286   // Record the user's settings.
287   historySwipeDirectionInverted_ = [self isEventDirectionInverted:event];
288 }
289
290 - (BOOL)systemSettingsAllowHistorySwiping:(NSEvent*)event {
291   if ([NSEvent
292           respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
293     return [NSEvent isSwipeTrackingFromScrollEventsEnabled];
294   return NO;
295 }
296
297 - (void)navigateBrowserInDirection:
298             (history_swiper::NavigationDirection)direction {
299   Browser* browser = chrome::FindBrowserWithWindow(
300       historyOverlay_.view.window);
301   if (browser) {
302     if (direction == history_swiper::kForwards)
303       chrome::GoForward(browser, CURRENT_TAB);
304     else
305       chrome::GoBack(browser, CURRENT_TAB);
306   }
307 }
308
309 - (BOOL)browserCanNavigateInDirection:
310         (history_swiper::NavigationDirection)direction
311                                 event:(NSEvent*)event {
312   Browser* browser = chrome::FindBrowserWithWindow([event window]);
313   if (!browser)
314     return NO;
315
316   if (direction == history_swiper::kForwards) {
317     return chrome::CanGoForward(browser);
318   } else {
319     return chrome::CanGoBack(browser);
320   }
321 }
322
323 // We use an entirely different set of logic for magic mouse swipe events,
324 // since we do not get NSTouch callbacks.
325 - (BOOL)maybeHandleMagicMouseHistorySwiping:(NSEvent*)theEvent {
326   // The 'trackSwipeEventWithOptions:' api doesn't handle momentum events.
327   if ([theEvent phase] == NSEventPhaseNone)
328     return NO;
329
330   mouseScrollDelta_.width += [theEvent scrollingDeltaX];
331   mouseScrollDelta_.height += [theEvent scrollingDeltaY];
332
333   BOOL isHorizontalGesture =
334     std::abs(mouseScrollDelta_.width) > std::abs(mouseScrollDelta_.height);
335   if (!isHorizontalGesture)
336     return NO;
337
338   BOOL isRightScroll = [theEvent scrollingDeltaX] < 0;
339   history_swiper::NavigationDirection direction =
340       isRightScroll ? history_swiper::kForwards : history_swiper::kBackwards;
341   BOOL browserCanMove =
342       [self browserCanNavigateInDirection:direction event:theEvent];
343   if (!browserCanMove)
344     return NO;
345
346   if (isRightScroll) {
347     if (hasHorizontalScrollbar_ && !isPinnedRight_)
348       return NO;
349   } else {
350     if (hasHorizontalScrollbar_ && !isPinnedLeft_)
351       return NO;
352   }
353
354   [self initiateMagicMouseHistorySwipe:isRightScroll event:theEvent];
355   return YES;
356 }
357
358 - (void)initiateMagicMouseHistorySwipe:(BOOL)isRightScroll
359                                  event:(NSEvent*)event {
360   // Released by the tracking handler once the gesture is complete.
361   __block HistoryOverlayController* historyOverlay =
362       [[HistoryOverlayController alloc]
363           initForMode:isRightScroll ? kHistoryOverlayModeForward
364                                     : kHistoryOverlayModeBack];
365
366   // The way this API works: gestureAmount is between -1 and 1 (float).  If
367   // the user does the gesture for more than about 30% (i.e. < -0.3 or >
368   // 0.3) and then lets go, it is accepted, we get a NSEventPhaseEnded,
369   // and after that the block is called with amounts animating towards 1
370   // (or -1, depending on the direction).  If the user lets go below that
371   // threshold, we get NSEventPhaseCancelled, and the amount animates
372   // toward 0.  When gestureAmount has reaches its final value, i.e. the
373   // track animation is done, the handler is called with |isComplete| set
374   // to |YES|.
375   // When starting a backwards navigation gesture (swipe from left to right,
376   // gestureAmount will go from 0 to 1), if the user swipes from left to
377   // right and then quickly back to the left, this call can send
378   // NSEventPhaseEnded and then animate to gestureAmount of -1. For a
379   // picture viewer, that makes sense, but for back/forward navigation users
380   // find it confusing. There are two ways to prevent this:
381   // 1. Set Options to NSEventSwipeTrackingLockDirection. This way,
382   //    gestureAmount will always stay > 0.
383   // 2. Pass min:0 max:1 (instead of min:-1 max:1). This way, gestureAmount
384   //    will become less than 0, but on the quick swipe back to the left,
385   //    NSEventPhaseCancelled is sent instead.
386   // The current UI looks nicer with (1) so that swiping the opposite
387   // direction after the initial swipe doesn't cause the shield to move
388   // in the wrong direction.
389   forceMagicMouse = YES;
390   [event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection
391       dampenAmountThresholdMin:-1
392       max:1
393       usingHandler:^(CGFloat gestureAmount,
394                      NSEventPhase phase,
395                      BOOL isComplete,
396                      BOOL* stop) {
397           if (phase == NSEventPhaseBegan) {
398             [historyOverlay
399                 showPanelForView:[delegate_ viewThatWantsHistoryOverlay]];
400             return;
401           }
402
403           BOOL ended = phase == NSEventPhaseEnded;
404
405           // Dismiss the panel before navigation for immediate visual feedback.
406           CGFloat progress = std::abs(gestureAmount) / 0.3;
407           BOOL finished = progress >= 1.0;
408           progress = MAX(0.0, progress);
409           progress = MIN(1.0, progress);
410           [historyOverlay setProgress:progress finished:finished];
411
412           // |gestureAmount| obeys -[NSEvent isDirectionInvertedFromDevice]
413           // automatically.
414           Browser* browser =
415               chrome::FindBrowserWithWindow(historyOverlay.view.window);
416           if (ended && browser) {
417             if (isRightScroll)
418               chrome::GoForward(browser, CURRENT_TAB);
419             else
420               chrome::GoBack(browser, CURRENT_TAB);
421           }
422
423           if (ended || isComplete) {
424             [historyOverlay dismiss];
425             [historyOverlay release];
426             historyOverlay = nil;
427           }
428       }];
429 }
430
431 // Checks if |theEvent| should trigger history swiping, and if so, does
432 // history swiping. Returns YES if the event was consumed or NO if it should
433 // be passed on to the renderer.
434 //
435 // There are 4 types of scroll wheel events:
436 // 1. Magic mouse swipe events.
437 //      These are identical to magic trackpad events, except that there are no
438 //      NSTouch callbacks.  The only way to accurately track these events is
439 //      with the  `trackSwipeEventWithOptions:` API. scrollingDelta{X,Y} is not
440 //      accurate over long distances (it is computed using the speed of the
441 //      swipe, rather than just the distance moved by the fingers).
442 // 2. Magic trackpad swipe events.
443 //      These are the most common history swipe events. Our logic is
444 //      predominantly designed to handle this use case.
445 // 3. Traditional mouse scrollwheel events.
446 //      These should not initiate scrolling. They can be distinguished by the
447 //      fact that `phase` and `momentumPhase` both return NSEventPhaseNone.
448 // 4. Momentum swipe events.
449 //      After a user finishes a swipe, the system continues to generate
450 //      artificial callbacks. `phase` returns NSEventPhaseNone, but
451 //      `momentumPhase` does not. Unfortunately, the callbacks don't work
452 //      properly (OSX 10.9). Sometimes, the system start sending momentum swipe
453 //      events instead of trackpad swipe events while the user is still
454 //      2-finger swiping.
455 - (BOOL)maybeHandleHistorySwiping:(NSEvent*)theEvent {
456   if (![theEvent respondsToSelector:@selector(phase)])
457     return NO;
458
459   // Check for regular mouse wheel scroll events.
460   if ([theEvent phase] == NSEventPhaseNone &&
461       [theEvent momentumPhase] == NSEventPhaseNone) {
462     return NO;
463   }
464
465   // We've already processed this gesture.
466   if (lastProcessedGestureId_ == currentGestureId_) {
467     // A new event may come in before it's recognized as a gesture.
468     // We have not yet reset the state from the last gesture.
469     // Let it pass through.
470     if ([theEvent phase] == NSEventPhaseBegan ||
471         [theEvent phase] == NSEventPhaseMayBegin) {
472       return NO;
473     }
474
475     // The user cancelled the history swiper. Ignore all events.
476     if (historySwipeCancelled_)
477       return NO;
478
479     // The user completed the history swiper. Swallow all events.
480     return YES;
481   }
482
483   BOOL systemSettingsValid = [self systemSettingsAllowHistorySwiping:theEvent];
484   if (!systemSettingsValid)
485     return NO;
486
487   if (![delegate_ shouldAllowHistorySwiping])
488     return NO;
489
490   // Don't even consider enabling history swiping until blink has decided it is
491   // not going to handle the event.
492   if (!gotUnhandledWheelEvent_)
493     return NO;
494
495   // If the window has a horizontal scroll bar, sometimes Cocoa gets confused
496   // and sends us momentum scroll wheel events instead of gesture scroll events
497   // (even though the user is still actively swiping).
498   if ([theEvent phase] != NSEventPhaseChanged &&
499       [theEvent momentumPhase] != NSEventPhaseChanged) {
500     return NO;
501   }
502
503   if (!inGesture_)
504     return NO;
505
506   if (!receivedTouch_ || forceMagicMouse) {
507     return [self maybeHandleMagicMouseHistorySwiping:theEvent];
508   }
509
510   CGFloat yDelta = gestureCurrentPoint_.y - gestureStartPoint_.y;
511   CGFloat xDelta = gestureCurrentPoint_.x - gestureStartPoint_.x;
512
513   // Require the user's gesture to have moved more than a minimal amount.
514   if (fabs(xDelta) < 0.01)
515     return NO;
516
517   // Require the user's gesture to be slightly more horizontal than vertical.
518   BOOL isHorizontalGesture = fabs(xDelta) > 1.3 * fabs(yDelta);
519
520   if (!isHorizontalGesture)
521     return NO;
522
523   BOOL isRightScroll = xDelta > 0;
524   BOOL inverted = [self isEventDirectionInverted:theEvent];
525   if (inverted)
526     isRightScroll = !isRightScroll;
527
528   if (isRightScroll) {
529     if (hasHorizontalScrollbar_ && !isPinnedRight_)
530       return NO;
531   } else {
532     if (hasHorizontalScrollbar_ && !isPinnedLeft_)
533       return NO;
534   }
535
536   history_swiper::NavigationDirection direction =
537       isRightScroll ? history_swiper::kForwards : history_swiper::kBackwards;
538   BOOL browserCanMove =
539       [self browserCanNavigateInDirection:direction event:theEvent];
540   if (!browserCanMove)
541     return NO;
542
543   lastProcessedGestureId_ = currentGestureId_;
544   [self beginHistorySwipeInDirection:direction event:theEvent];
545   return YES;
546 }
547 @end
548
549 @implementation HistorySwiper (PrivateExposedForTesting)
550 + (void)resetMagicMouseState {
551   forceMagicMouse = NO;
552 }
553 @end