| // Copyright 2020 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/drag_drop/tab_drag_drop_delegate.h" |
| |
| #include "ash/constants/app_types.h" |
| #include "ash/constants/ash_features.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_browser_window_drag_session_windows_hider.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 "ui/aura/client/aura_constants.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(https://crbug.com/1069869): 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 = |
| static_cast<AppType>(window->GetProperty(aura::client::kAppType)); |
| return app_type == 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) { |
| if (!features::IsWebUITabStripTabDragIntegrationEnabled()) |
| return false; |
| |
| 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_->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_->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_); |
| |
| SplitViewController::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 = SplitViewController::SnapPosition::NONE; |
| |
| 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(); |
| |
| auto closure = base::BindOnce(&TabDragDropDelegate::OnNewBrowserWindowCreated, |
| base::Owned(this), location_in_screen); |
| NewWindowDelegate::GetPrimary()->NewWindowForDetachingTab( |
| source_window_, drop_data, std::move(closure)); |
| } |
| |
| void TabDragDropDelegate::OnNewBrowserWindowCreated( |
| const gfx::Point& location_in_screen, |
| aura::Window* new_window) { |
| 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::IsLacrosPrimaryEnabled()) { |
| LOG(ERROR) |
| << "New browser window creation for tab detaching failed.\n" |
| << "Check whether about:flags#lacros-primary is enabled or " |
| << "--enable-features=LacrosPrimary is passed in when launching Ash"; |
| } |
| return; |
| } |
| |
| const gfx::Rect area = |
| screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| root_window_); |
| |
| SplitViewController::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 = SplitViewController::SnapPosition::NONE; |
| |
| if (snap_position_in_snapping_zone == SplitViewController::SnapPosition::NONE) |
| 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(); |
| SplitViewController::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 == SplitViewController::SnapPosition::NONE) |
| 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 == |
| SplitViewController::SnapPosition::NONE && |
| split_view_controller->GetPositionOfSnappedWindow(source_window_) != |
| snap_position) { |
| overview_session->MergeWindowIntoOverviewForWebUITabStrip(new_window); |
| } else { |
| WindowState::Get(new_window) |
| ->set_snap_action_source(WindowSnapActionSource::kDragTabToSnap); |
| split_view_controller->SnapWindow(new_window, snap_position, |
| /*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. |
| SplitViewController::SnapPosition opposite_position = |
| (snap_position == SplitViewController::SnapPosition::LEFT) |
| ? SplitViewController::SnapPosition::RIGHT |
| : SplitViewController::SnapPosition::LEFT; |
| |
| // |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. |
| WindowState::Get(new_window) |
| ->set_snap_action_source(WindowSnapActionSource::kDragTabToSnap); |
| split_view_controller->SnapWindow(source_window_, opposite_position); |
| } |
| |
| bool TabDragDropDelegate::ShouldPreventSnapToTheEdge( |
| const gfx::Point& location_in_screen) { |
| SplitViewController* const split_view_controller = |
| SplitViewController::Get(source_window_); |
| return !split_view_controller->InSplitViewMode() && |
| split_view_controller->IsLayoutHorizontal(source_window_) && |
| location_in_screen.y() < |
| Shell::Get()->shell_delegate()->GetBrowserWebUITabStripHeight(); |
| } |
| |
| void TabDragDropDelegate::UpdateSourceWindowBoundsIfNecessary( |
| SplitViewController::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<TabletModeBrowserWindowDragSessionWindowsHider>( |
| source_window_, nullptr); |
| } |
| |
| gfx::Rect new_source_window_bounds; |
| if (candidate_snap_position == SplitViewController::SnapPosition::NONE) { |
| 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 SplitViewController::SnapPosition opposite_position = |
| (candidate_snap_position == SplitViewController::SnapPosition::LEFT) |
| ? SplitViewController::SnapPosition::RIGHT |
| : SplitViewController::SnapPosition::LEFT; |
| new_source_window_bounds = |
| SplitViewController::Get(source_window_) |
| ->GetSnappedWindowBoundsInScreen(opposite_position, source_window_); |
| } |
| 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; |
| |
| const gfx::Rect area = |
| screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| root_window_); |
| |
| source_window_->SetBounds(area); |
| } |
| |
| } // namespace ash |