517 lines
15 KiB
C++
517 lines
15 KiB
C++
/*
|
|
KWin - the KDE window manager
|
|
This file is part of the KDE project.
|
|
|
|
SPDX-FileCopyrightText: 2019 Roman Gilg <subdiff@gmail.com>
|
|
SPDX-FileCopyrightText: 2021 David Edmundson <davidedmundson@kde.org>
|
|
SPDX-FileCopyrightText: 2021 David Redondo <kde@david-redondo.de>
|
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
#include "drag_x.h"
|
|
|
|
#include "databridge.h"
|
|
#include "datasource.h"
|
|
#include "dnd.h"
|
|
#include "selection_source.h"
|
|
#include "xwayland.h"
|
|
|
|
#include "atoms.h"
|
|
#include "wayland/datadevice_interface.h"
|
|
#include "wayland/datasource_interface.h"
|
|
#include "wayland/seat_interface.h"
|
|
#include "wayland/surface_interface.h"
|
|
#include "wayland_server.h"
|
|
#include "window.h"
|
|
#include "workspace.h"
|
|
|
|
#include <QMouseEvent>
|
|
#include <QTimer>
|
|
|
|
namespace KWin
|
|
{
|
|
namespace Xwl
|
|
{
|
|
|
|
using DnDAction = KWaylandServer::DataDeviceManagerInterface::DnDAction;
|
|
using DnDActions = KWaylandServer::DataDeviceManagerInterface::DnDActions;
|
|
|
|
static QStringList atomToMimeTypes(xcb_atom_t atom)
|
|
{
|
|
QStringList mimeTypes;
|
|
|
|
if (atom == atoms->utf8_string) {
|
|
mimeTypes << QString::fromLatin1("text/plain;charset=utf-8");
|
|
} else if (atom == atoms->text) {
|
|
mimeTypes << QString::fromLatin1("text/plain");
|
|
} else if (atom == atoms->uri_list || atom == atoms->netscape_url || atom == atoms->moz_url) {
|
|
// We identify netscape and moz format as less detailed formats text/uri-list,
|
|
// text/x-uri and accept the information loss.
|
|
mimeTypes << QString::fromLatin1("text/uri-list") << QString::fromLatin1("text/x-uri");
|
|
} else {
|
|
mimeTypes << Selection::atomName(atom);
|
|
}
|
|
return mimeTypes;
|
|
}
|
|
|
|
XToWlDrag::XToWlDrag(X11Source *source)
|
|
: m_source(source)
|
|
{
|
|
connect(DataBridge::self()->dnd(), &Dnd::transferFinished, this, [this](xcb_timestamp_t eventTime) {
|
|
// we use this mechanism, because the finished call is not
|
|
// reliable done by Wayland clients
|
|
auto it = std::find_if(m_dataRequests.begin(), m_dataRequests.end(), [eventTime](const QPair<xcb_timestamp_t, bool> &req) {
|
|
return req.first == eventTime && req.second == false;
|
|
});
|
|
if (it == m_dataRequests.end()) {
|
|
// transfer finished for a different drag
|
|
return;
|
|
}
|
|
(*it).second = true;
|
|
checkForFinished();
|
|
});
|
|
connect(source, &X11Source::transferReady, this, [this](xcb_atom_t target, qint32 fd) {
|
|
Q_UNUSED(target);
|
|
Q_UNUSED(fd);
|
|
m_dataRequests << QPair<xcb_timestamp_t, bool>(m_source->timestamp(), false);
|
|
});
|
|
connect(&m_selectionSource, &XwlDataSource::dropped, this, [this] {
|
|
m_performed = true;
|
|
if (m_visit) {
|
|
connect(m_visit, &WlVisit::finish, this, [this](WlVisit *visit) {
|
|
Q_UNUSED(visit);
|
|
checkForFinished();
|
|
});
|
|
|
|
QTimer::singleShot(2000, this, [this] {
|
|
if (!m_visit->entered() || !m_visit->dropHandled()) {
|
|
// X client timed out
|
|
Q_EMIT finish(this);
|
|
} else if (m_dataRequests.size() == 0) {
|
|
// Wl client timed out
|
|
m_visit->sendFinished();
|
|
Q_EMIT finish(this);
|
|
}
|
|
});
|
|
}
|
|
// Dave do we need this async finish check anymore?
|
|
checkForFinished();
|
|
});
|
|
connect(&m_selectionSource, &XwlDataSource::finished, this, [this] {
|
|
checkForFinished();
|
|
});
|
|
connect(&m_selectionSource, &XwlDataSource::cancelled, this, [this] {
|
|
if (m_visit && !m_visit->leave()) {
|
|
connect(m_visit, &WlVisit::finish, this, &XToWlDrag::checkForFinished);
|
|
}
|
|
checkForFinished();
|
|
});
|
|
connect(&m_selectionSource, &XwlDataSource::dataRequested, source, &X11Source::startTransfer);
|
|
|
|
auto *seat = waylandServer()->seat();
|
|
int serial = waylandServer()->seat()->pointerButtonSerial(Qt::LeftButton);
|
|
// we know we are the focussed surface as dnd checks
|
|
seat->startDrag(&m_selectionSource, seat->focusedPointerSurface(), serial);
|
|
}
|
|
|
|
XToWlDrag::~XToWlDrag()
|
|
{
|
|
}
|
|
|
|
DragEventReply XToWlDrag::moveFilter(Window *target, const QPoint &pos)
|
|
{
|
|
Q_UNUSED(pos);
|
|
|
|
auto *seat = waylandServer()->seat();
|
|
|
|
if (m_visit && m_visit->target() == target) {
|
|
// still same Wl target, wait for X events
|
|
return DragEventReply::Ignore;
|
|
}
|
|
if (m_visit) {
|
|
if (m_visit->leave()) {
|
|
delete m_visit;
|
|
} else {
|
|
connect(m_visit, &WlVisit::finish, this, [this](WlVisit *visit) {
|
|
m_oldVisits.removeOne(visit);
|
|
delete visit;
|
|
});
|
|
m_oldVisits << m_visit;
|
|
}
|
|
}
|
|
const bool hasCurrent = m_visit;
|
|
m_visit = nullptr;
|
|
|
|
if (!target || !target->surface() || target->surface()->client() == waylandServer()->xWaylandConnection()) {
|
|
// currently there is no target or target is an Xwayland window
|
|
// handled here and by X directly
|
|
if (hasCurrent) {
|
|
// last received enter event is now void,
|
|
// wait for the next one
|
|
seat->setDragTarget(nullptr, nullptr);
|
|
}
|
|
return DragEventReply::Ignore;
|
|
}
|
|
// new Wl native target
|
|
auto *ac = static_cast<Window *>(target);
|
|
m_visit = new WlVisit(ac, this);
|
|
connect(m_visit, &WlVisit::offersReceived, this, &XToWlDrag::setOffers);
|
|
return DragEventReply::Ignore;
|
|
}
|
|
|
|
bool XToWlDrag::handleClientMessage(xcb_client_message_event_t *event)
|
|
{
|
|
for (auto *visit : qAsConst(m_oldVisits)) {
|
|
if (visit->handleClientMessage(event)) {
|
|
return true;
|
|
}
|
|
}
|
|
if (m_visit && m_visit->handleClientMessage(event)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void XToWlDrag::setDragAndDropAction(DnDAction action)
|
|
{
|
|
m_selectionSource.setSupportedDndActions(action);
|
|
}
|
|
|
|
DnDAction XToWlDrag::selectedDragAndDropAction()
|
|
{
|
|
return m_selectionSource.selectedDragAndDropAction();
|
|
}
|
|
|
|
void XToWlDrag::setOffers(const Mimes &offers)
|
|
{
|
|
m_source->setOffers(offers);
|
|
if (offers.isEmpty()) {
|
|
// There are no offers, so just directly set the drag target,
|
|
// no transfer possible anyways.
|
|
setDragTarget();
|
|
return;
|
|
}
|
|
if (m_offers == offers) {
|
|
// offers had been set already by a previous visit
|
|
// Wl side is already configured
|
|
setDragTarget();
|
|
return;
|
|
}
|
|
|
|
m_offers = offers;
|
|
QStringList mimeTypes;
|
|
mimeTypes.reserve(offers.size());
|
|
for (const auto &mimePair : offers) {
|
|
mimeTypes.append(mimePair.first);
|
|
}
|
|
m_selectionSource.setMimeTypes(mimeTypes);
|
|
setDragTarget();
|
|
}
|
|
|
|
using Mime = QPair<QString, xcb_atom_t>;
|
|
|
|
void XToWlDrag::setDragTarget()
|
|
{
|
|
if (!m_visit) {
|
|
return;
|
|
}
|
|
|
|
auto *ac = m_visit->target();
|
|
|
|
auto seat = waylandServer()->seat();
|
|
auto dropTarget = seat->dropHandlerForSurface(ac->surface());
|
|
|
|
if (!dropTarget || !ac->surface()) {
|
|
return;
|
|
}
|
|
seat->setDragTarget(dropTarget, ac->surface(), ac->inputTransformation());
|
|
}
|
|
|
|
bool XToWlDrag::checkForFinished()
|
|
{
|
|
if (!m_visit) {
|
|
// not dropped above Wl native target
|
|
Q_EMIT finish(this);
|
|
return true;
|
|
}
|
|
if (!m_visit->finished()) {
|
|
return false;
|
|
}
|
|
if (m_dataRequests.size() == 0 && m_selectionSource.isAccepted()) {
|
|
// need to wait for first data request
|
|
return false;
|
|
}
|
|
const bool transfersFinished = std::all_of(m_dataRequests.begin(), m_dataRequests.end(),
|
|
[](QPair<xcb_timestamp_t, bool> req) {
|
|
return req.second;
|
|
});
|
|
if (transfersFinished) {
|
|
m_visit->sendFinished();
|
|
Q_EMIT finish(this);
|
|
}
|
|
return transfersFinished;
|
|
}
|
|
|
|
WlVisit::WlVisit(Window *target, XToWlDrag *drag)
|
|
: QObject(drag)
|
|
, m_target(target)
|
|
, m_drag(drag)
|
|
{
|
|
xcb_connection_t *xcbConn = kwinApp()->x11Connection();
|
|
|
|
m_window = xcb_generate_id(xcbConn);
|
|
DataBridge::self()->dnd()->overwriteRequestorWindow(m_window);
|
|
|
|
const uint32_t dndValues[] = {XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE};
|
|
xcb_create_window(xcbConn,
|
|
XCB_COPY_FROM_PARENT,
|
|
m_window,
|
|
kwinApp()->x11RootWindow(),
|
|
0, 0,
|
|
8192, 8192, // TODO: get current screen size and connect to changes
|
|
0,
|
|
XCB_WINDOW_CLASS_INPUT_OUTPUT,
|
|
XCB_COPY_FROM_PARENT,
|
|
XCB_CW_EVENT_MASK,
|
|
dndValues);
|
|
|
|
uint32_t version = Dnd::version();
|
|
xcb_change_property(xcbConn,
|
|
XCB_PROP_MODE_REPLACE,
|
|
m_window,
|
|
atoms->xdnd_aware,
|
|
XCB_ATOM_ATOM,
|
|
32, 1, &version);
|
|
|
|
xcb_map_window(xcbConn, m_window);
|
|
workspace()->addManualOverlay(m_window);
|
|
workspace()->updateStackingOrder(true);
|
|
|
|
xcb_flush(xcbConn);
|
|
m_mapped = true;
|
|
}
|
|
|
|
WlVisit::~WlVisit()
|
|
{
|
|
xcb_connection_t *xcbConn = kwinApp()->x11Connection();
|
|
xcb_destroy_window(xcbConn, m_window);
|
|
xcb_flush(xcbConn);
|
|
}
|
|
|
|
bool WlVisit::leave()
|
|
{
|
|
DataBridge::self()->dnd()->overwriteRequestorWindow(XCB_WINDOW_NONE);
|
|
unmapProxyWindow();
|
|
return m_finished;
|
|
}
|
|
|
|
bool WlVisit::handleClientMessage(xcb_client_message_event_t *event)
|
|
{
|
|
if (event->window != m_window) {
|
|
// different window
|
|
return false;
|
|
}
|
|
|
|
if (event->type == atoms->xdnd_enter) {
|
|
return handleEnter(event);
|
|
} else if (event->type == atoms->xdnd_position) {
|
|
return handlePosition(event);
|
|
} else if (event->type == atoms->xdnd_drop) {
|
|
return handleDrop(event);
|
|
} else if (event->type == atoms->xdnd_leave) {
|
|
return handleLeave(event);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool hasMimeName(const Mimes &mimes, const QString &name)
|
|
{
|
|
return std::any_of(mimes.begin(), mimes.end(),
|
|
[name](const Mime &m) {
|
|
return m.first == name;
|
|
});
|
|
}
|
|
|
|
bool WlVisit::handleEnter(xcb_client_message_event_t *event)
|
|
{
|
|
if (m_entered) {
|
|
// a drag already entered
|
|
return true;
|
|
}
|
|
m_entered = true;
|
|
|
|
xcb_client_message_data_t *data = &event->data;
|
|
m_srcWindow = data->data32[0];
|
|
m_version = data->data32[1] >> 24;
|
|
|
|
// get types
|
|
Mimes offers;
|
|
if (!(data->data32[1] & 1)) {
|
|
// message has only max 3 types (which are directly in data)
|
|
for (size_t i = 0; i < 3; i++) {
|
|
xcb_atom_t mimeAtom = data->data32[2 + i];
|
|
const auto mimeStrings = atomToMimeTypes(mimeAtom);
|
|
for (const auto &mime : mimeStrings) {
|
|
if (!hasMimeName(offers, mime)) {
|
|
offers << Mime(mime, mimeAtom);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// more than 3 types -> in window property
|
|
getMimesFromWinProperty(offers);
|
|
}
|
|
|
|
Q_EMIT offersReceived(offers);
|
|
return true;
|
|
}
|
|
|
|
void WlVisit::getMimesFromWinProperty(Mimes &offers)
|
|
{
|
|
xcb_connection_t *xcbConn = kwinApp()->x11Connection();
|
|
auto cookie = xcb_get_property(xcbConn,
|
|
0,
|
|
m_srcWindow,
|
|
atoms->xdnd_type_list,
|
|
XCB_GET_PROPERTY_TYPE_ANY,
|
|
0, 0x1fffffff);
|
|
|
|
auto *reply = xcb_get_property_reply(xcbConn, cookie, nullptr);
|
|
if (reply == nullptr) {
|
|
return;
|
|
}
|
|
if (reply->type != XCB_ATOM_ATOM || reply->value_len == 0) {
|
|
// invalid reply value
|
|
free(reply);
|
|
return;
|
|
}
|
|
|
|
xcb_atom_t *mimeAtoms = static_cast<xcb_atom_t *>(xcb_get_property_value(reply));
|
|
for (size_t i = 0; i < reply->value_len; ++i) {
|
|
const auto mimeStrings = atomToMimeTypes(mimeAtoms[i]);
|
|
for (const auto &mime : mimeStrings) {
|
|
if (!hasMimeName(offers, mime)) {
|
|
offers << Mime(mime, mimeAtoms[i]);
|
|
}
|
|
}
|
|
}
|
|
free(reply);
|
|
}
|
|
|
|
bool WlVisit::handlePosition(xcb_client_message_event_t *event)
|
|
{
|
|
xcb_client_message_data_t *data = &event->data;
|
|
m_srcWindow = data->data32[0];
|
|
|
|
if (!m_target) {
|
|
// not over Wl window at the moment
|
|
m_action = DnDAction::None;
|
|
m_actionAtom = XCB_ATOM_NONE;
|
|
sendStatus();
|
|
return true;
|
|
}
|
|
const uint32_t pos = data->data32[2];
|
|
Q_UNUSED(pos);
|
|
|
|
const xcb_timestamp_t timestamp = data->data32[3];
|
|
m_drag->x11Source()->setTimestamp(timestamp);
|
|
|
|
xcb_atom_t actionAtom = m_version > 1 ? data->data32[4] : atoms->xdnd_action_copy;
|
|
auto action = Dnd::atomToClientAction(actionAtom);
|
|
|
|
if (action == DnDAction::None) {
|
|
// copy action is always possible in XDND
|
|
action = DnDAction::Copy;
|
|
actionAtom = atoms->xdnd_action_copy;
|
|
}
|
|
|
|
if (m_action != action) {
|
|
m_action = action;
|
|
m_actionAtom = actionAtom;
|
|
m_drag->setDragAndDropAction(m_action);
|
|
}
|
|
|
|
sendStatus();
|
|
return true;
|
|
}
|
|
|
|
bool WlVisit::handleDrop(xcb_client_message_event_t *event)
|
|
{
|
|
m_dropHandled = true;
|
|
|
|
xcb_client_message_data_t *data = &event->data;
|
|
m_srcWindow = data->data32[0];
|
|
const xcb_timestamp_t timestamp = data->data32[2];
|
|
m_drag->x11Source()->setTimestamp(timestamp);
|
|
|
|
// we do nothing more here, the drop is being processed
|
|
// through the X11Source object
|
|
doFinish();
|
|
return true;
|
|
}
|
|
|
|
void WlVisit::doFinish()
|
|
{
|
|
m_finished = true;
|
|
unmapProxyWindow();
|
|
Q_EMIT finish(this);
|
|
}
|
|
|
|
bool WlVisit::handleLeave(xcb_client_message_event_t *event)
|
|
{
|
|
m_entered = false;
|
|
xcb_client_message_data_t *data = &event->data;
|
|
m_srcWindow = data->data32[0];
|
|
doFinish();
|
|
return true;
|
|
}
|
|
|
|
void WlVisit::sendStatus()
|
|
{
|
|
// receive position events
|
|
uint32_t flags = 1 << 1;
|
|
if (targetAcceptsAction()) {
|
|
// accept the drop
|
|
flags |= (1 << 0);
|
|
}
|
|
xcb_client_message_data_t data = {};
|
|
data.data32[0] = m_window;
|
|
data.data32[1] = flags;
|
|
data.data32[4] = flags & (1 << 0) ? m_actionAtom : static_cast<uint32_t>(XCB_ATOM_NONE);
|
|
Drag::sendClientMessage(m_srcWindow, atoms->xdnd_status, &data);
|
|
}
|
|
|
|
void WlVisit::sendFinished()
|
|
{
|
|
const bool accepted = m_entered && m_action != DnDAction::None;
|
|
xcb_client_message_data_t data = {};
|
|
data.data32[0] = m_window;
|
|
data.data32[1] = accepted;
|
|
data.data32[2] = accepted ? m_actionAtom : static_cast<uint32_t>(XCB_ATOM_NONE);
|
|
Drag::sendClientMessage(m_srcWindow, atoms->xdnd_finished, &data);
|
|
}
|
|
|
|
bool WlVisit::targetAcceptsAction() const
|
|
{
|
|
if (m_action == DnDAction::None) {
|
|
return false;
|
|
}
|
|
const auto selAction = m_drag->selectedDragAndDropAction();
|
|
return selAction == m_action || selAction == DnDAction::Copy;
|
|
}
|
|
|
|
void WlVisit::unmapProxyWindow()
|
|
{
|
|
if (!m_mapped) {
|
|
return;
|
|
}
|
|
xcb_connection_t *xcbConn = kwinApp()->x11Connection();
|
|
xcb_unmap_window(xcbConn, m_window);
|
|
workspace()->removeManualOverlay(m_window);
|
|
workspace()->updateStackingOrder(true);
|
|
xcb_flush(xcbConn);
|
|
m_mapped = false;
|
|
}
|
|
|
|
} // namespace Xwl
|
|
} // namespace KWin
|