blob: 3ea0e81e26f406e5aa5c9056d80e101cd623e7b0 [file] [log] [blame]
// 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 "components/exo/extended_drag_source.h"
#include <memory>
#include <string>
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/wm/toplevel_window_event_handler.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "components/exo/data_source.h"
#include "components/exo/surface.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_observer.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h"
#include "ui/base/hit_test.h"
#include "ui/events/event.h"
#include "ui/events/event_target.h"
#include "ui/events/event_utils.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/public/window_move_client.h"
namespace exo {
using ::ui::mojom::DragOperation;
// static
ExtendedDragSource* ExtendedDragSource::instance_ = nullptr;
// Internal representation of a toplevel window, backed by an Exo shell surface,
// which is being dragged. It supports both already mapped/visible windows as
// well as newly created ones (i.e: not added to a root window yet), in which
// case OnDraggedWindowVisibilityChanged callback is called to notify when it
// has just got visible.
class ExtendedDragSource::DraggedWindowHolder : public aura::WindowObserver {
public:
DraggedWindowHolder(Surface* surface,
const gfx::Vector2d& drag_offset,
ExtendedDragSource* source)
: surface_(surface), drag_offset_(drag_offset), source_(source) {
DCHECK(surface_);
DCHECK(surface_->window());
if (!FindToplevelWindow()) {
DVLOG(1) << "Dragged window not added to root window yet.";
surface_->window()->AddObserver(this);
}
}
DraggedWindowHolder(const DraggedWindowHolder&) = delete;
DraggedWindowHolder& operator=(const DraggedWindowHolder&) = delete;
~DraggedWindowHolder() override {
if (toplevel_window_) {
toplevel_window_->RemoveObserver(this);
toplevel_window_ = nullptr;
} else {
surface_->window()->RemoveObserver(this);
}
}
aura::Window* toplevel_window() { return toplevel_window_; }
const gfx::Vector2d& offset() const { return drag_offset_; }
private:
// aura::WindowObserver:
void OnWindowAddedToRootWindow(aura::Window* window) override {
DCHECK_EQ(window, surface_->window());
FindToplevelWindow();
DCHECK(toplevel_window_);
surface_->window()->RemoveObserver(this);
}
void OnWindowVisibilityChanged(aura::Window* window, bool visible) override {
DCHECK(window);
if (window == toplevel_window_)
source_->OnDraggedWindowVisibilityChanged(visible);
}
bool FindToplevelWindow() {
if (!surface_->window()->GetRootWindow())
return false;
toplevel_window_ = surface_->window()->GetToplevelWindow();
toplevel_window_->AddObserver(this);
return true;
}
Surface* const surface_;
gfx::Vector2d drag_offset_;
ExtendedDragSource* const source_;
aura::Window* toplevel_window_ = nullptr;
};
// static
ExtendedDragSource* ExtendedDragSource::Get() {
return instance_;
}
ExtendedDragSource::ExtendedDragSource(DataSource* source, Delegate* delegate)
: source_(source), delegate_(delegate) {
DCHECK(source_);
DCHECK(delegate_);
source_->AddObserver(this);
DCHECK(!instance_);
instance_ = this;
}
ExtendedDragSource::~ExtendedDragSource() {
delegate_->OnDataSourceDestroying();
for (auto& observer : observers_)
observer.OnExtendedDragSourceDestroying(this);
if (source_)
source_->RemoveObserver(this);
DCHECK_EQ(instance_, this);
instance_ = nullptr;
}
void ExtendedDragSource::AddObserver(Observer* observer) {
DCHECK(observer);
observers_.AddObserver(observer);
}
void ExtendedDragSource::RemoveObserver(Observer* observer) {
DCHECK(observer);
observers_.RemoveObserver(observer);
}
void ExtendedDragSource::Drag(Surface* dragged_surface,
const gfx::Vector2d& drag_offset) {
// Associated data source already destroyed.
if (!source_)
return;
if (!dragged_surface) {
DVLOG(1) << "Unsetting dragged surface.";
dragged_window_holder_.reset();
return;
}
DVLOG(1) << "Dragged surface changed:"
<< " surface=" << dragged_surface
<< " offset=" << drag_offset.ToString();
// Ensure that the surface already has a "role" assigned.
DCHECK(dragged_surface->HasSurfaceDelegate());
dragged_window_holder_ =
std::make_unique<DraggedWindowHolder>(dragged_surface, drag_offset, this);
// Drag process will be started once OnDragStarted gets called.
}
bool ExtendedDragSource::IsActive() const {
return !!source_;
}
void ExtendedDragSource::OnToplevelWindowDragStarted(
const gfx::PointF& start_location,
ui::mojom::DragEventSource source,
aura::Window* drag_source_window) {
pointer_location_ = start_location;
drag_event_source_ = source;
drag_source_window_ =
drag_source_window ? drag_source_window->GetToplevelWindow() : nullptr;
MaybeLockCursor();
if (dragged_window_holder_ && dragged_window_holder_->toplevel_window())
StartDrag(dragged_window_holder_->toplevel_window(), start_location);
}
DragOperation ExtendedDragSource::OnToplevelWindowDragDropped() {
DVLOG(1) << "OnDragDropped()";
Cleanup();
return delegate_->ShouldAllowDropAnywhere() ? DragOperation::kMove
: DragOperation::kNone;
}
void ExtendedDragSource::OnToplevelWindowDragCancelled() {
DVLOG(1) << "OnDragCancelled()";
// TODO(crbug.com/1099418): Handle cancellation/revert.
Cleanup();
}
void ExtendedDragSource::OnToplevelWindowDragEvent(ui::LocatedEvent* event) {
DCHECK(event);
pointer_location_ = event->root_location_f();
if (!dragged_window_holder_)
return;
auto* handler = ash::Shell::Get()->toplevel_window_event_handler();
if (event->IsMouseEvent()) {
handler->OnMouseEvent(event->AsMouseEvent());
return;
}
if (event->IsGestureEvent()) {
handler->OnGestureEvent(event->AsGestureEvent());
return;
}
NOTREACHED() << "Only mouse and touch events are supported.";
}
void ExtendedDragSource::OnDataSourceDestroying(DataSource* source) {
DCHECK_EQ(source, source_);
source_->RemoveObserver(this);
source_ = nullptr;
}
void ExtendedDragSource::MaybeLockCursor() {
if (delegate_->ShouldLockCursor()) {
ash::Shell::Get()->cursor_manager()->LockCursor();
cursor_locked_ = true;
}
}
void ExtendedDragSource::UnlockCursor() {
if (cursor_locked_) {
ash::Shell::Get()->cursor_manager()->UnlockCursor();
cursor_locked_ = false;
}
}
void ExtendedDragSource::StartDrag(aura::Window* toplevel,
const gfx::PointF& pointer_location) {
// Ensure |toplevel| window does skip events while it's being dragged.
event_blocker_ =
std::make_unique<aura::ScopedWindowEventTargetingBlocker>(toplevel);
// Disable visibility change animations on the dragged window.
toplevel->SetProperty(aura::client::kAnimationsDisabledKey, true);
DVLOG(1) << "Starting drag. pointer_loc=" << pointer_location.ToString();
auto* toplevel_handler = ash::Shell::Get()->toplevel_window_event_handler();
auto move_source = drag_event_source_ == ui::mojom::DragEventSource::kTouch
? ::wm::WINDOW_MOVE_SOURCE_TOUCH
: ::wm::WINDOW_MOVE_SOURCE_MOUSE;
auto end_closure = base::BindOnce(
[](aura::Window* toplevel,
base::WeakPtr<ExtendedDragSource> extended_drag_source,
ash::ToplevelWindowEventHandler::DragResult result) {
if (toplevel) {
toplevel->ClearProperty(ash::kIsDraggingTabsKey);
toplevel->ClearProperty(ash::kTabDraggingSourceWindowKey);
}
if (extended_drag_source)
extended_drag_source->dragged_window_holder_.reset();
},
base::Unretained(toplevel), weak_factory_.GetWeakPtr());
// TODO(crbug.com/1167581): Experiment setting |update_gesture_target| back
// to true when capture is removed from drag and drop.
toplevel_handler->AttemptToStartDrag(toplevel, pointer_location, HTCAPTION,
move_source, std::move(end_closure),
/*update_gesture_target=*/false,
/*grab_capture=*/false);
}
void ExtendedDragSource::OnDraggedWindowVisibilityChanged(bool visible) {
DCHECK(dragged_window_holder_);
DVLOG(1) << "Dragged window visibility changed. visible=" << visible;
if (!visible) {
dragged_window_holder_.reset();
return;
}
aura::Window* toplevel = dragged_window_holder_->toplevel_window();
DCHECK(toplevel);
DCHECK(drag_source_window_);
toplevel->SetProperty(ash::kIsDraggingTabsKey, true);
if (drag_source_window_ != toplevel) {
toplevel->SetProperty(ash::kTabDraggingSourceWindowKey,
drag_source_window_);
}
// The |toplevel| window for the dragged surface has just been created and
// it's about to be mapped. Calculate and set its position based on
// |drag_offset_| and |pointer_location_| before starting the actual drag.
auto screen_location = CalculateOrigin(toplevel);
toplevel->SetBounds({screen_location, toplevel->bounds().size()});
DVLOG(1) << "Dragged window mapped. toplevel=" << toplevel
<< " origin=" << screen_location.ToString();
gfx::PointF pointer_location(screen_location +
dragged_window_holder_->offset());
StartDrag(toplevel, pointer_location);
}
gfx::Point ExtendedDragSource::CalculateOrigin(aura::Window* target) const {
DCHECK(dragged_window_holder_);
gfx::Point screen_location = gfx::ToRoundedPoint(pointer_location_);
wm::ConvertPointToScreen(target->GetRootWindow(), &screen_location);
return screen_location - dragged_window_holder_->offset();
}
void ExtendedDragSource::Cleanup() {
if (dragged_window_holder_ && dragged_window_holder_->toplevel_window()) {
dragged_window_holder_->toplevel_window()->ClearProperty(
aura::client::kAnimationsDisabledKey);
}
event_blocker_.reset();
drag_source_window_ = nullptr;
UnlockCursor();
}
aura::Window* ExtendedDragSource::GetDraggedWindowForTesting() {
return dragged_window_holder_ ? dragged_window_holder_->toplevel_window()
: nullptr;
}
absl::optional<gfx::Vector2d> ExtendedDragSource::GetDragOffsetForTesting()
const {
return dragged_window_holder_
? absl::optional<gfx::Vector2d>(dragged_window_holder_->offset())
: absl::nullopt;
}
} // namespace exo