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