[go: nahoru, domu]

blob: e2399bfeddb13adcd2db058f7123d8307e657259 [file] [log] [blame]
// Copyright 2018 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/desk.h"
#include <algorithm>
#include <utility>
#include "ash/public/cpp/app_types.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/window_positioner.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_transient_descendant_iterator.h"
#include "ash/wm/window_util.h"
#include "ash/wm/workspace/backdrop_controller.h"
#include "ash/wm/workspace/workspace_layout_manager.h"
#include "ash/wm/workspace_controller.h"
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/macros.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/stl_util.h"
#include "chromeos/ui/base/window_properties.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window_tracker.h"
#include "ui/display/screen.h"
#include "ui/wm/core/window_util.h"
namespace ash {
namespace {
// The name of the histogram for consecutive daily visits.
constexpr char kConsecutiveDailyVisitsHistogramName[] =
"Ash.Desks.ConsecutiveDailyVisits";
// Prefix for the desks lifetime histogram.
constexpr char kDeskLifetimeHistogramNamePrefix[] = "Ash.Desks.DeskLifetime_";
void UpdateBackdropController(aura::Window* desk_container) {
auto* workspace_controller = GetWorkspaceController(desk_container);
// Work might have already been cleared when the display is removed. See
// |RootWindowController::MoveWindowsTo()|.
if (!workspace_controller)
return;
WorkspaceLayoutManager* layout_manager =
workspace_controller->layout_manager();
BackdropController* backdrop_controller =
layout_manager->backdrop_controller();
backdrop_controller->OnDeskContentChanged();
}
// Returns true if |window| can be managed by the desk, and therefore can be
// moved out of the desk when the desk is removed.
bool CanMoveWindowOutOfDeskContainer(aura::Window* window) {
// The desks bar widget is an activatable window placed in the active desk's
// container, therefore it should be allowed to move outside of its desk when
// its desk is removed.
if (window->id() == kShellWindowId_DesksBarWindow)
return true;
// We never move transient descendants directly, this is taken care of by
// `wm::TransientWindowManager::OnWindowHierarchyChanged()`.
auto* transient_root = ::wm::GetTransientRoot(window);
if (transient_root != window)
return false;
// Only allow app windows to move to other desks.
return window->GetProperty(aura::client::kAppType) !=
static_cast<int>(AppType::NON_APP);
}
// Adjusts the z-order stacking of |window_to_fix| in its parent to match its
// order in the MRU window list. This is done after the window is moved from one
// desk container to another by means of calling AddChild() which adds it as the
// top-most window, which doesn't necessarily match the MRU order.
// |window_to_fix| must be a child of a desk container, and the root of a
// transient hierarchy (if it belongs to one).
// This function must be called AddChild() was called to add the |window_to_fix|
// (i.e. |window_to_fix| is the top-most window or the top-most window is a
// transient child of |window_to_fix|).
void FixWindowStackingAccordingToGlobalMru(aura::Window* window_to_fix) {
aura::Window* container = window_to_fix->parent();
DCHECK(desks_util::IsDeskContainer(container));
DCHECK_EQ(window_to_fix, wm::GetTransientRoot(window_to_fix));
DCHECK(window_to_fix == container->children().back() ||
window_to_fix == wm::GetTransientRoot(container->children().back()));
const auto mru_windows =
Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
DesksMruType::kAllDesks);
// Find the closest sibling that is not a transient descendant, which
// |window_to_fix| should be stacked below.
aura::Window* closest_sibling_above_window = nullptr;
for (auto* window : mru_windows) {
if (window == window_to_fix) {
if (closest_sibling_above_window)
container->StackChildBelow(window_to_fix, closest_sibling_above_window);
return;
}
if (window->parent() == container &&
!wm::HasTransientAncestor(window, window_to_fix)) {
closest_sibling_above_window = window;
}
}
}
// Returns Jan 1, 2010 00:00:00 as a base::Time object in the local timezone.
base::Time GetLocalEpoch() {
static base::NoDestructor<base::Time> local_epoch;
if (local_epoch->is_null()) {
ignore_result(base::Time::FromLocalExploded({2010, 1, 5, 1, 0, 0, 0, 0},
local_epoch.get()));
}
return *local_epoch;
}
// Used to temporarily turn off the automatic window positioning while windows
// are being moved between desks.
class ScopedWindowPositionerDisabler {
public:
ScopedWindowPositionerDisabler() {
WindowPositioner::DisableAutoPositioning(true);
}
~ScopedWindowPositionerDisabler() {
WindowPositioner::DisableAutoPositioning(false);
}
private:
DISALLOW_COPY_AND_ASSIGN(ScopedWindowPositionerDisabler);
};
} // namespace
class DeskContainerObserver : public aura::WindowObserver {
public:
DeskContainerObserver(Desk* owner, aura::Window* container)
: owner_(owner), container_(container) {
DCHECK_EQ(container_->id(), owner_->container_id());
container->AddObserver(this);
}
~DeskContainerObserver() override { container_->RemoveObserver(this); }
// aura::WindowObserver:
void OnWindowAdded(aura::Window* new_window) override {
// TODO(afakhry): Overview mode creates a new widget for each window under
// the same parent for the OverviewItemView. We will be notified with
// this window addition here. Consider ignoring these windows if they cause
// problems.
owner_->AddWindowToDesk(new_window);
MaybeNotifyAllDesksOfContentChange(new_window);
}
void OnWindowRemoved(aura::Window* removed_window) override {
// We listen to `OnWindowRemoved()` as opposed to `OnWillRemoveWindow()`
// since we want to refresh the mini_views only after the window has been
// removed from the window tree hierarchy.
owner_->RemoveWindowFromDesk(removed_window);
MaybeNotifyAllDesksOfContentChange(removed_window);
}
void OnWindowDestroyed(aura::Window* window) override {
// We should never get here. We should be notified in
// `OnRootWindowClosing()` before the child containers of the root window
// are destroyed, and this object should have already been destroyed.
NOTREACHED();
}
private:
void MaybeNotifyAllDesksOfContentChange(aura::Window* window) {
// If a visible on all desks window is added/removed from a desk, only the
// desks directly involved will know about their contents changing since it
// only resides on the active desk. Since visible on all desks windows
// appear in each desks' preview view, we need to notify each desk.
auto* desks_controller = DesksController::Get();
if (desks_controller->visible_on_all_desks_windows().contains(window))
desks_controller->NotifyAllDesksForContentChanged();
}
Desk* const owner_;
aura::Window* const container_;
DISALLOW_COPY_AND_ASSIGN(DeskContainerObserver);
};
// -----------------------------------------------------------------------------
// Desk:
Desk::Desk(int associated_container_id)
: container_id_(associated_container_id),
creation_time_(base::Time::Now()) {
// For the very first default desk added during initialization, there won't be
// any root windows yet. That's OK, OnRootWindowAdded() will be called
// explicitly by the RootWindowController when they're initialized.
for (aura::Window* root : Shell::GetAllRootWindows())
OnRootWindowAdded(root);
}
Desk::~Desk() {
#if DCHECK_IS_ON()
for (auto* window : windows_) {
DCHECK(!CanMoveWindowOutOfDeskContainer(window))
<< "DesksController should remove this desk's application windows "
"first.";
}
#endif
for (auto& observer : observers_) {
observers_.RemoveObserver(&observer);
observer.OnDeskDestroyed(this);
}
}
void Desk::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void Desk::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void Desk::OnRootWindowAdded(aura::Window* root) {
DCHECK(!roots_to_containers_observers_.count(root));
// No windows should be added to the desk container on |root| prior to
// tracking it by the desk.
aura::Window* desk_container = root->GetChildById(container_id_);
DCHECK(desk_container->children().empty());
auto container_observer =
std::make_unique<DeskContainerObserver>(this, desk_container);
roots_to_containers_observers_.emplace(root, std::move(container_observer));
}
void Desk::OnRootWindowClosing(aura::Window* root) {
const size_t count = roots_to_containers_observers_.erase(root);
DCHECK(count);
// The windows on this root are about to be destroyed. We already stopped
// observing the container above, so we won't get a call to
// DeskContainerObserver::OnWindowRemoved(). Therefore, we must remove those
// windows manually. If this is part of shutdown (i.e. when the
// RootWindowController is being destroyed), then we're done with those
// windows. If this is due to a display being removed, then the
// WindowTreeHostManager will move those windows to another host/root, and
// they will be added again to the desk container on the new root.
const auto windows = windows_;
for (auto* window : windows) {
if (window->GetRootWindow() == root)
base::Erase(windows_, window);
}
}
void Desk::AddWindowToDesk(aura::Window* window) {
DCHECK(!base::Contains(windows_, window));
windows_.push_back(window);
// No need to refresh the mini_views if the destroyed window doesn't show up
// there in the first place. Also don't refresh for visible on all desks
// windows since they're already refreshed in OnWindowAdded().
if (!window->GetProperty(kHideInDeskMiniViewKey) &&
!window->GetProperty(aura::client::kVisibleOnAllWorkspacesKey)) {
NotifyContentChanged();
}
// Update the window's workspace to this parent desk.
if ((features::IsBentoEnabled() || features::IsFullRestoreEnabled()) &&
!is_desk_being_removed_) {
auto* desks_controller = DesksController::Get();
window->SetProperty(aura::client::kWindowWorkspaceKey,
desks_controller->GetDeskIndex(this));
}
}
void Desk::RemoveWindowFromDesk(aura::Window* window) {
DCHECK(base::Contains(windows_, window));
base::Erase(windows_, window);
// No need to refresh the mini_views if the destroyed window doesn't show up
// there in the first place. Also don't refresh for visible on all desks
// windows since they're already refreshed in OnWindowRemoved().
if (!window->GetProperty(kHideInDeskMiniViewKey) &&
!window->GetProperty(aura::client::kVisibleOnAllWorkspacesKey)) {
NotifyContentChanged();
}
}
base::AutoReset<bool> Desk::GetScopedNotifyContentChangedDisabler() {
return base::AutoReset<bool>(&should_notify_content_changed_, false);
}
void Desk::SetName(std::u16string new_name, bool set_by_user) {
// Even if the user focuses the DeskNameView for the first time and hits enter
// without changing the desk's name (i.e. |new_name| is the same,
// |is_name_set_by_user_| is false, and |set_by_user| is true), we don't
// change |is_name_set_by_user_| and keep considering the name as a default
// name.
if (name_ == new_name)
return;
name_ = std::move(new_name);
is_name_set_by_user_ = set_by_user;
for (auto& observer : observers_)
observer.OnDeskNameChanged(name_);
}
void Desk::PrepareForActivationAnimation() {
DCHECK(!is_active_);
for (aura::Window* root : Shell::GetAllRootWindows()) {
auto* container = root->GetChildById(container_id_);
container->layer()->SetOpacity(0);
container->Show();
}
started_activation_animation_ = true;
}
void Desk::Activate(bool update_window_activation) {
DCHECK(!is_active_);
if (!MaybeResetContainersOpacities()) {
for (aura::Window* root : Shell::GetAllRootWindows())
root->GetChildById(container_id_)->Show();
}
is_active_ = true;
if (!IsConsecutiveDailyVisit())
RecordAndResetConsecutiveDailyVisits(/*being_removed=*/false);
int current_date = GetDaysFromLocalEpoch();
if (current_date < last_day_visited_ || first_day_visited_ == -1) {
// If |current_date| < |last_day_visited_| then the user has moved timezones
// or the stored data has been corrupted so reset |first_day_visited_|.
first_day_visited_ = current_date;
}
last_day_visited_ = current_date;
if (!update_window_activation || windows_.empty())
return;
// Activate the window on this desk that was most recently used right before
// the user switched to another desk, so as not to break the user's workflow.
for (auto* window :
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk)) {
if (!base::Contains(windows_, window))
continue;
// Do not activate minimized windows, otherwise they will unminimize.
if (WindowState::Get(window)->IsMinimized())
continue;
wm::ActivateWindow(window);
return;
}
}
void Desk::Deactivate(bool update_window_activation) {
DCHECK(is_active_);
auto* active_window = window_util::GetActiveWindow();
// Hide the associated containers on all roots.
for (aura::Window* root : Shell::GetAllRootWindows())
root->GetChildById(container_id_)->Hide();
is_active_ = false;
last_day_visited_ = GetDaysFromLocalEpoch();
if (!update_window_activation)
return;
// Deactivate the active window (if it belongs to this desk; active window may
// be on a different container, or one of the widgets created by overview mode
// which are not considered desk windows) after this desk's associated
// containers have been hidden. This is to prevent the focus controller from
// activating another window on the same desk when the active window loses
// focus.
if (active_window && base::Contains(windows_, active_window))
wm::DeactivateWindow(active_window);
}
void Desk::MoveWindowsToDesk(Desk* target_desk) {
DCHECK(target_desk);
{
ScopedWindowPositionerDisabler window_positioner_disabler;
// Throttle notifying the observers, while we move those windows and notify
// them only once when done.
auto this_desk_throttled = GetScopedNotifyContentChangedDisabler();
auto target_desk_throttled =
target_desk->GetScopedNotifyContentChangedDisabler();
// Moving windows will change the hierarchy and hence |windows_|, and has to
// be done without changing the relative z-order. So we make a copy of all
// the top-level windows on all the containers of this desk, such that
// windows in each container are copied from top-most (z-order) to
// bottom-most.
// Note that moving windows out of the container and restacking them
// differently may trigger events that lead to destroying a window on the
// list. For example moving the top-most window which has a backdrop will
// cause the backdrop to be destroyed. Therefore observe such events using
// an |aura::WindowTracker|.
aura::WindowTracker windows_to_move;
for (aura::Window* root : Shell::GetAllRootWindows()) {
const aura::Window* container = GetDeskContainerForRoot(root);
for (auto* window : base::Reversed(container->children()))
windows_to_move.Add(window);
}
auto* mru_tracker = Shell::Get()->mru_window_tracker();
while (!windows_to_move.windows().empty()) {
auto* window = windows_to_move.Pop();
if (!CanMoveWindowOutOfDeskContainer(window))
continue;
// Note that windows that belong to the same container in
// |windows_to_move| are sorted from top-most to bottom-most, hence
// calling |StackChildAtBottom()| on each in this order will maintain that
// same order in the |target_desk|'s container.
MoveWindowToDeskInternal(window, target_desk, window->GetRootWindow());
window->parent()->StackChildAtBottom(window);
mru_tracker->OnWindowMovedOutFromRemovingDesk(window);
}
}
NotifyContentChanged();
target_desk->NotifyContentChanged();
}
void Desk::MoveWindowToDesk(aura::Window* window,
Desk* target_desk,
aura::Window* target_root) {
DCHECK(window);
DCHECK(target_desk);
DCHECK(target_root);
DCHECK(base::Contains(windows_, window));
DCHECK(this != target_desk);
// The desks bar should not be allowed to move individually to another desk.
// Only as part of `MoveWindowsToDesk()` when the desk is removed.
DCHECK_NE(window->id(), kShellWindowId_DesksBarWindow);
{
ScopedWindowPositionerDisabler window_positioner_disabler;
// Throttling here is necessary even though we're attempting to move a
// single window. This is because that window might exist in a transient
// window tree, which will result in actually moving multiple windows if the
// transient children used to be on the same container.
// See `wm::TransientWindowManager::OnWindowHierarchyChanged()`.
auto this_desk_throttled = GetScopedNotifyContentChangedDisabler();
auto target_desk_throttled =
target_desk->GetScopedNotifyContentChangedDisabler();
// Always move the root of the transient window tree. We should never move a
// transient child and leave its parent behind. Moving the transient
// descendants that exist on the same desk container will be taken care of
// by `wm::TransientWindowManager::OnWindowHierarchyChanged()`.
aura::Window* transient_root = ::wm::GetTransientRoot(window);
MoveWindowToDeskInternal(transient_root, target_desk, target_root);
FixWindowStackingAccordingToGlobalMru(transient_root);
// Unminimize the window so that it shows up in the mini_view after it had
// been dragged and moved to another desk. Don't unminimize if the window is
// visible on all desks since it's being moved during desk activation.
auto* window_state = WindowState::Get(transient_root);
if (window_state->IsMinimized() &&
!window->GetProperty(aura::client::kVisibleOnAllWorkspacesKey)) {
window_state->Unminimize();
}
}
NotifyContentChanged();
target_desk->NotifyContentChanged();
}
aura::Window* Desk::GetDeskContainerForRoot(aura::Window* root) const {
DCHECK(root);
return root->GetChildById(container_id_);
}
void Desk::NotifyContentChanged() {
if (!should_notify_content_changed_)
return;
// Updating the backdrops below may lead to the removal or creation of
// backdrop windows in this desk, which can cause us to recurse back here.
// Disable this.
auto disable_recursion = GetScopedNotifyContentChangedDisabler();
// The availability and visibility of backdrops of all containers associated
// with this desk will be updated *before* notifying observer, so that the
// mini_views update *after* the backdrops do.
// This is *only* needed if the WorkspaceLayoutManager won't take care of this
// for us while overview is active.
if (Shell::Get()->overview_controller()->InOverviewSession())
UpdateDeskBackdrops();
for (auto& observer : observers_)
observer.OnContentChanged();
}
void Desk::UpdateDeskBackdrops() {
for (auto* root : Shell::GetAllRootWindows())
UpdateBackdropController(GetDeskContainerForRoot(root));
}
void Desk::SetDeskBeingRemoved() {
is_desk_being_removed_ = true;
}
void Desk::RecordLifetimeHistogram() {
// Desk index is 1-indexed in histograms.
const int desk_index =
Shell::Get()->desks_controller()->GetDeskIndex(this) + 1;
base::UmaHistogramCounts1000(
base::StringPrintf("%s%i", kDeskLifetimeHistogramNamePrefix, desk_index),
(base::Time::Now() - creation_time_).InHours());
}
bool Desk::IsConsecutiveDailyVisit() const {
if (last_day_visited_ == -1)
return true;
const int days_since_last_visit = GetDaysFromLocalEpoch() - last_day_visited_;
return days_since_last_visit <= 1;
}
void Desk::RecordAndResetConsecutiveDailyVisits(bool being_removed) {
if (being_removed && is_active_) {
// When the user removes the active desk, update |last_day_visited_| to the
// current day to account for the time they spent on this desk.
last_day_visited_ = GetDaysFromLocalEpoch();
}
const int consecutive_daily_visits =
last_day_visited_ - first_day_visited_ + 1;
DCHECK_GE(consecutive_daily_visits, 1);
base::UmaHistogramCounts1000(kConsecutiveDailyVisitsHistogramName,
consecutive_daily_visits);
last_day_visited_ = -1;
first_day_visited_ = -1;
}
int Desk::GetDaysFromLocalEpoch() const {
base::Time now = override_clock_ ? override_clock_->Now() : base::Time::Now();
return (now - GetLocalEpoch()).InDays();
}
void Desk::OverrideClockForTesting(base::Clock* test_clock) {
DCHECK(!override_clock_);
override_clock_ = test_clock;
}
void Desk::ResetVisitedMetricsForTesting() {
const int current_date = GetDaysFromLocalEpoch();
first_day_visited_ = current_date;
last_day_visited_ = current_date;
}
void Desk::MoveWindowToDeskInternal(aura::Window* window,
Desk* target_desk,
aura::Window* target_root) {
DCHECK(base::Contains(windows_, window));
DCHECK(CanMoveWindowOutOfDeskContainer(window))
<< "Non-desk windows are not allowed to move out of the container.";
// When |target_root| is different than the current window's |root|, this can
// only happen when dragging and dropping a window on mini desk view on
// another display. Therefore |target_desk| is an inactive desk (i.e.
// invisible). The order doesn't really matter, but we move the window to the
// target desk's container first (so that it becomes hidden), then move it to
// the target display (while it's hidden).
aura::Window* root = window->GetRootWindow();
aura::Window* source_container = GetDeskContainerForRoot(root);
aura::Window* target_container = target_desk->GetDeskContainerForRoot(root);
DCHECK(window->parent() == source_container);
target_container->AddChild(window);
if (root != target_root) {
// Move the window to the container with the same ID on the target display's
// root (i.e. container that belongs to the same desk), and adjust its
// bounds to fit in the new display's work area.
window_util::MoveWindowToDisplay(window,
display::Screen::GetScreen()
->GetDisplayNearestWindow(target_root)
.id());
DCHECK_EQ(target_desk->container_id_, window->parent()->id());
}
}
bool Desk::MaybeResetContainersOpacities() {
if (!started_activation_animation_)
return false;
for (aura::Window* root : Shell::GetAllRootWindows()) {
auto* container = root->GetChildById(container_id_);
container->layer()->SetOpacity(1);
}
started_activation_animation_ = false;
return true;
}
} // namespace ash