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