immersive_mode_controller_ash.cc revision 1e9bf3e0803691d0a228da41fc608347b6db4340
1// Copyright 2012 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#include "chrome/browser/ui/views/frame/immersive_mode_controller_ash.h"
6
7#include <set>
8#include <vector>
9
10#include "ash/shell.h"
11#include "ash/wm/window_state.h"
12#include "chrome/browser/chrome_notification_types.h"
13#include "chrome/browser/ui/fullscreen/fullscreen_controller.h"
14#include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h"
15#include "chrome/browser/ui/views/frame/top_container_view.h"
16#include "content/public/browser/notification_service.h"
17#include "content/public/browser/web_contents.h"
18#include "content/public/browser/web_contents_view.h"
19#include "ui/aura/client/activation_client.h"
20#include "ui/aura/client/aura_constants.h"
21#include "ui/aura/client/capture_client.h"
22#include "ui/aura/client/cursor_client.h"
23#include "ui/aura/client/screen_position_client.h"
24#include "ui/aura/env.h"
25#include "ui/aura/root_window.h"
26#include "ui/aura/window.h"
27#include "ui/gfx/animation/slide_animation.h"
28#include "ui/views/bubble/bubble_delegate.h"
29#include "ui/views/view.h"
30#include "ui/views/widget/widget.h"
31#include "ui/views/window/non_client_view.h"
32
33using views::View;
34
35namespace {
36
37// The slide open/closed animation looks better if it starts and ends just a
38// few pixels before the view goes completely off the screen, which reduces
39// the visual "pop" as the 2-pixel tall immersive-style tabs become visible.
40const int kAnimationOffsetY = 3;
41
42// Duration for the reveal show/hide slide animation. The slower duration is
43// used for the initial slide out to give the user more change to see what
44// happened.
45const int kRevealSlowAnimationDurationMs = 400;
46const int kRevealFastAnimationDurationMs = 200;
47
48// The delay in milliseconds between the mouse stopping at the top edge of the
49// screen and the top-of-window views revealing.
50const int kMouseRevealDelayMs = 200;
51
52// The maximum amount of pixels that the cursor can move for the cursor to be
53// considered "stopped". This allows the user to reveal the top-of-window views
54// without holding the cursor completely still.
55const int kMouseRevealXThresholdPixels = 3;
56
57// How many pixels a gesture can start away from |top_container_| when in
58// closed state and still be considered near it. This is needed to overcome
59// issues with poor location values near the edge of the display.
60const int kNearTopContainerDistance = 8;
61
62// Used to multiply x value of an update in check to determine if gesture is
63// vertical. This is used to make sure that gesture is close to vertical instead
64// of just more vertical then horizontal.
65const int kSwipeVerticalThresholdMultiplier = 3;
66
67// The height in pixels of the region above the top edge of the display which
68// hosts the immersive fullscreen window in which mouse events are ignored
69// (cannot reveal or unreveal the top-of-window views).
70// See ShouldIgnoreMouseEventAtLocation() for more details.
71const int kHeightOfDeadRegionAboveTopContainer = 10;
72
73// The height in pixels of the region below the top edge of the display in which
74// the mouse can trigger revealing the top-of-window views. The height must be
75// greater than 1px because the top pixel is used to trigger moving the cursor
76// between displays if the user has a vertical display layout (primary display
77// above/below secondary display).
78const int kMouseRevealBoundsHeight = 3;
79
80// Returns the BubbleDelegateView corresponding to |maybe_bubble| if
81// |maybe_bubble| is a bubble.
82views::BubbleDelegateView* AsBubbleDelegate(aura::Window* maybe_bubble) {
83  if (!maybe_bubble)
84    return NULL;
85  views::Widget* widget = views::Widget::GetWidgetForNativeView(maybe_bubble);
86  if (!widget)
87    return NULL;
88  return widget->widget_delegate()->AsBubbleDelegate();
89}
90
91// Returns true if |maybe_transient| is a transient child of |toplevel|.
92bool IsWindowTransientChildOf(aura::Window* maybe_transient,
93                              aura::Window* toplevel) {
94  if (!maybe_transient || !toplevel)
95    return false;
96
97  for (aura::Window* window = maybe_transient; window;
98       window = window->transient_parent()) {
99    if (window == toplevel)
100      return true;
101  }
102  return false;
103}
104
105// Returns the location of |event| in screen coordinates.
106gfx::Point GetEventLocationInScreen(const ui::LocatedEvent& event) {
107  gfx::Point location_in_screen = event.location();
108  aura::Window* target = static_cast<aura::Window*>(event.target());
109  aura::client::ScreenPositionClient* screen_position_client =
110      aura::client::GetScreenPositionClient(target->GetRootWindow());
111  screen_position_client->ConvertPointToScreen(target, &location_in_screen);
112  return location_in_screen;
113}
114
115////////////////////////////////////////////////////////////////////////////////
116
117class RevealedLockAsh : public ImmersiveRevealedLock {
118 public:
119  RevealedLockAsh(const base::WeakPtr<ImmersiveModeControllerAsh>& controller,
120                  ImmersiveModeController::AnimateReveal animate_reveal)
121      : controller_(controller) {
122    DCHECK(controller_);
123    controller_->LockRevealedState(animate_reveal);
124  }
125
126  virtual ~RevealedLockAsh() {
127    if (controller_)
128      controller_->UnlockRevealedState();
129  }
130
131 private:
132  base::WeakPtr<ImmersiveModeControllerAsh> controller_;
133
134  DISALLOW_COPY_AND_ASSIGN(RevealedLockAsh);
135};
136
137}  // namespace
138
139////////////////////////////////////////////////////////////////////////////////
140
141// Class which keeps the top-of-window views revealed as long as one of the
142// bubbles it is observing is visible. The logic to keep the top-of-window
143// views revealed based on the visibility of bubbles anchored to
144// children of |ImmersiveModeController::top_container_| is separate from
145// the logic related to |ImmersiveModeControllerAsh::focus_revealed_lock_|
146// so that bubbles which are not activatable and bubbles which do not close
147// upon deactivation also keep the top-of-window views revealed for the
148// duration of their visibility.
149class ImmersiveModeControllerAsh::BubbleManager : public aura::WindowObserver {
150 public:
151  explicit BubbleManager(ImmersiveModeControllerAsh* controller);
152  virtual ~BubbleManager();
153
154  // Start / stop observing changes to |bubble|'s visibility.
155  void StartObserving(aura::Window* bubble);
156  void StopObserving(aura::Window* bubble);
157
158 private:
159  // Updates |revealed_lock_| based on whether any of |bubbles_| is visible.
160  void UpdateRevealedLock();
161
162  // aura::WindowObserver overrides:
163  virtual void OnWindowVisibilityChanged(aura::Window* window,
164                                         bool visible) OVERRIDE;
165  virtual void OnWindowDestroying(aura::Window* window) OVERRIDE;
166
167  ImmersiveModeControllerAsh* controller_;
168
169  std::set<aura::Window*> bubbles_;
170
171  // Lock which keeps the top-of-window views revealed based on whether any of
172  // |bubbles_| is visible.
173  scoped_ptr<ImmersiveRevealedLock> revealed_lock_;
174
175  DISALLOW_COPY_AND_ASSIGN(BubbleManager);
176};
177
178ImmersiveModeControllerAsh::BubbleManager::BubbleManager(
179    ImmersiveModeControllerAsh* controller)
180    : controller_(controller) {
181}
182
183ImmersiveModeControllerAsh::BubbleManager::~BubbleManager() {
184  for (std::set<aura::Window*>::const_iterator it = bubbles_.begin();
185       it != bubbles_.end(); ++it) {
186    (*it)->RemoveObserver(this);
187  }
188}
189
190void ImmersiveModeControllerAsh::BubbleManager::StartObserving(
191    aura::Window* bubble) {
192  if (bubbles_.insert(bubble).second) {
193    bubble->AddObserver(this);
194    UpdateRevealedLock();
195  }
196}
197
198void ImmersiveModeControllerAsh::BubbleManager::StopObserving(
199    aura::Window* bubble) {
200  if (bubbles_.erase(bubble)) {
201    bubble->RemoveObserver(this);
202    UpdateRevealedLock();
203  }
204}
205
206void ImmersiveModeControllerAsh::BubbleManager::UpdateRevealedLock() {
207  bool has_visible_bubble = false;
208  for (std::set<aura::Window*>::const_iterator it = bubbles_.begin();
209       it != bubbles_.end(); ++it) {
210    if ((*it)->IsVisible()) {
211      has_visible_bubble = true;
212      break;
213    }
214  }
215
216  bool was_revealed = controller_->IsRevealed();
217  if (has_visible_bubble) {
218    if (!revealed_lock_.get()) {
219      // Reveal the top-of-window views without animating because it looks
220      // weird for the top-of-window views to animate and the bubble not to
221      // animate along with the top-of-window views.
222      revealed_lock_.reset(controller_->GetRevealedLock(
223          ImmersiveModeController::ANIMATE_REVEAL_NO));
224    }
225  } else {
226    revealed_lock_.reset();
227  }
228
229  if (!was_revealed && revealed_lock_.get()) {
230    // Currently, there is no nice way for bubbles to reposition themselves
231    // whenever the anchor view moves. Tell the bubbles to reposition themselves
232    // explicitly instead. The hidden bubbles are also repositioned because
233    // BubbleDelegateView does not reposition its widget as a result of a
234    // visibility change.
235    for (std::set<aura::Window*>::const_iterator it = bubbles_.begin();
236         it != bubbles_.end(); ++it) {
237      AsBubbleDelegate(*it)->OnAnchorViewBoundsChanged();
238    }
239  }
240}
241
242void ImmersiveModeControllerAsh::BubbleManager::OnWindowVisibilityChanged(
243    aura::Window*,
244    bool visible) {
245  UpdateRevealedLock();
246}
247
248void ImmersiveModeControllerAsh::BubbleManager::OnWindowDestroying(
249    aura::Window* window) {
250  StopObserving(window);
251}
252
253////////////////////////////////////////////////////////////////////////////////
254
255ImmersiveModeControllerAsh::ImmersiveModeControllerAsh()
256    : delegate_(NULL),
257      widget_(NULL),
258      top_container_(NULL),
259      observers_enabled_(false),
260      enabled_(false),
261      reveal_state_(CLOSED),
262      revealed_lock_count_(0),
263      tab_indicator_visibility_(TAB_INDICATORS_HIDE),
264      mouse_x_when_hit_top_in_screen_(-1),
265      gesture_begun_(false),
266      native_window_(NULL),
267      animation_(new gfx::SlideAnimation(this)),
268      animations_disabled_for_test_(false),
269      weak_ptr_factory_(this) {
270}
271
272ImmersiveModeControllerAsh::~ImmersiveModeControllerAsh() {
273  // The browser view is being destroyed so there's no need to update its
274  // layout or layers, even if the top views are revealed. But the window
275  // observers still need to be removed.
276  EnableWindowObservers(false);
277}
278
279void ImmersiveModeControllerAsh::LockRevealedState(
280      AnimateReveal animate_reveal) {
281  ++revealed_lock_count_;
282  Animate animate = (animate_reveal == ANIMATE_REVEAL_YES) ?
283      ANIMATE_FAST : ANIMATE_NO;
284  MaybeStartReveal(animate);
285}
286
287void ImmersiveModeControllerAsh::UnlockRevealedState() {
288  --revealed_lock_count_;
289  DCHECK_GE(revealed_lock_count_, 0);
290  if (revealed_lock_count_ == 0) {
291    // Always animate ending the reveal fast.
292    MaybeEndReveal(ANIMATE_FAST);
293  }
294}
295
296void ImmersiveModeControllerAsh::Init(
297    Delegate* delegate,
298    views::Widget* widget,
299    views::View* top_container) {
300  delegate_ = delegate;
301  widget_ = widget;
302  // Browser view is detached from its widget during destruction. Cache the
303  // window pointer so |this| can stop observing during destruction.
304  native_window_ = widget_->GetNativeWindow();
305  top_container_ = top_container;
306}
307
308void ImmersiveModeControllerAsh::SetEnabled(bool enabled) {
309  DCHECK(native_window_) << "Must initialize before enabling";
310  if (enabled_ == enabled)
311    return;
312  enabled_ = enabled;
313
314  EnableWindowObservers(enabled_);
315
316  UpdateUseMinimalChrome(LAYOUT_NO);
317
318  if (enabled_) {
319    // Animate enabling immersive mode by sliding out the top-of-window views.
320    // No animation occurs if a lock is holding the top-of-window views open.
321
322    // Do a reveal to set the initial state for the animation. (And any
323    // required state in case the animation cannot run because of a lock holding
324    // the top-of-window views open.) This call has the side effect of relaying
325    // out |browser_view_|'s root view.
326    MaybeStartReveal(ANIMATE_NO);
327
328    // Reset the located event and the focus revealed locks so that they do not
329    // affect whether the top-of-window views are hidden.
330    located_event_revealed_lock_.reset();
331    focus_revealed_lock_.reset();
332
333    // Try doing the animation.
334    MaybeEndReveal(ANIMATE_SLOW);
335
336    if (reveal_state_ == REVEALED) {
337      // Reveal was unsuccessful. Reacquire the revealed locks if appropriate.
338      UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO);
339      UpdateFocusRevealedLock();
340    }
341  } else {
342    // Stop cursor-at-top tracking.
343    top_edge_hover_timer_.Stop();
344    // Snap immediately to the closed state.
345    reveal_state_ = CLOSED;
346    top_container_->SetPaintToLayer(false);
347    delegate_->SetImmersiveStyle(false);
348    SetRenderWindowTopInsetsForTouch(0);
349
350    // Layout the root view so that incognito avatar icon, if any, gets laid
351    // out.
352    LayoutBrowserRootView();
353  }
354}
355
356bool ImmersiveModeControllerAsh::IsEnabled() const {
357  return enabled_;
358}
359
360bool ImmersiveModeControllerAsh::ShouldHideTabIndicators() const {
361  return tab_indicator_visibility_ != TAB_INDICATORS_SHOW;
362}
363
364bool ImmersiveModeControllerAsh::ShouldHideTopViews() const {
365  return enabled_ && reveal_state_ == CLOSED;
366}
367
368bool ImmersiveModeControllerAsh::IsRevealed() const {
369  return enabled_ && reveal_state_ != CLOSED;
370}
371
372int ImmersiveModeControllerAsh::GetTopContainerVerticalOffset(
373    const gfx::Size& top_container_size) const {
374  if (!enabled_ || reveal_state_ == REVEALED || reveal_state_ == CLOSED)
375    return 0;
376
377  return animation_->CurrentValueBetween(
378      -top_container_size.height() + kAnimationOffsetY, 0);
379}
380
381ImmersiveRevealedLock* ImmersiveModeControllerAsh::GetRevealedLock(
382    AnimateReveal animate_reveal) {
383  return new RevealedLockAsh(weak_ptr_factory_.GetWeakPtr(), animate_reveal);
384}
385
386void ImmersiveModeControllerAsh::OnFindBarVisibleBoundsChanged(
387    const gfx::Rect& new_visible_bounds_in_screen) {
388  find_bar_visible_bounds_in_screen_ = new_visible_bounds_in_screen;
389}
390
391void ImmersiveModeControllerAsh::SetupForTest() {
392  DCHECK(!enabled_);
393  animations_disabled_for_test_ = true;
394
395  // Move the mouse off of the top-of-window views so that it does not keep
396  // the top-of-window views revealed.
397  gfx::Point cursor_pos(0, top_container_->bounds().bottom() + 100);
398  views::View::ConvertPointToScreen(top_container_, &cursor_pos);
399  aura::Env::GetInstance()->set_last_mouse_location(cursor_pos);
400  UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO);
401}
402
403////////////////////////////////////////////////////////////////////////////////
404// Observers:
405
406void ImmersiveModeControllerAsh::Observe(
407    int type,
408    const content::NotificationSource& source,
409    const content::NotificationDetails& details) {
410  DCHECK_EQ(chrome::NOTIFICATION_FULLSCREEN_CHANGED, type);
411  if (enabled_)
412    UpdateUseMinimalChrome(LAYOUT_YES);
413}
414
415void ImmersiveModeControllerAsh::OnMouseEvent(ui::MouseEvent* event) {
416  if (!enabled_)
417    return;
418
419  if (event->type() != ui::ET_MOUSE_MOVED &&
420      event->type() != ui::ET_MOUSE_PRESSED &&
421      event->type() != ui::ET_MOUSE_RELEASED &&
422      event->type() != ui::ET_MOUSE_CAPTURE_CHANGED) {
423    return;
424  }
425
426  // Mouse hover should not initiate revealing the top-of-window views while
427  // |native_window_| is inactive.
428  if (!views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive())
429    return;
430
431  // Mouse hover should not initiate revealing the top-of-window views while
432  // a window has mouse capture.
433  if (aura::client::GetCaptureWindow(native_window_))
434    return;
435
436  if (IsRevealed())
437    UpdateLocatedEventRevealedLock(event, ALLOW_REVEAL_WHILE_CLOSING_NO);
438
439  // Trigger a reveal if the cursor pauses at the top of the screen for a
440  // while.
441  if (event->type() != ui::ET_MOUSE_CAPTURE_CHANGED)
442    UpdateTopEdgeHoverTimer(event);
443}
444
445void ImmersiveModeControllerAsh::OnTouchEvent(ui::TouchEvent* event) {
446  if (!enabled_ || event->type() != ui::ET_TOUCH_PRESSED)
447    return;
448
449  UpdateLocatedEventRevealedLock(event, ALLOW_REVEAL_WHILE_CLOSING_NO);
450}
451
452void ImmersiveModeControllerAsh::OnGestureEvent(ui::GestureEvent* event) {
453  if (!enabled_)
454    return;
455
456  // Touch gestures should not initiate revealing the top-of-window views while
457  // |native_window_| is inactive.
458  if (!views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive())
459    return;
460
461  switch (event->type()) {
462    case ui::ET_GESTURE_SCROLL_BEGIN:
463      if (ShouldHandleGestureEvent(GetEventLocationInScreen(*event))) {
464        gesture_begun_ = true;
465        event->SetHandled();
466      }
467      break;
468    case ui::ET_GESTURE_SCROLL_UPDATE:
469      if (gesture_begun_) {
470        if (UpdateRevealedLocksForSwipe(GetSwipeType(event)))
471          event->SetHandled();
472        gesture_begun_ = false;
473      }
474      break;
475    case ui::ET_GESTURE_SCROLL_END:
476    case ui::ET_SCROLL_FLING_START:
477      gesture_begun_ = false;
478      break;
479    default:
480      break;
481  }
482}
483
484void ImmersiveModeControllerAsh::OnWillChangeFocus(views::View* focused_before,
485                                                   views::View* focused_now) {
486}
487
488void ImmersiveModeControllerAsh::OnDidChangeFocus(views::View* focused_before,
489                                                  views::View* focused_now) {
490  UpdateFocusRevealedLock();
491}
492
493void ImmersiveModeControllerAsh::OnWidgetDestroying(views::Widget* widget) {
494  EnableWindowObservers(false);
495  native_window_ = NULL;
496
497  // Set |enabled_| to false such that any calls to MaybeStartReveal() and
498  // MaybeEndReveal() have no effect.
499  enabled_ = false;
500}
501
502void ImmersiveModeControllerAsh::OnWidgetActivationChanged(
503    views::Widget* widget,
504    bool active) {
505  // Mouse hover should not initiate revealing the top-of-window views while
506  // |native_window_| is inactive.
507  top_edge_hover_timer_.Stop();
508
509  UpdateFocusRevealedLock();
510
511  // Allow the top-of-window views to stay revealed if all of the revealed locks
512  // were released in the process of activating |widget| but the mouse is still
513  // hovered above the top-of-window views. For instance, if the bubble which
514  // has been keeping the top-of-window views revealed is hidden but the mouse
515  // is hovered above the top-of-window views, the top-of-window views should
516  // stay revealed. We cannot call UpdateLocatedEventRevealedLock() from
517  // BubbleManager::UpdateRevealedLock() because |widget| is not yet active
518  // at that time.
519  UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_YES);
520}
521
522////////////////////////////////////////////////////////////////////////////////
523// Animation delegate:
524
525void ImmersiveModeControllerAsh::AnimationEnded(
526    const gfx::Animation* animation) {
527  if (reveal_state_ == SLIDING_OPEN) {
528    // AnimationProgressed() is called immediately before AnimationEnded()
529    // and does a layout.
530    OnSlideOpenAnimationCompleted(LAYOUT_NO);
531  } else if (reveal_state_ == SLIDING_CLOSED) {
532    OnSlideClosedAnimationCompleted();
533  }
534}
535
536void ImmersiveModeControllerAsh::AnimationProgressed(
537    const gfx::Animation* animation) {
538  // Relayout. This will also move any views whose position depends on the
539  // top container position such as the find bar.
540  // We do not call LayoutBrowserRootView() here because we are not toggling
541  // the tab strip's immersive style so relaying out the non client view is not
542  // necessary.
543  top_container_->parent()->Layout();
544}
545
546////////////////////////////////////////////////////////////////////////////////
547// aura::WindowObserver overrides:
548
549void ImmersiveModeControllerAsh::OnWindowPropertyChanged(aura::Window* window,
550                                                         const void* key,
551                                                         intptr_t old) {
552  if (!enabled_)
553    return;
554
555  if (key == aura::client::kShowStateKey) {
556    // Disable immersive mode when the user exits fullscreen without going
557    // through FullscreenController::ToggleFullscreenMode(). This is the case
558    // if the user exits fullscreen via the restore button.
559    ui::WindowShowState show_state = static_cast<ui::WindowShowState>(
560        native_window_->GetProperty(aura::client::kShowStateKey));
561    if (show_state != ui::SHOW_STATE_FULLSCREEN &&
562        show_state != ui::SHOW_STATE_MINIMIZED) {
563      delegate_->FullscreenStateChanged();
564    }
565  }
566}
567
568void ImmersiveModeControllerAsh::OnAddTransientChild(aura::Window* window,
569                                                     aura::Window* transient) {
570  views::BubbleDelegateView* bubble_delegate = AsBubbleDelegate(transient);
571  if (bubble_delegate &&
572      bubble_delegate->GetAnchorView() &&
573      top_container_->Contains(bubble_delegate->GetAnchorView())) {
574    // Observe the aura::Window because the BubbleDelegateView may not be
575    // parented to the widget's root view yet so |bubble_delegate->GetWidget()|
576    // may still return NULL.
577    bubble_manager_->StartObserving(transient);
578  }
579}
580
581void ImmersiveModeControllerAsh::OnRemoveTransientChild(
582    aura::Window* window,
583    aura::Window* transient) {
584  bubble_manager_->StopObserving(transient);
585}
586
587////////////////////////////////////////////////////////////////////////////////
588// private:
589
590void ImmersiveModeControllerAsh::EnableWindowObservers(bool enable) {
591  if (observers_enabled_ == enable)
592    return;
593  observers_enabled_ = enable;
594
595  if (!native_window_) {
596    NOTREACHED() << "ImmersiveModeControllerAsh not initialized";
597    return;
598  }
599
600  views::Widget* widget =
601      views::Widget::GetWidgetForNativeWindow(native_window_);
602  views::FocusManager* focus_manager = widget->GetFocusManager();
603  if (enable) {
604    widget->AddObserver(this);
605    focus_manager->AddFocusChangeListener(this);
606  } else {
607    widget->RemoveObserver(this);
608    focus_manager->RemoveFocusChangeListener(this);
609  }
610
611  if (enable)
612    ash::Shell::GetInstance()->AddPreTargetHandler(this);
613  else
614    ash::Shell::GetInstance()->RemovePreTargetHandler(this);
615
616  if (enable) {
617    native_window_->AddObserver(this);
618  } else {
619    native_window_->RemoveObserver(this);
620  }
621
622  if (enable) {
623    RecreateBubbleManager();
624  } else {
625    // We have stopped observing whether transient children are added or removed
626    // to |native_window_|. The set of bubbles that BubbleManager is observing
627    // will become stale really quickly. Destroy BubbleManager and recreate it
628    // when we start observing |native_window_| again.
629    bubble_manager_.reset();
630  }
631
632  if (enable) {
633    registrar_.Add(
634        this,
635        chrome::NOTIFICATION_FULLSCREEN_CHANGED,
636        content::Source<FullscreenController>(
637            delegate_->GetFullscreenController()));
638  } else {
639    registrar_.Remove(
640        this,
641        chrome::NOTIFICATION_FULLSCREEN_CHANGED,
642        content::Source<FullscreenController>(
643            delegate_->GetFullscreenController()));
644  }
645
646  if (!enable)
647    animation_->Stop();
648}
649
650void ImmersiveModeControllerAsh::UpdateTopEdgeHoverTimer(
651    ui::MouseEvent* event) {
652  DCHECK(enabled_);
653  // Stop the timer if the top-of-window views are already revealed.
654  if (reveal_state_ == SLIDING_OPEN || reveal_state_ == REVEALED) {
655    top_edge_hover_timer_.Stop();
656    return;
657  }
658
659  gfx::Point location_in_screen = GetEventLocationInScreen(*event);
660  if (ShouldIgnoreMouseEventAtLocation(location_in_screen))
661    return;
662
663  // Stop the timer if the cursor left the top edge or is on a different
664  // display. The bounds of |top_container_|'s parent are used to infer the hit
665  // bounds because |top_container_| will be partially offscreen if it is
666  // animating closed.
667  gfx::Rect hit_bounds_in_screen =
668      top_container_->parent()->GetBoundsInScreen();
669  hit_bounds_in_screen.set_height(kMouseRevealBoundsHeight);
670  if (!hit_bounds_in_screen.Contains(location_in_screen)) {
671    top_edge_hover_timer_.Stop();
672    return;
673  }
674
675  // The cursor is now at the top of the screen. Consider the cursor "not
676  // moving" even if it moves a little bit because users don't have perfect
677  // pointing precision. (The y position is not tested because
678  // |hit_bounds_in_screen| is short.)
679  if (top_edge_hover_timer_.IsRunning() &&
680      abs(location_in_screen.x() - mouse_x_when_hit_top_in_screen_) <=
681          kMouseRevealXThresholdPixels)
682    return;
683
684  // Start the reveal if the cursor doesn't move for some amount of time.
685  mouse_x_when_hit_top_in_screen_ = location_in_screen.x();
686  top_edge_hover_timer_.Stop();
687  // Timer is stopped when |this| is destroyed, hence Unretained() is safe.
688  top_edge_hover_timer_.Start(
689      FROM_HERE,
690      base::TimeDelta::FromMilliseconds(kMouseRevealDelayMs),
691      base::Bind(&ImmersiveModeControllerAsh::AcquireLocatedEventRevealedLock,
692                 base::Unretained(this)));
693}
694
695void ImmersiveModeControllerAsh::UpdateLocatedEventRevealedLock(
696    ui::LocatedEvent* event,
697    AllowRevealWhileClosing allow_reveal_while_closing) {
698  if (!enabled_)
699    return;
700  DCHECK(!event || event->IsMouseEvent() || event->IsTouchEvent());
701
702  // Neither the mouse nor touch can initiate a reveal when the top-of-window
703  // views are sliding closed or are closed with the following exceptions:
704  // - Hovering at y = 0 which is handled in OnMouseEvent().
705  // - Doing a SWIPE_OPEN edge gesture which is handled in OnGestureEvent().
706  if (reveal_state_ == CLOSED ||
707      (reveal_state_ == SLIDING_CLOSED &&
708       allow_reveal_while_closing == ALLOW_REVEAL_WHILE_CLOSING_NO)) {
709    return;
710  }
711
712  // Neither the mouse nor touch should keep the top-of-window views revealed if
713  // |native_window_| is not active.
714  if (!views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive()) {
715    located_event_revealed_lock_.reset();
716    return;
717  }
718
719  // Ignore all events while a window has capture. This keeps the top-of-window
720  // views revealed during a drag.
721  if (aura::client::GetCaptureWindow(native_window_))
722    return;
723
724  gfx::Point location_in_screen;
725  if (event && event->type() != ui::ET_MOUSE_CAPTURE_CHANGED) {
726    location_in_screen = GetEventLocationInScreen(*event);
727  } else {
728    aura::client::CursorClient* cursor_client = aura::client::GetCursorClient(
729        native_window_->GetRootWindow());
730    if (!cursor_client->IsMouseEventsEnabled()) {
731      // If mouse events are disabled, the user's last interaction was probably
732      // via touch. Do no do further processing in this case as there is no easy
733      // way of retrieving the position of the user's last touch.
734      return;
735    }
736    location_in_screen = aura::Env::GetInstance()->last_mouse_location();
737  }
738
739  if ((!event || event->IsMouseEvent()) &&
740      ShouldIgnoreMouseEventAtLocation(location_in_screen)) {
741    return;
742  }
743
744  gfx::Rect hit_bounds_in_top_container = top_container_->GetVisibleBounds();
745  // TODO(tdanderson): Implement View::ConvertRectToScreen();
746  gfx::Point hit_bounds_in_screen_origin = hit_bounds_in_top_container.origin();
747  views::View::ConvertPointToScreen(top_container_,
748      &hit_bounds_in_screen_origin);
749  gfx::Rect hit_bounds_in_screen(hit_bounds_in_screen_origin,
750      hit_bounds_in_top_container.size());
751
752  gfx::Rect find_bar_hit_bounds_in_screen = find_bar_visible_bounds_in_screen_;
753
754  // Allow the cursor to move slightly off the top-of-window views before
755  // sliding closed. This helps when the user is attempting to click on the
756  // bookmark bar and overshoots slightly.
757  if (event && event->type() == ui::ET_MOUSE_MOVED) {
758    const int kBoundsOffsetY = 8;
759    hit_bounds_in_screen.Inset(0, 0, 0, -kBoundsOffsetY);
760    find_bar_hit_bounds_in_screen.Inset(0, 0, 0, -kBoundsOffsetY);
761  }
762
763  if (hit_bounds_in_screen.Contains(location_in_screen) ||
764      find_bar_hit_bounds_in_screen.Contains(location_in_screen)) {
765    AcquireLocatedEventRevealedLock();
766  } else {
767    located_event_revealed_lock_.reset();
768  }
769}
770
771void ImmersiveModeControllerAsh::AcquireLocatedEventRevealedLock() {
772  // CAUTION: Acquiring the lock results in a reentrant call to
773  // AcquireLocatedEventRevealedLock() when
774  // |ImmersiveModeControllerAsh::animations_disabled_for_test_| is true.
775  if (!located_event_revealed_lock_.get())
776    located_event_revealed_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES));
777}
778
779void ImmersiveModeControllerAsh::UpdateFocusRevealedLock() {
780  if (!enabled_)
781    return;
782
783  bool hold_lock = false;
784  views::Widget* widget =
785      views::Widget::GetWidgetForNativeWindow(native_window_);
786  if (widget->IsActive()) {
787    views::View* focused_view = widget->GetFocusManager()->GetFocusedView();
788    if (top_container_->Contains(focused_view))
789      hold_lock = true;
790  } else {
791    aura::Window* active_window = aura::client::GetActivationClient(
792        native_window_->GetRootWindow())->GetActiveWindow();
793    views::BubbleDelegateView* bubble_delegate =
794        AsBubbleDelegate(active_window);
795    if (bubble_delegate && bubble_delegate->anchor_widget()) {
796      // BubbleManager will already have locked the top-of-window views if the
797      // bubble is anchored to a child of |top_container_|. Don't acquire
798      // |focus_revealed_lock_| here for the sake of simplicity.
799      // Note: Instead of checking for the existence of the |anchor_view|,
800      // the existence of the |anchor_widget| is performed to avoid the case
801      // where the view is already gone (and the widget is still running).
802    } else {
803      // The currently active window is not |native_window_| and it is not a
804      // bubble with an anchor view. The top-of-window views should be revealed
805      // if:
806      // 1) The active window is a transient child of |native_window_|.
807      // 2) The top-of-window views are already revealed. This restriction
808      //    prevents a transient window opened by the web contents while the
809      //    top-of-window views are hidden from from initiating a reveal.
810      // The top-of-window views will stay revealed till |native_window_| is
811      // reactivated.
812      if (IsRevealed() &&
813          IsWindowTransientChildOf(active_window, native_window_)) {
814        hold_lock = true;
815      }
816    }
817  }
818
819  if (hold_lock) {
820    if (!focus_revealed_lock_.get())
821      focus_revealed_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES));
822  } else {
823    focus_revealed_lock_.reset();
824  }
825}
826
827bool ImmersiveModeControllerAsh::UpdateRevealedLocksForSwipe(
828    SwipeType swipe_type) {
829  if (!enabled_ || swipe_type == SWIPE_NONE)
830    return false;
831
832  // Swipes while |native_window_| is inactive should have been filtered out in
833  // OnGestureEvent().
834  DCHECK(views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive());
835
836  if (reveal_state_ == SLIDING_CLOSED || reveal_state_ == CLOSED) {
837    if (swipe_type == SWIPE_OPEN && !located_event_revealed_lock_.get()) {
838      located_event_revealed_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES));
839      return true;
840    }
841  } else {
842    if (swipe_type == SWIPE_CLOSE) {
843      // Attempt to end the reveal. If other code is holding onto a lock, the
844      // attempt will be unsuccessful.
845      located_event_revealed_lock_.reset();
846      focus_revealed_lock_.reset();
847
848      if (reveal_state_ == SLIDING_CLOSED || reveal_state_ == CLOSED)
849        return true;
850
851      // Ending the reveal was unsuccessful. Reaquire the locks if appropriate.
852      UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO);
853      UpdateFocusRevealedLock();
854    }
855  }
856  return false;
857}
858
859void ImmersiveModeControllerAsh::UpdateUseMinimalChrome(Layout layout) {
860  // May be NULL in tests.
861  FullscreenController* fullscreen_controller =
862      delegate_->GetFullscreenController();
863  bool in_tab_fullscreen = fullscreen_controller ?
864      fullscreen_controller->IsFullscreenForTabOrPending() : false;
865  bool use_minimal_chrome = !in_tab_fullscreen && enabled_;
866
867  // When using minimal chrome, the shelf is auto-hidden. The auto-hidden shelf
868  // displays a 3px 'light bar' when it is closed. Otherwise, the shelf is
869  // hidden completely and cannot be revealed.
870  ash::wm::GetWindowState(native_window_)->set_hide_shelf_when_fullscreen(
871      !use_minimal_chrome);
872
873  TabIndicatorVisibility previous_tab_indicator_visibility =
874      tab_indicator_visibility_;
875  if (tab_indicator_visibility_ != TAB_INDICATORS_FORCE_HIDE) {
876    tab_indicator_visibility_ = use_minimal_chrome ?
877        TAB_INDICATORS_SHOW : TAB_INDICATORS_HIDE;
878  }
879
880  ash::Shell::GetInstance()->UpdateShelfVisibility();
881
882  if (tab_indicator_visibility_ != previous_tab_indicator_visibility) {
883    // If the top-of-window views are revealed or animating, the change will
884    // take effect with the layout once the top-of-window views are closed.
885    if (layout == LAYOUT_YES && reveal_state_ == CLOSED)
886      LayoutBrowserRootView();
887  }
888}
889
890int ImmersiveModeControllerAsh::GetAnimationDuration(Animate animate) const {
891  switch (animate) {
892    case ANIMATE_NO:
893      return 0;
894    case ANIMATE_SLOW:
895      return kRevealSlowAnimationDurationMs;
896    case ANIMATE_FAST:
897      return kRevealFastAnimationDurationMs;
898  }
899  NOTREACHED();
900  return 0;
901}
902
903void ImmersiveModeControllerAsh::MaybeStartReveal(Animate animate) {
904  if (!enabled_)
905    return;
906
907  if (animations_disabled_for_test_)
908    animate = ANIMATE_NO;
909
910  // Callers with ANIMATE_NO expect this function to synchronously reveal the
911  // top-of-window views.
912  if (reveal_state_ == REVEALED ||
913      (reveal_state_ == SLIDING_OPEN && animate != ANIMATE_NO)) {
914    return;
915  }
916
917  RevealState previous_reveal_state = reveal_state_;
918  reveal_state_ = SLIDING_OPEN;
919  if (previous_reveal_state == CLOSED) {
920    // Turn on layer painting so that we can overlap the web contents.
921    top_container_->SetPaintToLayer(true);
922
923    // Ensure window caption buttons are updated and the view bounds are
924    // computed at normal (non-immersive-style) size. The layout call moves the
925    // top-of-window views to their initial offscreen position for the
926    // animation.
927    delegate_->SetImmersiveStyle(false);
928    SetRenderWindowTopInsetsForTouch(0);
929    LayoutBrowserRootView();
930
931    // Do not do any more processing if LayoutBrowserView() changed
932    // |reveal_state_|.
933    if (reveal_state_ != SLIDING_OPEN) {
934      if (reveal_state_ == REVEALED)
935        FOR_EACH_OBSERVER(Observer, observers_, OnImmersiveRevealStarted());
936      return;
937    }
938  }
939  // Slide in the reveal view.
940  if (animate == ANIMATE_NO) {
941    animation_->Reset(1);
942    OnSlideOpenAnimationCompleted(LAYOUT_YES);
943  } else {
944    animation_->SetSlideDuration(GetAnimationDuration(animate));
945    animation_->Show();
946  }
947
948  if (previous_reveal_state == CLOSED)
949     FOR_EACH_OBSERVER(Observer, observers_, OnImmersiveRevealStarted());
950}
951
952void ImmersiveModeControllerAsh::LayoutBrowserRootView() {
953  // Update the window caption buttons.
954  widget_->non_client_view()->frame_view()->ResetWindowControls();
955  // Layout all views, including BrowserView.
956  widget_->GetRootView()->Layout();
957}
958
959void ImmersiveModeControllerAsh::OnSlideOpenAnimationCompleted(Layout layout) {
960  DCHECK_EQ(SLIDING_OPEN, reveal_state_);
961  reveal_state_ = REVEALED;
962
963  if (layout == LAYOUT_YES)
964    top_container_->parent()->Layout();
965
966  // The user may not have moved the mouse since the reveal was initiated.
967  // Update the revealed lock to reflect the mouse's current state.
968  UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO);
969}
970
971void ImmersiveModeControllerAsh::MaybeEndReveal(Animate animate) {
972  if (!enabled_ || revealed_lock_count_ != 0)
973    return;
974
975  if (animations_disabled_for_test_)
976    animate = ANIMATE_NO;
977
978  // Callers with ANIMATE_NO expect this function to synchronously close the
979  // top-of-window views.
980  if (reveal_state_ == CLOSED ||
981      (reveal_state_ == SLIDING_CLOSED && animate != ANIMATE_NO)) {
982    return;
983  }
984
985  reveal_state_ = SLIDING_CLOSED;
986  int duration_ms = GetAnimationDuration(animate);
987  if (duration_ms > 0) {
988    animation_->SetSlideDuration(duration_ms);
989    animation_->Hide();
990  } else {
991    animation_->Reset(0);
992    OnSlideClosedAnimationCompleted();
993  }
994}
995
996void ImmersiveModeControllerAsh::OnSlideClosedAnimationCompleted() {
997  DCHECK_EQ(SLIDING_CLOSED, reveal_state_);
998  reveal_state_ = CLOSED;
999  // Layers aren't needed after animation completes.
1000  top_container_->SetPaintToLayer(false);
1001  // Update tabstrip for closed state.
1002  delegate_->SetImmersiveStyle(true);
1003  SetRenderWindowTopInsetsForTouch(kNearTopContainerDistance);
1004  LayoutBrowserRootView();
1005}
1006
1007ImmersiveModeControllerAsh::SwipeType ImmersiveModeControllerAsh::GetSwipeType(
1008    ui::GestureEvent* event) const {
1009  if (event->type() != ui::ET_GESTURE_SCROLL_UPDATE)
1010    return SWIPE_NONE;
1011  // Make sure that it is a clear vertical gesture.
1012  if (abs(event->details().scroll_y()) <=
1013      kSwipeVerticalThresholdMultiplier * abs(event->details().scroll_x()))
1014    return SWIPE_NONE;
1015  if (event->details().scroll_y() < 0)
1016    return SWIPE_CLOSE;
1017  else if (event->details().scroll_y() > 0)
1018    return SWIPE_OPEN;
1019  return SWIPE_NONE;
1020}
1021
1022bool ImmersiveModeControllerAsh::ShouldIgnoreMouseEventAtLocation(
1023    const gfx::Point& location) const {
1024  // Ignore mouse events in the region immediately above the top edge of the
1025  // display. This is to handle the case of a user with a vertical display
1026  // layout (primary display above/below secondary display) and the immersive
1027  // fullscreen window on the bottom display. It is really hard to trigger a
1028  // reveal in this case because:
1029  // - It is hard to stop the cursor in the top |kMouseRevealBoundsHeight|
1030  //   pixels of the bottom display.
1031  // - The cursor is warped to the top display if the cursor gets to the top
1032  //   edge of the bottom display.
1033  // Mouse events are ignored in the bottom few pixels of the top display
1034  // (Mouse events in this region cannot start or end a reveal). This allows a
1035  // user to overshoot the top of the bottom display and still reveal the
1036  // top-of-window views.
1037  gfx::Rect dead_region = top_container_->parent()->GetBoundsInScreen();
1038  dead_region.set_y(dead_region.y() - kHeightOfDeadRegionAboveTopContainer);
1039  dead_region.set_height(kHeightOfDeadRegionAboveTopContainer);
1040  return dead_region.Contains(location);
1041}
1042
1043bool ImmersiveModeControllerAsh::ShouldHandleGestureEvent(
1044    const gfx::Point& location) const {
1045  gfx::Rect top_container_bounds_in_screen =
1046      top_container_->GetBoundsInScreen();
1047
1048  // All of the gestures that are of interest start in a region with left &
1049  // right edges agreeing with |top_container_|. When CLOSED it is difficult to
1050  // hit the bounds due to small size of the tab strip, so the hit target needs
1051  // to be extended on the bottom, thus the inset call.
1052  gfx::Rect near_bounds = top_container_bounds_in_screen;
1053  if (reveal_state_ == CLOSED)
1054    near_bounds.Inset(gfx::Insets(0, 0, -kNearTopContainerDistance, 0));
1055  if (near_bounds.Contains(location))
1056    return true;
1057
1058  // There may be a bezel sensor off screen logically above |top_container_|
1059  // thus the test needs to include gestures starting above, but this needs to
1060  // be distinguished from events originating on another screen from
1061  // (potentially) an extended desktop. The check for the event not contained by
1062  // the closest screen ensures that the event is from a valid bezel and can be
1063  // interpreted as such.
1064  gfx::Rect screen_bounds =
1065      ash::Shell::GetScreen()->GetDisplayNearestPoint(location).bounds();
1066  return (!screen_bounds.Contains(location) &&
1067          location.y() < top_container_bounds_in_screen.y() &&
1068          location.x() >= top_container_bounds_in_screen.x() &&
1069          location.x() < top_container_bounds_in_screen.right());
1070}
1071
1072void ImmersiveModeControllerAsh::SetRenderWindowTopInsetsForTouch(
1073    int top_inset) {
1074  content::WebContents* contents = delegate_->GetWebContents();
1075  if (contents) {
1076    aura::Window* window = contents->GetView()->GetContentNativeView();
1077    // |window| is NULL if the renderer crashed.
1078    if (window) {
1079      gfx::Insets inset(top_inset, 0, 0, 0);
1080      window->SetHitTestBoundsOverrideOuter(
1081          window->hit_test_bounds_override_outer_mouse(),
1082          inset);
1083    }
1084  }
1085}
1086
1087void ImmersiveModeControllerAsh::RecreateBubbleManager() {
1088  bubble_manager_.reset(new BubbleManager(this));
1089  const std::vector<aura::Window*> transient_children =
1090      native_window_->transient_children();
1091  for (size_t i = 0; i < transient_children.size(); ++i) {
1092    aura::Window* transient_child = transient_children[i];
1093    views::BubbleDelegateView* bubble_delegate =
1094        AsBubbleDelegate(transient_child);
1095    if (bubble_delegate &&
1096        bubble_delegate->GetAnchorView() &&
1097        top_container_->Contains(bubble_delegate->GetAnchorView())) {
1098      bubble_manager_->StartObserving(transient_child);
1099    }
1100  }
1101}
1102