blob: b394e9ec8875f9733bd957d56123495dd98a7dd5 [file] [log] [blame]
// Copyright 2019 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 "ui/base/x/x11_drag_drop_client.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/base/x/x11_os_exchange_data_provider.h"
#include "ui/base/x/x11_util.h"
#include "ui/gfx/x/connection.h"
#include "ui/gfx/x/x11_atom_cache.h"
#include "ui/gfx/x/xproto.h"
#include "ui/gfx/x/xproto_util.h"
// Reading recommended for understanding the implementation in this file:
//
// * The X Window System Concepts section in The X New Developer’s Guide
// * The X Selection Mechanism paper by Keith Packard
// * The Peer-to-Peer Communication by Means of Selections section in the
// ICCCM (X Consortium's Inter-Client Communication Conventions Manual)
// * The XDND specification, Drag-and-Drop Protocol for the X Window System
// * The XDS specification, The Direct Save Protocol for the X Window System
//
// All the readings are freely available online.
namespace ui {
namespace {
constexpr int kWillAcceptDrop = 1;
constexpr int kWantFurtherPosEvents = 2;
// The lowest XDND protocol version that we understand.
//
// The XDND protocol specification says that we must support all versions
// between 3 and the version we advertise in the XDndAware property.
constexpr int kMinXdndVersion = 3;
// The value used in the XdndAware property.
//
// The XDND protocol version used between two windows will be the minimum
// between the two versions advertised in the XDndAware property.
constexpr int kMaxXdndVersion = 5;
// Window property that tells other applications the window understands XDND.
const char kXdndAware[] = "XdndAware";
// Window property that holds the supported drag and drop data types.
// This property is set on the XDND source window when the drag and drop data
// can be converted to more than 3 types.
const char kXdndTypeList[] = "XdndTypeList";
// These actions have the same meaning as in the W3C Drag and Drop spec.
const char kXdndActionCopy[] = "XdndActionCopy";
const char kXdndActionMove[] = "XdndActionMove";
const char kXdndActionLink[] = "XdndActionLink";
// Triggers the XDS protocol.
const char kXdndActionDirectSave[] = "XdndActionDirectSave";
// Window property that contains the possible actions that will be presented to
// the user when the drag and drop action is kXdndActionAsk.
const char kXdndActionList[] = "XdndActionList";
// Window property on the source window and message used by the XDS protocol.
// This atom name intentionally includes the XDS protocol version (0).
// After the source sends the XdndDrop message, this property stores the
// (path-less) name of the file to be saved, and has the type text/plain, with
// an optional charset attribute.
// When receiving an XdndDrop event, the target needs to check for the
// XdndDirectSave property on the source window. The target then modifies the
// XdndDirectSave on the source window, and sends an XdndDirectSave message to
// the source.
// After the target sends the XdndDirectSave message, this property stores an
// URL indicating the location where the source should save the file.
const char kXdndDirectSave0[] = "XdndDirectSave0";
// Window property pointing to a proxy window to receive XDND target messages.
// The XDND source must check the proxy window must for the XdndAware property,
// and must send all XDND messages to the proxy instead of the target. However,
// the target field in the messages must still represent the original target
// window (the window pointed to by the cursor).
const char kXdndProxy[] = "XdndProxy";
// Message sent from an XDND source to the target when the user confirms the
// drag and drop operation.
const char kXdndDrop[] = "XdndDrop";
// Message sent from an XDND source to the target to start the XDND protocol.
// The target must wait for an XDndPosition event before querying the data.
const char kXdndEnter[] = "XdndEnter";
// Message sent from an XDND target to the source in response to an XdndDrop.
// The message must be sent whether the target acceepts the drop or not.
const char kXdndFinished[] = "XdndFinished";
// Message sent from an XDND source to the target when the user cancels the drag
// and drop operation.
const char kXdndLeave[] = "XdndLeave";
// Message sent by the XDND source when the cursor position changes.
// The source will also send an XdndPosition event right after the XdndEnter
// event, to tell the target about the initial cursor position and the desired
// drop action.
// The time stamp in the XdndPosition must be used when requesting selection
// information.
// After the target optionally acquires selection information, it must tell the
// source if it can accept the drop via an XdndStatus message.
const char kXdndPosition[] = "XdndPosition";
// Message sent by the XDND target in response to an XdndPosition message.
// The message informs the source if the target will accept the drop, and what
// action will be taken if the drop is accepted.
const char kXdndStatus[] = "XdndStatus";
static base::LazyInstance<std::map<x11::Window, XDragDropClient*>>::Leaky
g_live_client_map = LAZY_INSTANCE_INITIALIZER;
// Converts a bitfield of actions into an Atom that represents what action
// we're most likely to take on drop.
x11::Atom XDragOperationToAtom(int drag_operation) {
if (drag_operation & DragDropTypes::DRAG_COPY)
return x11::GetAtom(kXdndActionCopy);
if (drag_operation & DragDropTypes::DRAG_MOVE)
return x11::GetAtom(kXdndActionMove);
if (drag_operation & DragDropTypes::DRAG_LINK)
return x11::GetAtom(kXdndActionLink);
return x11::Atom::None;
}
// Converts a single action atom to a drag operation.
DragDropTypes::DragOperation AtomToDragOperation(x11::Atom atom) {
if (atom == x11::GetAtom(kXdndActionCopy))
return DragDropTypes::DRAG_COPY;
if (atom == x11::GetAtom(kXdndActionMove))
return DragDropTypes::DRAG_MOVE;
if (atom == x11::GetAtom(kXdndActionLink))
return DragDropTypes::DRAG_LINK;
return DragDropTypes::DRAG_NONE;
}
} // namespace
int XGetMaskAsEventFlags() {
x11::KeyButMask mask{};
auto* connection = x11::Connection::Get();
if (auto reply =
connection->QueryPointer({connection->default_root()}).Sync()) {
mask = reply->mask;
}
int modifiers = ui::EF_NONE;
if (static_cast<bool>(mask & x11::KeyButMask::Shift))
modifiers |= ui::EF_SHIFT_DOWN;
if (static_cast<bool>(mask & x11::KeyButMask::Control))
modifiers |= ui::EF_CONTROL_DOWN;
if (static_cast<bool>(mask & x11::KeyButMask::Mod1))
modifiers |= ui::EF_ALT_DOWN;
if (static_cast<bool>(mask & x11::KeyButMask::Mod4))
modifiers |= ui::EF_COMMAND_DOWN;
if (static_cast<bool>(mask & x11::KeyButMask::Button1))
modifiers |= ui::EF_LEFT_MOUSE_BUTTON;
if (static_cast<bool>(mask & x11::KeyButMask::Button2))
modifiers |= ui::EF_MIDDLE_MOUSE_BUTTON;
if (static_cast<bool>(mask & x11::KeyButMask::Button3))
modifiers |= ui::EF_RIGHT_MOUSE_BUTTON;
return modifiers;
}
// static
XDragDropClient* XDragDropClient::GetForWindow(x11::Window window) {
std::map<x11::Window, XDragDropClient*>::const_iterator it =
g_live_client_map.Get().find(window);
if (it == g_live_client_map.Get().end())
return nullptr;
return it->second;
}
XDragDropClient::XDragDropClient(XDragDropClient::Delegate* delegate,
x11::Window xwindow)
: delegate_(delegate), xwindow_(xwindow) {
DCHECK(delegate_);
// Mark that we are aware of drag and drop concepts.
uint32_t xdnd_version = kMaxXdndVersion;
SetProperty(xwindow_, x11::GetAtom(kXdndAware), x11::Atom::ATOM,
xdnd_version);
// Some tests change the DesktopDragDropClientAuraX11 associated with an
// |xwindow|.
g_live_client_map.Get()[xwindow] = this;
}
XDragDropClient::~XDragDropClient() {
g_live_client_map.Get().erase(xwindow());
}
std::vector<x11::Atom> XDragDropClient::GetOfferedDragOperations() const {
std::vector<x11::Atom> operations;
if (drag_operation_ & DragDropTypes::DRAG_COPY)
operations.push_back(x11::GetAtom(kXdndActionCopy));
if (drag_operation_ & DragDropTypes::DRAG_MOVE)
operations.push_back(x11::GetAtom(kXdndActionMove));
if (drag_operation_ & DragDropTypes::DRAG_LINK)
operations.push_back(x11::GetAtom(kXdndActionLink));
return operations;
}
void XDragDropClient::CompleteXdndPosition(x11::Window source_window,
const gfx::Point& screen_point) {
int drag_operation = delegate_->UpdateDrag(screen_point);
// Sends an XdndStatus message back to the source_window. l[2,3]
// theoretically represent an area in the window where the current action is
// the same as what we're returning, but I can't find any implementation that
// actually making use of this. A client can return (0, 0) and/or set the
// first bit of l[1] to disable the feature, and it appears that gtk neither
// sets this nor respects it if set.
auto xev = PrepareXdndClientMessage(kXdndStatus, source_window);
xev.data.data32[1] =
(drag_operation != 0) ? (kWantFurtherPosEvents | kWillAcceptDrop) : 0;
xev.data.data32[4] =
static_cast<uint32_t>(XDragOperationToAtom(drag_operation));
SendXClientEvent(source_window, xev);
}
void XDragDropClient::ProcessMouseMove(const gfx::Point& screen_point,
unsigned long event_time) {
if (source_state_ != SourceState::kOther)
return;
// Find the current window the cursor is over.
x11::Window dest_window = FindWindowFor(screen_point);
if (target_current_window_ != dest_window) {
if (target_current_window_ != x11::Window::None)
SendXdndLeave(target_current_window_);
target_current_window_ = dest_window;
waiting_on_status_ = false;
next_position_message_.reset();
status_received_since_enter_ = false;
negotiated_operation_ = DragDropTypes::DRAG_NONE;
if (target_current_window_ != x11::Window::None) {
std::vector<x11::Atom> targets;
source_provider_->RetrieveTargets(&targets);
SendXdndEnter(target_current_window_, targets);
}
}
if (target_current_window_ != x11::Window::None) {
if (waiting_on_status_) {
next_position_message_ =
std::make_unique<std::pair<gfx::Point, unsigned long>>(screen_point,
event_time);
} else {
SendXdndPosition(dest_window, screen_point, event_time);
}
}
}
bool XDragDropClient::HandleXdndEvent(const x11::ClientMessageEvent& event) {
x11::Atom message_type = event.type;
if (message_type == x11::GetAtom("XdndEnter"))
OnXdndEnter(event);
else if (message_type == x11::GetAtom("XdndLeave"))
OnXdndLeave(event);
else if (message_type == x11::GetAtom("XdndPosition"))
OnXdndPosition(event);
else if (message_type == x11::GetAtom("XdndStatus"))
OnXdndStatus(event);
else if (message_type == x11::GetAtom("XdndFinished"))
OnXdndFinished(event);
else if (message_type == x11::GetAtom("XdndDrop"))
OnXdndDrop(event);
else
return false;
return true;
}
void XDragDropClient::OnXdndEnter(const x11::ClientMessageEvent& event) {
DVLOG(1) << "OnXdndEnter, version "
<< ((event.data.data32[1] & 0xff000000) >> 24);
int version = (event.data.data32[1] & 0xff000000) >> 24;
if (version < kMinXdndVersion) {
// This protocol version is not documented in the XDND standard (last
// revised in 1999), so we don't support it. Since don't understand the
// protocol spoken by the source, we can't tell it that we can't talk to it.
LOG(ERROR) << "XdndEnter message discarded because its version is too old.";
return;
}
if (version > kMaxXdndVersion) {
// The XDND version used should be the minimum between the versions
// advertised by the source and the target. We advertise kMaxXdndVersion, so
// this should never happen when talking to an XDND-compliant application.
LOG(ERROR) << "XdndEnter message discarded because its version is too new.";
return;
}
// Make sure that we've run ~X11DragContext() before creating another one.
ResetDragContext();
auto* source_client =
GetForWindow(static_cast<x11::Window>(event.data.data32[0]));
DCHECK(!source_client || source_client->source_provider_);
target_current_context_ = std::make_unique<XDragContext>(
xwindow_, event,
(source_client ? source_client->source_provider_->GetFormatMap()
: SelectionFormatMap()));
if (!source_client) {
// The window doesn't have a DesktopDragDropClientAuraX11, which means it's
// created by some other process. Listen for messages on it.
delegate_->OnBeginForeignDrag(
static_cast<x11::Window>(event.data.data32[0]));
}
// In the Windows implementation, we immediately call DesktopDropTargetWin::
// Translate(). The XDND specification demands that we wait until we receive
// an XdndPosition message before we use XConvertSelection or send an
// XdndStatus message.
}
void XDragDropClient::OnXdndPosition(const x11::ClientMessageEvent& event) {
DVLOG(1) << "OnXdndPosition";
auto source_window = static_cast<x11::Window>(event.data.data32[0]);
int x_root_window = event.data.data32[2] >> 16;
int y_root_window = event.data.data32[2] & 0xffff;
x11::Time time_stamp = static_cast<x11::Time>(event.data.data32[3]);
x11::Atom suggested_action = static_cast<x11::Atom>(event.data.data32[4]);
if (!target_current_context()) {
NOTREACHED();
return;
}
target_current_context()->OnXdndPositionMessage(
this, suggested_action, source_window, time_stamp,
gfx::Point(x_root_window, y_root_window));
}
void XDragDropClient::OnXdndStatus(const x11::ClientMessageEvent& event) {
DVLOG(1) << "OnXdndStatus";
auto source_window = static_cast<x11::Window>(event.data.data32[0]);
if (source_window != target_current_window_)
return;
if (source_state_ != SourceState::kPendingDrop &&
source_state_ != SourceState::kOther) {
return;
}
waiting_on_status_ = false;
status_received_since_enter_ = true;
if (event.data.data32[1] & 1) {
x11::Atom atom_operation = static_cast<x11::Atom>(event.data.data32[4]);
negotiated_operation_ = AtomToDragOperation(atom_operation);
} else {
negotiated_operation_ = DragDropTypes::DRAG_NONE;
}
if (source_state_ == SourceState::kPendingDrop) {
// We were waiting on the status message so we could send the XdndDrop.
if (negotiated_operation_ == DragDropTypes::DRAG_NONE) {
EndMoveLoop();
return;
}
source_state_ = SourceState::kDropped;
SendXdndDrop(source_window);
return;
}
delegate_->UpdateCursor(negotiated_operation_);
// Note: event.data.[2,3] specify a rectangle. It is a request by the other
// window to not send further XdndPosition messages while the cursor is
// within it. However, it is considered advisory and (at least according to
// the spec) the other side must handle further position messages within
// it. GTK+ doesn't bother with this, so neither should we.
if (next_position_message_.get()) {
// We were waiting on the status message so we could send off the next
// position message we queued up.
gfx::Point p = next_position_message_->first;
unsigned long event_time = next_position_message_->second;
next_position_message_.reset();
SendXdndPosition(source_window, p, event_time);
}
}
void XDragDropClient::OnXdndLeave(const x11::ClientMessageEvent& event) {
DVLOG(1) << "OnXdndLeave";
delegate_->OnBeforeDragLeave();
ResetDragContext();
}
void XDragDropClient::OnXdndDrop(const x11::ClientMessageEvent& event) {
DVLOG(1) << "OnXdndDrop";
auto source_window = static_cast<x11::Window>(event.data.data32[0]);
int drag_operation = delegate_->PerformDrop();
auto xev = PrepareXdndClientMessage(kXdndFinished, source_window);
xev.data.data32[1] = (drag_operation != 0) ? 1 : 0;
xev.data.data32[2] =
static_cast<uint32_t>(XDragOperationToAtom(drag_operation));
SendXClientEvent(source_window, xev);
}
void XDragDropClient::OnXdndFinished(const x11::ClientMessageEvent& event) {
DVLOG(1) << "OnXdndFinished";
auto source_window = static_cast<x11::Window>(event.data.data32[0]);
if (target_current_window_ != source_window)
return;
// Clear |negotiated_operation_| if the drag was rejected.
if ((event.data.data32[1] & 1) == 0)
negotiated_operation_ = DragDropTypes::DRAG_NONE;
// Clear |target_current_window_| to avoid sending XdndLeave upon ending the
// move loop.
target_current_window_ = x11::Window::None;
EndMoveLoop();
}
void XDragDropClient::OnSelectionNotify(
const x11::SelectionNotifyEvent& xselection) {
DVLOG(1) << "OnSelectionNotify";
if (target_current_context_)
target_current_context_->OnSelectionNotify(xselection);
// ICCCM requires us to delete the property passed into SelectionNotify.
if (xselection.property != x11::Atom::None)
x11::DeleteProperty(xwindow_, xselection.property);
}
void XDragDropClient::InitDrag(int operation, const OSExchangeData* data) {
target_current_window_ = x11::Window::None;
source_state_ = SourceState::kOther;
waiting_on_status_ = false;
next_position_message_.reset();
status_received_since_enter_ = false;
drag_operation_ = operation;
negotiated_operation_ = DragDropTypes::DRAG_NONE;
source_provider_ =
static_cast<const XOSExchangeDataProvider*>(&data->provider());
source_provider_->TakeOwnershipOfSelection();
std::vector<x11::Atom> actions = GetOfferedDragOperations();
if (!source_provider_->file_contents_name().empty()) {
actions.push_back(x11::GetAtom(kXdndActionDirectSave));
SetStringProperty(xwindow_, x11::GetAtom(kXdndDirectSave0),
x11::GetAtom(kMimeTypeText),
source_provider_->file_contents_name().AsUTF8Unsafe());
}
SetArrayProperty(xwindow_, x11::GetAtom(kXdndActionList), x11::Atom::ATOM,
actions);
}
void XDragDropClient::CleanupDrag() {
source_provider_ = nullptr;
x11::DeleteProperty(xwindow_, x11::GetAtom(kXdndActionList));
x11::DeleteProperty(xwindow_, x11::GetAtom(kXdndDirectSave0));
}
void XDragDropClient::UpdateModifierState(int flags) {
const int kModifiers = EF_SHIFT_DOWN | EF_CONTROL_DOWN | EF_ALT_DOWN |
EF_COMMAND_DOWN | EF_LEFT_MOUSE_BUTTON |
EF_MIDDLE_MOUSE_BUTTON | EF_RIGHT_MOUSE_BUTTON;
current_modifier_state_ = flags & kModifiers;
}
void XDragDropClient::ResetDragContext() {
if (!target_current_context())
return;
XDragDropClient* source_client =
GetForWindow(target_current_context()->source_window());
if (!source_client)
delegate_->OnEndForeignDrag();
target_current_context_.reset();
}
void XDragDropClient::StopRepeatMouseMoveTimer() {
repeat_mouse_move_timer_.Stop();
}
void XDragDropClient::StartEndMoveLoopTimer() {
end_move_loop_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(1000),
this, &XDragDropClient::EndMoveLoop);
}
void XDragDropClient::StopEndMoveLoopTimer() {
end_move_loop_timer_.Stop();
}
void XDragDropClient::HandleMouseMovement(const gfx::Point& screen_point,
int flags,
base::TimeTicks event_time) {
UpdateModifierState(flags);
StopRepeatMouseMoveTimer();
ProcessMouseMove(screen_point,
(event_time - base::TimeTicks()).InMilliseconds());
}
void XDragDropClient::HandleMouseReleased() {
StopRepeatMouseMoveTimer();
if (source_state_ != SourceState::kOther) {
// The user has previously released the mouse and is clicking in
// frustration.
EndMoveLoop();
return;
}
if (target_current_window_ != x11::Window::None) {
if (waiting_on_status_) {
if (status_received_since_enter_) {
// If we are waiting for an XdndStatus message, we need to wait for it
// to complete.
source_state_ = SourceState::kPendingDrop;
// Start timer to end the move loop if the target takes too long to send
// the XdndStatus and XdndFinished messages.
StartEndMoveLoopTimer();
return;
}
EndMoveLoop();
return;
}
if (negotiated_operation() != DragDropTypes::DRAG_NONE) {
// Start timer to end the move loop if the target takes too long to send
// an XdndFinished message. It is important that StartEndMoveLoopTimer()
// is called before SendXdndDrop() because SendXdndDrop()
// sends XdndFinished synchronously if the drop target is a Chrome
// window.
StartEndMoveLoopTimer();
// We have negotiated an action with the other end.
source_state_ = SourceState::kDropped;
SendXdndDrop(target_current_window_);
return;
} else {
// No transfer is negotiated. We need to tell the target window that we
// are leaving.
SendXdndLeave(target_current_window_);
}
}
EndMoveLoop();
}
void XDragDropClient::HandleMoveLoopEnded() {
if (target_current_window_ != x11::Window::None) {
SendXdndLeave(target_current_window_);
target_current_window_ = x11::Window::None;
}
ResetDragContext();
StopRepeatMouseMoveTimer();
StopEndMoveLoopTimer();
}
x11::ClientMessageEvent XDragDropClient::PrepareXdndClientMessage(
const char* message,
x11::Window recipient) const {
x11::ClientMessageEvent xev;
xev.type = x11::GetAtom(message);
xev.window = recipient;
xev.format = 32;
xev.data.data32.fill(0);
xev.data.data32[0] = static_cast<uint32_t>(xwindow_);
return xev;
}
x11::Window XDragDropClient::FindWindowFor(const gfx::Point& screen_point) {
auto finder = delegate_->CreateWindowFinder();
x11::Window target = finder->FindWindowAt(screen_point);
if (target == x11::Window::None)
return x11::Window::None;
// TODO(crbug/651775): The proxy window should be reported separately from the
// target window. XDND messages should be sent to the proxy, and their
// window field should point to the target.
// Figure out which window we should test as XdndAware. If |target| has
// XdndProxy, it will set that proxy on target, and if not, |target|'s
// original value will remain.
GetProperty(target, x11::GetAtom(kXdndProxy), &target);
uint32_t version;
if (GetProperty(target, x11::GetAtom(kXdndAware), &version) &&
version >= kMaxXdndVersion) {
return target;
}
return x11::Window::None;
}
void XDragDropClient::SendXClientEvent(x11::Window window,
const x11::ClientMessageEvent& xev) {
// Don't send messages to the X11 message queue if we can help it.
XDragDropClient* short_circuit = GetForWindow(window);
if (short_circuit && short_circuit->HandleXdndEvent(xev))
return;
// I don't understand why the GTK+ code is doing what it's doing here. It
// goes out of its way to send the XEvent so that it receives a callback on
// success or failure, and when it fails, it then sends an internal
// GdkEvent about the failed drag. (And sending this message doesn't appear
// to go through normal xlib machinery, but instead passes through the low
// level xProto (the x11 wire format) that I don't understand.
//
// I'm unsure if I have to jump through those hoops, or if XSendEvent is
// sufficient.
x11::SendEvent(xev, window, x11::EventMask::NoEvent);
}
void XDragDropClient::SendXdndEnter(x11::Window dest_window,
const std::vector<x11::Atom>& targets) {
auto xev = PrepareXdndClientMessage(kXdndEnter, dest_window);
xev.data.data32[1] = (kMaxXdndVersion << 24); // The version number.
if (targets.size() > 3) {
xev.data.data32[1] |= 1;
SetArrayProperty(xwindow(), x11::GetAtom(kXdndTypeList), x11::Atom::ATOM,
targets);
} else {
// Pack the targets into the enter message.
for (size_t i = 0; i < targets.size(); ++i)
xev.data.data32[2 + i] = static_cast<uint32_t>(targets[i]);
}
SendXClientEvent(dest_window, xev);
}
void XDragDropClient::SendXdndPosition(x11::Window dest_window,
const gfx::Point& screen_point,
unsigned long event_time) {
waiting_on_status_ = true;
auto xev = PrepareXdndClientMessage(kXdndPosition, dest_window);
xev.data.data32[2] = (screen_point.x() << 16) | screen_point.y();
xev.data.data32[3] = event_time;
xev.data.data32[4] =
static_cast<uint32_t>(XDragOperationToAtom(drag_operation_));
SendXClientEvent(dest_window, xev);
// http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html and
// the Xdnd protocol both recommend that drag events should be sent
// periodically.
repeat_mouse_move_timer_.Start(
FROM_HERE, base::TimeDelta::FromMilliseconds(350),
base::BindOnce(&XDragDropClient::ProcessMouseMove, base::Unretained(this),
screen_point, event_time));
}
void XDragDropClient::SendXdndLeave(x11::Window dest_window) {
auto xev = PrepareXdndClientMessage(kXdndLeave, dest_window);
SendXClientEvent(dest_window, xev);
}
void XDragDropClient::SendXdndDrop(x11::Window dest_window) {
auto xev = PrepareXdndClientMessage(kXdndDrop, dest_window);
xev.data.data32[2] = static_cast<uint32_t>(x11::Time::CurrentTime);
SendXClientEvent(dest_window, xev);
}
void XDragDropClient::EndMoveLoop() {
StopEndMoveLoopTimer();
delegate_->EndDragLoop();
}
} // namespace ui