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