4c18d156e2
Summary: Some X clients acting as drag sources might try to optimize finding the current target window by checking if a window manager, that sets the root window _NET_CLIENT_LIST_STACKING property, is present. An example for this is Chromium and since KWin sets the property the drag proxy windows must be added to this list. Otherwise the origin client will not detect the proxy window and not send an XdndEnter message. Test Plan: Manually with Chromium. Reviewers: #kwin Subscribers: zzag, kwin Tags: #kwin Differential Revision: https://phabricator.kde.org/D15628
548 lines
17 KiB
C++
548 lines
17 KiB
C++
/********************************************************************
|
|
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;
|
|
}
|
|
|
|
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);
|
|
workspace()->addManualOverlay(m_window);
|
|
workspace()->updateStackingOrder(true);
|
|
|
|
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);
|
|
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]);
|
|
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);
|
|
workspace()->removeManualOverlay(m_window);
|
|
workspace()->updateStackingOrder(true);
|
|
xcb_flush(xcbConn);
|
|
m_mapped = false;
|
|
}
|
|
|
|
}
|
|
}
|