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