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