diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 7cd34a7567..e625a7fe4d 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -191,21 +191,21 @@ def native(self): # App lifecycle ###################################################################### - def create(self): - # The `_listener` listens for activity event callbacks. For simplicity, - # the app's `.native` is the listener's native Java class. - self._listener = TogaApp(self) - # Call user code to populate the main window - self.interface._startup() - self.create_app_commands() - def main_loop(self): # In order to support user asyncio code, start the Python/Android cooperative event loop. self.loop.run_forever_cooperatively() - # On Android, Toga UI integrates automatically into the main Android event loop by virtue - # of the Android Activity system. - self.create() + # On Android, Toga UI integrates automatically into the main Android event loop + # by virtue of the Android Activity system. The `_listener` listens for activity + # event callbacks. For simplicity, the app's `.native` is the listener's native + # Java class. + self._listener = TogaApp(self) + # Call user code to populate the main window + self.interface._startup() + + def finalize(self): + self.create_app_commands() + self.create_menus() def set_main_window(self, window): if window is None: diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 8b14200876..01993a84cb 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -27,8 +27,6 @@ def onGlobalLayout(self): class Window(Container): - has_titlebar = False - def __init__(self, interface, title, position, size): super().__init__() self.interface = interface @@ -48,8 +46,11 @@ def set_app(self, app): ) self.set_title(self._initial_title) - if not self.has_titlebar: - self.app.native.getSupportActionBar().hide() + self._configure_titlebar() + + def _configure_titlebar(self): + # Simple windows hide the titlebar. + self.app.native.getSupportActionBar().hide() def get_title(self): return str(self.app.native.getTitle()) @@ -121,4 +122,6 @@ def get_image_data(self): class MainWindow(Window): - has_titlebar = True + def _configure_titlebar(self): + # The titlebar will be visible by default. + pass diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 10666bd8bf..b8cc88b7df 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 @@ -68,10 +67,6 @@ def applicationShouldOpenUntitledFile_(self, sender) -> bool: @objc_method def application_openFiles_(self, app, filenames) -> None: - # If there's no document types registered, we can't open files. - if not self.interface.document_types: - return - for i in range(0, len(filenames)): filename = filenames[i] # If you start your Toga application as `python myapp.py` or @@ -93,7 +88,14 @@ def application_openFiles_(self, app, filenames) -> None: else: return - self.impl.open_document(str(fileURL.absoluteString)) + # Convert a Cocoa fileURL to a Python file path. + path = Path(unquote(urlparse(str(fileURL.absoluteString)).path)) + if hasattr(self.interface, "open"): + # If the user has provided an `open()` method on the app, use it. + self.interface.open(path) + elif self.interface.document_types: + # Use the document-based default implementation. + self.interface._open(path) @objc_method def selectMenuItem_(self, sender) -> None: @@ -122,30 +124,17 @@ def __init__(self, interface): self.native = NSApplication.sharedApplication self.native.setApplicationIconImage(self.interface.icon._impl.native) - 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) # Call user code to populate the main window self.interface._startup() - # Add any platform-specific app commands. This is done *after* startup to ensure - # that the main_window has been assigned, which informs which app commands are - # needed. - self.create_app_commands() - - # Create the lookup table of menu and status items, - # then force the creation of the menus. - self._menu_groups = {} - self._menu_items = {} - self.create_menus() - ###################################################################### # App lifecycle ###################################################################### @@ -153,6 +142,18 @@ def __init__(self, interface): def main_loop(self): self.loop.run_forever(lifecycle=CocoaLifecycle(self.native)) + def finalize(self): + # Set up the lookup tables for menu items + self._menu_groups = {} + self._menu_items = {} + + # Add any platform-specific app commands. This is done during finalization to + # ensure that the main_window has been assigned, which informs which app + # commands are needed. + self.create_app_commands() + + self.create_menus() + def set_main_window(self, window): # If it's a background app, don't display the app icon. if window == toga.App.BACKGROUND: @@ -243,9 +244,11 @@ def create_app_commands(self): ), ) + # Register the Apple HIG commands unless the app is using a simple Window + # as the main window, or it's a background app. if ( isinstance(self.interface.main_window, toga.MainWindow) - or self.interface.main_window is None + and self.interface.main_window != toga.App.BACKGROUND ): self.interface.commands.add( toga.Command( @@ -371,12 +374,12 @@ def create_app_commands(self): ), ) - # If document types have been registered, provide file manipulation commands. + # Add a "New" menu item for each registered document type. if self.interface.document_types: for document_class in self.interface.document_types.values(): self.interface.commands.add( toga.Command( - self._menu_new_file(document_class), + self._menu_new_document(document_class), text=f"New {document_class.document_type}", shortcut=( toga.Key.MOD_1 + "n" @@ -387,6 +390,10 @@ def create_app_commands(self): section=0, ), ) + + # If there's a user-provided open() implementation, or there are registered + # document types, add an Open menu item. + if hasattr(self.interface, "open") or self.interface.document_types: self.interface.commands.add( toga.Command( self._menu_open_file, @@ -396,6 +403,11 @@ def create_app_commands(self): section=10, ), ) + + # TODO: implement a save interface. + # If there are registered document types, add dummy Save/Save as/Save All menu + # items. + if self.interface.document_types: self.interface.commands.add( toga.Command( None, @@ -432,7 +444,7 @@ def _menu_about(self, command, **kwargs): def _menu_quit(self, command, **kwargs): self.interface.on_exit() - def _menu_new_file(self, document_class): + def _menu_new_document(self, document_class): def new_file_handler(app, **kwargs): self.interface._new(document_class) @@ -566,6 +578,13 @@ def show_about_dialog(self): self.native.orderFrontStandardAboutPanelWithOptions(options) + def beep(self): + NSBeep() + + ###################################################################### + # Platform utilities + ###################################################################### + def select_file(self, **kwargs): # FIXME This should be all we need; but for some reason, application types # aren't being registered correctly.. @@ -585,14 +604,3 @@ def select_file(self, **kwargs): # print("Untitled File opened?", panel.URLs) self.appDelegate.application(None, openFiles=panel.URLs) - - def open_document(self, fileURL): - # Convert a cocoa fileURL to a file path. - fileURL = fileURL.rstrip("/") - path = Path(unquote(urlparse(fileURL).path)) - - # Create and show the document instance - self.interface._open(path) - - def beep(self): - NSBeep() diff --git a/cocoa/src/toga_cocoa/documents.py b/cocoa/src/toga_cocoa/documents.py index d47c8ae0a9..b44b847cc1 100644 --- a/cocoa/src/toga_cocoa/documents.py +++ b/cocoa/src/toga_cocoa/documents.py @@ -27,7 +27,7 @@ def __init__(self, interface): self.native.interface = interface self.native.impl = self - def load(self): + def open(self): self.native.initWithContentsOfURL( NSURL.URLWithString(f"file://{quote(os.fsdecode(self.interface.path))}"), ofType=self.interface.document_type, diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 660d9040b2..b6a9d5c881 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -161,6 +161,8 @@ def __init__(self, interface, title, position, size): self.set_title(title) self.set_size(size) + + # Cascade the position of new windows. pos = 100 + len(App.app.windows) * 40 self.set_position(position if position else (pos, pos)) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 1a6d1b0ced..6ead9447de 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -389,9 +389,6 @@ def __init__( self.factory.App(interface=self) - # Now that we have an impl, set the on_change handler for commands - self.commands.on_change = self._impl.create_menus - ###################################################################### # App properties ###################################################################### @@ -543,6 +540,15 @@ def _startup(self): # the app hasn't defined a main window. _ = self.main_window + # The App's impl is created when the app is constructed; however, on some + # platforms, (GTK, Windows), there are some activities that can't happen until + # the app manifests in some way (usually as a result of the app loop starting). + # Call the impl to allow for this finalization activity. + self._impl.finalize() + + # Now that we have a finalized impl, set the on_change handler for commands + self.commands.on_change = self._impl.create_menus + @property def main_window(self) -> MainWindow: """The main window for the app.""" @@ -717,7 +723,7 @@ def _open(self, path): raise ValueError(f"Don't know how to open documents of type {path.suffix}") else: document = DocType(app=self) - document.load(path) + document.open(path) self._documents.append(document) document.show() diff --git a/core/src/toga/command.py b/core/src/toga/command.py index 0a9e1514ca..0a918ef17f 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -1,6 +1,5 @@ from __future__ import annotations -from contextlib import contextmanager from typing import TYPE_CHECKING, Any, Protocol from toga.handlers import wrapped_handler @@ -333,23 +332,6 @@ def clear(self): if self.on_change: self.on_change() - @contextmanager - def suspend_updates(self): - """Temporarily suspend ``on_change`` updates. - - Inside this context manager, the ``on_change`` handler will not be - invoked if the CommandSet is modified. The ``on_change`` handler (if it - exists0 will be invoked when the context manager exits. - """ - orig_on_change = self.on_change - self.on_change = None - try: - yield - finally: - self.on_change = orig_on_change - if self.on_change: - self.on_change() - @property def app(self) -> App: return self._app diff --git a/core/src/toga/documents.py b/core/src/toga/documents.py index b84dc25208..59c29c60aa 100644 --- a/core/src/toga/documents.py +++ b/core/src/toga/documents.py @@ -11,7 +11,7 @@ class Document(ABC): - # Subclasses should override this definition this. + # Subclasses should override this definition. document_type = "Unknown Document" def __init__(self, app: App): @@ -82,9 +82,9 @@ def show(self) -> None: """Show the :any:`main_window` for this document.""" self.main_window.show() - def load(self, path: str | Path): + def open(self, path: str | Path): self._path = Path(path) - self._impl.load() + self._impl.open() @abstractmethod def create(self) -> None: diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 7b20f3d96d..f82a037539 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -367,7 +367,7 @@ def cleanup(window: Window, should_close: bool) -> None: # Closing the window marked as the main window exits the app self.app.on_exit() elif self.app.main_window == self.app.BACKGROUND: - # Otherwise (i.e., background apps) + # In a background app, closing a window has no special handling. window.close() else: # Otherwise: closing the *last* window exits the app (if platform diff --git a/core/tests/test_documents.py b/core/tests/test_documents.py index 7c611a2c3e..992b1fad7f 100644 --- a/core/tests/test_documents.py +++ b/core/tests/test_documents.py @@ -36,7 +36,7 @@ def test_create_document(app, path): assert doc.path is None assert doc.app == app - doc.load(path) + doc.open(path) assert doc.path == Path("/path/to/doc.mydoc") @@ -51,8 +51,8 @@ def test_new_document(app): assert_action_performed(doc.main_window, "create Window") assert_action_performed(doc.main_window, "show") - # No load operation occurred - assert_action_not_performed(doc, "load") + # No open operation occurred + assert_action_not_performed(doc, "open") assert doc._data is None @@ -70,8 +70,8 @@ def test_open_document(app): assert_action_performed(doc.main_window, "create Window") assert_action_performed(doc.main_window, "show") - # A load operation was performed - assert_action_performed(doc, "load document") + # A open operation was performed + assert_action_performed(doc, "open document") assert doc._data == "content" diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index d41728da07..7bcdf48dd0 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -17,6 +17,9 @@ def __init__(self, interface): self._action("create App") self.interface._startup() + def finalize_create(self): + self._action("finalize creation") + def create_menus(self): self._action("create App menus") diff --git a/dummy/src/toga_dummy/documents.py b/dummy/src/toga_dummy/documents.py index 1ad319ec74..fb33f8329e 100644 --- a/dummy/src/toga_dummy/documents.py +++ b/dummy/src/toga_dummy/documents.py @@ -6,6 +6,6 @@ def __init__(self, interface): super().__init__() self.interface = interface - def load(self): - self._action("load document") + def open(self): + self._action("open document") self.interface.read() diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 35b507a32d..dbe9f0a47f 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -42,38 +42,7 @@ def __init__(self, interface): ###################################################################### def gtk_startup(self, data=None): - # As the call to _startup() isn't made in the constructor, the on_change - # handler for commands is already installed. We don't want to start creating - # menus until after _startup() and any default app commands have been created, - # so suspend updates; this will call create_menus when the context exits. - with self.interface.commands.suspend_updates(): - self.interface._startup() - - self.create_app_commands() - - # Set any custom styles - css_provider = Gtk.CssProvider() - css_provider.load_from_data(TOGA_DEFAULT_STYLES) - - context = Gtk.StyleContext() - context.add_provider_for_screen( - Gdk.Screen.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER - ) - - if self.interface._is_document_app: - # If this is a document-based app, open every argument on the command line - # as a document. If no arguments were provided, or no valid filenames were - # provided, open a blank document. - for filename in sys.argv[1:]: - try: - self.interface._open(Path(filename)) - except ValueError as e: - print(e) - except FileNotFoundError: - print("Document {filename} not found") - - if len(self.interface.documents) == 0: - self.interface._new(self.interface.main_window) + self.interface._startup() def gtk_activate(self, data=None): pass @@ -91,6 +60,37 @@ def main_loop(self): # Release the reference to the app self.native.release() + def finalize(self): + # Set any custom styles + css_provider = Gtk.CssProvider() + css_provider.load_from_data(TOGA_DEFAULT_STYLES) + + context = Gtk.StyleContext() + context.add_provider_for_screen( + Gdk.Screen.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER + ) + + # Create the app commands and populate app menus. + self.create_app_commands() + self.create_menus() + + # If this is a document-based app, open every argument on the command line + # as a document. If no arguments were provided, or no valid filenames were + # provided, open a blank document. + if self.interface.main_window is None: + for filename in sys.argv[1:]: + print("OPEN", Path(filename)) + # TODO: Finalize this... + # try: + # self.interface._open(Path(filename)) + # except ValueError as e: + # print(e) + # except FileNotFoundError: + # print("Document {filename} not found") + + # if len(self.interface.documents) == 0: + # self.interface._new(self.interface.main_window) + def set_main_window(self, window): if isinstance(window, toga.Window): window._impl.native.set_role("MainWindow") diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index ba48bf7321..fc3fddad96 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -66,10 +66,6 @@ def __init__(self, interface): # App lifecycle ###################################################################### - def create(self): - """Calls the startup method on the interface.""" - self.interface._startup() - def main_loop(self): # Main loop is non-blocking on iOS. The app loop is integrated with the # main iOS event loop, so this call will return; however, it will leave @@ -77,6 +73,14 @@ def main_loop(self): # iOS event loop. self.loop.run_forever_cooperatively(lifecycle=iOSLifecycle()) + def create(self): + # Call the app's startup method + self.interface._startup() + + def finalize(self): + self.create_app_commands() + self.create_menus() + def set_main_window(self, window): if window is None: raise RuntimeError("Document-based apps are not supported on mobile") @@ -119,6 +123,9 @@ def exit_full_screen(self, windows): # Command definitions ###################################################################### + def create_app_commands(self): + pass + ###################################################################### # Menu creation ###################################################################### diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index d2939e1350..0fbc8da4c1 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -21,28 +21,37 @@ def __init__(self, interface): self.loop = asyncio.new_event_loop() self.native = TogaApp(self) + ###################################################################### + # App lifecycle + ###################################################################### + def create(self): self.interface._startup() self.set_current_window(self.interface.main_window._impl) - def create_menus(self): - pass - def main_loop(self): self.native.run() + def finalize(self): + self.create_app_commands() + self.create_menus() + def set_main_window(self, window): self.native.push_screen(self.interface.main_window.id) - def show_about_dialog(self): - pass - - def beep(self): - self.native.bell() - def exit(self): self.native.exit() + ###################################################################### + # Cursor and Window control + ###################################################################### + + def show_cursor(self): + pass + + def hide_cursor(self): + pass + def get_current_window(self): pass @@ -56,8 +65,26 @@ def enter_full_screen(self, windows): def exit_full_screen(self, windows): pass - def show_cursor(self): + ###################################################################### + # Command definitions + ###################################################################### + + def create_app_commands(self): pass - def hide_cursor(self): + ###################################################################### + # Menu creation + ###################################################################### + + def create_menus(self): pass + + ###################################################################### + # App functionality + ###################################################################### + + def show_about_dialog(self): + pass + + def beep(self): + self.native.bell() diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index 97d1316df2..2e8db90e60 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -8,17 +8,71 @@ def __init__(self, interface): self.interface = interface self.interface._impl = self + ###################################################################### + # App lifecycle + ###################################################################### + + def main_loop(self): + self.create() + def create(self): # self.resource_path = os.path.dirname(os.path.dirname(NSBundle.mainBundle.bundlePath)) self.native = js.document.getElementById("app-placeholder") - formal_name = self.interface.formal_name + # Create a stub header to hold the menubar. + self.native.append( + create_element( + "header", + id=f"{self.interface.app_id}-header", + classes=["toga"], + ) + ) + + # Call user code to populate the main window + self.interface._startup() + + def finalize(self): + self.create_app_commands() + self.create_menus() + + def set_main_window(self, window): + pass + + def exit(self): + pass + + ###################################################################### + # Cursor and Window control + ###################################################################### + def show_cursor(self): + self.interface.factory.not_implemented("App.show_cursor()") + + def hide_cursor(self): + self.interface.factory.not_implemented("App.hide_cursor()") + + def get_current_window(self): + self.interface.factory.not_implemented("App.get_current_window()") + + def set_current_window(self): + self.interface.factory.not_implemented("App.set_current_window()") + + def enter_full_screen(self, windows): + self.interface.factory.not_implemented("App.enter_full_screen()") + + def exit_full_screen(self, windows): + self.interface.factory.not_implemented("App.exit_full_screen()") + + ###################################################################### + # Command definitions + ###################################################################### + + def create_app_commands(self): self.interface.commands.add( # ---- Help menu ---------------------------------- toga.Command( self._menu_about, - "About " + formal_name, + "About " + self.interface.formal_name, group=toga.Group.HELP, ), toga.Command( @@ -28,13 +82,9 @@ def create(self): ), ) - # Create the menus. This is done before main window content to ensure - # the
for the menubar is inserted before the
for the - # main window. - self.create_menus() - - # Call user code to populate the main window - self.interface._startup() + ###################################################################### + # Menu creation + ###################################################################### def _create_submenu(self, group, items): submenu = create_element( @@ -120,21 +170,15 @@ def create_menus(self): ], ) - # If there's an existing menubar, replace it. - old_menubar = js.document.getElementById(menubar_id) - if old_menubar: - old_menubar.replaceWith(self.menubar) - else: - self.native.append(self.menubar) + # Replace the existing menubar. + js.document.getElementById(menubar_id).replaceWith(self.menubar) def _menu_about(self, command, **kwargs): self.interface.about() - def main_loop(self): - self.create() - - def set_main_window(self, window): - pass + ###################################################################### + # App functionality + ###################################################################### def show_about_dialog(self): name_and_version = f"{self.interface.formal_name}" @@ -182,24 +226,3 @@ def show_dialog(promise): def beep(self): self.interface.factory.not_implemented("App.beep()") - - def exit(self): - pass - - def get_current_window(self): - self.interface.factory.not_implemented("App.get_current_window()") - - def set_current_window(self): - self.interface.factory.not_implemented("App.set_current_window()") - - def enter_full_screen(self, windows): - self.interface.factory.not_implemented("App.enter_full_screen()") - - def exit_full_screen(self, windows): - self.interface.factory.not_implemented("App.exit_full_screen()") - - def show_cursor(self): - self.interface.factory.not_implemented("App.show_cursor()") - - def hide_cursor(self): - self.interface.factory.not_implemented("App.hide_cursor()") diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index a14fb8c699..7ebae9e87e 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -135,29 +135,28 @@ def create(self): "You may experience difficulties accessing some web server content." ) - # As creation has been deferred, the on_change handler for commands has already - # been installed - even though we haven't added any commands yet. Temporarily - # suspend on_change events so we can create all the commands. This will call - # create_menus when when context exits. - with self.interface.commands.suspend_updates(): - # Call user code to populate the main window - self.interface._startup() - self.create_app_commands() - - if self.interface._is_document_app: - # If this is a document-based app, open every argument on the command line - # as a document. If no arguments were provided, or no valid filenames were - # provided, open a blank document. + self.interface._startup() + + def finalize(self): + self.create_app_commands() + self.create_menus() + + # If this is a document-based app, open every argument on the command line + # as a document. If no arguments were provided, or no valid filenames were + # provided, open a blank document. + if self.interface.main_window is None: for filename in sys.argv[1:]: - try: - self.interface._open(Path(filename)) - except ValueError as e: - print(e) - except FileNotFoundError: - print("Document {filename} not found") - - if len(self.interface.documents) == 0: - self.interface._new(self.interface.main_window) + print("OPEN", Path(filename)) + # TODO: Finalize this... + # try: + # self.interface._open(Path(filename)) + # except ValueError as e: + # print(e) + # except FileNotFoundError: + # print("Document {filename} not found") + + # if len(self.interface.documents) == 0: + # self.interface._new(self.interface.main_window) def run_app(self): # pragma: no cover # Enable coverage tracing on this non-Python-created thread