| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/wm/splitview/split_view_controller.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| |
| #include "ash/accessibility/accessibility_controller_impl.h" |
| #include "ash/constants/app_types.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/display/screen_orientation_controller.h" |
| #include "ash/keyboard/ui/keyboard_ui_controller.h" |
| #include "ash/public/cpp/metrics_util.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/root_window_settings.h" |
| #include "ash/screen_util.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shell.h" |
| #include "ash/style/ash_color_provider.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/delayed_animation_observer_impl.h" |
| #include "ash/wm/overview/overview_controller.h" |
| #include "ash/wm/overview/overview_grid.h" |
| #include "ash/wm/overview/overview_item.h" |
| #include "ash/wm/overview/overview_types.h" |
| #include "ash/wm/overview/overview_utils.h" |
| #include "ash/wm/snap_group/snap_group_controller.h" |
| #include "ash/wm/splitview/auto_snap_controller.h" |
| #include "ash/wm/splitview/split_view_constants.h" |
| #include "ash/wm/splitview/split_view_divider.h" |
| #include "ash/wm/splitview/split_view_metrics_controller.h" |
| #include "ash/wm/splitview/split_view_observer.h" |
| #include "ash/wm/splitview/split_view_overview_session.h" |
| #include "ash/wm/splitview/split_view_utils.h" |
| #include "ash/wm/tablet_mode/tablet_mode_window_state.h" |
| #include "ash/wm/window_positioning_utils.h" |
| #include "ash/wm/window_properties.h" |
| #include "ash/wm/window_resizer.h" |
| #include "ash/wm/window_restore/window_restore_controller.h" |
| #include "ash/wm/window_state.h" |
| #include "ash/wm/window_transient_descendant_iterator.h" |
| #include "ash/wm/window_util.h" |
| #include "base/auto_reset.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_set.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/time/time.h" |
| #include "chromeos/ui/base/window_properties.h" |
| #include "chromeos/ui/frame/caption_buttons/snap_controller.h" |
| #include "components/app_restore/desk_template_read_handler.h" |
| #include "components/app_restore/window_properties.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "ui/aura/client/aura_constants.h" |
| #include "ui/aura/window_delegate.h" |
| #include "ui/base/hit_test.h" |
| #include "ui/base/ime/ash/ime_bridge.h" |
| #include "ui/base/ime/input_method.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_animator.h" |
| #include "ui/compositor/presentation_time_recorder.h" |
| #include "ui/compositor/throughput_tracker.h" |
| #include "ui/display/types/display_constants.h" |
| #include "ui/gfx/animation/slide_animation.h" |
| #include "ui/gfx/animation/tween.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/transform_util.h" |
| #include "ui/views/animation/compositor_animation_runner.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/coordinate_conversion.h" |
| #include "ui/wm/core/shadow_controller.h" |
| #include "ui/wm/core/window_util.h" |
| #include "ui/wm/public/activation_client.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| using ::chromeos::WindowStateType; |
| |
| // Three fixed position ratios of the divider, which means the divider can |
| // always be moved to these three positions. |
| constexpr float kFixedPositionRatios[] = {0.f, chromeos::kDefaultSnapRatio, |
| 1.0f}; |
| |
| // The black scrim starts to fade in when the divider is moved past the two |
| // optional positions (`chromeos::kOneThirdSnapRatio`, |
| // `chromeos::kTwoThirdSnapRatio`) and reaches to its maximum opacity |
| // (`kBlackScrimOpacity`) after moving `kBlackScrimFadeInRatio` of the screen |
| // width. See https://crbug.com/827730 for details. |
| constexpr float kBlackScrimFadeInRatio = 0.1f; |
| constexpr float kBlackScrimOpacity = 0.4f; |
| |
| // The speed at which the divider is moved controls whether windows are scaled |
| // or translated. If the divider is moved more than this many pixels per second, |
| // the "fast" mode is enabled. |
| constexpr int kSplitViewThresholdPixelsPerSec = 72; |
| |
| // This is how often the divider drag speed is checked. |
| constexpr base::TimeDelta kSplitViewChunkTime = base::Milliseconds(500); |
| |
| // Records the animation smoothness when the divider is released during a resize |
| // and animated to a fixed position ratio. |
| constexpr char kDividerAnimationSmoothness[] = |
| "Ash.SplitViewResize.AnimationSmoothness.DividerAnimation"; |
| |
| // Histogram names that record presentation time of resize operation with |
| // following conditions: |
| // a) tablet split view, one snapped window, empty overview grid; |
| // b) tablet split view, two snapped windows; |
| // c) tablet split view, one snapped window, nonempty overview grid; |
| constexpr char kTabletSplitViewResizeSingleHistogram[] = |
| "Ash.SplitViewResize.PresentationTime.TabletMode.SingleWindow"; |
| constexpr char kTabletSplitViewResizeMultiHistogram[] = |
| "Ash.SplitViewResize.PresentationTime.TabletMode.MultiWindow"; |
| constexpr char kTabletSplitViewResizeWithOverviewHistogram[] = |
| "Ash.SplitViewResize.PresentationTime.TabletMode.WithOverview"; |
| |
| constexpr char kTabletSplitViewResizeSingleMaxLatencyHistogram[] = |
| "Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.SingleWindow"; |
| constexpr char kTabletSplitViewResizeMultiMaxLatencyHistogram[] = |
| "Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.MultiWindow"; |
| constexpr char kTabletSplitViewResizeWithOverviewMaxLatencyHistogram[] = |
| "Ash.SplitViewResize.PresentationTime.MaxLatency.TabletMode.WithOverview"; |
| |
| // Histogram of the number of swapping window operations in split view. |
| constexpr char kSplitViewSwapWindowsSource[] = "Ash.SplitView.SwapWindowSource"; |
| |
| // The time when the number of roots in split view changes from one to two. Used |
| // for the purpose of metric collection. |
| base::Time g_multi_display_split_view_start_time; |
| |
| bool IsExactlyOneRootInSplitView() { |
| const aura::Window::Windows all_root_windows = Shell::GetAllRootWindows(); |
| return 1 == |
| base::ranges::count_if( |
| all_root_windows, [](aura::Window* root_window) { |
| return SplitViewController::Get(root_window)->InSplitViewMode(); |
| }); |
| } |
| |
| ui::InputMethod* GetCurrentInputMethod() { |
| if (auto* bridge = IMEBridge::Get()) { |
| if (auto* handler = bridge->GetInputContextHandler()) |
| return handler->GetInputMethod(); |
| } |
| return nullptr; |
| } |
| |
| WindowStateType GetStateTypeFromSnapPosition( |
| SplitViewController::SnapPosition snap_position) { |
| DCHECK(snap_position != SplitViewController::SnapPosition::kNone); |
| if (snap_position == SplitViewController::SnapPosition::kPrimary) |
| return WindowStateType::kPrimarySnapped; |
| if (snap_position == SplitViewController::SnapPosition::kSecondary) |
| return WindowStateType::kSecondarySnapped; |
| NOTREACHED(); |
| return WindowStateType::kDefault; |
| } |
| |
| // Returns the minimum length of the window according to the screen orientation. |
| int GetMinimumWindowLength(aura::Window* window, bool horizontal) { |
| int minimum_width = 0; |
| if (window && window->delegate()) { |
| gfx::Size minimum_size = window->delegate()->GetMinimumSize(); |
| minimum_width = horizontal ? minimum_size.width() : minimum_size.height(); |
| } |
| return minimum_width; |
| } |
| |
| // Returns the length of the window according to the screen orientation. |
| int GetWindowLength(aura::Window* window, bool horizontal) { |
| const auto& bounds = window->bounds(); |
| return horizontal ? bounds.width() : bounds.height(); |
| } |
| |
| // Returns true if |window| is currently snapped. |
| bool IsSnapped(aura::Window* window) { |
| if (!window) |
| return false; |
| return WindowState::Get(window)->IsSnapped(); |
| } |
| |
| bool IsInTabletMode() { |
| TabletModeController* tablet_mode_controller = |
| Shell::Get()->tablet_mode_controller(); |
| return tablet_mode_controller && tablet_mode_controller->InTabletMode(); |
| } |
| |
| void RemoveSnappingWindowFromOverviewIfApplicable( |
| OverviewSession* overview_session, |
| aura::Window* window) { |
| if (!overview_session) { |
| return; |
| } |
| |
| OverviewItemBase* item = overview_session->GetOverviewItemForWindow(window); |
| if (!item) { |
| return; |
| } |
| |
| // Remove it from overview. The transform will be reset later after the window |
| // is snapped. Note the remaining windows in overview don't need to be |
| // repositioned in this case as they have been positioned to the right place |
| // during dragging. |
| item->EnsureVisible(); |
| item->RestoreWindow(/*reset_transform=*/false, /*animate=*/true); |
| overview_session->RemoveItem(item); |
| } |
| |
| // If there is a window in the snap position, trigger a WMEvent to snap it in |
| // the opposite position. |
| void TriggerWMEventToSnapWindow(WindowState* window_state, |
| WMEventType event_type) { |
| CHECK(event_type == WM_EVENT_SNAP_PRIMARY || |
| event_type == WM_EVENT_SNAP_SECONDARY); |
| |
| const WindowSnapWMEvent window_event( |
| event_type, |
| window_state->snap_ratio().value_or(chromeos::kDefaultSnapRatio)); |
| window_state->OnWMEvent(&window_event); |
| } |
| |
| } // namespace |
| |
| // ----------------------------------------------------------------------------- |
| // DividerSnapAnimation: |
| |
| // Animates the divider to its closest fixed position. |
| // `SplitViewController::IsResizingWithDivider()` is assumed to be already false |
| // before this animation starts, but some resizing logic is delayed until this |
| // animation ends. |
| class SplitViewController::DividerSnapAnimation |
| : public gfx::SlideAnimation, |
| public gfx::AnimationDelegate { |
| public: |
| DividerSnapAnimation(SplitViewController* split_view_controller, |
| int starting_position, |
| int ending_position, |
| base::TimeDelta duration, |
| gfx::Tween::Type tween_type) |
| : gfx::SlideAnimation(this), |
| split_view_controller_(split_view_controller), |
| starting_position_(starting_position), |
| ending_position_(ending_position) { |
| SetSlideDuration(duration); |
| SetTweenType(tween_type); |
| |
| aura::Window* window = split_view_controller->primary_window() |
| ? split_view_controller->primary_window() |
| : split_view_controller->secondary_window(); |
| DCHECK(window); |
| |
| // |widget| may be null in tests. It will use the default animation |
| // container in this case. |
| views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window); |
| if (!widget) |
| return; |
| |
| gfx::AnimationContainer* container = new gfx::AnimationContainer(); |
| container->SetAnimationRunner( |
| std::make_unique<views::CompositorAnimationRunner>(widget, FROM_HERE)); |
| SetContainer(container); |
| |
| tracker_.emplace(widget->GetCompositor()->RequestNewThroughputTracker()); |
| tracker_->Start( |
| metrics_util::ForSmoothness(base::BindRepeating([](int smoothness) { |
| UMA_HISTOGRAM_PERCENTAGE(kDividerAnimationSmoothness, smoothness); |
| }))); |
| } |
| DividerSnapAnimation(const DividerSnapAnimation&) = delete; |
| DividerSnapAnimation& operator=(const DividerSnapAnimation&) = delete; |
| ~DividerSnapAnimation() override = default; |
| |
| int ending_position() const { return ending_position_; } |
| |
| private: |
| // gfx::AnimationDelegate: |
| void AnimationEnded(const gfx::Animation* animation) override { |
| DCHECK(split_view_controller_->InSplitViewMode()); |
| DCHECK(!split_view_controller_->IsResizingWithDivider()); |
| DCHECK_EQ(ending_position_, split_view_controller_->divider_position_); |
| |
| split_view_controller_->EndResizeWithDividerImpl(); |
| split_view_controller_->EndSplitViewAfterResizingAtEdgeIfAppropriate(); |
| |
| if (tracker_) |
| tracker_->Stop(); |
| } |
| |
| void AnimationProgressed(const gfx::Animation* animation) override { |
| DCHECK(split_view_controller_->InSplitViewMode()); |
| DCHECK(!split_view_controller_->IsResizingWithDivider()); |
| |
| split_view_controller_->divider_position_ = |
| CurrentValueBetween(starting_position_, ending_position_); |
| split_view_controller_->NotifyDividerPositionChanged(); |
| split_view_controller_->UpdateSnappedWindowsAndDividerBounds(); |
| // Updating the window may stop animation. |
| if (is_animating()) { |
| split_view_controller_->UpdateResizeBackdrop(); |
| split_view_controller_->SetWindowsTransformDuringResizing(); |
| } |
| } |
| |
| void AnimationCanceled(const gfx::Animation* animation) override { |
| if (tracker_) |
| tracker_->Cancel(); |
| } |
| |
| raw_ptr<SplitViewController, ExperimentalAsh> split_view_controller_; |
| int starting_position_; |
| int ending_position_; |
| absl::optional<ui::ThroughputTracker> tracker_; |
| }; |
| |
| // ----------------------------------------------------------------------------- |
| // ToBeSnappedWindowsObserver: |
| |
| // Helper class that prepares windows that are changing to snapped window state. |
| // This allows async window state type changes and handles calls to |
| // SplitViewController when necessary. |
| class SplitViewController::ToBeSnappedWindowsObserver |
| : public aura::WindowObserver, |
| public WindowStateObserver { |
| public: |
| explicit ToBeSnappedWindowsObserver( |
| SplitViewController* split_view_controller) |
| : split_view_controller_(split_view_controller) {} |
| ToBeSnappedWindowsObserver(const ToBeSnappedWindowsObserver&) = delete; |
| ToBeSnappedWindowsObserver& operator=(const ToBeSnappedWindowsObserver&) = |
| delete; |
| |
| ~ToBeSnappedWindowsObserver() override { |
| for (auto& to_be_snapped_window : to_be_snapped_windows_) { |
| aura::Window* window = to_be_snapped_window.second; |
| if (window) { |
| window->RemoveObserver(this); |
| WindowState::Get(window)->RemoveObserver(this); |
| } |
| } |
| to_be_snapped_windows_.clear(); |
| } |
| |
| void AddToBeSnappedWindow(aura::Window* window, |
| SplitViewController::SnapPosition snap_position) { |
| // If `window` is already snapped in split screen, do nothing. |
| if (split_view_controller_->IsWindowInSplitView(window)) { |
| if (WindowState::Get(window)->GetStateType() != |
| GetStateTypeFromSnapPosition(snap_position)) { |
| // This can happen when swapping the positions of the two snapped |
| // windows in the split view. |
| split_view_controller_->AttachSnappingWindow(window, snap_position); |
| } |
| return; |
| } |
| |
| aura::Window* old_window = to_be_snapped_windows_[snap_position]; |
| if (old_window == window) |
| return; |
| |
| // Stop observe any previous to-be-snapped window in `snap_position`. This |
| // can happen to Android windows as its window state and bounds change are |
| // async, so it's possible to snap another window to the same position while |
| // waiting for the snapping of the previous window. |
| if (old_window) { |
| to_be_snapped_windows_[snap_position] = nullptr; |
| WindowState::Get(old_window)->RemoveObserver(this); |
| old_window->RemoveObserver(this); |
| } |
| |
| // If the to-be-snapped window already has the desired snapped window state, |
| // no need to listen to the state change notification (there will be none |
| // anyway), instead just attach the window to split screen directly. |
| if (WindowState::Get(window)->GetStateType() == |
| GetStateTypeFromSnapPosition(snap_position)) { |
| split_view_controller_->AttachSnappingWindow(window, snap_position); |
| split_view_controller_->OnWindowSnapped(window, |
| /*previous_state=*/absl::nullopt); |
| } else { |
| to_be_snapped_windows_[snap_position] = window; |
| WindowState::Get(window)->AddObserver(this); |
| window->AddObserver(this); |
| } |
| } |
| |
| bool IsObserving(const aura::Window* window) const { |
| return FindWindow(window) != to_be_snapped_windows_.end(); |
| } |
| |
| // aura::WindowObserver: |
| void OnWindowDestroying(aura::Window* window) override { |
| auto iter = FindWindow(window); |
| DCHECK(iter != to_be_snapped_windows_.end()); |
| window->RemoveObserver(this); |
| WindowState::Get(window)->RemoveObserver(this); |
| to_be_snapped_windows_.erase(iter); |
| } |
| |
| // WindowStateObserver: |
| void OnPreWindowStateTypeChange(WindowState* window_state, |
| WindowStateType old_type) override { |
| // When arriving here, we know the to-be-snapped window's state has just |
| // changed and its bounds will be changed soon. |
| auto iter = FindWindow(window_state->window()); |
| DCHECK(iter != to_be_snapped_windows_.end()); |
| SnapPosition snap_position = iter->first; |
| |
| // If the new window type is the target snapped state, remove the window |
| // from `to_be_snapped_windows_` and doing some prep work for snapping it in |
| // split screen. Otherwise (i.e. if the new window type is not the target |
| // one) just ignore the event and keep waiting for the next event. |
| if (window_state->GetStateType() == |
| GetStateTypeFromSnapPosition(snap_position)) { |
| to_be_snapped_windows_.erase(iter); |
| window_state->RemoveObserver(this); |
| window_state->window()->RemoveObserver(this); |
| split_view_controller_->AttachSnappingWindow(window_state->window(), |
| snap_position); |
| } |
| } |
| |
| private: |
| std::map<SnapPosition, aura::Window*>::const_iterator FindWindow( |
| const aura::Window* window) const { |
| for (auto iter = to_be_snapped_windows_.begin(); |
| iter != to_be_snapped_windows_.end(); iter++) { |
| if (iter->second == window) |
| return iter; |
| } |
| return to_be_snapped_windows_.end(); |
| } |
| |
| const raw_ptr<SplitViewController, ExperimentalAsh> split_view_controller_; |
| // Tracks to-be-snapped windows. |
| std::map<SnapPosition, aura::Window*> to_be_snapped_windows_; |
| }; |
| |
| // static |
| SplitViewController* SplitViewController::Get(const aura::Window* window) { |
| DCHECK(window); |
| DCHECK(window->GetRootWindow()); |
| DCHECK(RootWindowController::ForWindow(window)); |
| return RootWindowController::ForWindow(window)->split_view_controller(); |
| } |
| |
| // static |
| bool SplitViewController::IsLayoutHorizontal(aura::Window* window) { |
| return IsLayoutHorizontal( |
| display::Screen::GetScreen()->GetDisplayNearestWindow(window)); |
| } |
| |
| // static |
| bool SplitViewController::IsLayoutHorizontal(const display::Display& display) { |
| if (IsInTabletMode()) { |
| return IsCurrentScreenOrientationLandscape(); |
| } |
| |
| // TODO(crbug.com/1233192): add DCHECK to avoid square size display. |
| DCHECK(display.is_valid()); |
| return chromeos::IsLandscapeOrientation(GetSnapDisplayOrientation(display)); |
| } |
| |
| // static |
| bool SplitViewController::IsLayoutPrimary(aura::Window* window) { |
| return IsLayoutPrimary( |
| display::Screen::GetScreen()->GetDisplayNearestWindow(window)); |
| } |
| |
| // static |
| bool SplitViewController::IsLayoutPrimary(const display::Display& display) { |
| if (IsInTabletMode()) { |
| return IsCurrentScreenOrientationPrimary(); |
| } |
| |
| DCHECK(display.is_valid()); |
| return chromeos::IsPrimaryOrientation(GetSnapDisplayOrientation(display)); |
| } |
| |
| // static |
| bool SplitViewController::IsPhysicalLeftOrTop(SnapPosition position, |
| aura::Window* window) { |
| DCHECK_NE(SnapPosition::kNone, position); |
| return position == (IsLayoutPrimary(window) ? SnapPosition::kPrimary |
| : SnapPosition::kSecondary); |
| } |
| |
| // static |
| bool SplitViewController::IsPhysicalLeftOrTop(SnapPosition position, |
| const display::Display& display) { |
| DCHECK_NE(SnapPosition::kNone, position); |
| return position == (IsLayoutPrimary(display) ? SnapPosition::kPrimary |
| : SnapPosition::kSecondary); |
| } |
| |
| // ----------------------------------------------------------------------------- |
| // SplitViewController: |
| |
| SplitViewController::SplitViewController(aura::Window* root_window) |
| : root_window_(root_window), |
| to_be_snapped_windows_observer_( |
| std::make_unique<ToBeSnappedWindowsObserver>(this)), |
| split_view_metrics_controller_( |
| std::make_unique<SplitViewMetricsController>(this)) { |
| Shell::Get()->accessibility_controller()->AddObserver(this); |
| Shell::Get()->tablet_mode_controller()->AddObserver(this); |
| if (SnapGroupController* snap_group_controller = SnapGroupController::Get()) { |
| snap_group_controller->AddObserver(this); |
| } |
| split_view_type_ = IsInTabletMode() ? SplitViewType::kTabletType |
| : SplitViewType::kClamshellType; |
| } |
| |
| SplitViewController::~SplitViewController() { |
| if (Shell::Get()->tablet_mode_controller()) { |
| Shell::Get()->tablet_mode_controller()->RemoveObserver(this); |
| } |
| if (Shell::Get()->accessibility_controller()) { |
| Shell::Get()->accessibility_controller()->RemoveObserver(this); |
| } |
| if (SnapGroupController* snap_group_controller = SnapGroupController::Get()) { |
| snap_group_controller->RemoveObserver(this); |
| } |
| |
| EndSplitView(EndReason::kRootWindowDestroyed); |
| } |
| |
| bool SplitViewController::IsResizingWithDivider() const { |
| return split_view_divider_ && split_view_divider_->is_resizing_with_divider(); |
| } |
| |
| bool SplitViewController::InSplitViewMode() const { |
| return state_ != State::kNoSnap; |
| } |
| |
| bool SplitViewController::BothSnapped() const { |
| return state_ == State::kBothSnapped; |
| } |
| |
| bool SplitViewController::InClamshellSplitViewMode() const { |
| return InSplitViewMode() && split_view_type_ == SplitViewType::kClamshellType; |
| } |
| |
| bool SplitViewController::InTabletSplitViewMode() const { |
| return InSplitViewMode() && split_view_type_ == SplitViewType::kTabletType; |
| } |
| |
| bool SplitViewController::CanSnapWindow(aura::Window* window) const { |
| return CanSnapWindow(window, chromeos::kDefaultSnapRatio); |
| } |
| |
| bool SplitViewController::CanSnapWindow(aura::Window* window, |
| float snap_ratio) const { |
| if (!ShouldAllowSplitView()) |
| return false; |
| |
| if (!WindowState::Get(window)->CanSnapOnDisplay( |
| display::Screen::GetScreen()->GetDisplayNearestWindow( |
| const_cast<aura::Window*>(root_window_.get())))) { |
| return false; |
| } |
| |
| // Windows created by window restore are not activatable while being restored. |
| // However, we still want to be able to snap these windows at this point. |
| const bool is_to_be_restored_window = |
| window == WindowRestoreController::Get()->to_be_snapped_window(); |
| |
| // TODO(sammiequon): Investigate if we need to check for window activation. |
| if (!is_to_be_restored_window && !wm::CanActivateWindow(window)) |
| return false; |
| |
| return GetMinimumWindowLength(window, IsLayoutHorizontal(window)) <= |
| GetDividerEndPosition() * snap_ratio - |
| kSplitviewDividerShortSideLength / 2; |
| } |
| |
| absl::optional<float> SplitViewController::ComputeSnapRatio( |
| aura::Window* window) { |
| // If there is no default snapped window, or it doesn't have a stored snap |
| // ratio try snapping it to 1/2. |
| aura::Window* default_window = GetDefaultSnappedWindow(); |
| absl::optional<float> default_window_snap_ratio = |
| default_window ? WindowState::Get(default_window)->snap_ratio() |
| : absl::nullopt; |
| if (!default_window_snap_ratio) { |
| return CanSnapWindow(window) |
| ? absl::make_optional(chromeos::kDefaultSnapRatio) |
| : absl::nullopt; |
| } |
| |
| // Maps the snap ratio of the default window to the snap ratio of the opposite |
| // window. |
| static constexpr auto kOppositeRatiosMap = |
| base::MakeFixedFlatMap<float, float>( |
| {{chromeos::kOneThirdSnapRatio, chromeos::kTwoThirdSnapRatio}, |
| {chromeos::kDefaultSnapRatio, chromeos::kDefaultSnapRatio}, |
| {chromeos::kTwoThirdSnapRatio, chromeos::kOneThirdSnapRatio}}); |
| auto* it = kOppositeRatiosMap.find(*default_window_snap_ratio); |
| // TODO(sammiequon): Investigate if this check is needed. It may be needed for |
| // rounding errors (i.e. 2/3 may be 0.67). |
| if (it == kOppositeRatiosMap.end()) { |
| return CanSnapWindow(window) |
| ? absl::make_optional(chromeos::kDefaultSnapRatio) |
| : absl::nullopt; |
| } |
| |
| // If `window` can be snapped to the ideal snap ratio, we are done. |
| float snap_ratio = it->second; |
| if (CanSnapWindow(window, snap_ratio)) { |
| return snap_ratio; |
| } |
| |
| // Reaching here, we cannot snap `window` to its ideal snap ratio. If the |
| // ideal snap ratio was 1/3, we try snapping to 1/2, but only if the default |
| // window can be snapped to 1/2 as well. |
| if (snap_ratio == chromeos::kOneThirdSnapRatio && CanSnapWindow(window) && |
| CanSnapWindow(default_window)) { |
| return chromeos::kDefaultSnapRatio; |
| } |
| |
| return absl::nullopt; |
| } |
| |
| bool SplitViewController::WillStartOverview() const { |
| // Note that at this point `state_` may not have been updated yet, so check if |
| // only one of `primary_window_` or `secondary_window_` are snapped. |
| return !IsInOverviewSession() && !DesksController::Get()->animation() && |
| split_view_type_ == SplitViewType::kTabletType && |
| !!primary_window_ != !!secondary_window_; |
| } |
| |
| void SplitViewController::SnapWindow(aura::Window* window, |
| SnapPosition snap_position, |
| WindowSnapActionSource snap_action_source, |
| bool activate_window, |
| float snap_ratio) { |
| DCHECK(window && CanSnapWindow(window)); |
| DCHECK_NE(snap_position, SnapPosition::kNone); |
| DCHECK(!IsResizingWithDivider()); |
| if (IsDividerAnimating()) { |
| StopSnapAnimation(); |
| } |
| |
| OverviewSession* overview_session = GetOverviewSession(); |
| if (activate_window || |
| (overview_session && |
| overview_session->IsWindowActiveWindowBeforeOverview(window))) { |
| to_be_activated_window_ = window; |
| } |
| |
| to_be_snapped_windows_observer_->AddToBeSnappedWindow(window, snap_position); |
| // Move |window| to the display of |root_window_| first before sending the |
| // WMEvent. Otherwise it may be snapped to the wrong display. |
| if (root_window_ != window->GetRootWindow()) { |
| window_util::MoveWindowToDisplay(window, |
| display::Screen::GetScreen() |
| ->GetDisplayNearestWindow(root_window_) |
| .id()); |
| } |
| const WindowSnapWMEvent event(snap_position == SnapPosition::kPrimary |
| ? WM_EVENT_SNAP_PRIMARY |
| : WM_EVENT_SNAP_SECONDARY, |
| snap_ratio, snap_action_source); |
| WindowState::Get(window)->OnWMEvent(&event); |
| |
| base::RecordAction(base::UserMetricsAction("SplitView_SnapWindow")); |
| } |
| |
| void SplitViewController::OnWMEvent(aura::Window* window, |
| WMEventType event_type) { |
| CHECK(event_type == WM_EVENT_SNAP_PRIMARY || |
| event_type == WM_EVENT_SNAP_SECONDARY); |
| |
| // If split view can't be enabled at the moment, do nothing. |
| if (!ShouldAllowSplitView()) |
| return; |
| |
| const bool in_overview = IsInOverviewSession(); |
| |
| // In clamshell mode, only if overview is active on window snapped or when the |
| // feature flag `kSnapGroup` is enabled and the feature param |
| // `kAutomaticallyLockGroup` is true, the window should be managed by |
| // `SplitViewController`. Otherwise, the window should be snapped normally and |
| // should be managed by `WindowState`. |
| if (split_view_type_ == SplitViewType::kClamshellType && |
| !(in_overview || IsSnapGroupEnabledInClamshellMode())) { |
| return; |
| } |
| |
| // If the snap wm event is from desk template launch when in overview, do not |
| // try to snap the window in split screen. Otherwise, overview might be exited |
| // because of window snapping. |
| const int32_t window_id = |
| window->GetProperty(app_restore::kRestoreWindowIdKey); |
| if (in_overview && |
| window == WindowRestoreController::Get()->to_be_snapped_window() && |
| app_restore::DeskTemplateReadHandler::Get()->GetWindowInfo(window_id)) { |
| return; |
| } |
| |
| // Do nothing if `window` is already waiting to be snapped in split screen. |
| // Order here matters: this must return for auto-snap windows before they try |
| // to override `divider_position_` from a `new_snap_ratio` below. |
| if (to_be_snapped_windows_observer_->IsObserving(window)) { |
| return; |
| } |
| |
| const SnapPosition to_snap_position = event_type == WM_EVENT_SNAP_PRIMARY |
| ? SnapPosition::kPrimary |
| : SnapPosition::kSecondary; |
| |
| // Snap events may request a new snap ratio for an already snapped window, so |
| // update the divider position and use it to calculate the snapped bounds. |
| // Note that only manually-requested snap events (via the multitask menu) will |
| // arrive here, as auto-snap (via `SnapWindow()`) will have added the |
| // to-be-snapped window. b/267380892 |
| absl::optional<float> new_snap_ratio = WindowState::Get(window)->snap_ratio(); |
| if (new_snap_ratio) { |
| const float old_divider_position = divider_position_; |
| divider_position_ = GetDividerPosition(to_snap_position, *new_snap_ratio); |
| if (split_view_divider_) { |
| split_view_divider_->UpdateDividerBounds(); |
| } |
| if (!primary_window_ != !secondary_window_) { |
| // Only notify observers if there is only one snapped window, i.e. |
| // overview is open. |
| NotifyDividerPositionChanged(); |
| } else if (primary_window_ && secondary_window_) { |
| auto* other_window = event_type == WM_EVENT_SNAP_PRIMARY |
| ? secondary_window_.get() |
| : primary_window_.get(); |
| DCHECK(other_window); |
| const int other_window_length = |
| GetMinimumWindowLength(other_window, IsLayoutHorizontal(window)); |
| const int work_area_size = GetDividerEndPosition(); |
| const int window_length = event_type == WM_EVENT_SNAP_PRIMARY |
| ? divider_position_ |
| : work_area_size - divider_position_ - |
| kSplitviewDividerShortSideLength; |
| if (window_length + other_window_length + |
| kSplitviewDividerShortSideLength > |
| work_area_size) { |
| // If `other_window` can't fit in the opposite position, set |
| // `divider_snap_animation_` to Hide then Show, to give off the |
| // impression of bouncing the divider back to `old_divider_position`. |
| // Note the duration is 2 * 250ms to bounce out then in. |
| tablet_resize_mode_ = TabletResizeMode::kFast; |
| divider_snap_animation_ = std::make_unique<DividerSnapAnimation>( |
| this, divider_position_, old_divider_position, |
| base::Milliseconds(500), gfx::Tween::FAST_OUT_SLOW_IN_3); |
| divider_snap_animation_->Hide(); |
| divider_snap_animation_->Show(); |
| } else { |
| // If both windows are already snapped and one window snap ratio |
| // changes, the other window should also get updated. |
| UpdateSnappedBounds(other_window); |
| } |
| } |
| } |
| |
| // Start observe the to-be-snapped window. |
| to_be_snapped_windows_observer_->AddToBeSnappedWindow(window, |
| to_snap_position); |
| } |
| |
| void SplitViewController::AttachSnappingWindow(aura::Window* window, |
| SnapPosition snap_position) { |
| // Save the transformed bounds in preparation for the snapping animation. |
| UpdateSnappingWindowTransformedBounds(window); |
| |
| OverviewSession* overview_session = GetOverviewSession(); |
| RemoveSnappingWindowFromOverviewIfApplicable(overview_session, window); |
| |
| if (state_ == State::kNoSnap) { |
| // Add observers when the split view mode starts. |
| Shell::Get()->AddShellObserver(this); |
| Shell::Get()->overview_controller()->AddObserver(this); |
| if (features::IsAdjustSplitViewForVKEnabled()) { |
| keyboard::KeyboardUIController::Get()->AddObserver(this); |
| Shell::Get()->activation_client()->AddObserver(this); |
| } |
| |
| if (!IsSnapGroupEnabledInClamshellMode()) { |
| // AutoSnapController will end overview in clamshell split view if a |
| // window is not in transitional state. See |
| // `AutoSnapController::AutoSnapWindowIfNeeded()`. |
| // TODO(b/302397864): Handle this logic in |
| // `OverviewSession::OnWindowActivating()`. |
| auto_snap_controller_ = |
| std::make_unique<AutoSnapController>(root_window_); |
| } |
| |
| if (!IsInTabletMode() && IsInOverviewSession()) { |
| // Start the clamshell split overview session. It is too late to create |
| // this in `OnOverviewModeStarting()`, since overview will already have |
| // started and we are dragging `window` into split view. |
| // TODO(b/294580642): Move this to SnapGroupController. |
| RootWindowController::ForWindow(window)->StartSplitViewOverviewSession( |
| window, /*action=*/absl::nullopt, |
| /*type=*/absl::nullopt); |
| } |
| |
| // Get the divider position given by `snap_ratio`, or if there is |
| // pre-set `divider_position_`, use it. It can happen during tablet <-> |
| // clamshell transition or multi-user transition. |
| absl::optional<float> snap_ratio = WindowState::Get(window)->snap_ratio(); |
| divider_position_ = |
| (divider_position_ < 0) |
| ? GetDividerPosition( |
| snap_position, |
| snap_ratio ? *snap_ratio : chromeos::kDefaultSnapRatio) |
| : divider_position_; |
| default_snap_position_ = snap_position; |
| |
| if (split_view_type_ == SplitViewType::kTabletType) { |
| split_view_divider_ = std::make_unique<SplitViewDivider>(this); |
| } |
| |
| splitview_start_time_ = base::Time::Now(); |
| // We are about to enter split view on |root_window_|. If split view is |
| // already active on exactly one root, then |root_window_| will be the |
| // second root, and so multi-display split view begins now. |
| if (IsExactlyOneRootInSplitView()) { |
| base::RecordAction( |
| base::UserMetricsAction("SplitView_MultiDisplaySplitView")); |
| g_multi_display_split_view_start_time = splitview_start_time_; |
| } |
| } |
| |
| aura::Window* previous_snapped_window = nullptr; |
| if (snap_position == SnapPosition::kPrimary) { |
| if (primary_window_ != window) { |
| previous_snapped_window = primary_window_; |
| StopObserving(SnapPosition::kPrimary); |
| primary_window_ = window; |
| } |
| if (secondary_window_ == window) { |
| // Remove `window` from `secondary_window_` if it was previously snapped |
| // there, i.e. during cycle snap or swap windows. |
| secondary_window_ = nullptr; |
| default_snap_position_ = SnapPosition::kPrimary; |
| } |
| } else if (snap_position == SnapPosition::kSecondary) { |
| if (secondary_window_ != window) { |
| previous_snapped_window = secondary_window_; |
| StopObserving(SnapPosition::kSecondary); |
| secondary_window_ = window; |
| } |
| if (primary_window_ == window) { |
| // See above comment. |
| primary_window_ = nullptr; |
| default_snap_position_ = SnapPosition::kSecondary; |
| } |
| } |
| StartObserving(window); |
| |
| // Insert the previous snapped window to overview if overview is active. |
| DCHECK_EQ(overview_session, GetOverviewSession()); |
| if (previous_snapped_window && overview_session) { |
| InsertWindowToOverview(previous_snapped_window); |
| // Ensure that the close icon will fade in. This part is redundant for |
| // dragging from overview, but necessary for dragging from the top. For |
| // dragging from overview, |OverviewItem::OnSelectorItemDragEnded| will be |
| // called on all overview items including the |previous_snapped_window| |
| // item anyway, whereas for dragging from the top, |
| // |OverviewItem::OnSelectorItemDragEnded| already was called on all |
| // overview items and |previous_snapped_window| was not yet among them. |
| overview_session->GetOverviewItemForWindow(previous_snapped_window) |
| ->OnOverviewItemDragEnded(/*snap=*/true); |
| } |
| |
| if (split_view_type_ == SplitViewType::kTabletType) { |
| divider_position_ = GetClosestFixedDividerPosition(); |
| split_view_divider_->UpdateDividerBounds(); |
| } |
| |
| base::RecordAction(base::UserMetricsAction("SplitView_SnapWindow")); |
| } |
| |
| void SplitViewController::SwapWindows(SwapWindowsSource swap_windows_source) { |
| DCHECK(InSplitViewMode()); |
| |
| // Ignore `IsResizingWithDivider()` because it will be true in case of |
| // double tapping (not double clicking) the divider without ever actually |
| // dragging it anywhere. Double tapping the divider triggers |
| // StartResizeWithDivider(), EndResizeWithDivider(), StartResizeWithDivider(), |
| // SwapWindows(), EndResizeWithDivider(). Double clicking the divider |
| // (possible by using the emulator or chrome://flags/#force-tablet-mode) |
| // triggers StartResizeWithDivider(), EndResizeWithDivider(), |
| // StartResizeWithDivider(), EndResizeWithDivider(), SwapWindows(). Those two |
| // sequences of function calls are what were mainly considered in writing the |
| // condition for bailing out here, to disallow swapping windows when the |
| // divider is being dragged or is animating. |
| if (IsDividerAnimating()) { |
| return; |
| } |
| |
| SnapGroupController* snap_group_controller = SnapGroupController::Get(); |
| if (snap_group_controller && snap_group_controller->AreWindowsInSnapGroup( |
| primary_window_, secondary_window_)) { |
| snap_group_controller->RemoveSnapGroupContainingWindow(primary_window_); |
| } |
| |
| SwapWindowsAndUpdateBounds(); |
| if (IsSnapped(primary_window_)) { |
| TriggerWMEventToSnapWindow(WindowState::Get(primary_window_), |
| WM_EVENT_SNAP_PRIMARY); |
| } |
| if (IsSnapped(secondary_window_)) { |
| TriggerWMEventToSnapWindow(WindowState::Get(secondary_window_), |
| WM_EVENT_SNAP_SECONDARY); |
| } |
| |
| // Update `default_snap_position_` if necessary. |
| if (!primary_window_ || !secondary_window_) { |
| default_snap_position_ = |
| primary_window_ ? SnapPosition::kPrimary : SnapPosition::kSecondary; |
| } |
| |
| divider_position_ = GetClosestFixedDividerPosition(); |
| UpdateStateAndNotifyObservers(); |
| NotifyWindowSwapped(); |
| |
| switch (swap_windows_source) { |
| case SwapWindowsSource::kDoubleTap: { |
| base::RecordAction( |
| base::UserMetricsAction("SplitView_DoubleTapDividerSwapWindows")); |
| break; |
| } |
| case SwapWindowsSource::kSnapGroupSwapWindowsButton: { |
| base::RecordAction( |
| base::UserMetricsAction("SplitView_SwapWindowsButtonSwapWindows")); |
| } |
| } |
| base::UmaHistogramEnumeration(kSplitViewSwapWindowsSource, |
| swap_windows_source); |
| } |
| |
| SplitViewController::SnapPosition |
| SplitViewController::GetPositionOfSnappedWindow( |
| const aura::Window* window) const { |
| DCHECK(IsWindowInSplitView(window)); |
| return window == primary_window_ ? SnapPosition::kPrimary |
| : SnapPosition::kSecondary; |
| } |
| |
| aura::Window* SplitViewController::GetSnappedWindow(SnapPosition position) { |
| DCHECK_NE(SnapPosition::kNone, position); |
| return position == SnapPosition::kPrimary ? primary_window_.get() |
| : secondary_window_.get(); |
| } |
| |
| aura::Window* SplitViewController::GetDefaultSnappedWindow() { |
| if (default_snap_position_ == SnapPosition::kPrimary) |
| return primary_window_; |
| if (default_snap_position_ == SnapPosition::kSecondary) |
| return secondary_window_; |
| return nullptr; |
| } |
| |
| gfx::Rect SplitViewController::GetSnappedWindowBoundsInParent( |
| SnapPosition snap_position, |
| aura::Window* window_for_minimum_size, |
| float snap_ratio) { |
| gfx::Rect bounds = GetSnappedWindowBoundsInScreen( |
| snap_position, window_for_minimum_size, snap_ratio); |
| wm::ConvertRectFromScreen(root_window_, &bounds); |
| return bounds; |
| } |
| |
| gfx::Rect SplitViewController::GetSnappedWindowBoundsInParent( |
| SnapPosition snap_position, |
| aura::Window* window_for_minimum_size) { |
| return GetSnappedWindowBoundsInParent(snap_position, window_for_minimum_size, |
| chromeos::kDefaultSnapRatio); |
| } |
| |
| gfx::Rect SplitViewController::GetSnappedWindowBoundsInScreen( |
| SnapPosition snap_position, |
| aura::Window* window_for_minimum_size, |
| float snap_ratio) { |
| const gfx::Rect work_area_bounds_in_screen = |
| screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| root_window_); |
| if (snap_position == SnapPosition::kNone) { |
| return work_area_bounds_in_screen; |
| } |
| |
| if (window_for_minimum_size && ShouldUseWindowBoundsDuringFastResize()) { |
| gfx::Rect bounds = window_for_minimum_size->bounds(); |
| wm::ConvertRectToScreen(window_for_minimum_size->parent(), &bounds); |
| return bounds; |
| } |
| |
| const bool horizontal = IsLayoutHorizontal(root_window_); |
| const bool snap_left_or_top = |
| IsPhysicalLeftOrTop(snap_position, root_window_); |
| |
| // TODO(crbug.com/1231308): Clean-up: make sure only tablet mode uses |
| // SplitViewController and migrate |
| // `SplitViewController::GetSnappedWindowBoundsInScreen()` calls in clamshell |
| // mode to `GetSnappedWindowBounds()` in window_positioning_utils.cc. |
| const bool in_tablet_mode = IsInTabletMode(); |
| const int work_area_size = GetDividerEndPosition(); |
| int divider_position = divider_position_ < 0 |
| ? GetDividerPosition(snap_position, snap_ratio) |
| : divider_position_; |
| // Edit `divider_position` if window restore is currently restoring a snapped |
| // window; take into account the snap percentage saved by the window. Only do |
| // this for clamshell mode; in tablet mode we are OK with restoring to the |
| // default half snap state. |
| if (divider_position_ < 0 && !in_tablet_mode) { |
| if (auto* window = WindowRestoreController::Get()->to_be_snapped_window()) { |
| app_restore::WindowInfo* window_info = |
| window->GetProperty(app_restore::kWindowInfoKey); |
| if (window_info && window_info->snap_percentage) { |
| const int snap_percentage = *window_info->snap_percentage; |
| divider_position = snap_percentage * work_area_size / 100; |
| if (!snap_left_or_top) |
| divider_position = work_area_size - divider_position; |
| } |
| } |
| } |
| |
| int window_size; |
| if (snap_left_or_top) { |
| window_size = divider_position; |
| } else { |
| window_size = work_area_size - divider_position; |
| // In tablet mode or when `IsSnapGroupEnabledInClamshellMode()` is true, |
| // there is a divider widget of which `divider_position` refers to the left |
| // or top, and so we should subtract the thickness. |
| if (in_tablet_mode || |
| (split_view_divider_ && IsSnapGroupEnabledInClamshellMode())) { |
| window_size -= kSplitviewDividerShortSideLength; |
| } |
| } |
| |
| const int minimum = |
| GetMinimumWindowLength(window_for_minimum_size, horizontal); |
| DCHECK(window_for_minimum_size || minimum == 0); |
| if (window_size < minimum) { |
| if (in_tablet_mode && !IsResizingWithDivider()) { |
| // If window with `window_for_minimum_size` gets snapped, the |
| // `split_view_divider_` will then be adjusted to its default position and |
| // `window_size` will be computed accordingly. |
| window_size = work_area_size / 2 - kSplitviewDividerShortSideLength / 2; |
| // If `work_area_size` is odd, then the default divider position is |
| // rounded down, toward the left or top, but then if `snap_left_or_top` is |
| // false, that means `window_size` should now be rounded up. |
| if (!snap_left_or_top && work_area_size % 2 == 1) |
| ++window_size; |
| } else { |
| window_size = minimum; |
| } |
| } |
| |
| if (window_for_minimum_size && !in_tablet_mode) { |
| // Apply the unresizable snapping constraint to the snapped bounds if we're |
| // in the clamshell mode. |
| const gfx::Size* preferred_size = |
| window_for_minimum_size->GetProperty(kUnresizableSnappedSizeKey); |
| if (preferred_size && |
| !WindowState::Get(window_for_minimum_size)->CanResize()) { |
| if (horizontal && preferred_size->width() > 0) |
| window_size = preferred_size->width(); |
| if (!horizontal && preferred_size->height() > 0) |
| window_size = preferred_size->height(); |
| } |
| } |
| |
| // Get the parameter values for which `gfx::Rect::SetByBounds` would recreate |
| // `work_area_bounds_in_screen`. |
| int left = work_area_bounds_in_screen.x(); |
| int top = work_area_bounds_in_screen.y(); |
| int right = work_area_bounds_in_screen.right(); |
| int bottom = work_area_bounds_in_screen.bottom(); |
| |
| // Make `snapped_window_bounds_in_screen` by modifying one of the above four |
| // values: the one that represents the inner edge of the snapped bounds. |
| int& left_or_top = horizontal ? left : top; |
| int& right_or_bottom = horizontal ? right : bottom; |
| if (snap_left_or_top) { |
| right_or_bottom = left_or_top + window_size; |
| } else { |
| left_or_top = right_or_bottom - window_size; |
| } |
| |
| gfx::Rect snapped_window_bounds_in_screen; |
| snapped_window_bounds_in_screen.SetByBounds(left, top, right, bottom); |
| return snapped_window_bounds_in_screen; |
| } |
| |
| gfx::Rect SplitViewController::GetSnappedWindowBoundsInScreen( |
| SnapPosition snap_position, |
| aura::Window* window_for_minimum_size) { |
| return GetSnappedWindowBoundsInScreen(snap_position, window_for_minimum_size, |
| chromeos::kDefaultSnapRatio); |
| } |
| |
| bool SplitViewController::ShouldUseWindowBoundsDuringFastResize() { |
| return IsResizingWithDivider() && |
| tablet_resize_mode_ == TabletResizeMode::kFast; |
| } |
| |
| int SplitViewController::GetDefaultDividerPosition() const { |
| return GetDividerPosition(SnapPosition::kPrimary, |
| chromeos::kDefaultSnapRatio); |
| } |
| |
| int SplitViewController::GetDividerPosition(SnapPosition snap_position, |
| float snap_ratio) const { |
| int divider_end_position = GetDividerEndPosition(); |
| // `snap_width` needs to be a float so that the rounding is performed at the |
| // end of the computation of `next_divider_position`. It's important because a |
| // 1-DIP gap between snapped windows precludes multiresizing. See b/262011280. |
| const float snap_width = divider_end_position * snap_ratio; |
| int next_divider_position = snap_position == SnapPosition::kPrimary |
| ? snap_width |
| : divider_end_position - snap_width; |
| if (split_view_divider_ || split_view_type_ == SplitViewType::kTabletType) { |
| // The divider may be visible in tablet mode, or between two windows in a |
| // snap group in clamshell mode. |
| // In tablet mode, we always consider the divider width even if |
| // `split_view_divider_` is not initialized yet because |
| // `ClientControlledState` may need to know the snapped bounds before |
| // actually snapping the windows. |
| next_divider_position -= kSplitviewDividerShortSideLength / 2; |
| } |
| return next_divider_position; |
| } |
| |
| bool SplitViewController::IsDividerAnimating() const { |
| return divider_snap_animation_ && divider_snap_animation_->is_animating(); |
| } |
| |
| void SplitViewController::EndSplitView(EndReason end_reason) { |
| if (!InSplitViewMode()) { |
| return; |
| } |
| |
| end_reason_ = end_reason; |
| |
| // If we are currently in a resize but split view is ending, make sure to end |
| // the resize. This can happen, for example, on the transition back to |
| // clamshell mode or when a task is minimized during a resize. Likewise, if |
| // split view is ending during the divider snap animation, then clean that up. |
| // But if the split view is ending due to the destroy of `root_window_`, we |
| // should skip the resize. |
| const bool is_divider_animating = IsDividerAnimating(); |
| if ((IsResizingWithDivider() || is_divider_animating) && |
| end_reason != EndReason::kRootWindowDestroyed) { |
| split_view_divider_->set_is_resizing_with_divider(false); |
| if (is_divider_animating) { |
| // Don't call StopAndShoveAnimatedDivider as it will call observers. |
| StopSnapAnimation(); |
| } |
| EndResizeWithDividerImpl(); |
| } |
| |
| // There is at least one case where this line of code is needed: if the user |
| // presses Ctrl+W while resizing a clamshell split view window. |
| presentation_time_recorder_.reset(); |
| |
| // Remove observers when the split view mode ends. |
| Shell::Get()->RemoveShellObserver(this); |
| Shell::Get()->overview_controller()->RemoveObserver(this); |
| if (features::IsAdjustSplitViewForVKEnabled()) { |
| keyboard::KeyboardUIController::Get()->RemoveObserver(this); |
| Shell::Get()->activation_client()->RemoveObserver(this); |
| } |
| |
| auto_snap_controller_.reset(); |
| |
| if (end_reason != EndReason::kRootWindowDestroyed) { |
| // `EndSplitView()` is also called upon `~RootWindowController()` and |
| // `~SplitViewController()`, during which `root_window_` would have been |
| // destroyed. |
| RootWindowController::ForWindow(root_window_) |
| ->EndSplitViewOverviewSession(); |
| } |
| |
| StopObserving(SnapPosition::kPrimary); |
| StopObserving(SnapPosition::kSecondary); |
| black_scrim_layer_.reset(); |
| default_snap_position_ = SnapPosition::kNone; |
| divider_position_ = -1; |
| divider_closest_ratio_ = std::numeric_limits<float>::quiet_NaN(); |
| snapping_window_transformed_bounds_map_.clear(); |
| |
| UpdateStateAndNotifyObservers(); |
| |
| // Close splitview divider widget after updating state so that |
| // OnDisplayMetricsChanged triggered by the widget closing correctly |
| // finds out !InSplitViewMode(). |
| split_view_divider_.reset(); |
| base::RecordAction(base::UserMetricsAction("SplitView_EndSplitView")); |
| const base::Time now = base::Time::Now(); |
| UMA_HISTOGRAM_LONG_TIMES("Ash.SplitView.TimeInSplitView", |
| now - splitview_start_time_); |
| // We just ended split view on |root_window_|. If there is exactly one root |
| // where split view is still active, then multi-display split view ends now. |
| if (IsExactlyOneRootInSplitView()) { |
| UMA_HISTOGRAM_LONG_TIMES("Ash.SplitView.TimeInMultiDisplaySplitView", |
| now - g_multi_display_split_view_start_time); |
| } |
| } |
| |
| bool SplitViewController::IsWindowInSplitView( |
| const aura::Window* window) const { |
| return window && (window == primary_window_ || window == secondary_window_); |
| } |
| |
| void SplitViewController::InitDividerPositionForTransition( |
| int divider_position) { |
| // This should only be called before the actual carry-over happens. |
| DCHECK(!InSplitViewMode()); |
| DCHECK_EQ(divider_position_, -1); |
| divider_position_ = divider_position; |
| } |
| |
| bool SplitViewController::IsWindowInTransitionalState( |
| const aura::Window* window) const { |
| return to_be_snapped_windows_observer_->IsObserving(window); |
| } |
| |
| void SplitViewController::OnOverviewButtonTrayLongPressed( |
| const gfx::Point& event_location) { |
| // Do nothing if split view is not enabled. |
| if (!ShouldAllowSplitView()) |
| return; |
| |
| // If in split view: The active snapped window becomes maximized. If overview |
| // was seen alongside a snapped window, then overview mode ends. |
| // |
| // Otherwise: Enter split view iff the cycle list has at least one window, and |
| // the first one is snappable. |
| |
| MruWindowTracker::WindowList mru_window_list = |
| Shell::Get()->mru_window_tracker()->BuildWindowForCycleList(kActiveDesk); |
| // Do nothing if there is one or less windows in the MRU list. |
| if (mru_window_list.empty()) |
| return; |
| |
| auto* overview_controller = Shell::Get()->overview_controller(); |
| aura::Window* target_window = mru_window_list[0]; |
| |
| // Exit split view mode if we are already in it. |
| if (InSplitViewMode()) { |
| DCHECK(IsWindowInSplitView(target_window)); |
| DCHECK(target_window); |
| EndSplitView(); |
| overview_controller->EndOverview( |
| OverviewEndAction::kOverviewButtonLongPress); |
| MaximizeIfSnapped(target_window); |
| wm::ActivateWindow(target_window); |
| base::RecordAction( |
| base::UserMetricsAction("Tablet_LongPressOverviewButtonExitSplitView")); |
| return; |
| } |
| |
| // Show a toast if the window cannot be snapped. |
| if (!CanSnapWindow(target_window)) { |
| ShowAppCannotSnapToast(); |
| return; |
| } |
| |
| SnapWindow(target_window, SnapPosition::kPrimary, |
| WindowSnapActionSource::kLongPressOverviewButtonToSnap, |
| /*activate_window=*/true); |
| |
| // Start overview mode if we aren't already in it. |
| RootWindowController::ForWindow(target_window) |
| ->StartSplitViewOverviewSession( |
| target_window, OverviewStartAction::kOverviewButtonLongPress, |
| OverviewEnterExitType::kImmediateEnter); |
| |
| base::RecordAction( |
| base::UserMetricsAction("Tablet_LongPressOverviewButtonEnterSplitView")); |
| } |
| |
| void SplitViewController::OnWindowDragStarted(aura::Window* dragged_window) { |
| DCHECK(dragged_window); |
| |
| // OnSnappedWindowDetached() may end split view mode. |
| if (IsWindowInSplitView(dragged_window)) { |
| OnSnappedWindowDetached(dragged_window, |
| WindowDetachedReason::kWindowDragged); |
| } |
| |
| if (split_view_divider_) { |
| split_view_divider_->OnWindowDragStarted(dragged_window); |
| } |
| } |
| |
| void SplitViewController::OnWindowDragEnded( |
| aura::Window* dragged_window, |
| SnapPosition desired_snap_position, |
| const gfx::Point& last_location_in_screen, |
| WindowSnapActionSource snap_action_source) { |
| DCHECK(!window_util::IsDraggingTabs(dragged_window)); |
| EndWindowDragImpl(dragged_window, dragged_window->is_destroying(), |
| desired_snap_position, last_location_in_screen, |
| snap_action_source); |
| } |
| |
| void SplitViewController::OnWindowDragCanceled() { |
| if (split_view_divider_) |
| split_view_divider_->OnWindowDragEnded(); |
| } |
| |
| SplitViewController::SnapPosition SplitViewController::ComputeSnapPosition( |
| const gfx::Point& last_location_in_screen) { |
| const int divider_position = InSplitViewMode() ? this->divider_position() |
| : GetDefaultDividerPosition(); |
| const int position = IsLayoutHorizontal(root_window_) |
| ? last_location_in_screen.x() |
| : last_location_in_screen.y(); |
| return (position <= divider_position) == IsLayoutPrimary(root_window_) |
| ? SnapPosition::kPrimary |
| : SnapPosition::kSecondary; |
| } |
| |
| bool SplitViewController::BoundsChangeIsFromVKAndAllowed( |
| aura::Window* window) const { |
| // Make sure that it is the bottom window who is requiring bounds change. |
| return features::IsAdjustSplitViewForVKEnabled() && changing_bounds_by_vk_ && |
| window == (IsLayoutPrimary(window) ? secondary_window_.get() |
| : primary_window_.get()); |
| } |
| |
| void SplitViewController::AddObserver(SplitViewObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void SplitViewController::RemoveObserver(SplitViewObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void SplitViewController::MaybeDetachWindow(aura::Window* dragged_window) { |
| // If one of the windows in the snap group is dragged, it may result in |
| // ending split view and some post processings to be done such as removing |
| // snap group and split view divider. |
| auto* snap_group_controller = SnapGroupController::Get(); |
| if (snap_group_controller && InSplitViewMode() && !IsResizingWithDivider() && |
| !Shell::Get()->overview_controller()->InOverviewSession()) { |
| OnSnappedWindowDetached(dragged_window, |
| WindowDetachedReason::kWindowDragged); |
| } |
| } |
| |
| void SplitViewController::OpenPartialOverviewToUpdateSnapGroup( |
| SnapPosition snap_position) { |
| // Update the value of `default_snap_position_` so that the other snap |
| // position will not been observed in `OnOverviewModeStarting()`. |
| default_snap_position_ = snap_position; |
| |
| if (!IsInOverviewSession() && !DesksController::Get()->animation()) { |
| SnapGroupController::Get()->RemoveSnapGroupContainingWindow( |
| primary_window_); |
| split_view_divider_.reset(); |
| aura::Window* window = snap_position == SnapPosition::kPrimary |
| ? primary_window_ |
| : secondary_window_; |
| RootWindowController::ForWindow(window)->StartSplitViewOverviewSession( |
| window, OverviewStartAction::kSplitView, |
| OverviewEnterExitType::kNormal); |
| } |
| } |
| |
| void SplitViewController::OnWindowPropertyChanged(aura::Window* window, |
| const void* key, |
| intptr_t old) { |
| // If the window's resizibility property changes (must be from resizable -> |
| // unresizable), end the split view mode and also end overview mode if |
| // overview mode is active at the moment. |
| if (key != aura::client::kResizeBehaviorKey) |
| return; |
| |
| // It is possible the property gets updated and is still the same value. |
| if (window->GetProperty(aura::client::kResizeBehaviorKey) == |
| static_cast<int>(old)) { |
| return; |
| } |
| |
| if (CanSnapWindow(window)) |
| return; |
| |
| EndSplitView(); |
| Shell::Get()->overview_controller()->EndOverview( |
| OverviewEndAction::kSplitView); |
| ShowAppCannotSnapToast(); |
| } |
| |
| void SplitViewController::OnWindowBoundsChanged( |
| aura::Window* window, |
| const gfx::Rect& old_bounds, |
| const gfx::Rect& new_bounds, |
| ui::PropertyChangeReason reason) { |
| CHECK_EQ(root_window_, window->GetRootWindow()); |
| |
| if (InTabletSplitViewMode() && IsResizingWithDivider()) { |
| // Bounds may be changed while we are processing a resize event. In this |
| // case, we don't update the windows transform here, since it will be done |
| // soon anyway. If we are *not* currently processing a resize, it means the |
| // bounds of a window have been updated "async", and we need to update the |
| // window's transform. |
| if (!split_view_divider_->processing_resize_event_) { |
| SetWindowsTransformDuringResizing(); |
| } |
| return; |
| } |
| } |
| |
| void SplitViewController::OnWindowDestroyed(aura::Window* window) { |
| DCHECK(InSplitViewMode()); |
| DCHECK(IsWindowInSplitView(window)); |
| auto iter = snapping_window_transformed_bounds_map_.find(window); |
| if (iter != snapping_window_transformed_bounds_map_.end()) { |
| snapping_window_transformed_bounds_map_.erase(iter); |
| } |
| |
| OnSnappedWindowDetached(window, WindowDetachedReason::kWindowDestroyed); |
| |
| if (to_be_activated_window_ == window) { |
| to_be_activated_window_ = nullptr; |
| } |
| } |
| |
| void SplitViewController::OnPostWindowStateTypeChange( |
| WindowState* window_state, |
| WindowStateType old_type) { |
| DCHECK_EQ( |
| window_state->GetDisplay().id(), |
| display::Screen::GetScreen()->GetDisplayNearestWindow(root_window_).id()); |
| |
| aura::Window* window = window_state->window(); |
| if (window_state->IsSnapped()) { |
| bool do_divider_spawn_animation = false; |
| // Only need to do divider spawn animation if split view is to be active, |
| // window is not minimized and has an non-identify transform in tablet mode. |
| // If window|is currently minimized then it will undergo the unminimizing |
| // animation instead, therefore skip the divider spawn animation if |
| // the window is minimized. |
| if (state_ == State::kNoSnap && |
| split_view_type_ == SplitViewType::kTabletType && |
| old_type != WindowStateType::kMinimized && |
| !window->transform().IsIdentity()) { |
| // For the divider spawn animation, at the end of the delay, the divider |
| // shall be visually aligned with an edge of |window|. This effect will |
| // be more easily achieved after |window| has been snapped and the |
| // corresponding transform animation has begun. So for now, just set a |
| // flag to indicate that the divider spawn animation should be done. |
| do_divider_spawn_animation = true; |
| } |
| OnWindowSnapped(window, old_type); |
| if (do_divider_spawn_animation) |
| DoSplitDividerSpawnAnimation(window); |
| } else if (window_state->IsNormalStateType() || window_state->IsMaximized() || |
| window_state->IsFullscreen() || window_state->IsFloated()) { |
| // End split view, and also overview if overview is active, in these cases: |
| // 1. A left clamshell split view window gets unsnapped by Alt+[. |
| // 2. A right clamshell split view window gets unsnapped by Alt+]. |
| // 3. A (clamshell or tablet) split view window gets maximized. |
| // 4. A (clamshell or tablet) split view window becomes full screen. |
| // 5. A split view window becomes floated. |
| EndSplitView(); |
| Shell::Get()->overview_controller()->EndOverview( |
| OverviewEndAction::kSplitView); |
| } else if (window_state->IsMinimized()) { |
| OnSnappedWindowDetached(window, WindowDetachedReason::kWindowMinimized); |
| |
| if (!InSplitViewMode()) { |
| // We have different behaviors for a minimized window: in tablet splitview |
| // mode, we'll insert the minimized window back to overview, as normally |
| // the window is not supposed to be minmized in tablet mode. And in |
| // clamshell splitview mode, we respect the minimization of the window |
| // and end overview instead. |
| if (split_view_type_ == SplitViewType::kTabletType) { |
| InsertWindowToOverview(window); |
| } else { |
| Shell::Get()->overview_controller()->EndOverview( |
| OverviewEndAction::kSplitView); |
| } |
| } |
| } |
| } |
| |
| void SplitViewController::OnPinnedStateChanged(aura::Window* pinned_window) { |
| // Disable split view for pinned windows. |
| if (WindowState::Get(pinned_window)->IsPinned() && InSplitViewMode()) |
| EndSplitView(EndReason::kUnsnappableWindowActivated); |
| } |
| |
| void SplitViewController::OnOverviewModeStarting() { |
| CHECK(InSplitViewMode()); |
| |
| // While in clamshell split view mode without being in a snap group |
| // creation session, a full overview session should be triggered. In this |
| // case, split view should end. |
| if (InClamshellSplitViewMode() && |
| !RootWindowController::ForWindow(root_window_) |
| ->split_view_overview_session()) { |
| EndSplitView(); |
| return; |
| } |
| |
| // If split view mode is active, reset |state_| to make it be able to select |
| // another window from overview window grid. |
| if (default_snap_position_ == SnapPosition::kPrimary) { |
| StopObserving(SnapPosition::kSecondary); |
| } else if (default_snap_position_ == SnapPosition::kSecondary) { |
| StopObserving(SnapPosition::kPrimary); |
| } |
| UpdateStateAndNotifyObservers(); |
| } |
| |
| void SplitViewController::OnOverviewModeEnding( |
| OverviewSession* overview_session) { |
| DCHECK(InSplitViewMode()); |
| |
| // If overview is ended because of a window getting snapped, suppress the |
| // overview exiting animation. |
| if (state_ == State::kBothSnapped) |
| overview_session->SetWindowListNotAnimatedWhenExiting(root_window_); |
| |
| // If clamshell split view mode is active, bail out. `OnOverviewModeEnded` |
| // will end split view. We do not end split view here, because that would mess |
| // up histograms of overview exit animation smoothness. |
| if (split_view_type_ == SplitViewType::kClamshellType) |
| return; |
| |
| // Tablet split view mode is active. If it still only has one snapped window, |
| // snap the first snappable window in the overview grid on the other side. |
| if (state_ == State::kBothSnapped) { |
| return; |
| } |
| |
| OverviewGrid* current_grid = |
| overview_session->GetGridWithRootWindow(root_window_); |
| if (!current_grid || current_grid->empty()) { |
| return; |
| } |
| |
| for (const auto& overview_item : current_grid->window_list()) { |
| aura::Window* window = overview_item->GetWindow(); |
| if (window != GetDefaultSnappedWindow()) { |
| absl::optional<float> snap_ratio = ComputeSnapRatio(window); |
| if (snap_ratio) { |
| const bool was_active = |
| overview_session->IsWindowActiveWindowBeforeOverview(window); |
| // Remove the overview item before snapping because the overview session |
| // is unavailable to retrieve outside this function after |
| // OnOverviewEnding is notified. |
| overview_item->RestoreWindow(/*reset_transform=*/false, |
| /*animate=*/true); |
| overview_session->RemoveItem(overview_item.get()); |
| |
| SnapWindow(window, |
| (default_snap_position_ == SnapPosition::kPrimary) |
| ? SnapPosition::kSecondary |
| : SnapPosition::kPrimary, |
| WindowSnapActionSource::kAutoSnapInSplitView, |
| /*activate_window=*/false, *snap_ratio); |
| if (was_active) { |
| wm::ActivateWindow(window); |
| } |
| |
| // If ending overview causes a window to snap, also do not do exiting |
| // overview animation. |
| overview_session->SetWindowListNotAnimatedWhenExiting(root_window_); |
| return; |
| } |
| } |
| } |
| |
| // The overview grid has at least one window, but has none that can be snapped |
| // in split view. If overview is ending because of switching between virtual |
| // desks, then there is no need to do anything here. Otherwise, end split view |
| // and show the cannot snap toast. |
| if (DesksController::Get()->AreDesksBeingModified()) { |
| return; |
| } |
| |
| EndSplitView(); |
| ShowAppCannotSnapToast(); |
| } |
| |
| void SplitViewController::OnOverviewModeEnded() { |
| DCHECK(InSplitViewMode()); |
| if (split_view_type_ == SplitViewType::kClamshellType && |
| !IsSnapGroupEnabledInClamshellMode()) { |
| EndSplitView(); |
| } |
| } |
| |
| void SplitViewController::OnDisplayRemoved( |
| const display::Display& old_display) { |
| // If the `root_window_`is the root window of the display which is going to |
| // be removed, there's no need to start overview. |
| if (GetRootWindowSettings(root_window_)->display_id == |
| display::kInvalidDisplayId) { |
| return; |
| } |
| |
| // If we are in tablet split view with only one snapped window, make sure we |
| // are in overview (see https://crbug.com/1027179). |
| if (state_ == State::kPrimarySnapped || state_ == State::kSecondarySnapped) { |
| aura::Window* window = |
| primary_window_ ? primary_window_ : secondary_window_; |
| RootWindowController::ForWindow(window)->StartSplitViewOverviewSession( |
| window, OverviewStartAction::kSplitView, |
| OverviewEnterExitType::kImmediateEnter); |
| } |
| } |
| |
| void SplitViewController::OnDisplayMetricsChanged( |
| const display::Display& display, |
| uint32_t metrics) { |
| // Avoid |ScreenAsh::GetDisplayNearestWindow|, which has a |DCHECK| that fails |
| // if the display is being deleted. Use |GetRootWindowSettings| directly, and |
| // if the display is being deleted, we will get |display::kInvalidDisplayId|. |
| if (GetRootWindowSettings(root_window_)->display_id != display.id()) |
| return; |
| |
| // We need to update |is_previous_layout_right_side_up_| even if split view |
| // mode is not active. |
| const bool is_previous_layout_right_side_up = |
| is_previous_layout_right_side_up_; |
| is_previous_layout_right_side_up_ = IsLayoutPrimary(display); |
| |
| if (!InSplitViewMode()) |
| return; |
| |
| // If one of the snapped windows becomes unsnappable, end the split view mode |
| // directly. |
| if ((primary_window_ && !CanSnapWindow(primary_window_)) || |
| (secondary_window_ && !CanSnapWindow(secondary_window_))) { |
| if (!Shell::Get()->session_controller()->IsUserSessionBlocked()) |
| EndSplitView(); |
| return; |
| } |
| |
| // In clamshell split view mode, the divider position will be adjusted in |
| // |OnWindowBoundsChanged|. |
| if (split_view_type_ == SplitViewType::kClamshellType) |
| return; |
| |
| // Before adjusting the divider position for the new display metrics, if the |
| // divider is animating to a snap position, then stop it and shove it there. |
| // Postpone `EndSplitViewAfterResizingAtEdgeIfAppropriate()` until after the |
| // adjustment, because the new display metrics will be used to compare the |
| // divider position against the edges of the screen. |
| if (IsDividerAnimating()) { |
| StopAndShoveAnimatedDivider(); |
| EndResizeWithDividerImpl(); |
| } |
| |
| if ((metrics & display::DisplayObserver::DISPLAY_METRIC_ROTATION) || |
| (metrics & display::DisplayObserver::DISPLAY_METRIC_WORK_AREA)) { |
| // Set default `divider_closest_ratio_` to kFixedPositionRatios[1]. |
| if (std::isnan(divider_closest_ratio_)) |
| divider_closest_ratio_ = kFixedPositionRatios[1]; |
| |
| // Reverse the position ratio if top/left window changes. |
| if (is_previous_layout_right_side_up != IsLayoutPrimary(display)) |
| divider_closest_ratio_ = 1.f - divider_closest_ratio_; |
| divider_position_ = |
| static_cast<int>(divider_closest_ratio_ * GetDividerEndPosition()) - |
| kSplitviewDividerShortSideLength / 2; |
| } |
| |
| // For other display configuration changes, we only move the divider to the |
| // closest fixed position. |
| if (!IsResizingWithDivider()) { |
| divider_position_ = GetClosestFixedDividerPosition(); |
| } |
| |
| EndSplitViewAfterResizingAtEdgeIfAppropriate(); |
| if (!InSplitViewMode()) |
| return; |
| |
| NotifyDividerPositionChanged(); |
| UpdateSnappedWindowsAndDividerBounds(); |
| } |
| |
| void SplitViewController::OnTabletModeStarting() { |
| split_view_type_ = SplitViewType::kTabletType; |
| } |
| |
| void SplitViewController::OnTabletModeStarted() { |
| is_previous_layout_right_side_up_ = IsCurrentScreenOrientationPrimary(); |
| // If splitview is active when tablet mode is starting, create the split view |
| // divider if not exists and adjust the `divider_position_` to be one |
| // of the fixed positions. |
| if (InSplitViewMode()) { |
| divider_position_ = GetClosestFixedDividerPosition(); |
| if (!split_view_divider_) { |
| split_view_divider_ = std::make_unique<SplitViewDivider>(this); |
| } |
| |
| UpdateSnappedWindowsAndDividerBounds(); |
| NotifyDividerPositionChanged(); |
| } |
| } |
| |
| void SplitViewController::OnTabletModeEnding() { |
| split_view_type_ = SplitViewType::kClamshellType; |
| |
| const bool is_divider_animating = IsDividerAnimating(); |
| if (IsResizingWithDivider() || is_divider_animating) { |
| split_view_divider_->set_is_resizing_with_divider(false); |
| if (is_divider_animating) { |
| StopAndShoveAnimatedDivider(); |
| } |
| |
| EndResizeWithDividerImpl(); |
| } |
| |
| // There is no divider in clamshell split view unless the two snapped windows |
| // belong to a snap group. |
| auto* snap_group_controller = SnapGroupController::Get(); |
| if (state_ != State::kBothSnapped || !snap_group_controller || |
| !snap_group_controller->AreWindowsInSnapGroup(primary_window_, |
| secondary_window_)) { |
| split_view_divider_.reset(); |
| } |
| } |
| |
| void SplitViewController::OnTabletModeEnded() { |
| is_previous_layout_right_side_up_ = true; |
| } |
| |
| void SplitViewController::OnAccessibilityStatusChanged() { |
| // TODO(crubg.com/853588): Exit split screen if ChromeVox is turned on until |
| // they are compatible. |
| if (Shell::Get()->accessibility_controller()->spoken_feedback().enabled()) |
| EndSplitView(); |
| } |
| |
| void SplitViewController::OnAccessibilityControllerShutdown() { |
| Shell::Get()->accessibility_controller()->RemoveObserver(this); |
| } |
| |
| void SplitViewController::OnKeyboardOccludedBoundsChanged( |
| const gfx::Rect& screen_bounds) { |
| if (!features::IsAdjustSplitViewForVKEnabled()) |
| return; |
| |
| // The window only needs to be moved if it is in the portrait mode. |
| if (IsLayoutHorizontal(root_window_)) |
| return; |
| |
| // We only modify the bottom window if there is one and the current active |
| // input field is in the bottom window. |
| aura::Window* bottom_window = GetPhysicalRightOrBottomWindow(); |
| if (!bottom_window && |
| !bottom_window->Contains(window_util::GetActiveWindow())) { |
| return; |
| } |
| |
| // If the virtual keyboard is disabled, restore to original layout. |
| if (screen_bounds.IsEmpty()) { |
| UpdateSnappedWindowsAndDividerBounds(); |
| return; |
| } |
| |
| // Get current active input field. |
| auto* text_input_client = GetCurrentInputMethod()->GetTextInputClient(); |
| if (!text_input_client) |
| return; |
| |
| const gfx::Rect caret_bounds = text_input_client->GetCaretBounds(); |
| if (caret_bounds == gfx::Rect()) |
| return; |
| |
| // Move the bottom window if the caret is less than `kMinCaretKeyboardDist` |
| // dip above the upper bounds of the virtual keyboard. |
| const int keyboard_occluded_y = screen_bounds.y(); |
| if (keyboard_occluded_y - caret_bounds.bottom() > kMinCaretKeyboardDist) |
| return; |
| |
| // Move bottom window above the virtual keyboard but the upper bounds cannot |
| // exceeds `kMinDividerPositionRatio` of the screen height. |
| gfx::Rect bottom_bounds = bottom_window->GetBoundsInScreen(); |
| const gfx::Rect work_area = |
| screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| root_window_); |
| const int y = |
| std::max(keyboard_occluded_y - bottom_bounds.height(), |
| static_cast<int>(work_area.y() + |
| work_area.height() * kMinDividerPositionRatio)); |
| bottom_bounds.set_y(y); |
| bottom_bounds.set_height(keyboard_occluded_y - y); |
| |
| int divider_position = y - kSplitviewDividerShortSideLength; |
| |
| // Set bottom window bounds. |
| { |
| base::AutoReset<bool> enable_bounds_change(&changing_bounds_by_vk_, true); |
| bottom_window->SetBoundsInScreen( |
| bottom_bounds, |
| display::Screen::GetScreen()->GetDisplayNearestWindow(root_window_)); |
| } |
| |
| // Set split view divider bounds. |
| split_view_divider_->divider_widget()->SetBounds( |
| SplitViewDivider::GetDividerBoundsInScreen(work_area, /*landscape=*/false, |
| divider_position, |
| /*is_dragging=*/false)); |
| // Make split view divider unadjustable. |
| split_view_divider_->SetAdjustable(false); |
| } |
| |
| void SplitViewController::OnWindowActivated(ActivationReason reason, |
| aura::Window* gained_active, |
| aura::Window* lost_active) { |
| if (!features::IsAdjustSplitViewForVKEnabled()) { |
| return; |
| } |
| |
| // If the bottom window is moved for the virtual keyboard (the split view |
| // divider bar is unadjustable), when the bottom window lost active, restore |
| // to the original layout. |
| if (!split_view_divider_ || split_view_divider_->IsAdjustable()) { |
| return; |
| } |
| |
| if (IsLayoutHorizontal(root_window_)) { |
| return; |
| } |
| |
| aura::Window* bottom_window = GetPhysicalRightOrBottomWindow(); |
| if (!bottom_window) |
| return; |
| |
| if (bottom_window->Contains(lost_active) && |
| !bottom_window->Contains(gained_active)) { |
| UpdateSnappedWindowsAndDividerBounds(); |
| } |
| } |
| |
| void SplitViewController::OnSnapGroupCreated() { |
| CHECK(IsSnapGroupEnabledInClamshellMode()); |
| CreateSplitViewDividerInClamshell(); |
| } |
| |
| void SplitViewController::OnSnapGroupRemoved() { |
| CHECK(Shell::Get()->snap_group_controller()); |
| split_view_divider_.reset(); |
| } |
| |
| aura::Window* SplitViewController::GetPhysicalLeftOrTopWindow() { |
| DCHECK(root_window_); |
| return IsLayoutPrimary(root_window_) ? primary_window_.get() |
| : secondary_window_.get(); |
| } |
| |
| aura::Window* SplitViewController::GetPhysicalRightOrBottomWindow() { |
| DCHECK(root_window_); |
| return IsLayoutPrimary(root_window_) ? secondary_window_.get() |
| : primary_window_.get(); |
| } |
| |
| void SplitViewController::StartObserving(aura::Window* window) { |
| if (window && !window->HasObserver(this)) { |
| Shell::Get()->shadow_controller()->UpdateShadowForWindow(window); |
| window->AddObserver(this); |
| WindowState::Get(window)->AddObserver(this); |
| if (split_view_divider_) { |
| split_view_divider_->AddObservedWindow(window); |
| } |
| } |
| } |
| |
| void SplitViewController::StopObserving(SnapPosition snap_position) { |
| aura::Window* window = GetSnappedWindow(snap_position); |
| if (window == primary_window_) { |
| primary_window_ = nullptr; |
| } else { |
| secondary_window_ = nullptr; |
| } |
| |
| if (window && window->HasObserver(this)) { |
| window->RemoveObserver(this); |
| WindowState::Get(window)->RemoveObserver(this); |
| if (split_view_divider_) |
| split_view_divider_->RemoveObservedWindow(window); |
| Shell::Get()->shadow_controller()->UpdateShadowForWindow(window); |
| |
| // It's possible that when we try to snap an ARC app window, while we are |
| // waiting for its state/bounds to the expected state/bounds, another window |
| // snap request comes in and causing the previous to-be-snapped window to |
| // be un-observed, in this case we should restore the previous to-be-snapped |
| // window's transform if it's unidentity. |
| RestoreTransformIfApplicable(window); |
| } |
| } |
| |
| void SplitViewController::UpdateStateAndNotifyObservers() { |
| State previous_state = state_; |
| if (IsSnapped(primary_window_) && IsSnapped(secondary_window_)) { |
| state_ = State::kBothSnapped; |
| } else if (IsSnapped(primary_window_)) { |
| state_ = State::kPrimarySnapped; |
| } else if (IsSnapped(secondary_window_)) { |
| state_ = State::kSecondarySnapped; |
| } else { |
| state_ = State::kNoSnap; |
| } |
| |
| // We still notify observers even if |state_| doesn't change as it's possible |
| // to snap a window to a position that already has a snapped window. However, |
| // |previous_state| and |state_| cannot both be |State::kNoSnap|. |
| // When |previous_state| is |State::kNoSnap|, it indicates to |
| // observers that split view mode started. Likewise, when |state_| is |
| // |State::kNoSnap|, it indicates to observers that split view mode |
| // ended. |
| DCHECK(previous_state != State::kNoSnap || state_ != State::kNoSnap); |
| for (auto& observer : observers_) { |
| observer.OnSplitViewStateChanged(previous_state, state_); |
| } |
| } |
| |
| void SplitViewController::NotifyDividerPositionChanged() { |
| for (auto& observer : observers_) |
| observer.OnSplitViewDividerPositionChanged(); |
| } |
| |
| void SplitViewController::NotifyWindowResized() { |
| for (auto& observer : observers_) |
| observer.OnSplitViewWindowResized(); |
| } |
| |
| void SplitViewController::NotifyWindowSwapped() { |
| for (auto& observer : observers_) |
| observer.OnSplitViewWindowSwapped(); |
| } |
| |
| void SplitViewController::UpdateDividerPositionOnWindowResize( |
| aura::Window* window, |
| const gfx::Rect& new_bounds) { |
| CHECK_EQ(root_window_, window->GetRootWindow()); |
| const gfx::Rect work_area = |
| screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| root_window_); |
| |
| if (IsLayoutHorizontal(window)) { |
| divider_position_ = window == primary_window_ |
| ? new_bounds.width() |
| : work_area.width() - new_bounds.width(); |
| } else { |
| divider_position_ = window == primary_window_ |
| ? new_bounds.height() |
| : work_area.height() - new_bounds.height(); |
| } |
| NotifyDividerPositionChanged(); |
| } |
| |
| void SplitViewController::MaybeEndOverviewOnWindowResize(aura::Window* window) { |
| const int divider_end_position(GetDividerEndPosition()); |
| if (divider_position_ < divider_end_position * chromeos::kOneThirdSnapRatio || |
| divider_position_ > divider_end_position * chromeos::kTwoThirdSnapRatio) { |
| // Ending overview will also end clamshell split view unless |
| // `SnapGroupController::IsArm1AutomaticallyLockEnabled()` returns true. |
| Shell::Get()->overview_controller()->EndOverview( |
| OverviewEndAction::kSplitView); |
| WindowState::Get(window)->Maximize(); |
| } |
| } |
| |
| void SplitViewController::CreateSplitViewDividerInClamshell() { |
| CHECK(InClamshellSplitViewMode()); |
| divider_position_ = GetClosestFixedDividerPosition(); |
| split_view_divider_ = std::make_unique<SplitViewDivider>(this); |
| UpdateSnappedWindowsAndDividerBounds(); |
| NotifyDividerPositionChanged(); |
| } |
| |
| void SplitViewController::UpdateBlackScrim( |
| const gfx::Point& location_in_screen) { |
| DCHECK(InSplitViewMode()); |
| |
| if (!black_scrim_layer_) { |
| // Create an invisible black scrim layer. |
| black_scrim_layer_ = std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR); |
| black_scrim_layer_->SetColor(AshColorProvider::Get()->GetBackgroundColor()); |
| // Set the black scrim layer underneath split view divider. |
| auto* divider_layer = |
| split_view_divider_->divider_widget()->GetNativeWindow()->layer(); |
| auto* divider_parent_layer = divider_layer->parent(); |
| divider_parent_layer->Add(black_scrim_layer_.get()); |
| divider_parent_layer->StackBelow(black_scrim_layer_.get(), divider_layer); |
| } |
| |
| // Decide where the black scrim should show and update its bounds. |
| SnapPosition position = GetBlackScrimPosition(location_in_screen); |
| if (position == SnapPosition::kNone) { |
| black_scrim_layer_.reset(); |
| return; |
| } |
| black_scrim_layer_->SetBounds(GetSnappedWindowBoundsInScreen( |
| position, /*window_for_minimum_size=*/nullptr)); |
| |
| // Update its opacity. The opacity increases as it gets closer to the edge of |
| // the screen. |
| const int location = IsLayoutHorizontal(root_window_) |
| ? location_in_screen.x() |
| : location_in_screen.y(); |
| gfx::Rect work_area_bounds = |
| screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| root_window_); |
| if (!IsLayoutHorizontal(root_window_)) |
| work_area_bounds.Transpose(); |
| float opacity = kBlackScrimOpacity; |
| const float ratio = chromeos::kOneThirdSnapRatio - kBlackScrimFadeInRatio; |
| const int distance = std::min(std::abs(location - work_area_bounds.x()), |
| std::abs(work_area_bounds.right() - location)); |
| if (distance > work_area_bounds.width() * ratio) { |
| opacity -= kBlackScrimOpacity * |
| (distance - work_area_bounds.width() * ratio) / |
| (work_area_bounds.width() * kBlackScrimFadeInRatio); |
| opacity = std::max(opacity, 0.f); |
| } |
| black_scrim_layer_->SetOpacity(opacity); |
| } |
| |
| void SplitViewController::UpdateResizeBackdrop() { |
| // Creates a backdrop layer. It is stacked below the snapped window. |
| auto create_backdrop = [](aura::Window* window) { |
| auto resize_backdrop_layer = |
| std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR); |
| |
| ui::Layer* parent = window->layer()->parent(); |
| ui::Layer* stacking_target = window->layer(); |
| parent->Add(resize_backdrop_layer.get()); |
| parent->StackBelow(resize_backdrop_layer.get(), stacking_target); |
| |
| return resize_backdrop_layer; |
| }; |
| |
| // Updates the bounds and color of a backdrop. |
| auto update_backdrop = [this](SnapPosition position, aura::Window* window, |
| ui::Layer* backdrop) { |
| backdrop->SetBounds(GetSnappedWindowBoundsInParent(position, nullptr)); |
| backdrop->SetColor(window->GetProperty( |
| wm::IsActiveWindow(window) ? chromeos::kFrameActiveColorKey |
| : chromeos::kFrameInactiveColorKey)); |
| }; |
| |
| if (state_ == State::kPrimarySnapped || state_ == State::kBothSnapped) { |
| if (!left_resize_backdrop_layer_) |
| left_resize_backdrop_layer_ = create_backdrop(primary_window_); |
| update_backdrop(SnapPosition::kPrimary, primary_window_, |
| left_resize_backdrop_layer_.get()); |
| } |
| if (state_ == State::kSecondarySnapped || state_ == State::kBothSnapped) { |
| if (!right_resize_backdrop_layer_) |
| right_resize_backdrop_layer_ = create_backdrop(secondary_window_); |
| update_backdrop(SnapPosition::kSecondary, secondary_window_, |
| right_resize_backdrop_layer_.get()); |
| } |
| } |
| |
| void SplitViewController::UpdateSnappedWindowsAndDividerBounds() { |
| // Update the snapped windows' bounds. If the window is already snapped in the |
| // correct position, simply update the snap ratio. |
| if (IsSnapped(primary_window_)) { |
| UpdateSnappedBounds(primary_window_); |
| } |
| if (IsSnapped(secondary_window_)) { |
| UpdateSnappedBounds(secondary_window_); |
| } |
| |
| // Update divider's bounds and make it adjustable. |
| if (split_view_divider_) { |
| split_view_divider_->UpdateDividerBounds(); |
| |
| // Make the split view divider adjustable. |
| if (features::IsAdjustSplitViewForVKEnabled()) { |
| split_view_divider_->SetAdjustable(true); |
| } |
| } |
| } |
| |
| void SplitViewController::UpdateSnappedBounds(aura::Window* window) { |
| DCHECK(IsWindowInSplitView(window)); |
| WindowState* window_state = WindowState::Get(window); |
| // SplitViewController will use the divider position to determine the |
| // window's snapped bounds. |
| const bool in_tablet_mode = IsInTabletMode(); |
| // TODO(b/264962634): Remove this workaround. Probably, we can rewrite |
| // `TabletModeWindowState::UpdateWindowPosition` to include this logic. |
| if (window->GetProperty(aura::client::kAppType) == |
| static_cast<int>(AppType::ARC_APP)) { |
| const gfx::Rect requested_bounds = |
| in_tablet_mode |
| ? TabletModeWindowState::GetBoundsInTabletMode(window_state) |
| : GetSnappedWindowBoundsInScreen(GetPositionOfSnappedWindow(window), |
| window); |
| const SetBoundsWMEvent event(requested_bounds, |
| /*animate=*/true); |
| window_state->OnWMEvent(&event); |
| return; |
| } |
| |
| if (in_tablet_mode) { |
| TabletModeWindowState::UpdateWindowPosition( |
| window_state, WindowState::BoundsChangeAnimationType::kAnimate); |
| return; |
| } else if (IsSnapGroupEnabledInClamshellMode()) { |
| const gfx::Rect requested_bounds = GetSnappedWindowBoundsInScreen( |
| GetPositionOfSnappedWindow(window), window); |
| const SetBoundsWMEvent event(requested_bounds, /*animate=*/true); |
| window_state->OnWMEvent(&event); |
| return; |
| } |
| |
| window_state->UpdateSnappedBounds(); |
| } |
| |
| SplitViewController::SnapPosition SplitViewController::GetBlackScrimPosition( |
| const gfx::Point& location_in_screen) { |
| const gfx::Rect work_area_bounds = |
| screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| root_window_); |
| if (!work_area_bounds.Contains(location_in_screen)) |
| return SnapPosition::kNone; |
| |
| gfx::Size primary_window_min_size, secondary_window_min_size; |
| if (primary_window_ && primary_window_->delegate()) |
| primary_window_min_size = primary_window_->delegate()->GetMinimumSize(); |
| if (secondary_window_ && secondary_window_->delegate()) |
| secondary_window_min_size = secondary_window_->delegate()->GetMinimumSize(); |
| |
| bool right_side_up = IsLayoutPrimary(root_window_); |
| int divider_end_position = GetDividerEndPosition(); |
| // The distance from the current resizing position to the left or right side |
| // of the screen. Note: left or right side here means the side of the |
| // |primary_window_| or |secondary_window_|. |
| int primary_window_distance = 0, secondary_window_distance = 0; |
| int min_left_length = 0, min_right_length = 0; |
| |
| if (IsLayoutHorizontal(root_window_)) { |
| int left_distance = location_in_screen.x() - work_area_bounds.x(); |
| int right_distance = work_area_bounds.right() - location_in_screen.x(); |
| primary_window_distance = right_side_up ? left_distance : right_distance; |
| secondary_window_distance = right_side_up ? right_distance : left_distance; |
| |
| min_left_length = primary_window_min_size.width(); |
| min_right_length = secondary_window_min_size.width(); |
| } else { |
| int top_distance = location_in_screen.y() - work_area_bounds.y(); |
| int bottom_distance = work_area_bounds.bottom() - location_in_screen.y(); |
| primary_window_distance = right_side_up ? top_distance : bottom_distance; |
| secondary_window_distance = right_side_up ? bottom_distance : top_distance; |
| |
| min_left_length = primary_window_min_size.height(); |
| min_right_length = secondary_window_min_size.height(); |
| } |
| |
| if (primary_window_distance < |
| divider_end_position * chromeos::kOneThirdSnapRatio || |
| primary_window_distance < min_left_length) { |
| return SnapPosition::kPrimary; |
| } |
| if (secondary_window_distance < |
| divider_end_position * chromeos::kOneThirdSnapRatio || |
| secondary_window_distance < min_right_length) { |
| return SnapPosition::kSecondary; |
| } |
| |
| return SnapPosition::kNone; |
| } |
| |
| void SplitViewController::UpdateDividerPosition( |
| const gfx::Point& location_in_screen) { |
| CHECK(split_view_divider_); |
| if (IsLayoutHorizontal(root_window_)) |
| divider_position_ += location_in_screen.x() - |
| split_view_divider_->previous_event_location_.x(); |
| else |
| divider_position_ += location_in_screen.y() - |
| split_view_divider_->previous_event_location_.y(); |
| divider_position_ = std::max(0, divider_position_); |
| } |
| |
| int SplitViewController::GetClosestFixedDividerPosition() { |
| // The values in |kFixedPositionRatios| represent the fixed position of the |
| // center of the divider while |divider_position_| represent the origin of the |
| // divider rectangle. So, before calling FindClosestFixedPositionRatio, |
| // extract the center from |divider_position_|. The result will also be the |
| // center of the divider, so extract the origin, unless the result is on of |
| // the endpoints. |
| int divider_end_position = GetDividerEndPosition(); |
| divider_closest_ratio_ = FindClosestPositionRatio( |
| float(divider_position_ + kSplitviewDividerShortSideLength / 2) / |
| divider_end_position); |
| int fixed_position = divider_end_position * divider_closest_ratio_; |
| if (divider_closest_ratio_ > 0.f && divider_closest_ratio_ < 1.f) { |
| fixed_position -= kSplitviewDividerShortSideLength / 2; |
| } |
| return fixed_position; |
| } |
| |
| void SplitViewController::StopAndShoveAnimatedDivider() { |
| CHECK(IsDividerAnimating()); |
| |
| StopSnapAnimation(); |
| NotifyDividerPositionChanged(); |
| UpdateSnappedWindowsAndDividerBounds(); |
| } |
| |
| void SplitViewController::StopSnapAnimation() { |
| divider_snap_animation_->Stop(); |
| divider_position_ = divider_snap_animation_->ending_position(); |
| } |
| |
| bool SplitViewController::ShouldEndSplitViewAfterResizingAtEdge() { |
| DCHECK(InTabletSplitViewMode() || IsSnapGroupEnabledInClamshellMode()); |
| |
| return divider_position_ == 0 || divider_position_ == GetDividerEndPosition(); |
| } |
| |
| void SplitViewController::EndSplitViewAfterResizingAtEdgeIfAppropriate() { |
| if (!ShouldEndSplitViewAfterResizingAtEdge()) { |
| return; |
| } |
| |
| aura::Window* active_window = GetActiveWindowAfterResizingUponExit(); |
| |
| // Track the window that needs to be put back into the overview list if we |
| // remain in overview mode. |
| aura::Window* insert_overview_window = nullptr; |
| if (IsInOverviewSession()) { |
| insert_overview_window = GetDefaultSnappedWindow(); |
| } |
| |
| EndSplitView(); |
| if (active_window) { |
| Shell::Get()->overview_controller()->EndOverview( |
| OverviewEndAction::kSplitView); |
| wm::ActivateWindow(active_window); |
| } else if (insert_overview_window) { |
| InsertWindowToOverview(insert_overview_window, /*animate=*/false); |
| } |
| } |
| |
| aura::Window* SplitViewController::GetActiveWindowAfterResizingUponExit() { |
| DCHECK(InSplitViewMode()); |
| |
| if (!ShouldEndSplitViewAfterResizingAtEdge()) { |
| return nullptr; |
| } |
| |
| return divider_position_ == 0 ? GetPhysicalRightOrBottomWindow() |
| : GetPhysicalLeftOrTopWindow(); |
| } |
| |
| int SplitViewController::GetDividerEndPosition() const { |
| const gfx::Rect work_area_bounds = |
| screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| root_window_); |
| return IsLayoutHorizontal(root_window_) ? work_area_bounds.width() |
| : work_area_bounds.height(); |
| } |
| |
| void SplitViewController::OnWindowSnapped( |
| aura::Window* window, |
| absl::optional<chromeos::WindowStateType> previous_state) { |
| RestoreTransformIfApplicable(window); |
| UpdateStateAndNotifyObservers(); |
| |
| SnapGroupController* snap_group_controller = SnapGroupController::Get(); |
| const bool snap_group_enabled_in_clamshell = |
| IsSnapGroupEnabledInClamshellMode(); |
| if (state_ == State::kBothSnapped && snap_group_enabled_in_clamshell && |
| snap_group_controller->IsArm1AutomaticallyLockEnabled()) { |
| // TODO(b/286963080): Move this to SnapGroupController. |
| CHECK(primary_window_); |
| CHECK(secondary_window_); |
| if (!snap_group_controller->AreWindowsInSnapGroup(primary_window_, |
| secondary_window_)) { |
| snap_group_controller->AddSnapGroup(primary_window_, secondary_window_); |
| } |
| |
| if (!split_view_divider_) { |
| CreateSplitViewDividerInClamshell(); |
| split_view_divider_->RefreshStackingOrder(); |
| } |
| } |
| |
| // If the snapped window was removed from overview and was the active window |
| // before entering overview, it should be the active window after snapping in |
| // splitview. |
| if (to_be_activated_window_ == window) { |
| to_be_activated_window_ = nullptr; |
| wm::ActivateWindow(window); |
| } |
| |
| // In tablet mode, if the window was previously floated, the other side is |
| // available, and there is another non-minimized window, do not enter overview |
| // but instead snap that window to the opposite side. |
| if (previous_state && |
| *previous_state == chromeos::WindowStateType::kFloated && |
| IsInTabletMode() && !BothSnapped()) { |
| auto mru_windows = |
| Shell::Get()->mru_window_tracker()->BuildWindowForCycleList( |
| kActiveDesk); |
| for (aura::Window* mru_window : mru_windows) { |
| auto* window_state = WindowState::Get(mru_window); |
| if (mru_window != window && !window_state->IsMinimized() && |
| window_state->CanSnap()) { |
| const SnapPosition snap_position = |
| GetPositionOfSnappedWindow(window) == SnapPosition::kPrimary |
| ? SnapPosition::kSecondary |
| : SnapPosition::kPrimary; |
| WindowSnapWMEvent event(snap_position == SnapPosition::kPrimary |
| ? WM_EVENT_SNAP_PRIMARY |
| : WM_EVENT_SNAP_SECONDARY, |
| WindowSnapActionSource::kAutoSnapInSplitView); |
| WindowState::Get(mru_window)->OnWMEvent(&event); |
| return; |
| } |
| } |
| } |
| |
| // Overview will be opened on the other side of the screen if there is |
| // only one snapped window in split screen when in tablet mode or clamshell |
| // mode when `CanEnterOverview()` returns true, the check will happen in |
| // `OverviewController`. |
| if (WillStartOverview()) { |
| RootWindowController::ForWindow(window)->StartSplitViewOverviewSession( |
| window, OverviewStartAction::kSplitView, |
| OverviewEnterExitType::kNormal); |
| return; |
| } |
| |
| // When `window` is snapped, it may push the divider position due to its |
| // minimum size constraint. If the other snap position holds a previously |
| // snapped window, we should update the other's snapped bounds to account for |
| // the newly snapped `window`. |
| if (primary_window_ && secondary_window_) { |
| UpdateSnappedBounds(window == primary_window_ ? secondary_window_.get() |
| : primary_window_.get()); |
| } |
| } |
| |
| void SplitViewController::OnSnappedWindowDetached(aura::Window* window, |
| WindowDetachedReason reason) { |
| const bool is_window_destroyed = |
| reason == WindowDetachedReason::kWindowDestroyed; |
| const SnapPosition position_of_snapped_window = |
| GetPositionOfSnappedWindow(window); |
| |
| // Detach it from splitview first if the window is to be destroyed to prevent |
| // unnecessary bounds/state update to it when ending splitview resizing. For |
| // the window that is not going to be destroyed, we still need its bounds and |
| // state to be updated to match the updated divider position before detaching |
| // it from splitview. |
| if (is_window_destroyed) { |
| StopObserving(position_of_snapped_window); |
| } |
| |
| // Stop resizing if one of the snapped window is detached from split |
| // view. |
| const bool is_divider_animating = IsDividerAnimating(); |
| if (IsResizingWithDivider() || is_divider_animating) { |
| split_view_divider_->set_is_resizing_with_divider(false); |
| if (is_divider_animating) { |
| StopAndShoveAnimatedDivider(); |
| } |
| EndResizeWithDividerImpl(); |
| } |
| |
| if (!is_window_destroyed) { |
| StopObserving(position_of_snapped_window); |
| } |
| |
| // End the Split View mode for the following two cases: |
| // 1. If there is no snapped window at this moment. Note that this will update |
| // overview window grid bounds if the overview mode is active at the moment; |
| // 2. When `kSnapGroup` is enabled and feature param |
| // `kAutomaticallyLockGroup` is true, `SplitViewController` will no longer |
| // manage the window(s) on one window detached. |
| if ((!primary_window_ && !secondary_window_) || |
| (IsSnapGroupEnabledInClamshellMode() && |
| (!primary_window_ || !secondary_window_))) { |
| EndSplitView(reason == WindowDetachedReason::kWindowDragged |
| ? EndReason::kWindowDragStarted |
| : EndReason::kNormal); |
| |
| // TODO(crbug.com/1351562): Consider not allowing one snapped window to be |
| // floated. Then this should be a DCHECK. |
| } else { |
| DCHECK(InTabletSplitViewMode()); |
| aura::Window* other_window = |
| GetSnappedWindow(position_of_snapped_window == SnapPosition::kPrimary |
| ? SnapPosition::kSecondary |
| : SnapPosition::kPrimary); |
| |
| if (reason == WindowDetachedReason::kWindowFloated && IsInTabletMode()) { |
| // Maximize the other window, which will end split view. |
| WMEvent event(WM_EVENT_MAXIMIZE); |
| WindowState::Get(other_window)->OnWMEvent(&event); |
| return; |
| } |
| |
| // If there is still one snapped window after minimizing/closing one snapped |
| // window, update its snap state and open overview window grid. |
| default_snap_position_ = |
| primary_window_ ? SnapPosition::kPrimary : SnapPosition::kSecondary; |
| UpdateStateAndNotifyObservers(); |
| RootWindowController::ForWindow(other_window) |
| ->StartSplitViewOverviewSession( |
| other_window, OverviewStartAction::kSplitView, |
| reason == WindowDetachedReason::kWindowDragged |
| ? OverviewEnterExitType::kImmediateEnter |
| : OverviewEnterExitType::kNormal); |
| } |
| } |
| |
| float SplitViewController::FindClosestPositionRatio(float current_ratio) { |
| float closest_ratio = 0.f; |
| std::vector<float> position_ratios( |
| kFixedPositionRatios, |
| kFixedPositionRatios + sizeof(kFixedPositionRatios) / sizeof(float)); |
| ModifyPositionRatios(&position_ratios); |
| for (const float ratio : position_ratios) { |
| if (std::abs(current_ratio - ratio) < |
| std::abs(current_ratio - closest_ratio)) { |
| closest_ratio = ratio; |
| } |
| } |
| return closest_ratio; |
| } |
| |
| void SplitViewController::ModifyPositionRatios( |
| std::vector<float>* out_position_ratios) { |
| const bool landscape = IsCurrentScreenOrientationLandscape(); |
| const int min_left_size = |
| GetMinimumWindowLength(GetPhysicalLeftOrTopWindow(), landscape); |
| const int min_right_size = |
| GetMinimumWindowLength(GetPhysicalRightOrBottomWindow(), landscape); |
| const int divider_end_position = GetDividerEndPosition(); |
| const float min_size_left_ratio = |
| static_cast<float>(min_left_size) / divider_end_position; |
| const float min_size_right_ratio = |
| static_cast<float>(min_right_size) / divider_end_position; |
| if (min_size_left_ratio <= chromeos::kOneThirdSnapRatio) { |
| out_position_ratios->push_back(chromeos::kOneThirdSnapRatio); |
| } |
| if (min_size_right_ratio <= chromeos::kOneThirdSnapRatio) { |
| out_position_ratios->push_back(chromeos::kTwoThirdSnapRatio); |
| } |
| |
| // Remove 0.5f if a window cannot be snapped. We can get into this state by |
| // snapping a window to two thirds. |
| if (min_size_left_ratio > chromeos::kDefaultSnapRatio || |
| min_size_right_ratio > chromeos::kDefaultSnapRatio) { |
| base::Erase(*out_position_ratios, chromeos::kDefaultSnapRatio); |
| } |
| } |
| |
| gfx::Point SplitViewController::GetEndDragLocationInScreen( |
| aura::Window* window, |
| const gfx::Point& location_in_screen) { |
| gfx::Point end_location(location_in_screen); |
| if (!IsWindowInSplitView(window)) |
| return end_location; |
| |
| const gfx::Rect bounds = GetSnappedWindowBoundsInScreen( |
| GetPositionOfSnappedWindow(window), window); |
| if (IsLayoutHorizontal(window)) { |
| end_location.set_x(window == GetPhysicalLeftOrTopWindow() ? bounds.right() |
| : bounds.x()); |
| } else { |
| end_location.set_y(window == GetPhysicalLeftOrTopWindow() ? bounds.bottom() |
| : bounds.y()); |
| } |
| return end_location; |
| } |
| |
| void SplitViewController::RestoreTransformIfApplicable(aura::Window* window) { |
| // If the transform of the window has been changed, calculate a good starting |
| // transform based on its transformed bounds before to be snapped. |
| auto iter = snapping_window_transformed_bounds_map_.find(window); |
| if (iter == snapping_window_transformed_bounds_map_.end()) |
| return; |
| |
| const gfx::Rect item_bounds = iter->second; |
| snapping_window_transformed_bounds_map_.erase(iter); |
| |
| // Restore the window's transform first if it's not identity. |
| if (!window->layer()->GetTargetTransform().IsIdentity()) { |
| // Calculate the starting transform based on the window's expected snapped |
| // bounds and its transformed bounds before to be snapped. |
| const gfx::Rect snapped_bounds = GetSnappedWindowBoundsInScreen( |
| GetPositionOfSnappedWindow(window), window); |
| const gfx::Transform starting_transform = gfx::TransformBetweenRects( |
| gfx::RectF(snapped_bounds), gfx::RectF(item_bounds)); |
| SetTransformWithAnimation(window, starting_transform, gfx::Transform()); |
| } |
| } |
| |
| void SplitViewController::SetWindowsTransformDuringResizing() { |
| DCHECK(InTabletSplitViewMode() || IsSnapGroupEnabledInClamshellMode()); |
| DCHECK_GE(divider_position_, 0); |
| const bool horizontal = IsLayoutHorizontal(root_window_); |
| aura::Window* left_or_top_window = GetPhysicalLeftOrTopWindow(); |
| aura::Window* right_or_bottom_window = GetPhysicalRightOrBottomWindow(); |
| |
| gfx::Transform left_or_top_transform; |
| if (left_or_top_window) { |
| const int left_size = divider_position_; |
| const int distance = |
| left_size - GetWindowLength(left_or_top_window, horizontal); |
| if (distance < 0) { |
| left_or_top_transform.Translate(horizontal ? distance : 0, |
| horizontal ? 0 : distance); |
| } |
| SetTransform(left_or_top_window, left_or_top_transform); |
| } |
| |
| gfx::Transform right_or_bottom_transform; |
| if (right_or_bottom_window) { |
| const int right_size = GetDividerEndPosition() - divider_position_ - |
| kSplitviewDividerShortSideLength; |
| const int distance = |
| right_size - GetWindowLength(right_or_bottom_window, horizontal); |
| if (distance < 0) { |
| right_or_bottom_transform.Translate(horizontal ? -distance : 0, |
| horizontal ? 0 : -distance); |
| } |
| SetTransform(right_or_bottom_window, right_or_bottom_transform); |
| } |
| } |
| |
| void SplitViewController::RestoreWindowsTransformAfterResizing() { |
| DCHECK(InSplitViewMode()); |
| if (primary_window_) |
| SetTransform(primary_window_, gfx::Transform()); |
| if (secondary_window_) |
| SetTransform(secondary_window_, gfx::Transform()); |
| if (black_scrim_layer_.get()) |
| black_scrim_layer_->SetTransform(gfx::Transform()); |
| } |
| |
| void SplitViewController::SetTransformWithAnimation( |
| aura::Window* window, |
| const gfx::Transform& start_transform, |
| const gfx::Transform& target_transform) { |
| const gfx::PointF target_origin = GetTargetBoundsInScreen(window).origin(); |
| for (auto* window_iter : GetTransientTreeIterator(window)) { |
| // Adjust |start_transform| and |target_transform| for the transient child. |
| aura::Window* parent_window = window_iter->parent(); |
| gfx::RectF original_bounds(window_iter->GetTargetBounds()); |
| ::wm::TranslateRectToScreen(parent_window, &original_bounds); |
| const gfx::PointF pivot(target_origin.x() - original_bounds.x(), |
| target_origin.y() - original_bounds.y()); |
| const gfx::Transform new_start_transform = |
| TransformAboutPivot(pivot, start_transform); |
| const gfx::Transform new_target_transform = |
| TransformAboutPivot(pivot, target_transform); |
| if (new_start_transform != window_iter->layer()->GetTargetTransform()) |
| window_iter->SetTransform(new_start_transform); |
| |
| std::vector<ui::ImplicitAnimationObserver*> animation_observers; |
| if (window_iter == window) { |
| animation_observers.push_back( |
| new WindowTransformAnimationObserver(window)); |
| |
| // If the overview exit animation is in progress or is about to start, add |
| // the |window| snap animation as one of the animations to be completed |
| // before |OverviewController::OnEndingAnimationComplete| should be called |
| // to unpause occlusion tracking, unblur the wallpaper, etc. |
| OverviewController* overview_controller = |
| Shell::Get()->overview_controller(); |
| if (overview_controller->IsCompletingShutdownAnimations() || |
| (overview_controller->overview_session() && |
| overview_controller->overview_session()->is_shutting_down() && |
| overview_controller->overview_session() |
| ->enter_exit_overview_type() != |
| OverviewEnterExitType::kImmediateExit)) { |
| auto overview_exit_animation_observer = |
| std::make_unique<ExitAnimationObserver>(); |
| animation_observers.push_back(overview_exit_animation_observer.get()); |
| overview_controller->AddExitAnimationObserver( |
| std::move(overview_exit_animation_observer)); |
| } |
| } |
| DoSplitviewTransformAnimation(window_iter->layer(), |
| SPLITVIEW_ANIMATION_SET_WINDOW_TRANSFORM, |
| new_target_transform, animation_observers); |
| } |
| } |
| |
| void SplitViewController::UpdateSnappingWindowTransformedBounds( |
| aura::Window* window) { |
| if (!window->layer()->GetTargetTransform().IsIdentity()) { |
| snapping_window_transformed_bounds_map_[window] = gfx::ToEnclosedRect( |
| window_util::GetTransformedBounds(window, /*top_inset=*/0)); |
| } |
| } |
| |
| void SplitViewController::InsertWindowToOverview(aura::Window* window, |
| bool animate) { |
| if (!window || !GetOverviewSession()) |
| return; |
| GetOverviewSession()->AddItemInMruOrder(window, /*reposition=*/true, animate, |
| /*restack=*/true, |
| /*use_spawn_animation=*/false); |
| } |
| |
| void SplitViewController::FinishWindowResizing(aura::Window* window) { |
| if (window != nullptr) { |
| WindowState* window_state = WindowState::Get(window); |
| if (window_state->is_dragged()) { |
| window_state->OnCompleteDrag(gfx::PointF( |
| GetEndDragLocationInScreen(window, previous_event_location_))); |
| window_state->DeleteDragDetails(); |
| } |
| } |
| } |
| |
| void SplitViewController::StartTabletResize() { |
| base::RecordAction(base::UserMetricsAction("SplitView_ResizeWindows")); |
| if (state_ == State::kBothSnapped) { |
| presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder( |
| split_view_divider_->divider_widget()->GetCompositor(), |
| kTabletSplitViewResizeMultiHistogram, |
| kTabletSplitViewResizeMultiMaxLatencyHistogram); |
| return; |
| } |
| |
| CHECK(GetOverviewSession()); |
| if (GetOverviewSession()->GetGridWithRootWindow(root_window_)->empty()) { |
| presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder( |
| split_view_divider_->divider_widget()->GetCompositor(), |
| kTabletSplitViewResizeSingleHistogram, |
| kTabletSplitViewResizeSingleMaxLatencyHistogram); |
| } else { |
| presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder( |
| split_view_divider_->divider_widget()->GetCompositor(), |
| kTabletSplitViewResizeWithOverviewHistogram, |
| kTabletSplitViewResizeWithOverviewMaxLatencyHistogram); |
| } |
| accumulated_drag_time_ticks_ = base::TimeTicks::Now(); |
| accumulated_drag_distance_ = 0; |
| |
| tablet_resize_mode_ = TabletResizeMode::kNormal; |
| } |
| |
| void SplitViewController::EndTabletResize() { |
| presentation_time_recorder_.reset(); |
| |
| // TODO(xdai): Use fade out animation instead of just removing it. |
| black_scrim_layer_.reset(); |
| |
| resize_timer_.Stop(); |
| tablet_resize_mode_ = TabletResizeMode::kNormal; |
| |
| const int target_divider_position = GetClosestFixedDividerPosition(); |
| // TODO(b/298515283): Separate Snap Group and tablet resize. |
| if (divider_position_ == target_divider_position || |
| IsSnapGroupEnabledInClamshellMode()) { |
| EndResizeWithDividerImpl(); |
| EndSplitViewAfterResizingAtEdgeIfAppropriate(); |
| } else { |
| divider_snap_animation_ = std::make_unique<DividerSnapAnimation>( |
| this, divider_position_, target_divider_position, |
| base::Milliseconds(300), gfx::Tween::EASE_IN); |
| divider_snap_animation_->Show(); |
| } |
| } |
| |
| void SplitViewController::EndTabletResizeImpl() { |
| // The backdrop layers are removed here (rather than in |
| // `EndResizeWithDivider()`) since they may be used while the divider is |
| // animating to a snapped position. |
| left_resize_backdrop_layer_.reset(); |
| right_resize_backdrop_layer_.reset(); |
| |
| // Resize may not end with `EndResizeWithDivider()`, so make sure to clear |
| // here too. |
| resize_timer_.Stop(); |
| } |
| |
| void SplitViewController::EndResizeWithDividerImpl() { |
| DCHECK(InSplitViewMode()); |
| DCHECK(!IsResizingWithDivider()); |
| |
| EndTabletResizeImpl(); |
| presentation_time_recorder_.reset(); |
| RestoreWindowsTransformAfterResizing(); |
| FinishWindowResizing(primary_window_); |
| FinishWindowResizing(secondary_window_); |
| } |
| |
| void SplitViewController::OnResizeTimer() { |
| if (InSplitViewMode() && split_view_divider_) { |
| split_view_divider_->ResizeWithDivider(previous_event_location_); |
| } |
| } |
| |
| void SplitViewController::UpdateTabletResizeMode( |
| base::TimeTicks event_time_ticks, |
| const gfx::Point& event_location) { |
| CHECK(presentation_time_recorder_); |
| presentation_time_recorder_->RequestNext(); |
| |
| if (IsLayoutHorizontal(root_window_)) { |
| accumulated_drag_distance_ += |
| std::abs(event_location.x() - previous_event_location_.x()); |
| } else { |
| accumulated_drag_distance_ += |
| std::abs(event_location.y() - previous_event_location_.y()); |
| } |
| |
| const base::TimeDelta chunk_time_ticks = |
| event_time_ticks - accumulated_drag_time_ticks_; |
| // We switch between fast and normal resize mode depending on how fast the |
| // divider is dragged. This is done in "chunks" by keeping track of how far |
| // the divider has been dragged. When the chunk gone on for long enough, we |
| // calculate the drag speed based on `accumulated_drag_distance_` and update |
| // the resize mode accordingly. |
| if (chunk_time_ticks >= kSplitViewChunkTime) { |
| int drag_per_second = |
| accumulated_drag_distance_ / chunk_time_ticks.InSecondsF(); |
| tablet_resize_mode_ = drag_per_second > kSplitViewThresholdPixelsPerSec |
| ? TabletResizeMode::kFast |
| : TabletResizeMode::kNormal; |
| |
| accumulated_drag_time_ticks_ = event_time_ticks; |
| accumulated_drag_distance_ = 0; |
| } |
| |
| // If we are in the fast mode, start a timer that automatically invokes |
| // `ResizeWithDivider()` after a timeout. This ensure that we can switch back |
| // to the normal mode if the user stops dragging. Note: if the timer is |
| // already active, this will simply move the deadline forward. |
| if (tablet_resize_mode_ == TabletResizeMode::kFast) { |
| resize_timer_.Start(FROM_HERE, kSplitViewChunkTime, this, |
| &SplitViewController::OnResizeTimer); |
| } |
| } |
| |
| void SplitViewController::EndWindowDragImpl( |
| aura::Window* window, |
| bool is_being_destroyed, |
| SnapPosition desired_snap_position, |
| const gfx::Point& last_location_in_screen, |
| WindowSnapActionSource snap_action_source) { |
| if (split_view_divider_) |
| split_view_divider_->OnWindowDragEnded(); |
| |
| // If the dragged window is to be destroyed, do not try to snap it. |
| if (is_being_destroyed) |
| return; |
| |
| // If dragged window was in overview before or it has been added to overview |
| // window by dropping on the new selector item, do nothing. |
| if (GetOverviewSession() && GetOverviewSession()->IsWindowInOverview(window)) |
| return; |
| |
| if (WindowState::Get(window)->IsFloated()) { |
| // If a floated window was dragged from shelf and released, don't snap. |
| return; |
| } |
| |
| DCHECK_EQ(root_window_, window->GetRootWindow()); |
| |
| const bool was_splitview_active = InSplitViewMode(); |
| if (desired_snap_position == SnapPosition::kNone) { |
| if (was_splitview_active) { |
| // Even though |snap_position| equals |SnapPosition::kNone|, the dragged |
| // window still needs to be snapped if splitview mode is active at the |
| // moment. |
| // Calculate the expected snap position based on the last event |
| // location. Note if there is already a window at |desired_snap_postion|, |
| // SnapWindow() will put the previous snapped window in overview. |
| SnapWindow(window, ComputeSnapPosition(last_location_in_screen), |
| snap_action_source, |
| /*activate_window=*/true); |
| } else { |
| // Restore the dragged window's transform first if it's not identity. It |
| // needs to be called before the transformed window's bounds change so |
| // that its transient children are layout'ed properly (the layout happens |
| // when window's bounds change). |
| SetTransformWithAnimation(window, window->layer()->GetTargetTransform(), |
| gfx::Transform()); |
| |
| OverviewSession* overview_session = GetOverviewSession(); |
| if (overview_session) { |
| overview_session->SetWindowListNotAnimatedWhenExiting(root_window_); |
| // Set the overview exit type to kImmediateExit to avoid update bounds |
| // animation of the windows in overview grid. |
| overview_session->set_enter_exit_overview_type( |
| OverviewEnterExitType::kImmediateExit); |
| } |
| // Activate the dragged window and end the overview. The dragged window |
| // will be restored back to its previous state before dragging. |
| wm::ActivateWindow(window); |
| Shell::Get()->overview_controller()->EndOverview( |
| OverviewEndAction::kSplitView); |
| |
| // Update the dragged window's bounds. It's possible that the dragged |
| // window's bounds was changed during dragging. Update its bounds after |
| // the drag ends to ensure it has the right bounds. |
| TabletModeWindowState::UpdateWindowPosition( |
| WindowState::Get(window), |
| WindowState::BoundsChangeAnimationType::kAnimate); |
| } |
| } else { |
| // Note SnapWindow() might put the previous window that was snapped at the |
| // |desired_snap_position| in overview. |
| SnapWindow(window, desired_snap_position, snap_action_source, |
| /*activate_window=*/true); |
| } |
| } |
| |
| void SplitViewController::DoSplitDividerSpawnAnimation(aura::Window* window) { |
| DCHECK(window->layer()->GetAnimator()->GetTargetTransform().IsIdentity()); |
| SnapPosition snap_position = GetPositionOfSnappedWindow(window); |
| const gfx::Rect bounds = |
| GetSnappedWindowBoundsInScreen(snap_position, window); |
| // Get one of the two corners of |window| that meet the divider. |
| gfx::Point p = IsPhysicalLeftOrTop(snap_position, window) |
| ? bounds.bottom_right() |
| : bounds.origin(); |
| // Apply the transform that |window| will undergo when the divider spawns. |
| static const double value = gfx::Tween::CalculateValue( |
| gfx::Tween::FAST_OUT_SLOW_IN, |
| kSplitviewDividerSpawnDelay / kSplitviewWindowTransformDuration); |
| p = gfx::TransformAboutPivot( |
| gfx::PointF(bounds.origin()), |
| gfx::Tween::TransformValueBetween(value, window->transform(), |
| gfx::Transform())) |
| .MapPoint(p); |
| // Use a coordinate of the transformed |window| corner for spawn_position. |
| split_view_divider_->DoSpawningAnimation(IsLayoutHorizontal(window) ? p.x() |
| : p.y()); |
| } |
| |
| void SplitViewController::SwapWindowsAndUpdateBounds() { |
| gfx::Rect primary_window_bounds = |
| primary_window_ ? primary_window_->GetBoundsInScreen() : gfx::Rect(); |
| gfx::Rect secondary_window_bounds = |
| secondary_window_ ? secondary_window_->GetBoundsInScreen() : gfx::Rect(); |
| aura::Window* cached_window = primary_window_; |
| primary_window_ = secondary_window_; |
| secondary_window_ = cached_window; |
| |
| const auto dst_display = |
| display::Screen::GetScreen()->GetDisplayNearestWindow(root_window_); |
| |
| if (primary_window_) { |
| primary_window_->SetBoundsInScreen(secondary_window_bounds, dst_display); |
| } |
| |
| if (secondary_window_) { |
| secondary_window_->SetBoundsInScreen(primary_window_bounds, dst_display); |
| } |
| } |
| |
| } // namespace ash |