| // 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/wm/overview/overview_controller.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "ash/keyboard/ui/keyboard_ui_controller.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shell.h" |
| #include "ash/wm/mru_window_tracker.h" |
| #include "ash/wm/overview/delayed_animation_observer_impl.h" |
| #include "ash/wm/overview/overview_constants.h" |
| #include "ash/wm/overview/overview_grid.h" |
| #include "ash/wm/overview/overview_item.h" |
| #include "ash/wm/overview/overview_session.h" |
| #include "ash/wm/overview/overview_utils.h" |
| #include "ash/wm/overview/overview_wallpaper_controller.h" |
| #include "ash/wm/screen_pinning_controller.h" |
| #include "ash/wm/splitview/split_view_controller.h" |
| #include "ash/wm/splitview/split_view_utils.h" |
| #include "ash/wm/tablet_mode/tablet_mode_controller.h" |
| #include "ash/wm/window_state.h" |
| #include "ash/wm/window_util.h" |
| #include "base/bind.h" |
| #include "base/containers/unique_ptr_adapters.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/stl_util.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/window_util.h" |
| #include "ui/wm/public/activation_client.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // It can take up to two frames until the frame created in the UI thread that |
| // triggered animation observer is drawn. Wait 50ms in attempt to let its draw |
| // and swap finish. |
| constexpr base::TimeDelta kOcclusionPauseDurationForStart = |
| base::TimeDelta::FromMilliseconds(50); |
| |
| // Wait longer when exiting overview mode in case when a user may re-enter |
| // overview mode immediately, contents are ready. |
| constexpr base::TimeDelta kOcclusionPauseDurationForEnd = |
| base::TimeDelta::FromMilliseconds(500); |
| |
| // Returns whether overview mode items should be slid in or out from the top of |
| // the screen. |
| bool ShouldSlideInOutOverview(const std::vector<aura::Window*>& windows) { |
| // No sliding if home launcher is not available. |
| if (!Shell::Get()->tablet_mode_controller()->InTabletMode()) { |
| return false; |
| } |
| |
| if (windows.empty()) |
| return false; |
| |
| // Only slide in if all windows are minimized. |
| for (const aura::Window* window : windows) { |
| if (!WindowState::Get(window)->IsMinimized()) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| OverviewController::OverviewController() |
| : occlusion_pause_duration_for_end_(kOcclusionPauseDurationForEnd), |
| overview_wallpaper_controller_( |
| std::make_unique<OverviewWallpaperController>()), |
| delayed_animation_task_delay_(kTransition) { |
| Shell::Get()->activation_client()->AddObserver(this); |
| } |
| |
| OverviewController::~OverviewController() { |
| Shell::Get()->activation_client()->RemoveObserver(this); |
| overview_wallpaper_controller_.reset(); |
| |
| // Destroy widgets that may be still animating if shell shuts down soon after |
| // exiting overview mode. |
| for (auto& animation_observer : delayed_animations_) |
| animation_observer->Shutdown(); |
| for (auto& animation_observer : start_animations_) |
| animation_observer->Shutdown(); |
| |
| if (overview_session_) { |
| overview_session_->Shutdown(); |
| overview_session_.reset(); |
| } |
| } |
| |
| bool OverviewController::StartOverview( |
| OverviewSession::EnterExitOverviewType type) { |
| // No need to start overview if overview is currently active. |
| if (InOverviewSession()) |
| return true; |
| |
| if (!CanEnterOverview()) |
| return false; |
| |
| ToggleOverview(type); |
| return true; |
| } |
| |
| bool OverviewController::EndOverview( |
| OverviewSession::EnterExitOverviewType type) { |
| // No need to end overview if overview is already ended. |
| if (!InOverviewSession()) |
| return true; |
| |
| if (!CanEndOverview(type)) |
| return false; |
| |
| ToggleOverview(type); |
| return true; |
| } |
| |
| bool OverviewController::InOverviewSession() const { |
| return overview_session_ && !overview_session_->is_shutting_down(); |
| } |
| |
| void OverviewController::IncrementSelection(bool forward) { |
| DCHECK(InOverviewSession()); |
| overview_session_->IncrementSelection(forward); |
| } |
| |
| bool OverviewController::AcceptSelection() { |
| DCHECK(InOverviewSession()); |
| return overview_session_->AcceptSelection(); |
| } |
| |
| void OverviewController::OnOverviewButtonTrayLongPressed( |
| const gfx::Point& event_location) { |
| // Do nothing if split view is not enabled. |
| if (!ShouldAllowSplitView()) |
| return; |
| |
| // Depending on the state of the windows and split view, a long press has many |
| // different results. |
| // 1. Already in split view - exit split view. The active snapped window |
| // becomes maximized. If overview was seen alongside a snapped window, then |
| // overview mode ends. |
| // 2. Not in overview mode - enter split view iff there is an active window |
| // and it is snappable. |
| // 3. In overview mode - enter split view iff there are at least two windows |
| // in the overview grid for the display where the overview button was long |
| // pressed, and the first window in that overview grid is snappable. |
| |
| auto* split_view_controller = SplitViewController::Get(); |
| // Exit split view mode if we are already in it. |
| if (split_view_controller->InSplitViewMode()) { |
| // In some cases the window returned by window_util::GetActiveWindow will be |
| // an item in overview mode (maybe the overview mode dummy focus widget). |
| // The active window may also be a transient descendant of the left or right |
| // snapped window, in which we want to activate the transient window's |
| // ancestor (left or right snapped window). Manually set |active_window| as |
| // either the left or right window. |
| aura::Window* active_window = window_util::GetActiveWindow(); |
| while (::wm::GetTransientParent(active_window)) |
| active_window = ::wm::GetTransientParent(active_window); |
| if (!split_view_controller->IsWindowInSplitView(active_window)) |
| active_window = split_view_controller->GetDefaultSnappedWindow(); |
| DCHECK(active_window); |
| split_view_controller->EndSplitView(); |
| EndOverview(); |
| MaximizeIfSnapped(active_window); |
| wm::ActivateWindow(active_window); |
| base::RecordAction( |
| base::UserMetricsAction("Tablet_LongPressOverviewButtonExitSplitView")); |
| return; |
| } |
| |
| OverviewItem* item_to_snap = nullptr; |
| if (!InOverviewSession()) { |
| // The current active window may be a transient child. |
| aura::Window* active_window = window_util::GetActiveWindow(); |
| while (active_window && ::wm::GetTransientParent(active_window)) |
| active_window = ::wm::GetTransientParent(active_window); |
| |
| // Do nothing if there are no active windows. |
| if (!active_window) |
| return; |
| |
| // Show a toast if the window cannot be snapped. |
| if (!CanSnapInSplitview(active_window)) { |
| ShowAppCannotSnapToast(); |
| return; |
| } |
| |
| // If we are not in overview mode, enter overview mode and then find the |
| // window item to snap. |
| StartOverview(); |
| DCHECK(overview_session_); |
| OverviewGrid* current_grid = overview_session_->GetGridWithRootWindow( |
| active_window->GetRootWindow()); |
| if (current_grid) |
| item_to_snap = current_grid->GetOverviewItemContaining(active_window); |
| } else { |
| // Currently in overview mode, with no snapped windows. Retrieve the first |
| // overview item and attempt to snap that window. |
| DCHECK(overview_session_); |
| OverviewGrid* current_grid = overview_session_->GetGridWithRootWindow( |
| window_util::GetRootWindowAt(event_location)); |
| if (current_grid) { |
| const auto& windows = current_grid->window_list(); |
| if (windows.size() > 1) |
| item_to_snap = windows[0].get(); |
| } |
| } |
| |
| // Do nothing if no item was retrieved, or if the retrieved item is |
| // unsnappable. |
| if (!item_to_snap || !CanSnapInSplitview(item_to_snap->GetWindow())) |
| return; |
| |
| split_view_controller->SnapWindow(item_to_snap->GetWindow(), |
| SplitViewController::LEFT); |
| base::RecordAction( |
| base::UserMetricsAction("Tablet_LongPressOverviewButtonEnterSplitView")); |
| } |
| |
| bool OverviewController::IsInStartAnimation() { |
| return !start_animations_.empty(); |
| } |
| |
| bool OverviewController::IsCompletingShutdownAnimations() const { |
| return !delayed_animations_.empty(); |
| } |
| |
| void OverviewController::PauseOcclusionTracker() { |
| if (occlusion_tracker_pauser_) |
| return; |
| |
| reset_pauser_task_.Cancel(); |
| occlusion_tracker_pauser_ = |
| std::make_unique<aura::WindowOcclusionTracker::ScopedPause>(); |
| } |
| |
| void OverviewController::UnpauseOcclusionTracker(base::TimeDelta delay) { |
| reset_pauser_task_.Reset(base::BindOnce(&OverviewController::ResetPauser, |
| weak_ptr_factory_.GetWeakPtr())); |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, reset_pauser_task_.callback(), delay); |
| } |
| |
| void OverviewController::AddObserver(OverviewObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void OverviewController::RemoveObserver(OverviewObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void OverviewController::DelayedUpdateRoundedCornersAndShadow() { |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&OverviewController::UpdateRoundedCornersAndShadow, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OverviewController::AddExitAnimationObserver( |
| std::unique_ptr<DelayedAnimationObserver> animation_observer) { |
| // No delayed animations should be created when overview mode is set to exit |
| // immediately. |
| DCHECK_NE(overview_session_->enter_exit_overview_type(), |
| OverviewSession::EnterExitOverviewType::kImmediateExit); |
| |
| animation_observer->SetOwner(this); |
| delayed_animations_.push_back(std::move(animation_observer)); |
| } |
| |
| void OverviewController::RemoveAndDestroyExitAnimationObserver( |
| DelayedAnimationObserver* animation_observer) { |
| const bool previous_empty = delayed_animations_.empty(); |
| base::EraseIf(delayed_animations_, |
| base::MatchesUniquePtr(animation_observer)); |
| |
| // If something has been removed and its the last observer, unblur the |
| // wallpaper and let observers know. This function may be called while still |
| // in overview (ie. splitview restores one window but leaves overview active) |
| // so check that |overview_session_| is null before notifying. |
| if (!overview_session_ && !previous_empty && delayed_animations_.empty()) |
| OnEndingAnimationComplete(/*canceled=*/false); |
| } |
| |
| void OverviewController::AddEnterAnimationObserver( |
| std::unique_ptr<DelayedAnimationObserver> animation_observer) { |
| animation_observer->SetOwner(this); |
| start_animations_.push_back(std::move(animation_observer)); |
| } |
| |
| void OverviewController::RemoveAndDestroyEnterAnimationObserver( |
| DelayedAnimationObserver* animation_observer) { |
| const bool previous_empty = start_animations_.empty(); |
| base::EraseIf(start_animations_, base::MatchesUniquePtr(animation_observer)); |
| |
| if (!previous_empty && start_animations_.empty()) |
| OnStartingAnimationComplete(/*canceled=*/false); |
| } |
| |
| void OverviewController::OnWindowActivating(ActivationReason reason, |
| aura::Window* gained_active, |
| aura::Window* lost_active) { |
| if (InOverviewSession()) |
| overview_session_->OnWindowActivating(reason, gained_active, lost_active); |
| } |
| |
| void OverviewController::OnAttemptToReactivateWindow( |
| aura::Window* request_active, |
| aura::Window* actual_active) { |
| if (InOverviewSession()) { |
| overview_session_->OnWindowActivating( |
| ::wm::ActivationChangeObserver::ActivationReason::ACTIVATION_CLIENT, |
| request_active, actual_active); |
| } |
| } |
| |
| bool OverviewController::HasBlurForTest() const { |
| return overview_wallpaper_controller_->has_blur(); |
| } |
| |
| bool OverviewController::HasBlurAnimationForTest() const { |
| return overview_wallpaper_controller_->HasBlurAnimationForTesting(); |
| } |
| |
| std::vector<aura::Window*> |
| OverviewController::GetWindowsListInOverviewGridsForTest() { |
| std::vector<aura::Window*> windows; |
| for (const std::unique_ptr<OverviewGrid>& grid : |
| overview_session_->grid_list()) { |
| for (const auto& overview_item : grid->window_list()) |
| windows.push_back(overview_item->GetWindow()); |
| } |
| return windows; |
| } |
| |
| std::vector<aura::Window*> |
| OverviewController::GetItemWindowListInOverviewGridsForTest() { |
| std::vector<aura::Window*> windows; |
| for (const std::unique_ptr<OverviewGrid>& grid : |
| overview_session_->grid_list()) { |
| for (const auto& overview_item : grid->window_list()) |
| windows.push_back(overview_item->item_widget()->GetNativeWindow()); |
| } |
| return windows; |
| } |
| |
| void OverviewController::ToggleOverview( |
| OverviewSession::EnterExitOverviewType type) { |
| // Hide the virtual keyboard as it obstructs the overview mode. |
| // Don't need to hide if it's the a11y keyboard, as overview mode |
| // can accept text input and it resizes correctly with the a11y keyboard. |
| keyboard::KeyboardUIController::Get()->HideKeyboardImplicitlyByUser(); |
| |
| auto windows = |
| Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk); |
| |
| // Hidden windows will be removed by window_util::ShouldExcludeForOverview so |
| // we must copy them out first. |
| std::vector<aura::Window*> hide_windows(windows.size()); |
| auto end = std::copy_if( |
| windows.begin(), windows.end(), hide_windows.begin(), |
| [](aura::Window* w) { return w->GetProperty(kHideInOverviewKey); }); |
| hide_windows.resize(end - hide_windows.begin()); |
| base::EraseIf(windows, window_util::ShouldExcludeForOverview); |
| // Overview windows will handle showing their transient related windows, so if |
| // a window in |windows| has a transient root also in |windows|, we can remove |
| // it as the transient root will handle showing the window. |
| window_util::RemoveTransientDescendants(&windows); |
| |
| if (InOverviewSession()) { |
| DCHECK(CanEndOverview(type)); |
| TRACE_EVENT_ASYNC_BEGIN0("ui", "OverviewController::ExitOverview", this); |
| |
| // Suspend occlusion tracker until the exit animation is complete. |
| PauseOcclusionTracker(); |
| |
| // We may want to slide out the overview grid in some cases, even if not |
| // explicitly stated. |
| OverviewSession::EnterExitOverviewType new_type = type; |
| if (type == OverviewSession::EnterExitOverviewType::kNormal && |
| ShouldSlideInOutOverview(windows)) { |
| new_type = OverviewSession::EnterExitOverviewType::kSlideOutExit; |
| } |
| overview_session_->set_enter_exit_overview_type(new_type); |
| if (type == OverviewSession::EnterExitOverviewType::kSlideOutExit || |
| type == OverviewSession::EnterExitOverviewType::kSwipeFromShelf) { |
| // Minimize the windows without animations. When the home launcher button |
| // is pressed, minimized widgets will get created in their place, and |
| // those widgets will be slid out of overview. Otherwise, |
| // HomeLauncherGestureHandler will handle sliding the windows out and when |
| // this function is called, we do not need to create minimized widgets. |
| std::vector<aura::Window*> windows_to_hide_minimize(windows.size()); |
| auto it = std::copy_if(windows.begin(), windows.end(), |
| windows_to_hide_minimize.begin(), |
| [](aura::Window* window) { |
| return !WindowState::Get(window)->IsMinimized(); |
| }); |
| windows_to_hide_minimize.resize( |
| std::distance(windows_to_hide_minimize.begin(), it)); |
| window_util::HideAndMaybeMinimizeWithoutAnimation( |
| windows_to_hide_minimize, true); |
| } |
| |
| if (!start_animations_.empty()) |
| OnStartingAnimationComplete(/*canceled=*/true); |
| start_animations_.clear(); |
| |
| overview_session_->set_is_shutting_down(true); |
| // Do not show mask and show during overview shutdown. |
| overview_session_->UpdateRoundedCornersAndShadow(); |
| |
| for (auto& observer : observers_) |
| observer.OnOverviewModeEnding(overview_session_.get()); |
| overview_session_->Shutdown(); |
| |
| if (overview_session_->enter_exit_overview_type() == |
| OverviewSession::EnterExitOverviewType::kImmediateExit) { |
| for (const auto& animation : delayed_animations_) |
| animation->Shutdown(); |
| delayed_animations_.clear(); |
| } |
| |
| // Don't delete |overview_session_| yet since the stack is still using it. |
| base::ThreadTaskRunnerHandle::Get()->DeleteSoon( |
| FROM_HERE, overview_session_.release()); |
| last_overview_session_time_ = base::Time::Now(); |
| for (auto& observer : observers_) |
| observer.OnOverviewModeEnded(); |
| if (delayed_animations_.empty()) |
| OnEndingAnimationComplete(/*canceled=*/false); |
| } else { |
| DCHECK(CanEnterOverview()); |
| TRACE_EVENT_ASYNC_BEGIN0("ui", "OverviewController::EnterOverview", this); |
| |
| // Clear any animations that may be running from last overview end. |
| for (const auto& animation : delayed_animations_) |
| animation->Shutdown(); |
| if (!delayed_animations_.empty()) |
| OnEndingAnimationComplete(/*canceled=*/true); |
| delayed_animations_.clear(); |
| |
| // |should_focus_overview_| shall be true except when split view mode starts |
| // on transition between clamshell mode and tablet mode, on transition |
| // between user sessions, or on transition between virtual desks. Those are |
| // the cases where code arranges split view by first snapping a window on |
| // one side and then starting overview to be seen on the other side, meaning |
| // that the split view state here will be |
| // |SplitViewController::State::kLeftSnapped| or |
| // |SplitViewController::State::kRightSnapped|. We have to check the split |
| // view state before |SplitViewController::OnOverviewModeStarting|, because |
| // in case of |SplitViewController::State::kBothSnapped|, that function will |
| // insert one of the two snapped windows to overview. |
| const SplitViewController::State split_view_state = |
| SplitViewController::Get()->state(); |
| should_focus_overview_ = |
| split_view_state == SplitViewController::State::kNoSnap || |
| split_view_state == SplitViewController::State::kBothSnapped; |
| |
| // Suspend occlusion tracker until the enter animation is complete. |
| PauseOcclusionTracker(); |
| |
| overview_session_ = std::make_unique<OverviewSession>(this); |
| // We may want to slide in the overview grid in some cases, even if not |
| // explicitly stated. |
| OverviewSession::EnterExitOverviewType new_type = type; |
| if (type == OverviewSession::EnterExitOverviewType::kNormal && |
| ShouldSlideInOutOverview(windows)) { |
| new_type = OverviewSession::EnterExitOverviewType::kSlideInEnter; |
| } |
| overview_session_->set_enter_exit_overview_type(new_type); |
| for (auto& observer : observers_) |
| observer.OnOverviewModeStarting(); |
| overview_session_->Init(windows, hide_windows); |
| overview_wallpaper_controller_->Blur(/*animate_only=*/false); |
| |
| // For app dragging, there are no start animations so add a delay to delay |
| // animations observing when the start animation ends, such as the shelf, |
| // shadow and rounded corners. |
| if (new_type == OverviewSession::EnterExitOverviewType::kImmediateEnter && |
| !delayed_animation_task_delay_.is_zero()) { |
| auto force_delay_observer = |
| std::make_unique<ForceDelayObserver>(delayed_animation_task_delay_); |
| AddEnterAnimationObserver(std::move(force_delay_observer)); |
| } |
| |
| if (start_animations_.empty()) |
| OnStartingAnimationComplete(/*canceled=*/false); |
| |
| if (!last_overview_session_time_.is_null()) { |
| UMA_HISTOGRAM_LONG_TIMES("Ash.WindowSelector.TimeBetweenUse", |
| base::Time::Now() - last_overview_session_time_); |
| } |
| } |
| } |
| |
| bool OverviewController::CanEnterOverview() { |
| // Prevent toggling overview during the split view divider snap animation. |
| if (SplitViewController::Get()->IsDividerAnimating()) |
| return false; |
| |
| // Don't allow a window overview if the user session is not active (e.g. |
| // locked or in user-adding screen) or a modal dialog is open or running in |
| // kiosk app session. |
| SessionControllerImpl* session_controller = |
| Shell::Get()->session_controller(); |
| return session_controller->GetSessionState() == |
| session_manager::SessionState::ACTIVE && |
| !Shell::IsSystemModalWindowOpen() && |
| !Shell::Get()->screen_pinning_controller()->IsPinned() && |
| !session_controller->IsRunningInAppMode(); |
| } |
| |
| bool OverviewController::CanEndOverview( |
| OverviewSession::EnterExitOverviewType type) { |
| SplitViewController* split_view_controller = SplitViewController::Get(); |
| // Prevent toggling overview during the split view divider snap animation. |
| if (split_view_controller->IsDividerAnimating()) |
| return false; |
| |
| // Do not allow ending overview if we're in single split mode unless swiping |
| // up from the shelf in tablet mode, or ending overview immediately without |
| // animations. |
| if (split_view_controller->InTabletSplitViewMode() && |
| split_view_controller->state() != |
| SplitViewController::State::kBothSnapped && |
| InOverviewSession() && overview_session_->IsEmpty() && |
| type != OverviewSession::EnterExitOverviewType::kSwipeFromShelf && |
| type != OverviewSession::EnterExitOverviewType::kImmediateExit) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void OverviewController::OnStartingAnimationComplete(bool canceled) { |
| if (!canceled) |
| overview_wallpaper_controller_->Blur(/*animate_only=*/true); |
| |
| for (auto& observer : observers_) |
| observer.OnOverviewModeStartingAnimationComplete(canceled); |
| if (overview_session_) { |
| overview_session_->OnStartingAnimationComplete(canceled, |
| should_focus_overview_); |
| } |
| UnpauseOcclusionTracker(kOcclusionPauseDurationForStart); |
| TRACE_EVENT_ASYNC_END1("ui", "OverviewController::EnterOverview", this, |
| "canceled", canceled); |
| } |
| |
| void OverviewController::OnEndingAnimationComplete(bool canceled) { |
| // Unblur when animation is completed (or right away if there was no |
| // delayed animation) unless it's canceled, in which case, we should keep |
| // the blur. |
| if (!canceled) |
| overview_wallpaper_controller_->Unblur(); |
| |
| for (auto& observer : observers_) |
| observer.OnOverviewModeEndingAnimationComplete(canceled); |
| UnpauseOcclusionTracker(occlusion_pause_duration_for_end_); |
| TRACE_EVENT_ASYNC_END1("ui", "OverviewController::ExitOverview", this, |
| "canceled", canceled); |
| } |
| |
| void OverviewController::ResetPauser() { |
| occlusion_tracker_pauser_.reset(); |
| } |
| |
| void OverviewController::UpdateRoundedCornersAndShadow() { |
| if (overview_session_) |
| overview_session_->UpdateRoundedCornersAndShadow(); |
| } |
| |
| } // namespace ash |