| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/wm/splitview/split_view_divider.h" |
| |
| #include <memory> |
| |
| #include "ash/display/screen_orientation_controller.h" |
| #include "ash/public/cpp/shell_window_ids.h" |
| #include "ash/screen_util.h" |
| #include "ash/shell.h" |
| #include "ash/wm/desks/desks_util.h" |
| #include "ash/wm/splitview/split_view_constants.h" |
| #include "ash/wm/splitview/split_view_controller.h" |
| #include "ash/wm/splitview/split_view_divider_handler_view.h" |
| #include "ash/wm/splitview/split_view_utils.h" |
| #include "ash/wm/window_util.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/stl_util.h" |
| #include "ui/aura/scoped_window_targeter.h" |
| #include "ui/aura/window_targeter.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_targeter_delegate.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_delegate.h" |
| #include "ui/wm/core/coordinate_conversion.h" |
| #include "ui/wm/core/transient_window_manager.h" |
| #include "ui/wm/core/window_util.h" |
| #include "ui/wm/public/activation_client.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| constexpr base::TimeDelta kDividerSelectionStatusChangeDuration = |
| base::TimeDelta::FromMilliseconds( |
| kSplitviewDividerSelectionStatusChangeDurationMs); |
| constexpr base::TimeDelta kDividerSpawnDuration = |
| base::TimeDelta::FromMilliseconds(kSplitviewDividerSpawnDurationMs); |
| constexpr base::TimeDelta kDividerSpawnDelay = |
| base::TimeDelta::FromMilliseconds(kSplitviewDividerSpawnDelayMs); |
| |
| // The distance to the divider edge in which a touch gesture will be considered |
| // as a valid event on the divider. |
| constexpr int kDividerEdgeInsetForTouch = 8; |
| |
| // The window targeter that is installed on the always on top container window |
| // when the split view mode is active. |
| class AlwaysOnTopWindowTargeter : public aura::WindowTargeter { |
| public: |
| explicit AlwaysOnTopWindowTargeter(aura::Window* divider_window) |
| : divider_window_(divider_window) {} |
| ~AlwaysOnTopWindowTargeter() override = default; |
| |
| private: |
| bool GetHitTestRects(aura::Window* target, |
| gfx::Rect* hit_test_rect_mouse, |
| gfx::Rect* hit_test_rect_touch) const override { |
| if (target == divider_window_) { |
| *hit_test_rect_mouse = *hit_test_rect_touch = gfx::Rect(target->bounds()); |
| hit_test_rect_touch->Inset( |
| gfx::Insets(-kDividerEdgeInsetForTouch, -kDividerEdgeInsetForTouch)); |
| return true; |
| } |
| return aura::WindowTargeter::GetHitTestRects(target, hit_test_rect_mouse, |
| hit_test_rect_touch); |
| } |
| |
| aura::Window* divider_window_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AlwaysOnTopWindowTargeter); |
| }; |
| |
| // The divider view class. Passes the mouse/gesture events to the controller. |
| // Has two child views, one for the divider and one for its white handler. The |
| // bounds and transforms of these two child views can be affected by the |
| // spawning animation and by dragging, but regardless, the controller receives |
| // mouse/gesture events in the bounds of the |DividerView| object itself. |
| class DividerView : public views::View, public views::ViewTargeterDelegate { |
| public: |
| explicit DividerView(SplitViewDivider* divider) |
| : controller_(Shell::Get()->split_view_controller()), divider_(divider) { |
| divider_view_ = new views::View(); |
| divider_view_->SetPaintToLayer(ui::LAYER_SOLID_COLOR); |
| divider_view_->layer()->SetColor(kSplitviewDividerColor); |
| |
| divider_handler_view_ = new SplitViewDividerHandlerView(); |
| |
| AddChildView(divider_view_); |
| AddChildView(divider_handler_view_); |
| |
| SetEventTargeter( |
| std::unique_ptr<views::ViewTargeter>(new views::ViewTargeter(this))); |
| } |
| ~DividerView() override = default; |
| |
| void DoSpawningAnimation(int spawn_position) { |
| const gfx::Rect bounds = GetBoundsInScreen(); |
| int divider_signed_offset; |
| // To animate the divider scaling up from nothing, animate its bounds rather |
| // than its transform, mostly because a transform that scales by zero would |
| // be singular. For that bounds animation, express |spawn_position| in local |
| // coordinates by subtracting a coordinate of the origin. Compute |
| // |divider_signed_offset| as described in the comment for |
| // |SplitViewDividerHandlerView::DoSpawningAnimation|. |
| if (IsCurrentScreenOrientationLandscape()) { |
| divider_view_->SetBounds(spawn_position - bounds.x(), 0, 0, |
| bounds.height()); |
| divider_signed_offset = spawn_position - bounds.CenterPoint().x(); |
| } else { |
| divider_view_->SetBounds(0, spawn_position - bounds.y(), bounds.width(), |
| 0); |
| divider_signed_offset = spawn_position - bounds.CenterPoint().y(); |
| } |
| ui::LayerAnimator* divider_animator = divider_view_->layer()->GetAnimator(); |
| ui::ScopedLayerAnimationSettings settings(divider_animator); |
| settings.SetTransitionDuration(kDividerSpawnDuration); |
| settings.SetTweenType(gfx::Tween::LINEAR_OUT_SLOW_IN); |
| settings.SetPreemptionStrategy(ui::LayerAnimator::ENQUEUE_NEW_ANIMATION); |
| divider_animator->SchedulePauseForProperties( |
| kDividerSpawnDelay, ui::LayerAnimationElement::BOUNDS); |
| divider_view_->SetBounds(0, 0, bounds.width(), bounds.height()); |
| divider_handler_view_->DoSpawningAnimation(divider_signed_offset); |
| } |
| |
| // views::View: |
| void Layout() override { |
| divider_view_->SetBoundsRect(GetLocalBounds()); |
| divider_handler_view_->Refresh(); |
| } |
| |
| bool OnMousePressed(const ui::MouseEvent& event) override { |
| gfx::Point location(event.location()); |
| views::View::ConvertPointToScreen(this, &location); |
| controller_->StartResize(location); |
| OnResizeStatusChanged(); |
| return true; |
| } |
| |
| bool OnMouseDragged(const ui::MouseEvent& event) override { |
| gfx::Point location(event.location()); |
| views::View::ConvertPointToScreen(this, &location); |
| controller_->Resize(location); |
| return true; |
| } |
| |
| void OnMouseReleased(const ui::MouseEvent& event) override { |
| gfx::Point location(event.location()); |
| views::View::ConvertPointToScreen(this, &location); |
| controller_->EndResize(location); |
| OnResizeStatusChanged(); |
| if (event.GetClickCount() == 2) |
| controller_->SwapWindows(); |
| } |
| |
| void OnGestureEvent(ui::GestureEvent* event) override { |
| gfx::Point location(event->location()); |
| views::View::ConvertPointToScreen(this, &location); |
| switch (event->type()) { |
| case ui::ET_GESTURE_TAP: |
| if (event->details().tap_count() == 2) |
| controller_->SwapWindows(); |
| break; |
| case ui::ET_GESTURE_TAP_DOWN: |
| case ui::ET_GESTURE_SCROLL_BEGIN: |
| controller_->StartResize(location); |
| OnResizeStatusChanged(); |
| break; |
| case ui::ET_GESTURE_SCROLL_UPDATE: |
| controller_->Resize(location); |
| break; |
| case ui::ET_GESTURE_END: |
| controller_->EndResize(location); |
| OnResizeStatusChanged(); |
| break; |
| default: |
| break; |
| } |
| event->SetHandled(); |
| } |
| |
| // views::ViewTargeterDelegate: |
| bool DoesIntersectRect(const views::View* target, |
| const gfx::Rect& rect) const override { |
| DCHECK_EQ(target, this); |
| return true; |
| } |
| |
| private: |
| void OnResizeStatusChanged() { |
| // It's possible that when this function is called, split view mode has |
| // been ended, and the divider widget is to be deleted soon. In this case |
| // no need to update the divider layout and do the animation. |
| if (!controller_->InSplitViewMode()) |
| return; |
| |
| // If |divider_view_|'s bounds are animating, it is for the divider spawning |
| // animation. Stop that before animating |divider_view_|'s transform. |
| ui::LayerAnimator* divider_animator = divider_view_->layer()->GetAnimator(); |
| divider_animator->StopAnimatingProperty(ui::LayerAnimationElement::BOUNDS); |
| |
| // Do the divider enlarge/shrink animation when starting/ending dragging. |
| divider_view_->SetBoundsRect(GetLocalBounds()); |
| const gfx::Rect old_bounds = |
| divider_->GetDividerBoundsInScreen(/*is_dragging=*/false); |
| const gfx::Rect new_bounds = |
| divider_->GetDividerBoundsInScreen(controller_->is_resizing()); |
| gfx::Transform transform; |
| transform.Translate(new_bounds.x() - old_bounds.x(), |
| new_bounds.y() - old_bounds.y()); |
| transform.Scale( |
| static_cast<float>(new_bounds.width()) / old_bounds.width(), |
| static_cast<float>(new_bounds.height()) / old_bounds.height()); |
| ui::ScopedLayerAnimationSettings settings(divider_animator); |
| settings.SetTransitionDuration(kDividerSelectionStatusChangeDuration); |
| settings.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN); |
| settings.SetPreemptionStrategy( |
| ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); |
| divider_view_->SetTransform(transform); |
| |
| divider_handler_view_->Refresh(); |
| } |
| |
| views::View* divider_view_ = nullptr; |
| SplitViewDividerHandlerView* divider_handler_view_ = nullptr; |
| SplitViewController* controller_; |
| SplitViewDivider* divider_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DividerView); |
| }; |
| |
| } // namespace |
| |
| SplitViewDivider::SplitViewDivider(SplitViewController* controller, |
| aura::Window* root_window) |
| : controller_(controller) { |
| Shell::Get()->activation_client()->AddObserver(this); |
| CreateDividerWidget(root_window); |
| |
| aura::Window* always_on_top_container = |
| Shell::GetContainer(root_window, kShellWindowId_AlwaysOnTopContainer); |
| split_view_window_targeter_ = std::make_unique<aura::ScopedWindowTargeter>( |
| always_on_top_container, std::make_unique<AlwaysOnTopWindowTargeter>( |
| divider_widget_->GetNativeWindow())); |
| } |
| |
| SplitViewDivider::~SplitViewDivider() { |
| Shell::Get()->activation_client()->RemoveObserver(this); |
| divider_widget_->Close(); |
| split_view_window_targeter_.reset(); |
| for (auto* window : observed_windows_) { |
| window->RemoveObserver(this); |
| ::wm::TransientWindowManager::GetOrCreate(window)->RemoveObserver(this); |
| } |
| observed_windows_.clear(); |
| } |
| |
| // static |
| gfx::Size SplitViewDivider::GetDividerSize( |
| const gfx::Rect& work_area_bounds, |
| OrientationLockType screen_orientation, |
| bool is_dragging) { |
| if (IsLandscapeOrientation(screen_orientation)) { |
| return is_dragging ? gfx::Size(kSplitviewDividerEnlargedShortSideLength, |
| work_area_bounds.height()) |
| : gfx::Size(kSplitviewDividerShortSideLength, |
| work_area_bounds.height()); |
| } else { |
| return is_dragging ? gfx::Size(work_area_bounds.width(), |
| kSplitviewDividerEnlargedShortSideLength) |
| : gfx::Size(work_area_bounds.width(), |
| kSplitviewDividerShortSideLength); |
| } |
| } |
| |
| // static |
| gfx::Rect SplitViewDivider::GetDividerBoundsInScreen( |
| const gfx::Rect& work_area_bounds_in_screen, |
| OrientationLockType screen_orientation, |
| int divider_position, |
| bool is_dragging) { |
| const gfx::Size divider_size = GetDividerSize( |
| work_area_bounds_in_screen, screen_orientation, is_dragging); |
| int dragging_diff = (kSplitviewDividerEnlargedShortSideLength - |
| kSplitviewDividerShortSideLength) / |
| 2; |
| switch (screen_orientation) { |
| case OrientationLockType::kLandscapePrimary: |
| case OrientationLockType::kLandscapeSecondary: |
| return is_dragging |
| ? gfx::Rect(work_area_bounds_in_screen.x() + divider_position - |
| dragging_diff, |
| work_area_bounds_in_screen.y(), |
| divider_size.width(), divider_size.height()) |
| : gfx::Rect(work_area_bounds_in_screen.x() + divider_position, |
| work_area_bounds_in_screen.y(), |
| divider_size.width(), divider_size.height()); |
| case OrientationLockType::kPortraitPrimary: |
| case OrientationLockType::kPortraitSecondary: |
| return is_dragging |
| ? gfx::Rect(work_area_bounds_in_screen.x(), |
| work_area_bounds_in_screen.y() + divider_position - |
| (kSplitviewDividerEnlargedShortSideLength - |
| kSplitviewDividerShortSideLength) / |
| 2, |
| divider_size.width(), divider_size.height()) |
| : gfx::Rect(work_area_bounds_in_screen.x(), |
| work_area_bounds_in_screen.y() + divider_position, |
| divider_size.width(), divider_size.height()); |
| default: |
| NOTREACHED(); |
| return gfx::Rect(); |
| } |
| } |
| |
| void SplitViewDivider::DoSpawningAnimation(int spawning_position) { |
| static_cast<DividerView*>(divider_widget_->GetContentsView()) |
| ->DoSpawningAnimation(spawning_position); |
| } |
| |
| void SplitViewDivider::UpdateDividerBounds() { |
| divider_widget_->SetBounds(GetDividerBoundsInScreen(/*is_dragging=*/false)); |
| } |
| |
| gfx::Rect SplitViewDivider::GetDividerBoundsInScreen(bool is_dragging) { |
| const gfx::Rect work_area_bounds_in_screen = |
| screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| Shell::GetPrimaryRootWindow()->GetChildById( |
| desks_util::GetActiveDeskContainerId())); |
| const int divider_position = controller_->divider_position(); |
| const OrientationLockType screen_orientation = GetCurrentScreenOrientation(); |
| return GetDividerBoundsInScreen(work_area_bounds_in_screen, |
| screen_orientation, divider_position, |
| is_dragging); |
| } |
| |
| void SplitViewDivider::AddObservedWindow(aura::Window* window) { |
| if (!base::Contains(observed_windows_, window)) { |
| window->AddObserver(this); |
| ::wm::TransientWindowManager::GetOrCreate(window)->AddObserver(this); |
| observed_windows_.push_back(window); |
| } |
| } |
| |
| void SplitViewDivider::RemoveObservedWindow(aura::Window* window) { |
| auto iter = |
| std::find(observed_windows_.begin(), observed_windows_.end(), window); |
| if (iter != observed_windows_.end()) { |
| window->RemoveObserver(this); |
| ::wm::TransientWindowManager::GetOrCreate(window)->RemoveObserver(this); |
| observed_windows_.erase(iter); |
| } |
| } |
| |
| void SplitViewDivider::OnWindowDragStarted(aura::Window* dragged_window) { |
| is_dragging_window_ = true; |
| SetAlwaysOnTop(false); |
| // Make sure |divider_widget_| is placed below the dragged window. |
| dragged_window->parent()->StackChildBelow(divider_widget_->GetNativeWindow(), |
| dragged_window); |
| } |
| |
| void SplitViewDivider::OnWindowDragEnded() { |
| is_dragging_window_ = false; |
| SetAlwaysOnTop(true); |
| } |
| |
| void SplitViewDivider::OnWindowDestroying(aura::Window* window) { |
| RemoveObservedWindow(window); |
| } |
| |
| void SplitViewDivider::OnWindowBoundsChanged(aura::Window* window, |
| const gfx::Rect& old_bounds, |
| const gfx::Rect& new_bounds, |
| ui::PropertyChangeReason reason) { |
| // We only care about the bounds change of windows in |
| // |transient_windows_observer_|. |
| if (!transient_windows_observer_.IsObserving(window)) |
| return; |
| |
| // |window|'s transient parent must be one of the windows in |
| // |observed_windows_|. |
| aura::Window* transient_parent = nullptr; |
| for (auto* observed_window : observed_windows_) { |
| if (::wm::HasTransientAncestor(window, observed_window)) { |
| transient_parent = observed_window; |
| break; |
| } |
| } |
| DCHECK(transient_parent); |
| |
| gfx::Rect transient_bounds = window->GetBoundsInScreen(); |
| transient_bounds.AdjustToFit(transient_parent->GetBoundsInScreen()); |
| window->SetBoundsInScreen( |
| transient_bounds, |
| display::Screen::GetScreen()->GetDisplayNearestWindow(window)); |
| } |
| |
| void SplitViewDivider::OnWindowActivated(ActivationReason reason, |
| aura::Window* gained_active, |
| aura::Window* lost_active) { |
| if (!is_dragging_window_ && |
| (!gained_active || base::Contains(observed_windows_, gained_active))) { |
| SetAlwaysOnTop(true); |
| } else { |
| // If |gained_active| is not one of the observed windows, or there is one |
| // window that is currently being dragged, |divider_widget_| should not |
| // be placed on top. |
| SetAlwaysOnTop(false); |
| } |
| } |
| |
| void SplitViewDivider::OnTransientChildAdded(aura::Window* window, |
| aura::Window* transient) { |
| // For now, we only care about dialog bubbles type transient child. We may |
| // observe other types transient child window as well if need arises in the |
| // future. |
| views::Widget* widget = views::Widget::GetWidgetForNativeWindow(transient); |
| if (!widget || !widget->widget_delegate()->AsBubbleDialogDelegate()) |
| return; |
| |
| // At this moment, the transient window may not have the valid bounds yet. |
| // Start observe the transient window. |
| transient_windows_observer_.Add(transient); |
| } |
| |
| void SplitViewDivider::OnTransientChildRemoved(aura::Window* window, |
| aura::Window* transient) { |
| if (transient_windows_observer_.IsObserving(transient)) |
| transient_windows_observer_.Remove(transient); |
| } |
| |
| void SplitViewDivider::CreateDividerWidget(aura::Window* root_window) { |
| DCHECK(!divider_widget_); |
| // Native widget owns this widget. |
| divider_widget_ = new views::Widget; |
| views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); |
| params.opacity = views::Widget::InitParams::OPAQUE_WINDOW; |
| params.activatable = views::Widget::InitParams::ACTIVATABLE_NO; |
| params.parent = |
| Shell::GetContainer(root_window, kShellWindowId_AlwaysOnTopContainer); |
| DividerView* divider_view = new DividerView(this); |
| divider_widget_->set_focus_on_creation(false); |
| divider_widget_->Init(params); |
| divider_widget_->SetVisibilityAnimationTransition( |
| views::Widget::ANIMATE_NONE); |
| divider_widget_->SetContentsView(divider_view); |
| divider_widget_->SetBounds(GetDividerBoundsInScreen(false /* is_dragging */)); |
| divider_widget_->Show(); |
| } |
| |
| void SplitViewDivider::SetAlwaysOnTop(bool on_top) { |
| if (on_top) { |
| divider_widget_->SetZOrderLevel(ui::ZOrderLevel::kFloatingUIElement); |
| |
| // Special handling when put divider into always_on_top container. We want |
| // to put it at the bottom so it won't block other always_on_top windows. |
| aura::Window* always_on_top_container = |
| Shell::GetContainer(divider_widget_->GetNativeWindow()->GetRootWindow(), |
| kShellWindowId_AlwaysOnTopContainer); |
| always_on_top_container->StackChildAtBottom( |
| divider_widget_->GetNativeWindow()); |
| } else { |
| divider_widget_->SetZOrderLevel(ui::ZOrderLevel::kNormal); |
| } |
| } |
| |
| } // namespace ash |