| // 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/window_state.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "ash/constants/ash_constants.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/focus_cycler.h" |
| #include "ash/metrics/pip_uma.h" |
| #include "ash/public/cpp/app_types_util.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/collision_detection/collision_detection_utils.h" |
| #include "ash/wm/default_state.h" |
| #include "ash/wm/desks/persistent_desks_bar_controller.h" |
| #include "ash/wm/pip/pip_positioner.h" |
| #include "ash/wm/tablet_mode/tablet_mode_controller.h" |
| #include "ash/wm/window_animations.h" |
| #include "ash/wm/window_positioning_utils.h" |
| #include "ash/wm/window_properties.h" |
| #include "ash/wm/window_restore/window_restore_controller.h" |
| #include "ash/wm/window_state_delegate.h" |
| #include "ash/wm/window_state_observer.h" |
| #include "ash/wm/window_util.h" |
| #include "ash/wm/wm_event.h" |
| #include "base/auto_reset.h" |
| #include "base/containers/adapters.h" |
| #include "base/containers/fixed_flat_map.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "chromeos/ui/base/window_properties.h" |
| #include "chromeos/ui/base/window_state_type.h" |
| #include "components/app_restore/features.h" |
| #include "components/app_restore/window_properties.h" |
| #include "ui/aura/client/aura_constants.h" |
| #include "ui/aura/layout_manager.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_delegate.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_tree_owner.h" |
| #include "ui/compositor/paint_recorder.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/painter.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_delegate.h" |
| #include "ui/wm/core/coordinate_conversion.h" |
| #include "ui/wm/core/ime_util_chromeos.h" |
| #include "ui/wm/core/window_util.h" |
| |
| namespace ash { |
| namespace { |
| |
| using ::chromeos::kHideShelfWhenFullscreenKey; |
| using ::chromeos::kImmersiveIsActive; |
| using ::chromeos::kWindowManagerManagesOpacityKey; |
| using ::chromeos::WindowStateType; |
| |
| // This defines the map from different window states to their restore layers. |
| // The assumption is that a window state with higher restore layer number can |
| // restore back to a window state with lower restore layer number, but not the |
| // other way around. For example, a window whose window state is kMinimized can |
| // restore to kMaximized window state, but kMaximized window state can not |
| // restore back to kMinimized window state. Please see |
| // go/window-state-restore-history for details. |
| // Note the map does not contain all WindowStateTypes, for the ones that's not |
| // in the map, they can't be put into the window state restore history stack, |
| // and restore from those state will simply go back to kNormal window state. |
| constexpr auto kWindowStateRestoreHistoryLayerMap = |
| base::MakeFixedFlatMap<WindowStateType, int>({ |
| {WindowStateType::kNormal, 0}, |
| {WindowStateType::kDefault, 0}, |
| {WindowStateType::kPrimarySnapped, 1}, |
| {WindowStateType::kSecondarySnapped, 1}, |
| {WindowStateType::kMaximized, 2}, |
| {WindowStateType::kFullscreen, 3}, |
| {WindowStateType::kPip, 4}, |
| {WindowStateType::kMinimized, 4}, |
| }); |
| |
| bool IsTabletModeEnabled() { |
| return Shell::Get()->tablet_mode_controller()->InTabletMode(); |
| } |
| |
| bool IsToplevelContainer(aura::Window* window) { |
| DCHECK(window); |
| int container_id = window->GetId(); |
| // ArcVirtualKeyboard is implemented as a exo window which requires |
| // WindowState to manage its state. |
| return IsActivatableShellWindowId(container_id) || |
| container_id == kShellWindowId_ArcVirtualKeyboardContainer; |
| } |
| |
| // ARC windows will not be in a top level container until they are associated |
| // with a task. We still want a WindowState created for these windows and their |
| // transient children as they will be moved to a top level container soon. |
| bool IsTemporarilyHiddenForFullrestore(aura::Window* window) { |
| if (window->GetProperty(app_restore::kParentToHiddenContainerKey)) |
| return true; |
| |
| auto* transient_parent = wm::GetTransientParent(window); |
| return transient_parent && transient_parent->GetProperty( |
| app_restore::kParentToHiddenContainerKey); |
| } |
| |
| // A tentative class to set the bounds on the window. |
| // TODO(oshima): Once all logic is cleaned up, move this to the real layout |
| // manager with proper friendship. |
| class BoundsSetter : public aura::LayoutManager { |
| public: |
| BoundsSetter() = default; |
| |
| BoundsSetter(const BoundsSetter&) = delete; |
| BoundsSetter& operator=(const BoundsSetter&) = delete; |
| |
| ~BoundsSetter() override = default; |
| |
| // aura::LayoutManager overrides: |
| void OnWindowResized() override {} |
| void OnWindowAddedToLayout(aura::Window* child) override {} |
| void OnWillRemoveWindowFromLayout(aura::Window* child) override {} |
| void OnWindowRemovedFromLayout(aura::Window* child) override {} |
| void OnChildWindowVisibilityChanged(aura::Window* child, |
| bool visible) override {} |
| void SetChildBounds(aura::Window* child, |
| const gfx::Rect& requested_bounds) override {} |
| |
| void SetBounds(aura::Window* window, const gfx::Rect& bounds) { |
| SetChildBoundsDirect(window, bounds); |
| } |
| }; |
| |
| WMEventType WMEventTypeFromShowState(ui::WindowShowState requested_show_state) { |
| switch (requested_show_state) { |
| case ui::SHOW_STATE_DEFAULT: |
| case ui::SHOW_STATE_NORMAL: |
| return WM_EVENT_NORMAL; |
| case ui::SHOW_STATE_MINIMIZED: |
| return WM_EVENT_MINIMIZE; |
| case ui::SHOW_STATE_MAXIMIZED: |
| return WM_EVENT_MAXIMIZE; |
| case ui::SHOW_STATE_FULLSCREEN: |
| return WM_EVENT_FULLSCREEN; |
| case ui::SHOW_STATE_INACTIVE: |
| return WM_EVENT_SHOW_INACTIVE; |
| |
| case ui::SHOW_STATE_END: |
| NOTREACHED() << "No WMEvent defined for the show state:" |
| << requested_show_state; |
| } |
| return WM_EVENT_NORMAL; |
| } |
| |
| float GetCurrentSnapRatio(aura::Window* window) { |
| gfx::Rect maximized_bounds = |
| screen_util::GetMaximizedWindowBoundsInParent(window); |
| if (SplitViewController::IsLayoutHorizontal(window)) { |
| return static_cast<float>(window->GetTargetBounds().width()) / |
| static_cast<float>(maximized_bounds.width()); |
| } |
| return static_cast<float>(window->GetTargetBounds().height()) / |
| static_cast<float>(maximized_bounds.height()); |
| } |
| |
| // Move all transient children to |dst_root|, including the ones in the child |
| // windows and transient children of the transient children. |
| void MoveAllTransientChildrenToNewRoot(aura::Window* window) { |
| aura::Window* dst_root = window->GetRootWindow(); |
| for (aura::Window* transient_child : ::wm::GetTransientChildren(window)) { |
| if (!transient_child->parent()) |
| continue; |
| const int container_id = transient_child->parent()->GetId(); |
| DCHECK_GE(container_id, 0); |
| aura::Window* container = dst_root->GetChildById(container_id); |
| if (container->Contains(transient_child)) |
| continue; |
| gfx::Rect child_bounds = transient_child->bounds(); |
| ::wm::ConvertRectToScreen(dst_root, &child_bounds); |
| container->AddChild(transient_child); |
| transient_child->SetBoundsInScreen( |
| child_bounds, |
| display::Screen::GetScreen()->GetDisplayNearestWindow(window)); |
| |
| // Transient children may have transient children. |
| MoveAllTransientChildrenToNewRoot(transient_child); |
| } |
| // Move transient children of the child windows if any. |
| for (aura::Window* child : window->children()) |
| MoveAllTransientChildrenToNewRoot(child); |
| } |
| |
| void ReportAshPipEvents(AshPipEvents event) { |
| UMA_HISTOGRAM_ENUMERATION(kAshPipEventsHistogramName, event); |
| } |
| |
| void ReportAshPipAndroidPipUseTime(base::TimeDelta duration) { |
| UMA_HISTOGRAM_CUSTOM_TIMES(kAshPipAndroidPipUseTimeHistogramName, duration, |
| base::Seconds(1), base::Hours(10), 50); |
| } |
| |
| // Notifies the window restore controller to write to file. |
| void SaveWindowForWindowRestore(WindowState* window_state) { |
| if (!full_restore::features::IsFullRestoreEnabled()) |
| return; |
| |
| auto* controller = WindowRestoreController::Get(); |
| if (controller) |
| controller->SaveWindow(window_state); |
| } |
| |
| } // namespace |
| |
| constexpr base::TimeDelta WindowState::kBoundsChangeSlideDuration; |
| |
| WindowState::ScopedBoundsChangeAnimation::ScopedBoundsChangeAnimation( |
| aura::Window* window, |
| BoundsChangeAnimationType bounds_animation_type) |
| : window_(window) { |
| window_->AddObserver(this); |
| previous_bounds_animation_type_ = |
| WindowState::Get(window_)->bounds_animation_type_; |
| WindowState::Get(window_)->bounds_animation_type_ = bounds_animation_type; |
| } |
| |
| WindowState::ScopedBoundsChangeAnimation::~ScopedBoundsChangeAnimation() { |
| if (window_) { |
| WindowState::Get(window_)->bounds_animation_type_ = |
| previous_bounds_animation_type_; |
| window_->RemoveObserver(this); |
| window_ = nullptr; |
| } |
| } |
| |
| void WindowState::ScopedBoundsChangeAnimation::OnWindowDestroying( |
| aura::Window* window) { |
| window_->RemoveObserver(this); |
| window_ = nullptr; |
| } |
| |
| WindowState::~WindowState() { |
| // WindowState is registered as an owned property of |window_|, and window |
| // unregisters all of its observers in its d'tor before destroying its |
| // properties. As a result, window_->RemoveObserver() doesn't need to (and |
| // shouldn't) be called here. |
| } |
| |
| bool WindowState::HasDelegate() const { |
| return !!delegate_; |
| } |
| |
| void WindowState::SetDelegate(std::unique_ptr<WindowStateDelegate> delegate) { |
| DCHECK((!delegate_.get() && !!delegate.get()) || |
| (!!delegate_.get() && !delegate.get())); |
| delegate_ = std::move(delegate); |
| } |
| |
| WindowStateType WindowState::GetStateType() const { |
| return current_state_->GetType(); |
| } |
| |
| bool WindowState::IsMinimized() const { |
| return IsMinimizedWindowStateType(GetStateType()); |
| } |
| |
| bool WindowState::IsMaximized() const { |
| return GetStateType() == WindowStateType::kMaximized; |
| } |
| |
| bool WindowState::IsFullscreen() const { |
| return GetStateType() == WindowStateType::kFullscreen; |
| } |
| |
| bool WindowState::IsMaximizedOrFullscreenOrPinned() const { |
| return IsMaximizedOrFullscreenOrPinnedWindowStateType(GetStateType()); |
| } |
| |
| bool WindowState::IsSnapped() const { |
| return GetStateType() == WindowStateType::kPrimarySnapped || |
| GetStateType() == WindowStateType::kSecondarySnapped; |
| } |
| |
| bool WindowState::IsPinned() const { |
| return GetStateType() == WindowStateType::kPinned || |
| GetStateType() == WindowStateType::kTrustedPinned; |
| } |
| |
| bool WindowState::IsTrustedPinned() const { |
| return GetStateType() == WindowStateType::kTrustedPinned; |
| } |
| |
| bool WindowState::IsPip() const { |
| return GetStateType() == WindowStateType::kPip; |
| } |
| |
| bool WindowState::IsNormalStateType() const { |
| return IsNormalWindowStateType(GetStateType()); |
| } |
| |
| bool WindowState::IsNormalOrSnapped() const { |
| return IsNormalStateType() || IsSnapped(); |
| } |
| |
| bool WindowState::IsActive() const { |
| return wm::IsActiveWindow(window_); |
| } |
| |
| bool WindowState::IsUserPositionable() const { |
| return window_util::IsWindowUserPositionable(window_); |
| } |
| |
| bool WindowState::HasMaximumWidthOrHeight() const { |
| if (!window_->delegate()) |
| return false; |
| |
| const gfx::Size max_size = window_->delegate()->GetMaximumSize(); |
| return max_size.width() || max_size.height(); |
| } |
| |
| bool WindowState::CanMaximize() const { |
| bool can_maximize = (window_->GetProperty(aura::client::kResizeBehaviorKey) & |
| aura::client::kResizeBehaviorCanMaximize) != 0; |
| #if DCHECK_IS_ON() |
| if (window_->delegate() && can_maximize) { |
| const gfx::Size max_size = window_->delegate()->GetMaximumSize(); |
| DCHECK(max_size.IsEmpty() || max_size.width() > kAllowMaximizeThreshold || |
| max_size.height() > kAllowMaximizeThreshold); |
| } |
| #endif |
| return can_maximize; |
| } |
| |
| bool WindowState::CanMinimize() const { |
| return (window_->GetProperty(aura::client::kResizeBehaviorKey) & |
| aura::client::kResizeBehaviorCanMinimize) != 0; |
| } |
| |
| bool WindowState::CanResize() const { |
| return (window_->GetProperty(aura::client::kResizeBehaviorKey) & |
| aura::client::kResizeBehaviorCanResize) != 0; |
| } |
| |
| bool WindowState::CanActivate() const { |
| return wm::CanActivateWindow(window_); |
| } |
| |
| bool WindowState::CanSnap() const { |
| return !IsPip() && CanResize() && CanMaximize(); |
| } |
| |
| bool WindowState::HasRestoreBounds() const { |
| gfx::Rect* bounds = window_->GetProperty(aura::client::kRestoreBoundsKey); |
| return bounds != nullptr && !bounds->IsEmpty(); |
| } |
| |
| void WindowState::Maximize() { |
| ::wm::SetWindowState(window_, ui::SHOW_STATE_MAXIMIZED); |
| } |
| |
| void WindowState::Minimize() { |
| ::wm::SetWindowState(window_, ui::SHOW_STATE_MINIMIZED); |
| } |
| |
| void WindowState::Unminimize() { |
| ::wm::Unminimize(window_); |
| } |
| |
| void WindowState::Activate() { |
| wm::ActivateWindow(window_); |
| } |
| |
| void WindowState::Deactivate() { |
| wm::DeactivateWindow(window_); |
| } |
| |
| void WindowState::Restore() { |
| const WMEvent event(WM_EVENT_RESTORE); |
| OnWMEvent(&event); |
| } |
| |
| void WindowState::DisableZOrdering(aura::Window* window_on_top) { |
| ui::ZOrderLevel z_order = GetZOrdering(); |
| if (z_order != ui::ZOrderLevel::kNormal && !IsPip()) { |
| // |window_| is hidden first to avoid canceling fullscreen mode when it is |
| // no longer always on top and gets added to default container. This avoids |
| // sending redundant OnFullscreenStateChanged to the layout manager. The |
| // |window_| visibility is restored after it no longer obscures the |
| // |window_on_top|. |
| bool visible = window_->IsVisible(); |
| if (visible) |
| window_->Hide(); |
| window_->SetProperty(aura::client::kZOrderingKey, ui::ZOrderLevel::kNormal); |
| // Technically it is possible that a |window_| could make itself |
| // always_on_top really quickly. This is probably not a realistic case but |
| // check if the two windows are in the same container just in case. |
| if (window_on_top && window_on_top->parent() == window_->parent()) |
| window_->parent()->StackChildAbove(window_on_top, window_); |
| if (visible) |
| window_->Show(); |
| cached_z_order_ = z_order; |
| } |
| } |
| |
| void WindowState::RestoreZOrdering() { |
| if (cached_z_order_ != ui::ZOrderLevel::kNormal) { |
| window_->SetProperty(aura::client::kZOrderingKey, cached_z_order_); |
| cached_z_order_ = ui::ZOrderLevel::kNormal; |
| } |
| } |
| |
| void WindowState::OnWMEvent(const WMEvent* event) { |
| current_state_->OnWMEvent(this, event); |
| |
| UpdateSnapRatio(event); |
| |
| PersistentDesksBarController* bar_controller = |
| Shell::Get()->persistent_desks_bar_controller(); |
| if (bar_controller) |
| bar_controller->UpdateBarOnWindowStateChanges(window_); |
| } |
| |
| void WindowState::SaveCurrentBoundsForRestore() { |
| gfx::Rect bounds_in_screen = window_->GetTargetBounds(); |
| ::wm::ConvertRectToScreen(window_->parent(), &bounds_in_screen); |
| SetRestoreBoundsInScreen(bounds_in_screen); |
| } |
| |
| gfx::Rect WindowState::GetRestoreBoundsInScreen() const { |
| gfx::Rect* restore_bounds = |
| window_->GetProperty(aura::client::kRestoreBoundsKey); |
| return restore_bounds ? *restore_bounds : gfx::Rect(); |
| } |
| |
| gfx::Rect WindowState::GetRestoreBoundsInParent() const { |
| gfx::Rect result = GetRestoreBoundsInScreen(); |
| ::wm::ConvertRectFromScreen(window_->parent(), &result); |
| return result; |
| } |
| |
| void WindowState::SetRestoreBoundsInScreen(const gfx::Rect& bounds) { |
| window_->SetProperty(aura::client::kRestoreBoundsKey, bounds); |
| } |
| |
| void WindowState::SetRestoreBoundsInParent(const gfx::Rect& bounds) { |
| gfx::Rect bounds_in_screen = bounds; |
| ::wm::ConvertRectToScreen(window_->parent(), &bounds_in_screen); |
| SetRestoreBoundsInScreen(bounds_in_screen); |
| } |
| |
| void WindowState::ClearRestoreBounds() { |
| window_->ClearProperty(aura::client::kRestoreBoundsKey); |
| window_->ClearProperty(::wm::kVirtualKeyboardRestoreBoundsKey); |
| } |
| |
| bool WindowState::VerticallyShrinkWindow(const gfx::Rect& work_area) { |
| if (!HasRestoreBounds()) |
| return false; |
| // Check if window is not work area vertical maximized. |
| gfx::Rect bounds = window_->bounds(); |
| if (bounds.height() != work_area.height() || bounds.y() != work_area.y()) |
| return false; |
| |
| gfx::Rect restore_bounds = GetRestoreBoundsInParent(); |
| gfx::Rect new_bounds = restore_bounds; |
| |
| // Shrink from work area maximized window. |
| if (bounds == work_area) { |
| new_bounds = gfx::Rect(work_area.x(), restore_bounds.y(), work_area.width(), |
| restore_bounds.height()); |
| // Restore bounds is not cleared here in case a 2nd shrink is called next. |
| } else { |
| ClearRestoreBounds(); |
| } |
| |
| SetBoundsDirectCrossFade(new_bounds); |
| return true; |
| } |
| |
| bool WindowState::HorizontallyShrinkWindow(const gfx::Rect& work_area) { |
| if (!HasRestoreBounds()) |
| return false; |
| // Check if window is not work area horizontal maximized. |
| gfx::Rect bounds = window_->bounds(); |
| if (bounds.width() != work_area.width() || bounds.x() != work_area.x()) |
| return false; |
| |
| gfx::Rect restore_bounds = GetRestoreBoundsInParent(); |
| gfx::Rect new_bounds = restore_bounds; |
| |
| // Shrink from work area maximized window. |
| if (bounds == work_area) { |
| new_bounds = gfx::Rect(restore_bounds.x(), work_area.y(), |
| restore_bounds.width(), work_area.height()); |
| } else { |
| ClearRestoreBounds(); |
| } |
| SetBoundsDirectCrossFade(new_bounds); |
| return true; |
| } |
| |
| std::unique_ptr<WindowState::State> WindowState::SetStateObject( |
| std::unique_ptr<WindowState::State> new_state) { |
| current_state_->DetachState(this); |
| std::unique_ptr<WindowState::State> old_object = std::move(current_state_); |
| current_state_ = std::move(new_state); |
| current_state_->AttachState(this, old_object.get()); |
| return old_object; |
| } |
| |
| void WindowState::UpdateSnapRatio(const WMEvent* event) { |
| if (!IsSnapped()) { |
| snap_ratio_.reset(); |
| return; |
| } |
| |
| const WMEventType type = event->type(); |
| // Initializes |snap_ratio_| whenever |event| is snapping event. |
| if (type == WM_EVENT_SNAP_PRIMARY || type == WM_EVENT_SNAP_SECONDARY || |
| type == WM_EVENT_CYCLE_SNAP_PRIMARY || |
| type == WM_EVENT_CYCLE_SNAP_SECONDARY) { |
| // Since |UpdateSnapRatio()| is called post WMEvent taking effect, |
| // |window_|'s bounds is in a correct state for ratio update. |
| snap_ratio_ = absl::make_optional(GetCurrentSnapRatio(window_)); |
| return; |
| } |
| |
| // |snap_ratio_| under snapped state may change due to bounds event. |
| if (event->IsBoundsEvent()) { |
| snap_ratio_ = absl::make_optional(GetCurrentSnapRatio(window_)); |
| } |
| } |
| |
| void WindowState::SetPreAutoManageWindowBounds(const gfx::Rect& bounds) { |
| pre_auto_manage_window_bounds_ = absl::make_optional(bounds); |
| } |
| |
| void WindowState::SetPreAddedToWorkspaceWindowBounds(const gfx::Rect& bounds) { |
| pre_added_to_workspace_window_bounds_ = absl::make_optional(bounds); |
| } |
| |
| void WindowState::SetPersistentWindowInfoOfDisplayRemoval( |
| const PersistentWindowInfo& info) { |
| persistent_window_info_of_display_removal_ = absl::make_optional(info); |
| } |
| |
| void WindowState::ResetPersistentWindowInfoOfDisplayRemoval() { |
| persistent_window_info_of_display_removal_.reset(); |
| } |
| |
| void WindowState::SetPersistentWindowInfoOfScreenRotation( |
| const PersistentWindowInfo& info) { |
| persistent_window_info_of_screen_rotation_ = absl::make_optional(info); |
| } |
| |
| void WindowState::AddObserver(WindowStateObserver* observer) { |
| observer_list_.AddObserver(observer); |
| } |
| |
| void WindowState::RemoveObserver(WindowStateObserver* observer) { |
| observer_list_.RemoveObserver(observer); |
| } |
| |
| bool WindowState::GetHideShelfWhenFullscreen() const { |
| return window_->GetProperty(kHideShelfWhenFullscreenKey); |
| } |
| |
| void WindowState::SetHideShelfWhenFullscreen(bool value) { |
| base::AutoReset<bool> resetter(&ignore_property_change_, true); |
| window_->SetProperty(kHideShelfWhenFullscreenKey, value); |
| } |
| |
| bool WindowState::GetWindowPositionManaged() const { |
| return window_->GetProperty(kWindowPositionManagedTypeKey); |
| } |
| |
| void WindowState::SetWindowPositionManaged(bool managed) { |
| window_->SetProperty(kWindowPositionManagedTypeKey, managed); |
| } |
| |
| bool WindowState::CanConsumeSystemKeys() const { |
| return window_->GetProperty(kCanConsumeSystemKeysKey); |
| } |
| |
| void WindowState::SetCanConsumeSystemKeys(bool can_consume_system_keys) { |
| window_->SetProperty(kCanConsumeSystemKeysKey, can_consume_system_keys); |
| } |
| |
| bool WindowState::IsInImmersiveFullscreen() const { |
| return window_->GetProperty(kImmersiveIsActive); |
| } |
| |
| void WindowState::set_bounds_changed_by_user(bool bounds_changed_by_user) { |
| bounds_changed_by_user_ = bounds_changed_by_user; |
| if (bounds_changed_by_user) { |
| pre_auto_manage_window_bounds_.reset(); |
| pre_added_to_workspace_window_bounds_.reset(); |
| persistent_window_info_of_display_removal_.reset(); |
| persistent_window_info_of_screen_rotation_.reset(); |
| } |
| } |
| |
| void WindowState::OnDragStarted(int window_component) { |
| DCHECK(drag_details_); |
| if (delegate_) |
| delegate_->OnDragStarted(window_component); |
| } |
| |
| void WindowState::OnCompleteDrag(const gfx::PointF& location) { |
| DCHECK(drag_details_); |
| if (delegate_) |
| delegate_->OnDragFinished(/*canceled=*/false, location); |
| SaveWindowForWindowRestore(this); |
| } |
| |
| void WindowState::OnRevertDrag(const gfx::PointF& location) { |
| DCHECK(drag_details_); |
| if (delegate_) |
| delegate_->OnDragFinished(/*canceled=*/true, location); |
| } |
| |
| void WindowState::OnActivationLost() { |
| if (IsPip()) { |
| views::Widget::GetWidgetForNativeWindow(window()) |
| ->widget_delegate() |
| ->SetCanActivate(false); |
| } |
| } |
| |
| display::Display WindowState::GetDisplay() const { |
| return display::Screen::GetScreen()->GetDisplayNearestWindow(window_); |
| } |
| |
| WindowStateType WindowState::GetRestoreWindowState() const { |
| WindowStateType restore_state = |
| window_state_restore_history_.empty() || |
| window_state_restore_history_.back() == WindowStateType::kDefault |
| ? WindowStateType::kNormal |
| : window_state_restore_history_.back(); |
| |
| // Different with the restore behaviors in clamshell mode, a window can not be |
| // restored to kNormal window state if it's a maximize-able window. |
| // We should still be able to restore a fullscreen/minimized/snapped window to |
| // kMaximized window state for a maximize-able window, and also should be able |
| // to support restoring a fullscreen/minimized/maximized window to snapped |
| // window states. |
| if (IsTabletModeEnabled() && restore_state == WindowStateType::kNormal) |
| restore_state = GetMaximizedOrCenteredWindowType(); |
| |
| return restore_state; |
| } |
| |
| void WindowState::CreateDragDetails(const gfx::PointF& point_in_parent, |
| int window_component, |
| ::wm::WindowMoveSource source) { |
| drag_details_ = std::make_unique<DragDetails>(window_, point_in_parent, |
| window_component, source); |
| } |
| |
| void WindowState::DeleteDragDetails() { |
| drag_details_.reset(); |
| } |
| |
| void WindowState::SetAndClearRestoreBounds() { |
| DCHECK(HasRestoreBounds()); |
| SetBoundsInScreen(GetRestoreBoundsInScreen()); |
| ClearRestoreBounds(); |
| } |
| |
| WindowState::WindowState(aura::Window* window) |
| : window_(window), |
| bounds_changed_by_user_(false), |
| can_consume_system_keys_(false), |
| unminimize_to_restore_bounds_(false), |
| hide_shelf_when_fullscreen_(true), |
| autohide_shelf_when_maximized_or_fullscreen_(false), |
| cached_z_order_(ui::ZOrderLevel::kNormal), |
| ignore_property_change_(false), |
| current_state_( |
| new DefaultState(chromeos::ToWindowStateType(GetShowState()))) { |
| window_->AddObserver(this); |
| UpdateWindowPropertiesFromStateType(); |
| OnPrePipStateChange(WindowStateType::kDefault); |
| } |
| |
| ui::ZOrderLevel WindowState::GetZOrdering() const { |
| return window_->GetProperty(aura::client::kZOrderingKey); |
| } |
| |
| ui::WindowShowState WindowState::GetShowState() const { |
| return window_->GetProperty(aura::client::kShowStateKey); |
| } |
| |
| void WindowState::SetBoundsInScreen(const gfx::Rect& bounds_in_screen) { |
| gfx::Rect bounds_in_parent = bounds_in_screen; |
| ::wm::ConvertRectFromScreen(window_->parent(), &bounds_in_parent); |
| window_->SetBounds(bounds_in_parent); |
| } |
| |
| void WindowState::AdjustSnappedBounds(gfx::Rect* bounds) { |
| auto* tablet_mode_controller = Shell::Get()->tablet_mode_controller(); |
| const bool in_tablet = |
| tablet_mode_controller && tablet_mode_controller->InTabletMode(); |
| |
| // Tablet mode should use bounds calculation in SplitViewController. |
| // However, transient state from transitioning clamshell to tablet mode |
| // might end up calling this function during work area changes, so we avoid |
| // unnecessary task in that case when it will be overwritten by tablet mode |
| // work. |
| if (is_dragged() || !IsSnapped() || in_tablet) |
| return; |
| gfx::Rect maximized_bounds = |
| screen_util::GetMaximizedWindowBoundsInParent(window_); |
| |
| const display::Display display = |
| display::Screen::GetScreen()->GetDisplayNearestWindow(window_); |
| |
| // For snapped window, `GetSnappedWindowBounds` computes bounds position |
| // from snap type and size from |snap_ratio|. |
| gfx::Rect snapped_bounds = |
| snap_ratio_ ? GetSnappedWindowBounds( |
| maximized_bounds, display, window_, |
| GetStateType() == WindowStateType::kPrimarySnapped |
| ? ash::SnapViewType::kPrimary |
| : ash::SnapViewType::kSecondary, |
| *snap_ratio_) |
| : maximized_bounds; |
| bounds->set_origin(snapped_bounds.origin()); |
| |
| // If |snap_ratio_| exists adjust the size of the window. Otherwise only |
| // maximize it vertically for horizontal screen and maximize horizontally for |
| // vertical screen. |
| if (snap_ratio_) |
| bounds->set_size(snapped_bounds.size()); |
| else if (SplitViewController::IsLayoutHorizontal(display)) |
| bounds->set_height(snapped_bounds.height()); |
| else |
| bounds->set_width(snapped_bounds.width()); |
| } |
| |
| void WindowState::UpdateWindowPropertiesFromStateType() { |
| ui::WindowShowState new_window_state = |
| ToWindowShowState(current_state_->GetType()); |
| // Clear |kPreMinimizedShowStateKey| property only when the window is actually |
| // Unminimized and not in tablet mode. |
| if (new_window_state != ui::SHOW_STATE_MINIMIZED && IsMinimized() && |
| !IsTabletModeEnabled()) { |
| window()->ClearProperty(aura::client::kPreMinimizedShowStateKey); |
| } |
| if (new_window_state != GetShowState()) { |
| base::AutoReset<bool> resetter(&ignore_property_change_, true); |
| window_->SetProperty(aura::client::kShowStateKey, new_window_state); |
| } |
| |
| if (GetStateType() != window_->GetProperty(chromeos::kWindowStateTypeKey)) { |
| base::AutoReset<bool> resetter(&ignore_property_change_, true); |
| window_->SetProperty(chromeos::kWindowStateTypeKey, GetStateType()); |
| } |
| |
| if (window_->GetProperty(ash::kWindowManagerManagesOpacityKey)) { |
| const gfx::Size& size = window_->bounds().size(); |
| // WindowManager manages the window opacity. Make it opaque unless |
| // the window is in normal state whose frame has rounded corners. |
| if (IsNormalStateType()) { |
| window_->SetTransparent(true); |
| window_->SetOpaqueRegionsForOcclusion({gfx::Rect(size)}); |
| } else { |
| window_->SetOpaqueRegionsForOcclusion({}); |
| window_->SetTransparent(false); |
| } |
| } |
| } |
| |
| void WindowState::NotifyPreStateTypeChange( |
| WindowStateType old_window_state_type) { |
| for (auto& observer : observer_list_) |
| observer.OnPreWindowStateTypeChange(this, old_window_state_type); |
| OnPrePipStateChange(old_window_state_type); |
| } |
| |
| void WindowState::NotifyPostStateTypeChange( |
| WindowStateType old_window_state_type) { |
| for (auto& observer : observer_list_) |
| observer.OnPostWindowStateTypeChange(this, old_window_state_type); |
| OnPostPipStateChange(old_window_state_type); |
| UpdateWindowStateRestoreHistoryStack(old_window_state_type); |
| SaveWindowForWindowRestore(this); |
| } |
| |
| void WindowState::OnPostPipStateChange(WindowStateType old_window_state_type) { |
| if (old_window_state_type == WindowStateType::kPip) { |
| // The animation type may be FADE_OUT_SLIDE_IN at this point, which we don't |
| // want it to be anymore if the window is not PIP anymore. |
| ::wm::SetWindowVisibilityAnimationType( |
| window_, ::wm::WINDOW_VISIBILITY_ANIMATION_TYPE_DEFAULT); |
| } |
| } |
| |
| void WindowState::SetBoundsDirect(const gfx::Rect& bounds) { |
| gfx::Rect actual_new_bounds(bounds); |
| // Ensure we don't go smaller than our minimum bounds in "normal" window |
| // modes |
| if (window_->delegate() && !IsMaximized() && !IsFullscreen()) { |
| // Get the minimum usable size of the minimum size and the screen size. |
| gfx::Size min_size = window_->delegate() |
| ? window_->delegate()->GetMinimumSize() |
| : gfx::Size(); |
| const display::Display display = |
| display::Screen::GetScreen()->GetDisplayNearestWindow(window_); |
| min_size.SetToMin(display.work_area().size()); |
| |
| actual_new_bounds.set_width( |
| std::max(min_size.width(), actual_new_bounds.width())); |
| actual_new_bounds.set_height( |
| std::max(min_size.height(), actual_new_bounds.height())); |
| |
| // Changing the size of the PIP window can detach it from one of the edges |
| // of the screen, which makes the snap fraction logic fail. Ensure to snap |
| // it again. |
| if (IsPip() && !is_dragged()) { |
| ::wm::ConvertRectToScreen(window_->GetRootWindow(), &actual_new_bounds); |
| actual_new_bounds = CollisionDetectionUtils::GetRestingPosition( |
| display, actual_new_bounds, |
| CollisionDetectionUtils::RelativePriority::kPictureInPicture); |
| ::wm::ConvertRectFromScreen(window_->GetRootWindow(), &actual_new_bounds); |
| } |
| } |
| BoundsSetter().SetBounds(window_, actual_new_bounds); |
| } |
| |
| void WindowState::SetBoundsConstrained(const gfx::Rect& bounds) { |
| gfx::Rect work_area_in_parent = |
| screen_util::GetDisplayWorkAreaBoundsInParent(window_); |
| gfx::Rect child_bounds(bounds); |
| AdjustBoundsSmallerThan(work_area_in_parent.size(), &child_bounds); |
| SetBoundsDirect(child_bounds); |
| } |
| |
| void WindowState::SetBoundsDirectAnimated(const gfx::Rect& bounds, |
| base::TimeDelta duration, |
| gfx::Tween::Type tween_type) { |
| if (::wm::WindowAnimationsDisabled(window_)) { |
| SetBoundsDirect(bounds); |
| return; |
| } |
| ui::Layer* layer = window_->layer(); |
| ui::ScopedLayerAnimationSettings slide_settings(layer->GetAnimator()); |
| slide_settings.SetPreemptionStrategy( |
| ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); |
| slide_settings.SetTweenType(tween_type); |
| slide_settings.SetTransitionDuration(duration); |
| SetBoundsDirect(bounds); |
| } |
| |
| void WindowState::SetBoundsDirectCrossFade(const gfx::Rect& new_bounds) { |
| // Some test results in invoking CrossFadeToBounds when window is not visible. |
| // No animation is necessary in that case, thus just change the bounds and |
| // quit. |
| if (!window_->TargetVisibility()) { |
| SetBoundsConstrained(new_bounds); |
| return; |
| } |
| |
| // If the window already has a transform in place, do not use the cross fade |
| // animation, set the bounds directly instead, or animation is disabled. |
| if (!window_->layer()->GetTargetTransform().IsIdentity() || |
| ::wm::WindowAnimationsDisabled(window_)) { |
| SetBoundsDirect(new_bounds); |
| return; |
| } |
| |
| // Create fresh layers for the window and all its children to paint into. |
| // Takes ownership of the old layer and all its children, which will be |
| // cleaned up after the animation completes. |
| // Specify |set_bounds| to true here to keep the old bounds in the child |
| // windows of |window|. |
| std::unique_ptr<ui::LayerTreeOwner> old_layer_owner = |
| ::wm::RecreateLayers(window_); |
| |
| // Resize the window to the new size, which will force a layout and paint. |
| SetBoundsDirect(new_bounds); |
| |
| CrossFadeAnimation(window_, std::move(old_layer_owner)); |
| } |
| |
| void WindowState::OnPrePipStateChange(WindowStateType old_window_state_type) { |
| auto* widget = views::Widget::GetWidgetForNativeWindow(window()); |
| const bool was_pip = old_window_state_type == WindowStateType::kPip; |
| if (IsPip()) { |
| CollisionDetectionUtils::MarkWindowPriorityForCollisionDetection( |
| window(), CollisionDetectionUtils::RelativePriority::kPictureInPicture); |
| // widget may not exit in some unit tests. |
| // TODO(oshima): Fix unit tests and add DCHECK. |
| if (widget) { |
| widget->widget_delegate()->SetCanActivate(false); |
| if (widget->IsActive()) |
| widget->Deactivate(); |
| Shell::Get()->focus_cycler()->AddWidget(widget); |
| } |
| ::wm::SetWindowVisibilityAnimationType( |
| window(), WINDOW_VISIBILITY_ANIMATION_TYPE_FADE_IN_SLIDE_OUT); |
| // There may already be a system ui window on the initial position. |
| UpdatePipBounds(); |
| if (!was_pip) { |
| window()->SetProperty(kPrePipWindowStateTypeKey, old_window_state_type); |
| |
| if (widget && widget->GetContentsView()) { |
| widget->GetContentsView()->GetViewAccessibility().AnnounceText( |
| l10n_util::GetStringUTF16(IDS_ENTER_PIP_A11Y_NOTIFICATION)); |
| } |
| } |
| |
| CollectPipEnterExitMetrics(/*enter=*/true); |
| |
| // PIP window shouldn't be tracked in MruWindowTracker. |
| window()->SetProperty(ash::kExcludeInMruKey, true); |
| } else if (was_pip) { |
| if (widget) { |
| widget->widget_delegate()->SetCanActivate(true); |
| Shell::Get()->focus_cycler()->RemoveWidget(widget); |
| } |
| ::wm::SetWindowVisibilityAnimationType( |
| window(), ::wm::WINDOW_VISIBILITY_ANIMATION_TYPE_DEFAULT); |
| |
| CollectPipEnterExitMetrics(/*enter=*/false); |
| window()->ClearProperty(ash::kExcludeInMruKey); |
| } |
| // PIP uses the snap fraction to place the PIP window at the correct position |
| // after screen rotation, system UI area change, etc. Make sure to reset this |
| // when the window enters/exits PIP so the obsolete fraction won't be used. |
| if (IsPip() || was_pip) |
| ash::PipPositioner::ClearSnapFraction(this); |
| } |
| |
| void WindowState::UpdatePipBounds() { |
| gfx::Rect new_bounds = |
| PipPositioner::GetPositionAfterMovementAreaChange(this); |
| ::wm::ConvertRectFromScreen(window()->GetRootWindow(), &new_bounds); |
| if (window()->bounds() != new_bounds) { |
| SetBoundsWMEvent event(new_bounds, /*animate=*/true); |
| OnWMEvent(&event); |
| } |
| } |
| |
| void WindowState::CollectPipEnterExitMetrics(bool enter) { |
| const bool is_arc = IsArcWindow(window()); |
| if (enter) { |
| pip_start_time_ = base::TimeTicks::Now(); |
| |
| ReportAshPipEvents(AshPipEvents::PIP_START); |
| ReportAshPipEvents(is_arc ? AshPipEvents::ANDROID_PIP_START |
| : AshPipEvents::CHROME_PIP_START); |
| } else { |
| ReportAshPipEvents(AshPipEvents::PIP_END); |
| ReportAshPipEvents(is_arc ? AshPipEvents::ANDROID_PIP_END |
| : AshPipEvents::CHROME_PIP_END); |
| |
| if (is_arc) { |
| DCHECK(!pip_start_time_.is_null()); |
| const auto session_duration = base::TimeTicks::Now() - pip_start_time_; |
| ReportAshPipAndroidPipUseTime(session_duration); |
| } |
| pip_start_time_ = base::TimeTicks(); |
| } |
| } |
| |
| void WindowState::UpdateWindowStateRestoreHistoryStack( |
| chromeos::WindowStateType previous_state_type) { |
| WindowStateType current_state_type = GetStateType(); |
| |
| const bool is_state_type_supported = |
| kWindowStateRestoreHistoryLayerMap.find(current_state_type) != |
| kWindowStateRestoreHistoryLayerMap.end(); |
| if (!is_state_type_supported) { |
| window_state_restore_history_.clear(); |
| return; |
| } |
| |
| // We'll need to pop out any window state that the `current_state_type` can |
| // not restore back to (i.e., whose restore order is equal or higher than |
| // `current_state_type`). |
| for (auto state : base::Reversed(window_state_restore_history_)) { |
| if (kWindowStateRestoreHistoryLayerMap.at(state) < |
| kWindowStateRestoreHistoryLayerMap.at(current_state_type)) { |
| break; |
| } |
| window_state_restore_history_.pop_back(); |
| } |
| |
| // If `current_state_type` can restore to `previous_state_type`, push |
| // `previous_state_type` into the stack. |
| const bool is_previous_state_type_supported = |
| kWindowStateRestoreHistoryLayerMap.find(previous_state_type) != |
| kWindowStateRestoreHistoryLayerMap.end(); |
| if (is_previous_state_type_supported && |
| (kWindowStateRestoreHistoryLayerMap.at(current_state_type) > |
| kWindowStateRestoreHistoryLayerMap.at(previous_state_type))) { |
| window_state_restore_history_.push_back(previous_state_type); |
| } |
| } |
| |
| chromeos::WindowStateType WindowState::GetMaximizedOrCenteredWindowType() |
| const { |
| return CanMaximize() && ::wm::GetTransientParent(window_) == nullptr |
| ? WindowStateType::kMaximized |
| : WindowStateType::kNormal; |
| } |
| |
| // static |
| WindowState* WindowState::Get(aura::Window* window) { |
| if (!window) |
| return nullptr; |
| |
| WindowState* state = window->GetProperty(kWindowStateKey); |
| if (state) |
| return state; |
| |
| if (window->GetType() == aura::client::WINDOW_TYPE_CONTROL) |
| return nullptr; |
| |
| DCHECK(window->parent()); |
| |
| // WindowState is only for windows in top level container, unless they are |
| // temporarily hidden when launched by window restore. The will be reparented |
| // to a top level container soon, and need a WindowState. |
| if (!IsToplevelContainer(window->parent()) && |
| !IsTemporarilyHiddenForFullrestore(window)) { |
| return nullptr; |
| } |
| |
| state = new WindowState(window); |
| window->SetProperty(kWindowStateKey, state); |
| return state; |
| } |
| |
| // static |
| const WindowState* WindowState::Get(const aura::Window* window) { |
| return Get(const_cast<aura::Window*>(window)); |
| } |
| |
| // static |
| WindowState* WindowState::ForActiveWindow() { |
| aura::Window* active = window_util::GetActiveWindow(); |
| return active ? WindowState::Get(active) : nullptr; |
| } |
| |
| void WindowState::OnWindowPropertyChanged(aura::Window* window, |
| const void* key, |
| intptr_t old) { |
| DCHECK_EQ(window_, window); |
| if (key == aura::client::kShowStateKey) { |
| if (!ignore_property_change_) { |
| WMEvent event(WMEventTypeFromShowState(GetShowState())); |
| OnWMEvent(&event); |
| } |
| return; |
| } |
| if (key == kWindowPipTypeKey) { |
| if (window->GetProperty(kWindowPipTypeKey)) { |
| WMEvent event(WM_EVENT_PIP); |
| OnWMEvent(&event); |
| } else { |
| // Currently "restore" is not implemented. |
| NOTIMPLEMENTED(); |
| } |
| return; |
| } |
| if (key == chromeos::kWindowStateTypeKey) { |
| if (!ignore_property_change_) { |
| // This change came from somewhere else. Revert it. |
| window->SetProperty(chromeos::kWindowStateTypeKey, GetStateType()); |
| } |
| return; |
| } |
| if (key == aura::client::kWindowWorkspaceKey) { |
| // Save the window for window restore purposes unless |
| // |ignore_property_change_| is true. Note that moving windows across |
| // displays will also trigger a kWindowWorkspaceKey change, even if the |
| // value stays the same, so we do not need to save the window when it |
| // changes root windows (OnWindowAddedToRootWindow). |
| if (!ignore_property_change_) |
| SaveWindowForWindowRestore(this); |
| return; |
| } |
| |
| // The shelf visibility should be updated if kHideShelfWhenFullscreenKey or |
| // kImmersiveIsActive change - these property affect the shelf behavior, and |
| // the shelf is expected to be hidden when fullscreen or immersive mode start. |
| const bool requires_shelf_visibility_update = |
| (key == kHideShelfWhenFullscreenKey && |
| old != window->GetProperty(kHideShelfWhenFullscreenKey)) || |
| (key == kImmersiveIsActive && |
| old != window->GetProperty(kImmersiveIsActive)); |
| |
| if (requires_shelf_visibility_update && !ignore_property_change_) { |
| Shell::Get()->UpdateShelfVisibility(); |
| return; |
| } |
| } |
| |
| void WindowState::OnWindowAddedToRootWindow(aura::Window* window) { |
| DCHECK_EQ(window_, window); |
| if (::wm::GetTransientParent(window)) |
| return; |
| MoveAllTransientChildrenToNewRoot(window); |
| } |
| |
| void WindowState::OnWindowDestroying(aura::Window* window) { |
| DCHECK_EQ(window_, window); |
| |
| PersistentDesksBarController* bar_controller = |
| Shell::Get()->persistent_desks_bar_controller(); |
| if (bar_controller) |
| bar_controller->UpdateBarOnWindowDestroying(window_); |
| |
| // If the window is destroyed during PIP, count that as exiting. |
| if (IsPip()) |
| CollectPipEnterExitMetrics(/*enter=*/false); |
| |
| auto* widget = views::Widget::GetWidgetForNativeWindow(window); |
| if (widget) |
| Shell::Get()->focus_cycler()->RemoveWidget(widget); |
| |
| current_state_->OnWindowDestroying(this); |
| delegate_.reset(); |
| } |
| |
| void WindowState::OnWindowBoundsChanged(aura::Window* window, |
| const gfx::Rect& old_bounds, |
| const gfx::Rect& new_bounds, |
| ui::PropertyChangeReason reason) { |
| DCHECK_EQ(this->window(), window); |
| if (window_->GetTransparent() && IsNormalStateType() && |
| window_->GetProperty(ash::kWindowManagerManagesOpacityKey)) { |
| window_->SetOpaqueRegionsForOcclusion({gfx::Rect(new_bounds.size())}); |
| } |
| |
| if (reason != ui::PropertyChangeReason::FROM_ANIMATION && !is_dragged()) |
| SaveWindowForWindowRestore(this); |
| } |
| |
| } // namespace ash |