diff --git a/src/gui/kernel/qevent.cpp b/src/gui/kernel/qevent.cpp index 762522daa0f..af3ad64689d 100644 --- a/src/gui/kernel/qevent.cpp +++ b/src/gui/kernel/qevent.cpp @@ -2250,6 +2250,11 @@ QContextMenuEvent::QContextMenuEvent(Reason reason, const QPoint &pos) variable can be used to set a selection starting from that point. The value is unused. + \value MimeData + If set, the variant contains a QMimeData object representing the + committed text. The commitString() still provides the plain text + representation of the committed text. + \sa Attribute */ diff --git a/src/gui/kernel/qevent.h b/src/gui/kernel/qevent.h index 2374e527e3e..2aa7de1e245 100644 --- a/src/gui/kernel/qevent.h +++ b/src/gui/kernel/qevent.h @@ -633,7 +633,8 @@ public: Cursor, Language, Ruby, - Selection + Selection, + MimeData }; class Attribute { public: diff --git a/src/gui/text/qinputcontrol.cpp b/src/gui/text/qinputcontrol.cpp index 4edd46ff0e7..d5f5290527b 100644 --- a/src/gui/text/qinputcontrol.cpp +++ b/src/gui/text/qinputcontrol.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qinputcontrol_p.h" + +#include #include QT_BEGIN_NAMESPACE @@ -103,6 +105,39 @@ bool QInputControl::isCommonTextEditShortcut(const QKeyEvent *ke) return false; } +/*! + \internal + + Creates a wrapper for returning QMimeData in response to + Qt::ImCurrentSelection, while being backwards compatible + with clients who only read the plain text string. +*/ +QVariant QInputControl::selectionWrapper(QMimeData *mimeData) +{ + struct MimeDataSelection + { + QMimeData *mimeData = nullptr; + operator QMimeData*() const { return mimeData; } + operator QString() const { return mimeData->text(); } + }; + + static bool registeredConversions = []{ + return QMetaType::registerConverter() + && QMetaType::registerConverter(); + }(); + Q_ASSERT(registeredConversions); + return QVariant::fromValue(MimeDataSelection{mimeData}); +} + +QMimeData *QInputControl::mimeDataForInputEvent(QInputMethodEvent *event) +{ + const auto &attributes = event->attributes(); + auto mimeDataAttr = std::find_if(attributes.begin(), attributes.end(), + [](auto a) { return a.type == QInputMethodEvent::MimeData; }); + return mimeDataAttr != event->attributes().end() ? + mimeDataAttr->value.value() : nullptr; +} + QT_END_NAMESPACE #include "moc_qinputcontrol_p.cpp" diff --git a/src/gui/text/qinputcontrol_p.h b/src/gui/text/qinputcontrol_p.h index fec73e1987b..bf7ed013874 100644 --- a/src/gui/text/qinputcontrol_p.h +++ b/src/gui/text/qinputcontrol_p.h @@ -22,6 +22,8 @@ QT_BEGIN_NAMESPACE class QKeyEvent; +class QMimeData; +class QInputMethodEvent; class Q_GUI_EXPORT QInputControl : public QObject { Q_OBJECT @@ -36,6 +38,9 @@ public: bool isAcceptableInput(const QKeyEvent *event) const; static bool isCommonTextEditShortcut(const QKeyEvent *ke); + static QVariant selectionWrapper(QMimeData *mimeData); + static QMimeData *mimeDataForInputEvent(QInputMethodEvent *event); + protected: explicit QInputControl(Type type, QObjectPrivate &dd, QObject *parent = nullptr); diff --git a/src/plugins/platforms/cocoa/qnsview_complextext.mm b/src/plugins/platforms/cocoa/qnsview_complextext.mm index 6060e5219df..2c53647a7f3 100644 --- a/src/plugins/platforms/cocoa/qnsview_complextext.mm +++ b/src/plugins/platforms/cocoa/qnsview_complextext.mm @@ -652,60 +652,149 @@ @implementation QNSView (ServicesMenu) -// Support for reading and writing from service menu pasteboards, which is also -// how the writing tools interact with custom NSView. Note that we only support -// plain text, which means that a rich text selection will lose all its styling -// when fed through a service that changes the text. To support rich text we -// need IM plumbing that operates on QMimeData. +// Support for reading and writing from service menu pasteboards. If the text +// input client supports returning the selection as a QMimeData we can convert +// that to rich text. Otherwise we fall back to plain text, which means that we +// lose any styling the selection might have when fed through a service that +// changes the text. - (id)validRequestorForSendType:(NSPasteboardType)sendType returnType:(NSPasteboardType)returnType { - bool canWriteToPasteboard = [&]{ - if (![sendType isEqualToString:NSPasteboardTypeString]) - return false; - if (auto queryResult = queryInputMethod(self.focusObject, Qt::ImCurrentSelection)) { - auto selectedText = queryResult.value(Qt::ImCurrentSelection).toString(); - if (!selectedText.isEmpty()) - return true; + if (auto queryResult = queryInputMethod(self.focusObject, Qt::ImReadOnly | Qt::ImCurrentSelection)) { + bool canWriteToPasteboard = false; + bool canReadFromPastboard = false; + + auto currentSelection = queryResult.value(Qt::ImCurrentSelection); + if (auto *mimeData = currentSelection.value()) { + // If the client reports the selection as mime-data we assume + // it can also insert mime-data via QInputMethodEvent::MimeData + auto scope = QUtiMimeConverter::HandlerScopeFlag::Clipboard; + auto availableConverters = QMacMimeRegistry::all(scope); + auto sendUti = [self utiForPasteboardType:sendType]; + auto returnUti = [self utiForPasteboardType:returnType]; + const auto mimeFormats = mimeData->formats(); + for (const auto *c : availableConverters) { + if (mimeFormats.contains(c->mimeForUti(sendUti))) + canWriteToPasteboard = true; + if (mimeFormats.contains(c->mimeForUti(returnUti))) + canReadFromPastboard = true; + if (canWriteToPasteboard && canReadFromPastboard) + break; // No need to continue looking + } + } else { + canWriteToPasteboard = [sendType isEqualToString:NSPasteboardTypeString] + && !currentSelection.toString().isEmpty(); + canReadFromPastboard = [returnType isEqualToString:NSPasteboardTypeString] + && !queryResult.value(Qt::ImReadOnly).toBool(); } - return false; - }(); - bool canReadFromPastboard = [returnType isEqualToString:NSPasteboardTypeString]; - - if ((sendType && !canWriteToPasteboard) || (returnType && !canReadFromPastboard)) { - return [super validRequestorForSendType:sendType returnType:returnType]; - } else { - qCDebug(lcQpaServices) << "Accepting service interaction for send" << sendType << "and receive" << returnType; - return self; + if (!((sendType && !canWriteToPasteboard) || (returnType && !canReadFromPastboard))) { + qCDebug(lcQpaServices) << "Accepting service interaction for send" << sendType << "and receive" << returnType; + return self; + } } + + return [super validRequestorForSendType:sendType returnType:returnType]; } - (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pasteboard types:(NSArray *)types { - if ([types containsObject:NSPasteboardTypeString] - // Check for the deprecated NSStringPboardType as well, as even if we - // claim to only support NSPasteboardTypeString, we get callbacks for - // the deprecated type. - || QT_IGNORE_DEPRECATIONS([types containsObject:NSStringPboardType])) { - if (auto queryResult = queryInputMethod(self.focusObject, Qt::ImCurrentSelection)) { - auto selectedText = queryResult.value(Qt::ImCurrentSelection).toString(); - qCDebug(lcQpaServices) << "Writing" << selectedText << "to service pasteboard" << pasteboard.name; - return [pasteboard writeObjects:@[ selectedText.toNSString() ]]; + bool didWrite = false; + + if (auto queryResult = queryInputMethod(self.focusObject, Qt::ImCurrentSelection)) { + auto currentSelection = queryResult.value(Qt::ImCurrentSelection); + if (auto *mimeData = currentSelection.value()) { + auto mimeFormats = mimeData->formats(); + auto scope = QUtiMimeConverter::HandlerScopeFlag::Clipboard; + auto availableConverters = QMacMimeRegistry::all(scope); + for (NSPasteboardType type in types) { + auto uti = [self utiForPasteboardType:type]; + if (uti.isEmpty()) { + qCWarning(lcQpaServices) << "Did not find UTI for type" << type; + continue; + } + for (const auto *converter : availableConverters) { + auto mime = converter->mimeForUti(uti); + if (mimeFormats.contains(mime)) { + auto utiDataList = converter->convertFromMime(mime, + mimeData->data(mime), uti); + if (utiDataList.isEmpty()) + continue; + auto utiData = utiDataList.first(); + qCDebug(lcQpaServices) << "Writing" << utiData << "to service pasteboard" + << "with UTI" << uti << "for type" << type << "based on mime" << mime; + didWrite |= [pasteboard setData:utiData.toNSData() forType:type]; + break; + } + } + } + } + + // Try plain text fallback if we didn't have QMimeData, or didn't write anything + if (!didWrite && ([types containsObject:NSPasteboardTypeString] + || QT_IGNORE_DEPRECATIONS([types containsObject:NSStringPboardType]))) { + auto selectedText = currentSelection.toString(); + qCDebug(lcQpaServices) << "Writing" << selectedText << "to service pasteboard" + << "as pain text" << "for type" << NSPasteboardTypeString; + didWrite |= [pasteboard writeObjects:@[ selectedText.toNSString() ]]; } } - return NO; + + return didWrite; } - (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pasteboard { - NSString *insertedString = [pasteboard stringForType:NSPasteboardTypeString]; - if (!insertedString) - return NO; + if (queryInputMethod(self.focusObject)) { + auto scope = QUtiMimeConverter::HandlerScopeFlag::Clipboard; + QMacPasteboard macPasteboard(CFStringRef(pasteboard.name), scope); + auto *mimeData = macPasteboard.mimeData(); + if (mimeData->formats().isEmpty()) { + qCWarning(lcQpaServices) << "Failed to resolve mime data from" << pasteboard.types; + return NO; + } - qCDebug(lcQpaServices) << "Reading" << insertedString << "from service pasteboard" << pasteboard.name; - [self insertText:insertedString replacementRange:{NSNotFound, 0}]; - return YES; + qCDebug(lcQpaServices) << "Replacing selected range" << [self selectedRange] + << "with mime data" << [&]() { + QMap formatMap; + for (const auto &format : mimeData->formats()) + formatMap.insert(format, mimeData->data(format)); + return formatMap; + }() << "from service pasteboard" << pasteboard.name; + + QList attributes; + attributes << QInputMethodEvent::Attribute( + QInputMethodEvent::MimeData, + 0, 0, QVariant::fromValue(mimeData)); + + QInputMethodEvent inputMethodEvent(QString(), attributes); + // Pass the plain text data as the commit string, for clients + // that don't know how to handle the new MimeData attribute. + // This also ensures that we clear the existing selected text. + inputMethodEvent.setCommitString(mimeData->text()); + QCoreApplication::sendEvent(self.focusObject, &inputMethodEvent); + return YES; + } else { + return NO; + } +} + +- (QString)utiForPasteboardType:(NSPasteboardType)pasteboardType +{ + if (!pasteboardType) + return QString(); + + UTType *uttype = [UTType typeWithIdentifier:pasteboardType]; + if (!uttype) { + // Although NSPasteboard types are declared as obsolete + // we still get callbacks for these types. As these types + // are not UTIs, we need to resolve the underlying UTI + // ourselves. + uttype = [UTType typeWithTag:pasteboardType + tagClass:QT_IGNORE_DEPRECATIONS((NSString*)kUTTagClassNSPboardType) + conformingToType:nil]; + } + return QString::fromNSString(uttype.identifier); } @end diff --git a/src/widgets/widgets/qwidgettextcontrol.cpp b/src/widgets/widgets/qwidgettextcontrol.cpp index 710540c6f29..ebe99b90bb9 100644 --- a/src/widgets/widgets/qwidgettextcontrol.cpp +++ b/src/widgets/widgets/qwidgettextcontrol.cpp @@ -2062,12 +2062,17 @@ void QWidgetTextControlPrivate::inputMethodEvent(QInputMethodEvent *e) // insert commit string if (!e->commitString().isEmpty() || e->replacementLength()) { - if (e->commitString().endsWith(QChar::LineFeed)) - block = cursor.block(); // Remember the block where the preedit text is - QTextCursor c = cursor; - c.setPosition(c.position() + e->replacementStart()); - c.setPosition(c.position() + e->replacementLength(), QTextCursor::KeepAnchor); - c.insertText(e->commitString()); + auto *mimeData = QInputControl::mimeDataForInputEvent(e); + if (mimeData && q->canInsertFromMimeData(mimeData)) { + q->insertFromMimeData(mimeData); + } else { + if (e->commitString().endsWith(QChar::LineFeed)) + block = cursor.block(); // Remember the block where the preedit text is + QTextCursor c = cursor; + c.setPosition(c.position() + e->replacementStart()); + c.setPosition(c.position() + e->replacementLength(), QTextCursor::KeepAnchor); + c.insertText(e->commitString()); + } } for (int i = 0; i < e->attributes().size(); ++i) { @@ -2181,8 +2186,11 @@ QVariant QWidgetTextControl::inputMethodQuery(Qt::InputMethodQuery property, QVa return QVariant(d->cursor.position() - block.position()); } case Qt::ImSurroundingText: return QVariant(block.text()); - case Qt::ImCurrentSelection: - return QVariant(d->cursor.selectedText()); + case Qt::ImCurrentSelection: { + QMimeData *mimeData = createMimeDataFromSelection(); + mimeData->deleteLater(); + return QInputControl::selectionWrapper(mimeData); + } case Qt::ImMaximumTextLength: return QVariant(); // No limit. case Qt::ImAnchorPosition: