Expose macOS app lifecycle notifications as overridable hooks - #4599
Conversation
The Cocoa AppDelegate now hands off every native app lifecycle notification (launch, activation, hide/unhide, screen changes, termination) to a same-named cocoa_ method on the App implementation class, most defaulting to a no-op. In the absence of a cross-platform lifecycle API, this lets apps hook into these events on macOS by overriding the corresponding cocoa_ method on the App class. Fixes beeware#4478. Co-authored-by: Cursor <cursoragent@cursor.com>
The Cocoa lifecycle-hook tests were gated on toga.platform.current_platform, which only reflects the host OS. The "textual-macOS" CI job runs the Textual backend on a macOS runner, so the tests weren't skipped there and failed with AttributeError/ModuleNotFoundError since app._impl isn't a toga_cocoa.App. Gate on toga.backend instead, which reflects the active backend. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The remaining failing check, The
|
freakboy3742
left a comment
There was a problem hiding this comment.
I've added some comments inline, mostly to do with minor formatting and verbosity issues; I've pushed an update to resolve those issues.
I've also worked out there's a much cleaner way to provide the method overrides, which I've implemented and documented.
Otherwise - this is great - thanks for the contribution.
| # ------------------------------------------------------------------ | ||
| # App lifecycle | ||
| # | ||
| # These methods are thin wrappers that hand off to a same-named | ||
| # ``cocoa_``-prefixed method on the Cocoa App implementation class. | ||
| # This allows a user to override the platform-specific behavior of a | ||
| # lifecycle event, in the absence of a cross-platform API for the | ||
| # event, by replacing the ``cocoa_`` method on the ``toga_cocoa.App`` | ||
| # *class* (not a specific app instance) with their own implementation. | ||
| # See the "Platform-specific APIs" section of the macOS docs for | ||
| # details, including why the override must be applied at the class | ||
| # level for normal Python method binding to work. | ||
| # ------------------------------------------------------------------ |
There was a problem hiding this comment.
Excessive verbosity:
| # ------------------------------------------------------------------ | |
| # App lifecycle | |
| # | |
| # These methods are thin wrappers that hand off to a same-named | |
| # ``cocoa_``-prefixed method on the Cocoa App implementation class. | |
| # This allows a user to override the platform-specific behavior of a | |
| # lifecycle event, in the absence of a cross-platform API for the | |
| # event, by replacing the ``cocoa_`` method on the ``toga_cocoa.App`` | |
| # *class* (not a specific app instance) with their own implementation. | |
| # See the "Platform-specific APIs" section of the macOS docs for | |
| # details, including why the override must be applied at the class | |
| # level for normal Python method binding to work. | |
| # ------------------------------------------------------------------ | |
| # ------------------------------------------------------------------ | |
| # App lifecycle | |
| # ------------------------------------------------------------------ |
| @@ -0,0 +1 @@ | |||
| On macOS, the logic in the app delegate's native lifecycle notification methods has been moved to `cocoa_`-prefixed methods on the `toga_cocoa` App implementation class. Every native app lifecycle notification now has a corresponding method, most defaulting to a no-op, that can be overridden to add custom platform-specific behavior in the absence of a cross-platform lifecycle API. | |||
There was a problem hiding this comment.
This is way too verbose for a release note.
| On macOS, the logic in the app delegate's native lifecycle notification methods has been moved to `cocoa_`-prefixed methods on the `toga_cocoa` App implementation class. Every native app lifecycle notification now has a corresponding method, most defaulting to a no-op, that can be overridden to add custom platform-specific behavior in the absence of a cross-platform lifecycle API. | |
| macOS apps now have a defined interface for responding to events in the Cocoa app lifecycle. |
| # | ||
| # These methods back the AppDelegate's handling of native app lifecycle | ||
| # notifications. In the absence of a cross-platform API for these | ||
| # events, they exist as override points for app authors who need to | ||
| # react to a native lifecycle event; a method can be replaced by | ||
| # assigning a new function to the *class* (e.g. | ||
| # ``App.cocoa_applicationDidBecomeActive = my_function``). Assigning | ||
| # directly to an app's ``_impl`` instance instead won't bind ``self``, | ||
| # so the replacement callable would need to be created with that in | ||
| # mind (e.g., via ``types.MethodType``, or a function that doesn't | ||
| # take ``self``). See the "Platform-specific APIs" section of the | ||
| # macOS docs for usage details. |
There was a problem hiding this comment.
Excessive verbosity:
| # | |
| # These methods back the AppDelegate's handling of native app lifecycle | |
| # notifications. In the absence of a cross-platform API for these | |
| # events, they exist as override points for app authors who need to | |
| # react to a native lifecycle event; a method can be replaced by | |
| # assigning a new function to the *class* (e.g. | |
| # ``App.cocoa_applicationDidBecomeActive = my_function``). Assigning | |
| # directly to an app's ``_impl`` instance instead won't bind ``self``, | |
| # so the replacement callable would need to be created with that in | |
| # mind (e.g., via ``types.MethodType``, or a function that doesn't | |
| # take ``self``). See the "Platform-specific APIs" section of the | |
| # macOS docs for usage details. |
| # macOS can also run the Textual backend (e.g. the "textual-macOS" CI job), which | ||
| # doesn't have any of these cocoa_ methods. | ||
| #################################################################################### | ||
| if toga.backend != "toga_cocoa": |
There was a problem hiding this comment.
I can see what you're trying to do here, but we don't put platform-specific tests in the testbed backend.
I'm not entirely convinced these tests are even needed... there wasn't a need for any no-cover exclusions in the existing implementation, so the same code should be covered here as well.
There was a problem hiding this comment.
Ah - turns out there is a coverage gap, but only for end-of-lifecycle events; we're generally OK adding no-cover for those.
Fixes (partly) #4478.
What this does
macOS notifies an app of lifecycle events (launch, activation, hide/unhide,
screen changes, termination, etc.) by calling methods on an
NSApplicationDelegateinstance. Today, the logic for the handful of eventsToga cares about lives directly inside
AppDelegatemethods, with no way foran app author to hook into it, and most native lifecycle notifications aren't
wired up at all.
Per the pattern proposed in #4478, this PR (scoped to Cocoa/macOS only, per
the maintainer's note that per-platform PRs are acceptable):
AppDelegate's existing lifecycle methods(
applicationDidFinishLaunching:,applicationWillHide:,applicationDidUnhide:) into same-namedcocoa_-prefixed methods ontoga_cocoa'sAppimplementation class.AppDelegatemethods are nowthin passthroughs that forward all arguments unchanged.
cocoa_implementations for every native lifecyclenotification Cocoa didn't previously handle (
applicationWillFinishLaunching:,application{Will,Did}{Become,Resign}Active:,applicationDidHide:,applicationWillUnhide:,applicationDidChangeScreenParameters:,applicationShouldTerminate:,applicationWillTerminate:), each defaultingto a documented no-op (or, for
applicationShouldTerminate:, to allowingtermination immediately — matching the native default).
NSTerminateNow/NSTerminateCancel/NSTerminateLaterconstants.In the absence of a cross-platform lifecycle API, an app can now hook into
one of these events on macOS:
Note the override is applied to the
Appclass, not an instance — thedocs explain why (Python only auto-binds
selffor methods found via theclass, not for plain functions assigned to an instance attribute).
Testing
testbed/tests/app/test_cocoa.py(macOS-only): verifies every newno-op hook is a safe no-op by default and can be overridden and observes
the correct notification argument, plus dedicated default/override tests
for
applicationShouldTerminate:.trigger_lifecycle_notification()probe helper incocoa/tests_backend/app.pyto invoke delegate methods directly.applicationDidFinishLaunching:,applicationWillHide:/applicationDidUnhide:is unchanged (verified bythe existing hide/unhide coverage in
testbed/tests/app/test_desktop.py)and confirmed with a manual script exercising the real
AppDelegate/Appwiring viarubicon-objc.tox -m test-corestill passes at 100% coverage (unaffected, as expectedfor a Cocoa-only change).
tox -e docs-enbuilds cleanly with the new reference section.Testbed (macOS-arm64)andTestbed (macOS-x86_64)both pass withthis change.
Docs
Added a "Platform-specific APIs" section to
docs/en/reference/platforms/macOS.mddocumenting all 13 hooks, with aworking usage example. Wired
cocoa/srcinto the mkdocstrings build(
docs/config.yml,tox.ini) so the new methods can be referenced.Status
changes/4478.feature.md).pre-commit run --all-filespasses on changed files.textual-linux,textual-macOSandlinux-wayland-qttestbed failures on this PR arepre-existing, unrelated widget-layout flakiness —
textual-linuxiscurrently failing the same way on
main, and theqtfailure is anunrelated
Selectionwidget sizing assertion. This PR does not touchthose backends.)
PR Checklist:
Assisted-by: Cursor