Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Do not activate app when calling focus on inactive panel window #40307

Merged

Conversation

felixrieseberg
Copy link
Member

Description of Change

This PR fixes #39914. On macOS Sonoma, a BrowserWindow of the panel type could no longer be focused without also making the app the foregrounded app.

This was likely caused by us incorrectly calling [[NSApplication sharedApplication] activateIgnoringOtherApps:NO]; in those scenarios. However, having done some sleuthing, we've tried to not call that method in recent years and saw different focus bugs. With that in mind, this PR does two things:

  1. Adds a test for focusing an inactive panel window
  2. Ensures that we do not call activateIgnoringOtherApps for panel windows on macOS 14.0 or newer.

We've also verified this approach internally at Notion. Anyone needing a fix for this urgently can use this native module.

Checklist

Release Notes

Notes: Fixed app incorrectly activating panel windows on macOS Sonoma

@electron-cation electron-cation bot added the new-pr 🌱 PR opened in the last 24 hours label Oct 23, 2023
@felixrieseberg felixrieseberg added target/26-x-y PR should also be added to the "26-x-y" branch. target/27-x-y PR should also be added to the "27-x-y" branch. target/28-x-y PR should also be added to the "28-x-y" branch. labels Oct 23, 2023
@felixrieseberg felixrieseberg added the semver/patch backwards-compatible bug fixes label Oct 24, 2023
@electron-cation electron-cation bot removed the new-pr 🌱 PR opened in the last 24 hours label Oct 24, 2023
@felixrieseberg felixrieseberg requested review from a team as code owners October 25, 2023 19:52
@felixrieseberg
Copy link
Member Author

No windows files changed, failed Windows build is a flake!

@codebytere
Copy link
Member

@felixrieseberg would you mind rebasing this?

@felixrieseberg
Copy link
Member Author

@codebytere Rebased!

@felixrieseberg
Copy link
Member Author

@tzahola Thank you, that's really helpful! You're right, maybe we just use activate across the board (starting with macOS Sonoma for a bit of safety).

Copy link
Member

@codebytere codebytere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving with latest change

@MarshallOfSound MarshallOfSound merged commit b55d7f4 into electron:main Nov 6, 2023
17 checks passed
Copy link

release-clerk bot commented Nov 6, 2023

Release Notes Persisted

Fixed app incorrectly activating panel windows on macOS Sonoma

@trop
Copy link
Contributor

trop bot commented Nov 6, 2023

I have automatically backported this PR to "26-x-y", please check out #40463

@trop trop bot added in-flight/26-x-y and removed target/26-x-y PR should also be added to the "26-x-y" branch. labels Nov 6, 2023
@trop
Copy link
Contributor

trop bot commented Nov 6, 2023

I have automatically backported this PR to "27-x-y", please check out #40464

@trop
Copy link
Contributor

trop bot commented Nov 6, 2023

I have automatically backported this PR to "28-x-y", please check out #40465

@trop trop bot added in-flight/27-x-y in-flight/28-x-y and removed target/27-x-y PR should also be added to the "27-x-y" branch. target/28-x-y PR should also be added to the "28-x-y" branch. labels Nov 6, 2023
@tzahola
Copy link
Contributor

tzahola commented Nov 8, 2023

So I've experimented a bit with app activation on macOS Sonoma, to see how this new "cooperative" app activation API works.

Contrary what the header docs and the WWDC23 video suggests, the new [NSApplication activate] API is not a 100% replacement for the - now deprecated - [NSApplication activateIgnoringOtherApps:]. There are some use cases that are not possible with the new API, that are still possible with the deprecated one.

According to the docs, when using the new API to activate an app, it is merely a suggestion to the system, and it is ultimately up to the OS to decide whether the currently active app will be replaced with the requested one or not:

Calling this function sends a message to the system requesting activation for the app. Requesting activation doesn’t guarantee your app gains focus. People ultimately decide which apps gain focus by activating them through their device’s user interface. But if, after considering the user’s intent and larger system context, the system honors your app’s request, your app activates.

In addition to [NSApplication activate] there's also an [NSApplication yieldActivationToApplication:] API, through which an active application can yield their active status to another app, so that when it tries to become active, the system will be more likely to honor its request. Apple also has a neat little diagram on this:

renderedDark2x-1698679038

Now, how the target app would know that some other app wants to activate it, is unclear, as there's no API that would notify it that someone wants it to become active... Are you supposed to communicate this via some side channel, or what? I could only see that working between two apps from the same developer though. And then Apple goes on to show a code snippet that makes even less sense:

// When you want to pass control to another application.

// Get an instance of the app you want to activate.
if let targetApp = NSRunningApplication.runningApplications(withBundleIdentifier:
                                                                "com.apple.TextEdit").first {
    // Yield to it.
    NSApp.yieldActivation(to:targetApp)
    
    // Then activate it.
    targetApp.activate()
}

So this is how you're supposed to pass control to another app with the new API. What's the point of calling two methods in sequence, that are semantically the same? First we're announcing that we're yielding activation to targetApp, then we're asking the system to activate targetApp. Why this needs to be two separate calls is beyond me. Obviously if my app is calling [targetApp activate], then I implicitly want to yield control to that app!

