blob: a5f1199d0c047242bb97eaa2f08eb1f3af328f58 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/shared/immersive_fullscreen_controller.h"
#include <set>
#include "ash/shared/immersive_context.h"
#include "ash/shared/immersive_focus_watcher.h"
#include "ash/shared/immersive_fullscreen_controller_delegate.h"
#include "ash/shared/immersive_gesture_handler.h"
#include "ash/shared/immersive_handler_factory.h"
#include "base/metrics/histogram_macros.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/bubble/bubble_dialog_delegate.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// Duration for the reveal show/hide slide animation. The slower duration is
// used for the initial slide out to give the user more change to see what
// happened.
const int kRevealSlowAnimationDurationMs = 400;
const int kRevealFastAnimationDurationMs = 200;
// The delay in milliseconds between the mouse stopping at the top edge of the
// screen and the top-of-window views revealing.
const int kMouseRevealDelayMs = 200;
// The maximum amount of pixels that the cursor can move for the cursor to be
// considered "stopped". This allows the user to reveal the top-of-window views
// without holding the cursor completely still.
const int kMouseRevealXThresholdPixels = 3;
// Used to multiply x value of an update in check to determine if gesture is
// vertical. This is used to make sure that gesture is close to vertical instead
// of just more vertical then horizontal.
const int kSwipeVerticalThresholdMultiplier = 3;
// The height in pixels of the region above the top edge of the display which
// hosts the immersive fullscreen window in which mouse events are ignored
// (cannot reveal or unreveal the top-of-window views).
// See ShouldIgnoreMouseEventAtLocation() for more details.
const int kHeightOfDeadRegionAboveTopContainer = 10;
} // namespace
// static
const int ImmersiveFullscreenController::kImmersiveFullscreenTopEdgeInset = 8;
// static
const int ImmersiveFullscreenController::kMouseRevealBoundsHeight = 3;
////////////////////////////////////////////////////////////////////////////////
ImmersiveFullscreenController::ImmersiveFullscreenController()
: delegate_(NULL),
top_container_(NULL),
widget_(NULL),
observers_enabled_(false),
enabled_(false),
reveal_state_(CLOSED),
revealed_lock_count_(0),
mouse_x_when_hit_top_in_screen_(-1),
gesture_begun_(false),
animation_(new gfx::SlideAnimation(this)),
animations_disabled_for_test_(false),
weak_ptr_factory_(this) {}
ImmersiveFullscreenController::~ImmersiveFullscreenController() {
EnableWindowObservers(false);
}
void ImmersiveFullscreenController::Init(
ImmersiveFullscreenControllerDelegate* delegate,
views::Widget* widget,
views::View* top_container) {
delegate_ = delegate;
top_container_ = top_container;
widget_ = widget;
ImmersiveContext::Get()->InstallResizeHandleWindowTargeter(this);
}
void ImmersiveFullscreenController::SetEnabled(WindowType window_type,
bool enabled) {
if (enabled_ == enabled)
return;
enabled_ = enabled;
EnableWindowObservers(enabled_);
ImmersiveContext::Get()->OnEnteringOrExitingImmersive(this, enabled);
if (enabled_) {
// Animate enabling immersive mode by sliding out the top-of-window views.
// No animation occurs if a lock is holding the top-of-window views open.
// Do a reveal to set the initial state for the animation. (And any
// required state in case the animation cannot run because of a lock holding
// the top-of-window views open.)
MaybeStartReveal(ANIMATE_NO);
// Reset the located event so that it does not affect whether the
// top-of-window views are hidden.
located_event_revealed_lock_.reset();
// Try doing the animation.
MaybeEndReveal(ANIMATE_SLOW);
if (reveal_state_ == REVEALED) {
// Reveal was unsuccessful. Reacquire the revealed locks if appropriate.
UpdateLocatedEventRevealedLock();
if (immersive_focus_watcher_)
immersive_focus_watcher_->UpdateFocusRevealedLock();
}
} else {
// Stop cursor-at-top tracking.
top_edge_hover_timer_.Stop();
reveal_state_ = CLOSED;
delegate_->OnImmersiveFullscreenExited();
}
if (enabled_) {
UMA_HISTOGRAM_ENUMERATION("Ash.ImmersiveFullscreen.WindowType", window_type,
WINDOW_TYPE_COUNT);
}
}
bool ImmersiveFullscreenController::IsEnabled() const {
return enabled_;
}
bool ImmersiveFullscreenController::IsRevealed() const {
return enabled_ && reveal_state_ != CLOSED;
}
ImmersiveRevealedLock* ImmersiveFullscreenController::GetRevealedLock(
AnimateReveal animate_reveal) {
return new ImmersiveRevealedLock(weak_ptr_factory_.GetWeakPtr(),
animate_reveal);
}
////////////////////////////////////////////////////////////////////////////////
void ImmersiveFullscreenController::OnMouseEvent(
const ui::MouseEvent& event,
const gfx::Point& location_in_screen,
views::Widget* target) {
if (!enabled_)
return;
if (event.type() != ui::ET_MOUSE_MOVED &&
event.type() != ui::ET_MOUSE_PRESSED &&
event.type() != ui::ET_MOUSE_RELEASED &&
event.type() != ui::ET_MOUSE_CAPTURE_CHANGED) {
return;
}
// Mouse hover can initiate revealing the top-of-window views while |widget_|
// is inactive.
if (reveal_state_ == SLIDING_OPEN || reveal_state_ == REVEALED) {
top_edge_hover_timer_.Stop();
UpdateLocatedEventRevealedLock(&event, location_in_screen);
} else if (event.type() != ui::ET_MOUSE_CAPTURE_CHANGED) {
// Trigger a reveal if the cursor pauses at the top of the screen for a
// while.
UpdateTopEdgeHoverTimer(event, location_in_screen, target);
}
}
void ImmersiveFullscreenController::OnTouchEvent(
const ui::TouchEvent& event,
const gfx::Point& location_in_screen) {
if (!enabled_ || event.type() != ui::ET_TOUCH_PRESSED)
return;
// Touch should not initiate revealing the top-of-window views while |widget_|
// is inactive.
if (!widget_->IsActive())
return;
UpdateLocatedEventRevealedLock(&event, location_in_screen);
}
void ImmersiveFullscreenController::OnGestureEvent(
ui::GestureEvent* event,
const gfx::Point& location_in_screen) {
if (!enabled_)
return;
// Touch gestures should not initiate revealing the top-of-window views while
// |widget_| is inactive.
if (!widget_->IsActive())
return;
switch (event->type()) {
case ui::ET_GESTURE_SCROLL_BEGIN:
if (ShouldHandleGestureEvent(location_in_screen)) {
gesture_begun_ = true;
// Do not consume the event. Otherwise, we end up consuming all
// ui::ET_GESTURE_SCROLL_BEGIN events in the top-of-window views
// when the top-of-window views are revealed.
}
break;
case ui::ET_GESTURE_SCROLL_UPDATE:
if (gesture_begun_) {
if (UpdateRevealedLocksForSwipe(GetSwipeType(*event)))
event->SetHandled();
gesture_begun_ = false;
}
break;
case ui::ET_GESTURE_SCROLL_END:
case ui::ET_SCROLL_FLING_START:
gesture_begun_ = false;
break;
default:
break;
}
}
void ImmersiveFullscreenController::OnPointerEventObserved(
const ui::PointerEvent& event,
const gfx::Point& location_in_screen,
views::Widget* target) {
if (event.IsMousePointerEvent()) {
if (event.type() == ui::ET_POINTER_WHEEL_CHANGED) {
const ui::MouseWheelEvent mouse_wheel_event(event);
OnMouseEvent(mouse_wheel_event, location_in_screen, target);
} else {
const ui::MouseEvent mouse_event(event);
OnMouseEvent(mouse_event, location_in_screen, target);
}
} else {
DCHECK(event.IsTouchPointerEvent());
const ui::TouchEvent touch_event(event);
OnTouchEvent(touch_event, location_in_screen);
}
}
////////////////////////////////////////////////////////////////////////////////
// views::WidgetObserver overrides:
void ImmersiveFullscreenController::OnWidgetDestroying(views::Widget* widget) {
EnableWindowObservers(false);
widget_window_ = nullptr;
// Set |enabled_| to false such that any calls to MaybeStartReveal() and
// MaybeEndReveal() have no effect.
enabled_ = false;
}
////////////////////////////////////////////////////////////////////////////////
// gfx::AnimationDelegate overrides:
void ImmersiveFullscreenController::AnimationEnded(
const gfx::Animation* animation) {
if (reveal_state_ == SLIDING_OPEN) {
OnSlideOpenAnimationCompleted();
} else if (reveal_state_ == SLIDING_CLOSED) {
OnSlideClosedAnimationCompleted();
}
}
void ImmersiveFullscreenController::AnimationProgressed(
const gfx::Animation* animation) {
delegate_->SetVisibleFraction(animation->GetCurrentValue());
}
////////////////////////////////////////////////////////////////////////////////
// ImmersiveRevealedLock::Delegate overrides:
void ImmersiveFullscreenController::LockRevealedState(
AnimateReveal animate_reveal) {
++revealed_lock_count_;
Animate animate =
(animate_reveal == ANIMATE_REVEAL_YES) ? ANIMATE_FAST : ANIMATE_NO;
MaybeStartReveal(animate);
}
void ImmersiveFullscreenController::UnlockRevealedState() {
--revealed_lock_count_;
DCHECK_GE(revealed_lock_count_, 0);
if (revealed_lock_count_ == 0) {
// Always animate ending the reveal fast.
MaybeEndReveal(ANIMATE_FAST);
}
}
////////////////////////////////////////////////////////////////////////////////
// private:
void ImmersiveFullscreenController::EnableWindowObservers(bool enable) {
if (observers_enabled_ == enable)
return;
observers_enabled_ = enable;
if (enable) {
immersive_focus_watcher_ =
ImmersiveHandlerFactory::Get()->CreateFocusWatcher(this);
immersive_gesture_handler_ =
ImmersiveHandlerFactory::Get()->CreateGestureHandler(this);
widget_->AddObserver(this);
ImmersiveContext::Get()->AddPointerWatcher(
this, views::PointerWatcherEventTypes::MOVES);
} else {
ImmersiveContext::Get()->RemovePointerWatcher(this);
widget_->RemoveObserver(this);
immersive_gesture_handler_.reset();
immersive_focus_watcher_.reset();
animation_->Stop();
}
}
void ImmersiveFullscreenController::UpdateTopEdgeHoverTimer(
const ui::MouseEvent& event,
const gfx::Point& location_in_screen,
views::Widget* target) {
DCHECK(enabled_);
DCHECK(reveal_state_ == SLIDING_CLOSED || reveal_state_ == CLOSED);
// Check whether |widget_| is the event target instead of checking for
// activation. This allows the timer to be started when |widget_| is inactive
// but prevents starting the timer if the mouse is over a portion of the top
// edge obscured by an unrelated widget.
if (!top_edge_hover_timer_.IsRunning() && target != widget_) {
return;
}
// Mouse hover should not initiate revealing the top-of-window views while a
// window has mouse capture.
if (ImmersiveContext::Get()->DoesAnyWindowHaveCapture())
return;
if (ShouldIgnoreMouseEventAtLocation(location_in_screen))
return;
// Stop the timer if the cursor left the top edge or is on a different
// display.
gfx::Rect hit_bounds_in_screen = GetDisplayBoundsInScreen();
hit_bounds_in_screen.set_height(kMouseRevealBoundsHeight);
if (!hit_bounds_in_screen.Contains(location_in_screen)) {
top_edge_hover_timer_.Stop();
return;
}
// The cursor is now at the top of the screen. Consider the cursor "not
// moving" even if it moves a little bit because users don't have perfect
// pointing precision. (The y position is not tested because
// |hit_bounds_in_screen| is short.)
if (top_edge_hover_timer_.IsRunning() &&
abs(location_in_screen.x() - mouse_x_when_hit_top_in_screen_) <=
kMouseRevealXThresholdPixels)
return;
// Start the reveal if the cursor doesn't move for some amount of time.
mouse_x_when_hit_top_in_screen_ = location_in_screen.x();
top_edge_hover_timer_.Stop();
// Timer is stopped when |this| is destroyed, hence Unretained() is safe.
top_edge_hover_timer_.Start(
FROM_HERE, base::TimeDelta::FromMilliseconds(kMouseRevealDelayMs),
base::Bind(
&ImmersiveFullscreenController::AcquireLocatedEventRevealedLock,
base::Unretained(this)));
}
void ImmersiveFullscreenController::UpdateLocatedEventRevealedLock(
const ui::LocatedEvent* event,
const gfx::Point& location_in_screen) {
if (!enabled_)
return;
DCHECK(!event || event->IsMouseEvent() || event->IsTouchEvent());
// Neither the mouse nor touch can initiate a reveal when the top-of-window
// views are sliding closed or are closed with the following exceptions:
// - Hovering at y = 0 which is handled in OnMouseEvent().
// - Doing a SWIPE_OPEN edge gesture which is handled in OnGestureEvent().
if (reveal_state_ == CLOSED || reveal_state_ == SLIDING_CLOSED)
return;
// For the sake of simplicity, ignore |widget_|'s activation in computing
// whether the top-of-window views should stay revealed. Ideally, the
// top-of-window views would stay revealed only when the mouse cursor is
// hovered above a non-obscured portion of the top-of-window views. The
// top-of-window views may be partially obscured when |widget_| is inactive.
// Ignore all events while a window has capture. This keeps the top-of-window
// views revealed during a drag.
if (ImmersiveContext::Get()->DoesAnyWindowHaveCapture())
return;
if ((!event || event->IsMouseEvent()) &&
ShouldIgnoreMouseEventAtLocation(location_in_screen)) {
return;
}
// The visible bounds of |top_container_| should be contained in
// |hit_bounds_in_screen|.
std::vector<gfx::Rect> hit_bounds_in_screen =
delegate_->GetVisibleBoundsInScreen();
bool keep_revealed = false;
for (size_t i = 0; i < hit_bounds_in_screen.size(); ++i) {
// Allow the cursor to move slightly off the top-of-window views before
// sliding closed. In the case of ImmersiveModeControllerAsh, this helps
// when the user is attempting to click on the bookmark bar and overshoots
// slightly.
if (event && event->type() == ui::ET_MOUSE_MOVED) {
const int kBoundsOffsetY = 8;
hit_bounds_in_screen[i].Inset(0, 0, 0, -kBoundsOffsetY);
}
if (hit_bounds_in_screen[i].Contains(location_in_screen)) {
keep_revealed = true;
break;
}
}
if (keep_revealed)
AcquireLocatedEventRevealedLock();
else
located_event_revealed_lock_.reset();
}
void ImmersiveFullscreenController::UpdateLocatedEventRevealedLock() {
if (!ImmersiveContext::Get()->IsMouseEventsEnabled()) {
// If mouse events are disabled, the user's last interaction was probably
// via touch. Do no do further processing in this case as there is no easy
// way of retrieving the position of the user's last touch.
return;
}
UpdateLocatedEventRevealedLock(
nullptr, display::Screen::GetScreen()->GetCursorScreenPoint());
}
void ImmersiveFullscreenController::AcquireLocatedEventRevealedLock() {
// CAUTION: Acquiring the lock results in a reentrant call to
// AcquireLocatedEventRevealedLock() when
// |ImmersiveFullscreenController::animations_disabled_for_test_| is true.
if (!located_event_revealed_lock_.get())
located_event_revealed_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES));
}
bool ImmersiveFullscreenController::UpdateRevealedLocksForSwipe(
SwipeType swipe_type) {
if (!enabled_ || swipe_type == SWIPE_NONE)
return false;
// Swipes while |widget_| is inactive should have been filtered out in
// OnGestureEvent().
DCHECK(widget_->IsActive());
if (reveal_state_ == SLIDING_CLOSED || reveal_state_ == CLOSED) {
if (swipe_type == SWIPE_OPEN && !located_event_revealed_lock_.get()) {
located_event_revealed_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES));
return true;
}
} else {
if (swipe_type == SWIPE_CLOSE) {
// Attempt to end the reveal. If other code is holding onto a lock, the
// attempt will be unsuccessful.
located_event_revealed_lock_.reset();
if (immersive_focus_watcher_)
immersive_focus_watcher_->ReleaseLock();
if (reveal_state_ == SLIDING_CLOSED || reveal_state_ == CLOSED) {
widget_->GetFocusManager()->ClearFocus();
return true;
}
// Ending the reveal was unsuccessful. Reaquire the locks if appropriate.
UpdateLocatedEventRevealedLock();
if (immersive_focus_watcher_)
immersive_focus_watcher_->UpdateFocusRevealedLock();
}
}
return false;
}
int ImmersiveFullscreenController::GetAnimationDuration(Animate animate) const {
switch (animate) {
case ANIMATE_NO:
return 0;
case ANIMATE_SLOW:
return kRevealSlowAnimationDurationMs;
case ANIMATE_FAST:
return kRevealFastAnimationDurationMs;
}
NOTREACHED();
return 0;
}
void ImmersiveFullscreenController::MaybeStartReveal(Animate animate) {
if (!enabled_)
return;
if (animations_disabled_for_test_)
animate = ANIMATE_NO;
// Callers with ANIMATE_NO expect this function to synchronously reveal the
// top-of-window views.
if (reveal_state_ == REVEALED ||
(reveal_state_ == SLIDING_OPEN && animate != ANIMATE_NO)) {
return;
}
RevealState previous_reveal_state = reveal_state_;
reveal_state_ = SLIDING_OPEN;
if (previous_reveal_state == CLOSED) {
delegate_->OnImmersiveRevealStarted();
// Do not do any more processing if OnImmersiveRevealStarted() changed
// |reveal_state_|.
if (reveal_state_ != SLIDING_OPEN)
return;
}
// Slide in the reveal view.
if (animate == ANIMATE_NO) {
animation_->Reset(1);
OnSlideOpenAnimationCompleted();
} else {
animation_->SetSlideDuration(GetAnimationDuration(animate));
animation_->Show();
}
}
void ImmersiveFullscreenController::OnSlideOpenAnimationCompleted() {
DCHECK_EQ(SLIDING_OPEN, reveal_state_);
reveal_state_ = REVEALED;
delegate_->SetVisibleFraction(1);
// The user may not have moved the mouse since the reveal was initiated.
// Update the revealed lock to reflect the mouse's current state.
UpdateLocatedEventRevealedLock();
}
void ImmersiveFullscreenController::MaybeEndReveal(Animate animate) {
if (!enabled_ || revealed_lock_count_ != 0)
return;
if (animations_disabled_for_test_)
animate = ANIMATE_NO;
// Callers with ANIMATE_NO expect this function to synchronously close the
// top-of-window views.
if (reveal_state_ == CLOSED ||
(reveal_state_ == SLIDING_CLOSED && animate != ANIMATE_NO)) {
return;
}
reveal_state_ = SLIDING_CLOSED;
int duration_ms = GetAnimationDuration(animate);
if (duration_ms > 0) {
animation_->SetSlideDuration(duration_ms);
animation_->Hide();
} else {
animation_->Reset(0);
OnSlideClosedAnimationCompleted();
}
}
void ImmersiveFullscreenController::OnSlideClosedAnimationCompleted() {
DCHECK_EQ(SLIDING_CLOSED, reveal_state_);
reveal_state_ = CLOSED;
delegate_->OnImmersiveRevealEnded();
}
ImmersiveFullscreenController::SwipeType
ImmersiveFullscreenController::GetSwipeType(
const ui::GestureEvent& event) const {
if (event.type() != ui::ET_GESTURE_SCROLL_UPDATE)
return SWIPE_NONE;
// Make sure that it is a clear vertical gesture.
if (std::abs(event.details().scroll_y()) <=
kSwipeVerticalThresholdMultiplier * std::abs(event.details().scroll_x()))
return SWIPE_NONE;
if (event.details().scroll_y() < 0)
return SWIPE_CLOSE;
if (event.details().scroll_y() > 0)
return SWIPE_OPEN;
return SWIPE_NONE;
}
bool ImmersiveFullscreenController::ShouldIgnoreMouseEventAtLocation(
const gfx::Point& location) const {
// Ignore mouse events in the region immediately above the top edge of the
// display. This is to handle the case of a user with a vertical display
// layout (primary display above/below secondary display) and the immersive
// fullscreen window on the bottom display. It is really hard to trigger a
// reveal in this case because:
// - It is hard to stop the cursor in the top |kMouseRevealBoundsHeight|
// pixels of the bottom display.
// - The cursor is warped to the top display if the cursor gets to the top
// edge of the bottom display.
// Mouse events are ignored in the bottom few pixels of the top display
// (Mouse events in this region cannot start or end a reveal). This allows a
// user to overshoot the top of the bottom display and still reveal the
// top-of-window views.
gfx::Rect dead_region = GetDisplayBoundsInScreen();
dead_region.set_y(dead_region.y() - kHeightOfDeadRegionAboveTopContainer);
dead_region.set_height(kHeightOfDeadRegionAboveTopContainer);
return dead_region.Contains(location);
}
bool ImmersiveFullscreenController::ShouldHandleGestureEvent(
const gfx::Point& location) const {
DCHECK(widget_->IsActive());
if (reveal_state_ == REVEALED) {
std::vector<gfx::Rect> hit_bounds_in_screen(
delegate_->GetVisibleBoundsInScreen());
for (size_t i = 0; i < hit_bounds_in_screen.size(); ++i) {
if (hit_bounds_in_screen[i].Contains(location))
return true;
}
return false;
}
// When the top-of-window views are not fully revealed, handle gestures which
// start in the top few pixels of the screen.
gfx::Rect hit_bounds_in_screen(GetDisplayBoundsInScreen());
hit_bounds_in_screen.set_height(kImmersiveFullscreenTopEdgeInset);
if (hit_bounds_in_screen.Contains(location))
return true;
// There may be a bezel sensor off screen logically above
// |hit_bounds_in_screen|. The check for the event not contained by the
// closest screen ensures that the event is from a valid bezel (as opposed to
// another screen in an extended desktop).
gfx::Rect screen_bounds =
display::Screen::GetScreen()->GetDisplayNearestPoint(location).bounds();
return (!screen_bounds.Contains(location) &&
location.y() < hit_bounds_in_screen.y() &&
location.x() >= hit_bounds_in_screen.x() &&
location.x() < hit_bounds_in_screen.right());
}
gfx::Rect ImmersiveFullscreenController::GetDisplayBoundsInScreen() const {
return ImmersiveContext::Get()->GetDisplayBoundsInScreen(widget_);
}
} // namespace ash