immersive_mode_controller.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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.h"
6
7#include "chrome/browser/ui/views/frame/browser_view.h"
8#include "chrome/browser/ui/views/frame/top_container_view.h"
9#include "chrome/browser/ui/views/tabs/tab_strip.h"
10#include "chrome/common/chrome_switches.h"
11#include "ui/compositor/layer_animation_observer.h"
12#include "ui/compositor/scoped_layer_animation_settings.h"
13#include "ui/gfx/transform.h"
14#include "ui/views/view.h"
15#include "ui/views/widget/widget.h"
16#include "ui/views/window/non_client_view.h"
17
18#if defined(USE_ASH)
19#include "ash/ash_switches.h"
20#include "ash/shell.h"
21#include "ash/wm/window_properties.h"
22#include "base/command_line.h"
23#endif
24
25#if defined(USE_AURA)
26#include "ui/aura/client/aura_constants.h"
27#include "ui/aura/window.h"
28#include "ui/aura/window_observer.h"
29#endif
30
31using views::View;
32
33namespace {
34
35// Time after which the edge trigger fires and top-chrome is revealed. This is
36// after the mouse stops moving.
37const int kTopEdgeRevealDelayMs = 200;
38
39// Duration for the reveal show/hide slide animation. The slower duration is
40// used for the initial slide out to give the user more change to see what
41// happened.
42const int kRevealSlowAnimationDurationMs = 400;
43const int kRevealFastAnimationDurationMs = 200;
44
45}  // namespace
46
47////////////////////////////////////////////////////////////////////////////////
48
49ImmersiveModeController::RevealedLock::RevealedLock(
50    const base::WeakPtr<ImmersiveModeController>& controller)
51    : controller_(controller) {
52  DCHECK(controller_);
53  controller_->LockRevealedState();
54}
55
56ImmersiveModeController::RevealedLock::~RevealedLock() {
57  if (controller_)
58    controller_->UnlockRevealedState();
59}
60
61////////////////////////////////////////////////////////////////////////////////
62
63#if defined(USE_AURA)
64// Observer to watch for window restore. views::Widget does not provide a hook
65// to observe for window restore, so do this at the Aura level.
66class ImmersiveModeController::WindowObserver : public aura::WindowObserver {
67 public:
68  explicit WindowObserver(ImmersiveModeController* controller)
69      : controller_(controller) {
70    controller_->native_window_->AddObserver(this);
71  }
72
73  virtual ~WindowObserver() {
74    controller_->native_window_->RemoveObserver(this);
75  }
76
77  // aura::WindowObserver overrides:
78  virtual void OnWindowPropertyChanged(aura::Window* window,
79                                       const void* key,
80                                       intptr_t old) OVERRIDE {
81    using aura::client::kShowStateKey;
82    if (key == kShowStateKey) {
83      // Disable immersive mode when leaving the fullscreen state.
84      if (window->GetProperty(kShowStateKey) != ui::SHOW_STATE_FULLSCREEN)
85        controller_->SetEnabled(false);
86      return;
87    }
88#if defined(USE_ASH)
89    using ash::internal::kImmersiveModeKey;
90    if (key == kImmersiveModeKey) {
91      // Another component has toggled immersive mode.
92      controller_->SetEnabled(window->GetProperty(kImmersiveModeKey));
93      return;
94    }
95#endif
96  }
97
98 private:
99  ImmersiveModeController* controller_;  // Not owned.
100
101  DISALLOW_COPY_AND_ASSIGN(WindowObserver);
102};
103#endif  // defined(USE_AURA)
104
105////////////////////////////////////////////////////////////////////////////////
106
107class ImmersiveModeController::AnimationObserver
108    : public ui::ImplicitAnimationObserver {
109 public:
110  enum AnimationType {
111    SLIDE_OPEN,
112    SLIDE_CLOSED,
113  };
114
115  AnimationObserver(ImmersiveModeController* controller, AnimationType type)
116      : controller_(controller), animation_type_(type) {}
117  virtual ~AnimationObserver() {}
118
119  // ui::ImplicitAnimationObserver overrides:
120  virtual void OnImplicitAnimationsCompleted() OVERRIDE {
121    if (animation_type_ == SLIDE_OPEN)
122      controller_->OnSlideOpenAnimationCompleted();
123    else if (animation_type_ == SLIDE_CLOSED)
124      controller_->OnSlideClosedAnimationCompleted();
125    else
126      NOTREACHED();
127  }
128
129 private:
130  ImmersiveModeController* controller_;
131  AnimationType animation_type_;
132
133  DISALLOW_COPY_AND_ASSIGN(AnimationObserver);
134};
135
136////////////////////////////////////////////////////////////////////////////////
137
138ImmersiveModeController::ImmersiveModeController()
139    : browser_view_(NULL),
140      enabled_(false),
141      reveal_state_(CLOSED),
142      revealed_lock_count_(0),
143      hide_tab_indicators_(false),
144      reveal_hovered_(false),
145      native_window_(NULL),
146      weak_ptr_factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)) {
147}
148
149ImmersiveModeController::~ImmersiveModeController() {
150  // The browser view is being destroyed so there's no need to update its
151  // layout or layers, even if the top views are revealed. But the window
152  // observers still need to be removed.
153  EnableWindowObservers(false);
154}
155
156void ImmersiveModeController::Init(BrowserView* browser_view) {
157  browser_view_ = browser_view;
158  // Browser view is detached from its widget during destruction. Cache the
159  // window pointer so |this| can stop observing during destruction.
160  native_window_ = browser_view_->GetNativeWindow();
161  DCHECK(native_window_);
162  EnableWindowObservers(true);
163
164  slide_open_observer_.reset(
165      new AnimationObserver(this, AnimationObserver::SLIDE_OPEN));
166  slide_closed_observer_.reset(
167      new AnimationObserver(this, AnimationObserver::SLIDE_CLOSED));
168
169#if defined(USE_ASH)
170  // Optionally allow the tab indicators to be hidden.
171  hide_tab_indicators_ = CommandLine::ForCurrentProcess()->
172      HasSwitch(ash::switches::kAshImmersiveHideTabIndicators);
173#endif
174}
175
176// static
177bool ImmersiveModeController::UseImmersiveFullscreen() {
178#if defined(OS_CHROMEOS)
179  // Kiosk mode needs the whole screen.
180  CommandLine* command_line = CommandLine::ForCurrentProcess();
181  return !command_line->HasSwitch(switches::kKioskMode) &&
182      command_line->HasSwitch(ash::switches::kAshImmersiveFullscreen);
183#endif
184  return false;
185}
186
187void ImmersiveModeController::SetEnabled(bool enabled) {
188  DCHECK(browser_view_) << "Must initialize before enabling";
189  if (enabled_ == enabled)
190    return;
191  enabled_ = enabled;
192
193  TopContainerView* top_container = browser_view_->top_container();
194  if (enabled_) {
195    // Reset the hovered state and the focused view so that they do not affect
196    // whether the top-of-window views are hidden.
197    reveal_hovered_ = false;
198    if (TopContainerChildHasFocus())
199      browser_view_->GetFocusManager()->ClearFocus();
200    // If no other code has a reveal lock, slide out the top-of-window views by
201    // triggering an end-reveal animation.
202    reveal_state_ = REVEALED;
203    top_container->SetPaintToLayer(true);
204    top_container->SetFillsBoundsOpaquely(true);
205    MaybeEndReveal(ANIMATE_SLOW);
206  } else {
207    // Stop cursor-at-top tracking.
208    top_timer_.Stop();
209    // Snap immediately to the closed state.
210    reveal_state_ = CLOSED;
211    top_container->SetFillsBoundsOpaquely(false);
212    top_container->SetPaintToLayer(false);
213    browser_view_->GetWidget()->non_client_view()->frame_view()->
214        ResetWindowControls();
215    browser_view_->tabstrip()->SetImmersiveStyle(false);
216  }
217  // Don't need explicit layout because we're inside a fullscreen transition
218  // and it blocks layout calls.
219
220#if defined(USE_ASH)
221  // This causes a no-op call to SetEnabled() since enabled_ is already set.
222  native_window_->SetProperty(ash::internal::kImmersiveModeKey, enabled_);
223  // Ash on Windows may not have a shell.
224  if (ash::Shell::HasInstance()) {
225    // Shelf auto-hides in immersive mode.
226    ash::Shell::GetInstance()->UpdateShelfVisibility();
227  }
228#endif
229}
230
231void ImmersiveModeController::MaybeStackViewAtTop() {
232#if defined(USE_AURA)
233  if (enabled_ && reveal_state_ != CLOSED) {
234    ui::Layer* reveal_layer = browser_view_->top_container()->layer();
235    if (reveal_layer)
236      reveal_layer->parent()->StackAtTop(reveal_layer);
237  }
238#endif
239}
240
241void ImmersiveModeController::MaybeStartReveal() {
242  if (enabled_ && reveal_state_ != REVEALED)
243    StartReveal(ANIMATE_FAST);
244}
245
246void ImmersiveModeController::CancelReveal() {
247  MaybeEndReveal(ANIMATE_NO);
248}
249
250ImmersiveModeController::RevealedLock*
251    ImmersiveModeController::GetRevealedLock() {
252  return new RevealedLock(weak_ptr_factory_.GetWeakPtr());
253}
254
255void ImmersiveModeController::OnRevealViewLostFocus() {
256  MaybeEndReveal(ANIMATE_FAST);
257}
258
259////////////////////////////////////////////////////////////////////////////////
260
261// ui::EventHandler overrides:
262void ImmersiveModeController::OnMouseEvent(ui::MouseEvent* event) {
263  if (!enabled_ || event->type() != ui::ET_MOUSE_MOVED)
264    return;
265  if (event->location().y() == 0) {
266    // Start a reveal if the mouse touches the top of the screen and then stops
267    // moving for a little while. This mirrors the Ash launcher behavior.
268    top_timer_.Stop();
269    // Timer is stopped when |this| is destroyed, hence Unretained() is safe.
270    top_timer_.Start(FROM_HERE,
271                     base::TimeDelta::FromMilliseconds(kTopEdgeRevealDelayMs),
272                     base::Bind(&ImmersiveModeController::StartReveal,
273                                base::Unretained(this),
274                                ANIMATE_FAST));
275  } else {
276    // Cursor left the top edge.
277    top_timer_.Stop();
278  }
279
280  if (reveal_state_ == SLIDING_OPEN || reveal_state_ == REVEALED) {
281    // Look for the mouse leaving the bottom edge of the revealed view.
282    int bottom_edge = browser_view_->top_container()->bounds().bottom();
283    if (event->location().y() > bottom_edge) {
284      reveal_hovered_ = false;
285      OnRevealViewLostMouse();
286    } else {
287      reveal_hovered_ = true;
288    }
289  }
290
291  // Pass along event for further handling.
292}
293
294////////////////////////////////////////////////////////////////////////////////
295// Testing interface:
296
297void ImmersiveModeController::SetHideTabIndicatorsForTest(bool hide) {
298  hide_tab_indicators_ = hide;
299}
300
301void ImmersiveModeController::StartRevealForTest() {
302  StartReveal(ANIMATE_NO);
303}
304
305void ImmersiveModeController::OnRevealViewLostMouseForTest() {
306  OnRevealViewLostMouse();
307}
308
309////////////////////////////////////////////////////////////////////////////////
310// private:
311
312void ImmersiveModeController::EnableWindowObservers(bool enable) {
313  if (!native_window_) {
314    NOTREACHED() << "ImmersiveModeController not initialized";
315    return;
316  }
317#if defined(USE_AURA)
318  // TODO(jamescook): Porting immersive mode to non-Aura views will require
319  // a method to monitor incoming mouse move events without handling them.
320  // Currently views uses GetEventHandlerForPoint() to route events directly
321  // to either a tab or the caption area, bypassing pre-target handlers and
322  // intermediate views.
323  if (enable)
324    native_window_->AddPreTargetHandler(this);
325  else
326    native_window_->RemovePreTargetHandler(this);
327
328  // The window observer adds and removes itself from the native window.
329  // TODO(jamescook): Porting to non-Aura will also require a method to monitor
330  // for window restore, which is not provided by views Widget.
331  window_observer_.reset(enable ? new WindowObserver(this) : NULL);
332#endif  // defined(USE_AURA)
333}
334
335void ImmersiveModeController::LockRevealedState() {
336  ++revealed_lock_count_;
337  if (revealed_lock_count_ == 1)
338    MaybeStartReveal();
339}
340
341void ImmersiveModeController::UnlockRevealedState() {
342  --revealed_lock_count_;
343  DCHECK_GE(revealed_lock_count_, 0);
344  if (revealed_lock_count_ == 0)
345    MaybeEndReveal(ANIMATE_FAST);
346}
347
348bool ImmersiveModeController::TopContainerChildHasFocus() const {
349  views::View* focused = browser_view_->GetFocusManager()->GetFocusedView();
350  return browser_view_->top_container()->Contains(focused);
351}
352
353void ImmersiveModeController::StartReveal(Animate animate) {
354  DCHECK_NE(ANIMATE_SLOW, animate);
355  if (reveal_state_ == CLOSED) {
356    reveal_state_ = SLIDING_OPEN;
357    // Turn on layer painting so we can smoothly animate.
358    TopContainerView* top_container = browser_view_->top_container();
359    top_container->SetPaintToLayer(true);
360    top_container->SetFillsBoundsOpaquely(true);
361
362    // Ensure window caption buttons are updated and the view bounds are
363    // computed at normal (non-immersive-style) size.
364    LayoutBrowserView(false);
365
366    // Slide in the reveal view.
367    if (animate != ANIMATE_NO)
368      AnimateSlideOpen();  // Show is always fast.
369  } else if (reveal_state_ == SLIDING_CLOSED) {
370    reveal_state_ = SLIDING_OPEN;
371    // Reverse the animation.
372    AnimateSlideOpen();
373  }
374}
375
376void ImmersiveModeController::LayoutBrowserView(bool immersive_style) {
377  // Update the window caption buttons.
378  browser_view_->GetWidget()->non_client_view()->frame_view()->
379      ResetWindowControls();
380  browser_view_->tabstrip()->SetImmersiveStyle(immersive_style);
381  browser_view_->Layout();
382}
383
384void ImmersiveModeController::AnimateSlideOpen() {
385  ui::Layer* layer = browser_view_->top_container()->layer();
386  // Stop any slide closed animation in progress.
387  layer->GetAnimator()->AbortAllAnimations();
388
389  gfx::Transform transform;
390  transform.Translate(0, -layer->bounds().height());
391  layer->SetTransform(transform);
392
393  ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
394  settings.AddObserver(slide_open_observer_.get());
395  settings.SetTweenType(ui::Tween::EASE_OUT);
396  settings.SetTransitionDuration(
397      base::TimeDelta::FromMilliseconds(kRevealFastAnimationDurationMs));
398  layer->SetTransform(gfx::Transform());
399}
400
401void ImmersiveModeController::OnSlideOpenAnimationCompleted() {
402  if (reveal_state_ == SLIDING_OPEN)
403    reveal_state_ = REVEALED;
404}
405
406void ImmersiveModeController::OnRevealViewLostMouse() {
407  MaybeEndReveal(ANIMATE_FAST);
408}
409
410void ImmersiveModeController::MaybeEndReveal(Animate animate) {
411  if (enabled_ &&
412      reveal_state_ != CLOSED &&
413      revealed_lock_count_ == 0 &&
414      !reveal_hovered_ &&
415      !TopContainerChildHasFocus()) {
416    EndReveal(animate);
417  }
418}
419
420void ImmersiveModeController::EndReveal(Animate animate) {
421  if (reveal_state_ == SLIDING_OPEN || reveal_state_ == REVEALED) {
422    reveal_state_ = SLIDING_CLOSED;
423    if (animate == ANIMATE_FAST)
424      AnimateSlideClosed(kRevealFastAnimationDurationMs);
425    else if (animate == ANIMATE_SLOW)
426      AnimateSlideClosed(kRevealSlowAnimationDurationMs);
427    else
428      OnSlideClosedAnimationCompleted();
429  }
430}
431
432void ImmersiveModeController::AnimateSlideClosed(int duration_ms) {
433  // Stop any slide open animation in progress, but don't skip to the end. This
434  // avoids a visual "pop" when starting a hide in the middle of a show.
435  ui::Layer* layer = browser_view_->top_container()->layer();
436  layer->GetAnimator()->AbortAllAnimations();
437
438  ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
439  settings.SetTweenType(ui::Tween::EASE_OUT);
440  settings.SetTransitionDuration(
441      base::TimeDelta::FromMilliseconds(duration_ms));
442  settings.AddObserver(slide_closed_observer_.get());
443  gfx::Transform transform;
444  transform.Translate(0, -layer->bounds().height());
445  layer->SetTransform(transform);
446}
447
448void ImmersiveModeController::OnSlideClosedAnimationCompleted() {
449  if (reveal_state_ == SLIDING_CLOSED) {
450    reveal_state_ = CLOSED;
451    TopContainerView* top_container = browser_view_->top_container();
452    // Layer isn't needed after animation completes.
453    top_container->SetFillsBoundsOpaquely(false);
454    top_container->SetPaintToLayer(false);
455    // Update tabstrip for closed state.
456    LayoutBrowserView(true);
457  }
458}
459