Anyway, this is only half of the story. You hardly ever care about activating other apps. How do you activate your own app when it's needed with the new API? Apple says:

// Call this function when you want your app to request activation for itself.
NSApp.activate()

and Apple also says that:

Calling this function sends a message to the system requesting activation for the app. Requesting activation doesn’t guarantee your app gains focus. People ultimately decide which apps gain focus by activating them through their device’s user interface. But if, after considering the user’s intent and larger system context, the system honors your app’s request, your app activates.

They don't elaborate how the system decides whether to honor our request and activate our app when it wants to come to the foreground, but they assure us that they're going to consider "the user’s intent and larger system context". (Again, there's no API which would let us negotiate with the currently active app about taking over the active status. Only the active app can influence the system's decision by yielding, but why would any random app - that happens to be active - even know about our app in the first place???)

So I've made a few experiments in a bare Cocoa app to see when the system honors the activation request from an inactive/backgrounded app. These are my findings as of macOS Sonoma 14.1:

  1. Activating the app in response to a global hotkey (either via [NSEvent addGlobalMonitor] or RegisterEventHotKey()) seems to work with the new API.
  2. Activating the app in response to a dock menu interaction, or system menubar interaction seems to work with the new API.
  3. Spontaneously activating the app in response to timers, network events, etc. does not work with the new API. It works with the old API however! (for now)
  4. In any of the above cases, if Terminal.app is the currently active app, neither the old API ([NSApp activateIgnoringOtherApps:YES]) nor the new one ([NSApp activate]) have any effect. Seriously, this only happens with Terminal.app somehow.

In conclusion: the new API seems to only activate our app, if there was some user interaction beforehand, like clicking a dock menu, a system menubar menu, or hitting a global hotkey. Activating the app if it was put in the background when starting some long-running process, or activating it in response to an incoming network message does not work with the new API.


Based on this, I'd recommend reverting to [NSApp activateIgnoringOtherApps:] (which needs to be gated for non-activating NSPanels to solve the original #39914 issue) Otherwise, using the new API can break some people's use cases for app activation (e.g. imagine a file manager doing a long-running copy, that has a feature to jump to the front when the copy is finished. This is not possible with the new API.)

What does everyone else think?

@castroCrea
Copy link

castroCrea commented Nov 8, 2023

Thanks, @tzahola for all this research.

In my sense, NSPanel, is the window used for the Mac OS spotlight. So by default, it is not activated.

What should be the behavior is that on win.showInactive() for a type: 'panel' window, it has to show the app and not activate it. No matter if you have multiple windows or not. The activate app should still be the one you were on before this action as been triggered.

What I understand from you comment

If I understand well (there is a lot of information to process) the actual PR will not fix the behavior. Because when you do a globalShortcut that calls win.showInactive() it will still activate the app.

@tzahola
Copy link
Contributor

tzahola commented Nov 8, 2023

If I understand well (there is a lot of information to process) the actual PR will not fix the behavior

What I'm saying basically, is that replacing the "deprecated" activateIgnoringOtherApps: API with the new activate API in this PR have inadvertently disabled some potentially useful use cases. I.e. Electron apps won't be able to programmatically jump to the foreground, unless there is a user interaction that triggers this (such as clicking a dock menu item, or a system menubar item, or triggering a global hotkey).

This is independent of the original issue that this PR fixes, that is, the undesirable app activation when opening nonactivating NSPanels (i.e. windowType: "panel" in Electron parlance).

Long story short: so your original issue with the Spotlight-like panel window is fixed by this PR, but a different, more subtle issue was introduced by switching to the new app activation API. This can be fixed by returning to the original version of @felixrieseberg which used the old API (which was replaced with the new API only because @codebytere noticed that the old one got marked as deprecated), but checked the window type to not activate the app inadvertently for NSPanels.

@codebytere
Copy link
Member

@felixrieseberg what are your thoughts?

@trop trop bot added merged/28-x-y PR was merged to the "28-x-y" branch. and removed in-flight/28-x-y labels Nov 10, 2023
@felixrieseberg
Copy link
Member Author

@tzahola Thank you for all this research and the very elaborate write-up. I appreciate it quite a bit.

@codebytere I think what we should do is to fix-forward to an earlier version of this PR - we'll use the new API for NSPanel - and stick with the old API for all other windows. That seems like the safest way to not break any existing behavior. I'll make a PR!

@felixrieseberg
Copy link
Member Author

Follow-up PR: #40570

MrHuangJser pushed a commit to MrHuangJser/electron that referenced this pull request Dec 11, 2023
…lectron#40307)

* fix: Do not activate app when calling focus on inactive panel window

* Use activate

* Use "activate" for all windows
@YongzeYao
Copy link

is this fixed? I am still encountering this issue...
Calling focus activates the app...

@YongzeYao
Copy link

YongzeYao commented Feb 9, 2024

@felixrieseberg hey Felix, does this commit fix the activation problem?
Cuz I am stilling encountering it with 28.2.1.
There seems no way for me to make something that behaves like spotlight with only electron API on mac.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merged/28-x-y PR was merged to the "28-x-y" branch. semver/patch backwards-compatible bug fixes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: In Mac OS Sonoma (14.0) showInactive, is not inactive anymore
6 participants