Skip to content

Commit

Permalink
Tweak main app lifecycle to remove need for suspending updates.
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed Feb 11, 2024
1 parent b624d27 commit 30bb3f6
Show file tree
Hide file tree
Showing 17 changed files with 256 additions and 196 deletions.
22 changes: 11 additions & 11 deletions android/src/toga_android/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 8 additions & 5 deletions android/src/toga_android/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -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
80 changes: 44 additions & 36 deletions cocoa/src/toga_cocoa/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import asyncio
import inspect
import os
import sys
from pathlib import Path
from urllib.parse import unquote, urlparse
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -122,37 +124,36 @@ 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
######################################################################

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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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..
Expand All @@ -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()
2 changes: 1 addition & 1 deletion cocoa/src/toga_cocoa/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions cocoa/src/toga_cocoa/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
14 changes: 10 additions & 4 deletions core/src/toga/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
######################################################################
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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()
Expand Down
18 changes: 0 additions & 18 deletions core/src/toga/command.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions core/src/toga/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion core/src/toga/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions core/tests/test_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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


Expand All @@ -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"


Expand Down
3 changes: 3 additions & 0 deletions dummy/src/toga_dummy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Loading

0 comments on commit 30bb3f6

Please sign in to comment.