// Copyright 2023 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/wm/overview/overview_item_base.h"
#include <vector>
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/style/rounded_label_widget.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/desks/templates/saved_desk_animations.h"
#include "ash/wm/drag_window_controller.h"
#include "ash/wm/overview/overview_constants.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_grid_event_handler.h"
#include "ash/wm/overview/overview_group_item.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/overview/scoped_overview_animation_settings.h"
#include "ash/wm/snap_group/snap_group.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_constants.h"
#include "base/memory/raw_ptr.h"
namespace ash {
OverviewItemBase::OverviewItemBase(OverviewSession* overview_session,
OverviewGrid* overview_grid,
aura::Window* root_window)
: root_window_(root_window),
overview_session_(overview_session),
overview_grid_(overview_grid) {}
OverviewItemBase::~OverviewItemBase() = default;
// static
std::unique_ptr<OverviewItemBase> OverviewItemBase::Create(
aura::Window* window,
OverviewSession* overview_session,
OverviewGrid* overview_grid) {
SnapGroupController* snap_group_controller = SnapGroupController::Get();
if (snap_group_controller) {
if (SnapGroup* snap_group =
snap_group_controller->GetSnapGroupForGivenWindow(window)) {
return std::make_unique<OverviewGroupItem>(
std::vector<raw_ptr<aura::Window, VectorExperimental>>{
snap_group->GetPhysicallyLeftOrTopWindow(),
snap_group->GetPhysicallyRightOrBottomWindow()},
overview_session, overview_grid);
}
}
return std::make_unique<OverviewItem>(window, overview_session, overview_grid,
/*destruction_delegate=*/overview_grid,
/*event_handler_delegate=*/nullptr,
/*eligible_for_shadow_config=*/true);
}
bool OverviewItemBase::IsDragItem() const {
// `overview_session_` may be null in tests.
// TODO(https://b/299391958): `overview_session_` should not be null even in
// tests.
return overview_session_ &&
overview_session_->GetCurrentDraggedOverviewItem() == this;
}
void OverviewItemBase::SetVisibleDuringItemDragging(bool visible,
bool animate) {
SetWindowsVisibleDuringItemDragging(GetWindowsForHomeGesture(), visible,
animate);
}
void OverviewItemBase::RefreshShadowVisuals(bool shadow_visible) {
const bool should_have_shadow = ShouldHaveShadow();
if (should_have_shadow != !!shadow_) {
if (should_have_shadow) {
CreateShadow();
} else {
shadow_.reset();
}
}
// On destruction, `shadow_` is cleaned up before `transform_window_`, which
// may call this function, so early exit if `shadow_` is nullptr.
if (!shadow_) {
return;
}
const gfx::RectF shadow_bounds_in_screen = target_bounds_;
auto* shadow_layer = shadow_->GetLayer();
// Shadow is normally turned off during animations and reapplied when on
// animation complete.
if (!shadow_visible || shadow_bounds_in_screen.IsEmpty()) {
shadow_layer->SetVisible(false);
return;
}
shadow_layer->SetVisible(true);
gfx::Rect shadow_content_bounds(
gfx::ToRoundedRect(shadow_bounds_in_screen).size());
shadow_->SetContentBounds(shadow_content_bounds);
shadow_->SetRoundedCornerRadius(
window_util::GetMiniWindowRoundedCornerRadius());
}
void OverviewItemBase::UpdateShadowTypeForDrag(bool is_dragging) {
if (shadow_) {
shadow_->SetType(is_dragging ? kDraggedShadowType : kDefaultShadowType);
}
}
void OverviewItemBase::HandleGestureEventForTabletModeLayout(
ui::GestureEvent* event,
OverviewItemBase* event_source_item) {
const gfx::PointF location = event->details().bounding_box_f().CenterPoint();
OverviewGridEventHandler* grid_event_handler =
overview_grid()->grid_event_handler();
const bool is_drag_item = IsDragItem();
switch (event->type()) {
case ui::EventType::kScrollFlingStart:
if (is_drag_item) {
HandleFlingStartEvent(location, event->details().velocity_x(),
event->details().velocity_y());
} else {
grid_event_handler->OnGestureEvent(event);
}
break;
case ui::EventType::kGestureScrollBegin:
if (std::abs(event->details().scroll_y_hint()) >
std::abs(event->details().scroll_x_hint())) {
HandlePressEvent(location, /*from_touch_gesture=*/true,
event_source_item);
} else {
grid_event_handler->OnGestureEvent(event);
}
break;
case ui::EventType::kGestureScrollUpdate:
if (is_drag_item) {
HandleDragEvent(location);
} else {
grid_event_handler->OnGestureEvent(event);
}
break;
case ui::EventType::kGestureScrollEnd:
if (is_drag_item) {
HandleReleaseEvent(location);
} else {
grid_event_handler->OnGestureEvent(event);
}
break;
case ui::EventType::kGestureLongPress:
HandlePressEvent(location, /*from_touch_gesture=*/true,
event_source_item);
HandleLongPressEvent(location);
break;
case ui::EventType::kGestureTap:
HandleTapEvent(location, event_source_item);
break;
case ui::EventType::kGestureEnd:
HandleGestureEndEvent();
break;
default:
grid_event_handler->OnGestureEvent(event);
break;
}
}
void OverviewItemBase::HandleMouseEvent(const ui::MouseEvent& event,
OverviewItemBase* event_source_item) {
if (!overview_session_->CanProcessEvent(this, /*from_touch_gesture=*/false)) {
return;
}
// `event.target()` will be null if we use search+space on this item with
// chromevox on. Accessibility API will synthesize a mouse event in that case
// without a target. We just use the centerpoint of the item so that
// search+space will select the item, leaving overview.
const gfx::PointF screen_location =
event.target() ? event.target()->GetScreenLocationF(event)
: gfx::PointF(GetWindowsUnionScreenBounds().CenterPoint());
switch (event.type()) {
case ui::EventType::kMousePressed:
HandlePressEvent(screen_location, /*from_touch_gesture=*/false,
event_source_item);
break;
case ui::EventType::kMouseReleased:
HandleReleaseEvent(screen_location);
break;
case ui::EventType::kMouseDragged:
HandleDragEvent(screen_location);
break;
default:
NOTREACHED();
}
}
void OverviewItemBase::HandleGestureEvent(ui::GestureEvent* event,
OverviewItemBase* event_source_item) {
if (!overview_session_->CanProcessEvent(this, /*from_touch_gesture=*/true)) {
event->StopPropagation();
event->SetHandled();
return;
}
if (ShouldUseTabletModeGridLayout()) {
HandleGestureEventForTabletModeLayout(event, event_source_item);
return;
}
const gfx::PointF location = event->details().bounding_box_f().CenterPoint();
switch (event->type()) {
case ui::EventType::kGestureTapDown:
HandlePressEvent(location, /*from_touch_gesture=*/true,
event_source_item);
break;
case ui::EventType::kGestureScrollUpdate:
HandleDragEvent(location);
break;
case ui::EventType::kScrollFlingStart:
HandleFlingStartEvent(location, event->details().velocity_x(),
event->details().velocity_y());
break;
case ui::EventType::kGestureScrollEnd:
HandleReleaseEvent(location);
break;
case ui::EventType::kGestureLongPress:
HandleLongPressEvent(location);
break;
case ui::EventType::kGestureTap:
HandleTapEvent(location, event_source_item);
break;
case ui::EventType::kGestureEnd:
HandleGestureEndEvent();
break;
default:
break;
}
}
void OverviewItemBase::SetOpacity(float opacity) {
item_widget_->SetOpacity(opacity);
if (cannot_snap_widget_) {
cannot_snap_widget_->SetOpacity(opacity);
}
}
aura::Window::Windows OverviewItemBase::GetWindowsForHomeGesture() {
aura::Window::Windows windows = {item_widget_->GetNativeWindow()};
if (cannot_snap_widget_) {
windows.push_back(cannot_snap_widget_->GetNativeWindow());
}
return windows;
}
void OverviewItemBase::HideForSavedDeskLibrary(bool animate) {
// Temporarily hide this window in overview, so that dark/light theme change
// does not reset the layer visible. If `animate` is false, the callback will
// not run in `PerformFadeOutLayer`. Thus, here we make sure the window is
// also hidden in that case.
DCHECK(item_widget_);
hide_window_in_overview_callback_.Reset(base::BindOnce(
&OverviewItemBase::HideItemWidgetWindow, weak_ptr_factory_.GetWeakPtr()));
PerformFadeOutLayer(item_widget_->GetLayer(), animate,
hide_window_in_overview_callback_.callback());
if (!animate) {
// Cancel the callback if we are going to run it directly.
hide_window_in_overview_callback_.Cancel();
HideItemWidgetWindow();
}
item_widget_event_blocker_ =
std::make_unique<aura::ScopedWindowEventTargetingBlocker>(
item_widget_->GetNativeWindow());
// TODO(http://b/339108996): Determine how to inform users when a group item
// cannot be snapped.
HideCannotSnapWarning(animate);
}
void OverviewItemBase::RevertHideForSavedDeskLibrary(bool animate) {
// This might run before `HideForSavedDeskLibrary()`, thus cancel the
// callback to prevent such case.
hide_window_in_overview_callback_.Cancel();
// Restore and show the window back to overview.
ShowItemWidgetWindow();
// `item_widget_` may be null during shutdown if the window is minimized.
if (item_widget_) {
PerformFadeInLayer(item_widget_->GetLayer(), animate);
}
item_widget_event_blocker_.reset();
// TODO(http://b/339108996): Determine how to inform users when a group item
// cannot be snapped.
UpdateCannotSnapWarningVisibility(animate);
}
void OverviewItemBase::UpdateMirrorsForDragging(bool is_touch_dragging) {
CHECK_GT(Shell::GetAllRootWindows().size(), 1u);
if (!item_mirror_for_dragging_) {
item_mirror_for_dragging_ = std::make_unique<DragWindowController>(
item_widget_->GetNativeWindow(), is_touch_dragging);
}
item_mirror_for_dragging_->Update();
}
// Resets the mirrors needed for multi display dragging.
void OverviewItemBase::DestroyMirrorsForDragging() {
item_mirror_for_dragging_.reset();
}
views::Widget::InitParams OverviewItemBase::CreateOverviewItemWidgetParams(
aura::Window* parent_window,
const std::string& widget_name,
bool accept_event) const {
views::Widget::InitParams params(
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
views::Widget::InitParams::TYPE_POPUP);
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.name = widget_name;
params.accept_events = accept_event;
params.parent = parent_window;
params.init_properties_container.SetProperty(kHideInDeskMiniViewKey, true);
params.init_properties_container.SetProperty(kOverviewUiKey, true);
return params;
}
void OverviewItemBase::CreateShadow() {
shadow_ = SystemShadow::CreateShadowOnNinePatchLayer(
kDefaultShadowType, SystemShadow::LayerRecreatedCallback());
auto* shadow_layer = shadow_->GetLayer();
auto* widget_layer = item_widget_->GetLayer();
widget_layer->Add(shadow_layer);
widget_layer->StackAtBottom(shadow_layer);
shadow_->ObserveColorProviderSource(item_widget_.get());
}
void OverviewItemBase::HandleDragEvent(const gfx::PointF& location_in_screen) {
if (IsDragItem()) {
overview_session_->Drag(this, location_in_screen);
}
}
void OverviewItemBase::HideItemWidgetWindow() {
ScopedOverviewHideWindows* hide_windows =
overview_session_->hide_windows_for_saved_desks_grid();
DCHECK(hide_windows);
// Hide the overview item window.
if (item_widget_ &&
!hide_windows->HasWindow(item_widget_->GetNativeWindow())) {
hide_windows->AddWindow(item_widget_->GetNativeWindow());
}
}
void OverviewItemBase::ShowItemWidgetWindow() {
ScopedOverviewHideWindows* hide_windows =
overview_session_->hide_windows_for_saved_desks_grid();
DCHECK(hide_windows);
// Show the overview item window.
if (item_widget_ &&
hide_windows->HasWindow(item_widget_->GetNativeWindow())) {
hide_windows->RemoveWindow(item_widget_->GetNativeWindow(),
/*show_window=*/true);
}
}
void OverviewItemBase::HandlePressEvent(const gfx::PointF& location_in_screen,
bool from_touch_gesture,
OverviewItemBase* event_source_item) {
// No need to start the drag again if already in a drag. This can happen if we
// switch fingers midway through a drag.
if (!IsDragItem()) {
StartDrag();
overview_session_->InitiateDrag(this, location_in_screen,
/*is_touch_dragging=*/from_touch_gesture,
event_source_item);
}
}
void OverviewItemBase::HandleReleaseEvent(
const gfx::PointF& location_in_screen) {
if (IsDragItem()) {
overview_session_->CompleteDrag(this, location_in_screen);
}
}
void OverviewItemBase::HandleLongPressEvent(
const gfx::PointF& location_in_screen) {
if (IsDragItem() && (IsEligibleForDraggingToSnapInOverview(this) ||
(desks_util::ShouldDesksBarBeCreated() &&
overview_grid_->IsDesksBarViewActive()))) {
overview_session_->StartNormalDragMode(location_in_screen);
}
}
void OverviewItemBase::HandleFlingStartEvent(
const gfx::PointF& location_in_screen,
float velocity_x,
float velocity_y) {
overview_session_->Fling(this, location_in_screen, velocity_x, velocity_y);
}
void OverviewItemBase::HandleTapEvent(const gfx::PointF& location_in_screen,
OverviewItemBase* event_source_item) {
if (IsDragItem()) {
overview_session_->ActivateDraggedWindow();
return;
}
overview_session_->SelectWindow(event_source_item);
}
void OverviewItemBase::HandleGestureEndEvent() {
if (IsDragItem()) {
// Gesture end events come from a long press getting canceled. Long press
// alters the stacking order, so on gesture end, make sure we restore the
// stacking order on the next reposition.
set_should_restack_on_animation_end(true);
overview_session_->ResetDraggedWindowGesture();
}
}
} // namespace ash