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