diff --git a/core/src/toga/app.py b/core/src/toga/app.py index c2c9d6772e..aca728de74 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -10,20 +10,20 @@ from toga.command import Command, CommandSet from toga.constants import WindowState +from toga.documents import Document, DocumentSet from toga.handlers import simple_handler, wrapped_handler from toga.icons import Icon from toga.paths import Paths from toga.platform import get_platform_factory +from toga.statusicons import StatusIconSet from toga.window import MainWindow, Window, WindowSet if TYPE_CHECKING: from toga.dialogs import Dialog - from toga.documents import Document, DocumentSet from toga.hardware.camera import Camera from toga.hardware.location import Location from toga.icons import IconContentT from toga.screens import Screen - from toga.statusicons import StatusIconSet from toga.widgets.base import Widget # Make sure deprecation warnings are shown by default @@ -316,7 +316,10 @@ def __init__( self.icon = icon # Set up the document types and collection of documents being managed. - self._document_types = [] if document_types is None else document_types + self._documents = DocumentSet( + self, + types=[] if document_types is None else document_types, + ) # Install the lifecycle handlers. If passed in as an argument, or assigned using # `app.on_event = my_handler`, the event handler will take the app as the first @@ -335,6 +338,7 @@ def __init__( # We need the command set to exist so that startup et al. can add commands; # but we don't have an impl yet, so we can't set the on_change handler self._commands = CommandSet() + self._status_icons = StatusIconSet() self._startup_method = startup @@ -578,22 +582,22 @@ def _create_standard_commands(self): ]: self.commands.add(Command.standard(self, cmd_id)) - if self._document_types: - default_document_type = self._document_types[0] + if self.documents.types: + default_document_type = self.documents.types[0] command = Command.standard( self, Command.NEW, action=simple_handler(self.documents.new, default_document_type), ) if command: - if len(self._document_types) == 1: + if len(self.documents.types) == 1: # There's only 1 document type. The new command can be used as is. self.commands.add(command) else: # There's more than one document type. Create a new command for each # document type, updating the title of the command to disambiguate, # and modifying the shortcut, order and ID of the document types 2+ - for i, document_class in enumerate(self._document_types): + for i, document_class in enumerate(self.documents.types): command = Command.standard( self, Command.NEW, @@ -628,16 +632,16 @@ def _create_initial_windows(self): """ # Process command line arguments if the backend doesn't handle them if not self._impl.HANDLES_COMMAND_LINE: - if self._document_types: + if self.documents.types: for filename in sys.argv[1:]: self._open_initial_document(filename) # Ensure there is at least one window if self.main_window is None and len(self.windows) == 0: - if self._document_types: + if self.documents.types: if self._impl.CLOSE_ON_LAST_WINDOW: # Pass in the first document type as the default - self.documents.new(self._document_types[0]) + self.documents.new(self.documents.types[0]) else: self.loop.run_until_complete(self.documents.request_open()) else: @@ -732,14 +736,7 @@ def commands(self) -> CommandSet: @property def documents(self) -> DocumentSet: """The list of documents associated with this app.""" - try: - return self._documents - except AttributeError: - # Initialize on first access. - from .documents import DocumentSet - - self._documents = DocumentSet(self, self._document_types) - return self._documents + return self._documents @property def location(self) -> Location: @@ -773,14 +770,7 @@ def screens(self) -> list[Screen]: @property def status_icons(self) -> StatusIconSet: """The status icons displayed by the app.""" - try: - return self._status_icons - except AttributeError: - # Initialize on first access. - from .statusicons import StatusIconSet - - self._status_icons = StatusIconSet() - return self._status_icons + return self._status_icons @property def widgets(self) -> WidgetRegistry: @@ -1064,6 +1054,6 @@ def document_types(self) -> dict[str, type[Document]]: ) return { extension: doc_type - for doc_type in self._document_types + for doc_type in self.documents.types for extension in doc_type.extensions } diff --git a/core/src/toga/documents.py b/core/src/toga/documents.py index 0dd9fa30e4..4a6bc171c3 100644 --- a/core/src/toga/documents.py +++ b/core/src/toga/documents.py @@ -9,7 +9,6 @@ import toga from toga import dialogs -from toga.handlers import overridable, overridden from toga.window import MainWindow, Window if TYPE_CHECKING: @@ -117,13 +116,15 @@ def open(self, path: str | Path): def save(self, path: str | Path | None = None): """Save the document as a file. - If a path is provided, and the :meth:`~toga.Document.write` method has been - overwritten, the path for the document will be updated. Otherwise, the existing - path will be used. + If a path is provided, the path for the document will be updated. Otherwise, the + existing path will be used. + + If the :meth:`~toga.Document.write` method has not been implemented, this method + is a no-op. :param path: If provided, the new file name for the document. """ - if overridden(self.write): + if self._writable(): if path: self._path = Path(path).absolute() # Re-set the title of the document with the new path @@ -132,6 +133,10 @@ def save(self, path: str | Path | None = None): # Clear the modification flag. self.modified = False + # A document is writable if its class overrides the `write` method. + def _writable(self): + return getattr(type(self), "write") is not Document.write + def show(self) -> None: """Show the visual representation for this document.""" self.main_window.show() @@ -162,7 +167,6 @@ def read(self) -> None: :attr:`~toga.Document.path`, and populate the document window. """ - @overridable def write(self) -> None: """Persist a representation of the current state of the document. @@ -452,13 +456,13 @@ def _close(self): async def save(self): """Save the document associated with this window. - If the document associated with a window hasn't been saved before, and the - document type defines a :meth:`~toga.Document.write` method, the user will be - prompted to provide a filename. + If the document associated with a window hasn't been saved before, the user will + be prompted to provide a filename. - :returns: True if the save was successful; False if the save was aborted. + :returns: True if the save was successful; False if the save was aborted, or the + document type doesn't define a :meth:`~toga.Document.write` method. """ - if overridden(self.doc.write): + if self.doc._writable(): if self.doc.path: # Document has been saved previously; save using that filename. self.doc.save() @@ -471,12 +475,12 @@ async def save_as(self): """Save the document associated with this window under a new filename. The default implementation will prompt the user for a new filename, then save - the document with that new filename. If the document type doesn't define a - :meth:`~toga.Document.write` method, the save-as request will be ignored. + the document with that new filename. - :returns: True if the save was successful; False if the save was aborted. + :returns: True if the save was successful; False if the save was aborted, or the + document type doesn't define a :meth:`~toga.Document.write` method. """ - if overridden(self.doc.write): + if self.doc._writable(): suggested_path = ( self.doc.path if self.doc.path else f"Untitled.{self.doc.extensions[0]}" ) diff --git a/core/src/toga/handlers.py b/core/src/toga/handlers.py index afc5606e92..4b5764578f 100644 --- a/core/src/toga/handlers.py +++ b/core/src/toga/handlers.py @@ -29,21 +29,6 @@ WrappedHandlerT: TypeAlias = Callable[..., object] -def overridable(method: T) -> T: - """Decorate the method as being user-overridable""" - method._overridden = True - return method - - -def overridden(coroutine_or_method: Callable) -> bool: - """Has the user overridden this method? - - This is based on the method *not* having a ``_overridden`` attribute. Overridable - default methods have this attribute; user-defined method will not. - """ - return not hasattr(coroutine_or_method, "_overridden") - - class NativeHandler: def __init__(self, handler: Callable[..., object]): self.native = handler