blob: 6197d6d8e947c26b8c6f6cc44d0391ef0d03cfc2 [file] [log] [blame]
// Copyright 2019 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/home_screen/drag_window_from_shelf_controller.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/home_screen/home_screen_controller.h"
#include "ash/home_screen/home_screen_delegate.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/scoped_animation_disabler.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/wallpaper/wallpaper_view.h"
#include "ash/wallpaper/wallpaper_widget_controller.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_constants.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_transient_descendant_iterator.h"
#include "base/numerics/ranges.h"
#include "ui/base/hit_test.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
// The minimum window scale factor when dragging a window from shelf.
constexpr float kMinimumWindowScaleDuringDragging = 0.2f;
// Amount of time to wait to show overview after the user slows down or stops
// window dragging.
constexpr base::TimeDelta kShowOverviewTimeWhenDragSuspend =
base::TimeDelta::FromMilliseconds(40);
// The time to do window transform to scale up to its original position or
// scale down to homescreen animation.
constexpr base::TimeDelta kWindowScaleUpOrDownTime =
base::TimeDelta::FromMilliseconds(350);
// The delay to do window opacity fade out when scaling down the dragged window.
constexpr base::TimeDelta kWindowFadeOutDelay =
base::TimeDelta::FromMilliseconds(100);
// The window scale down factor if we head to home screen after drag ends.
constexpr float kWindowScaleDownFactor = 0.001f;
// The class the does the dragged window scale down animation to home screen
// after drag ends. The window will be minimized after animation complete.
class WindowTransformToHomeScreenAnimation
: public ui::ImplicitAnimationObserver,
public aura::WindowObserver {
public:
WindowTransformToHomeScreenAnimation(
aura::Window* window,
base::Optional<BackdropWindowMode> original_backdrop_mode)
: window_(window), original_backdrop_mode_(original_backdrop_mode) {
window_->AddObserver(this);
ui::ScopedLayerAnimationSettings settings(window_->layer()->GetAnimator());
settings.SetTransitionDuration(kWindowScaleUpOrDownTime);
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
settings.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
settings.AddObserver(this);
window_->layer()->GetAnimator()->SchedulePauseForProperties(
kWindowFadeOutDelay, ui::LayerAnimationElement::OPACITY);
window_->layer()->SetTransform(GetWindowTransformToHomeScreen());
window_->layer()->SetOpacity(0.f);
}
~WindowTransformToHomeScreenAnimation() override {
if (window_)
window_->RemoveObserver(this);
}
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override {
// Minimize the dragged window after transform animation is completed.
ScopedAnimationDisabler disable(window_);
window_->Hide();
WindowState::Get(window_)->Minimize();
// Reset its transform to identity transform and its original backdrop mode.
window_->layer()->SetTransform(gfx::Transform());
window_->layer()->SetOpacity(1.f);
if (original_backdrop_mode_.has_value())
window_->SetProperty(kBackdropWindowMode, *original_backdrop_mode_);
delete this;
}
// aura::WindowObserver:
void OnWindowDestroying(aura::Window* window) override {
window_ = nullptr;
delete this;
}
private:
// Returns the transform that should be applied to the dragged window if we
// should head to homescreen after dragging.
gfx::Transform GetWindowTransformToHomeScreen() {
gfx::Transform transform;
HomeScreenDelegate* home_screen_delegate =
Shell::Get()->home_screen_controller()->delegate();
DCHECK(home_screen_delegate);
const gfx::Rect app_list_item_bounds =
home_screen_delegate->GetInitialAppListItemScreenBoundsForWindow(
window_);
if (!app_list_item_bounds.IsEmpty()) {
const gfx::Rect window_bounds = window_->GetBoundsInScreen();
transform.Translate(app_list_item_bounds.x(), app_list_item_bounds.y());
transform.Scale(
float(app_list_item_bounds.width()) / float(window_bounds.width()),
float(app_list_item_bounds.height()) / float(window_bounds.height()));
} else {
const gfx::Rect work_area =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window_);
transform.Translate(work_area.width() / 2, work_area.height() / 2);
transform.Scale(kWindowScaleDownFactor, kWindowScaleDownFactor);
}
return transform;
}
aura::Window* window_;
base::Optional<BackdropWindowMode> original_backdrop_mode_;
DISALLOW_COPY_AND_ASSIGN(WindowTransformToHomeScreenAnimation);
};
} // namespace
// Hide all visible windows expect the dragged windows or the window showing in
// splitview during dragging.
class DragWindowFromShelfController::WindowsHider
: public aura::WindowObserver {
public:
explicit WindowsHider(aura::Window* dragged_window)
: dragged_window_(dragged_window) {
std::vector<aura::Window*> windows =
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
for (auto* window : windows) {
if (window == dragged_window_)
continue;
if (!window->IsVisible())
continue;
if (SplitViewController::Get(window)->IsWindowInSplitView(window))
continue;
hidden_windows_.push_back(window);
{
ScopedAnimationDisabler disabler(window);
// Minimize so that they can show up correctly in overview.
WindowState::Get(window)->Minimize();
window->Hide();
}
window->AddObserver(this);
}
}
~WindowsHider() override {
for (auto* window : hidden_windows_)
window->RemoveObserver(this);
hidden_windows_.clear();
}
void RestoreWindowsVisibility() {
for (auto* window : hidden_windows_) {
window->RemoveObserver(this);
ScopedAnimationDisabler disabler(window);
window->Show();
}
hidden_windows_.clear();
}
// aura::WindowObserver:
void OnWindowDestroying(aura::Window* window) override {
window->RemoveObserver(this);
hidden_windows_.erase(
std::find(hidden_windows_.begin(), hidden_windows_.end(), window));
}
private:
aura::Window* dragged_window_;
std::vector<aura::Window*> hidden_windows_;
DISALLOW_COPY_AND_ASSIGN(WindowsHider);
};
DragWindowFromShelfController::DragWindowFromShelfController(
aura::Window* window)
: window_(window) {}
DragWindowFromShelfController::~DragWindowFromShelfController() {
CancelDrag();
}
void DragWindowFromShelfController::Drag(const gfx::Point& location_in_screen,
float scroll_x,
float scroll_y) {
if (!drag_started_) {
// Do not start drag until the drag goes above the shelf.
const gfx::Rect work_area =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
window_);
if (location_in_screen.y() > work_area.bottom())
return;
OnDragStarted(location_in_screen);
}
UpdateDraggedWindow(location_in_screen);
// Open overview if the window has been dragged far enough and the scroll
// delta has decreased to kOpenOverviewThreshold or less.
OverviewController* overview_controller = Shell::Get()->overview_controller();
if (std::abs(scroll_y) <= kOpenOverviewThreshold &&
!overview_controller->InOverviewSession()) {
overview_controller->StartOverview(
OverviewSession::EnterExitOverviewType::kImmediateEnter);
overview_controller->overview_session()->OnWindowDragStarted(
window_, /*animate=*/false);
}
// If overview is active, update its splitview indicator during dragging if
// splitview is allowed in current configuration.
if (overview_controller->InOverviewSession()) {
IndicatorState indicator_state = GetIndicatorState(location_in_screen);
OverviewSession* overview_session = overview_controller->overview_session();
overview_session->SetSplitViewDragIndicatorsIndicatorState(
indicator_state, location_in_screen);
overview_session->OnWindowDragContinued(
window_, gfx::PointF(location_in_screen), indicator_state);
if (indicator_state == IndicatorState::kPreviewAreaLeft ||
indicator_state == IndicatorState::kPreviewAreaRight) {
// If the dragged window is in snap preview area, make sure overview is
// visible.
ShowOverviewDuringOrAfterDrag();
} else if (std::abs(scroll_x) > kShowOverviewThreshold ||
std::abs(scroll_y) > kShowOverviewThreshold) {
// If the dragging velocity is large enough, hide overview windows.
show_overview_timer_.Stop();
overview_session->SetVisibleDuringWindowDragging(/*visible=*/false);
} else {
// Otherwise start the |show_overview_timer_| to show and update overview
// when the dragging slows down or stops.
show_overview_timer_.Start(
FROM_HERE, kShowOverviewTimeWhenDragSuspend, this,
&DragWindowFromShelfController::ShowOverviewDuringOrAfterDrag);
}
}
previous_location_in_screen_ = location_in_screen;
}
void DragWindowFromShelfController::EndDrag(
const gfx::Point& location_in_screen,
base::Optional<float> velocity_y) {
if (!drag_started_)
return;
drag_started_ = false;
OverviewController* overview_controller = Shell::Get()->overview_controller();
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
const bool in_overview = overview_controller->InOverviewSession();
const bool in_splitview = split_view_controller->InSplitViewMode();
if (ShouldGoToHomeScreen(velocity_y)) {
DCHECK(!in_splitview);
if (in_overview) {
overview_controller->EndOverview(
OverviewSession::EnterExitOverviewType::kImmediateExit);
}
ScaleDownWindowAfterDrag();
} else if (ShouldRestoreToOriginalBounds(location_in_screen)) {
// TODO(crbug.com/997885): Add animation.
SetTransform(window_, gfx::Transform());
window_->SetProperty(kBackdropWindowMode, original_backdrop_mode_);
if (!in_splitview && in_overview) {
overview_controller->EndOverview(
OverviewSession::EnterExitOverviewType::kImmediateExit);
}
ReshowHiddenWindowsOnDragEnd();
} else if (!in_overview) {
// if overview is not active during the entire drag process, scale down the
// dragged window to go to home screen.
ScaleDownWindowAfterDrag();
}
OnDragEnded(location_in_screen,
ShouldDropWindowInOverview(location_in_screen, velocity_y),
GetSnapPositionOnDragEnd(location_in_screen, velocity_y));
}
void DragWindowFromShelfController::CancelDrag() {
if (!drag_started_)
return;
drag_started_ = false;
// Reset the window's transform to identity transform.
window_->SetTransform(gfx::Transform());
window_->SetProperty(kBackdropWindowMode, original_backdrop_mode_);
// End overview if it was opened during dragging.
OverviewController* overview_controller = Shell::Get()->overview_controller();
if (overview_controller->InOverviewSession()) {
overview_controller->EndOverview(
OverviewSession::EnterExitOverviewType::kImmediateExit);
}
ReshowHiddenWindowsOnDragEnd();
OnDragEnded(previous_location_in_screen_,
/*should_drop_window_in_overview=*/false,
/*snap_position=*/SplitViewController::NONE);
}
void DragWindowFromShelfController::OnDragStarted(
const gfx::Point& location_in_screen) {
drag_started_ = true;
initial_location_in_screen_ = location_in_screen;
previous_location_in_screen_ = location_in_screen;
WindowState::Get(window_)->CreateDragDetails(
initial_location_in_screen_, HTCLIENT, ::wm::WINDOW_MOVE_SOURCE_TOUCH);
// Disable the backdrop on the dragged window during dragging.
original_backdrop_mode_ = window_->GetProperty(kBackdropWindowMode);
window_->SetProperty(kBackdropWindowMode, BackdropWindowMode::kDisabled);
// Hide all visible windows behind the dragged window during dragging.
windows_hider_ = std::make_unique<WindowsHider>(window_);
// Hide the home launcher until it's eligible to show it.
Shell::Get()->home_screen_controller()->OnWindowDragStarted();
// Use the same dim and blur as in overview during dragging.
auto* wallpaper_view =
RootWindowController::ForWindow(window_->GetRootWindow())
->wallpaper_widget_controller()
->wallpaper_view();
if (wallpaper_view) {
wallpaper_view->RepaintBlurAndOpacity(kWallpaperBlurSigma, kShieldOpacity);
}
// If the dragged window is one of the snapped window in splitview, it needs
// to be detached from splitview before start dragging.
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
split_view_controller->OnWindowDragStarted(window_);
// Note SplitViewController::OnWindowDragStarted() may open overview.
OverviewController* overview_controller = Shell::Get()->overview_controller();
if (overview_controller->InOverviewSession()) {
overview_controller->overview_session()->OnWindowDragStarted(
window_, /*animate=*/false);
}
}
void DragWindowFromShelfController::OnDragEnded(
const gfx::Point& location_in_screen,
bool should_drop_window_in_overview,
SplitViewController::SnapPosition snap_position) {
OverviewController* overview_controller = Shell::Get()->overview_controller();
if (overview_controller->InOverviewSession()) {
// Make sure overview is visible after drag ends.
ShowOverviewDuringOrAfterDrag();
OverviewSession* overview_session = overview_controller->overview_session();
overview_session->SetSplitViewDragIndicatorsIndicatorState(
IndicatorState::kNone, location_in_screen);
overview_session->OnWindowDragEnded(
window_, gfx::PointF(location_in_screen),
should_drop_window_in_overview,
/*snap=*/snap_position != SplitViewController::NONE);
}
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
if (split_view_controller->InSplitViewMode() ||
snap_position != SplitViewController::NONE) {
split_view_controller->OnWindowDragEnded(window_, snap_position,
location_in_screen);
}
Shell::Get()->home_screen_controller()->OnWindowDragEnded();
// Clear the wallpaper dim and blur if not in overview after drag ends.
// If in overview, the dim and blur will be cleared after overview ends.
if (!overview_controller->InOverviewSession()) {
auto* wallpaper_view =
RootWindowController::ForWindow(window_->GetRootWindow())
->wallpaper_widget_controller()
->wallpaper_view();
if (wallpaper_view) {
wallpaper_view->RepaintBlurAndOpacity(kWallpaperClearBlurSigma,
kShieldOpacity);
}
}
WindowState::Get(window_)->DeleteDragDetails();
}
void DragWindowFromShelfController::UpdateDraggedWindow(
const gfx::Point& location_in_screen) {
gfx::Rect bounds = window_->bounds();
::wm::ConvertRectToScreen(window_->parent(), &bounds);
// Calculate the window's transform based on the location.
// For scale, at |initial_location_in_screen_|, the scale is 1.0, and at the
// middle y position of its bounds, it reaches to its minimum scale 0.2.
// Calculate the desired scale based on the current y position.
int y_full = bounds.bottom() - bounds.CenterPoint().y();
int y_diff = location_in_screen.y() - bounds.CenterPoint().y();
float scale = (1.0f - kMinimumWindowScaleDuringDragging) * y_diff / y_full +
kMinimumWindowScaleDuringDragging;
scale = base::ClampToRange(scale, /*min=*/kMinimumWindowScaleDuringDragging,
/*max=*/1.f);
// Calculate the desired translation so that the dragged window stays under
// the finger during the dragging.
gfx::Transform transform;
transform.Translate(
(location_in_screen.x() - bounds.x()) -
(initial_location_in_screen_.x() - bounds.x()) * scale,
(location_in_screen.y() - bounds.y()) -
(initial_location_in_screen_.y() - bounds.y()) * scale);
transform.Scale(scale, scale);
SetTransform(window_, transform);
}
SplitViewController::SnapPosition
DragWindowFromShelfController::GetSnapPosition(
const gfx::Point& location_in_screen) const {
// if |location_in_screen| is close to the bottom of the screen and is
// inside of kReturnToMaximizedThreshold threshold, we should not try to
// snap the window.
if (ShouldRestoreToOriginalBounds(location_in_screen))
return SplitViewController::NONE;
const gfx::Rect work_area = display::Screen::GetScreen()
->GetDisplayNearestPoint(location_in_screen)
.work_area();
SplitViewController::SnapPosition snap_position =
::ash::GetSnapPosition(window_, location_in_screen, work_area);
// For portrait mode, since the drag starts from the bottom of the screen,
// we should only allow the window to snap to the top of the screen.
const bool is_landscape = IsCurrentScreenOrientationLandscape();
const bool is_primary = IsCurrentScreenOrientationPrimary();
if (!is_landscape &&
((is_primary && snap_position == SplitViewController::RIGHT) ||
(!is_primary && snap_position == SplitViewController::LEFT))) {
snap_position = SplitViewController::NONE;
}
return snap_position;
}
IndicatorState DragWindowFromShelfController::GetIndicatorState(
const gfx::Point& location_in_screen) const {
if (!drag_started_)
return IndicatorState::kNone;
if (!ShouldAllowSplitView())
return IndicatorState::kNone;
// if |location_in_screen| is close to the bottom of the screen and is
// inside of kReturnToMaximizedThreshold threshold, we do not show the
// indicators.
if (ShouldRestoreToOriginalBounds(location_in_screen))
return IndicatorState::kNone;
IndicatorState indicator_state =
::ash::GetIndicatorState(window_, GetSnapPosition(location_in_screen));
// Do not show drag-to-snap or cannot-snap drag indicator so that the drag is
// is less distracting.
if (indicator_state == IndicatorState::kDragArea ||
indicator_state == IndicatorState::kCannotSnap) {
indicator_state = IndicatorState::kNone;
}
return indicator_state;
}
bool DragWindowFromShelfController::ShouldRestoreToOriginalBounds(
const gfx::Point& location_in_screen) const {
const gfx::Rect work_area = display::Screen::GetScreen()
->GetDisplayNearestPoint(location_in_screen)
.work_area();
return location_in_screen.y() >
work_area.bottom() - kReturnToMaximizedThreshold;
}
bool DragWindowFromShelfController::ShouldGoToHomeScreen(
base::Optional<float> velocity_y) const {
return velocity_y.has_value() && *velocity_y < 0 &&
std::abs(*velocity_y) >= kVelocityToHomeScreenThreshold &&
!SplitViewController::Get(Shell::GetPrimaryRootWindow())
->InSplitViewMode();
}
SplitViewController::SnapPosition
DragWindowFromShelfController::GetSnapPositionOnDragEnd(
const gfx::Point& location_in_screen,
base::Optional<float> velocity_y) const {
if (ShouldRestoreToOriginalBounds(location_in_screen) ||
ShouldGoToHomeScreen(velocity_y)) {
return SplitViewController::NONE;
}
return GetSnapPosition(location_in_screen);
}
bool DragWindowFromShelfController::ShouldDropWindowInOverview(
const gfx::Point& location_in_screen,
base::Optional<float> velocity_y) const {
if (!Shell::Get()->overview_controller()->InOverviewSession())
return false;
if (ShouldGoToHomeScreen(velocity_y))
return false;
const bool in_splitview =
SplitViewController::Get(Shell::GetPrimaryRootWindow())
->InSplitViewMode();
if (!in_splitview && ShouldRestoreToOriginalBounds(location_in_screen)) {
return false;
}
if (in_splitview) {
if (velocity_y.has_value() && *velocity_y < 0 &&
std::abs(*velocity_y) >= kVelocityToOverviewThreshold) {
return true;
}
if (ShouldRestoreToOriginalBounds(location_in_screen))
return false;
}
return GetSnapPositionOnDragEnd(location_in_screen, velocity_y) ==
SplitViewController::NONE;
}
void DragWindowFromShelfController::ReshowHiddenWindowsOnDragEnd() {
windows_hider_->RestoreWindowsVisibility();
}
void DragWindowFromShelfController::ShowOverviewDuringOrAfterDrag() {
show_overview_timer_.Stop();
OverviewController* overview_controller = Shell::Get()->overview_controller();
if (!overview_controller->InOverviewSession())
return;
overview_controller->overview_session()->SetVisibleDuringWindowDragging(
/*visible=*/true);
}
void DragWindowFromShelfController::ScaleDownWindowAfterDrag() {
// Do the scale-down transform for the entire transient tree.
for (auto* window : GetTransientTreeIterator(window_)) {
// self-destructed when window transform animation is done.
new WindowTransformToHomeScreenAnimation(
window, window == window_ ? base::make_optional(original_backdrop_mode_)
: base::nullopt);
}
}
} // namespace ash