Starting in Sonoma, apps can no longer force themselves to the foreground with -activateIgnoringOtherApps:. winemac currently does that in a few places - when an app creates its first window, and in the implementation of APIs like SetFocus.
There's nothing we can do to work around the new behavior in the general case. This patch makes Wine apps running in the same prefix yield to one another, so that windows from multiple EXEs can at least behave as intended.
---
As noted in a comment, there's an inherent race condition in handling this issue the way I've done it here. Sonoma does provide another API that would theoretically alleviate that, but in practice it has some quirks that make it unsuitable. Gory details follow.
An app can yield activation to either another `NSRunningApplication` (`-yieldActivationToApplication:`) or to the bundle identifier of an application that may not have launched yet (`-yieldActivationToApplicationWithBundleIdentifier:`). Ideally we could just use the latter to issue a blanket yield to the loader/preloader.
A temporary roadblock: yielding to the bundle ID of the loader/preloader doesn't work at all at the moment. It seems that the new APIs rely on LaunchServices' picture of the world, and even though the loader & preloader have an embedded Info.plist, LS only seems to notice them if they're actually in a .app bundle. I hacked that together locally, and yielding to a bundled preloader works, but...
Yields to bundle IDs seem to be canceled if the user interacts with the yielding application in certain ways. This includes clicking or typing into a window, or moving a window around. This is probably part of the "heuristics" that Apple mentioned are involved in deciding whether to let an app come forward.
So, it seems that we need to issue yields directly in response to another app trying to activate itself, rather than trying to do it preemptively. The distributed notification in this patch was the cleanest way I could think to orchestrate that.
From: Tim Clem tclem@codeweavers.com
Starting in Sonoma, apps can no longer force themselves to the foreground with -activateIgnoringOtherApps:. winemac currently does that in a few places - when an app creates its first window, and in the implementation of APIs like SetFocus.
There's nothing we can do to work around the new behavior in the general case. This patch makes Wine apps running in the same prefix yield to one another, so that windows from multiple EXEs can at least behave as intended. --- dlls/winemac.drv/cocoa_app.h | 1 + dlls/winemac.drv/cocoa_app.m | 121 +++++++++++++++++++++++++++++++- dlls/winemac.drv/cocoa_window.m | 7 +- 3 files changed, 125 insertions(+), 4 deletions(-)
diff --git a/dlls/winemac.drv/cocoa_app.h b/dlls/winemac.drv/cocoa_app.h index 52c91c0621f..c4c8bbd16f2 100644 --- a/dlls/winemac.drv/cocoa_app.h +++ b/dlls/winemac.drv/cocoa_app.h @@ -142,6 +142,7 @@ enum { + (WineApplicationController*) sharedController;
- (void) transformProcessToForeground:(BOOL)activateIfTransformed; + - (void) tryToActivateIgnoringOtherApps:(BOOL)ignore;
- (BOOL) registerEventQueue:(WineEventQueue*)queue; - (void) unregisterEventQueue:(WineEventQueue*)queue; diff --git a/dlls/winemac.drv/cocoa_app.m b/dlls/winemac.drv/cocoa_app.m index 6a16d1ba832..5a6a3e01005 100644 --- a/dlls/winemac.drv/cocoa_app.m +++ b/dlls/winemac.drv/cocoa_app.m @@ -34,6 +34,12 @@ static NSString* const WineAppWaitQueryResponseMode = @"WineAppWaitQueryResponse static NSString* const NSWindowWillStartDraggingNotification = @"NSWindowWillStartDraggingNotification"; static NSString* const NSWindowDidEndDraggingNotification = @"NSWindowDidEndDraggingNotification";
+// Internal distributed notification to handle cooperative app activation in Sonoma. +static NSString* const WineAppWillActivateNotification = @"WineAppWillActivateNotification"; +static NSString* const WineActivatingAppPIDKey = @"ActivatingAppPID"; +static NSString* const WineActivatingAppPrefixKey = @"ActivatingAppPrefix"; +static NSString* const WineActivatingAppConfigDirKey = @"ActivatingAppConfigDir"; +
int macdrv_err_on;
@@ -47,6 +53,24 @@ int macdrv_err_on; #endif
+#if !defined(MAC_OS_VERSION_14_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_14_0 +@interface NSApplication (CooperativeActivationSelectorsForOldSDKs) + + - (void)activate; + - (void)yieldActivationToApplication:(NSRunningApplication *)application; + - (void)yieldActivationToApplicationWithBundleIdentifier:(NSString *)bundleIdentifier; + +@end + +@interface NSRunningApplication (CooperativeActivationSelectorsForOldSDKs) + + - (BOOL)activateFromApplication:(NSRunningApplication *)application + options:(NSApplicationActivationOptions)options; + +@end +#endif + + /*********************************************************************** * WineLocalizedString * @@ -227,7 +251,7 @@ static NSString* WineLocalizedString(unsigned int stringID) [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
if (activateIfTransformed) - [NSApp activateIgnoringOtherApps:YES]; + [self tryToActivateIgnoringOtherApps:YES];
#if defined(MAC_OS_X_VERSION_10_9) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_9 if (!enable_app_nap && [NSProcessInfo instancesRespondToSelector:@selector(beginActivityWithOptions:reason:)]) @@ -1939,8 +1963,103 @@ static NSString* WineLocalizedString(unsigned int stringID) selector:@selector(enabledKeyboardInputSourcesChanged) name:(NSString*)kTISNotifyEnabledKeyboardInputSourcesChanged object:nil]; + + if ([NSApplication instancesRespondToSelector:@selector(yieldActivationToApplication:)]) + { + /* App activation cooperation, starting in macOS 14 Sonoma. */ + [dnc addObserver:self + selector:@selector(otherWineAppWillActivate:) + name:WineAppWillActivateNotification + object:nil + suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately]; + } + } + + - (void) otherWineAppWillActivate:(NSNotification *)note + { + NSProcessInfo *ourProcess; + pid_t otherPID; + NSString *ourConfigDir, *otherConfigDir, *ourPrefix, *otherPrefix; + NSRunningApplication *otherApp; + + /* No point in yielding if we're not the foreground app. */ + if (![NSApp isActive]) return; + + /* Ignore requests from ourself, dead processes, and other prefixes. */ + ourProcess = [NSProcessInfo processInfo]; + otherPID = [note.userInfo[WineActivatingAppPIDKey] integerValue]; + if (otherPID == ourProcess.processIdentifier) return; + + otherApp = [NSRunningApplication runningApplicationWithProcessIdentifier:otherPID]; + if (!otherApp) return; + + ourConfigDir = ourProcess.environment[@"WINECONFIGDIR"]; + otherConfigDir = note.userInfo[WineActivatingAppConfigDirKey]; + if (ourConfigDir.length && otherConfigDir.length && + ![ourConfigDir isEqualToString:otherConfigDir]) + { + return; + } + + ourPrefix = ourProcess.environment[@"WINEPREFIX"]; + otherPrefix = note.userInfo[WineActivatingAppPrefixKey]; + if (ourPrefix.length && otherPrefix.length && + ![ourPrefix isEqualToString:otherPrefix]) + { + return; + } + + /* There's a race condition here. The requesting app sends out + WineAppWillActivateNotification and then activates itself, but since + distributed notifications are asynchronous, we may not have yielded + in time. So we call activateFromApplication: on the other app here, + which will work around that race if it happened. If we didn't hit the + race, the activateFromApplication: call will be a no-op. */ + + /* We only add this observer if NSApplication responds to the yield + methods, so they're safe to call without checking here. */ + [NSApp yieldActivationToApplication:otherApp]; + [otherApp activateFromApplication:[NSRunningApplication currentApplication] + options:0]; }
+ - (void) tryToActivateIgnoringOtherApps:(BOOL)ignore + { + NSProcessInfo *processInfo; + NSString *configDir, *prefix; + NSDictionary *userInfo; + + if ([NSApp isActive]) return; /* Nothing to do. */ + + if (!ignore || + ![NSApplication instancesRespondToSelector:@selector(yieldActivationToApplication:)]) + { + /* Either we don't need to force activation, or the OS is old enough + that this is our only option. */ + [NSApp activateIgnoringOtherApps:ignore]; + return; + } + + /* Ask other Wine apps to yield activation to us. */ + processInfo = [NSProcessInfo processInfo]; + configDir = processInfo.environment[@"WINECONFIGDIR"]; + prefix = processInfo.environment[@"WINEPREFIX"]; + userInfo = @{ + WineActivatingAppPIDKey: @(processInfo.processIdentifier), + WineActivatingAppPrefixKey: prefix ? prefix : @"", + WineActivatingAppConfigDirKey: configDir ? configDir : @"" + }; + + [[NSDistributedNotificationCenter defaultCenter] + postNotificationName:WineAppWillActivateNotification + object:nil + userInfo:userInfo + deliverImmediately:YES]; + + /* This is racy. See the note in otherWineAppWillActivate:. */ + [NSApp activate]; + } + - (BOOL) inputSourceIsInputMethod { if (!inputSourceIsInputMethodValid) diff --git a/dlls/winemac.drv/cocoa_window.m b/dlls/winemac.drv/cocoa_window.m index ac4cc214239..cc9a9e13c2b 100644 --- a/dlls/winemac.drv/cocoa_window.m +++ b/dlls/winemac.drv/cocoa_window.m @@ -1720,7 +1720,7 @@ static CVReturn WineDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTi wasVisible = [self isVisible];
if (activate) - [NSApp activateIgnoringOtherApps:YES]; + [controller tryToActivateIgnoringOtherApps:YES];
NSDisableScreenUpdates();
@@ -2084,8 +2084,9 @@ static CVReturn WineDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTi { if (activate) { - [[WineApplicationController sharedController] transformProcessToForeground:YES]; - [NSApp activateIgnoringOtherApps:YES]; + WineApplicationController *controller = [WineApplicationController sharedController]; + [controller transformProcessToForeground:YES]; + [controller tryToActivateIgnoringOtherApps:YES]; }
causing_becomeKeyWindow = self;