diff --git a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m index 2af0935c4d2fa..bc5ac53df2d5b 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m +++ b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m @@ -221,22 +221,15 @@ - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu subMenuItem.submenu = subMenu; subMenuItem.image = [[NSBundle mainBundle] imageForResource:@"app.icns"]; - // There is an annoying bug in macOS (at least 10.13.3), it does not use/copy over the representedObject of a menu item - // So we have to use tag instead. int idx = 0; - for (NSArray* item in menuItems) { NSMenuItem *actionItem = [subMenu addItemWithTitle:[item valueForKey:@"text"] action:@selector(subMenuActionClicked:) keyEquivalent:@""]; - // Carry the command on the item itself rather than an index into _menuItems. The - // menu outlives the array it was built from: a later GET_MENU_ITEMS round trip can - // reset and repopulate _menuItems before the user clicks, after which an index - // either selects the wrong command or is out of bounds and throws — crashing the - // extension. identifier is a plain string property from - // NSUserInterfaceItemIdentification, so unlike representedObject it survives - // whatever copying Finder does to the menu. - actionItem.identifier = [item valueForKey:@"command"]; + + // Finder copies the menu into its own process to display it but identifier does not survive it + // Use tag to pass the command: an index into _menuItems that subMenuActionClicked: resolves locally + [actionItem setTag:idx]; [actionItem setTarget:self]; NSString *flags = [item valueForKey:@"flags"]; // e.g. "d" @@ -257,7 +250,14 @@ - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu } - (void)subMenuActionClicked:(id)sender { - NSString *command = [(NSMenuItem *)sender identifier]; + const NSInteger tag = [(NSMenuItem *)sender tag]; + NSString *command = nil; + [self->_menuIsComplete lock]; + if (tag >= 0 && tag < (NSInteger)_menuItems.count) { + command = _menuItems[tag][@"command"]; + os_log_debug(_log, "command for menu: %{public}@", command); + } + [self->_menuIsComplete unlock]; if (command.length == 0) { os_log_error(_log, "Menu item clicked with no command attached; ignoring"); diff --git a/src/gui/macOS/findersyncservice.mm b/src/gui/macOS/findersyncservice.mm index 67ad284ee2f83..9075280cee3dd 100644 --- a/src/gui/macOS/findersyncservice.mm +++ b/src/gui/macOS/findersyncservice.mm @@ -25,6 +25,25 @@ } // namespace OCC +// SocketApi handlers require a listener and write output via sendMessage() +namespace { +class ResponseCapturingListener : public OCC::SocketListener +{ +public: + mutable QStringList messages; + + explicit ResponseCapturingListener() + : SocketListener(nullptr) + { + } + + void sendMessage(const QString &message, bool = false) const override + { + messages.append(message); + } +}; +} // anonymous namespace + /** * @brief Objective-C delegate that implements the FinderSyncAppProtocol. */ @@ -237,21 +256,19 @@ - (void)executeMenuCommand:(NSString *)command // Build the command method name (e.g., "SHARE" -> "command_SHARE") const QString methodName = QStringLiteral("command_%1").arg(qCommand); - // Create a null listener since we're not sending responses back via socket - // Commands will execute but won't send socket responses - OCC::SocketListener *nullListener = nullptr; - // Join all paths with record separator (same encoding as the socket protocol) // to preserve multi-selection for commands like MAKE_AVAILABLE_LOCALLY const QString argument = qPaths.join(QChar(0x1e)); + ResponseCapturingListener captureListener; + // Try to invoke the command using Qt's meta-object system bool invoked = QMetaObject::invokeMethod( socketApi, methodName.toUtf8().constData(), Qt::DirectConnection, Q_ARG(QString, argument), - Q_ARG(OCC::SocketListener*, nullListener) + Q_ARG(OCC::SocketListener*, &captureListener) ); if (invoked) { @@ -352,39 +369,6 @@ - (void)performHandshakeWithReply:(void(^)(void))completionHandler return {true, statusString}; } -/** - * @brief SocketListener subclass that captures command responses in-memory. - * - * SocketApi command methods (e.g., command_GET_STRINGS, command_GET_MENU_ITEMS) - * produce their output by calling listener->sendMessage(). This design dates - * back to the UNIX-socket transport where responses were written directly to - * the socket. - * - * With the XPC transport there is no socket, yet we still need to invoke these - * commands and collect their output. ResponseCapturingListener acts as an - * in-memory adapter: it receives the sendMessage() calls and stores them in a - * QStringList that the caller can parse and convert into XPC reply values. - * - * This is production code, not a test helper. - */ -namespace { -class ResponseCapturingListener : public SocketListener -{ -public: - mutable QStringList messages; - - explicit ResponseCapturingListener() - : SocketListener(nullptr) - { - } - - void sendMessage(const QString &message, bool = false) const override - { - messages.append(message); - } -}; -} // anonymous namespace - QMap FinderSyncService::getLocalizedStrings() const { QMap result;