winemac.drv: Support cooperative app activation in macOS 14 Sonoma.

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.
This commit is contained in:
Tim Clem 2023-08-15 11:38:43 -07:00 committed by Alexandre Julliard
parent a1d429dabd
commit fd67d67850
3 changed files with 125 additions and 4 deletions

View file

@ -142,6 +142,7 @@ @interface WineApplicationController : NSObject <NSApplicationDelegate>
+ (WineApplicationController*) sharedController;
- (void) transformProcessToForeground:(BOOL)activateIfTransformed;
- (void) tryToActivateIgnoringOtherApps:(BOOL)ignore;
- (BOOL) registerEventQueue:(WineEventQueue*)queue;
- (void) unregisterEventQueue:(WineEventQueue*)queue;

View file

@ -34,6 +34,12 @@
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 @@ + (void) setAllowsAutomaticWindowTabbing:(BOOL)allows;
#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 @@ - (void) transformProcessToForeground:(BOOL)activateIfTransformed
[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 @@ - (void) setupObservations
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)

View file

@ -1720,7 +1720,7 @@ - (void) orderBelow:(WineWindow*)prev orAbove:(WineWindow*)next activate:(BOOL)a
wasVisible = [self isVisible];
if (activate)
[NSApp activateIgnoringOtherApps:YES];
[controller tryToActivateIgnoringOtherApps:YES];
NSDisableScreenUpdates();
@ -2084,8 +2084,9 @@ - (void) makeFocused:(BOOL)activate
{
if (activate)
{
[[WineApplicationController sharedController] transformProcessToForeground:YES];
[NSApp activateIgnoringOtherApps:YES];
WineApplicationController *controller = [WineApplicationController sharedController];
[controller transformProcessToForeground:YES];
[controller tryToActivateIgnoringOtherApps:YES];
}
causing_becomeKeyWindow = self;