From b50b2c33ccdcbe7f9da742e0ecebf3d6064fae2a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 15 Jun 2024 11:38:05 +0800 Subject: [PATCH] Add backend implementations for background and session apps. --- android/src/toga_android/app.py | 17 ++++++++++++---- cocoa/src/toga_cocoa/app.py | 21 ++++++++++---------- gtk/src/toga_gtk/app.py | 32 +++++++++++++++++++------------ iOS/src/toga_iOS/app.py | 9 ++++++++- textual/src/toga_textual/app.py | 11 ++++++++++- web/src/toga_web/app.py | 11 +++++++++-- winforms/src/toga_winforms/app.py | 16 ++++++++++------ 7 files changed, 80 insertions(+), 37 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 347d9126d1..9193a764ab 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -10,6 +10,7 @@ from java import dynamic_proxy from org.beeware.android import IPythonApp, MainActivity +import toga from toga.command import Command, Group, Separator from toga.handlers import simple_handler @@ -182,6 +183,9 @@ def onPrepareOptionsMenu(self, menu): class App: + # Android apps exit when the last window is closed + CLOSE_ON_LAST_WINDOW = True + def __init__(self, interface): self.interface = interface self.interface._impl = self @@ -245,10 +249,15 @@ def set_icon(self, icon): pass # pragma: no cover def set_main_window(self, window): - # The default layout of an Android app includes a titlebar; a simple App then - # hides that titlebar. We know what type of app we have when the main window is - # set. - self.interface.main_window._impl.configure_titlebar() + if window is None: + raise RuntimeError("Session-based apps are not supported on Android") + elif window == toga.App.BACKGROUND: + raise RuntimeError("Background apps are not supported on Android") + else: + # The default layout of an Android app includes a titlebar; a simple App + # then hides that titlebar. We know what type of app we have when the main + # window is set. + self.interface.main_window._impl.configure_titlebar() ###################################################################### # App resources diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 4339610b98..9490c12b11 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -1,6 +1,5 @@ import asyncio import inspect -import os import sys from pathlib import Path from urllib.parse import unquote, urlparse @@ -28,6 +27,7 @@ NSAboutPanelOptionApplicationVersion, NSAboutPanelOptionVersion, NSApplication, + NSApplicationActivationPolicyAccessory, NSApplicationActivationPolicyRegular, NSBeep, NSBundle, @@ -108,6 +108,9 @@ def validateMenuItem_(self, sender) -> bool: class App: + # macOS apps persist when there are no windows open + CLOSE_ON_LAST_WINDOW = False + def __init__(self, interface): self.interface = interface self.interface._impl = self @@ -117,12 +120,7 @@ def __init__(self, interface): asyncio.set_event_loop_policy(EventLoopPolicy()) self.loop = asyncio.new_event_loop() - # Stimulate the build of the app - self.create() - - def create(self): self.native = NSApplication.sharedApplication - self.native.setActivationPolicy(NSApplicationActivationPolicyRegular) # The app icon been set *before* the app instance is created. However, we only # need to set the icon on the app if it has been explicitly defined; the default @@ -130,15 +128,13 @@ def create(self): if self.interface.icon._impl.path: self.set_icon(self.interface.icon) # pragma: no cover - self.resource_path = os.path.dirname( - os.path.dirname(NSBundle.mainBundle.bundlePath) - ) + self.resource_path = Path(NSBundle.mainBundle.bundlePath).parent.parent self.appDelegate = AppDelegate.alloc().init() self.appDelegate.impl = self self.appDelegate.interface = self.interface self.appDelegate.native = self.native - self.native.setDelegate_(self.appDelegate) + self.native.setDelegate(self.appDelegate) # Create the lookup table for menu items self._menu_groups = {} @@ -422,7 +418,10 @@ def set_icon(self, icon): self.native.setApplicationIconImage(None) def set_main_window(self, window): - pass + if window == toga.App.BACKGROUND: + self.native.setActivationPolicy(NSApplicationActivationPolicyAccessory) + else: + self.native.setActivationPolicy(NSApplicationActivationPolicyRegular) ###################################################################### # App resources diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index a66f4a2a00..d7f0904ded 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -17,6 +17,9 @@ class App: + # GTK apps exit when the last window is closed + CLOSE_ON_LAST_WINDOW = True + def __init__(self, interface): self.interface = interface self.interface._impl = self @@ -24,9 +27,6 @@ def __init__(self, interface): gbulb.install(gtk=True) self.loop = asyncio.new_event_loop() - self.create() - - def create(self): # Stimulate the build of the app self.native = Gtk.Application( application_id=self.interface.app_id, @@ -36,6 +36,7 @@ def create(self): # Connect the GTK signal that will cause app startup to occur self.native.connect("startup", self.gtk_startup) + # Activate is a no-op, but GTK complains if you don't implement it self.native.connect("activate", self.gtk_activate) self.actions = None @@ -46,14 +47,6 @@ def gtk_activate(self, data=None): def gtk_startup(self, data=None): self.interface._startup() - # Now that we have menus, make the app take responsibility for - # showing the menubar. - # This is required because of inconsistencies in how the Gnome - # shell operates on different windowing environments; - # see #872 for details. - settings = Gtk.Settings.get_default() - settings.set_property("gtk-shell-shows-menubar", False) - # Set any custom styles css_provider = Gtk.CssProvider() css_provider.load_from_data(TOGA_DEFAULT_STYLES) @@ -178,6 +171,14 @@ def create_menus(self): # Set the menu for the app. self.native.set_menubar(menubar) + # Now that we have menus, make the app take responsibility for + # showing the menubar. + # This is required because of inconsistencies in how the Gnome + # shell operates on different windowing environments; + # see #872 for details. + settings = Gtk.Settings.get_default() + settings.set_property("gtk-shell-shows-menubar", False) + ###################################################################### # App lifecycle ###################################################################### @@ -190,14 +191,21 @@ def main_loop(self): # Modify signal handlers to make sure Ctrl-C is caught and handled. signal.signal(signal.SIGINT, signal.SIG_DFL) + # Retain a reference to the app so that no-window apps can exist + self.native.hold() + self.loop.run_forever(application=self.native) + # Release the reference to the app + self.native.release() + def set_icon(self, icon): for window in self.interface.windows: window._impl.native.set_icon(icon._impl.native(72)) def set_main_window(self, window): - pass + if isinstance(window, toga.Window): + window._impl.native.set_role("MainWindow") ###################################################################### # App resources diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index b5178d89b4..195433b84d 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -4,6 +4,7 @@ from rubicon.objc import objc_method from rubicon.objc.eventloop import EventLoopPolicy, iOSLifecycle +import toga from toga.command import Command from toga.handlers import simple_handler from toga_iOS.libs import UIResponder, UIScreen, av_foundation @@ -51,6 +52,9 @@ def application_didChangeStatusBarOrientation_( class App: + # iOS apps exit when the last window is closed + CLOSE_ON_LAST_WINDOW = True + def __init__(self, interface): self.interface = interface self.interface._impl = self @@ -110,7 +114,10 @@ def set_icon(self, icon): pass # pragma: no cover def set_main_window(self, window): - pass + if window is None: + raise RuntimeError("Session-based apps are not supported on Android") + elif window == toga.App.BACKGROUND: + raise RuntimeError("Background apps are not supported on Android") ###################################################################### # App resources diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index d0ac5aaa1a..9a9e13d78e 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -1,5 +1,6 @@ import asyncio +import toga from textual.app import App as TextualApp from .screens import Screen as ScreenImpl @@ -16,6 +17,9 @@ def on_mount(self) -> None: class App: + # Textual apps exit when the last window is closed + CLOSE_ON_LAST_WINDOW = True + def __init__(self, interface): self.interface = interface self.interface._impl = self @@ -54,7 +58,12 @@ def set_icon(self, icon): pass def set_main_window(self, window): - self.native.push_screen(self.interface.main_window.id) + if window is None: + raise RuntimeError("Session-based apps are not supported on Textual") + elif window == toga.App.BACKGROUND: + raise RuntimeError("Background apps are not supported on Textual") + else: + self.native.push_screen(self.interface.main_window.id) ###################################################################### # App resources diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index 983f75015e..f86781a177 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -1,3 +1,4 @@ +import toga from toga.app import overridden from toga.command import Command, Group from toga.handlers import simple_handler @@ -7,13 +8,16 @@ class App: + # Web apps exit when the last window is closed + CLOSE_ON_LAST_WINDOW = True + def __init__(self, interface): self.interface = interface self.interface._impl = self def create(self): - # self.resource_path = os.path.dirname(os.path.dirname(NSBundle.mainBundle.bundlePath)) self.native = js.document.getElementById("app-placeholder") + # self.resource_path = ??? # Call user code to populate the main window self.interface._startup() @@ -70,7 +74,10 @@ def set_icon(self, icon): pass def set_main_window(self, window): - pass + if window is None: + raise RuntimeError("Session-based apps are not supported on Web") + elif window == toga.App.BACKGROUND: + raise RuntimeError("Background apps are not supported on Web") ###################################################################### # App resources diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 4ec29d7cf4..e2b783a139 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -10,7 +10,7 @@ from System.Net import SecurityProtocolType, ServicePointManager from System.Windows.Threading import Dispatcher -from toga import Key +import toga from toga.app import overridden from toga.command import Command, Group from toga.handlers import simple_handler @@ -55,6 +55,9 @@ def print_stack_trace(stack_trace_line): # pragma: no cover class App: + # Winforms apps exit when the last window is closed + CLOSE_ON_LAST_WINDOW = True + def __init__(self, interface): self.interface = interface self.interface._impl = self @@ -126,8 +129,6 @@ def create(self): # Call user code to populate the main window self.interface._startup() - self.interface.main_window._impl.set_app(self) - ###################################################################### # Commands and menus ###################################################################### @@ -154,7 +155,7 @@ def create_standard_app_commands(self): Command( self.interface.on_exit, "Exit", - shortcut=Key.MOD_1 + "q", + shortcut=toga.Key.MOD_1 + "q", group=Group.FILE, section=sys.maxsize, id=Command.EXIT, @@ -233,7 +234,10 @@ def set_icon(self, icon): window._impl.native.Icon = icon._impl.native def set_main_window(self, window): - self.app_context.MainForm = window._impl.native + if isinstance(window, toga.Window): + self.app_context.MainForm = window._impl.native + else: + self.app_context.MainForm = None ###################################################################### # App resources @@ -319,7 +323,7 @@ def create_app_commands(self): Command( lambda w: self.open_file, text="Open...", - shortcut=Key.MOD_1 + "o", + shortcut=toga.Key.MOD_1 + "o", group=Group.FILE, section=0, ),