#include "ash/wm/tablet_mode/tablet_mode_window_state.h"
#include <utility>
#include "ash/constants/ash_features.h"
#include "ash/drag_drop/tab_drag_drop_delegate.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_animation_types.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/screen_pinning_controller.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/tablet_mode/tablet_mode_window_manager.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/window_state_delegate.h"
#include "ash/wm/window_state_util.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "base/notreached.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/wm/window_util.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/base/mojom/window_show_state.mojom.h"
#include "ui/compositor/layer.h"
#include "ui/wm/core/ime_util_chromeos.h"
#include "ui/wm/core/window_util.h"
namespace ash {
namespace {
using ::chromeos::WindowStateType;
void SetWindowRestoreOverrides(
aura::Window* window,
const gfx::Rect& bounds_override,
ui::mojom::WindowShowState window_state_override) {
if (bounds_override.IsEmpty()) {
window->ClearProperty(kRestoreWindowStateTypeOverrideKey);
window->ClearProperty(kRestoreBoundsOverrideKey);
return;
}
window->SetProperty(kRestoreWindowStateTypeOverrideKey,
chromeos::ToWindowStateType(window_state_override));
window->SetProperty(kRestoreBoundsOverrideKey,
new gfx::Rect(bounds_override));
}
gfx::Size GetMaximumSizeOfWindow(WindowState* window_state) {
DCHECK(window_state->CanMaximize() || window_state->CanResize());
gfx::Size workspace_size =
screen_util::GetMaximizedWindowBoundsInParent(window_state->window())
.size();
gfx::Size size =
window_state->window()->delegate()
? window_state->window()->delegate()->GetMaximumSize().value_or(
gfx::Size())
: gfx::Size();
if (size.IsEmpty())
return workspace_size;
size.SetToMin(workspace_size);
return size;
}
gfx::Rect GetCenteredBounds(const gfx::Rect& bounds_in_parent,
WindowState* state_object) {
gfx::Rect work_area_in_parent =
screen_util::GetDisplayWorkAreaBoundsInParent(state_object->window());
work_area_in_parent.ClampToCenteredSize(bounds_in_parent.size());
return work_area_in_parent;
}
gfx::Rect GetRestoreBounds(WindowState* window_state) {
if (window_state->IsMinimized() || window_state->IsMaximized() ||
window_state->IsFullscreen()) {
gfx::Rect restore_bounds = window_state->GetRestoreBoundsInScreen();
if (!restore_bounds.IsEmpty())
return restore_bounds;
}
return window_state->window()->GetBoundsInScreen();
}
bool ShouldAnimateWindowForTransition(aura::Window* window) {
DCHECK(window);
if (WindowState::Get(window)->IsFloated())
return true;
MruWindowTracker::WindowList window_list =
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
auto* first_mru_window =
window_list.empty() ? nullptr : window_list.front().get();
if (first_mru_window && WindowState::Get(first_mru_window)->IsFloated()) {
auto* second_mru_window =
window_list.size() < 2u ? nullptr : window_list[1].get();
return window == second_mru_window;
}
return window == first_mru_window;
}
bool BoundsChangeIsFromVKAndAllowed(aura::Window* window) {
WindowStateType state_type = WindowState::Get(window)->GetStateType();
if (chromeos::IsNormalWindowStateType(state_type)) {
return window->GetProperty(wm::kVirtualKeyboardRestoreBoundsKey) != nullptr;
}
if (chromeos::IsSnappedWindowStateType(state_type)) {
return SplitViewController::Get(window)->BoundsChangeIsFromVKAndAllowed(
window);
}
return false;
}
}
TabletModeWindowState::TabletModeWindowState(
aura::Window* window,
base::WeakPtr<TabletModeWindowManager> creator,
bool snap,
bool animate_bounds_on_attach,
bool entering_tablet_mode)
: window_(window),
creator_(creator),
animate_bounds_on_attach_(animate_bounds_on_attach) {
WindowState* state = WindowState::Get(window);
current_state_type_ = state->GetStateType();
DCHECK(!snap || SplitViewController::Get(Shell::GetPrimaryRootWindow())
->CanKeepCurrentSnapRatio(window));
if (snap || state->IsFloated()) {
state_type_on_attach_ = current_state_type_;
} else if (const WindowState* source_state =
window_util::GetTabDraggingSourceWindowState(window);
source_state && source_state->IsSnapped()) {
state_type_on_attach_ = source_state->GetStateType();
} else {
state_type_on_attach_ = state->GetWindowTypeOnMaximizable();
}
WindowState::ScopedBoundsChangeAnimation bounds_animation(
window, entering_tablet_mode && !ShouldAnimateWindowForTransition(window)
? WindowState::BoundsChangeAnimationType::kAnimateZero
: WindowState::BoundsChangeAnimationType::kAnimate);
old_window_bounds_in_screen_ = window->GetBoundsInScreen();
old_state_.reset(
state->SetStateObject(std::unique_ptr<State>(this)).release());
}
TabletModeWindowState::~TabletModeWindowState() {
if (creator_) {
creator_->WindowStateDestroyed(window_);
}
}
void TabletModeWindowState::UpdateWindowPosition(
WindowState* window_state,
WindowState::BoundsChangeAnimationType animation_type) {
const gfx::Rect bounds_in_parent = GetBoundsInTabletMode(window_state);
if (bounds_in_parent == window_state->window()->GetTargetBounds())
return;
switch (animation_type) {
case WindowState::BoundsChangeAnimationType::kNone:
window_state->SetBoundsDirect(bounds_in_parent);
break;
case WindowState::BoundsChangeAnimationType::kCrossFade:
window_state->SetBoundsDirectCrossFade(bounds_in_parent);
break;
case WindowState::BoundsChangeAnimationType::kAnimate:
window_state->SetBoundsDirectAnimated(bounds_in_parent);
break;
case WindowState::BoundsChangeAnimationType::kCrossFadeFloat:
window_state->SetBoundsDirectCrossFade(bounds_in_parent,
true);
break;
case WindowState::BoundsChangeAnimationType::kCrossFadeUnfloat:
window_state->SetBoundsDirectCrossFade(bounds_in_parent,
false);
break;
case WindowState::BoundsChangeAnimationType::kAnimateZero:
NOTREACHED();
}
}
gfx::Rect TabletModeWindowState::GetBoundsInTabletMode(
WindowState* state_object) {
aura::Window* window = state_object->window();
if (state_object->IsFullscreen() || state_object->IsPinned())
return screen_util::GetFullscreenWindowBoundsInParent(window);
if (state_object->GetStateType() == WindowStateType::kPrimarySnapped) {
return SplitViewController::Get(Shell::GetPrimaryRootWindow())
->GetSnappedWindowBoundsInParent(
SnapPosition::kPrimary, window,
state_object->snap_ratio().value_or(chromeos::kDefaultSnapRatio));
}
if (state_object->GetStateType() == WindowStateType::kSecondarySnapped) {
return SplitViewController::Get(Shell::GetPrimaryRootWindow())
->GetSnappedWindowBoundsInParent(
SnapPosition::kSecondary, window,
state_object->snap_ratio().value_or(chromeos::kDefaultSnapRatio));
}
if (state_object->IsFloated()) {
return FloatController::GetFloatWindowTabletBounds(window);
}
gfx::Rect bounds_in_parent;
if ((state_object->CanMaximize() || state_object->CanResize()) &&
::wm::GetTransientParent(state_object->window()) == nullptr) {
bounds_in_parent.set_size(GetMaximumSizeOfWindow(state_object));
} else {
if (state_object->HasRestoreBounds())
bounds_in_parent = state_object->GetRestoreBoundsInParent();
else
bounds_in_parent = state_object->window()->bounds();
}
return GetCenteredBounds(bounds_in_parent, state_object);
}
void TabletModeWindowState::LeaveTabletMode(WindowState* window_state,
bool was_in_overview) {
WindowState::BoundsChangeAnimationType animation_type =
was_in_overview || window_state->IsSnapped() ||
ShouldAnimateWindowForTransition(window_state->window())
? WindowState::BoundsChangeAnimationType::kAnimate
: WindowState::BoundsChangeAnimationType::kNone;
if (old_state_->GetType() == window_state->GetStateType() &&
!window_state->IsNormalStateType() && !window_state->IsFloated()) {
animation_type = WindowState::BoundsChangeAnimationType::kNone;
}
WindowState::ScopedBoundsChangeAnimation bounds_animation(
window_state->window(), animation_type);
std::unique_ptr<WindowState::State> our_reference =
window_state->SetStateObject(std::move(old_state_));
}
void TabletModeWindowState::OnWMEvent(WindowState* window_state,
const WMEvent* event) {
if (ignore_wm_events_) {
return;
}
const chromeos::WindowStateType previous_state_type =
window_state->GetStateType();
switch (event->type()) {
case WM_EVENT_TOGGLE_FULLSCREEN:
ToggleFullScreen(window_state, window_state->delegate());
break;
case WM_EVENT_FULLSCREEN:
UpdateWindow(window_state, WindowStateType::kFullscreen,
true);
break;
case WM_EVENT_PIN:
if (!Shell::Get()->screen_pinning_controller()->IsPinned()) {
UpdateWindow(window_state, WindowStateType::kPinned,
true);
}
break;
case WM_EVENT_PIP:
NOTREACHED();
case WM_EVENT_LOCKED_FULLSCREEN:
if (!Shell::Get()->screen_pinning_controller()->IsPinned()) {
UpdateWindow(window_state, WindowStateType::kLockedFullscreen,
true);
}
break;
case WM_EVENT_TOGGLE_MAXIMIZE_CAPTION:
case WM_EVENT_TOGGLE_VERTICAL_MAXIMIZE:
case WM_EVENT_TOGGLE_HORIZONTAL_MAXIMIZE:
case WM_EVENT_TOGGLE_MAXIMIZE:
case WM_EVENT_MAXIMIZE:
UpdateWindow(window_state, window_state->GetWindowTypeOnMaximizable(),
true);
break;
case WM_EVENT_NORMAL: {
if (window_state->window()->GetProperty(aura::client::kIsRestoringKey)) {
DoRestore(window_state);
} else {
UpdateWindow(window_state, window_state->GetWindowTypeOnMaximizable(),
true);
}
break;
}
case WM_EVENT_RESTORE: {
DoRestore(window_state);
break;
}
case WM_EVENT_FLOAT:
if (!chromeos::wm::CanFloatWindow(window_state->window()))
break;
UpdateWindow(window_state, WindowStateType::kFloated,
true);
break;
case WM_EVENT_SNAP_PRIMARY:
case WM_EVENT_SNAP_SECONDARY:
CHECK(event->AsSnapEvent());
DoTabletSnap(window_state, event->type(),
event->AsSnapEvent()->snap_ratio(),
event->AsSnapEvent()->snap_action_source());
break;
case WM_EVENT_CYCLE_SNAP_PRIMARY:
CycleTabletSnap(window_state, SnapPosition::kPrimary);
break;
case WM_EVENT_CYCLE_SNAP_SECONDARY:
CycleTabletSnap(window_state, SnapPosition::kSecondary);
break;
case WM_EVENT_MINIMIZE:
UpdateWindow(window_state, WindowStateType::kMinimized,
true);
break;
case WM_EVENT_SHOW_INACTIVE:
break;
case WM_EVENT_SET_BOUNDS: {
gfx::Rect bounds_in_parent =
event->AsSetBoundsWMEvent()->requested_bounds_in_parent();
if (bounds_in_parent.IsEmpty())
break;
if (window_state->is_dragged() ||
TabDragDropDelegate::IsSourceWindowForDrag(window_state->window()) ||
BoundsChangeIsFromVKAndAllowed(window_state->window())) {
window_state->SetBoundsDirect(bounds_in_parent);
} else if (current_state_type_ == WindowStateType::kMaximized) {
window_state->SetRestoreBoundsInParent(bounds_in_parent);
} else if (current_state_type_ != WindowStateType::kMinimized &&
current_state_type_ != WindowStateType::kFullscreen &&
current_state_type_ != WindowStateType::kPinned &&
current_state_type_ != WindowStateType::kLockedFullscreen &&
current_state_type_ != WindowStateType::kPrimarySnapped &&
current_state_type_ != WindowStateType::kSecondarySnapped &&
current_state_type_ != WindowStateType::kFloated) {
bounds_in_parent = GetCenteredBounds(bounds_in_parent, window_state);
if (bounds_in_parent != window_state->window()->bounds()) {
if (window_state->window()->IsVisible() &&
event->AsSetBoundsWMEvent()->animate()) {
window_state->SetBoundsDirectAnimated(bounds_in_parent);
} else {
window_state->SetBoundsDirect(bounds_in_parent);
}
}
}
break;
}
case WM_EVENT_ADDED_TO_WORKSPACE:
UpdateWindow(window_state, AdjustStateForTabletMode(window_state),
true);
break;
case WM_EVENT_DISPLAY_METRICS_CHANGED:
if (current_state_type_ == WindowStateType::kMinimized) {
break;
}
const DisplayMetricsChangedWMEvent* display_event =
event->AsDisplayMetricsChangedWMEvent();
const bool display_bounds_changed =
display_event->display_bounds_changed();
const bool work_area_changed = display_event->work_area_changed();
if (display_bounds_changed || work_area_changed) {
UpdateBounds(window_state, previous_state_type,
work_area_changed);
}
break;
}
}
WindowStateType TabletModeWindowState::GetType() const {
return current_state_type_;
}
void TabletModeWindowState::AttachState(WindowState* window_state,
WindowState::State* previous_state) {
current_state_type_ = previous_state->GetType();
gfx::Rect restore_bounds = GetRestoreBounds(window_state);
if (!restore_bounds.IsEmpty()) {
SetWindowRestoreOverrides(window_state->window(), restore_bounds,
window_state->GetShowState());
}
if (current_state_type_ != WindowStateType::kMaximized &&
current_state_type_ != WindowStateType::kMinimized &&
current_state_type_ != WindowStateType::kFullscreen &&
current_state_type_ != WindowStateType::kFloated &&
current_state_type_ != WindowStateType::kPinned &&
current_state_type_ != WindowStateType::kLockedFullscreen) {
UpdateWindow(window_state, state_type_on_attach_,
animate_bounds_on_attach_);
}
}
void TabletModeWindowState::DetachState(WindowState* window_state) {
SetWindowRestoreOverrides(window_state->window(), gfx::Rect(),
ui::mojom::WindowShowState::kNormal);
}
void TabletModeWindowState::UpdateWindow(WindowState* window_state,
WindowStateType target_state,
bool animated) {
aura::Window* window = window_state->window();
DCHECK(target_state == WindowStateType::kMinimized ||
target_state == WindowStateType::kMaximized ||
target_state == WindowStateType::kPinned ||
target_state == WindowStateType::kLockedFullscreen ||
(target_state == WindowStateType::kNormal &&
(!window_state->CanMaximize() || !!wm::GetTransientParent(window))) ||
target_state == WindowStateType::kFullscreen ||
target_state == WindowStateType::kPrimarySnapped ||
target_state == WindowStateType::kSecondarySnapped ||
target_state == WindowStateType::kFloated);
if (current_state_type_ == target_state) {
if (target_state == WindowStateType::kMinimized)
return;
UpdateBounds(window_state, current_state_type_, animated);
return;
}
const WindowStateType old_state_type = current_state_type_;
current_state_type_ = target_state;
window_state->UpdateWindowPropertiesFromStateType();
window_state->NotifyPreStateTypeChange(old_state_type);
if (target_state == WindowStateType::kFloated)
Shell::Get()->float_controller()->FloatForTablet(window, old_state_type);
if (old_state_type == WindowStateType::kFloated)
Shell::Get()->float_controller()->UnfloatImpl(window);
if (target_state == WindowStateType::kMinimized) {
wm::SetWindowVisibilityAnimationType(
window, WINDOW_VISIBILITY_ANIMATION_TYPE_MINIMIZE);
window->Hide();
if (window_state->IsActive())
window_state->Deactivate();
} else {
UpdateBounds(window_state, old_state_type, animated);
}
if ((window->layer()->GetTargetVisibility() ||
old_state_type == WindowStateType::kMinimized) &&
!window->layer()->visible()) {
window->Show();
}
window_state->NotifyPostStateTypeChange(old_state_type);
if (chromeos::IsPinnedWindowStateType(old_state_type) ||
chromeos::IsPinnedWindowStateType(target_state)) {
Shell::Get()->screen_pinning_controller()->SetPinnedWindow(window);
if (window_state->delegate()) {
window_state->delegate()->ToggleLockedFullscreen(window_state);
}
}
}
WindowStateType TabletModeWindowState::AdjustStateForTabletMode(
WindowState* window_state) {
auto current_state_type = window_state->GetStateType();
if (chromeos::IsSnappedWindowStateType(current_state_type) ||
chromeos::IsPinnedWindowStateType(current_state_type) ||
current_state_type == chromeos::WindowStateType::kFloated ||
current_state_type == chromeos::WindowStateType::kFullscreen) {
return current_state_type;
}
return window_state->GetWindowTypeOnMaximizable();
}
void TabletModeWindowState::UpdateBounds(
WindowState* window_state,
chromeos::WindowStateType previous_state,
bool animated) {
if (current_state_type_ == WindowStateType::kMinimized)
return;
gfx::Rect bounds_in_parent = GetBoundsInTabletMode(window_state);
if (!bounds_in_parent.IsEmpty() &&
bounds_in_parent != window_state->window()->bounds()) {
if (!window_state->window()->IsVisible() || !animated) {
window_state->SetBoundsDirect(bounds_in_parent);
} else {
if (window_state->bounds_animation_type() ==
WindowState::BoundsChangeAnimationType::kAnimateZero) {
window_state->SetBoundsDirectAnimated(
bounds_in_parent, base::Seconds(1), gfx::Tween::ZERO);
return;
}
const bool previous_floated = previous_state == WindowStateType::kFloated;
const bool current_floated = window_state->IsFloated();
if (previous_floated ^ current_floated) {
window_state->SetBoundsDirectCrossFade(bounds_in_parent,
current_floated);
return;
}
if (window_state->IsMaximized()) {
window_state->SetBoundsDirectCrossFade(bounds_in_parent);
} else {
window_state->SetBoundsDirectAnimated(bounds_in_parent);
}
}
}
}
void TabletModeWindowState::CycleTabletSnap(WindowState* window_state,
SnapPosition snap_position) {
aura::Window* window = window_state->window();
SplitViewController* split_view_controller = SplitViewController::Get(window);
if (window == split_view_controller->GetSnappedWindow(snap_position)) {
UpdateWindow(window_state, window_state->GetWindowTypeOnMaximizable(),
true);
window_state->ReadOutWindowCycleSnapAction(
IDS_WM_RESTORE_SNAPPED_WINDOW_ON_SHORTCUT);
return;
}
if (split_view_controller->CanSnapWindow(window,
chromeos::kDefaultSnapRatio)) {
split_view_controller->SnapWindow(
window, snap_position, WindowSnapActionSource::kKeyboardShortcutToSnap);
window_state->ReadOutWindowCycleSnapAction(
snap_position == SnapPosition::kPrimary
? IDS_WM_SNAP_WINDOW_TO_LEFT_ON_SHORTCUT
: IDS_WM_SNAP_WINDOW_TO_RIGHT_ON_SHORTCUT);
return;
}
ShowAppCannotSnapToast();
}
void TabletModeWindowState::DoTabletSnap(
WindowState* window_state,
WMEventType snap_event_type,
float snap_ratio,
WindowSnapActionSource snap_action_source) {
DCHECK(snap_event_type == WM_EVENT_SNAP_PRIMARY ||
snap_event_type == WM_EVENT_SNAP_SECONDARY);
aura::Window* window = window_state->window();
SplitViewController* split_view_controller = SplitViewController::Get(window);
if (!split_view_controller->CanSnapWindow(window, snap_ratio)) {
ShowAppCannotSnapToast();
return;
}
window_state->SetBoundsChangedByUser(true);
chromeos::WindowStateType new_state_type =
snap_event_type == WM_EVENT_SNAP_PRIMARY
? WindowStateType::kPrimarySnapped
: WindowStateType::kSecondarySnapped;
window_state->RecordWindowSnapActionSource(snap_action_source);
split_view_controller->OnSnapEvent(window, snap_event_type,
snap_action_source,
WindowSnapGrouping::kGrouped);
UpdateWindow(window_state, new_state_type, false);
}
void TabletModeWindowState::DoRestore(WindowState* window_state) {
WindowStateType restore_state = window_state->GetRestoreWindowState();
if (chromeos::IsSnappedWindowStateType(restore_state)) {
DoTabletSnap(window_state,
restore_state == WindowStateType::kPrimarySnapped
? WM_EVENT_SNAP_PRIMARY
: WM_EVENT_SNAP_SECONDARY,
chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowStateRestore);
return;
}
UpdateWindow(window_state, restore_state, true);
}
}