From 987b215f084f92c33bff17a7dbc61b0b6b9f90bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Tue, 6 Aug 2024 14:29:39 +0200 Subject: [PATCH] macOS: Implement NSServicesMenuRequestor protocol for Writing Tools support The AI based Writing Tools in macOS 15 uses the NSServicesMenuRequestor protocol to interact with custom NSView. By implementing the protocol we also gain support for service menu items such as "Look Up in Dictionary" or "Make New Sticky Note". Note that we only support plain text for now, 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. The Writing Tools feature itself is only available on macOS 15.1. To trigger it a native context menu with edit actions has to be shown, meaning this will not work out of the box for Qt Widgets. For Qt Quick with popupType set to Popup.Native the menu item is added as expected. Task-number: QTBUG-126238 Change-Id: I2cd4aa9af8d613c7c67b3c19a70a23660dde2154 Reviewed-by: Timur Pocheptsov (cherry picked from commit 888429a734bf379ee59519b5d6047561df66c9a5) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/platforms/cocoa/qcocoahelpers.h | 1 + src/plugins/platforms/cocoa/qcocoahelpers.mm | 1 + src/plugins/platforms/cocoa/qnsview.mm | 3 + .../platforms/cocoa/qnsview_complextext.mm | 62 +++++++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.h b/src/plugins/platforms/cocoa/qcocoahelpers.h index c6862a9e659..0e5f8212e74 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.h +++ b/src/plugins/platforms/cocoa/qcocoahelpers.h @@ -43,6 +43,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcQpaClipboard) Q_DECLARE_LOGGING_CATEGORY(lcInputDevices) Q_DECLARE_LOGGING_CATEGORY(lcQpaDialogs) Q_DECLARE_LOGGING_CATEGORY(lcQpaMenus) +Q_DECLARE_LOGGING_CATEGORY(lcQpaServices) class QPixmap; class QString; diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.mm b/src/plugins/platforms/cocoa/qcocoahelpers.mm index 1eba88d5e33..67f2f9fb094 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.mm +++ b/src/plugins/platforms/cocoa/qcocoahelpers.mm @@ -30,6 +30,7 @@ Q_LOGGING_CATEGORY(lcQpaClipboard, "qt.qpa.clipboard") Q_LOGGING_CATEGORY(lcInputDevices, "qt.qpa.input.devices") Q_LOGGING_CATEGORY(lcQpaDialogs, "qt.qpa.dialogs") Q_LOGGING_CATEGORY(lcQpaMenus, "qt.qpa.menus") +Q_LOGGING_CATEGORY(lcQpaServices, "qt.qpa.services") // // Conversion Functions diff --git a/src/plugins/platforms/cocoa/qnsview.mm b/src/plugins/platforms/cocoa/qnsview.mm index eb998b04099..7289c66ea65 100644 --- a/src/plugins/platforms/cocoa/qnsview.mm +++ b/src/plugins/platforms/cocoa/qnsview.mm @@ -82,6 +82,9 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); @property (readonly) QObject* focusObject; @end +@interface QNSView (ServicesMenu) +@end + @interface QT_MANGLE_NAMESPACE(QNSViewMenuHelper) : NSObject - (instancetype)initWithView:(QNSView *)theView; @end diff --git a/src/plugins/platforms/cocoa/qnsview_complextext.mm b/src/plugins/platforms/cocoa/qnsview_complextext.mm index d7f8f4baf09..cb512d5893d 100644 --- a/src/plugins/platforms/cocoa/qnsview_complextext.mm +++ b/src/plugins/platforms/cocoa/qnsview_complextext.mm @@ -598,3 +598,65 @@ } @end + +@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. + +- (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; + } + 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; + } +} + +- (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() ]]; + } + } + return NO; +} + +- (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pasteboard +{ + NSString *insertedString = [pasteboard stringForType:NSPasteboardTypeString]; + if (!insertedString) + return NO; + + qCDebug(lcQpaServices) << "Reading" << insertedString << "from service pasteboard" << pasteboard.name; + [self insertText:insertedString replacementRange:{NSNotFound, 0}]; + return YES; +} + +@end + +