1 /*
2  * Copyright (C) 2016  Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17  *
18  */
19 
20 #include <QtGlobal>
21 #include "xdndworkaround.h"
22 #include <QApplication>
23 #include <QDebug>
24 #include <QX11Info>
25 #include <QMimeData>
26 #include <QCursor>
27 #include <QWidget>
28 
29 #include <QDrag>
30 #include <QUrl>
31 #include <cstring>
32 
33 // these are private Qt headers which are not part of Qt APIs
34 #include <private/qdnd_p.h>  // Too bad that we need to use private headers of Qt :-(
35 
36 // For some unknown reasons, the event type constants defined in
37 // xcb/input.h are different from that in X11/extension/XI2.h
38 // To be safe, we define it ourselves.
39 #undef XI_ButtonRelease
40 #define XI_ButtonRelease                 5
41 
42 
XdndWorkaround()43 XdndWorkaround::XdndWorkaround() {
44     if(!QX11Info::isPlatformX11()) {
45         return;
46     }
47 
48     // we need to filter all X11 events
49     qApp->installNativeEventFilter(this);
50 
51     lastDrag_ = nullptr;
52 
53     // initialize xinput2 since newer versions of Qt5 uses it.
54     static char xi_name[] = "XInputExtension";
55     xcb_connection_t* conn = QX11Info::connection();
56     xcb_query_extension_cookie_t cookie = xcb_query_extension(conn, strlen(xi_name), xi_name);
57     xcb_generic_error_t* err = nullptr;
58     xcb_query_extension_reply_t* reply = xcb_query_extension_reply(conn, cookie, &err);
59     if(err == nullptr) {
60         xinput2Enabled_ = true;
61         xinputOpCode_ = reply->major_opcode;
62         xinputEventBase_ = reply->first_event;
63         xinputErrorBase_ = reply->first_error;
64         // qDebug() << "xinput: " << m_xi2Enabled << m_xiOpCode << m_xiEventBase;
65     }
66     else {
67         xinput2Enabled_ = false;
68         free(err);
69     }
70     free(reply);
71 }
72 
~XdndWorkaround()73 XdndWorkaround::~XdndWorkaround() {
74     if(!QX11Info::isPlatformX11()) {
75         return;
76     }
77     qApp->removeNativeEventFilter(this);
78 }
79 
nativeEventFilter(const QByteArray & eventType,void * message,long *)80 bool XdndWorkaround::nativeEventFilter(const QByteArray& eventType, void* message, long* /*result*/) {
81     if(Q_LIKELY(eventType == "xcb_generic_event_t")) {
82         xcb_generic_event_t* event = static_cast<xcb_generic_event_t*>(message);
83         switch(event->response_type & ~0x80) {
84         case XCB_CLIENT_MESSAGE:
85             return clientMessage(reinterpret_cast<xcb_client_message_event_t*>(event));
86         case XCB_SELECTION_NOTIFY:
87             return selectionNotify(reinterpret_cast<xcb_selection_notify_event_t*>(event));
88         case XCB_SELECTION_REQUEST:
89             return selectionRequest(reinterpret_cast<xcb_selection_request_event_t*>(event));
90         case XCB_GE_GENERIC:
91             // newer versions of Qt5 supports xinput2, which sends its mouse events via XGE.
92             return genericEvent(reinterpret_cast<xcb_ge_generic_event_t*>(event));
93         case XCB_BUTTON_RELEASE:
94             // older versions of Qt5 receive mouse events via old XCB events.
95             buttonRelease();
96             break;
97         default:
98             break;
99         }
100     }
101     return false;
102 }
103 
104 // static
atomName(xcb_atom_t atom)105 QByteArray XdndWorkaround::atomName(xcb_atom_t atom) {
106     QByteArray name;
107     xcb_connection_t* conn = QX11Info::connection();
108     xcb_get_atom_name_cookie_t cookie = xcb_get_atom_name(conn, atom);
109     xcb_get_atom_name_reply_t* reply = xcb_get_atom_name_reply(conn, cookie, nullptr);
110     int len = xcb_get_atom_name_name_length(reply);
111     if(len > 0) {
112         name.append(xcb_get_atom_name_name(reply), len);
113     }
114     free(reply);
115     return name;
116 }
117 
118 // static
internAtom(const char * name,int len)119 xcb_atom_t XdndWorkaround::internAtom(const char* name, int len) {
120     xcb_atom_t atom = 0;
121     if(len == -1) {
122         len = strlen(name);
123     }
124     xcb_connection_t* conn = QX11Info::connection();
125     xcb_intern_atom_cookie_t cookie = xcb_intern_atom(conn, false, len, name);
126     xcb_generic_error_t* err = nullptr;
127     xcb_intern_atom_reply_t* reply = xcb_intern_atom_reply(conn, cookie, &err);
128     if(reply != nullptr) {
129         atom = reply->atom;
130         free(reply);
131     }
132     if(err != nullptr) {
133         free(err);
134     }
135     return atom;
136 }
137 
138 // static
windowProperty(xcb_window_t window,xcb_atom_t propAtom,xcb_atom_t typeAtom,int len)139 QByteArray XdndWorkaround::windowProperty(xcb_window_t window, xcb_atom_t propAtom, xcb_atom_t typeAtom, int len) {
140     QByteArray data;
141     xcb_connection_t* conn = QX11Info::connection();
142     xcb_get_property_cookie_t cookie = xcb_get_property(conn, false, window, propAtom, typeAtom, 0, len);
143     xcb_generic_error_t* err = nullptr;
144     xcb_get_property_reply_t* reply = xcb_get_property_reply(conn, cookie, &err);
145     if(reply != nullptr) {
146         len = xcb_get_property_value_length(reply);
147         const char* buf = (const char*)xcb_get_property_value(reply);
148         data.append(buf, len);
149         free(reply);
150     }
151     if(err != nullptr) {
152         free(err);
153     }
154     return data;
155 }
156 
157 // static
setWindowProperty(xcb_window_t window,xcb_atom_t propAtom,xcb_atom_t typeAtom,void * data,int len,int format)158 void XdndWorkaround::setWindowProperty(xcb_window_t window, xcb_atom_t propAtom, xcb_atom_t typeAtom, void* data, int len, int format) {
159     xcb_connection_t* conn = QX11Info::connection();
160     xcb_change_property(conn, XCB_PROP_MODE_REPLACE, window, propAtom, typeAtom, format, len, data);
161 }
162 
163 
clientMessage(xcb_client_message_event_t * event)164 bool XdndWorkaround::clientMessage(xcb_client_message_event_t* event) {
165     QByteArray event_type = atomName(event->type);
166     // qDebug() << "client message:" << event_type;
167 
168     // NOTE: Because of the limitation of Qt, this hack is required to provide
169     // Xdnd direct save (XDS) protocol support.
170     // https://www.freedesktop.org/wiki/Specifications/XDS/#index4h2
171     //
172     // XDS requires that the drop target should get and set the window property of the
173     // drag source to pass the file path, but in Qt there is NO way to know the
174     // window ID of the drag source so it's not possible to implement XDS with Qt alone.
175     // Here is a simple hack. We get the drag source window ID with raw XCB code.
176     // Then, save it on the drop target widget using QObject dynamic property.
177     // So in the drop event handler of the target widget, it can obtain the
178     // window ID of the drag source with QObject::property().
179     // This hack works 99.99% of the time, but it's not bullet-proof.
180     // In theory, there is one corner case for which this will not work.
181     // That is, when you drag multiple XDS sources at the same time and drop
182     // all of them on the same widget. (Does XDND support doing this?)
183     // I do not think that any app at the moment support this.
184     // Even if somebody is using it, X11 will die and we should solve this in Wayland instead.
185     //
186     if(event_type == "XdndDrop") {
187         // data.l[0] contains the XID of the source window.
188         // data.l[1] is reserved for future use (flags).
189         // data.l[2] contains the time stamp for retrieving the data. (new in version 1)
190         QWidget* target = QWidget::find(event->window);
191         if(target != nullptr) { // drop on our widget
192             target = qApp->widgetAt(QCursor::pos()); // get the exact child widget that receives the drop
193             if(target != nullptr) {
194                 target->setProperty("xdnd::lastDragSource", event->data.data32[0]);
195                 target->setProperty("xdnd::lastDropTime", event->data.data32[2]);
196             }
197         }
198     }
199     else if(event_type == "XdndFinished") {
200         lastDrag_ = nullptr;
201     }
202     return false;
203 }
204 
selectionNotify(xcb_selection_notify_event_t * event)205 bool XdndWorkaround::selectionNotify(xcb_selection_notify_event_t* event) {
206     qDebug() << "selection notify" << atomName(event->selection);
207     return false;
208 }
209 
210 
selectionRequest(xcb_selection_request_event_t * event)211 bool XdndWorkaround::selectionRequest(xcb_selection_request_event_t* event) {
212     xcb_connection_t* conn = QX11Info::connection();
213     if(event->property == XCB_ATOM_PRIMARY || event->property == XCB_ATOM_SECONDARY) {
214         return false;    // we only touch selection requests related to XDnd
215     }
216     QByteArray prop_name = atomName(event->property);
217     if(prop_name == "CLIPBOARD") {
218         return false;    // we do not touch clipboard, either
219     }
220 
221     xcb_atom_t atomFormat = event->target;
222     QByteArray type_name = atomName(atomFormat);
223     // qDebug() << "selection request" << prop_name << type_name;
224     // We only want to handle text/x-moz-url and text/uri-list
225     if(type_name == "text/x-moz-url" || type_name.startsWith("text/uri-list")) {
226         QDragManager* mgr = QDragManager::self();
227         QDrag* drag = mgr->object();
228         if(drag == nullptr) {
229             drag = lastDrag_;
230         }
231         QMimeData* mime = drag ? drag->mimeData() : nullptr;
232         if(mime != nullptr && mime->hasUrls()) {
233             QByteArray data;
234             const QList<QUrl> uris = mime->urls();
235             if(type_name == "text/x-moz-url") {
236                 QString mozurl = uris.at(0).toString(QUrl::FullyEncoded);
237                 data.append((const char*)mozurl.utf16(), mozurl.length() * 2);
238             }
239             else { // text/uri-list
240                 for(const QUrl& uri : uris) {
241                     data.append(uri.toString(QUrl::FullyEncoded).toUtf8());
242                     data.append("\r\n");
243                 }
244             }
245             xcb_change_property(conn, XCB_PROP_MODE_REPLACE, event->requestor, event->property,
246                                 atomFormat, 8, data.size(), (const void*)data.constData());
247             xcb_selection_notify_event_t notify;
248             notify.response_type = XCB_SELECTION_NOTIFY;
249             notify.requestor = event->requestor;
250             notify.selection = event->selection;
251             notify.time = event->time;
252             notify.property = event->property;
253             notify.target = atomFormat;
254             xcb_window_t proxy_target = event->requestor;
255             xcb_send_event(conn, false, proxy_target, XCB_EVENT_MASK_NO_EVENT, (const char*)&notify);
256             return true; // stop Qt 5 from touching the event
257         }
258     }
259     return false; // let Qt handle this
260 }
261 
genericEvent(xcb_ge_generic_event_t * event)262 bool XdndWorkaround::genericEvent(xcb_ge_generic_event_t* event) {
263     // check this is an xinput event
264     if(xinput2Enabled_ && event->extension == xinputOpCode_) {
265         if(event->event_type == XI_ButtonRelease) {
266             buttonRelease();
267         }
268     }
269     return false;
270 }
271 
buttonRelease()272 void XdndWorkaround::buttonRelease() {
273     QDragManager* mgr = QDragManager::self();
274     lastDrag_ = mgr->object();
275     // qDebug() << "BUTTON RELEASE!!!!" << xcbDrag()->canDrop() << lastDrag_;
276 }
277 
278