// Copyright 2020 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/drag_drop/tab_drag_drop_delegate.h"
#include "ash/constants/ash_features.h"
#include "ash/drag_drop/tab_drag_drop_windows_hider.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_drag_indicators.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/tablet_mode/tablet_mode_window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_metrics.h"
#include "base/pickle.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/crosapi/cpp/lacros_startup_state.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "ui/base/clipboard/clipboard_format_type.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
// The following distances are copied from tablet_mode_window_drag_delegate.cc.
// TODO(crbug.com/40126106): share these constants.
// Items dragged to within |kDistanceFromEdgeDp| of the screen will get snapped
// even if they have not moved by |kMinimumDragToSnapDistanceDp|.
constexpr float kDistanceFromEdgeDp = 16.f;
// The minimum distance that an item must be moved before it is snapped. This
// prevents accidental snaps.
constexpr float kMinimumDragToSnapDistanceDp = 96.f;
// 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;
// The UMA histogram that records presentation time for tab dragging in
// tablet mode with webui tab strip enable.
constexpr char kTabDraggingInTabletModeHistogram[] =
"Ash.TabDrag.PresentationTime.TabletMode";
constexpr char kTabDraggingInTabletModeMaxLatencyHistogram[] =
"Ash.TabDrag.PresentationTime.MaxLatency.TabletMode";
DEFINE_UI_CLASS_PROPERTY_KEY(bool, kIsSourceWindowForDrag, false)
bool IsLacrosWindow(const aura::Window* window) {
auto app_type = window->GetProperty(chromeos::kAppTypeKey);
return app_type == chromeos::AppType::LACROS;
}
// Returns the overview session if overview mode is active, otherwise returns
// nullptr.
OverviewSession* GetOverviewSession() {
return Shell::Get()->overview_controller()->InOverviewSession()
? Shell::Get()->overview_controller()->overview_session()
: nullptr;
}
} // namespace
// static
bool TabDragDropDelegate::IsChromeTabDrag(const ui::OSExchangeData& drag_data) {
return Shell::Get()->shell_delegate()->IsTabDrag(drag_data);
}
// static
bool TabDragDropDelegate::IsSourceWindowForDrag(const aura::Window* window) {
return window->GetProperty(kIsSourceWindowForDrag);
}
TabDragDropDelegate::TabDragDropDelegate(
aura::Window* root_window,
aura::Window* source_window,
const gfx::Point& start_location_in_screen)
: root_window_(root_window),
source_window_(source_window->GetToplevelWindow()),
start_location_in_screen_(start_location_in_screen) {
DCHECK(root_window_);
DCHECK(source_window_);
source_window_->AddObserver(this);
source_window_->SetProperty(kIsSourceWindowForDrag, true);
split_view_drag_indicators_ =
std::make_unique<SplitViewDragIndicators>(root_window_);
tab_dragging_recorder_ = CreatePresentationTimeHistogramRecorder(
source_window_->layer()->GetCompositor(),
kTabDraggingInTabletModeHistogram,
kTabDraggingInTabletModeMaxLatencyHistogram);
}
TabDragDropDelegate::~TabDragDropDelegate() {
tab_dragging_recorder_.reset();
if (!source_window_) {
return;
}
source_window_->RemoveObserver(this);
if (source_window_->is_destroying())
return;
if (!source_window_->GetProperty(kIsSourceWindowForDrag))
return;
// If we didn't drop to a new window, we must restore the original window.
RestoreSourceWindowBounds();
source_window_->ClearProperty(kIsSourceWindowForDrag);
}
void TabDragDropDelegate::DragUpdate(const gfx::Point& location_in_screen) {
const gfx::Rect area =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window_);
SnapPosition snap_position = ash::GetSnapPositionForLocation(
Shell::GetPrimaryRootWindow(), location_in_screen,
start_location_in_screen_,
/*snap_distance_from_edge=*/kDistanceFromEdgeDp,
/*minimum_drag_distance=*/kMinimumDragToSnapDistanceDp,
/*horizontal_edge_inset=*/area.width() *
kHighlightScreenPrimaryAxisRatio +
kHighlightScreenEdgePaddingDp,
/*vertical_edge_inset=*/area.height() * kHighlightScreenPrimaryAxisRatio +
kHighlightScreenEdgePaddingDp);
if (ShouldPreventSnapToTheEdge(location_in_screen))
snap_position = SnapPosition::kNone;
split_view_drag_indicators_->SetWindowDraggingState(
SplitViewDragIndicators::ComputeWindowDraggingState(
true, SplitViewDragIndicators::WindowDraggingState::kFromTop,
snap_position));
UpdateSourceWindowBoundsIfNecessary(snap_position, location_in_screen);
tab_dragging_recorder_->RequestNext();
}
void TabDragDropDelegate::DropAndDeleteSelf(
const gfx::Point& location_in_screen,
const ui::OSExchangeData& drop_data) {
tab_dragging_recorder_.reset();
// Release input capture in advance.
ReleaseCapture();
auto closure = base::BindOnce(&TabDragDropDelegate::OnNewBrowserWindowCreated,
base::Owned(this), location_in_screen);
NewWindowDelegate::GetPrimary()->NewWindowForDetachingTab(
source_window_, drop_data, std::move(closure));
}
void TabDragDropDelegate::OnWindowDestroying(aura::Window* window) {
if (source_window_ == window) {
windows_hider_.reset();
source_window_->RemoveObserver(this);
source_window_ = nullptr;
}
}
void TabDragDropDelegate::OnNewBrowserWindowCreated(
const gfx::Point& location_in_screen,
aura::Window* new_window) {
// `source_window_` could reset to nullptr during the drag.
if (!source_window_) {
DCHECK(!new_window);
return;
}
auto is_lacros = IsLacrosWindow(source_window_);
// https://crbug.com/1286203:
// It's possible new window is created when the dragged WebContents
// closes itself during the drag session.
if (!new_window) {
if (is_lacros && !crosapi::lacros_startup_state::IsLacrosEnabled()) {
LOG(ERROR) << "New browser window creation for tab detaching failed.\n"
<< "Check whether Lacros is enabled";
}
return;
}
const gfx::Rect area =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window_);
SnapPosition snap_position_in_snapping_zone = ash::GetSnapPosition(
root_window_, new_window, location_in_screen, start_location_in_screen_,
/*snap_distance_from_edge=*/kDistanceFromEdgeDp,
/*minimum_drag_distance=*/kMinimumDragToSnapDistanceDp,
/*horizontal_edge_inset=*/area.width() *
kHighlightScreenPrimaryAxisRatio +
kHighlightScreenEdgePaddingDp,
/*vertical_edge_inset=*/area.height() * kHighlightScreenPrimaryAxisRatio +
kHighlightScreenEdgePaddingDp);
if (ShouldPreventSnapToTheEdge(location_in_screen))
snap_position_in_snapping_zone = SnapPosition::kNone;
if (snap_position_in_snapping_zone == SnapPosition::kNone) {
RestoreSourceWindowBounds();
}
// This must be done after restoring the source window's bounds since
// otherwise the SetBounds() call may have no effect.
source_window_->ClearProperty(kIsSourceWindowForDrag);
SplitViewController* const split_view_controller =
SplitViewController::Get(new_window);
// If it's already in split view mode, either snap the new window
// to the left or the right depending on the drop location.
const bool in_split_view_mode = split_view_controller->InSplitViewMode();
SnapPosition snap_position = snap_position_in_snapping_zone;
if (in_split_view_mode) {
snap_position =
split_view_controller->ComputeSnapPosition(location_in_screen);
}
if (snap_position == SnapPosition::kNone) {
return;
}
OverviewSession* overview_session = GetOverviewSession();
// If overview session is present on the other side and the new window is
// about to snap to that side but not in the snapping zone then drop the new
// window into overview.
if (overview_session &&
snap_position_in_snapping_zone == SnapPosition::kNone &&
split_view_controller->GetPositionOfSnappedWindow(source_window_) !=
snap_position) {
overview_session->MergeWindowIntoOverviewForWebUITabStrip(new_window);
} else {
split_view_controller->SnapWindow(new_window, snap_position,
WindowSnapActionSource::kDragTabToSnap,
/*activate_window=*/true);
}
// Do not snap the source window if already in split view mode.
if (in_split_view_mode)
return;
// The tab drag source window is the last window the user was
// interacting with. When dropping into split view, it makes the most
// sense to snap this window to the opposite side. Do this.
SnapPosition opposite_position = (snap_position == SnapPosition::kPrimary)
? SnapPosition::kSecondary
: SnapPosition::kPrimary;
// |source_window_| is itself a child window of the browser since it
// hosts web content (specifically, the tab strip WebUI). Snap its
// toplevel window which is the browser window.
split_view_controller->SnapWindow(source_window_, opposite_position,
WindowSnapActionSource::kDragTabToSnap);
}
bool TabDragDropDelegate::ShouldPreventSnapToTheEdge(
const gfx::Point& location_in_screen) {
SplitViewController* const split_view_controller =
SplitViewController::Get(source_window_);
return !split_view_controller->InSplitViewMode() &&
IsLayoutHorizontal(source_window_) &&
location_in_screen.y() <
Shell::Get()->shell_delegate()->GetBrowserWebUITabStripHeight();
}
void TabDragDropDelegate::UpdateSourceWindowBoundsIfNecessary(
SnapPosition candidate_snap_position,
const gfx::Point& location_in_screen) {
SplitViewController* const split_view_controller =
SplitViewController::Get(source_window_);
if (split_view_controller->IsWindowInSplitView(source_window_))
return;
if (!windows_hider_) {
windows_hider_ = std::make_unique<TabDragDropWindowsHider>(source_window_);
}
gfx::Rect new_source_window_bounds;
if (candidate_snap_position == SnapPosition::kNone) {
const gfx::Rect area =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window_);
new_source_window_bounds = area;
// Only shrink the window when the tab is dragged out of WebUI tab strip.
if (location_in_screen.y() >
Shell::Get()->shell_delegate()->GetBrowserWebUITabStripHeight()) {
new_source_window_bounds.ClampToCenteredSize(
gfx::Size(area.width() * kSourceWindowScale,
area.height() * kSourceWindowScale));
}
} else {
const SnapPosition opposite_position =
(candidate_snap_position == SnapPosition::kPrimary)
? SnapPosition::kSecondary
: SnapPosition::kPrimary;
new_source_window_bounds =
SplitViewController::Get(source_window_)
->GetSnappedWindowBoundsInScreen(
opposite_position, source_window_,
window_util::GetSnapRatioForWindow(source_window_),
/*account_for_divider_width=*/
display::Screen::GetScreen()->InTabletMode());
}
wm::ConvertRectFromScreen(source_window_->parent(),
&new_source_window_bounds);
if (new_source_window_bounds != source_window_->GetTargetBounds()) {
ui::ScopedLayerAnimationSettings settings(
source_window_->layer()->GetAnimator());
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
source_window_->SetBounds(new_source_window_bounds);
}
}
void TabDragDropDelegate::RestoreSourceWindowBounds() {
if (SplitViewController::Get(source_window_)
->IsWindowInSplitView(source_window_)) {
return;
}
auto* window_state = WindowState::Get(source_window_);
if (window_state->IsFloated()) {
// This will notify `FloatController` to find the ideal floated window
// bounds in tablet mode.
TabletModeWindowState::UpdateWindowPosition(
window_state, WindowState::BoundsChangeAnimationType::kNone);
return;
}
const gfx::Rect area =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
root_window_);
source_window_->SetBounds(area);
}
} // namespace ash