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 <timur.pocheptsov@qt.io>
(cherry picked from commit 888429a734bf379ee59519b5d6047561df66c9a5)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Tor Arne Vestbø 2024-08-06 14:29:39 +02:00 committed by Qt Cherry-pick Bot
parent ece128fc5e
commit 987b215f08
4 changed files with 67 additions and 0 deletions

View File

@ -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;

View File

@ -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

View File

@ -82,6 +82,9 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper);
@property (readonly) QObject* focusObject;
@end
@interface QNSView (ServicesMenu) <NSServicesMenuRequestor>
@end
@interface QT_MANGLE_NAMESPACE(QNSViewMenuHelper) : NSObject
- (instancetype)initWithView:(QNSView *)theView;
@end

View File

@ -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<NSPasteboardType> *)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