#include "ash/wm/tablet_mode/tablet_mode_window_manager.h"
#include <memory>
#include <vector>
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_types.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/tablet_mode/scoped_skip_user_session_blocked_check.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_multitask_menu_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_toggle_fullscreen_event_handler.h"
#include "ash/wm/tablet_mode/tablet_mode_window_state.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.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/contains.h"
#include "base/memory/raw_ptr.h"
#include "chromeos/ui/base/window_properties.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/display/screen.h"
#include "ui/wm/core/scoped_animation_disabler.h"
namespace ash {
namespace {
using ::chromeos::WindowStateType;
bool IsCarryOverCandidateForSplitView(
const MruWindowTracker::WindowList& windows,
size_t i,
aura::Window* root_window) {
return windows.size() > i && windows[i]->GetRootWindow() == root_window &&
SplitViewController::Get(root_window)
->CanKeepCurrentSnapRatio(windows[i]);
}
void MaybeEndSplitViewAndOverview() {
Shell* shell = Shell::Get();
OverviewController* overview_controller = shell->overview_controller();
const bool empty_or_inactive_overview =
!overview_controller->InOverviewSession() ||
overview_controller->overview_session()->IsEmpty();
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
SnapGroupController* snap_group_controller = SnapGroupController::Get();
auto* primary_window = split_view_controller->primary_window();
auto* secondary_window = split_view_controller->secondary_window();
const bool windows_in_snap_group =
snap_group_controller && primary_window && secondary_window &&
snap_group_controller->AreWindowsInSnapGroup(primary_window,
secondary_window);
if (split_view_controller->InClamshellSplitViewMode() &&
empty_or_inactive_overview && !windows_in_snap_group) {
split_view_controller->EndSplitView(
SplitViewController::EndReason::kExitTabletMode);
overview_controller->EndOverview(OverviewEndAction::kSplitView);
}
}
void DoSplitViewTransition(
std::vector<std::pair<aura::Window*, WindowStateType>> windows,
int divider_position,
WindowSnapActionSource snap_action_source) {
if (windows.empty())
return;
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
for (auto& iter : windows) {
std::optional<float> snap_ratio =
WindowState::Get(iter.first)->snap_ratio();
split_view_controller->SnapWindow(
iter.first,
iter.second == WindowStateType::kPrimarySnapped
? SnapPosition::kPrimary
: SnapPosition::kSecondary,
snap_action_source,
false,
snap_ratio ? *snap_ratio : chromeos::kDefaultSnapRatio);
}
MaybeEndSplitViewAndOverview();
}
void UpdateDeskContainersBackdrops() {
for (aura::Window* root : Shell::GetAllRootWindows()) {
for (auto* desk_container : desks_util::GetDesksContainers(root)) {
WorkspaceController* controller = GetWorkspaceController(desk_container);
WorkspaceLayoutManager* layout_manager = controller->layout_manager();
BackdropController* backdrop_controller =
layout_manager->backdrop_controller();
backdrop_controller->UpdateBackdrop();
}
}
}
}
class ScopedObserveWindowAnimation {
public:
ScopedObserveWindowAnimation(aura::Window* window,
TabletModeWindowManager* manager,
bool exiting_tablet_mode)
: window_(window),
manager_(manager),
exiting_tablet_mode_(exiting_tablet_mode) {
if (Shell::Get()->tablet_mode_controller() && window_) {
Shell::Get()->tablet_mode_controller()->MaybeObserveBoundsAnimation(
window_);
}
}
ScopedObserveWindowAnimation(const ScopedObserveWindowAnimation&) = delete;
ScopedObserveWindowAnimation& operator=(const ScopedObserveWindowAnimation&) =
delete;
~ScopedObserveWindowAnimation() {
if (!Shell::Get()->tablet_mode_controller())
return;
if (!window_)
return;
const bool is_animating =
window_->layer()->GetAnimator()->IsAnimatingProperty(
TabletModeController::GetObservedTabletTransitionProperty());
if (is_animating &&
(exiting_tablet_mode_ || manager_->IsTrackingWindow(window_))) {
return;
}
Shell::Get()->tablet_mode_controller()->StopObservingAnimation(
false, true);
}
private:
raw_ptr<aura::Window> window_;
raw_ptr<TabletModeWindowManager> manager_;
bool exiting_tablet_mode_;
};
TabletModeWindowManager::TabletModeWindowManager() = default;
TabletModeWindowManager::~TabletModeWindowManager() = default;
void TabletModeWindowManager::Init() {
{
ScopedObserveWindowAnimation scoped_observe(
window_util::GetTopNonFloatedWindow(), this,
false);
ArrangeWindowsForTabletMode();
}
AddWindowCreationObservers();
display_observer_.emplace(this);
SplitViewController::Get(Shell::GetPrimaryRootWindow())->AddObserver(this);
Shell::Get()->session_controller()->AddObserver(this);
Shell::Get()->overview_controller()->AddObserver(this);
accounts_since_entering_tablet_.insert(
Shell::Get()->session_controller()->GetActiveAccountId());
event_handler_ = std::make_unique<TabletModeToggleFullscreenEventHandler>();
tablet_mode_multitask_menu_controller_ =
std::make_unique<TabletModeMultitaskMenuController>();
}
void TabletModeWindowManager::Shutdown(ShutdownReason shutdown_reason) {
WindowAndStateTypeList carryover_windows_in_splitview;
const bool was_in_overview =
Shell::Get()->overview_controller()->InOverviewSession();
if (shutdown_reason == ShutdownReason::kExitTabletUIMode) {
carryover_windows_in_splitview =
GetCarryOverWindowsInSplitView(false);
MaybeEndSplitViewAndOverview();
}
for (aura::Window* window : windows_to_track_)
window->RemoveObserver(this);
windows_to_track_.clear();
SplitViewController::Get(Shell::GetPrimaryRootWindow())->RemoveObserver(this);
Shell::Get()->session_controller()->RemoveObserver(this);
Shell::Get()->overview_controller()->RemoveObserver(this);
display_observer_.reset();
RemoveWindowCreationObservers();
if (shutdown_reason == ShutdownReason::kExitTabletUIMode) {
ScopedObserveWindowAnimation scoped_observe(
window_util::GetTopNonFloatedWindow(), this,
true);
ArrangeWindowsForClamshellMode(carryover_windows_in_splitview,
was_in_overview);
} else {
CHECK_EQ(shutdown_reason, ShutdownReason::kSystemShutdown);
while (window_state_map_.size()) {
WindowToState::iterator iter = window_state_map_.begin();
iter->first->RemoveObserver(this);
window_state_map_.erase(iter);
}
}
}
bool TabletModeWindowManager::IsTrackingWindow(aura::Window* window) {
return base::Contains(window_state_map_, window);
}
int TabletModeWindowManager::GetNumberOfManagedWindows() {
return window_state_map_.size();
}
void TabletModeWindowManager::AddWindow(aura::Window* window) {
if (IsTrackingWindow(window) || !IsContainerWindow(window->parent()))
return;
TrackWindow(window);
}
void TabletModeWindowManager::WindowStateDestroyed(aura::Window* window) {
DCHECK(!window->HasObserver(this));
auto it = window_state_map_.find(window);
if (it != window_state_map_.end())
window_state_map_.erase(it);
}
void TabletModeWindowManager::SetIgnoreWmEventsForExit() {
is_exiting_ = true;
for (auto& pair : window_state_map_)
pair.second->set_ignore_wm_events(true);
}
void TabletModeWindowManager::StopWindowAnimations() {
for (auto& pair : window_state_map_)
pair.first->layer()->GetAnimator()->StopAnimating();
}
void TabletModeWindowManager::OnOverviewModeEndingAnimationComplete(
bool canceled) {
if (canceled)
return;
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
const MruWindowTracker::WindowList windows =
Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
kActiveDesk);
for (aura::Window* window : windows) {
if (split_view_controller->primary_window() != window &&
split_view_controller->secondary_window() != window) {
MaximizeIfSnapped(window);
}
}
}
void TabletModeWindowManager::OnSplitViewStateChanged(
SplitViewController::State previous_state,
SplitViewController::State state) {
if (is_exiting_)
return;
if (state != SplitViewController::State::kNoSnap)
return;
aura::Window* primary_root = Shell::GetPrimaryRootWindow();
switch (SplitViewController::Get(primary_root)->end_reason()) {
case SplitViewController::EndReason::kNormal:
case SplitViewController::EndReason::kUnsnappableWindowActivated:
case SplitViewController::EndReason::kRootWindowDestroyed:
break;
case SplitViewController::EndReason::kHomeLauncherPressed:
case SplitViewController::EndReason::kActiveUserChanged:
case SplitViewController::EndReason::kWindowDragStarted:
case SplitViewController::EndReason::kExitTabletMode:
case SplitViewController::EndReason::kDesksChange:
case SplitViewController::EndReason::kSnapGroups:
return;
}
const MruWindowTracker::WindowList windows =
Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
kActiveDesk);
for (aura::Window* window : windows) {
if (window->GetRootWindow() != primary_root)
continue;
MaximizeIfSnapped(window);
}
}
void TabletModeWindowManager::OnWindowDestroying(aura::Window* window) {
if (IsContainerWindow(window)) {
window->RemoveObserver(this);
observed_container_windows_.erase(window);
} else if (base::Contains(windows_to_track_, window)) {
windows_to_track_.erase(window);
window->RemoveObserver(this);
} else {
ForgetWindow(window, true);
}
}
void TabletModeWindowManager::OnWindowHierarchyChanged(
const HierarchyChangeParams& params) {
if (params.new_parent && IsContainerWindow(params.new_parent) &&
!IsTrackingWindow(params.target)) {
if (!params.target->IsVisible()) {
if (!base::Contains(windows_to_track_, params.target)) {
windows_to_track_.insert(params.target);
params.target->AddObserver(this);
}
return;
}
TrackWindow(params.target);
if (IsTrackingWindow(params.target)) {
WMEvent event(WM_EVENT_ADDED_TO_WORKSPACE);
WindowState::Get(params.target)->OnWMEvent(&event);
}
}
}
void TabletModeWindowManager::OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old) {
if (key == aura::client::kZOrderingKey &&
window->GetProperty(aura::client::kZOrderingKey) !=
ui::ZOrderLevel::kNormal) {
ForgetWindow(window, false );
}
}
void TabletModeWindowManager::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
if (!IsContainerWindow(window))
return;
auto* session = Shell::Get()->overview_controller()->overview_session();
if (session)
session->SuspendReposition();
for (auto& pair : window_state_map_) {
TabletModeWindowState::UpdateWindowPosition(
WindowState::Get(pair.first),
WindowState::BoundsChangeAnimationType::kNone);
}
if (session)
session->ResumeReposition();
}
void TabletModeWindowManager::OnWindowVisibilityChanged(aura::Window* window,
bool visible) {
if (IsTrackingWindow(window))
return;
if (IsContainerWindow(window->parent()) &&
base::Contains(windows_to_track_, window) && visible) {
TrackWindow(window);
if (IsTrackingWindow(window)) {
WMEvent event(WM_EVENT_ADDED_TO_WORKSPACE);
WindowState::Get(window)->OnWMEvent(&event);
}
}
}
void TabletModeWindowManager::OnDisplayAdded(const display::Display& display) {
DisplayConfigurationChanged();
}
void TabletModeWindowManager::OnDisplaysRemoved(
const display::Displays& removed_displays) {
DisplayConfigurationChanged();
}
void TabletModeWindowManager::OnActiveUserSessionChanged(
const AccountId& account_id) {
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
split_view_controller->EndSplitView(
SplitViewController::EndReason::kActiveUserChanged);
bool refresh_snapped_windows = false;
if (accounts_since_entering_tablet_.count(account_id) == 0u) {
WindowAndStateTypeList windows_in_splitview =
GetCarryOverWindowsInSplitView(true);
const int divider_position = CalculateCarryOverDividerPosition(
windows_in_splitview, true);
DoSplitViewTransition(windows_in_splitview, divider_position,
WindowSnapActionSource::kSnapByDeskOrSessionChange);
accounts_since_entering_tablet_.insert(account_id);
} else {
refresh_snapped_windows = true;
}
MaybeRestoreSplitView(refresh_snapped_windows);
}
gfx::Rect TabletModeWindowManager::GetWindowBoundsInScreen(
aura::Window* window,
bool from_clamshell) const {
auto iter = window_state_map_.find(window);
return !from_clamshell || iter == window_state_map_.end()
? window->GetBoundsInScreen()
: iter->second->old_window_bounds_in_screen();
}
WindowStateType TabletModeWindowManager::GetWindowStateType(
aura::Window* window,
bool from_clamshell) const {
auto iter = window_state_map_.find(window);
return !from_clamshell || iter == window_state_map_.end()
? WindowState::Get(window)->GetStateType()
: iter->second->old_state()->GetType();
}
TabletModeWindowManager::WindowAndStateTypeList
TabletModeWindowManager::GetCarryOverWindowsInSplitView(
bool clamshell_to_tablet) const {
WindowAndStateTypeList windows;
MruWindowTracker::WindowList mru_windows =
Shell::Get()->mru_window_tracker()->BuildWindowForCycleList(kActiveDesk);
std::erase_if(mru_windows, [](aura::Window* window) {
return window->GetProperty(chromeos::kIsShowingInOverviewKey);
});
aura::Window* root_window = Shell::GetPrimaryRootWindow();
if (IsCarryOverCandidateForSplitView(mru_windows, 0u, root_window)) {
if (GetWindowStateType(mru_windows[0], clamshell_to_tablet) ==
WindowStateType::kPrimarySnapped) {
windows.emplace_back(
std::make_pair(mru_windows[0], WindowStateType::kPrimarySnapped));
if (IsCarryOverCandidateForSplitView(mru_windows, 1u, root_window) &&
GetWindowStateType(mru_windows[1], clamshell_to_tablet) ==
WindowStateType::kSecondarySnapped) {
windows.emplace_back(
std::make_pair(mru_windows[1], WindowStateType::kSecondarySnapped));
}
} else if (GetWindowStateType(mru_windows[0], clamshell_to_tablet) ==
WindowStateType::kSecondarySnapped) {
windows.emplace_back(
std::make_pair(mru_windows[0], WindowStateType::kSecondarySnapped));
if (IsCarryOverCandidateForSplitView(mru_windows, 1u, root_window) &&
GetWindowStateType(mru_windows[1], clamshell_to_tablet) ==
WindowStateType::kPrimarySnapped) {
windows.emplace_back(
std::make_pair(mru_windows[1], WindowStateType::kPrimarySnapped));
}
}
}
return windows;
}
int TabletModeWindowManager::CalculateCarryOverDividerPosition(
const WindowAndStateTypeList& windows_in_splitview,
bool clamshell_to_tablet) const {
aura::Window* left_window = nullptr;
aura::Window* right_window = nullptr;
for (auto& iter : windows_in_splitview) {
if (iter.second == WindowStateType::kPrimarySnapped)
left_window = iter.first;
else if (iter.second == WindowStateType::kSecondarySnapped)
right_window = iter.first;
}
if (!left_window && !right_window)
return -1;
const display::Display display =
display::Screen::Get()->GetDisplayNearestWindow(
left_window ? left_window : right_window);
gfx::Rect work_area = display.work_area();
gfx::Rect left_window_bounds =
left_window ? GetWindowBoundsInScreen(left_window, clamshell_to_tablet)
: gfx::Rect();
gfx::Rect right_window_bounds =
right_window ? GetWindowBoundsInScreen(right_window, clamshell_to_tablet)
: gfx::Rect();
const bool horizontal = IsLayoutHorizontal(display);
const bool primary = IsLayoutPrimary(display);
const int divider_padding =
(clamshell_to_tablet ? -1 : 1) * kSplitviewDividerShortSideLength / 2;
if (horizontal) {
if (primary) {
return left_window ? left_window_bounds.width() + divider_padding
: work_area.width() - right_window_bounds.width() -
divider_padding;
} else {
return left_window ? work_area.width() - left_window_bounds.width() -
divider_padding
: right_window_bounds.width() + divider_padding;
}
} else {
if (primary) {
return left_window ? left_window_bounds.height() + divider_padding
: work_area.height() - right_window_bounds.height() -
divider_padding;
} else {
return left_window ? work_area.height() - left_window_bounds.height() -
divider_padding
: right_window_bounds.height() + divider_padding;
}
}
}
void TabletModeWindowManager::ArrangeWindowsForTabletMode() {
ScopedSkipUserSessionBlockedCheck scoped_skip_user_session_blocked_check;
MruWindowTracker::WindowList activatable_windows =
Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(kAllDesks);
WindowAndStateTypeList windows_in_splitview =
GetCarryOverWindowsInSplitView(true);
const int divider_position = CalculateCarryOverDividerPosition(
windows_in_splitview, true);
if (windows_in_splitview.empty()) {
for (aura::Window* window : activatable_windows) {
TrackWindow(window, true);
}
return;
}
for (aura::Window* window : activatable_windows) {
bool snap = false;
for (auto& iter : windows_in_splitview) {
if (window == iter.first) {
snap = true;
break;
}
}
TrackWindow(window, true, snap,
false);
}
DoSplitViewTransition(
windows_in_splitview, divider_position,
WindowSnapActionSource::kSnapByClamshellTabletTransition);
}
void TabletModeWindowManager::ArrangeWindowsForClamshellMode(
WindowAndStateTypeList windows_in_splitview,
bool was_in_overview) {
const int divider_position = CalculateCarryOverDividerPosition(
windows_in_splitview, false);
while (window_state_map_.size()) {
aura::Window* window = window_state_map_.begin()->first;
ForgetWindow(window, false, was_in_overview);
}
DoSplitViewTransition(
windows_in_splitview, divider_position,
WindowSnapActionSource::kSnapByClamshellTabletTransition);
}
void TabletModeWindowManager::TrackWindow(aura::Window* window,
bool entering_tablet_mode,
bool snap,
bool animate_bounds_on_attach) {
if (base::Contains(windows_to_track_, window)) {
windows_to_track_.erase(window);
window->RemoveObserver(this);
}
if (!ShouldHandleWindow(window))
return;
DCHECK(!IsTrackingWindow(window));
window->AddObserver(this);
window_state_map_.emplace(
window, new TabletModeWindowState(window, weak_ptr_factory_.GetWeakPtr(),
snap, animate_bounds_on_attach,
entering_tablet_mode));
}
void TabletModeWindowManager::ForgetWindow(aura::Window* window,
bool destroyed,
bool was_in_overview) {
windows_to_track_.erase(window);
window->RemoveObserver(this);
WindowToState::iterator it = window_state_map_.find(window);
if (it == window_state_map_.end())
return;
if (destroyed) {
window_state_map_.erase(it);
} else {
it->second->LeaveTabletMode(WindowState::Get(it->first), was_in_overview);
DCHECK(!IsTrackingWindow(window));
}
}
bool TabletModeWindowManager::ShouldHandleWindow(aura::Window* window) {
DCHECK(window);
if (window->GetProperty(aura::client::kZOrderingKey) !=
ui::ZOrderLevel::kNormal) {
return false;
}
if (!WindowState::Get(window) ||
WindowState::Get(window)->allow_set_bounds_direct()) {
return false;
}
return window->GetType() == aura::client::WINDOW_TYPE_NORMAL;
}
void TabletModeWindowManager::AddWindowCreationObservers() {
DCHECK(observed_container_windows_.empty());
for (aura::Window* root : Shell::GetAllRootWindows()) {
for (auto* desk_container : desks_util::GetDesksContainers(root)) {
DCHECK(!base::Contains(observed_container_windows_, desk_container));
desk_container->AddObserver(this);
observed_container_windows_.insert(desk_container);
}
}
}
void TabletModeWindowManager::RemoveWindowCreationObservers() {
for (aura::Window* window : observed_container_windows_)
window->RemoveObserver(this);
observed_container_windows_.clear();
}
void TabletModeWindowManager::DisplayConfigurationChanged() {
RemoveWindowCreationObservers();
AddWindowCreationObservers();
UpdateDeskContainersBackdrops();
}
bool TabletModeWindowManager::IsContainerWindow(aura::Window* window) {
return base::Contains(observed_container_windows_, window);
}
}