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