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