Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qmacclipboard.mm
Go to the documentation of this file.
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// Qt-Security score:significant reason:default
4
5#include <AppKit/AppKit.h>
6
8#include <QtGui/private/qmacmimeregistry_p.h>
9#include <QtGui/qutimimeconverter.h>
10#include <QtGui/qclipboard.h>
11#include <QtGui/qguiapplication.h>
12#include <QtGui/qbitmap.h>
13#include <QtCore/qdatetime.h>
14#include <QtCore/qmetatype.h>
15#include <QtCore/qmimedata.h>
16#include <QtCore/qdebug.h>
17#include <QtCore/private/qcore_mac_p.h>
18#include <QtGui/qguiapplication.h>
19#include <QtGui/qevent.h>
20#include <QtCore/qurl.h>
21#include <stdlib.h>
22#include <string.h>
23#include "qcocoahelpers.h"
24#include <type_traits>
25
26QT_BEGIN_NAMESPACE
27
28using namespace Qt::StringLiterals;
29
30/*****************************************************************************
31 QMacPasteboard code
32*****************************************************************************/
33
34namespace
35{
36OSStatus PasteboardGetItemCountSafe(PasteboardRef paste, ItemCount *cnt)
37{
38 Q_ASSERT(paste);
39 Q_ASSERT(cnt);
40 const OSStatus result = PasteboardGetItemCount(paste, cnt);
41 // Despite being declared unsigned, this API can return -1
42 if (std::make_signed<ItemCount>::type(*cnt) < 0)
43 *cnt = 0;
44 return result;
45}
46} // namespace
47
48// Ensure we don't call the broken one later on
49#define PasteboardGetItemCount
50
51class QMacMimeData : public QMimeData
52{
53public:
54 QVariant variantData(const QString &mime) { return retrieveData(mime, QMetaType()); }
55private:
56 QMacMimeData();
57};
58
59QMacPasteboard::Promise::Promise(int itemId, const QUtiMimeConverter *c, const QString &m, QMimeData *md, int o, DataRequestType drt)
60 : itemId(itemId), offset(o), converter(c), mime(m), dataRequestType(drt)
61{
62 // Request the data from the application immediately for eager requests.
63 if (dataRequestType == QMacPasteboard::EagerRequest) {
64 variantData = static_cast<QMacMimeData *>(md)->variantData(m);
65 isPixmap = variantData.metaType().id() == QMetaType::QPixmap;
66 mimeData = nullptr;
67 } else {
68 mimeData = md;
69 if (md->hasImage())
70 isPixmap = md->imageData().metaType().id() == QMetaType::QPixmap;
71 }
72}
73
74QMacPasteboard::QMacPasteboard(PasteboardRef p, QUtiMimeConverter::HandlerScope scope)
75 : scope(scope)
76{
77 mac_mime_source = false;
78 paste = p;
79 CFRetain(paste);
80 resolvingBeforeDestruction = false;
81}
82
83QMacPasteboard::QMacPasteboard(QUtiMimeConverter::HandlerScope scope)
84 : scope(scope)
85{
86 mac_mime_source = false;
87 paste = nullptr;
88 OSStatus err = PasteboardCreate(nullptr, &paste);
89 if (err == noErr)
90 PasteboardSetPromiseKeeper(paste, promiseKeeper, this);
91 else
92 qDebug("PasteBoard: Error creating pasteboard: [%d]", (int)err);
93 resolvingBeforeDestruction = false;
94}
95
96QMacPasteboard::QMacPasteboard(CFStringRef name, QUtiMimeConverter::HandlerScope scope)
97 : scope(scope)
98{
99 mac_mime_source = false;
100 paste = nullptr;
101 OSStatus err = PasteboardCreate(name, &paste);
102 if (err == noErr) {
103 PasteboardSetPromiseKeeper(paste, promiseKeeper, this);
104 } else {
105 qDebug("PasteBoard: Error creating pasteboard: %s [%d]", QString::fromCFString(name).toLatin1().constData(), (int)err);
106 }
107 resolvingBeforeDestruction = false;
108}
109
111{
112 /*
113 Commit all promises for paste when shutting down,
114 unless we are the stack-allocated clipboard used by QCocoaDrag.
115 */
116 if (scope == QUtiMimeConverter::HandlerScopeFlag::DnD)
117 resolvingBeforeDestruction = true;
118 PasteboardResolvePromises(paste);
119 if (paste)
120 CFRelease(paste);
121}
122
124{
125 return paste;
126}
127
128OSStatus QMacPasteboard::promiseKeeper(PasteboardRef paste, PasteboardItemID id,
129 CFStringRef uti, void *_qpaste)
130{
131 QMacPasteboard *qpaste = (QMacPasteboard*)_qpaste;
132 const long promise_id = (long)id;
133
134 // Find the kept promise
135 const QList<QUtiMimeConverter*> availableConverters = QMacMimeRegistry::all(QUtiMimeConverter::HandlerScopeFlag::All);
136 const QString utiAsQString = QString::fromCFString(uti);
137 QMacPasteboard::Promise promise;
138 for (int i = 0; i < qpaste->promises.size(); i++){
139 const QMacPasteboard::Promise tmp = qpaste->promises[i];
140 if (!availableConverters.contains(tmp.converter)) {
141 // promise.converter is a pointer initialized by the value found
142 // in QUtiMimeConverter's global list of QMacMimes.
143 // We add pointers to this list in QUtiMimeConverter's ctor;
144 // we remove these pointers in QUtiMimeConverter's dtor.
145 // If tmp.converter was not found in this list, we probably have a
146 // dangling pointer so let's skip it.
147 continue;
148 }
149
150 if (tmp.itemId == promise_id && tmp.converter->canConvert(tmp.mime, utiAsQString)) {
151 promise = tmp;
152 break;
153 }
154 }
155
156 if (!promise.itemId && utiAsQString == "com.trolltech.qt.MimeTypeName"_L1) {
157 // we have promised this data, but won't be able to convert, so return null data.
158 // This helps in making the application/x-qt-mime-type-name hidden from normal use.
159 QByteArray ba;
160 const QCFType<CFDataRef> data = CFDataCreate(nullptr, (UInt8*)ba.constData(), ba.size());
161 PasteboardPutItemFlavor(paste, id, uti, data, kPasteboardFlavorNoFlags);
162 return noErr;
163 }
164
165 if (!promise.itemId) {
166 // There was no promise that could deliver data for the
167 // given id and uti. This should not happen.
168 qDebug("Pasteboard: %d: Request for %ld, %s, but no promise found!", __LINE__, promise_id, qPrintable(utiAsQString));
169 return cantGetFlavorErr;
170 }
171
172 qCDebug(lcQpaClipboard, "PasteBoard: Calling in promise for %s[%ld] [%s] [%d]", qPrintable(promise.mime), promise_id,
173 qPrintable(utiAsQString), promise.offset);
174
175 // Get the promise data. If this is a "lazy" promise call variantData()
176 // to request the data from the application.
177 QVariant promiseData;
178 if (promise.dataRequestType == LazyRequest) {
179 if (!qpaste->resolvingBeforeDestruction && !promise.mimeData.isNull()) {
180 if (promise.isPixmap && !QGuiApplication::instance()) {
181 qCWarning(lcQpaClipboard,
182 "Cannot keep promise, data contains QPixmap and requires livining QGuiApplication");
183 return cantGetFlavorErr;
184 }
185 promiseData = static_cast<QMacMimeData *>(promise.mimeData.data())->variantData(promise.mime);
186 }
187 } else {
188 promiseData = promise.variantData;
189 }
190
191 const QList<QByteArray> md = promise.converter->convertFromMime(promise.mime, promiseData, utiAsQString);
192 if (md.size() <= promise.offset)
193 return cantGetFlavorErr;
194 const QByteArray &ba = md[promise.offset];
195 const QCFType<CFDataRef> data = CFDataCreate(nullptr, (UInt8*)ba.constData(), ba.size());
196 PasteboardPutItemFlavor(paste, id, uti, data, kPasteboardFlavorNoFlags);
197 return noErr;
198}
199
200bool QMacPasteboard::hasUti(const QString &uti) const
201{
202 if (!paste)
203 return false;
204
205 sync();
206
207 ItemCount cnt = 0;
208 if (PasteboardGetItemCountSafe(paste, &cnt) || !cnt)
209 return false;
210
211 qCDebug(lcQpaClipboard, "PasteBoard: hasUti [%s]", qPrintable(uti));
212 const QCFString c_uti(uti);
213 for (uint index = 1; index <= cnt; ++index) {
214
215 PasteboardItemID id;
216 if (PasteboardGetItemIdentifier(paste, index, &id) != noErr)
217 return false;
218
219 PasteboardFlavorFlags flags;
220 if (PasteboardGetItemFlavorFlags(paste, id, c_uti, &flags) == noErr) {
221 qCDebug(lcQpaClipboard, " - Found!");
222 return true;
223 }
224 }
225 qCDebug(lcQpaClipboard, " - NotFound!");
226 return false;
227}
228
230{
231 const QMacPasteboard *paste;
232public:
235 QStringList formats() const override { return paste->formats(); }
236 QVariant retrieveData(const QString &format, QMetaType) const override
237 {
238 return paste->retrieveData(format);
239 }
240};
241
243{
244 if (!mime) {
245 mac_mime_source = true;
246 mime = new QMacPasteboardMimeSource(this);
247
248 }
249 return mime;
250}
251
252void QMacPasteboard::setMimeData(QMimeData *mime_src, DataRequestType dataRequestType)
253{
254 if (!paste)
255 return;
256
257 if (mime == mime_src || (!mime_src && mime && mac_mime_source))
258 return;
259 mac_mime_source = false;
260 delete mime;
261 mime = mime_src;
262
263 const QList<QUtiMimeConverter*> availableConverters = QMacMimeRegistry::all(scope);
264 if (mime != nullptr) {
265 clear_helper();
266 QStringList formats = mime_src->formats();
267
268 // QMimeData sub classes reimplementing the formats() might not expose the
269 // temporary "application/x-qt-mime-type-name" mimetype. So check the existence
270 // of this mime type while doing drag and drop.
271 QString dummyMimeType("application/x-qt-mime-type-name"_L1);
272 if (!formats.contains(dummyMimeType)) {
273 QByteArray dummyType = mime_src->data(dummyMimeType);
274 if (!dummyType.isEmpty())
275 formats.append(dummyMimeType);
276 }
277 for (const auto &mimeType : formats) {
278 for (const auto *c : availableConverters) {
279 // Hack: The Rtf handler converts incoming Rtf to Html. We do
280 // not want to convert outgoing Html to Rtf but instead keep
281 // posting it as Html. Skip the Rtf handler here.
282 if (c->utiForMime("text/html"_L1) == "public.rtf"_L1)
283 continue;
284 const QString uti(c->utiForMime(mimeType));
285 if (!uti.isEmpty()) {
286
287 const int numItems = c->count(mime_src);
288 for (int item = 0; item < numItems; ++item) {
289 const NSInteger itemID = item + 1; //id starts at 1
290 //QMacPasteboard::Promise promise = (dataRequestType == QMacPasteboard::EagerRequest) ?
291 // QMacPasteboard::Promise::eagerPromise(itemID, c, mimeType, mimeData, item) :
292 // QMacPasteboard::Promise::lazyPromise(itemID, c, mimeType, mimeData, item);
293
294 const QMacPasteboard::Promise promise(itemID, c, mimeType, mime_src, item, dataRequestType);
295 promises.append(promise);
296 PasteboardPutItemFlavor(paste, reinterpret_cast<PasteboardItemID>(itemID), QCFString(uti), 0, kPasteboardFlavorNoFlags);
297 qCDebug(lcQpaClipboard, " - adding %ld %s [%s] [%d]",
298 itemID, qPrintable(mimeType), qPrintable(uti), item);
299 }
300 }
301 }
302 }
303 }
304}
305
307{
308 if (!paste)
309 return QStringList();
310
311 sync();
312
313 QStringList ret;
314 ItemCount cnt = 0;
315 if (PasteboardGetItemCountSafe(paste, &cnt) || !cnt)
316 return ret;
317
318 qCDebug(lcQpaClipboard, "PasteBoard: Formats [%d]", (int)cnt);
319 for (uint index = 1; index <= cnt; ++index) {
320
321 PasteboardItemID id;
322 if (PasteboardGetItemIdentifier(paste, index, &id) != noErr)
323 continue;
324
325 QCFType<CFArrayRef> types;
326 if (PasteboardCopyItemFlavors(paste, id, &types ) != noErr)
327 continue;
328
329 const int type_count = CFArrayGetCount(types);
330 for (int i = 0; i < type_count; ++i) {
331 const QString uti = QString::fromCFString((CFStringRef)CFArrayGetValueAtIndex(types, i));
332 qCDebug(lcQpaClipboard, " -%s", qPrintable(QString(uti)));
333 const QString mimeType = QMacMimeRegistry::flavorToMime(scope, uti);
334 if (!mimeType.isEmpty() && !ret.contains(mimeType)) {
335 qCDebug(lcQpaClipboard, " -<%lld> %s [%s]", ret.size(), qPrintable(mimeType), qPrintable(QString(uti)));
336 ret << mimeType;
337 }
338 }
339 }
340 return ret;
341}
342
343bool QMacPasteboard::hasFormat(const QString &format) const
344{
345 if (!paste)
346 return false;
347
348 sync();
349
350 ItemCount cnt = 0;
351 if (PasteboardGetItemCountSafe(paste, &cnt) || !cnt)
352 return false;
353
354 qCDebug(lcQpaClipboard, "PasteBoard: hasFormat [%s]", qPrintable(format));
355 for (uint index = 1; index <= cnt; ++index) {
356
357 PasteboardItemID id;
358 if (PasteboardGetItemIdentifier(paste, index, &id) != noErr)
359 continue;
360
361 QCFType<CFArrayRef> types;
362 if (PasteboardCopyItemFlavors(paste, id, &types ) != noErr)
363 continue;
364
365 const int type_count = CFArrayGetCount(types);
366 for (int i = 0; i < type_count; ++i) {
367 const QString uti = QString::fromCFString((CFStringRef)CFArrayGetValueAtIndex(types, i));
368 qCDebug(lcQpaClipboard, " -%s [0x%x]", qPrintable(uti), uchar(scope));
369 QString mimeType = QMacMimeRegistry::flavorToMime(scope, uti);
370 if (!mimeType.isEmpty())
371 qCDebug(lcQpaClipboard, " - %s", qPrintable(mimeType));
372 if (mimeType == format)
373 return true;
374 }
375 }
376 return false;
377}
378
379QVariant QMacPasteboard::retrieveData(const QString &format) const
380{
381 if (!paste)
382 return QVariant();
383
384 sync();
385
386 ItemCount cnt = 0;
387 if (PasteboardGetItemCountSafe(paste, &cnt) || !cnt)
388 return QByteArray();
389
390 qCDebug(lcQpaClipboard, "Pasteboard: retrieveData [%s]", qPrintable(format));
391 const QList<QUtiMimeConverter *> availableConverters = QMacMimeRegistry::all(scope);
392 for (const auto *c : availableConverters) {
393 const QString c_uti = c->utiForMime(format);
394 if (!c_uti.isEmpty()) {
395 // Converting via PasteboardCopyItemFlavorData below will for some UITs result
396 // in newlines mapping to '\r' instead of '\n'. To work around this we shortcut
397 // the conversion via NSPasteboard's NSStringPboardType if possible.
398 if (c_uti == "com.apple.traditional-mac-plain-text"_L1
399 || c_uti == "public.utf8-plain-text"_L1
400 || c_uti == "public.utf16-plain-text"_L1) {
401 const QString str = qt_mac_get_pasteboardString(paste);
402 if (!str.isEmpty())
403 return str;
404 }
405
406 QVariant ret;
407 QList<QByteArray> retList;
408 for (uint index = 1; index <= cnt; ++index) {
409 PasteboardItemID id;
410 if (PasteboardGetItemIdentifier(paste, index, &id) != noErr)
411 continue;
412
413 QCFType<CFArrayRef> types;
414 if (PasteboardCopyItemFlavors(paste, id, &types ) != noErr)
415 continue;
416
417 const int type_count = CFArrayGetCount(types);
418 for (int i = 0; i < type_count; ++i) {
419 const CFStringRef uti = static_cast<CFStringRef>(CFArrayGetValueAtIndex(types, i));
420 if (c_uti == QString::fromCFString(uti)) {
421 QCFType<CFDataRef> macBuffer;
422 if (PasteboardCopyItemFlavorData(paste, id, uti, &macBuffer) == noErr) {
423 QByteArray buffer((const char *)CFDataGetBytePtr(macBuffer),
424 CFDataGetLength(macBuffer));
425 if (!buffer.isEmpty()) {
426 qCDebug(lcQpaClipboard, " - %s [%s]", qPrintable(format),
427 qPrintable(c_uti));
428 buffer.detach(); //detach since we release the macBuffer
429 retList.append(buffer);
430 break; //skip to next element
431 }
432 }
433 } else {
434 qCDebug(lcQpaClipboard, " - NoMatch %s [%s]", qPrintable(c_uti),
435 qPrintable(QString::fromCFString(uti)));
436 }
437 }
438 }
439
440 if (!retList.isEmpty()) {
441 ret = c->convertToMime(format, retList, c_uti);
442 return ret;
443 }
444 }
445 }
446 return QVariant();
447}
448
449void QMacPasteboard::clear_helper()
450{
451 if (paste)
452 PasteboardClear(paste);
453 promises.clear();
454}
455
457{
458 qCDebug(lcQpaClipboard, "PasteBoard: clear!");
459 clear_helper();
460}
461
462bool QMacPasteboard::sync() const
463{
464 if (!paste)
465 return false;
466 const bool fromGlobal = PasteboardSynchronize(paste) & kPasteboardModified;
467
468 if (fromGlobal)
469 const_cast<QMacPasteboard *>(this)->setMimeData(nullptr);
470
471 if (fromGlobal)
472 qCDebug(lcQpaClipboard, "Pasteboard: Synchronize!");
473 return fromGlobal;
474}
475
476
478{
479 QMacAutoReleasePool pool;
480 NSPasteboard *pb = nil;
481 CFStringRef pbname;
482 if (PasteboardCopyName(paste, &pbname) == noErr) {
483 pb = [NSPasteboard pasteboardWithName:const_cast<NSString *>(reinterpret_cast<const NSString *>(pbname))];
484 CFRelease(pbname);
485 } else {
486 pb = [NSPasteboard generalPasteboard];
487 }
488 if (pb) {
489 NSString *text = [pb stringForType:NSPasteboardTypeString];
490 if (text)
491 return QString::fromNSString(text);
492 }
493 return QString();
494}
495
496QT_END_NAMESPACE
QVariant variantData(const QString &mime)
QVariant retrieveData(const QString &format, QMetaType) const override
Returns a variant with the given type containing data for the MIME type specified by mimeType.
QMacPasteboardMimeSource(const QMacPasteboard *p)
QStringList formats() const override
Returns a list of formats supported by the object.
QMacPasteboard(QUtiMimeConverter::HandlerScope scope)
QStringList formats() const
PasteboardRef pasteBoard() const
QMimeData * mimeData() const
QMacPasteboard(PasteboardRef p, QUtiMimeConverter::HandlerScope scope=QUtiMimeConverter::HandlerScopeFlag::All)
bool sync() const
bool hasUti(const QString &uti) const
QVariant retrieveData(const QString &format) const
bool hasFormat(const QString &format) const
QString qt_mac_get_pasteboardString(PasteboardRef paste)