diff --git a/src/plugins/platforms/cocoa/qnsview.mm b/src/plugins/platforms/cocoa/qnsview.mm index bd32d729a5..1d59a0c03e 100644 --- a/src/plugins/platforms/cocoa/qnsview.mm +++ b/src/plugins/platforms/cocoa/qnsview.mm @@ -36,13 +36,6 @@ #include "qcocoaintegration.h" #include -// Private interface -@interface QNSView () -- (BOOL)isTransparentForUserInput; -@property (assign) NSView* previousSuperview; -@property (assign) NSWindow* previousWindow; -@end - @interface QNSView (Drawing) - (void)initDrawing; @end @@ -86,6 +79,19 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); @property (readonly) QObject* focusObject; @end +@interface QT_MANGLE_NAMESPACE(QNSViewMenuHelper) : NSObject +- (instancetype)initWithView:(QNSView *)theView; +@end +QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMenuHelper); + +// Private interface +@interface QNSView () +- (BOOL)isTransparentForUserInput; +@property (assign) NSView* previousSuperview; +@property (assign) NSWindow* previousWindow; +@property (retain) QNSViewMenuHelper* menuHelper; +@end + @implementation QNSView { QPointer m_platformWindow; @@ -140,6 +146,8 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); m_sendKeyEvent = false; m_currentlyInterpretedKeyEvent = nil; m_lastSeenContext = NSDraggingContextWithinApplication; + + self.menuHelper = [[[QNSViewMenuHelper alloc] initWithView:self] autorelease]; } return self; } diff --git a/src/plugins/platforms/cocoa/qnsview_menus.mm b/src/plugins/platforms/cocoa/qnsview_menus.mm index 644f2139de..2840936975 100644 --- a/src/plugins/platforms/cocoa/qnsview_menus.mm +++ b/src/plugins/platforms/cocoa/qnsview_menus.mm @@ -9,23 +9,55 @@ #include "qcocoamenu.h" #include "qcocoamenubar.h" -static bool selectorIsCutCopyPaste(SEL selector) +@implementation QNSView (Menus) + +// Qt does not (yet) have a mechanism for propagating generic actions, +// so we can only support actions that originate from a QCocoaNSMenuItem, +// where we can forward the action by emitting QPlatformMenuItem::activated(). +// But waiting for forwardInvocation to check that the sender is a +// QCocoaNSMenuItem is too late, as AppKit has at that point chosen +// our view as the target for the action, and if we can't handle it +// the action will not propagate up the responder chain as it should. +// Instead, we hook in early in the process of determining the target +// via the supplementalTargetForAction API, and if we can support the +// action we forward it to a helper. The helper must be tied to the +// view, as the menu validation logic depends on the view's state. + +- (id)supplementalTargetForAction:(SEL)action sender:(id)sender { - return (selector == @selector(cut:) - || selector == @selector(copy:) - || selector == @selector(paste:) - || selector == @selector(selectAll:)); + qCDebug(lcQpaMenus) << "Resolving action target for" << action << "from" << sender << "via" << self; + + if (qt_objc_cast(sender)) { + // The supplemental target must support the selector, but we + // determine so dynamically, so check here before continuing. + if ([self.menuHelper respondsToSelector:action]) + return self.menuHelper; + } else { + qCDebug(lcQpaMenus) << "Ignoring action for menu item we didn't create"; + } + + return [super supplementalTargetForAction:action sender:sender]; } -@interface QNSView (Menus) -- (void)qt_itemFired:(QCocoaNSMenuItem *)item; @end -@implementation QNSView (Menus) +@interface QNSViewMenuHelper () +@property (assign) QNSView* view; +@end + +@implementation QNSViewMenuHelper + +- (instancetype)initWithView:(QNSView *)theView +{ + if ((self = [super init])) + self.view = theView; + + return self; +} - (BOOL)validateMenuItem:(NSMenuItem*)item { - qCDebug(lcQpaMenus) << "Validating" << item << "for" << self; + qCDebug(lcQpaMenus) << "Validating" << item << "for" << self.view; auto *nativeItem = qt_objc_cast(item); if (!nativeItem) @@ -53,7 +85,7 @@ static bool selectorIsCutCopyPaste(SEL selector) } if ((!menuWindow || menuWindow->window() != QGuiApplication::modalWindow()) - && (!menubar || menubar->cocoaWindow() != self.platformWindow)) + && (!menubar || menubar->cocoaWindow() != self.view.platformWindow)) return NO; } @@ -62,54 +94,42 @@ static bool selectorIsCutCopyPaste(SEL selector) - (BOOL)respondsToSelector:(SEL)selector { - // Not exactly true. Both copy: and selectAll: can work on non key views. - if (selectorIsCutCopyPaste(selector)) - return ([NSApp keyWindow] == self.window) && (self.window.firstResponder == self); + // See QCocoaMenuItem::resolveTargetAction() + + if (selector == @selector(cut:) + || selector == @selector(copy:) + || selector == @selector(paste:) + || selector == @selector(selectAll:)) { + // Not exactly true. Both copy: and selectAll: can work on non key views. + return NSApp.keyWindow == self.view.window + && self.view.window.firstResponder == self.view; + } + + if (selector == @selector(qt_itemFired:)) + return YES; return [super respondsToSelector:selector]; } -- (void)qt_itemFired:(QCocoaNSMenuItem *)item -{ - auto *appDelegate = [QCocoaApplicationDelegate sharedDelegate]; - [appDelegate qt_itemFired:item]; -} - - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { - if (selectorIsCutCopyPaste(selector)) { - NSMethodSignature *itemFiredSign = [super methodSignatureForSelector:@selector(qt_itemFired:)]; - return itemFiredSign; - } + // Double check, in case something has cached that we respond + // to the selector, but the result has changed since then. + if (![self respondsToSelector:selector]) + return nil; - return [super methodSignatureForSelector:selector]; + auto *appDelegate = [QCocoaApplicationDelegate sharedDelegate]; + return [appDelegate methodSignatureForSelector:@selector(qt_itemFired:)]; } - (void)forwardInvocation:(NSInvocation *)invocation { - if (selectorIsCutCopyPaste(invocation.selector)) { - NSObject *sender; - [invocation getArgument:&sender atIndex:2]; - qCDebug(lcQpaMenus) << "Forwarding" << invocation.selector << "from" << sender; - - // We claim to respond to standard edit actions such as cut/copy/paste, - // but these might not be exclusively coming from menu items that we - // control. For example, when embedded into a native UI (as a plugin), - // the menu items might be part of the host application, and if we're - // the first responder, we'll be the target of these actions. As we - // don't have a mechanism in Qt to trigger generic actions, we have - // to bail out if we don't have a QCocoaNSMenuItem we can activate(). - // Note that we skip the call to super as well, as that would just - // try to invoke the current action on ourselves again. - if (auto *nativeItem = qt_objc_cast(sender)) - [self qt_itemFired:nativeItem]; - else - qCDebug(lcQpaMenus) << "Ignoring action for menu item we didn't create"; - - return; - } - - [super forwardInvocation:invocation]; + NSObject *sender; + [invocation getArgument:&sender atIndex:2]; + qCDebug(lcQpaMenus) << "Forwarding" << invocation.selector << "from" << sender; + Q_ASSERT(qt_objc_cast(sender)); + invocation.selector = @selector(qt_itemFired:); + [invocation invokeWithTarget:[QCocoaApplicationDelegate sharedDelegate]]; } @end