| // Copyright 2012 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 "chrome/browser/ui/views/frame/immersive_mode_controller_ash.h" |
| |
| #include <set> |
| #include <vector> |
| |
| #include "ash/ash_switches.h" |
| #include "ash/shell.h" |
| #include "ash/wm/window_properties.h" |
| #include "base/command_line.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/ui/fullscreen/fullscreen_controller.h" |
| #include "chrome/browser/ui/immersive_fullscreen_configuration.h" |
| #include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h" |
| #include "chrome/browser/ui/views/frame/top_container_view.h" |
| #include "content/public/browser/notification_service.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_view.h" |
| #include "ui/aura/client/activation_client.h" |
| #include "ui/aura/client/aura_constants.h" |
| #include "ui/aura/client/capture_client.h" |
| #include "ui/aura/client/cursor_client.h" |
| #include "ui/aura/client/screen_position_client.h" |
| #include "ui/aura/env.h" |
| #include "ui/aura/root_window.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/animation/slide_animation.h" |
| #include "ui/views/bubble/bubble_delegate.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/window/non_client_view.h" |
| |
| using views::View; |
| |
| namespace { |
| |
| // The slide open/closed animation looks better if it starts and ends just a |
| // few pixels before the view goes completely off the screen, which reduces |
| // the visual "pop" as the 2-pixel tall immersive-style tabs become visible. |
| const int kAnimationOffsetY = 3; |
| |
| // 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; |
| |
| // How many pixels a gesture can start away from |top_container_| when in |
| // closed state and still be considered near it. This is needed to overcome |
| // issues with poor location values near the edge of the display. |
| const int kNearTopContainerDistance = 8; |
| |
| // 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; |
| |
| // If |hovered| is true, moves the mouse above |view|. Moves it outside of |
| // |view| otherwise. |
| // Should not be called outside of tests. |
| void MoveMouse(views::View* view, bool hovered) { |
| gfx::Point cursor_pos; |
| if (!hovered) { |
| int bottom_edge = view->bounds().bottom(); |
| cursor_pos = gfx::Point(0, bottom_edge + 100); |
| } |
| views::View::ConvertPointToScreen(view, &cursor_pos); |
| aura::Env::GetInstance()->set_last_mouse_location(cursor_pos); |
| } |
| |
| // Returns the BubbleDelegateView corresponding to |maybe_bubble| if |
| // |maybe_bubble| is a bubble. |
| views::BubbleDelegateView* AsBubbleDelegate(aura::Window* maybe_bubble) { |
| if (!maybe_bubble) |
| return NULL; |
| views::Widget* widget = views::Widget::GetWidgetForNativeView(maybe_bubble); |
| if (!widget) |
| return NULL; |
| return widget->widget_delegate()->AsBubbleDelegate(); |
| } |
| |
| // Returns true if |maybe_transient| is a transient child of |toplevel|. |
| bool IsWindowTransientChildOf(aura::Window* maybe_transient, |
| aura::Window* toplevel) { |
| if (!maybe_transient || !toplevel) |
| return false; |
| |
| for (aura::Window* window = maybe_transient; window; |
| window = window->transient_parent()) { |
| if (window == toplevel) |
| return true; |
| } |
| return false; |
| } |
| |
| // Returns the location of |event| in screen coordinates. |
| gfx::Point GetEventLocationInScreen(const ui::LocatedEvent& event) { |
| gfx::Point location_in_screen = event.location(); |
| aura::Window* target = static_cast<aura::Window*>(event.target()); |
| aura::client::ScreenPositionClient* screen_position_client = |
| aura::client::GetScreenPositionClient(target->GetRootWindow()); |
| screen_position_client->ConvertPointToScreen(target, &location_in_screen); |
| return location_in_screen; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| class RevealedLockAsh : public ImmersiveRevealedLock { |
| public: |
| RevealedLockAsh(const base::WeakPtr<ImmersiveModeControllerAsh>& controller, |
| ImmersiveModeController::AnimateReveal animate_reveal) |
| : controller_(controller) { |
| DCHECK(controller_); |
| controller_->LockRevealedState(animate_reveal); |
| } |
| |
| virtual ~RevealedLockAsh() { |
| if (controller_) |
| controller_->UnlockRevealedState(); |
| } |
| |
| private: |
| base::WeakPtr<ImmersiveModeControllerAsh> controller_; |
| |
| DISALLOW_COPY_AND_ASSIGN(RevealedLockAsh); |
| }; |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // Class which keeps the top-of-window views revealed as long as one of the |
| // bubbles it is observing is visible. The logic to keep the top-of-window |
| // views revealed based on the visibility of bubbles anchored to |
| // children of |ImmersiveModeController::top_container_| is separate from |
| // the logic related to |ImmersiveModeControllerAsh::focus_revealed_lock_| |
| // so that bubbles which are not activatable and bubbles which do not close |
| // upon deactivation also keep the top-of-window views revealed for the |
| // duration of their visibility. |
| class ImmersiveModeControllerAsh::BubbleManager : public aura::WindowObserver { |
| public: |
| explicit BubbleManager(ImmersiveModeControllerAsh* controller); |
| virtual ~BubbleManager(); |
| |
| // Start / stop observing changes to |bubble|'s visibility. |
| void StartObserving(aura::Window* bubble); |
| void StopObserving(aura::Window* bubble); |
| |
| private: |
| // Updates |revealed_lock_| based on whether any of |bubbles_| is visible. |
| void UpdateRevealedLock(); |
| |
| // aura::WindowObserver overrides: |
| virtual void OnWindowVisibilityChanged(aura::Window* window, |
| bool visible) OVERRIDE; |
| virtual void OnWindowDestroying(aura::Window* window) OVERRIDE; |
| |
| ImmersiveModeControllerAsh* controller_; |
| |
| std::set<aura::Window*> bubbles_; |
| |
| // Lock which keeps the top-of-window views revealed based on whether any of |
| // |bubbles_| is visible. |
| scoped_ptr<ImmersiveRevealedLock> revealed_lock_; |
| |
| DISALLOW_COPY_AND_ASSIGN(BubbleManager); |
| }; |
| |
| ImmersiveModeControllerAsh::BubbleManager::BubbleManager( |
| ImmersiveModeControllerAsh* controller) |
| : controller_(controller) { |
| } |
| |
| ImmersiveModeControllerAsh::BubbleManager::~BubbleManager() { |
| for (std::set<aura::Window*>::const_iterator it = bubbles_.begin(); |
| it != bubbles_.end(); ++it) { |
| (*it)->RemoveObserver(this); |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::BubbleManager::StartObserving( |
| aura::Window* bubble) { |
| if (bubbles_.insert(bubble).second) { |
| bubble->AddObserver(this); |
| UpdateRevealedLock(); |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::BubbleManager::StopObserving( |
| aura::Window* bubble) { |
| if (bubbles_.erase(bubble)) { |
| bubble->RemoveObserver(this); |
| UpdateRevealedLock(); |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::BubbleManager::UpdateRevealedLock() { |
| bool has_visible_bubble = false; |
| for (std::set<aura::Window*>::const_iterator it = bubbles_.begin(); |
| it != bubbles_.end(); ++it) { |
| if ((*it)->IsVisible()) { |
| has_visible_bubble = true; |
| break; |
| } |
| } |
| |
| bool was_revealed = controller_->IsRevealed(); |
| if (has_visible_bubble) { |
| if (!revealed_lock_.get()) { |
| // Reveal the top-of-window views without animating because it looks |
| // weird for the top-of-window views to animate and the bubble not to |
| // animate along with the top-of-window views. |
| revealed_lock_.reset(controller_->GetRevealedLock( |
| ImmersiveModeController::ANIMATE_REVEAL_NO)); |
| } |
| } else { |
| revealed_lock_.reset(); |
| } |
| |
| if (!was_revealed && revealed_lock_.get()) { |
| // Currently, there is no nice way for bubbles to reposition themselves |
| // whenever the anchor view moves. Tell the bubbles to reposition themselves |
| // explicitly instead. The hidden bubbles are also repositioned because |
| // BubbleDelegateView does not reposition its widget as a result of a |
| // visibility change. |
| for (std::set<aura::Window*>::const_iterator it = bubbles_.begin(); |
| it != bubbles_.end(); ++it) { |
| AsBubbleDelegate(*it)->OnAnchorViewBoundsChanged(); |
| } |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::BubbleManager::OnWindowVisibilityChanged( |
| aura::Window*, |
| bool visible) { |
| UpdateRevealedLock(); |
| } |
| |
| void ImmersiveModeControllerAsh::BubbleManager::OnWindowDestroying( |
| aura::Window* window) { |
| StopObserving(window); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| ImmersiveModeControllerAsh::ImmersiveModeControllerAsh() |
| : delegate_(NULL), |
| widget_(NULL), |
| top_container_(NULL), |
| observers_enabled_(false), |
| enabled_(false), |
| reveal_state_(CLOSED), |
| revealed_lock_count_(0), |
| tab_indicator_visibility_(TAB_INDICATORS_HIDE), |
| mouse_x_when_hit_top_(-1), |
| gesture_begun_(false), |
| native_window_(NULL), |
| animation_(new ui::SlideAnimation(this)), |
| animations_disabled_for_test_(false), |
| weak_ptr_factory_(this) { |
| } |
| |
| ImmersiveModeControllerAsh::~ImmersiveModeControllerAsh() { |
| // The browser view is being destroyed so there's no need to update its |
| // layout or layers, even if the top views are revealed. But the window |
| // observers still need to be removed. |
| EnableWindowObservers(false); |
| } |
| |
| void ImmersiveModeControllerAsh::LockRevealedState( |
| AnimateReveal animate_reveal) { |
| ++revealed_lock_count_; |
| Animate animate = (animate_reveal == ANIMATE_REVEAL_YES) ? |
| ANIMATE_FAST : ANIMATE_NO; |
| MaybeStartReveal(animate); |
| } |
| |
| void ImmersiveModeControllerAsh::UnlockRevealedState() { |
| --revealed_lock_count_; |
| DCHECK_GE(revealed_lock_count_, 0); |
| if (revealed_lock_count_ == 0) { |
| // Always animate ending the reveal fast. |
| MaybeEndReveal(ANIMATE_FAST); |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::MaybeExitImmersiveFullscreen() { |
| if (ShouldExitImmersiveFullscreen()) |
| delegate_->FullscreenStateChanged(); |
| } |
| |
| void ImmersiveModeControllerAsh::Init( |
| Delegate* delegate, |
| views::Widget* widget, |
| views::View* top_container) { |
| delegate_ = delegate; |
| widget_ = widget; |
| // Browser view is detached from its widget during destruction. Cache the |
| // window pointer so |this| can stop observing during destruction. |
| native_window_ = widget_->GetNativeWindow(); |
| top_container_ = top_container; |
| |
| // Optionally allow the tab indicators to be hidden. |
| if (CommandLine::ForCurrentProcess()-> |
| HasSwitch(ash::switches::kAshImmersiveHideTabIndicators)) { |
| tab_indicator_visibility_ = TAB_INDICATORS_FORCE_HIDE; |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::SetEnabled(bool enabled) { |
| DCHECK(native_window_) << "Must initialize before enabling"; |
| if (enabled_ == enabled) |
| return; |
| enabled_ = enabled; |
| |
| EnableWindowObservers(enabled_); |
| |
| UpdateUseMinimalChrome(LAYOUT_NO); |
| |
| 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.) This call has the side effect of relaying |
| // out |browser_view_|'s root view. |
| MaybeStartReveal(ANIMATE_NO); |
| |
| // Reset the located event and the focus revealed locks so that they do not |
| // affect whether the top-of-window views are hidden. |
| located_event_revealed_lock_.reset(); |
| focus_revealed_lock_.reset(); |
| |
| // Try doing the animation. |
| MaybeEndReveal(ANIMATE_SLOW); |
| |
| if (reveal_state_ == REVEALED) { |
| // Reveal was unsuccessful. Reacquire the revealed locks if appropriate. |
| UpdateLocatedEventRevealedLock(NULL); |
| UpdateFocusRevealedLock(); |
| } |
| } else { |
| // Stop cursor-at-top tracking. |
| top_edge_hover_timer_.Stop(); |
| // Snap immediately to the closed state. |
| reveal_state_ = CLOSED; |
| EnablePaintToLayer(false); |
| delegate_->SetImmersiveStyle(false); |
| SetRenderWindowTopInsetsForTouch(0); |
| |
| // Relayout the root view because disabling immersive fullscreen may have |
| // changed the result of NonClientFrameView::GetBoundsForClientView(). |
| LayoutBrowserRootView(); |
| } |
| } |
| |
| bool ImmersiveModeControllerAsh::IsEnabled() const { |
| return enabled_; |
| } |
| |
| bool ImmersiveModeControllerAsh::ShouldHideTabIndicators() const { |
| return tab_indicator_visibility_ != TAB_INDICATORS_SHOW; |
| } |
| |
| bool ImmersiveModeControllerAsh::ShouldHideTopViews() const { |
| return enabled_ && reveal_state_ == CLOSED; |
| } |
| |
| bool ImmersiveModeControllerAsh::IsRevealed() const { |
| return enabled_ && reveal_state_ != CLOSED; |
| } |
| |
| int ImmersiveModeControllerAsh::GetTopContainerVerticalOffset( |
| const gfx::Size& top_container_size) const { |
| if (!enabled_ || reveal_state_ == REVEALED || reveal_state_ == CLOSED) |
| return 0; |
| |
| return animation_->CurrentValueBetween( |
| -top_container_size.height() + kAnimationOffsetY, 0); |
| } |
| |
| ImmersiveRevealedLock* ImmersiveModeControllerAsh::GetRevealedLock( |
| AnimateReveal animate_reveal) { |
| return new RevealedLockAsh(weak_ptr_factory_.GetWeakPtr(), animate_reveal); |
| } |
| |
| void ImmersiveModeControllerAsh::OnFindBarVisibleBoundsChanged( |
| const gfx::Rect& new_visible_bounds_in_screen) { |
| find_bar_visible_bounds_in_screen_ = new_visible_bounds_in_screen; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Observers: |
| |
| void ImmersiveModeControllerAsh::Observe( |
| int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) { |
| DCHECK_EQ(chrome::NOTIFICATION_FULLSCREEN_CHANGED, type); |
| if (enabled_) |
| UpdateUseMinimalChrome(LAYOUT_YES); |
| } |
| |
| void ImmersiveModeControllerAsh::OnMouseEvent(ui::MouseEvent* event) { |
| 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 should not initiate revealing the top-of-window views while |
| // |native_window_| is inactive. |
| if (!views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive()) |
| return; |
| |
| // Mouse hover should not initiate revealing the top-of-window views while |
| // a window has mouse capture. |
| if (aura::client::GetCaptureWindow(native_window_)) |
| return; |
| |
| if (IsRevealed()) |
| UpdateLocatedEventRevealedLock(event); |
| |
| // Trigger a reveal if the cursor pauses at the top of the screen for a |
| // while. |
| if (event->type() != ui::ET_MOUSE_CAPTURE_CHANGED) |
| UpdateTopEdgeHoverTimer(event); |
| } |
| |
| void ImmersiveModeControllerAsh::OnTouchEvent(ui::TouchEvent* event) { |
| if (!enabled_ || event->type() != ui::ET_TOUCH_PRESSED) |
| return; |
| |
| UpdateLocatedEventRevealedLock(event); |
| } |
| |
| void ImmersiveModeControllerAsh::OnGestureEvent(ui::GestureEvent* event) { |
| if (!enabled_) |
| return; |
| |
| // Touch gestures should not initiate revealing the top-of-window views while |
| // |native_window_| is inactive. |
| if (!views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive()) |
| return; |
| |
| switch (event->type()) { |
| case ui::ET_GESTURE_SCROLL_BEGIN: |
| if (ShouldHandleGestureEvent(GetEventLocationInScreen(*event))) { |
| gesture_begun_ = true; |
| event->SetHandled(); |
| } |
| 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 ImmersiveModeControllerAsh::OnWillChangeFocus(views::View* focused_before, |
| views::View* focused_now) { |
| } |
| |
| void ImmersiveModeControllerAsh::OnDidChangeFocus(views::View* focused_before, |
| views::View* focused_now) { |
| scoped_ptr<ImmersiveRevealedLock> lock; |
| if (reveal_state_ == REVEALED || reveal_state_ == SLIDING_OPEN) { |
| // Acquire a lock so that if UpdateLocatedEventRevealedLock() or |
| // UpdateFocusRevealedLock() ends the reveal, it occurs after the |
| // function terminates. This is useful in tests. |
| lock.reset(GetRevealedLock(ANIMATE_REVEAL_YES)); |
| } |
| |
| UpdateLocatedEventRevealedLock(NULL); |
| UpdateFocusRevealedLock(); |
| } |
| |
| void ImmersiveModeControllerAsh::OnWidgetDestroying(views::Widget* widget) { |
| EnableWindowObservers(false); |
| native_window_ = NULL; |
| |
| // Set |enabled_| to false such that any calls to MaybeStartReveal() and |
| // MaybeEndReveal() have no effect. |
| enabled_ = false; |
| } |
| |
| void ImmersiveModeControllerAsh::OnWidgetActivationChanged( |
| views::Widget* widget, |
| bool active) { |
| scoped_ptr<ImmersiveRevealedLock> lock; |
| if (reveal_state_ == REVEALED || reveal_state_ == SLIDING_OPEN) { |
| // Acquire a lock so that if UpdateLocatedEventRevealedLock() or |
| // UpdateFocusRevealedLock() ends the reveal, it occurs after the |
| // function terminates. This is useful in tests. |
| lock.reset(GetRevealedLock(ANIMATE_REVEAL_YES)); |
| } |
| |
| // Mouse hover should not initiate revealing the top-of-window views while |
| // |native_window_| is inactive. |
| top_edge_hover_timer_.Stop(); |
| |
| UpdateLocatedEventRevealedLock(NULL); |
| UpdateFocusRevealedLock(); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Animation delegate: |
| |
| void ImmersiveModeControllerAsh::AnimationEnded( |
| const ui::Animation* animation) { |
| if (reveal_state_ == SLIDING_OPEN) { |
| // AnimationProgressed() is called immediately before AnimationEnded() |
| // and does a layout. |
| OnSlideOpenAnimationCompleted(LAYOUT_NO); |
| } else if (reveal_state_ == SLIDING_CLOSED) { |
| OnSlideClosedAnimationCompleted(); |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::AnimationProgressed( |
| const ui::Animation* animation) { |
| // Relayout. This will also move any views whose position depends on the |
| // top container position such as the find bar. |
| // We do not call LayoutBrowserRootView() here because we are not toggling |
| // the tab strip's immersive style so relaying out the non client view is not |
| // necessary. |
| top_container_->parent()->Layout(); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // aura::WindowObserver overrides: |
| void ImmersiveModeControllerAsh::OnWindowPropertyChanged(aura::Window* window, |
| const void* key, |
| intptr_t old) { |
| if (!enabled_) |
| return; |
| |
| if (key == aura::client::kShowStateKey) { |
| // Disable immersive mode when the user exits fullscreen without going |
| // through FullscreenController::ToggleFullscreenMode(). This is the case |
| // if the user exits fullscreen via the restore button. |
| if (ShouldExitImmersiveFullscreen()) { |
| // Other "property change" observers may want to animate between the |
| // current visuals and the new window state. Do not alter the current |
| // visuals yet and post a task to exit immersive fullscreen instead. |
| base::MessageLoopForUI::current()->PostTask( |
| FROM_HERE, |
| base::Bind(&ImmersiveModeControllerAsh::MaybeExitImmersiveFullscreen, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| ui::WindowShowState show_state = native_window_->GetProperty( |
| aura::client::kShowStateKey); |
| if (show_state == ui::SHOW_STATE_FULLSCREEN && |
| old == ui::SHOW_STATE_MINIMIZED) { |
| // Relayout in case there was a layout while the window show state was |
| // ui::SHOW_STATE_MINIMIZED. |
| LayoutBrowserRootView(); |
| } |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::OnAddTransientChild(aura::Window* window, |
| aura::Window* transient) { |
| views::BubbleDelegateView* bubble_delegate = AsBubbleDelegate(transient); |
| if (bubble_delegate && |
| bubble_delegate->anchor_view() && |
| top_container_->Contains(bubble_delegate->anchor_view())) { |
| // Observe the aura::Window because the BubbleDelegateView may not be |
| // parented to the widget's root view yet so |bubble_delegate->GetWidget()| |
| // may still return NULL. |
| bubble_manager_->StartObserving(transient); |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::OnRemoveTransientChild( |
| aura::Window* window, |
| aura::Window* transient) { |
| bubble_manager_->StopObserving(transient); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Testing interface: |
| |
| void ImmersiveModeControllerAsh::SetForceHideTabIndicatorsForTest(bool force) { |
| if (force) |
| tab_indicator_visibility_ = TAB_INDICATORS_FORCE_HIDE; |
| else if (tab_indicator_visibility_ == TAB_INDICATORS_FORCE_HIDE) |
| tab_indicator_visibility_ = TAB_INDICATORS_HIDE; |
| UpdateUseMinimalChrome(LAYOUT_YES); |
| } |
| |
| void ImmersiveModeControllerAsh::StartRevealForTest(bool hovered) { |
| MaybeStartReveal(ANIMATE_NO); |
| MoveMouse(top_container_, hovered); |
| UpdateLocatedEventRevealedLock(NULL); |
| } |
| |
| void ImmersiveModeControllerAsh::SetMouseHoveredForTest(bool hovered) { |
| MoveMouse(top_container_, hovered); |
| UpdateLocatedEventRevealedLock(NULL); |
| } |
| |
| void ImmersiveModeControllerAsh::DisableAnimationsForTest() { |
| animations_disabled_for_test_ = true; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // private: |
| |
| void ImmersiveModeControllerAsh::EnableWindowObservers(bool enable) { |
| if (observers_enabled_ == enable) |
| return; |
| observers_enabled_ = enable; |
| |
| if (!native_window_) { |
| NOTREACHED() << "ImmersiveModeControllerAsh not initialized"; |
| return; |
| } |
| |
| views::Widget* widget = |
| views::Widget::GetWidgetForNativeWindow(native_window_); |
| views::FocusManager* focus_manager = widget->GetFocusManager(); |
| if (enable) { |
| widget->AddObserver(this); |
| focus_manager->AddFocusChangeListener(this); |
| } else { |
| widget->RemoveObserver(this); |
| focus_manager->RemoveFocusChangeListener(this); |
| } |
| |
| if (enable) |
| ash::Shell::GetInstance()->AddPreTargetHandler(this); |
| else |
| ash::Shell::GetInstance()->RemovePreTargetHandler(this); |
| |
| if (enable) { |
| native_window_->AddObserver(this); |
| } else { |
| native_window_->RemoveObserver(this); |
| } |
| |
| if (enable) { |
| RecreateBubbleManager(); |
| } else { |
| // We have stopped observing whether transient children are added or removed |
| // to |native_window_|. The set of bubbles that BubbleManager is observing |
| // will become stale really quickly. Destroy BubbleManager and recreate it |
| // when we start observing |native_window_| again. |
| bubble_manager_.reset(); |
| } |
| |
| if (enable) { |
| registrar_.Add( |
| this, |
| chrome::NOTIFICATION_FULLSCREEN_CHANGED, |
| content::Source<FullscreenController>( |
| delegate_->GetFullscreenController())); |
| } else { |
| registrar_.Remove( |
| this, |
| chrome::NOTIFICATION_FULLSCREEN_CHANGED, |
| content::Source<FullscreenController>( |
| delegate_->GetFullscreenController())); |
| } |
| |
| if (!enable) |
| animation_->Stop(); |
| } |
| |
| void ImmersiveModeControllerAsh::UpdateTopEdgeHoverTimer( |
| ui::MouseEvent* event) { |
| DCHECK(enabled_); |
| // Stop the timer if the top-of-window views are already revealed. |
| if (reveal_state_ == SLIDING_OPEN || reveal_state_ == REVEALED) { |
| top_edge_hover_timer_.Stop(); |
| return; |
| } |
| |
| gfx::Point location_in_screen = GetEventLocationInScreen(*event); |
| gfx::Rect top_container_bounds_in_screen = |
| top_container_->GetBoundsInScreen(); |
| |
| // Stop the timer if the cursor left the top edge or is on a different |
| // display. |
| if (location_in_screen.y() != top_container_bounds_in_screen.y() || |
| location_in_screen.x() < top_container_bounds_in_screen.x() || |
| location_in_screen.x() >= top_container_bounds_in_screen.right()) { |
| 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 in x, because users don't have |
| // perfect pointing precision. |
| int mouse_x = location_in_screen.x() - top_container_bounds_in_screen.x(); |
| if (top_edge_hover_timer_.IsRunning() && |
| abs(mouse_x - mouse_x_when_hit_top_) <= |
| ImmersiveFullscreenConfiguration:: |
| immersive_mode_reveal_x_threshold_pixels()) |
| return; |
| |
| // Start the reveal if the cursor doesn't move for some amount of time. |
| mouse_x_when_hit_top_ = mouse_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( |
| ImmersiveFullscreenConfiguration::immersive_mode_reveal_delay_ms()), |
| base::Bind(&ImmersiveModeControllerAsh::AcquireLocatedEventRevealedLock, |
| base::Unretained(this))); |
| } |
| |
| void ImmersiveModeControllerAsh::UpdateLocatedEventRevealedLock( |
| ui::LocatedEvent* event) { |
| 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_ == SLIDING_CLOSED || reveal_state_ == CLOSED) |
| return; |
| |
| // Neither the mouse nor touch should keep the top-of-window views revealed if |
| // |native_window_| is not active. |
| if (!views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive()) { |
| located_event_revealed_lock_.reset(); |
| return; |
| } |
| |
| // Ignore all events while a window has capture. This keeps the top-of-window |
| // views revealed during a drag. |
| if (aura::client::GetCaptureWindow(native_window_)) |
| return; |
| |
| gfx::Point location_in_screen; |
| if (event && event->type() != ui::ET_MOUSE_CAPTURE_CHANGED) { |
| location_in_screen = GetEventLocationInScreen(*event); |
| } else { |
| aura::client::CursorClient* cursor_client = aura::client::GetCursorClient( |
| native_window_->GetRootWindow()); |
| if (!cursor_client->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; |
| } |
| location_in_screen = aura::Env::GetInstance()->last_mouse_location(); |
| } |
| |
| gfx::Rect hit_bounds_in_screen = top_container_->GetBoundsInScreen(); |
| gfx::Rect find_bar_hit_bounds_in_screen = find_bar_visible_bounds_in_screen_; |
| |
| // Allow the cursor to move slightly off the top-of-window views before |
| // sliding closed. 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.Inset(0, 0, 0, -kBoundsOffsetY); |
| find_bar_hit_bounds_in_screen.Inset(0, 0, 0, -kBoundsOffsetY); |
| } |
| |
| if (hit_bounds_in_screen.Contains(location_in_screen) || |
| find_bar_hit_bounds_in_screen.Contains(location_in_screen)) { |
| AcquireLocatedEventRevealedLock(); |
| } else { |
| located_event_revealed_lock_.reset(); |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::AcquireLocatedEventRevealedLock() { |
| // CAUTION: Acquiring the lock results in a reentrant call to |
| // AcquireLocatedEventRevealedLock() when |
| // |ImmersiveModeControllerAsh::animations_disabled_for_test_| is true. |
| if (!located_event_revealed_lock_.get()) |
| located_event_revealed_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES)); |
| } |
| |
| void ImmersiveModeControllerAsh::UpdateFocusRevealedLock() { |
| if (!enabled_) |
| return; |
| |
| bool hold_lock = false; |
| views::Widget* widget = |
| views::Widget::GetWidgetForNativeWindow(native_window_); |
| if (widget->IsActive()) { |
| views::View* focused_view = widget->GetFocusManager()->GetFocusedView(); |
| if (top_container_->Contains(focused_view)) |
| hold_lock = true; |
| } else { |
| aura::Window* active_window = aura::client::GetActivationClient( |
| native_window_->GetRootWindow())->GetActiveWindow(); |
| views::BubbleDelegateView* bubble_delegate = |
| AsBubbleDelegate(active_window); |
| if (bubble_delegate && bubble_delegate->anchor_view()) { |
| // BubbleManager will already have locked the top-of-window views if the |
| // bubble is anchored to a child of |top_container_|. Don't acquire |
| // |focus_revealed_lock_| here for the sake of simplicity. |
| } else { |
| // The currently active window is not |native_window_| and it is not a |
| // bubble with an anchor view. The top-of-window views should be revealed |
| // if: |
| // 1) The active window is a transient child of |native_window_|. |
| // 2) The top-of-window views are already revealed. This restriction |
| // prevents a transient window opened by the web contents while the |
| // top-of-window views are hidden from from initiating a reveal. |
| // The top-of-window views will stay revealed till |native_window_| is |
| // reactivated. |
| if (IsRevealed() && |
| IsWindowTransientChildOf(active_window, native_window_)) { |
| hold_lock = true; |
| } |
| } |
| } |
| |
| if (hold_lock) { |
| if (!focus_revealed_lock_.get()) |
| focus_revealed_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES)); |
| } else { |
| focus_revealed_lock_.reset(); |
| } |
| } |
| |
| bool ImmersiveModeControllerAsh::UpdateRevealedLocksForSwipe( |
| SwipeType swipe_type) { |
| if (!enabled_ || swipe_type == SWIPE_NONE) |
| return false; |
| |
| // Swipes while |native_window_| is inactive should have been filtered out in |
| // OnGestureEvent(). |
| DCHECK(views::Widget::GetWidgetForNativeWindow(native_window_)->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(); |
| focus_revealed_lock_.reset(); |
| |
| if (reveal_state_ == SLIDING_CLOSED || reveal_state_ == CLOSED) |
| return true; |
| |
| // Ending the reveal was unsuccessful. Reaquire the locks if appropriate. |
| UpdateLocatedEventRevealedLock(NULL); |
| UpdateFocusRevealedLock(); |
| } |
| } |
| return false; |
| } |
| |
| void ImmersiveModeControllerAsh::UpdateUseMinimalChrome(Layout layout) { |
| // May be NULL in tests. |
| FullscreenController* fullscreen_controller = |
| delegate_->GetFullscreenController(); |
| bool in_tab_fullscreen = fullscreen_controller ? |
| fullscreen_controller->IsFullscreenForTabOrPending() : false; |
| bool use_minimal_chrome = !in_tab_fullscreen && enabled_; |
| native_window_->SetProperty(ash::internal::kFullscreenUsesMinimalChromeKey, |
| use_minimal_chrome); |
| |
| TabIndicatorVisibility previous_tab_indicator_visibility = |
| tab_indicator_visibility_; |
| if (tab_indicator_visibility_ != TAB_INDICATORS_FORCE_HIDE) { |
| tab_indicator_visibility_ = use_minimal_chrome ? |
| TAB_INDICATORS_SHOW : TAB_INDICATORS_HIDE; |
| } |
| |
| // Ash on Windows may not have a shell. |
| if (ash::Shell::HasInstance()) { |
| // When using minimal chrome, the shelf is auto-hidden. The auto-hidden |
| // shelf displays a 3px 'light bar' when it is closed. |
| ash::Shell::GetInstance()->UpdateShelfVisibility(); |
| } |
| |
| if (tab_indicator_visibility_ != previous_tab_indicator_visibility) { |
| // If the top-of-window views are revealed or animating, the change will |
| // take effect with the layout once the top-of-window views are closed. |
| if (layout == LAYOUT_YES && reveal_state_ == CLOSED) |
| LayoutBrowserRootView(); |
| } |
| } |
| |
| int ImmersiveModeControllerAsh::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 ImmersiveModeControllerAsh::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) { |
| // Turn on layer painting so that we can overlap the web contents. |
| EnablePaintToLayer(true); |
| |
| // Ensure window caption buttons are updated and the view bounds are |
| // computed at normal (non-immersive-style) size. The layout call moves the |
| // top-of-window views to their initial offscreen position for the |
| // animation. |
| delegate_->SetImmersiveStyle(false); |
| SetRenderWindowTopInsetsForTouch(0); |
| LayoutBrowserRootView(); |
| |
| // Do not do any more processing if LayoutBrowserView() changed |
| // |reveal_state_|. |
| if (reveal_state_ != SLIDING_OPEN) { |
| if (reveal_state_ == REVEALED) |
| FOR_EACH_OBSERVER(Observer, observers_, OnImmersiveRevealStarted()); |
| return; |
| } |
| } |
| // Slide in the reveal view. |
| if (animate == ANIMATE_NO) { |
| animation_->Reset(1); |
| OnSlideOpenAnimationCompleted(LAYOUT_YES); |
| } else { |
| animation_->SetSlideDuration(GetAnimationDuration(animate)); |
| animation_->Show(); |
| } |
| |
| if (previous_reveal_state == CLOSED) |
| FOR_EACH_OBSERVER(Observer, observers_, OnImmersiveRevealStarted()); |
| } |
| |
| void ImmersiveModeControllerAsh::EnablePaintToLayer(bool enable) { |
| top_container_->SetPaintToLayer(enable); |
| |
| // Views software compositing is not fully layer aware. If the bookmark bar |
| // is detached while the top container layer slides on or off the screen, |
| // the pixels that become exposed are the remnants of the last software |
| // composite of the BrowserView, not the freshly-exposed bookmark bar. |
| // Force the bookmark bar to paint to a layer so the views composite |
| // properly. The infobar container does not need this treatment because |
| // BrowserView::PaintChildren() always draws it last when it is visible. |
| BookmarkBarView* bookmark_bar = delegate_->GetBookmarkBar(); |
| if (!bookmark_bar) |
| return; |
| if (enable && bookmark_bar->IsDetached()) |
| bookmark_bar->SetPaintToLayer(true); |
| else |
| bookmark_bar->SetPaintToLayer(false); |
| } |
| |
| void ImmersiveModeControllerAsh::LayoutBrowserRootView() { |
| // Update the window caption buttons. |
| widget_->non_client_view()->frame_view()->ResetWindowControls(); |
| // Layout all views, including BrowserView. |
| widget_->GetRootView()->Layout(); |
| } |
| |
| void ImmersiveModeControllerAsh::OnSlideOpenAnimationCompleted(Layout layout) { |
| DCHECK_EQ(SLIDING_OPEN, reveal_state_); |
| reveal_state_ = REVEALED; |
| |
| if (layout == LAYOUT_YES) |
| top_container_->parent()->Layout(); |
| |
| // 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(NULL); |
| } |
| |
| void ImmersiveModeControllerAsh::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) { |
| // The bookmark bar may have become detached during the reveal so ensure |
| // layers are available. This is a no-op for the top container. |
| EnablePaintToLayer(true); |
| |
| animation_->SetSlideDuration(duration_ms); |
| animation_->Hide(); |
| } else { |
| animation_->Reset(0); |
| OnSlideClosedAnimationCompleted(); |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::OnSlideClosedAnimationCompleted() { |
| DCHECK_EQ(SLIDING_CLOSED, reveal_state_); |
| reveal_state_ = CLOSED; |
| // Layers aren't needed after animation completes. |
| EnablePaintToLayer(false); |
| // Update tabstrip for closed state. |
| delegate_->SetImmersiveStyle(true); |
| SetRenderWindowTopInsetsForTouch(kNearTopContainerDistance); |
| LayoutBrowserRootView(); |
| } |
| |
| bool ImmersiveModeControllerAsh::ShouldExitImmersiveFullscreen() const { |
| if (!native_window_) |
| return false; |
| |
| ui::WindowShowState show_state = static_cast<ui::WindowShowState>( |
| native_window_->GetProperty(aura::client::kShowStateKey)); |
| return IsEnabled() && |
| show_state != ui::SHOW_STATE_FULLSCREEN && |
| show_state != ui::SHOW_STATE_MINIMIZED; |
| } |
| |
| ImmersiveModeControllerAsh::SwipeType ImmersiveModeControllerAsh::GetSwipeType( |
| 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 (abs(event->details().scroll_y()) <= |
| kSwipeVerticalThresholdMultiplier * abs(event->details().scroll_x())) |
| return SWIPE_NONE; |
| if (event->details().scroll_y() < 0) |
| return SWIPE_CLOSE; |
| else if (event->details().scroll_y() > 0) |
| return SWIPE_OPEN; |
| return SWIPE_NONE; |
| } |
| |
| bool ImmersiveModeControllerAsh::ShouldHandleGestureEvent( |
| const gfx::Point& location) const { |
| // All of the gestures that are of interest start in a region with left & |
| // right edges agreeing with |top_container_|. When CLOSED it is difficult to |
| // hit the bounds due to small size of the tab strip, so the hit target needs |
| // to be extended on the bottom, thus the inset call. Finally there may be a |
| // bezel sensor off screen logically above |top_container_| thus the test |
| // needs to include gestures starting above. |
| gfx::Rect near_bounds = top_container_->GetBoundsInScreen(); |
| if (reveal_state_ == CLOSED) |
| near_bounds.Inset(gfx::Insets(0, 0, -kNearTopContainerDistance, 0)); |
| return near_bounds.Contains(location) || |
| ((location.y() < near_bounds.y()) && |
| (location.x() >= near_bounds.x()) && |
| (location.x() < near_bounds.right())); |
| } |
| |
| void ImmersiveModeControllerAsh::SetRenderWindowTopInsetsForTouch( |
| int top_inset) { |
| content::WebContents* contents = delegate_->GetWebContents(); |
| if (contents) { |
| aura::Window* window = contents->GetView()->GetContentNativeView(); |
| gfx::Insets inset(top_inset, 0, 0, 0); |
| window->SetHitTestBoundsOverrideOuter( |
| window->hit_test_bounds_override_outer_mouse(), |
| inset); |
| } |
| } |
| |
| void ImmersiveModeControllerAsh::RecreateBubbleManager() { |
| bubble_manager_.reset(new BubbleManager(this)); |
| const std::vector<aura::Window*> transient_children = |
| native_window_->transient_children(); |
| for (size_t i = 0; i < transient_children.size(); ++i) { |
| aura::Window* transient_child = transient_children[i]; |
| views::BubbleDelegateView* bubble_delegate = |
| AsBubbleDelegate(transient_child); |
| if (bubble_delegate && |
| bubble_delegate->anchor_view() && |
| top_container_->Contains(bubble_delegate->anchor_view())) { |
| bubble_manager_->StartObserving(transient_child); |
| } |
| } |
| } |