1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the plugins of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qxcbclipboard.h" |
41 | |
42 | #include "qxcbconnection.h" |
43 | #include "qxcbscreen.h" |
44 | #include "qxcbmime.h" |
45 | #include "qxcbwindow.h" |
46 | |
47 | #include <private/qguiapplication_p.h> |
48 | #include <QElapsedTimer> |
49 | |
50 | #include <QtCore/QDebug> |
51 | |
52 | QT_BEGIN_NAMESPACE |
53 | |
54 | #ifndef QT_NO_CLIPBOARD |
55 | |
56 | class QXcbClipboardMime : public QXcbMime |
57 | { |
58 | Q_OBJECT |
59 | public: |
60 | QXcbClipboardMime(QClipboard::Mode mode, QXcbClipboard *clipboard) |
61 | : QXcbMime() |
62 | , m_clipboard(clipboard) |
63 | { |
64 | switch (mode) { |
65 | case QClipboard::Selection: |
66 | modeAtom = XCB_ATOM_PRIMARY; |
67 | break; |
68 | |
69 | case QClipboard::Clipboard: |
70 | modeAtom = m_clipboard->atom(QXcbAtom::CLIPBOARD); |
71 | break; |
72 | |
73 | default: |
74 | qWarning("QXcbClipboardMime: Internal error: Unsupported clipboard mode" ); |
75 | break; |
76 | } |
77 | } |
78 | |
79 | void reset() |
80 | { |
81 | formatList.clear(); |
82 | } |
83 | |
84 | bool isEmpty() const |
85 | { |
86 | return m_clipboard->getSelectionOwner(modeAtom) == XCB_NONE; |
87 | } |
88 | |
89 | protected: |
90 | QStringList formats_sys() const override |
91 | { |
92 | if (isEmpty()) |
93 | return QStringList(); |
94 | |
95 | if (!formatList.count()) { |
96 | QXcbClipboardMime *that = const_cast<QXcbClipboardMime *>(this); |
97 | // get the list of targets from the current clipboard owner - we do this |
98 | // once so that multiple calls to this function don't require multiple |
99 | // server round trips... |
100 | that->format_atoms = m_clipboard->getDataInFormat(modeAtom, m_clipboard->atom(QXcbAtom::TARGETS)); |
101 | |
102 | if (format_atoms.size() > 0) { |
103 | const xcb_atom_t *targets = (const xcb_atom_t *) format_atoms.data(); |
104 | int size = format_atoms.size() / sizeof(xcb_atom_t); |
105 | |
106 | for (int i = 0; i < size; ++i) { |
107 | if (targets[i] == 0) |
108 | continue; |
109 | |
110 | QString format = mimeAtomToString(m_clipboard->connection(), targets[i]); |
111 | if (!formatList.contains(format)) |
112 | that->formatList.append(format); |
113 | } |
114 | } |
115 | } |
116 | |
117 | return formatList; |
118 | } |
119 | |
120 | bool hasFormat_sys(const QString &format) const override |
121 | { |
122 | QStringList list = formats(); |
123 | return list.contains(format); |
124 | } |
125 | |
126 | QVariant retrieveData_sys(const QString &fmt, QVariant::Type requestedType) const override |
127 | { |
128 | if (fmt.isEmpty() || isEmpty()) |
129 | return QByteArray(); |
130 | |
131 | (void)formats(); // trigger update of format list |
132 | |
133 | QVector<xcb_atom_t> atoms; |
134 | const xcb_atom_t *targets = (const xcb_atom_t *) format_atoms.data(); |
135 | int size = format_atoms.size() / sizeof(xcb_atom_t); |
136 | atoms.reserve(size); |
137 | for (int i = 0; i < size; ++i) |
138 | atoms.append(targets[i]); |
139 | |
140 | QByteArray encoding; |
141 | xcb_atom_t fmtatom = mimeAtomForFormat(m_clipboard->connection(), fmt, requestedType, atoms, &encoding); |
142 | |
143 | if (fmtatom == 0) |
144 | return QVariant(); |
145 | |
146 | return mimeConvertToFormat(m_clipboard->connection(), fmtatom, m_clipboard->getDataInFormat(modeAtom, fmtatom), fmt, requestedType, encoding); |
147 | } |
148 | private: |
149 | |
150 | xcb_atom_t modeAtom; |
151 | QXcbClipboard *m_clipboard; |
152 | QStringList formatList; |
153 | QByteArray format_atoms; |
154 | }; |
155 | |
156 | QXcbClipboardTransaction::QXcbClipboardTransaction(QXcbClipboard *clipboard, xcb_window_t w, |
157 | xcb_atom_t p, QByteArray d, xcb_atom_t t, int f) |
158 | : m_clipboard(clipboard), m_window(w), m_property(p), m_data(d), m_target(t), m_format(f) |
159 | { |
160 | const quint32 values[] = { XCB_EVENT_MASK_PROPERTY_CHANGE }; |
161 | xcb_change_window_attributes(m_clipboard->xcb_connection(), m_window, |
162 | XCB_CW_EVENT_MASK, values); |
163 | |
164 | m_abortTimerId = startTimer(m_clipboard->clipboardTimeout()); |
165 | } |
166 | |
167 | QXcbClipboardTransaction::~QXcbClipboardTransaction() |
168 | { |
169 | if (m_abortTimerId) |
170 | killTimer(m_abortTimerId); |
171 | m_abortTimerId = 0; |
172 | m_clipboard->removeTransaction(m_window); |
173 | } |
174 | |
175 | bool QXcbClipboardTransaction::updateIncrementalProperty(const xcb_property_notify_event_t *event) |
176 | { |
177 | if (event->atom != m_property || event->state != XCB_PROPERTY_DELETE) |
178 | return false; |
179 | |
180 | // restart the timer |
181 | if (m_abortTimerId) |
182 | killTimer(m_abortTimerId); |
183 | m_abortTimerId = startTimer(m_clipboard->clipboardTimeout()); |
184 | |
185 | uint bytes_left = uint(m_data.size()) - m_offset; |
186 | if (bytes_left > 0) { |
187 | int increment = m_clipboard->increment(); |
188 | uint bytes_to_send = qMin(uint(increment), bytes_left); |
189 | |
190 | qCDebug(lcQpaClipboard, "sending %d bytes, %d remaining, transaction: %p)" , |
191 | bytes_to_send, bytes_left - bytes_to_send, this); |
192 | |
193 | uint32_t dataSize = bytes_to_send / (m_format / 8); |
194 | xcb_change_property(m_clipboard->xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, |
195 | m_property, m_target, m_format, dataSize, m_data.constData() + m_offset); |
196 | m_offset += bytes_to_send; |
197 | } else { |
198 | qCDebug(lcQpaClipboard, "transaction %p completed" , this); |
199 | |
200 | xcb_change_property(m_clipboard->xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, |
201 | m_property, m_target, m_format, 0, nullptr); |
202 | |
203 | const quint32 values[] = { XCB_EVENT_MASK_NO_EVENT }; |
204 | xcb_change_window_attributes(m_clipboard->xcb_connection(), m_window, |
205 | XCB_CW_EVENT_MASK, values); |
206 | delete this; // self destroy |
207 | } |
208 | return true; |
209 | } |
210 | |
211 | |
212 | void QXcbClipboardTransaction::timerEvent(QTimerEvent *ev) |
213 | { |
214 | if (ev->timerId() == m_abortTimerId) { |
215 | // this can happen when the X client we are sending data |
216 | // to decides to exit (normally or abnormally) |
217 | qCDebug(lcQpaClipboard, "timed out while sending data to %p" , this); |
218 | delete this; // self destroy |
219 | } |
220 | } |
221 | |
222 | const int QXcbClipboard::clipboard_timeout = 5000; |
223 | |
224 | QXcbClipboard::QXcbClipboard(QXcbConnection *c) |
225 | : QXcbObject(c), QPlatformClipboard() |
226 | { |
227 | Q_ASSERT(QClipboard::Clipboard == 0); |
228 | Q_ASSERT(QClipboard::Selection == 1); |
229 | m_clientClipboard[QClipboard::Clipboard] = 0; |
230 | m_clientClipboard[QClipboard::Selection] = 0; |
231 | m_timestamp[QClipboard::Clipboard] = XCB_CURRENT_TIME; |
232 | m_timestamp[QClipboard::Selection] = XCB_CURRENT_TIME; |
233 | m_owner = connection()->getQtSelectionOwner(); |
234 | |
235 | if (connection()->hasXFixes()) { |
236 | const uint32_t mask = XCB_XFIXES_SELECTION_EVENT_MASK_SET_SELECTION_OWNER | |
237 | XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_WINDOW_DESTROY | |
238 | XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_CLIENT_CLOSE; |
239 | xcb_xfixes_select_selection_input_checked(xcb_connection(), m_owner, XCB_ATOM_PRIMARY, mask); |
240 | xcb_xfixes_select_selection_input_checked(xcb_connection(), m_owner, atom(QXcbAtom::CLIPBOARD), mask); |
241 | } |
242 | |
243 | // xcb_change_property_request_t and xcb_get_property_request_t are the same size |
244 | m_maxPropertyRequestDataBytes = connection()->maxRequestDataBytes(sizeof(xcb_change_property_request_t)); |
245 | } |
246 | |
247 | QXcbClipboard::~QXcbClipboard() |
248 | { |
249 | m_clipboard_closing = true; |
250 | // Transfer the clipboard content to the clipboard manager if we own a selection |
251 | if (m_timestamp[QClipboard::Clipboard] != XCB_CURRENT_TIME || |
252 | m_timestamp[QClipboard::Selection] != XCB_CURRENT_TIME) { |
253 | |
254 | // First we check if there is a clipboard manager. |
255 | auto reply = Q_XCB_REPLY(xcb_get_selection_owner, xcb_connection(), atom(QXcbAtom::CLIPBOARD_MANAGER)); |
256 | if (reply && reply->owner != XCB_NONE) { |
257 | // we delete the property so the manager saves all TARGETS. |
258 | xcb_delete_property(xcb_connection(), m_owner, atom(QXcbAtom::_QT_SELECTION)); |
259 | xcb_convert_selection(xcb_connection(), m_owner, atom(QXcbAtom::CLIPBOARD_MANAGER), atom(QXcbAtom::SAVE_TARGETS), |
260 | atom(QXcbAtom::_QT_SELECTION), connection()->time()); |
261 | connection()->sync(); |
262 | |
263 | // waiting until the clipboard manager fetches the content. |
264 | if (auto event = waitForClipboardEvent(m_owner, XCB_SELECTION_NOTIFY, true)) { |
265 | free(event); |
266 | } else { |
267 | qWarning("QXcbClipboard: Unable to receive an event from the " |
268 | "clipboard manager in a reasonable time" ); |
269 | } |
270 | } |
271 | } |
272 | |
273 | if (m_clientClipboard[QClipboard::Clipboard] != m_clientClipboard[QClipboard::Selection]) |
274 | delete m_clientClipboard[QClipboard::Clipboard]; |
275 | delete m_clientClipboard[QClipboard::Selection]; |
276 | } |
277 | |
278 | bool QXcbClipboard::handlePropertyNotify(const xcb_generic_event_t *event) |
279 | { |
280 | if (m_transactions.isEmpty() || event->response_type != XCB_PROPERTY_NOTIFY) |
281 | return false; |
282 | |
283 | auto propertyNotify = reinterpret_cast<const xcb_property_notify_event_t *>(event); |
284 | TransactionMap::Iterator it = m_transactions.find(propertyNotify->window); |
285 | if (it == m_transactions.constEnd()) |
286 | return false; |
287 | |
288 | return (*it)->updateIncrementalProperty(propertyNotify); |
289 | } |
290 | |
291 | xcb_window_t QXcbClipboard::getSelectionOwner(xcb_atom_t atom) const |
292 | { |
293 | return connection()->getSelectionOwner(atom); |
294 | } |
295 | |
296 | xcb_atom_t QXcbClipboard::atomForMode(QClipboard::Mode mode) const |
297 | { |
298 | if (mode == QClipboard::Clipboard) |
299 | return atom(QXcbAtom::CLIPBOARD); |
300 | if (mode == QClipboard::Selection) |
301 | return XCB_ATOM_PRIMARY; |
302 | return XCB_NONE; |
303 | } |
304 | |
305 | QClipboard::Mode QXcbClipboard::modeForAtom(xcb_atom_t a) const |
306 | { |
307 | if (a == XCB_ATOM_PRIMARY) |
308 | return QClipboard::Selection; |
309 | if (a == atom(QXcbAtom::CLIPBOARD)) |
310 | return QClipboard::Clipboard; |
311 | // not supported enum value, used to detect errors |
312 | return QClipboard::FindBuffer; |
313 | } |
314 | |
315 | |
316 | QMimeData * QXcbClipboard::mimeData(QClipboard::Mode mode) |
317 | { |
318 | if (mode > QClipboard::Selection) |
319 | return 0; |
320 | |
321 | xcb_window_t clipboardOwner = getSelectionOwner(atomForMode(mode)); |
322 | if (clipboardOwner == owner()) { |
323 | return m_clientClipboard[mode]; |
324 | } else { |
325 | if (!m_xClipboard[mode]) |
326 | m_xClipboard[mode].reset(new QXcbClipboardMime(mode, this)); |
327 | |
328 | return m_xClipboard[mode].data(); |
329 | } |
330 | } |
331 | |
332 | void QXcbClipboard::setMimeData(QMimeData *data, QClipboard::Mode mode) |
333 | { |
334 | if (mode > QClipboard::Selection) |
335 | return; |
336 | |
337 | QXcbClipboardMime *xClipboard = 0; |
338 | // verify if there is data to be cleared on global X Clipboard. |
339 | if (!data) { |
340 | xClipboard = qobject_cast<QXcbClipboardMime *>(mimeData(mode)); |
341 | if (xClipboard) { |
342 | if (xClipboard->isEmpty()) |
343 | return; |
344 | } |
345 | } |
346 | |
347 | if (!xClipboard && (m_clientClipboard[mode] == data)) |
348 | return; |
349 | |
350 | xcb_atom_t modeAtom = atomForMode(mode); |
351 | xcb_window_t newOwner = XCB_NONE; |
352 | |
353 | if (m_clientClipboard[mode]) { |
354 | if (m_clientClipboard[QClipboard::Clipboard] != m_clientClipboard[QClipboard::Selection]) |
355 | delete m_clientClipboard[mode]; |
356 | m_clientClipboard[mode] = 0; |
357 | m_timestamp[mode] = XCB_CURRENT_TIME; |
358 | } |
359 | |
360 | if (connection()->time() == XCB_CURRENT_TIME) |
361 | connection()->setTime(connection()->getTimestamp()); |
362 | |
363 | if (data) { |
364 | newOwner = owner(); |
365 | |
366 | m_clientClipboard[mode] = data; |
367 | m_timestamp[mode] = connection()->time(); |
368 | } |
369 | |
370 | xcb_set_selection_owner(xcb_connection(), newOwner, modeAtom, connection()->time()); |
371 | |
372 | if (getSelectionOwner(modeAtom) != newOwner) { |
373 | qWarning("QXcbClipboard::setMimeData: Cannot set X11 selection owner" ); |
374 | } |
375 | |
376 | emitChanged(mode); |
377 | } |
378 | |
379 | bool QXcbClipboard::supportsMode(QClipboard::Mode mode) const |
380 | { |
381 | if (mode <= QClipboard::Selection) |
382 | return true; |
383 | return false; |
384 | } |
385 | |
386 | bool QXcbClipboard::ownsMode(QClipboard::Mode mode) const |
387 | { |
388 | if (m_owner == XCB_NONE || mode > QClipboard::Selection) |
389 | return false; |
390 | |
391 | Q_ASSERT(m_timestamp[mode] == XCB_CURRENT_TIME || getSelectionOwner(atomForMode(mode)) == m_owner); |
392 | |
393 | return m_timestamp[mode] != XCB_CURRENT_TIME; |
394 | } |
395 | |
396 | QXcbScreen *QXcbClipboard::screen() const |
397 | { |
398 | return connection()->primaryScreen(); |
399 | } |
400 | |
401 | xcb_window_t QXcbClipboard::requestor() const |
402 | { |
403 | QXcbScreen *platformScreen = screen(); |
404 | |
405 | if (!m_requestor && platformScreen) { |
406 | const int x = 0, y = 0, w = 3, h = 3; |
407 | QXcbClipboard *that = const_cast<QXcbClipboard *>(this); |
408 | |
409 | xcb_window_t window = xcb_generate_id(xcb_connection()); |
410 | xcb_create_window(xcb_connection(), |
411 | XCB_COPY_FROM_PARENT, // depth -- same as root |
412 | window, // window id |
413 | platformScreen->screen()->root, // parent window id |
414 | x, y, w, h, |
415 | 0, // border width |
416 | XCB_WINDOW_CLASS_INPUT_OUTPUT, // window class |
417 | platformScreen->screen()->root_visual, // visual |
418 | 0, // value mask |
419 | 0); // value list |
420 | |
421 | QXcbWindow::setWindowTitle(connection(), window, |
422 | QStringLiteral("Qt Clipboard Requestor Window" )); |
423 | |
424 | uint32_t mask = XCB_EVENT_MASK_PROPERTY_CHANGE; |
425 | xcb_change_window_attributes(xcb_connection(), window, XCB_CW_EVENT_MASK, &mask); |
426 | |
427 | that->setRequestor(window); |
428 | } |
429 | return m_requestor; |
430 | } |
431 | |
432 | void QXcbClipboard::setRequestor(xcb_window_t window) |
433 | { |
434 | if (m_requestor != XCB_NONE) { |
435 | xcb_destroy_window(xcb_connection(), m_requestor); |
436 | } |
437 | m_requestor = window; |
438 | } |
439 | |
440 | xcb_window_t QXcbClipboard::owner() const |
441 | { |
442 | return m_owner; |
443 | } |
444 | |
445 | xcb_atom_t QXcbClipboard::sendTargetsSelection(QMimeData *d, xcb_window_t window, xcb_atom_t property) |
446 | { |
447 | QVector<xcb_atom_t> types; |
448 | QStringList formats = QInternalMimeData::formatsHelper(d); |
449 | for (int i = 0; i < formats.size(); ++i) { |
450 | QVector<xcb_atom_t> atoms = QXcbMime::mimeAtomsForFormat(connection(), formats.at(i)); |
451 | for (int j = 0; j < atoms.size(); ++j) { |
452 | if (!types.contains(atoms.at(j))) |
453 | types.append(atoms.at(j)); |
454 | } |
455 | } |
456 | types.append(atom(QXcbAtom::TARGETS)); |
457 | types.append(atom(QXcbAtom::MULTIPLE)); |
458 | types.append(atom(QXcbAtom::TIMESTAMP)); |
459 | types.append(atom(QXcbAtom::SAVE_TARGETS)); |
460 | |
461 | xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, window, property, XCB_ATOM_ATOM, |
462 | 32, types.size(), (const void *)types.constData()); |
463 | return property; |
464 | } |
465 | |
466 | xcb_atom_t QXcbClipboard::sendSelection(QMimeData *d, xcb_atom_t target, xcb_window_t window, xcb_atom_t property) |
467 | { |
468 | xcb_atom_t atomFormat = target; |
469 | int dataFormat = 0; |
470 | QByteArray data; |
471 | |
472 | QString fmt = QXcbMime::mimeAtomToString(connection(), target); |
473 | if (fmt.isEmpty()) { // Not a MIME type we have |
474 | // qDebug() << "QClipboard: send_selection(): converting to type" << connection()->atomName(target) << "is not supported"; |
475 | return XCB_NONE; |
476 | } |
477 | // qDebug() << "QClipboard: send_selection(): converting to type" << fmt; |
478 | |
479 | if (QXcbMime::mimeDataForAtom(connection(), target, d, &data, &atomFormat, &dataFormat)) { |
480 | |
481 | // don't allow INCR transfers when using MULTIPLE or to |
482 | // Motif clients (since Motif doesn't support INCR) |
483 | static xcb_atom_t motif_clip_temporary = atom(QXcbAtom::CLIP_TEMPORARY); |
484 | bool allow_incr = property != motif_clip_temporary; |
485 | // This 'bool' can be removed once there is a proper fix for QTBUG-32853 |
486 | if (m_clipboard_closing) |
487 | allow_incr = false; |
488 | |
489 | if (data.size() > m_maxPropertyRequestDataBytes && allow_incr) { |
490 | long bytes = data.size(); |
491 | xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, window, property, |
492 | atom(QXcbAtom::INCR), 32, 1, (const void *)&bytes); |
493 | auto transaction = new QXcbClipboardTransaction(this, window, property, data, atomFormat, dataFormat); |
494 | m_transactions.insert(window, transaction); |
495 | return property; |
496 | } |
497 | |
498 | // make sure we can perform the XChangeProperty in a single request |
499 | if (data.size() > m_maxPropertyRequestDataBytes) |
500 | return XCB_NONE; // ### perhaps use several XChangeProperty calls w/ PropModeAppend? |
501 | int dataSize = data.size() / (dataFormat / 8); |
502 | // use a single request to transfer data |
503 | xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, window, property, atomFormat, |
504 | dataFormat, dataSize, (const void *)data.constData()); |
505 | } |
506 | return property; |
507 | } |
508 | |
509 | void QXcbClipboard::handleSelectionClearRequest(xcb_selection_clear_event_t *event) |
510 | { |
511 | QClipboard::Mode mode = modeForAtom(event->selection); |
512 | if (mode > QClipboard::Selection) |
513 | return; |
514 | |
515 | // ignore the event if it was generated before we gained selection ownership |
516 | if (m_timestamp[mode] != XCB_CURRENT_TIME && event->time <= m_timestamp[mode]) |
517 | return; |
518 | |
519 | // DEBUG("QClipboard: new selection owner 0x%lx at time %lx (ours %lx)", |
520 | // XGetSelectionOwner(dpy, XA_PRIMARY), |
521 | // xevent->xselectionclear.time, d->timestamp); |
522 | |
523 | xcb_window_t newOwner = getSelectionOwner(event->selection); |
524 | |
525 | /* If selection ownership was given up voluntarily from QClipboard::clear(), then we do nothing here |
526 | since its already handled in setMimeData. Otherwise, the event must have come from another client |
527 | as a result of a call to xcb_set_selection_owner in which case we need to delete the local mime data |
528 | */ |
529 | if (newOwner != XCB_NONE) { |
530 | if (m_clientClipboard[QClipboard::Clipboard] != m_clientClipboard[QClipboard::Selection]) |
531 | delete m_clientClipboard[mode]; |
532 | m_clientClipboard[mode] = 0; |
533 | m_timestamp[mode] = XCB_CURRENT_TIME; |
534 | } |
535 | } |
536 | |
537 | void QXcbClipboard::handleSelectionRequest(xcb_selection_request_event_t *req) |
538 | { |
539 | if (requestor() && req->requestor == requestor()) { |
540 | qWarning("QXcbClipboard: Selection request should be caught before" ); |
541 | return; |
542 | } |
543 | |
544 | q_padded_xcb_event<xcb_selection_notify_event_t> event = {}; |
545 | event.response_type = XCB_SELECTION_NOTIFY; |
546 | event.requestor = req->requestor; |
547 | event.selection = req->selection; |
548 | event.target = req->target; |
549 | event.property = XCB_NONE; |
550 | event.time = req->time; |
551 | |
552 | QMimeData *d; |
553 | QClipboard::Mode mode = modeForAtom(req->selection); |
554 | if (mode > QClipboard::Selection) { |
555 | qWarning() << "QXcbClipboard: Unknown selection" << connection()->atomName(req->selection); |
556 | xcb_send_event(xcb_connection(), false, req->requestor, XCB_EVENT_MASK_NO_EVENT, (const char *)&event); |
557 | return; |
558 | } |
559 | |
560 | d = m_clientClipboard[mode]; |
561 | |
562 | if (!d) { |
563 | qWarning("QXcbClipboard: Cannot transfer data, no data available" ); |
564 | xcb_send_event(xcb_connection(), false, req->requestor, XCB_EVENT_MASK_NO_EVENT, (const char *)&event); |
565 | return; |
566 | } |
567 | |
568 | if (m_timestamp[mode] == XCB_CURRENT_TIME // we don't own the selection anymore |
569 | || (req->time != XCB_CURRENT_TIME && req->time < m_timestamp[mode])) { |
570 | qWarning("QXcbClipboard: SelectionRequest too old" ); |
571 | xcb_send_event(xcb_connection(), false, req->requestor, XCB_EVENT_MASK_NO_EVENT, (const char *)&event); |
572 | return; |
573 | } |
574 | |
575 | xcb_atom_t targetsAtom = atom(QXcbAtom::TARGETS); |
576 | xcb_atom_t multipleAtom = atom(QXcbAtom::MULTIPLE); |
577 | xcb_atom_t timestampAtom = atom(QXcbAtom::TIMESTAMP); |
578 | |
579 | struct AtomPair { xcb_atom_t target; xcb_atom_t property; } *multi = 0; |
580 | xcb_atom_t multi_type = XCB_NONE; |
581 | int multi_format = 0; |
582 | int nmulti = 0; |
583 | int imulti = -1; |
584 | bool multi_writeback = false; |
585 | |
586 | if (req->target == multipleAtom) { |
587 | QByteArray multi_data; |
588 | if (req->property == XCB_NONE |
589 | || !clipboardReadProperty(req->requestor, req->property, false, &multi_data, |
590 | 0, &multi_type, &multi_format) |
591 | || multi_format != 32) { |
592 | // MULTIPLE property not formatted correctly |
593 | xcb_send_event(xcb_connection(), false, req->requestor, XCB_EVENT_MASK_NO_EVENT, (const char *)&event); |
594 | return; |
595 | } |
596 | nmulti = multi_data.size()/sizeof(*multi); |
597 | multi = new AtomPair[nmulti]; |
598 | memcpy(multi,multi_data.data(),multi_data.size()); |
599 | imulti = 0; |
600 | } |
601 | |
602 | for (; imulti < nmulti; ++imulti) { |
603 | xcb_atom_t target; |
604 | xcb_atom_t property; |
605 | |
606 | if (multi) { |
607 | target = multi[imulti].target; |
608 | property = multi[imulti].property; |
609 | } else { |
610 | target = req->target; |
611 | property = req->property; |
612 | if (property == XCB_NONE) // obsolete client |
613 | property = target; |
614 | } |
615 | |
616 | xcb_atom_t ret = XCB_NONE; |
617 | if (target == XCB_NONE || property == XCB_NONE) { |
618 | ; |
619 | } else if (target == timestampAtom) { |
620 | if (m_timestamp[mode] != XCB_CURRENT_TIME) { |
621 | xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, req->requestor, |
622 | property, XCB_ATOM_INTEGER, 32, 1, &m_timestamp[mode]); |
623 | ret = property; |
624 | } else { |
625 | qWarning("QXcbClipboard: Invalid data timestamp" ); |
626 | } |
627 | } else if (target == targetsAtom) { |
628 | ret = sendTargetsSelection(d, req->requestor, property); |
629 | } else { |
630 | ret = sendSelection(d, target, req->requestor, property); |
631 | } |
632 | |
633 | if (nmulti > 0) { |
634 | if (ret == XCB_NONE) { |
635 | multi[imulti].property = XCB_NONE; |
636 | multi_writeback = true; |
637 | } |
638 | } else { |
639 | event.property = ret; |
640 | break; |
641 | } |
642 | } |
643 | |
644 | if (nmulti > 0) { |
645 | if (multi_writeback) { |
646 | // according to ICCCM 2.6.2 says to put None back |
647 | // into the original property on the requestor window |
648 | xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, req->requestor, req->property, |
649 | multi_type, 32, nmulti*2, (const void *)multi); |
650 | } |
651 | |
652 | delete [] multi; |
653 | event.property = req->property; |
654 | } |
655 | |
656 | // send selection notify to requestor |
657 | xcb_send_event(xcb_connection(), false, req->requestor, XCB_EVENT_MASK_NO_EVENT, (const char *)&event); |
658 | } |
659 | |
660 | void QXcbClipboard::handleXFixesSelectionRequest(xcb_xfixes_selection_notify_event_t *event) |
661 | { |
662 | QClipboard::Mode mode = modeForAtom(event->selection); |
663 | if (mode > QClipboard::Selection) |
664 | return; |
665 | |
666 | // Note1: Here we care only about the xfixes events that come from other processes. |
667 | // Note2: If the QClipboard::clear() is issued, event->owner is XCB_NONE, |
668 | // so we check selection_timestamp to not handle our own QClipboard::clear(). |
669 | if (event->owner != owner() && event->selection_timestamp > m_timestamp[mode]) { |
670 | if (!m_xClipboard[mode]) { |
671 | m_xClipboard[mode].reset(new QXcbClipboardMime(mode, this)); |
672 | } else { |
673 | m_xClipboard[mode]->reset(); |
674 | } |
675 | emitChanged(mode); |
676 | } else if (event->subtype == XCB_XFIXES_SELECTION_EVENT_SELECTION_CLIENT_CLOSE || |
677 | event->subtype == XCB_XFIXES_SELECTION_EVENT_SELECTION_WINDOW_DESTROY) |
678 | emitChanged(mode); |
679 | } |
680 | |
681 | bool QXcbClipboard::clipboardReadProperty(xcb_window_t win, xcb_atom_t property, bool deleteProperty, QByteArray *buffer, int *size, xcb_atom_t *type, int *format) |
682 | { |
683 | xcb_atom_t dummy_type; |
684 | int dummy_format; |
685 | |
686 | if (!type) // allow null args |
687 | type = &dummy_type; |
688 | if (!format) |
689 | format = &dummy_format; |
690 | |
691 | // Don't read anything, just get the size of the property data |
692 | auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, win, property, XCB_GET_PROPERTY_TYPE_ANY, 0, 0); |
693 | if (!reply || reply->type == XCB_NONE) { |
694 | buffer->resize(0); |
695 | return false; |
696 | } |
697 | *type = reply->type; |
698 | *format = reply->format; |
699 | |
700 | auto bytes_left = reply->bytes_after; |
701 | |
702 | int offset = 0, buffer_offset = 0; |
703 | |
704 | int newSize = bytes_left; |
705 | buffer->resize(newSize); |
706 | |
707 | bool ok = (buffer->size() == newSize); |
708 | |
709 | if (ok && newSize) { |
710 | // could allocate buffer |
711 | |
712 | while (bytes_left) { |
713 | // more to read... |
714 | |
715 | reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, win, property, |
716 | XCB_GET_PROPERTY_TYPE_ANY, offset, m_maxPropertyRequestDataBytes / 4); |
717 | if (!reply || reply->type == XCB_NONE) |
718 | break; |
719 | |
720 | *type = reply->type; |
721 | *format = reply->format; |
722 | bytes_left = reply->bytes_after; |
723 | char *data = (char *)xcb_get_property_value(reply.get()); |
724 | int length = xcb_get_property_value_length(reply.get()); |
725 | |
726 | // Here we check if we get a buffer overflow and tries to |
727 | // recover -- this shouldn't normally happen, but it doesn't |
728 | // hurt to be defensive |
729 | if ((int)(buffer_offset + length) > buffer->size()) { |
730 | qWarning("QXcbClipboard: buffer overflow" ); |
731 | length = buffer->size() - buffer_offset; |
732 | |
733 | // escape loop |
734 | bytes_left = 0; |
735 | } |
736 | |
737 | memcpy(buffer->data() + buffer_offset, data, length); |
738 | buffer_offset += length; |
739 | |
740 | if (bytes_left) { |
741 | // offset is specified in 32-bit multiples |
742 | offset += length / 4; |
743 | } |
744 | } |
745 | } |
746 | |
747 | |
748 | // correct size, not 0-term. |
749 | if (size) |
750 | *size = buffer_offset; |
751 | if (*type == atom(QXcbAtom::INCR)) |
752 | m_incr_receive_time = connection()->getTimestamp(); |
753 | if (deleteProperty) |
754 | xcb_delete_property(xcb_connection(), win, property); |
755 | |
756 | connection()->flush(); |
757 | |
758 | return ok; |
759 | } |
760 | |
761 | xcb_generic_event_t *QXcbClipboard::waitForClipboardEvent(xcb_window_t window, int type, bool checkManager) |
762 | { |
763 | QElapsedTimer timer; |
764 | timer.start(); |
765 | QXcbEventQueue *queue = connection()->eventQueue(); |
766 | do { |
767 | auto e = queue->peek([window, type](xcb_generic_event_t *event, int eventType) { |
768 | if (eventType != type) |
769 | return false; |
770 | if (eventType == XCB_PROPERTY_NOTIFY) { |
771 | auto propertyNotify = reinterpret_cast<xcb_property_notify_event_t *>(event); |
772 | return propertyNotify->window == window; |
773 | } |
774 | if (eventType == XCB_SELECTION_NOTIFY) { |
775 | auto selectionNotify = reinterpret_cast<xcb_selection_notify_event_t *>(event); |
776 | return selectionNotify->requestor == window; |
777 | } |
778 | return false; |
779 | }); |
780 | if (e) // found the waited for event |
781 | return e; |
782 | |
783 | if (checkManager) { |
784 | auto reply = Q_XCB_REPLY(xcb_get_selection_owner, xcb_connection(), atom(QXcbAtom::CLIPBOARD_MANAGER)); |
785 | if (!reply || reply->owner == XCB_NONE) |
786 | return nullptr; |
787 | } |
788 | |
789 | // process other clipboard events, since someone is probably requesting data from us |
790 | auto clipboardAtom = atom(QXcbAtom::CLIPBOARD); |
791 | e = queue->peek([clipboardAtom](xcb_generic_event_t *event, int type) { |
792 | xcb_atom_t selection = XCB_ATOM_NONE; |
793 | if (type == XCB_SELECTION_REQUEST) |
794 | selection = reinterpret_cast<xcb_selection_request_event_t *>(event)->selection; |
795 | else if (type == XCB_SELECTION_CLEAR) |
796 | selection = reinterpret_cast<xcb_selection_clear_event_t *>(event)->selection; |
797 | return selection == XCB_ATOM_PRIMARY || selection == clipboardAtom; |
798 | }); |
799 | if (e) { |
800 | connection()->handleXcbEvent(e); |
801 | free(e); |
802 | } |
803 | |
804 | connection()->flush(); |
805 | |
806 | const auto elapsed = timer.elapsed(); |
807 | if (elapsed < clipboard_timeout) |
808 | queue->waitForNewEvents(clipboard_timeout - elapsed); |
809 | } while (timer.elapsed() < clipboard_timeout); |
810 | |
811 | return nullptr; |
812 | } |
813 | |
814 | QByteArray QXcbClipboard::clipboardReadIncrementalProperty(xcb_window_t win, xcb_atom_t property, int nbytes, bool nullterm) |
815 | { |
816 | QByteArray buf; |
817 | QByteArray tmp_buf; |
818 | bool alloc_error = false; |
819 | int length; |
820 | int offset = 0; |
821 | xcb_timestamp_t prev_time = m_incr_receive_time; |
822 | |
823 | if (nbytes > 0) { |
824 | // Reserve buffer + zero-terminator (for text data) |
825 | // We want to complete the INCR transfer even if we cannot |
826 | // allocate more memory |
827 | buf.resize(nbytes+1); |
828 | alloc_error = buf.size() != nbytes+1; |
829 | } |
830 | |
831 | for (;;) { |
832 | connection()->flush(); |
833 | xcb_generic_event_t *ge = waitForClipboardEvent(win, XCB_PROPERTY_NOTIFY); |
834 | if (!ge) |
835 | break; |
836 | xcb_property_notify_event_t *event = (xcb_property_notify_event_t *)ge; |
837 | QScopedPointer<xcb_property_notify_event_t, QScopedPointerPodDeleter> guard(event); |
838 | |
839 | if (event->atom != property |
840 | || event->state != XCB_PROPERTY_NEW_VALUE |
841 | || event->time < prev_time) |
842 | continue; |
843 | prev_time = event->time; |
844 | |
845 | if (clipboardReadProperty(win, property, true, &tmp_buf, &length, 0, 0)) { |
846 | if (length == 0) { // no more data, we're done |
847 | if (nullterm) { |
848 | buf.resize(offset+1); |
849 | buf[offset] = '\0'; |
850 | } else { |
851 | buf.resize(offset); |
852 | } |
853 | return buf; |
854 | } else if (!alloc_error) { |
855 | if (offset+length > (int)buf.size()) { |
856 | buf.resize(offset+length+65535); |
857 | if (buf.size() != offset+length+65535) { |
858 | alloc_error = true; |
859 | length = buf.size() - offset; |
860 | } |
861 | } |
862 | memcpy(buf.data()+offset, tmp_buf.constData(), length); |
863 | tmp_buf.resize(0); |
864 | offset += length; |
865 | } |
866 | } else { |
867 | break; |
868 | } |
869 | } |
870 | |
871 | // timed out ... create a new requestor window, otherwise the requestor |
872 | // could consider next request to be still part of this timed out request |
873 | setRequestor(0); |
874 | |
875 | return QByteArray(); |
876 | } |
877 | |
878 | QByteArray QXcbClipboard::getDataInFormat(xcb_atom_t modeAtom, xcb_atom_t fmtAtom) |
879 | { |
880 | return getSelection(modeAtom, fmtAtom, atom(QXcbAtom::_QT_SELECTION)); |
881 | } |
882 | |
883 | QByteArray QXcbClipboard::getSelection(xcb_atom_t selection, xcb_atom_t target, xcb_atom_t property, xcb_timestamp_t time) |
884 | { |
885 | QByteArray buf; |
886 | xcb_window_t win = requestor(); |
887 | |
888 | if (time == 0) time = connection()->time(); |
889 | |
890 | xcb_delete_property(xcb_connection(), win, property); |
891 | xcb_convert_selection(xcb_connection(), win, selection, target, property, time); |
892 | |
893 | connection()->sync(); |
894 | |
895 | xcb_generic_event_t *ge = waitForClipboardEvent(win, XCB_SELECTION_NOTIFY); |
896 | bool no_selection = !ge || ((xcb_selection_notify_event_t *)ge)->property == XCB_NONE; |
897 | free(ge); |
898 | |
899 | if (no_selection) |
900 | return buf; |
901 | |
902 | xcb_atom_t type; |
903 | if (clipboardReadProperty(win, property, true, &buf, 0, &type, 0)) { |
904 | if (type == atom(QXcbAtom::INCR)) { |
905 | int nbytes = buf.size() >= 4 ? *((int*)buf.data()) : 0; |
906 | buf = clipboardReadIncrementalProperty(win, property, nbytes, false); |
907 | } |
908 | } |
909 | |
910 | return buf; |
911 | } |
912 | |
913 | #endif // QT_NO_CLIPBOARD |
914 | |
915 | QT_END_NAMESPACE |
916 | |
917 | #include "qxcbclipboard.moc" |
918 | |