blob: e840ec2a137e9d4a820ce49b26c113e2c022e96e [file] [log] [blame]
// Copyright 2018 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/tablet_mode/tablet_mode_browser_window_drag_delegate.h"
#include <vector>
#include "ash/home_screen/home_screen_controller.h"
#include "ash/public/cpp/wallpaper_types.h"
#include "ash/root_window_controller.h"
#include "ash/scoped_animation_disabler.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_utils.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/tablet_mode/tablet_mode_window_state.h"
#include "ash/wm/window_util.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h"
#include "ui/gfx/color_analysis.h"
#include "ui/gfx/color_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
// The scale factor that the source window should scale if the source window is
// not the dragged window && is not in splitscreen when drag starts && the user
// has dragged the window to pass the |kIndicatorThresholdRatio| vertical
// threshold.
constexpr float kSourceWindowScale = 0.85;
// Threshold of the fling velocity to keep the dragged window as a new separate
// window after drag ends and do not try to merge it back into source window.
constexpr float kFlingToStayAsNewWindowThreshold = 2000.f;
// The class to observe the source window's bounds change animation. It's used
// to prevent the dragged window to merge back into the source window during
// dragging. Only when the source window restores to its maximized window size,
// the dragged window can be merged back into the source window.
class SourceWindowAnimationObserver : public ui::ImplicitAnimationObserver,
public aura::WindowObserver {
public:
SourceWindowAnimationObserver(aura::Window* source_window,
aura::Window* dragged_window)
: source_window_(source_window), dragged_window_(dragged_window) {
source_window_->AddObserver(this);
dragged_window_->AddObserver(this);
}
~SourceWindowAnimationObserver() override { StopObserving(); }
// ui::ImplicitAnimationObserver:
void OnLayerAnimationStarted(ui::LayerAnimationSequence* sequence) override {
DCHECK(dragged_window_ && source_window_);
dragged_window_->SetProperty(ash::kCanAttachToAnotherWindowKey, false);
}
void OnImplicitAnimationsCompleted() override {
DCHECK(dragged_window_ && source_window_);
// When arriving here, we know the source window bounds change animation
// just ended. Only clear the property ash::kCanAttachToAnotherWindowKey if
// the source window bounds restores to its maximized window size.
gfx::Rect work_area_bounds = display::Screen::GetScreen()
->GetDisplayNearestWindow(source_window_)
.work_area();
::wm::ConvertRectFromScreen(source_window_->parent(), &work_area_bounds);
if (source_window_->bounds() == work_area_bounds)
StopObserving();
}
// aura::WindowObserver:
void OnWindowDestroying(aura::Window* window) override {
DCHECK(window == source_window_ || window == dragged_window_);
StopObserving();
}
private:
void StopObserving() {
StopObservingImplicitAnimations();
if (source_window_) {
source_window_->RemoveObserver(this);
source_window_ = nullptr;
}
if (dragged_window_) {
dragged_window_->RemoveObserver(this);
dragged_window_->ClearProperty(ash::kCanAttachToAnotherWindowKey);
dragged_window_ = nullptr;
}
}
aura::Window* source_window_;
aura::Window* dragged_window_;
DISALLOW_COPY_AND_ASSIGN(SourceWindowAnimationObserver);
};
} // namespace
// WindowsHider hides all visible windows except the currently dragged window
// and the dragged window's source window upon its creation, and restores the
// windows' visibility upon its destruction. It also blurs and darkens the
// background, hides the home launcher if home launcher is enabled. Only need to
// do so if we need to scale up and down the source window when dragging a tab
// window out of it.
class TabletModeBrowserWindowDragDelegate::WindowsHider
: public aura::WindowObserver {
public:
explicit WindowsHider(aura::Window* dragged_window)
: dragged_window_(dragged_window) {
DCHECK(dragged_window);
aura::Window* source_window =
dragged_window->GetProperty(ash::kTabDraggingSourceWindowKey);
DCHECK(source_window);
// Disable the backdrop for |source_window| during dragging.
source_window_backdrop_ = source_window->GetProperty(kBackdropWindowMode);
source_window->SetProperty(kBackdropWindowMode,
BackdropWindowMode::kDisabled);
DCHECK(!Shell::Get()->overview_controller()->InOverviewSession());
aura::Window* root_window = dragged_window->GetRootWindow();
std::vector<aura::Window*> windows =
Shell::Get()->mru_window_tracker()->BuildMruWindowList();
for (aura::Window* window : windows) {
if (window == dragged_window || window == source_window ||
window->GetRootWindow() != root_window) {
continue;
}
window_visibility_map_.emplace(window, window->IsVisible());
if (window->IsVisible()) {
ScopedAnimationDisabler disabler(window);
window->Hide();
}
window->AddObserver(this);
}
// Hide the home launcher if it's enabled during dragging.
Shell::Get()->home_screen_controller()->OnWindowDragStarted();
// Blurs the wallpaper background.
auto* wallpaper_view = RootWindowController::ForWindow(root_window)
->wallpaper_widget_controller()
->wallpaper_view();
if (wallpaper_view) {
wallpaper_view->RepaintBlurAndOpacity(kWallpaperBlurSigma,
kShieldOpacity);
}
}
~WindowsHider() override {
// It might be possible that |source_window| is destroyed during dragging.
aura::Window* source_window =
dragged_window_->GetProperty(ash::kTabDraggingSourceWindowKey);
if (source_window)
source_window->SetProperty(kBackdropWindowMode, source_window_backdrop_);
for (auto iter = window_visibility_map_.begin();
iter != window_visibility_map_.end(); ++iter) {
iter->first->RemoveObserver(this);
if (iter->second) {
ScopedAnimationDisabler disabler(iter->first);
iter->first->Show();
}
}
DCHECK(!Shell::Get()->overview_controller()->InOverviewSession());
// May reshow the home launcher after dragging.
Shell::Get()->home_screen_controller()->OnWindowDragEnded();
// Clears the background wallpaper blur.
auto* wallpaper_view =
RootWindowController::ForWindow(dragged_window_->GetRootWindow())
->wallpaper_widget_controller()
->wallpaper_view();
if (wallpaper_view)
wallpaper_view->RepaintBlurAndOpacity(kWallpaperClearBlurSigma, 1.f);
}
// aura::WindowObserver:
void OnWindowDestroying(aura::Window* window) override {
window->RemoveObserver(this);
window_visibility_map_.erase(window);
}
void OnWindowVisibilityChanged(aura::Window* window, bool visible) override {
if (visible) {
// Do not let |window| change to visible during the lifetime of |this|.
// Also update |window_visibility_map_| so that we can restore the window
// visibility correctly.
window->Hide();
window_visibility_map_[window] = visible;
}
// else do nothing. It must come from Hide() function above thus should be
// ignored.
}
private:
// The currently dragged window. Guaranteed to be non-nullptr during the
// lifetime of |this|.
aura::Window* dragged_window_;
// Maintains the map between windows and their visibilities. All windows
// except the dragged window and the source window should stay hidden during
// dragging.
std::map<aura::Window*, bool> window_visibility_map_;
// The original backdrop mode of the source window. Should be disabled during
// dragging.
BackdropWindowMode source_window_backdrop_ = BackdropWindowMode::kAuto;
DISALLOW_COPY_AND_ASSIGN(WindowsHider);
};
TabletModeBrowserWindowDragDelegate::TabletModeBrowserWindowDragDelegate() =
default;
TabletModeBrowserWindowDragDelegate::~TabletModeBrowserWindowDragDelegate() =
default;
void TabletModeBrowserWindowDragDelegate::PrepareWindowDrag(
const gfx::Point& location_in_screen) {
DCHECK(dragged_window_);
wm::WindowState* window_state = wm::GetWindowState(dragged_window_);
window_state->OnDragStarted(window_state->drag_details()->window_component);
}
void TabletModeBrowserWindowDragDelegate::UpdateWindowDrag(
const gfx::Point& location_in_screen) {
DCHECK(dragged_window_);
// Update the source window if necessary.
UpdateSourceWindow(location_in_screen);
}
void TabletModeBrowserWindowDragDelegate::EndingWindowDrag(
ToplevelWindowEventHandler::DragResult result,
const gfx::Point& location_in_screen) {
if (result == ToplevelWindowEventHandler::DragResult::SUCCESS)
wm::GetWindowState(dragged_window_)->OnCompleteDrag(location_in_screen);
else
wm::GetWindowState(dragged_window_)->OnRevertDrag(location_in_screen);
}
void TabletModeBrowserWindowDragDelegate::EndedWindowDrag(
const gfx::Point& location_in_screen) {
MergeBackToSourceWindowIfApplicable(location_in_screen);
}
void TabletModeBrowserWindowDragDelegate::StartFling(
const ui::GestureEvent* event) {
if (event->details().velocity_y() > kFlingToStayAsNewWindowThreshold)
can_merge_back_to_source_window_ = false;
}
bool TabletModeBrowserWindowDragDelegate::ShouldOpenOverviewWhenDragStarts() {
DCHECK(dragged_window_);
aura::Window* source_window =
dragged_window_->GetProperty(ash::kTabDraggingSourceWindowKey);
return !source_window;
}
void TabletModeBrowserWindowDragDelegate::UpdateSourceWindow(
const gfx::Point& location_in_screen) {
// Only do the scale if the source window is not the dragged window && the
// source window is not in splitscreen && the source window is not in
// overview.
aura::Window* source_window =
dragged_window_->GetProperty(ash::kTabDraggingSourceWindowKey);
if (!source_window || source_window == dragged_window_ ||
split_view_controller_->IsWindowInSplitView(source_window) ||
source_window->GetProperty(ash::kIsShowingInOverviewKey)) {
return;
}
// Only create WindowHider if we need to scale up/down the source window.
if (!windows_hider_)
windows_hider_ = std::make_unique<WindowsHider>(dragged_window_);
const gfx::Rect work_area_bounds =
display::Screen::GetScreen()
->GetDisplayNearestWindow(dragged_window_)
.work_area();
gfx::Rect expected_bounds(work_area_bounds);
if (location_in_screen.y() >=
GetIndicatorsVerticalThreshold(work_area_bounds)) {
SplitViewController::SnapPosition snap_position =
GetSnapPosition(location_in_screen);
if (snap_position == SplitViewController::NONE) {
// Scale down the source window if the event location passes the vertical
// |kIndicatorThresholdRatio| threshold.
expected_bounds.ClampToCenteredSize(
gfx::Size(work_area_bounds.width() * kSourceWindowScale,
work_area_bounds.height() * kSourceWindowScale));
} else {
// Put the source window on the other side of the split screen.
expected_bounds = split_view_controller_->GetSnappedWindowBoundsInScreen(
source_window, snap_position == SplitViewController::LEFT
? SplitViewController::RIGHT
: SplitViewController::LEFT);
}
}
::wm::ConvertRectFromScreen(source_window->parent(), &expected_bounds);
if (expected_bounds != source_window->GetTargetBounds()) {
ui::ScopedLayerAnimationSettings settings(
source_window->layer()->GetAnimator());
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
source_window_bounds_observer_ =
std::make_unique<SourceWindowAnimationObserver>(source_window,
dragged_window_);
settings.AddObserver(source_window_bounds_observer_.get());
source_window->SetBounds(expected_bounds);
}
}
void TabletModeBrowserWindowDragDelegate::MergeBackToSourceWindowIfApplicable(
const gfx::Point& location_in_screen) {
// No need to merge back if we're not in tab dragging process.
if (!wm::IsDraggingTabs(dragged_window_))
return;
aura::Window* source_window =
dragged_window_->GetProperty(ash::kTabDraggingSourceWindowKey);
// Do not merge back if there is no source window.
if (!source_window)
return;
// Do not merge back if the dragged window is not capable of merging back.
// This may happen if the drag ends because of a fling event and the fling
// velocity has exceeded kFlingToStayAsNewWindowThreshold.
if (!can_merge_back_to_source_window_)
return;
// Do not merge back if the window has dragged farther than half of the screen
// height.
const gfx::Rect work_area_bounds =
display::Screen::GetScreen()
->GetDisplayNearestWindow(dragged_window_)
.work_area();
if (location_in_screen.y() >= work_area_bounds.CenterPoint().y())
return;
SplitViewController::SnapPosition desired_snap_position =
GetSnapPosition(location_in_screen);
// If splitscreen is not active, do not merge back if the dragged window is
// in the drag-to-snap preview area.
if (!split_view_controller_->InSplitViewMode() &&
desired_snap_position != SplitViewController::NONE) {
return;
}
// In splitscreen, do not merge back if the drag point is on one side of the
// split view divider and the source window is snapped on the opposite side.
if (split_view_controller_->InSplitViewMode()) {
const int drag_position = IsCurrentScreenOrientationLandscape()
? location_in_screen.x()
: location_in_screen.y();
const bool is_dragging_on_left =
IsCurrentScreenOrientationPrimary()
? drag_position < split_view_controller_->divider_position()
: drag_position > split_view_controller_->divider_position();
aura::Window* window_on_opposite_side =
is_dragging_on_left ? split_view_controller_->right_window()
: split_view_controller_->left_window();
if (source_window == window_on_opposite_side)
return;
}
// Arriving here we know the dragged window should merge back into its source
// window.
source_window->SetProperty(ash::kIsDeferredTabDraggingTargetWindowKey, true);
}
} // namespace ash