/******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright 2019 Roman Gilg 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 . *********************************************************************/ #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 #include #include #include #include #include #include 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_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 &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(m_source->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(), &KWaylandServer::DataDeviceManagerInterface::dataSourceCreated, this, [this, dc](KWaylandServer::DataSourceInterface *dsi) { Q_ASSERT(dsi); if (dsi->client() != waylandServer()->internalConnection()->client()) { return; } QObject::disconnect(*dc); delete dc; connect(dsi, &KWaylandServer::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, 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 (AbstractClient *ac = qobject_cast(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(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_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; } // 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; 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 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) { 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, 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() { 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_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 = 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 *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 = {0}; data.data32[0] = m_window; data.data32[1] = flags; data.data32[4] = flags & (1 << 0) ? m_actionAtom : static_cast(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(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