blob: 8ee5993e12d54b95449ff6d866280601a33b0e47 [file] [log] [blame]
// Copyright 2019 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/desks/desks_controller.h"
#include <utility>
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/desks/root_window_desk_switch_animator.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/window_util.h"
#include "base/auto_reset.h"
#include "base/logging.h"
#include "ui/aura/window_tree_host.h"
#include "ui/compositor/compositor.h"
namespace ash {
namespace {
// Appends the given |windows| to the end of the currently active overview mode
// session such that the most-recently used window is added first. If
// |should_animate| is true, the windows will animate to their positions in the
// overview grid.
void AppendWindowsToOverview(const base::flat_set<aura::Window*>& windows,
bool should_animate) {
DCHECK(Shell::Get()->overview_controller()->InOverviewSession());
auto* overview_session =
Shell::Get()->overview_controller()->overview_session();
for (auto* window :
Shell::Get()->mru_window_tracker()->BuildMruWindowList()) {
if (!windows.contains(window) || wm::ShouldExcludeForOverview(window))
continue;
overview_session->AppendItem(window, /*reposition=*/true, should_animate);
}
}
// Removes the given |windows| from the currently active overview mode session.
void RemoveWindowsFromOverview(const base::flat_set<aura::Window*>& windows) {
DCHECK(Shell::Get()->overview_controller()->InOverviewSession());
auto* overview_session =
Shell::Get()->overview_controller()->overview_session();
for (auto* window : windows) {
auto* item = overview_session->GetOverviewItemForWindow(window);
if (item)
overview_session->RemoveItem(item);
}
}
} // namespace
DesksController::DesksController() {
for (int id : desks_util::GetDesksContainersIds())
available_container_ids_.push(id);
// There's always one default desk.
NewDesk();
active_desk_ = desks_.back().get();
active_desk_->Activate(/*update_window_activation=*/true);
}
DesksController::~DesksController() = default;
// static
DesksController* DesksController::Get() {
return Shell::Get()->desks_controller();
}
void DesksController::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void DesksController::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
bool DesksController::AreDesksBeingModified() const {
return are_desks_being_modified_ || !desk_switch_animators_.empty();
}
bool DesksController::CanCreateDesks() const {
// TODO(afakhry): Disable creating new desks in tablet mode.
return desks_.size() < desks_util::kMaxNumberOfDesks;
}
bool DesksController::CanRemoveDesks() const {
return desks_.size() > 1;
}
void DesksController::NewDesk() {
DCHECK(CanCreateDesks());
DCHECK(!available_container_ids_.empty());
base::AutoReset<bool> in_progress(&are_desks_being_modified_, true);
desks_.emplace_back(std::make_unique<Desk>(available_container_ids_.front()));
available_container_ids_.pop();
for (auto& observer : observers_)
observer.OnDeskAdded(desks_.back().get());
}
void DesksController::RemoveDesk(const Desk* desk) {
DCHECK(CanRemoveDesks());
base::AutoReset<bool> in_progress(&are_desks_being_modified_, true);
auto iter = std::find_if(
desks_.begin(), desks_.end(),
[desk](const std::unique_ptr<Desk>& d) { return d.get() == desk; });
DCHECK(iter != desks_.end());
// Keep the removed desk alive until the end of this function.
std::unique_ptr<Desk> removed_desk = std::move(*iter);
DCHECK_EQ(removed_desk.get(), desk);
auto iter_after = desks_.erase(iter);
DCHECK(!desks_.empty());
const bool in_overview =
Shell::Get()->overview_controller()->InOverviewSession();
const base::flat_set<aura::Window*> removed_desk_windows =
removed_desk->windows();
// No need to spend time refreshing the mini_views of the removed desk.
auto removed_desk_mini_views_pauser =
removed_desk->GetScopedNotifyContentChangedDisabler();
// - Move windows in removed desk (if any) to the currently active desk.
// - If the active desk is the one being removed, activate the desk to its
// left, if no desk to the left, activate one on the right.
if (removed_desk.get() != active_desk_) {
// We will refresh the mini_views of the active desk only once at the end.
auto active_desk_mini_view_pauser =
active_desk_->GetScopedNotifyContentChangedDisabler();
removed_desk->MoveWindowsToDesk(active_desk_);
// If overview mode is active, we add the windows of the removed desk to the
// overview grid in the order of their MRU. Note that this can only be done
// after the windows have moved to the active desk above, so that building
// the window MRU list should contain those windows.
if (in_overview)
AppendWindowsToOverview(removed_desk_windows, /*should_animate=*/true);
} else {
Desk* target_desk = nullptr;
if (iter_after == desks_.begin()) {
// Nothing before this desk.
target_desk = (*iter_after).get();
} else {
// Back up to select the desk on the left.
target_desk = (*(--iter_after)).get();
}
DCHECK(target_desk);
// The target desk, which is about to become active, will have its
// mini_views refreshed at the end.
auto target_desk_mini_view_pauser =
target_desk->GetScopedNotifyContentChangedDisabler();
// The removed desk is the active desk, so temporarily remove its windows
// from the overview grid which will result in removing the
// "OverviewModeLabel" widgets created by overview mode for these windows.
// This way the removed desk tracks only real windows, which are now ready
// to be moved to the target desk.
if (in_overview)
RemoveWindowsFromOverview(removed_desk_windows);
// If overview mode is active, change desk activation without changing
// window activation. Activation should remain on the dummy
// "OverviewModeFocusedWidget" while overview mode is active.
removed_desk->MoveWindowsToDesk(target_desk);
ActivateDeskInternal(target_desk,
/*update_window_activation=*/!in_overview);
// Now that the windows from the removed and target desks merged, add them
// all without animation to the grid in the order of their MRU.
if (in_overview)
AppendWindowsToOverview(target_desk->windows(), /*should_animate=*/false);
}
// It's OK now to refresh the mini_views of *only* the active desk, and only
// if windows from the removed desk moved to it.
DCHECK(active_desk_->should_notify_content_changed());
if (!removed_desk_windows.empty())
active_desk_->NotifyContentChanged();
for (auto& observer : observers_)
observer.OnDeskRemoved(removed_desk.get());
available_container_ids_.push(removed_desk->container_id());
DCHECK_LE(available_container_ids_.size(), desks_util::kMaxNumberOfDesks);
}
void DesksController::ActivateDesk(const Desk* desk) {
DCHECK(HasDesk(desk));
if (desk == active_desk_) {
OverviewController* overview_controller =
Shell::Get()->overview_controller();
if (overview_controller->InOverviewSession()) {
// Selecting the active desk's mini_view in overview mode is allowed and
// should just exit overview mode normally.
overview_controller->ToggleOverview();
}
return;
}
// New desks are always added at the end of the list to the right of existing
// desks. Therefore, desks at lower indices are located on the left of desks
// with higher indices.
const bool move_left = GetDeskIndex(active_desk_) < GetDeskIndex(desk);
for (auto* root : Shell::GetAllRootWindows()) {
desk_switch_animators_.emplace_back(
std::make_unique<RootWindowDeskSwitchAnimator>(root, desk, this,
move_left));
}
// Once all animators are created, start them all by taking the starting desk
// screenshots. This is to avoid any potential race conditions that might
// happen if one animator finished phase (1) of the animation while other
// animators are still being constructed.
for (auto& animator : desk_switch_animators_)
animator->TakeStartingDeskScreenshot();
}
void DesksController::MoveWindowFromActiveDeskTo(aura::Window* window,
Desk* target_desk) {
DCHECK(active_desk_->windows().contains(window));
DCHECK_NE(active_desk_, target_desk);
base::AutoReset<bool> in_progress(&are_desks_being_modified_, true);
active_desk_->MoveWindowToDesk(window, target_desk);
}
void DesksController::OnRootWindowAdded(aura::Window* root_window) {
for (auto& desk : desks_)
desk->OnRootWindowAdded(root_window);
}
void DesksController::OnRootWindowClosing(aura::Window* root_window) {
for (auto& desk : desks_)
desk->OnRootWindowClosing(root_window);
}
void DesksController::OnStartingDeskScreenshotTaken(const Desk* ending_desk) {
DCHECK(!desk_switch_animators_.empty());
// Once all starting desk screenshots on all roots are taken and placed on the
// screens, do the actual desk activation logic.
for (const auto& animator : desk_switch_animators_) {
if (!animator->starting_desk_screenshot_taken())
return;
}
// Extend the compositors' timeouts in order to prevents any repaints until
// the desks are switched and overview mode exits.
const auto roots = Shell::GetAllRootWindows();
for (auto* root : roots)
root->GetHost()->compositor()->SetAllowLocksToExtendTimeout(true);
const bool in_overview =
Shell::Get()->overview_controller()->InOverviewSession();
ActivateDeskInternal(ending_desk, /*update_window_activation=*/true);
// Activating a desk should not change the overview mode state.
DCHECK_EQ(in_overview,
Shell::Get()->overview_controller()->InOverviewSession());
if (in_overview) {
// Exit overview mode immediately without any animations before taking the
// ending desk screenshot. This makes sure that the ending desk
// screenshot will only show the windows in that desk, not overview stuff.
Shell::Get()->overview_controller()->ToggleOverview(
OverviewSession::EnterExitOverviewType::kImmediateExit);
}
for (auto* root : roots)
root->GetHost()->compositor()->SetAllowLocksToExtendTimeout(false);
// Continue the second phase of the animation by taking the ending desk
// screenshot and actually animating the layers.
for (auto& animator : desk_switch_animators_)
animator->TakeEndingDeskScreenshot();
}
void DesksController::OnEndingDeskScreenshotTaken() {
DCHECK(!desk_switch_animators_.empty());
// Once all ending desk screenshots on all roots are taken, start the
// animation on all roots at the same time, so that they look synchrnoized.
for (const auto& animator : desk_switch_animators_) {
if (!animator->ending_desk_screenshot_taken())
return;
}
for (auto& animator : desk_switch_animators_)
animator->StartAnimation();
}
void DesksController::OnDeskSwitchAnimationFinished() {
DCHECK(!desk_switch_animators_.empty());
// Once all desk switch animations on all roots finish, destroy all the
// animators.
for (const auto& animator : desk_switch_animators_) {
if (!animator->animation_finished())
return;
}
desk_switch_animators_.clear();
for (auto& observer : observers_)
observer.OnDeskSwitchAnimationFinished();
}
bool DesksController::HasDesk(const Desk* desk) const {
auto iter = std::find_if(
desks_.begin(), desks_.end(),
[desk](const std::unique_ptr<Desk>& d) { return d.get() == desk; });
return iter != desks_.end();
}
int DesksController::GetDeskIndex(const Desk* desk) const {
for (size_t i = 0; i < desks_.size(); ++i) {
if (desk == desks_[i].get())
return i;
}
NOTREACHED();
return -1;
}
void DesksController::ActivateDeskInternal(const Desk* desk,
bool update_window_activation) {
DCHECK(HasDesk(desk));
if (desk == active_desk_)
return;
base::AutoReset<bool> in_progress(&are_desks_being_modified_, true);
// Mark the new desk as active first, so that deactivating windows on the
// `old_active` desk do not activate other windows on the same desk. See
// `ash::IsWindowConsideredVisibleForActivation()`.
Desk* old_active = active_desk_;
active_desk_ = const_cast<Desk*>(desk);
// There should always be an active desk at any time.
DCHECK(old_active);
old_active->Deactivate(update_window_activation);
active_desk_->Activate(update_window_activation);
for (auto& observer : observers_)
observer.OnDeskActivationChanged(active_desk_, old_active);
}
} // namespace ash