kwin/xwl/drag_x.cpp

550 lines
17 KiB
C++
Raw Normal View History

[xwl] Drag and drop between Xwayland and Wayland native clients Summary: Building upon the generic X Selection support this patch establishes another selection class representing the XDND selection and provides interfaces to communicate drags originating from Xwayland windows to the Wayland server KWin and drags originating from Wayland native drags to Xwayland. For Wayland native drags KWin will claim the XDND selection as owner and will simply translate all relevant events to the XDND protocol and receive alike messages by X clients. When an X client claims the XDND selection KWin is notified via the X protocol and it decides if it allows the X drag to transcend into the Wayland protocol. If this is the case the mouse position is tracked and on entering a Wayland native window a proxy X Window is mapped to the top of the window stack. This proxy window acts as a drag destination for the drag origin window and again X messages will be translated into respective Wayland protocol calls. If the cursor leaves the Wayland window geometry before a drop is registered, the proxy window is unmapped, what triggers a subsequent drag leave event. In both directions the necessary core integration is minimal. There is a single call to be done in the drag and drop event filter through the Xwayland interface class. From my tests this patch facilitates drags between any Qt/KDE apps. What needs extra care are the browsers, which use target formats, that are not directly compatible with the Wayland protocol's MIME representation. For Chromium an additional integration step must be done in order to provide it with a net window stack containing the proxy window. Test Plan: Manually. Auto tests planned. Reviewers: #kwin Subscribers: zzag, kwin, alexde Tags: #kwin Maniphest Tasks: T4611 Differential Revision: https://phabricator.kde.org/D15627
2018-08-22 12:56:48 +00:00
/********************************************************************
KWin - the KDE window manager
This file is part of the KDE project.
Copyright 2019 Roman Gilg <subdiff@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#include "drag_x.h"
#include "databridge.h"
#include "dnd.h"
#include "selection_source.h"
#include "xwayland.h"
#include "abstract_client.h"
#include "atoms.h"
#include "wayland_server.h"
#include "workspace.h"
#include <KWayland/Client/datadevice.h>
#include <KWayland/Client/datasource.h>
#include <KWayland/Server/datasource_interface.h>
#include <KWayland/Server/seat_interface.h>
#include <KWayland/Server/surface_interface.h>
#include <QMouseEvent>
#include <QTimer>
namespace KWin {
namespace Xwl {
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;
}
[xwl] Drag and drop between Xwayland and Wayland native clients Summary: Building upon the generic X Selection support this patch establishes another selection class representing the XDND selection and provides interfaces to communicate drags originating from Xwayland windows to the Wayland server KWin and drags originating from Wayland native drags to Xwayland. For Wayland native drags KWin will claim the XDND selection as owner and will simply translate all relevant events to the XDND protocol and receive alike messages by X clients. When an X client claims the XDND selection KWin is notified via the X protocol and it decides if it allows the X drag to transcend into the Wayland protocol. If this is the case the mouse position is tracked and on entering a Wayland native window a proxy X Window is mapped to the top of the window stack. This proxy window acts as a drag destination for the drag origin window and again X messages will be translated into respective Wayland protocol calls. If the cursor leaves the Wayland window geometry before a drop is registered, the proxy window is unmapped, what triggers a subsequent drag leave event. In both directions the necessary core integration is minimal. There is a single call to be done in the drag and drop event filter through the Xwayland interface class. From my tests this patch facilitates drags between any Qt/KDE apps. What needs extra care are the browsers, which use target formats, that are not directly compatible with the Wayland protocol's MIME representation. For Chromium an additional integration step must be done in order to provide it with a net window stack containing the proxy window. Test Plan: Manually. Auto tests planned. Reviewers: #kwin Subscribers: zzag, kwin, alexde Tags: #kwin Maniphest Tasks: T4611 Differential Revision: https://phabricator.kde.org/D15627
2018-08-22 12:56:48 +00:00
XToWlDrag::XToWlDrag(X11Source *source)
: m_src(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(), [this, eventTime](QPair<xcb_timestamp_t, bool> req) {
return req.first == eventTime;
});
if (it == m_dataRequests.end()) {
// transfer finished for a different drag
return;
}
Q_ASSERT(!(*it).second);
(*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_src->timestamp(), false);
});
auto *ddm = waylandServer()->internalDataDeviceManager();
m_dataSource = ddm->createDataSource(this);
connect(m_dataSource, &KWayland::Client::DataSource::dragAndDropPerformed, 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);
}
});
}
checkForFinished();
});
connect(m_dataSource, &KWayland::Client::DataSource::dragAndDropFinished, this, [this] {
// this call is not reliably initiated by Wayland clients
checkForFinished();
});
// source does _not_ take ownership of m_dataSource
source->setDataSource(m_dataSource);
auto *dc = new QMetaObject::Connection();
*dc = connect(waylandServer()->dataDeviceManager(), &KWayland::Server::DataDeviceManagerInterface::dataSourceCreated, this,
[this, dc](KWayland::Server::DataSourceInterface *dsi) {
Q_ASSERT(dsi);
if (dsi->client() != waylandServer()->internalConnection()) {
return;
}
QObject::disconnect(*dc);
delete dc;
connect(dsi, &KWayland::Server::DataSourceInterface::mimeTypeOffered, this, &XToWlDrag::offerCallback);
}
);
// Start drag with serial of last left pointer button press.
// This means X to Wl drags can only be executed with the left pointer button being pressed.
// For touch and (maybe) other pointer button drags we have to revisit this.
//
// Until then we accept the restriction for Xwayland clients.
DataBridge::self()->dataDevice()->startDrag(waylandServer()->seat()->pointerButtonSerial(Qt::LeftButton),
m_dataSource,
DataBridge::self()->dnd()->surface());
waylandServer()->dispatch();
}
XToWlDrag::~XToWlDrag()
{
delete m_dataSource;
m_dataSource = nullptr;
}
DragEventReply XToWlDrag::moveFilter(Toplevel *target, 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 (AbstractClient *ac = qobject_cast<AbstractClient*>(target)) {
if (workspace()->activeClient() != ac) {
workspace()->activateClient(ac);
}
}
if (hasCurrent) {
// last received enter event is now void,
// wait for the next one
seat->setDragTarget(nullptr);
}
return DragEventReply::Ignore;
}
// new Wl native target
auto *ac = static_cast<AbstractClient*>(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 : 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_dataSource->setDragAndDropActions(action);
}
DnDAction XToWlDrag::selectedDragAndDropAction()
{
// Take the last received action only from before the drag was performed,
// because the action gets reset as soon as the drag is performed
// (this seems to be a bug in KWayland -> TODO).
if (!m_performed) {
m_lastSelectedDragAndDropAction = m_dataSource->selectedDragAndDropAction();
}
return m_lastSelectedDragAndDropAction;
}
void XToWlDrag::setOffers(const Mimes &offers)
{
m_src->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;
}
// TODO: make sure that offers are not changed in between visits
m_offersPending = m_offers = offers;
for (const auto mimePair : offers) {
m_dataSource->offer(mimePair.first);
}
}
using Mime = QPair<QString, xcb_atom_t>;
void XToWlDrag::offerCallback(const QString &mime)
{
m_offersPending.erase(std::remove_if(m_offersPending.begin(), m_offersPending.end(),
[mime](const Mime &m) { return m.first == mime; }));
if (m_offersPending.isEmpty() && m_visit && m_visit->entered()) {
setDragTarget();
}
}
void XToWlDrag::setDragTarget()
{
auto *ac = m_visit->target();
workspace()->activateClient(ac);
waylandServer()->seat()->setDragTarget(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) {
// 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(AbstractClient *target, XToWlDrag *drag)
: QObject(drag),
m_target(target),
m_drag(drag)
{
auto *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,
Xwayland::self()->xcbScreen()->root_visual,
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);
const uint32_t stackValues[] = { XCB_STACK_MODE_ABOVE };
xcb_configure_window (xcbConn,
m_window,
XCB_CONFIG_WINDOW_STACK_MODE,
stackValues);
xcb_flush(xcbConn);
m_mapped = true;
}
WlVisit::~WlVisit()
{
auto *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 *ev)
{
if (m_entered) {
// a drag already entered
return true;
}
m_entered = true;
xcb_client_message_data_t *data = &ev->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);
[xwl] Drag and drop between Xwayland and Wayland native clients Summary: Building upon the generic X Selection support this patch establishes another selection class representing the XDND selection and provides interfaces to communicate drags originating from Xwayland windows to the Wayland server KWin and drags originating from Wayland native drags to Xwayland. For Wayland native drags KWin will claim the XDND selection as owner and will simply translate all relevant events to the XDND protocol and receive alike messages by X clients. When an X client claims the XDND selection KWin is notified via the X protocol and it decides if it allows the X drag to transcend into the Wayland protocol. If this is the case the mouse position is tracked and on entering a Wayland native window a proxy X Window is mapped to the top of the window stack. This proxy window acts as a drag destination for the drag origin window and again X messages will be translated into respective Wayland protocol calls. If the cursor leaves the Wayland window geometry before a drop is registered, the proxy window is unmapped, what triggers a subsequent drag leave event. In both directions the necessary core integration is minimal. There is a single call to be done in the drag and drop event filter through the Xwayland interface class. From my tests this patch facilitates drags between any Qt/KDE apps. What needs extra care are the browsers, which use target formats, that are not directly compatible with the Wayland protocol's MIME representation. For Chromium an additional integration step must be done in order to provide it with a net window stack containing the proxy window. Test Plan: Manually. Auto tests planned. Reviewers: #kwin Subscribers: zzag, kwin, alexde Tags: #kwin Maniphest Tasks: T4611 Differential Revision: https://phabricator.kde.org/D15627
2018-08-22 12:56:48 +00:00
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)
{
auto *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, NULL);
if (reply == NULL) {
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]);
[xwl] Drag and drop between Xwayland and Wayland native clients Summary: Building upon the generic X Selection support this patch establishes another selection class representing the XDND selection and provides interfaces to communicate drags originating from Xwayland windows to the Wayland server KWin and drags originating from Wayland native drags to Xwayland. For Wayland native drags KWin will claim the XDND selection as owner and will simply translate all relevant events to the XDND protocol and receive alike messages by X clients. When an X client claims the XDND selection KWin is notified via the X protocol and it decides if it allows the X drag to transcend into the Wayland protocol. If this is the case the mouse position is tracked and on entering a Wayland native window a proxy X Window is mapped to the top of the window stack. This proxy window acts as a drag destination for the drag origin window and again X messages will be translated into respective Wayland protocol calls. If the cursor leaves the Wayland window geometry before a drop is registered, the proxy window is unmapped, what triggers a subsequent drag leave event. In both directions the necessary core integration is minimal. There is a single call to be done in the drag and drop event filter through the Xwayland interface class. From my tests this patch facilitates drags between any Qt/KDE apps. What needs extra care are the browsers, which use target formats, that are not directly compatible with the Wayland protocol's MIME representation. For Chromium an additional integration step must be done in order to provide it with a net window stack containing the proxy window. Test Plan: Manually. Auto tests planned. Reviewers: #kwin Subscribers: zzag, kwin, alexde Tags: #kwin Maniphest Tasks: T4611 Differential Revision: https://phabricator.kde.org/D15627
2018-08-22 12:56:48 +00:00
for (const auto mime : mimeStrings ) {
if (!hasMimeName(offers, mime)) {
offers << Mime(mime, mimeAtoms[i]);
}
}
}
free(reply);
}
bool WlVisit::handlePosition(xcb_client_message_event_t *ev)
{
xcb_client_message_data_t *data = &ev->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 = Drag::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 *ev)
{
m_dropHandled = true;
xcb_client_message_data_t *data = &ev->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 *ev)
{
m_entered = false;
xcb_client_message_data_t *data = &ev->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 = {0};
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 = {0};
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;
}
auto *xcbConn = kwinApp()->x11Connection();
xcb_unmap_window(xcbConn, m_window);
xcb_flush(xcbConn);
m_mapped = false;
}
}
}