diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -659,6 +659,10 @@ ${CMAKE_CURRENT_SOURCE_DIR}/xwl/selection_source.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/transfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/clipboard.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/dnd.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/drag.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/drag_wl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/drag_x.cpp ) include(ECMQtDeclareLoggingCategory) ecm_qt_declare_logging_category(kwin_XWAYLAND_SRCS diff --git a/atoms.h b/atoms.h --- a/atoms.h +++ b/atoms.h @@ -52,8 +52,18 @@ Xcb::Atom kde_net_wm_user_creation_time; Xcb::Atom net_wm_take_activity; Xcb::Atom net_wm_window_opacity; + Xcb::Atom xdnd_selection; Xcb::Atom xdnd_aware; + Xcb::Atom xdnd_enter; + Xcb::Atom xdnd_type_list; Xcb::Atom xdnd_position; + Xcb::Atom xdnd_status; + Xcb::Atom xdnd_action_copy; + Xcb::Atom xdnd_action_move; + Xcb::Atom xdnd_action_ask; + Xcb::Atom xdnd_drop; + Xcb::Atom xdnd_leave; + Xcb::Atom xdnd_finished; Xcb::Atom net_frame_extents; Xcb::Atom kde_net_wm_frame_strut; Xcb::Atom net_wm_sync_request_counter; diff --git a/atoms.cpp b/atoms.cpp --- a/atoms.cpp +++ b/atoms.cpp @@ -43,8 +43,18 @@ , kde_net_wm_user_creation_time(QByteArrayLiteral("_KDE_NET_WM_USER_CREATION_TIME")) , net_wm_take_activity(QByteArrayLiteral("_NET_WM_TAKE_ACTIVITY")) , net_wm_window_opacity(QByteArrayLiteral("_NET_WM_WINDOW_OPACITY")) + , xdnd_selection(QByteArrayLiteral("XdndSelection")) , xdnd_aware(QByteArrayLiteral("XdndAware")) + , xdnd_enter(QByteArrayLiteral("XdndEnter")) + , xdnd_type_list(QByteArrayLiteral("XdndTypeList")) , xdnd_position(QByteArrayLiteral("XdndPosition")) + , xdnd_status(QByteArrayLiteral("XdndStatus")) + , xdnd_action_copy(QByteArrayLiteral("XdndActionCopy")) + , xdnd_action_move(QByteArrayLiteral("XdndActionMove")) + , xdnd_action_ask(QByteArrayLiteral("XdndActionAsk")) + , xdnd_drop(QByteArrayLiteral("XdndDrop")) + , xdnd_leave(QByteArrayLiteral("XdndLeave")) + , xdnd_finished(QByteArrayLiteral("XdndFinished")) , net_frame_extents(QByteArrayLiteral("_NET_FRAME_EXTENTS")) , kde_net_wm_frame_strut(QByteArrayLiteral("_KDE_NET_WM_FRAME_STRUT")) , net_wm_sync_request_counter(QByteArrayLiteral("_NET_WM_SYNC_REQUEST_COUNTER")) diff --git a/input.h b/input.h --- a/input.h +++ b/input.h @@ -165,6 +165,7 @@ void uninstallInputEventSpy(InputEventSpy *spy); Toplevel *findToplevel(const QPoint &pos); + Toplevel *findManagedToplevel(const QPoint &pos); GlobalShortcutsManager *shortcuts() const { return m_shortcuts; } diff --git a/input.cpp b/input.cpp --- a/input.cpp +++ b/input.cpp @@ -44,6 +44,7 @@ #include "popup_input_filter.h" #include "shell_client.h" #include "wayland_server.h" +#include "xwl/xwayland_interface.h" #include #include #include @@ -1473,7 +1474,20 @@ case QEvent::MouseMove: { const auto pos = input()->globalPointer(); seat->setPointerPos(pos); - if (Toplevel *t = input()->pointer()->at()) { + + const auto eventPos = event->globalPos(); + // TODO: use InputDeviceHandler::at() here and check isClient()? + Toplevel *t = input()->findManagedToplevel(eventPos); + if (auto *xwl = xwayland()) { + const auto ret = xwl->dragMoveFilter(t, eventPos); + if (ret == Xwl::DragEventReply::Ignore) { + return false; + } else if (ret == Xwl::DragEventReply::Take) { + break; + } + } + + if (t) { // TODO: consider decorations if (t->surface() != seat->dragSurface()) { if (AbstractClient *c = qobject_cast(t)) { @@ -2082,6 +2096,15 @@ } } } + return findManagedToplevel(pos); +} + +Toplevel *InputRedirection::findManagedToplevel(const QPoint &pos) +{ + if (!Workspace::self()) { + return nullptr; + } + const bool isScreenLocked = waylandServer() && waylandServer()->isScreenLocked(); const ToplevelList &stacking = Workspace::self()->stackingOrder(); if (stacking.isEmpty()) { return NULL; diff --git a/wayland_server.h b/wayland_server.h --- a/wayland_server.h +++ b/wayland_server.h @@ -34,6 +34,7 @@ { class ConnectionThread; class Registry; +class Compositor; class Seat; class DataDeviceManager; class ShmPool; @@ -178,6 +179,9 @@ KWayland::Server::ClientConnection *screenLockerClientConnection() const { return m_screenLockerClientConnection; } + KWayland::Client::Compositor *internalCompositor() { + return m_internalConnection.compositor; + } KWayland::Client::Seat *internalSeat() { return m_internalConnection.seat; } @@ -263,6 +267,7 @@ KWayland::Client::ConnectionThread *client = nullptr; QThread *clientThread = nullptr; KWayland::Client::Registry *registry = nullptr; + KWayland::Client::Compositor *compositor = nullptr; KWayland::Client::Seat *seat = nullptr; KWayland::Client::DataDeviceManager *ddm = nullptr; KWayland::Client::ShmPool *shm = nullptr; diff --git a/wayland_server.cpp b/wayland_server.cpp --- a/wayland_server.cpp +++ b/wayland_server.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -113,6 +114,7 @@ } delete m_internalConnection.registry; + delete m_internalConnection.compositor; delete m_internalConnection.seat; delete m_internalConnection.ddm; delete m_internalConnection.shm; @@ -567,6 +569,10 @@ [this, registry] { m_internalConnection.interfacesAnnounced = true; + const auto compInterface = registry->interface(Registry::Interface::Compositor); + if (compInterface.name != 0) { + m_internalConnection.compositor = registry->createCompositor(compInterface.name, compInterface.version, this); + } const auto seatInterface = registry->interface(Registry::Interface::Seat); if (seatInterface.name != 0) { m_internalConnection.seat = registry->createSeat(seatInterface.name, seatInterface.version, this); diff --git a/xwl/clipboard.cpp b/xwl/clipboard.cpp --- a/xwl/clipboard.cpp +++ b/xwl/clipboard.cpp @@ -26,7 +26,7 @@ #include "wayland_server.h" #include "workspace.h" -#include "abstract_client.h" +#include "client.h" #include #include @@ -135,6 +135,14 @@ void Clipboard::doHandleXfixesNotify(xcb_xfixes_selection_notify_event_t *event) { + createX11Source(NULL); + + const auto *ac = workspace()->activeClient(); + if (!qobject_cast(ac)) { + // clipboard is only allowed to be acquired when Xwayland has focus + // TODO: can we make this stronger (window id comparision)? + return; + } createX11Source(event); auto *xSrc = x11Source(); if (xSrc) { diff --git a/xwl/databridge.h b/xwl/databridge.h --- a/xwl/databridge.h +++ b/xwl/databridge.h @@ -21,6 +21,7 @@ #define KWIN_XWL_DATABRIDGE #include +#include #include @@ -38,11 +39,14 @@ namespace KWin { +class Toplevel; namespace Xwl { class Xwayland; class Clipboard; +class Dnd; +enum class DragEventReply; /* * Interface class for all data sharing in the context of X selections @@ -60,20 +64,28 @@ ~DataBridge(); bool filterEvent(xcb_generic_event_t *event); + DragEventReply dragMoveFilter(Toplevel *target, QPoint pos); - KWayland::Client::DataDevice *dataDevice() const { + KWayland::Client::DataDevice *dataDevice() const + { return m_dd; } - KWayland::Server::DataDeviceInterface *dataDeviceIface() const { + KWayland::Server::DataDeviceInterface *dataDeviceIface() const + { return m_ddi; } + Dnd* dnd() const + { + return m_dnd; + } private: void init(); bool handleXfixesNotify(xcb_xfixes_selection_notify_event_t *event); Clipboard *m_clipboard = nullptr; + Dnd *m_dnd = nullptr; /* Internal data device interface */ KWayland::Client::DataDevice *m_dd = nullptr; diff --git a/xwl/databridge.cpp b/xwl/databridge.cpp --- a/xwl/databridge.cpp +++ b/xwl/databridge.cpp @@ -21,6 +21,7 @@ #include "xwayland.h" #include "selection.h" #include "clipboard.h" +#include "dnd.h" #include "atoms.h" #include "wayland_server.h" @@ -76,14 +77,18 @@ void DataBridge::init() { m_clipboard = new Clipboard(atoms->clipboard, this); + m_dnd = new Dnd(atoms->xdnd_selection, this); waylandServer()->dispatch(); } bool DataBridge::filterEvent(xcb_generic_event_t *event) { if (m_clipboard->filterEvent(event)) { return true; } + if (m_dnd->filterEvent(event)) { + return true; + } if (event->response_type - Xwayland::self()->xfixes()->first_event == XCB_XFIXES_SELECTION_NOTIFY) { return handleXfixesNotify((xcb_xfixes_selection_notify_event_t *)event); } @@ -96,11 +101,22 @@ if (atom == atoms->clipboard) { return m_clipboard; } + if (atom == atoms->xdnd_selection) { + return m_dnd; + } return nullptr; }; auto *sel = getSelection(event->selection); return sel && sel->handleXfixesNotify(event); } +DragEventReply DataBridge::dragMoveFilter(Toplevel *target, QPoint pos) +{ + if (!m_dnd) { + return DragEventReply::Wayland; + } + return m_dnd->dragMoveFilter(target, pos); +} + } } diff --git a/xwl/dnd.h b/xwl/dnd.h new file mode 100644 --- /dev/null +++ b/xwl/dnd.h @@ -0,0 +1,90 @@ +/******************************************************************** + 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 . +*********************************************************************/ +#ifndef KWIN_XWL_DND +#define KWIN_XWL_DND + +#include "selection.h" + +#include + +namespace KWayland +{ +namespace Client +{ +class Surface; +} +namespace Server +{ +class SurfaceInterface; +} +} + +namespace KWin +{ +class Toplevel; + +namespace Xwl +{ +class Drag; +enum class DragEventReply; + +/** + * Represents the drag and drop mechanism, on X side this is the XDND protocol. + * For more information on XDND see: http://johnlindal.wixsite.com/xdnd + */ +class Dnd : public Selection +{ + Q_OBJECT +public: + explicit Dnd(xcb_atom_t atom, QObject *parent); + + static uint32_t version(); + + void doHandleXfixesNotify(xcb_xfixes_selection_notify_event_t *event) override; + void x11OffersChanged(const QVector &added, const QVector &removed) override; + bool handleClientMessage(xcb_client_message_event_t *event) override; + + DragEventReply dragMoveFilter(Toplevel *target, QPoint pos); + + KWayland::Server::SurfaceInterface *surfaceIface() const { + return m_surfaceIface; + } + KWayland::Client::Surface *surface() const { + return m_surface; + } + +private: + // start and end Wl native client drags (Wl -> Xwl) + void startDrag(); + void endDrag(); + void clearOldDrag(Drag *drag); + + // active drag or null when no drag active + Drag *m_currentDrag = nullptr; + QVector m_oldDrags; + + KWayland::Client::Surface *m_surface; + KWayland::Server::SurfaceInterface *m_surfaceIface = nullptr; +}; + +} +} + +#endif diff --git a/xwl/dnd.cpp b/xwl/dnd.cpp new file mode 100644 --- /dev/null +++ b/xwl/dnd.cpp @@ -0,0 +1,227 @@ +/******************************************************************** + 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 "dnd.h" + +#include "databridge.h" +#include "selection_source.h" +#include "drag_wl.h" +#include "drag_x.h" + +#include "atoms.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xwayland.h" +#include "abstract_client.h" + +#include +#include + +#include +#include + +#include + +#include + +namespace KWin +{ +namespace Xwl +{ + +// version of DnD support in X +const static uint32_t s_version = 5; +uint32_t Dnd::version() +{ + return s_version; +} + +Dnd::Dnd(xcb_atom_t atom, QObject *parent) + : Selection(atom, parent) +{ + auto *xcbConn = kwinApp()->x11Connection(); + + const uint32_t dndValues[] = { XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | + XCB_EVENT_MASK_PROPERTY_CHANGE }; + xcb_create_window(xcbConn, + XCB_COPY_FROM_PARENT, + 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); + registerXfixes(); + + xcb_change_property(xcbConn, + XCB_PROP_MODE_REPLACE, + window(), + atoms->xdnd_aware, + XCB_ATOM_ATOM, + 32, 1, &s_version); + xcb_flush(xcbConn); + + connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragStarted, this, &Dnd::startDrag); + connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragEnded, this, &Dnd::endDrag); + + const auto *comp = waylandServer()->compositor(); + m_surface = waylandServer()->internalCompositor()->createSurface(this); + m_surface->setInputRegion(nullptr); + m_surface->commit(KWayland::Client::Surface::CommitFlag::None); + auto *dc = new QMetaObject::Connection(); + *dc = connect(comp, &KWayland::Server::CompositorInterface::surfaceCreated, this, + [this, dc](KWayland::Server::SurfaceInterface *si) { + // TODO: how to make sure that it is the iface of m_surface? + if (m_surfaceIface || si->client() != waylandServer()->internalConnection()) { + return; + } + QObject::disconnect(*dc); + delete dc; + m_surfaceIface = si; + connect(workspace(), &Workspace::clientActivated, this, + [this](AbstractClient *ac) { + if (!ac || !ac->inherits("KWin::Client")) { + return; + } + auto *surface = ac->surface(); + if (surface) { + surface->setDataProxy(m_surfaceIface); + } else { + auto *dc = new QMetaObject::Connection(); + *dc = connect(ac, &AbstractClient::surfaceChanged, this, [this, ac, dc] { + if (auto *surface = ac->surface()) { + surface->setDataProxy(m_surfaceIface); + QObject::disconnect(*dc); + delete dc; + } + } + ); + } + }); + } + ); + waylandServer()->dispatch(); +} + +void Dnd::doHandleXfixesNotify(xcb_xfixes_selection_notify_event_t *event) +{ + if (qobject_cast(m_currentDrag)) { + // X drag is in progress, rogue X client took over the selection. + return; + } + if (m_currentDrag) { + // Wl drag is in progress - don't overwrite by rogue X client, + // get it back instead! + ownSelection(true); + return; + } + createX11Source(NULL); + const auto *seat = waylandServer()->seat(); + auto *originSurface = seat->focusedPointerSurface(); + if (!originSurface) { + return; + } + if (originSurface->client() != waylandServer()->xWaylandConnection()) { + // focused surface client is not Xwayland - do not allow drag to start + // TODO: can we make this stronger (window id comparision)? + return; + } + if (!seat->isPointerButtonPressed(Qt::LeftButton)) { + // we only allow drags to be started on (left) pointer button being + // pressed for now + return; + } + createX11Source(event); + auto *xSrc = x11Source(); + if (!xSrc) { + return; + } + DataBridge::self()->dataDeviceIface()->updateProxy(originSurface); + m_currentDrag = new XToWlDrag(xSrc); +} + +void Dnd::x11OffersChanged(const QVector &added, const QVector &removed) +{ + Q_UNUSED(added); + Q_UNUSED(removed); + // TODO: handled internally +} + +bool Dnd::handleClientMessage(xcb_client_message_event_t *event) +{ + for (auto *drag : m_oldDrags) { + if (drag->handleClientMessage(event)) { + return true; + } + } + if (m_currentDrag && m_currentDrag->handleClientMessage(event)) { + return true; + } + return false; +} + +DragEventReply Dnd::dragMoveFilter(Toplevel *target, QPoint pos) +{ + // this filter only is used when a drag is in process + Q_ASSERT(m_currentDrag); + return m_currentDrag->moveFilter(target, pos); +} + +void Dnd::startDrag() +{ + auto *ddi = waylandServer()->seat()->dragSource(); + if (ddi == DataBridge::self()->dataDeviceIface()) { + // X to Wl drag, started by us, is in progress + Q_ASSERT(m_currentDrag); + return; + } + // there can only ever be one Wl native drag at the same time + Q_ASSERT(!m_currentDrag); + + // new Wl to X drag, init drag and Wl source + m_currentDrag = new WlToXDrag(); + auto *wls = new WlSource(this, ddi); + wls->setDataSourceIface(ddi->dragSource()); + setWlSource(wls); + ownSelection(true); +} + +void Dnd::endDrag() +{ + Q_ASSERT(m_currentDrag); + if (m_currentDrag->end()) { + delete m_currentDrag; + } else { + connect(m_currentDrag, &Drag::finish, this, &Dnd::clearOldDrag); + m_oldDrags << m_currentDrag; + } + m_currentDrag = nullptr; +} + +void Dnd::clearOldDrag(Drag *drag) +{ + m_oldDrags.removeOne(drag); + delete drag; +} + +} +} diff --git a/xwl/xwayland_interface.h b/xwl/drag.h copy from xwl/xwayland_interface.h copy to xwl/drag.h --- a/xwl/xwayland_interface.h +++ b/xwl/drag.h @@ -17,31 +17,47 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ -#ifndef KWIN_XWL_XWAYLAND_INTERFACE -#define KWIN_XWL_XWAYLAND_INTERFACE +#ifndef KWIN_XWL_DRAG +#define KWIN_XWL_DRAG -#include +#include -#include +#include + +#include namespace KWin { +class Toplevel; + +namespace Xwl +{ +enum class DragEventReply; + +using DnDAction = KWayland::Client::DataDeviceManager::DnDAction; -class KWIN_EXPORT XwaylandInterface : public QObject +/** + * An ongoing drag operation. + */ +class Drag : public QObject { Q_OBJECT public: - static XwaylandInterface *self(); + static void sendClientMessage(xcb_window_t target, xcb_atom_t type, xcb_client_message_data_t *data); + static DnDAction atomToClientAction(xcb_atom_t atom); + static xcb_atom_t clientActionToAtom(DnDAction action); -protected: - explicit XwaylandInterface(QObject *parent = nullptr); - virtual ~XwaylandInterface(); + virtual ~Drag() = default; + virtual bool handleClientMessage(xcb_client_message_event_t *event) = 0; + virtual DragEventReply moveFilter(Toplevel *target, QPoint pos) = 0; + + virtual bool end() = 0; + +Q_SIGNALS: + void finish(Drag *self); }; -inline XwaylandInterface *xwayland() { - return XwaylandInterface::self(); } - } #endif diff --git a/xwl/drag.cpp b/xwl/drag.cpp new file mode 100644 --- /dev/null +++ b/xwl/drag.cpp @@ -0,0 +1,78 @@ +/******************************************************************** + 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.h" + +#include "atoms.h" + +namespace KWin +{ +namespace Xwl +{ + +void Drag::sendClientMessage(xcb_window_t target, xcb_atom_t type, xcb_client_message_data_t *data) +{ + xcb_client_message_event_t event { + XCB_CLIENT_MESSAGE, // response_type + 32, // format + 0, // sequence + target, // window + type, // type + *data, // data + }; + + auto *xcbConn = kwinApp()->x11Connection(); + xcb_send_event(xcbConn, + 0, + target, + XCB_EVENT_MASK_NO_EVENT, + reinterpret_cast(&event)); + xcb_flush(xcbConn); +} + +DnDAction Drag::atomToClientAction(xcb_atom_t atom) +{ + if (atom == atoms->xdnd_action_copy) { + return DnDAction::Copy; + } else if (atom == atoms->xdnd_action_move) { + return DnDAction::Move; + } else if (atom == atoms->xdnd_action_ask) { + // we currently do not support it - need some test client first + return DnDAction::None; +// return DnDAction::Ask; + } + return DnDAction::None; +} + +xcb_atom_t Drag::clientActionToAtom(DnDAction action) +{ + if (action == DnDAction::Copy) { + return atoms->xdnd_action_copy; + } else if (action == DnDAction::Move) { + return atoms->xdnd_action_move; + } else if (action == DnDAction::Ask) { + // we currently do not support it - need some test client first + return XCB_ATOM_NONE; +// return atoms->xdnd_action_ask; + } + return XCB_ATOM_NONE; +} + +} +} diff --git a/xwl/drag_wl.h b/xwl/drag_wl.h new file mode 100644 --- /dev/null +++ b/xwl/drag_wl.h @@ -0,0 +1,160 @@ +/******************************************************************** + 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 . +*********************************************************************/ +#ifndef KWIN_XWL_DRAG_WL +#define KWIN_XWL_DRAG_WL + +#include "drag.h" + +#include + +#include +#include +#include + +namespace KWayland +{ +namespace Client +{ +class Surface; +} +namespace Server +{ +class DataDeviceInterface; +class DataSourceInterface; +class SurfaceInterface; +} +} + +namespace KWin +{ +class Toplevel; +class AbstractClient; + +namespace Xwl +{ +class X11Source; +enum class DragEventReply; +class Xvisit; + +using DnDActions = KWayland::Client::DataDeviceManager::DnDActions; + +class WlToXDrag : public Drag +{ + Q_OBJECT +public: + explicit WlToXDrag(); + + DragEventReply moveFilter(Toplevel *target, QPoint pos) override; + bool handleClientMessage(xcb_client_message_event_t *event) override; + + bool end() override; + + KWayland::Server::DataSourceInterface *dataSourceIface() const { + return m_dsi; + } + +private: + KWayland::Server::DataSourceInterface *m_dsi; + Xvisit *m_visit = nullptr; +}; + +// visit to an X window +class Xvisit : public QObject +{ + Q_OBJECT +public: + // TODO: handle ask action + + Xvisit(WlToXDrag *drag, AbstractClient *target); + + bool handleClientMessage(xcb_client_message_event_t *event); + bool handleStatus(xcb_client_message_event_t *ev); + bool handleFinished(xcb_client_message_event_t *ev); + + void sendPosition(const QPointF &globalPos); + void leave(); + + bool finished() const { + return m_state.finished; + } + AbstractClient *target() const { + return m_target; + } + +Q_SIGNALS: + void finish(Xvisit *self); + +private: + void sendEnter(); + void sendDrop(uint32_t time); + void sendLeave(); + + void receiveOffer(); + void enter(); + + void retrieveSupportedActions(); + void determineProposedAction(); + void requestDragAndDropAction(); + void setProposedAction(); + + void drop(); + + void doFinish(); + void stopConnections(); + + WlToXDrag *m_drag; + AbstractClient *m_target; + uint32_t m_version = 0; + + QMetaObject::Connection m_enterCon; + QMetaObject::Connection m_motionCon; + QMetaObject::Connection m_actionCon; + QMetaObject::Connection m_dropCon; + + struct { + bool pending = false; + bool cached = false; + QPoint cache; + } m_pos; + + // Must be QPointer, because KWayland::Client::DataDevice + // might delete it. + QPointer m_dataOffer; + + // supported by the Wl source + DnDActions m_supportedActions = DnDAction::None; + // preferred by the X client + DnDAction m_preferredAction = DnDAction::None; + // decided upon by the compositor + DnDAction m_proposedAction = DnDAction::None; + + struct { + bool entered = false; + bool dropped = false; + bool finished = false; + } m_state; + + bool m_accepts = false; +}; + +} +} + +#endif diff --git a/xwl/drag_wl.cpp b/xwl/drag_wl.cpp new file mode 100644 --- /dev/null +++ b/xwl/drag_wl.cpp @@ -0,0 +1,446 @@ +/******************************************************************** + 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_wl.h" + +#include "xwayland.h" +#include "databridge.h" +#include "dnd.h" + +#include "atoms.h" +#include "wayland_server.h" +#include "workspace.h" +#include "client.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace KWin +{ +namespace Xwl +{ + +WlToXDrag::WlToXDrag() +{ + m_dsi = waylandServer()->seat()->dragSource()->dragSource(); +} + +DragEventReply WlToXDrag::moveFilter(Toplevel *target, QPoint pos) +{ + AbstractClient *ac = qobject_cast(target); + auto *seat = waylandServer()->seat(); + if (m_visit && m_visit->target() == ac) { + // no target change + return DragEventReply::Take; + } + // leave current target + if (m_visit) { + seat->setDragTarget(nullptr); + m_visit->leave(); + delete m_visit; + m_visit = nullptr; + } + if (!qobject_cast(ac)) { + // no target or wayland native target, + // handled by input code directly + return DragEventReply::Wayland; + } + // new target + workspace()->activateClient(ac, false); + seat->setDragTarget(DataBridge::self()->dnd()->surfaceIface(), pos, ac->inputTransformation()); + m_visit = new Xvisit(this, ac); + return DragEventReply::Take; +} + +bool WlToXDrag::handleClientMessage(xcb_client_message_event_t *event) +{ + if (m_visit && m_visit->handleClientMessage(event)) { + return true; + } + return false; +} + +bool WlToXDrag::end() +{ + if (!m_visit || m_visit->finished()) { + delete m_visit; + m_visit = nullptr; + return true; + } + connect(m_visit, &Xvisit::finish, this, [this](Xvisit *visit) { + Q_ASSERT(m_visit == visit); + delete visit; + m_visit = nullptr; + // we direclty allow to delete previous visits + Q_EMIT finish(this); + }); + return false; +} + +Xvisit::Xvisit(WlToXDrag *drag, AbstractClient *target) + : QObject(drag), + m_drag(drag), + m_target(target) +{ + // first check supported DND version + auto *xcbConn = kwinApp()->x11Connection(); + xcb_get_property_cookie_t cookie = xcb_get_property(xcbConn, + 0, + m_target->window(), + atoms->xdnd_aware, + XCB_GET_PROPERTY_TYPE_ANY, + 0, 1); + auto *reply = xcb_get_property_reply(xcbConn, cookie, NULL); + if (reply == NULL) { + doFinish(); + return; + } + if (reply->type != XCB_ATOM_ATOM) { + doFinish(); + free(reply); + return; + } + xcb_atom_t *value = static_cast(xcb_get_property_value(reply)); + m_version = qMin(*value, Dnd::version()); + if (m_version < 1) { + // minimal version we accept is 1 + doFinish(); + free(reply); + return; + } + free(reply); + + const auto *dd = DataBridge::self()->dataDevice(); + // proxy drop + m_enterCon = connect(dd, &KWayland::Client::DataDevice::dragEntered, + this, &Xvisit::receiveOffer); + m_dropCon = connect(dd, &KWayland::Client::DataDevice::dropped, + this, &Xvisit::drop); +} + +bool Xvisit::handleClientMessage(xcb_client_message_event_t *event) +{ + if (event->type == atoms->xdnd_status) { + return handleStatus(event); + } else if (event->type == atoms->xdnd_finished) { + return handleFinished(event); + } + return false; +} + +bool Xvisit::handleStatus(xcb_client_message_event_t *ev) +{ + xcb_client_message_data_t *data = &ev->data; + if (data->data32[0] != m_target->window()) { + // wrong target window + return false; + } + + m_accepts = data->data32[1] & 1; + xcb_atom_t actionAtom = data->data32[4]; + + // TODO: we could optimize via rectangle in data32[2] and data32[3] + + // position round trip finished + m_pos.pending = false; + + if (!m_state.dropped) { + // as long as the drop is not yet done determine requested action + m_preferredAction = Drag::atomToClientAction(actionAtom); + determineProposedAction(); + requestDragAndDropAction(); + } + + if (m_pos.cached) { + // send cached position + m_pos.cached = false; + sendPosition(m_pos.cache); + } else if (m_state.dropped) { + // drop was done in between, now close it out + drop(); + } + return true; +} + +bool Xvisit::handleFinished(xcb_client_message_event_t *ev) +{ + xcb_client_message_data_t *data = &ev->data; + + if (data->data32[0] != m_target->window()) { + // different target window + return false; + } + + if (!m_state.dropped) { + // drop was never done + doFinish(); + return true; + } + + const bool success = m_version > 4 ? data->data32[1] & 1 : true; + const xcb_atom_t usedActionAtom = m_version > 4 ? data->data32[2] : + static_cast(XCB_ATOM_NONE); + Q_UNUSED(success); + Q_UNUSED(usedActionAtom); + + // data offer might have been deleted already by the DataDevice + if (!m_dataOffer.isNull()) { + m_dataOffer->dragAndDropFinished(); + delete m_dataOffer; + m_dataOffer = nullptr; + } + doFinish(); + return true; +} + +void Xvisit::sendPosition(const QPointF &globalPos) +{ + const int16_t x = globalPos.x(); + const int16_t y = globalPos.y(); + + if (m_pos.pending) { + m_pos.cache = QPoint(x, y); + m_pos.cached = true; + return; + } + m_pos.pending = true; + + xcb_client_message_data_t data = {0}; + data.data32[0] = DataBridge::self()->dnd()->window(); + data.data32[2] = (x << 16) | y; + data.data32[3] = XCB_CURRENT_TIME; + data.data32[4] = Drag::clientActionToAtom(m_proposedAction); + + Drag::sendClientMessage(m_target->window(), atoms->xdnd_position, &data); +} + +void Xvisit::leave() +{ + Q_ASSERT(!m_state.dropped); + if (m_state.finished) { + // was already finished + return; + } + // we only need to leave, when we entered before + if (m_state.entered) { + sendLeave(); + } + doFinish(); +} + +void Xvisit::receiveOffer() +{ + if (m_state.finished) { + // already ended + return; + } + + Q_ASSERT(m_dataOffer.isNull()); + m_dataOffer = DataBridge::self()->dataDevice()->dragOffer(); + Q_ASSERT(!m_dataOffer.isNull()); + + retrieveSupportedActions(); + m_actionCon = connect(m_dataOffer, &KWayland::Client::DataOffer::sourceDragAndDropActionsChanged, + this, &Xvisit::retrieveSupportedActions); + enter(); +} + +void Xvisit::enter() +{ + m_state.entered = true; + // send enter event and current position to X client + sendEnter(); + sendPosition(waylandServer()->seat()->pointerPos()); + + // proxy future pointer position changes + m_motionCon = connect(waylandServer()->seat(), + &KWayland::Server::SeatInterface::pointerPosChanged, + this, &Xvisit::sendPosition); +} + +void Xvisit::sendEnter() +{ + xcb_client_message_data_t data = {0}; + data.data32[0] = DataBridge::self()->dnd()->window(); + data.data32[1] = m_version << 24; + + // TODO: replace this with the mime type getter from m_dataOffer, + // then we can get rid of m_drag. + const auto mimeTypesNames = m_drag->dataSourceIface()->mimeTypes(); + const int mimesCount = mimeTypesNames.size(); + size_t cnt = 0; + size_t totalCnt = 0; + for (const auto mimeName : mimeTypesNames) { + // 3 mimes and less can be sent directly in the XdndEnter message + if (totalCnt == 3) { + break; + } + const auto atom = Selection::mimeTypeToAtom(mimeName); + + if (atom != XCB_ATOM_NONE) { + data.data32[cnt + 2] = atom; + cnt++; + } + totalCnt++; + } + for (int i = cnt; i < 4; i++) { + data.data32[i + 2] = XCB_ATOM_NONE; + } + + if (mimesCount > 3) { + // need to first transfer all available mime types + data.data32[1] |= 1; + + QVector targets; + targets.resize(mimesCount); + + size_t cnt = 0; + for (const auto mimeName : mimeTypesNames) { + const auto atom = Selection::mimeTypeToAtom(mimeName); + if (atom != XCB_ATOM_NONE) { + targets[cnt] = atom; + cnt++; + } + } + + xcb_change_property(kwinApp()->x11Connection(), + XCB_PROP_MODE_REPLACE, + DataBridge::self()->dnd()->window(), + atoms->xdnd_type_list, + XCB_ATOM_ATOM, + 32, cnt, targets.data()); + } + Drag::sendClientMessage(m_target->window(), atoms->xdnd_enter, &data); +} + +void Xvisit::sendDrop(uint32_t time) +{ + xcb_client_message_data_t data = {0}; + data.data32[0] = DataBridge::self()->dnd()->window(); + data.data32[2] = time; + + Drag::sendClientMessage(m_target->window(), atoms->xdnd_drop, &data); + + if (m_version < 2) { + doFinish(); + } +} + +void Xvisit::sendLeave() +{ + xcb_client_message_data_t data = {0}; + data.data32[0] = DataBridge::self()->dnd()->window(); + Drag::sendClientMessage(m_target->window(), atoms->xdnd_leave, &data); +} + +void Xvisit::retrieveSupportedActions() +{ + m_supportedActions = m_dataOffer->sourceDragAndDropActions(); + determineProposedAction(); + requestDragAndDropAction(); +} + +void Xvisit::determineProposedAction() +{ + DnDAction oldProposedAction = m_proposedAction; + if (m_supportedActions.testFlag(m_preferredAction)) { + m_proposedAction = m_preferredAction; + } else if (m_supportedActions.testFlag(DnDAction::Copy)) { + m_proposedAction = DnDAction::Copy; + } else { + m_proposedAction = DnDAction::None; + } + // send updated action to X target + if (oldProposedAction != m_proposedAction) { + sendPosition(waylandServer()->seat()->pointerPos()); + } +} + +void Xvisit::requestDragAndDropAction() +{ + if (m_dataOffer.isNull()) { + return; + } + const auto pref = m_preferredAction != DnDAction::None ? m_preferredAction: + DnDAction::Copy; + // we assume the X client supports Move, but this might be wrong - then + // the drag just cancels, if the user tries to force it. + + m_dataOffer->setDragAndDropActions(DnDAction::Copy | DnDAction::Move, pref); + waylandServer()->dispatch(); +} + +void Xvisit::drop() +{ + Q_ASSERT(!m_state.finished); + m_state.dropped = true; + // stop further updates + // TODO: revisit when we allow ask action + stopConnections(); + if (!m_state.entered) { + // wait for enter (init + offers) + return; + } + if (m_pos.pending) { + // wait for pending position roundtrip + return; + } + if (!m_accepts) { + // target does not accept current action/offer + sendLeave(); + doFinish(); + return; + } + // dnd session ended successfully + sendDrop(XCB_CURRENT_TIME); +} + +void Xvisit::doFinish() +{ + m_state.finished = true; + m_pos.cached = false; + stopConnections(); + Q_EMIT finish(this); +} + +void Xvisit::stopConnections() +{ + // final outcome has been determined from Wayland side + // no more updates needed + disconnect(m_enterCon); + m_enterCon = QMetaObject::Connection(); + disconnect(m_dropCon); + m_dropCon = QMetaObject::Connection(); + + disconnect(m_motionCon); + m_motionCon = QMetaObject::Connection(); + disconnect(m_actionCon); + m_actionCon = QMetaObject::Connection(); +} + +} +} diff --git a/xwl/drag_x.h b/xwl/drag_x.h new file mode 100644 --- /dev/null +++ b/xwl/drag_x.h @@ -0,0 +1,162 @@ +/******************************************************************** + 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 . +*********************************************************************/ +#ifndef KWIN_XWL_DRAG_X +#define KWIN_XWL_DRAG_X + +#include "drag.h" + +#include +#include + +#include + +#include +#include +#include + +namespace KWayland { +namespace Client { +class DataSource; +} +} + +namespace KWin +{ +class Toplevel; +class AbstractClient; + +namespace Xwl +{ +class X11Source; +enum class DragEventReply; +class WlVisit; + +using Mimes = QVector >; + +class XToWlDrag : public Drag +{ + Q_OBJECT +public: + explicit XToWlDrag(X11Source *source); + ~XToWlDrag() override; + + DragEventReply moveFilter(Toplevel *target, QPoint pos) override; + bool handleClientMessage(xcb_client_message_event_t *event) override; + + void setDragAndDropAction(DnDAction action); + DnDAction selectedDragAndDropAction(); + + bool end() override { + return false; + } + X11Source* x11Source() const { + return m_src; + } + +private: + void setOffers(const Mimes &offers); + void offerCallback(const QString &mime); + void setDragTarget(); + + bool checkForFinished(); + + KWayland::Client::DataSource *m_dataSource; + + Mimes m_offers; + Mimes m_offersPending; + + X11Source *m_src; + QVector > m_dataRequests; + + WlVisit *m_visit = nullptr; + QVector m_oldVisits; + + bool m_performed = false; + DnDAction m_lastSelectedDragAndDropAction = DnDAction::None; +}; + +class WlVisit : public QObject +{ + Q_OBJECT +public: + WlVisit(AbstractClient *target, XToWlDrag *drag); + ~WlVisit(); + + bool handleClientMessage(xcb_client_message_event_t *event); + bool leave(); + + AbstractClient *target() const { + return m_target; + } + xcb_window_t window() const { + return m_window; + } + bool entered() const { + return m_entered; + } + bool dropHandled() const { + return m_dropHandled; + } + bool finished() const { + return m_finished; + } + void sendFinished(); + +Q_SIGNALS: + void offersReceived(const Mimes &offers); + void finish(WlVisit *self); + +private: + bool handleEnter(xcb_client_message_event_t *ev); + bool handlePosition(xcb_client_message_event_t *ev); + bool handleDrop(xcb_client_message_event_t *ev); + bool handleLeave(xcb_client_message_event_t *ev); + + void sendStatus(); + + void getMimesFromWinProperty(Mimes &offers); + + bool targetAcceptsAction() const; + + void doFinish(); + void unmapProxyWindow(); + + AbstractClient *m_target; + xcb_window_t m_window; + + xcb_window_t m_srcWindow = XCB_WINDOW_NONE; + XToWlDrag *m_drag; + + uint32_t m_version = 0; + + xcb_atom_t m_actionAtom; + DnDAction m_action = DnDAction::None; + + bool m_mapped = false; + bool m_entered = false; + bool m_dropHandled = false; + bool m_finished = false; + +}; + +} +} + +#endif diff --git a/xwl/drag_x.cpp b/xwl/drag_x.cpp new file mode 100644 --- /dev/null +++ b/xwl/drag_x.cpp @@ -0,0 +1,531 @@ +/******************************************************************** + 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 { + +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 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(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(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_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; + +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) +{ + 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 = Selection::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_get_property_value(reply)); + for (size_t i = 0; i < reply->value_len; ++i) { + const auto mimeStrings = Selection::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(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; + } + auto *xcbConn = kwinApp()->x11Connection(); + xcb_unmap_window(xcbConn, m_window); + xcb_flush(xcbConn); + m_mapped = false; +} + +} +} diff --git a/xwl/selection.h b/xwl/selection.h --- a/xwl/selection.h +++ b/xwl/selection.h @@ -60,18 +60,19 @@ static xcb_atom_t mimeTypeToAtom(const QString &mimeType); static xcb_atom_t mimeTypeToAtomLiteral(const QString &mimeType); static QStringList atomToMimeTypes(xcb_atom_t atom); + static void sendSelNotify(xcb_selection_request_event_t *event, bool success); // on selection owner changes by X clients (Xwl -> Wl) bool handleXfixesNotify(xcb_xfixes_selection_notify_event_t *event); bool filterEvent(xcb_generic_event_t *event); - void sendSelNotify(xcb_selection_request_event_t *event, bool success); xcb_atom_t atom() const { return m_atom; } xcb_window_t window() const { return m_window; } + void overwriteRequestorWindow(xcb_window_t window); Q_SIGNALS: void transferFinished(xcb_timestamp_t eventTime); @@ -117,6 +118,7 @@ xcb_atom_t m_atom = XCB_ATOM_NONE; xcb_window_t m_window = XCB_WINDOW_NONE; + xcb_window_t m_requestorWindow = XCB_WINDOW_NONE; xcb_timestamp_t m_timestamp; // Active source, if any. Only one of them at max can exist diff --git a/xwl/selection.cpp b/xwl/selection.cpp --- a/xwl/selection.cpp +++ b/xwl/selection.cpp @@ -83,6 +83,7 @@ { auto *xcbConn = kwinApp()->x11Connection(); m_window = xcb_generate_id(kwinApp()->x11Connection()); + m_requestorWindow = m_window; xcb_flush(xcbConn); } @@ -109,14 +110,6 @@ } // Being here means some other X window has claimed the selection. - delete m_xSrc; - m_xSrc = nullptr; - const auto *ac = workspace()->activeClient(); - if (!ac || !ac->inherits("KWin::Client")) { - // selections are only allowed to be acquired when Xwayland has focus - // TODO: can we make this stronger (window id comparision)? - return true; - } doHandleXfixesNotify(event); return true; } @@ -200,7 +193,7 @@ delete m_xSrc; m_wlSrc = nullptr; m_xSrc = nullptr; - if (event->owner == XCB_WINDOW_NONE) { + if (!event || event->owner == XCB_WINDOW_NONE) { return; } m_xSrc = new X11Source(this, event); @@ -227,6 +220,17 @@ xcb_flush(xcbConn); } +void Selection::overwriteRequestorWindow(xcb_window_t window) +{ + Q_ASSERT(m_xSrc); + if (window == XCB_WINDOW_NONE) { + // reset + window = m_window; + } + m_requestorWindow = window; + m_xSrc->setRequestor(window); +} + bool Selection::handleSelRequest(xcb_selection_request_event_t *event) { if (event->selection != m_atom) { @@ -283,7 +287,7 @@ void Selection::startTransferToWayland(xcb_atom_t target, qint32 fd) { // create new x to wl data transfer object - auto *transfer = new TransferXtoWl(m_atom, target, fd, m_xSrc->timestamp(), m_window, this); + auto *transfer = new TransferXtoWl(m_atom, target, fd, m_xSrc->timestamp(), m_requestorWindow, this); m_xToWlTransfers << transfer; connect(transfer, &TransferXtoWl::finished, this, [this, transfer]() { diff --git a/xwl/selection_source.h b/xwl/selection_source.h --- a/xwl/selection_source.h +++ b/xwl/selection_source.h @@ -69,10 +69,17 @@ Selection *selection() const { return m_sel; } + void setWindow(xcb_window_t window) { + m_window = window; + } + xcb_window_t window() const { + return m_window; + } private: xcb_timestamp_t m_timestamp = XCB_CURRENT_TIME; Selection *m_sel; + xcb_window_t m_window; }; /** @@ -135,6 +142,10 @@ bool handleSelNotify(xcb_selection_notify_event_t *event); + void setRequestor(xcb_window_t window) { + setWindow(window); + } + Q_SIGNALS: void offersChanged(QVector added, QVector removed); void transferReady(xcb_atom_t target, qint32 fd); diff --git a/xwl/selection_source.cpp b/xwl/selection_source.cpp --- a/xwl/selection_source.cpp +++ b/xwl/selection_source.cpp @@ -42,7 +42,8 @@ SelectionSource::SelectionSource(Selection *sel) : QObject(sel), - m_sel(sel) + m_sel(sel), + m_window(sel->window()) { } @@ -74,7 +75,7 @@ void WlSource::sendSelNotify(xcb_selection_request_event_t *event, bool success) { - selection()->sendSelNotify(event, success); + Selection::sendSelNotify(event, success); } bool WlSource::handleSelRequest(xcb_selection_request_event_t *event) @@ -183,7 +184,7 @@ auto *xcbConn = kwinApp()->x11Connection(); /* will lead to a selection request event for the new owner */ xcb_convert_selection(xcbConn, - selection()->window(), + window(), selection()->atom(), atoms->targets, atoms->wl_selection, @@ -199,7 +200,7 @@ auto *xcbConn = kwinApp()->x11Connection(); xcb_get_property_cookie_t cookie = xcb_get_property(xcbConn, 1, - selection()->window(), + window(), atoms->wl_selection, XCB_GET_PROPERTY_TYPE_ANY, 0, @@ -278,7 +279,7 @@ bool X11Source::handleSelNotify(xcb_selection_notify_event_t *event) { - if (event->requestor != selection()->window()) { + if (event->requestor != window()) { return false; } if (event->selection != selection()->atom()) { diff --git a/xwl/xwayland.h b/xwl/xwayland.h --- a/xwl/xwayland.h +++ b/xwl/xwayland.h @@ -62,6 +62,8 @@ void createX11Connection(); void continueStartupWithX(); + DragEventReply dragMoveFilter(Toplevel *target, QPoint pos) override; + int m_xcbConnectionFd = -1; QProcess *m_xwaylandProcess = nullptr; QMetaObject::Connection m_xwaylandFailConnection; diff --git a/xwl/xwayland.cpp b/xwl/xwayland.cpp --- a/xwl/xwayland.cpp +++ b/xwl/xwayland.cpp @@ -270,5 +270,13 @@ m_app->notifyKSplash(); } +DragEventReply Xwayland::dragMoveFilter(Toplevel *target, QPoint pos) +{ + if (!m_dataBridge) { + return DragEventReply::Wayland; + } + return m_dataBridge->dragMoveFilter(target, pos); +} + } } diff --git a/xwl/xwayland_interface.h b/xwl/xwayland_interface.h --- a/xwl/xwayland_interface.h +++ b/xwl/xwayland_interface.h @@ -23,16 +23,32 @@ #include #include +#include namespace KWin { +class Toplevel; + +namespace Xwl +{ +enum class DragEventReply { + // event should be ignored by the filter + Ignore, + // event is filtered out + Take, + // event should be handled as a Wayland native one + Wayland, +}; +} class KWIN_EXPORT XwaylandInterface : public QObject { Q_OBJECT public: static XwaylandInterface *self(); + virtual Xwl::DragEventReply dragMoveFilter(Toplevel *target, QPoint pos) = 0; + protected: explicit XwaylandInterface(QObject *parent = nullptr); virtual ~XwaylandInterface();