diff --git a/changes/2209.feature.rst b/changes/2209.feature.rst new file mode 100644 index 0000000000..8e104b0247 --- /dev/null +++ b/changes/2209.feature.rst @@ -0,0 +1 @@ +The API for Documents and document types has been finalized. diff --git a/changes/2209.removal.rst b/changes/2209.removal.rst new file mode 100644 index 0000000000..66ebbb5c72 --- /dev/null +++ b/changes/2209.removal.rst @@ -0,0 +1,8 @@ +The API for Documents and Document-based apps has been significantly modified. Unfortunately. these changes are not backwards compatible; any existing Document-based app will require modification. + +The ``DocumentApp`` base class is no longer required. Apps can subclass ``App``, providing the same arguments as before. + +The API for ``Document`` subclasses has also changed: +* A path is no longer provided at time of construction; +* The ``document_type`` is now specified as a class property; and +* The ``can_close()`` handler is no longer automatically honored. It should be converted into a ``on_close`` handler and explicitly installed on the document's ``main_window``. diff --git a/core/src/toga/app.py b/core/src/toga/app.py index e8a72c0ef4..0e2ca85a9e 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -226,6 +226,7 @@ def __init__( home_page: str | None = None, description: str | None = None, startup: AppStartupMethod | None = None, + document_types: dict[str, type[Document]] = None, on_exit: OnExitHandler | None = None, id: None = None, # DEPRECATED windows: None = None, # DEPRECATED @@ -263,6 +264,8 @@ def __init__( the metadata key ``Summary`` will be used. :param startup: A callable to run before starting the app. :param on_exit: The initial :any:`on_exit` handler. + :param document_types: A mapping of document types managed by this app, to + the :any:`Document` class managing that document type. :param id: **DEPRECATED** - This argument will be ignored. If you need a machine-friendly identifier, use ``app_id``. :param windows: **DEPRECATED** – Windows are now automatically added to the @@ -379,6 +382,10 @@ def __init__( self.on_exit = on_exit + # Set up the document types and list of documents being managed. + self._document_types = document_types + self._documents = [] + # 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() @@ -391,9 +398,6 @@ def __init__( self._full_screen_windows: tuple[Window, ...] | None = None # Create the implementation. This will trigger any startup logic. - self._create_impl() - - def _create_impl(self) -> None: self.factory.App(interface=self) ###################################################################### @@ -502,14 +506,17 @@ def create_app_commands(self) -> None: This method is called automatically after :meth:`~toga.App.startup()` has completed, but before menus have been created. - By default, it will create the commands that are appropriate for your platform. - You can override this method to modify (or remove entirely) the default platform - commands that have been created. + By default, it will create the commands that are appropriate for your platform + and application type. You can override this method to modify (or remove + entirely) the default platform commands that have been created. """ self._impl.create_minimal_app_commands() - if isinstance(self.main_window, MainWindow): + if isinstance(self.main_window, MainWindow) or self.main_window is None: self._impl.create_standard_app_commands() + if self._document_types: + self._impl.create_document_type_commands() + def exit(self) -> None: """Exit the application gracefully. @@ -604,6 +611,23 @@ def on_app_running(): self.loop.call_soon_threadsafe(on_app_running) + def _create_initial_windows(self): + """Internal utility method for creating initial windows based on command line + arguments. This method is used when the platform doesn't provide it's own + command-line handling interface. + + If document types are defined, try to open every argument on the command line as + a document. If no document types are defined, this method does nothing. + """ + if self.document_types: + for filename in sys.argv[1:]: + try: + self.open(Path(filename).absolute()) + except ValueError as e: + print(e) + except FileNotFoundError: + print(f"Document {filename} not found") + def startup(self) -> None: """Create and show the main window for the application. @@ -648,6 +672,21 @@ def commands(self) -> CommandSet: """The commands available in the app.""" return self._commands + @property + def document_types(self) -> dict[str, type[Document]]: + """The document types this app can manage. + + A dictionary of file extensions, without leading dots, mapping to the + :class:`toga.Document` subclass that will be created when a document with that + extension is opened. + """ + return self._document_types + + @property + def documents(self) -> list[Document]: + """The list of documents associated with this app.""" + return self._documents + @property def location(self) -> Location: """A representation of the device's location service.""" @@ -710,6 +749,39 @@ def beep(self) -> None: """Play the default system notification sound.""" self._impl.beep() + async def _open(self, **kwargs): + # The menu interface to open(). Prompt the user to select a file; then open that + # file. + path = await self.current_window.open_file_dialog( + self.formal_name, + file_types=list(self.document_types.keys()), + ) + + if path: + self.open(path) + + def open(self, path: Path | str) -> None: + """Open a document in this app, and show the document window. + + The default implementation uses registered document types to open the file. Apps + can overwrite this implementation if they wish to provide custom behavior for + opening a file path. + + :param path: The path to the document to be opened. + :raises ValueError: If the path cannot be opened. + """ + try: + path = Path(path).absolute() + DocType = self.document_types[path.suffix[1:]] + except KeyError: + raise ValueError(f"Don't know how to open documents of type {path.suffix}") + else: + document = DocType(app=self) + document.open(path) + + self._documents.append(document) + document.show() + @overridable def preferences(self) -> None: """Open a preferences panel for the app. @@ -836,87 +908,13 @@ def windows(self, windows: WindowSet) -> None: class DocumentApp(App): - def __init__( - self, - formal_name: str | None = None, - app_id: str | None = None, - app_name: str | None = None, - *, - icon: IconContentT | None = None, - author: str | None = None, - version: str | None = None, - home_page: str | None = None, - description: str | None = None, - startup: AppStartupMethod | None = None, - document_types: dict[str, type[Document]] | None = None, - on_exit: OnExitHandler | None = None, - id: None = None, # DEPRECATED - ): - """Create a document-based application. - - A document-based application is the same as a normal application, with the - exception that there is no main window. Instead, each document managed by the - app will create and manage its own window (or windows). - - :param document_types: Initial :any:`document_types` mapping. + def __init__(self, *args, **kwargs): + """**DEPRECATED** - :any:`toga.DocumentApp` can be replaced with + :any:`toga.App`. """ - if document_types is None: - raise ValueError("A document must manage at least one document type.") - - self._document_types = document_types - self._documents: list[Document] = [] - - super().__init__( - formal_name=formal_name, - app_id=app_id, - app_name=app_name, - icon=icon, - author=author, - version=version, - home_page=home_page, - description=description, - startup=startup, - on_exit=on_exit, - id=id, + warnings.warn( + "toga.DocumentApp is no longer required. Use toga.App instead", + DeprecationWarning, + stacklevel=2, ) - - def _create_impl(self) -> None: - self.factory.DocumentApp(interface=self) - - @property - def document_types(self) -> dict[str, type[Document]]: - """The document types this app can manage. - - A dictionary of file extensions, without leading dots, mapping to the - :class:`toga.Document` subclass that will be created when a document with that - extension is opened. The subclass must take exactly 2 arguments in its - constructor: ``path`` and ``app``. - """ - return self._document_types - - @property - def documents(self) -> list[Document]: - """The list of documents associated with this app.""" - return self._documents - - def startup(self) -> None: - """No-op; a DocumentApp has no windows until a document is opened. - - Subclasses can override this method to define customized startup behavior. - """ - - def _open(self, path: Path) -> None: - """Internal utility method; open a new document in this app, and shows the document. - - :param path: The path to the document to be opened. - :raises ValueError: If the document is of a type that can't be opened. Backends can - suppress this exception if necessary to preserve platform-native behavior. - """ - try: - DocType = self.document_types[path.suffix[1:]] - except KeyError: - raise ValueError(f"Don't know how to open documents of type {path.suffix}") - else: - document = DocType(path, app=self) - self._documents.append(document) - document.show() + super().__init__(*args, **kwargs) diff --git a/core/src/toga/command.py b/core/src/toga/command.py index cdea78de31..a0a11eeb11 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -164,6 +164,8 @@ class Command: ABOUT: str = "about" #: An identifier for the system-installed "Exit" menu item EXIT: str = "on_exit" + #: An identifier for the system-installed "Open" menu item + OPEN: str = "open" #: An identifier for the system-installed "Preferences" menu item PREFERENCES: str = "preferences" #: An identifier for the system-installed "Visit Homepage" menu item diff --git a/core/src/toga/documents.py b/core/src/toga/documents.py index 3858a2d593..9d65a5879a 100644 --- a/core/src/toga/documents.py +++ b/core/src/toga/documents.py @@ -1,7 +1,5 @@ from __future__ import annotations -import asyncio -import warnings from abc import ABC, abstractmethod from pathlib import Path from typing import TYPE_CHECKING @@ -12,20 +10,17 @@ class Document(ABC): - def __init__( - self, - path: str | Path, - document_type: str, - app: App, - ): + # Subclasses should override this definition + + #: A short description of the type of document + document_type: str + + def __init__(self, app: App): """Create a new Document. - :param path: The path where the document is stored. - :param document_type: A human-readable description of the document type. :param app: The application the document is associated with. """ - self._path = Path(path) - self._document_type = document_type + self._path: Path | None = None self._app = app self._main_window: Window | None = None @@ -35,61 +30,15 @@ def __init__( # Create a platform specific implementation of the Document self._impl = app.factory.Document(interface=self) - # TODO: This will be covered when the document API is finalized - def can_close(self) -> bool: # pragma: no cover - """Is the main document window allowed to close? - - The default implementation always returns ``True``; subclasses can override this - to prevent a window closing with unsaved changes, etc. - - This default implementation is a function; however, subclasses can define it - as an asynchronous co-routine if necessary to allow for dialog confirmations. - """ - return True - - # TODO: This will be covered when the document API is finalized - async def handle_close( - self, window: Window, **kwargs: object - ) -> bool: # pragma: no cover - """An ``on-close`` handler for the main window of this document that implements - platform-specific document close behavior. - - It interrogates the :meth:`~toga.Document.can_close()` method to determine if - the document is allowed to close. - """ - if asyncio.iscoroutinefunction(self.can_close): - can_close = await self.can_close() - else: - can_close = self.can_close() - - if can_close: - if self._impl.SINGLE_DOCUMENT_APP: - self.app.exit() - return False - else: - return True - else: - return False + ###################################################################### + # Document properties + ###################################################################### @property def path(self) -> Path: """The path where the document is stored (read-only).""" return self._path - @property - def filename(self) -> Path: - """**DEPRECATED** - Use :attr:`path`.""" - warnings.warn( - "Document.filename has been renamed Document.path.", - DeprecationWarning, - ) - return self._path - - @property - def document_type(self) -> str: - """A human-readable description of the document type (read-only).""" - return self._document_type - @property def app(self) -> App: """The app that this document is associated with (read-only).""" @@ -104,10 +53,52 @@ def main_window(self) -> Window | None: def main_window(self, window: Window) -> None: self._main_window = window + @property + def title(self) -> str: + """The title of the document. + + This will be used as the default title of a :any:`toga.DocumentMainWindow` that + contains the document. + """ + return f"{self.document_type}: {self.path.name if self.path else 'Untitled'}" + + ###################################################################### + # Document operations + ###################################################################### + + def open(self, path: str | Path): + """Open a file as a document. + + :param path: The file to open. + """ + self._path = Path(path).absolute() + self._impl.open() + + # Set the title of the document window to match the path + self._main_window.title = self._main_window._default_title + + def save(self, path: str | Path | None = None): + """Save the document as a file. + + If a path is provided, the path for the document will be updated. + Otherwise, the existing path will be used. + + :param path: If provided, the new file name for the document. + """ + if path: + self._path = Path(path).absolute() + # Re-set the title of the document with the new path + self._main_window.title = self._main_window._default_title + self.write() + def show(self) -> None: - """Show the :any:`main_window` for this document.""" + """Show the visual representation for this document.""" self.main_window.show() + ###################################################################### + # Abstract interface + ###################################################################### + @abstractmethod def create(self) -> None: """Create the window (or windows) for the document. @@ -118,5 +109,12 @@ def create(self) -> None: @abstractmethod def read(self) -> None: - """Load a representation of the document into memory and populate the document - window.""" + """Load a representation of the document into memory from + :attr:`~toga.Document.path`, and populate the document window. + """ + + @abstractmethod + def write(self) -> None: + """Persist a representation of the current state of the document at the + location described by :attr:`~toga.Document.path`. + """ diff --git a/core/src/toga/handlers.py b/core/src/toga/handlers.py index 6119a8f054..15fc032ddc 100644 --- a/core/src/toga/handlers.py +++ b/core/src/toga/handlers.py @@ -90,26 +90,32 @@ async def handler_with_cleanup( return result -def simple_handler(fn): +def simple_handler(fn, *args, **kwargs): """Wrap a function (with args and kwargs) so it can be used as a command handler. This essentially accepts and ignores the handler-related arguments (i.e., the required ``command`` argument passed to handlers), so that you can use a method like :meth:`~toga.App.about()` as a command handler. - It can accept either a function or a coroutine. + It can accept either a function or a coroutine. Arguments that will be passed to the + function/coroutine are provided at the time the wrapper is defined. It is assumed + that the mechanism invoking the handler will add no additional arguments other than + the ``command`` that is invoking the handler. + :param fn: The callable to invoke as a handler. + :param args: Positional arguments that should be passed to the invoked handler. + :param kwargs: Keyword arguments that should be passed to the invoked handler. :returns: A handler that will invoke the callable. """ if inspect.iscoroutinefunction(fn): - async def _handler(command, *args, **kwargs): + async def _handler(command): return await fn(*args, **kwargs) else: - def _handler(command, *args, **kwargs): + def _handler(command): return fn(*args, **kwargs) return _handler diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 579431f244..3b66b7a108 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -1031,7 +1031,7 @@ def __init__( doc: Document, id: str | None = None, title: str | None = None, - position: PositionT = Position(100, 100), + position: PositionT | None = None, size: SizeT = Size(640, 480), resizable: bool = True, minimizable: bool = True, @@ -1039,23 +1039,21 @@ def __init__( ): """Create a new document Main Window. - This installs a default on_close handler that honors platform-specific document - closing behavior. If you want to control whether a document is allowed to close - (e.g., due to having unsaved change), override - :meth:`toga.Document.can_close()`, rather than implementing an on_close handler. + A document main window is a normal MainWindow, bound to a specific document + instance. :param doc: The document being managed by this window :param id: The ID of the window. :param title: Title for the window. Defaults to the formal name of the app. :param position: Position of the window, as a :any:`toga.Position` or tuple of ``(x, y)`` coordinates. - :param size: Size of the window, as a :any:`toga.Size` or tuple of - ``(width, height)``, in pixels. + :param size: Size of the window, as a :any:`toga.Size` or tuple of ``(width, + height)``, in pixels. :param resizable: Can the window be manually resized by the user? :param minimizable: Can the window be minimized by the user? :param on_close: The initial :any:`on_close` handler. """ - self.doc = doc + self._doc = doc super().__init__( id=id, title=title, @@ -1064,9 +1062,20 @@ def __init__( resizable=resizable, closable=True, minimizable=minimizable, - on_close=doc.handle_close if on_close is None else on_close, + on_close=on_close, ) + @property + def doc(self) -> Document: + """The document managed by this window""" + return self._doc + @property def _default_title(self) -> str: - return self.doc.path.name + return self.doc.title + + def _close(self): + # When then window is closed, remove the document it is managing from the app's + # list of managed documents. + self._app._documents.remove(self.doc) + super()._close() diff --git a/docs/reference/api/documentapp.rst b/docs/reference/api/documentapp.rst deleted file mode 100644 index 88a96fe724..0000000000 --- a/docs/reference/api/documentapp.rst +++ /dev/null @@ -1,90 +0,0 @@ -DocumentApp -=========== - -The top-level representation of an application that manages documents. - -.. rst-class:: widget-support -.. csv-filter:: Availability (:ref:`Key `) - :header-rows: 1 - :file: ../data/widgets_by_platform.csv - :included_cols: 4,5,6,7,8,9,10 - :exclude: {0: '(?!(DocumentApp|Component))'} - - -Usage ------ - -A DocumentApp is a specialized subclass of App that is used to manage documents. A -DocumentApp does *not* have a main window; each document that the app manages has it's -own main window. Each document may also define additional windows, if necessary. - -The types of documents that the DocumentApp can manage must be declared as part of the -instantiation of the DocumentApp. This requires that you define a subclass of -:class:`toga.Document` that describes how your document can be read and displayed. In -this example, the code declares an "Example Document" document type, whose files have an -extension of ``mydoc``: - -.. code-block:: python - - import toga - - class ExampleDocument(toga.Document): - def __init__(self, path, app): - super().__init__(document_type="Example Document", path=path, app=app) - - def create(self): - # Create the representation for the document's main window - self.main_window = toga.DocumentMainWindow(self) - self.main_window.content = toga.MultilineTextInput() - - def read(self): - # Put your logic to read the document here. For example: - with self.path.open() as f: - self.content = f.read() - - self.main_window.content.value = self.content - - app = toga.DocumentApp("Document App", "com.example.document", {"mydoc": MyDocument}) - app.main_loop() - -The exact behavior of a DocumentApp is slightly different on each platform, reflecting -platform differences. - -macOS -~~~~~ - -On macOS, there is only ever a single instance of a DocumentApp running at any given -time. That instance can manage multiple documents. If you use the Finder to open a -second document of a type managed by the DocumentApp, it will be opened in the existing -DocumentApp instance. Closing all documents will not cause the app to exit; the app will -keep executing until explicitly exited. - -If the DocumentApp is started without an explicit file reference, a file dialog will be -displayed prompting the user to select a file to open. If this dialog can be dismissed, -the app will continue running. Selecting "Open" from the file menu will also display this -dialog; if a file is selected, a new document window will be opened. - -Linux/Windows -~~~~~~~~~~~~~ - -On Linux and Windows, each DocumentApp instance manages a single document. If your app -is running, and you use the file manager to open a second document, a second instance of -the app will be started. If you close a document's main window, the app instance -associated with that document will exit, but any other app instances will keep running. - -If the DocumentApp is started without an explicit file reference, a file dialog will be -displayed prompting the user to select a file to open. If this dialog is dismissed, the -app will continue running, but will show an empty document. Selecting "Open" from the -file menu will also display this dialog; if a file is selected, the current document -will be replaced. - -Reference ---------- - -.. autoclass:: toga.DocumentApp - :members: - :undoc-members: - -.. autoclass:: toga.Document - :members: - :undoc-members: diff --git a/docs/reference/api/documentmainwindow.rst b/docs/reference/api/documentmainwindow.rst new file mode 100644 index 0000000000..22213c2e9b --- /dev/null +++ b/docs/reference/api/documentmainwindow.rst @@ -0,0 +1,55 @@ +DocumentMainWindow +================== + +A window that can be used as the main interface to a document-based app. + +.. tabs:: + + .. group-tab:: macOS + + .. figure:: /reference/images/mainwindow-cocoa.png + :align: center + :width: 450px + + .. group-tab:: Linux + + .. figure:: /reference/images/mainwindow-gtk.png + :align: center + :width: 450px + + .. group-tab:: Windows + + .. figure:: /reference/images/mainwindow-winforms.png + :align: center + :width: 450px + + .. group-tab:: Android |no| + + Not supported + + .. group-tab:: iOS |no| + + Not supported + + .. group-tab:: Web |no| + + Not supported + + .. group-tab:: Textual |no| + + Not supported + +Usage +----- + +A DocumentMainWindow is the same as a :any:`toga.MainWindow`, except that it is bound to +a :any:`toga.Document` instance, exposed as the :any:`toga.DocumentMainWindow.doc` +attribute. + +Instances of :any:`toga.DocumentMainWindow` should be created as part of the +:meth:`~toga.Document.create()` method of an implementation of :any:`toga.Document`. + +Reference +--------- + +.. autoclass:: toga.DocumentMainWindow diff --git a/docs/reference/api/index.rst b/docs/reference/api/index.rst index 4748606753..93b4e38811 100644 --- a/docs/reference/api/index.rst +++ b/docs/reference/api/index.rst @@ -7,14 +7,14 @@ API Reference Core application components --------------------------- -================================================= =================================================== - Component Description -================================================= =================================================== - :doc:`App ` The top-level representation of an application. - :doc:`DocumentApp ` An application that manages documents. - :doc:`Window ` An operating system-managed container of widgets. - :doc:`MainWindow ` The main window of the application. -================================================= =================================================== +=============================================================== ========================================================================== + Component Description +=============================================================== ========================================================================== + :doc:`App ` The top-level representation of an application. + :doc:`Window ` An operating system-managed container of widgets. + :doc:`MainWindow ` The main window of the application. + :doc:`DocumentMainWindow ` A window that can be used as the main interface to a document-based app. +=============================================================== ========================================================================== General widgets --------------- @@ -80,7 +80,9 @@ Resources for an application. :doc:`Command ` A representation of app functionality that the user can invoke from menus or toolbars. - :doc:`Font ` Fonts + :doc:`Document ` A representation of a file on disk that will be displayed in one or + more windows + :doc:`Font ` A representation of a Font :doc:`Icon ` An icon for buttons, menus, etc :doc:`Image ` An image :doc:`Source ` A base class for data source implementations. @@ -116,9 +118,9 @@ Other :hidden: app - documentapp window mainwindow + documentmainwindow containers/index hardware/index resources/index diff --git a/docs/reference/api/resources/document.rst b/docs/reference/api/resources/document.rst new file mode 100644 index 0000000000..c39251e318 --- /dev/null +++ b/docs/reference/api/resources/document.rst @@ -0,0 +1,86 @@ +Document +======== + +A representation of a file on disk that will be displayed in one or more windows. + +.. rst-class:: widget-support +.. csv-filter:: Availability (:ref:`Key `) + :header-rows: 1 + :file: ../../data/widgets_by_platform.csv + :included_cols: 4,5,6,7,8,9,10 + :include: {0: '^Document$'} + + +Usage +----- + +A common requirement for apps is to view or edit a particular type of file. To write +this sort of app with Toga, you define a :class:`toga.Document` class to represent each +type of content that your app can manipulate. This :class:`~toga.Document` class is then +registered with your app when the :class:`~toga.App` instance is created. + +The :class:`toga.Document` class describes how your document can be read and displayed. In +this example, the code declares an "Example Document" document type, whose main window +contains a :class:`~toga.MultilineTextInput`: + +.. code-block:: python + + import toga + + class ExampleDocument(toga.Document): + document_type = "Example Document" + + def create(self): + # Create the representation for the document's main window + self.main_window = toga.DocumentMainWindow(doc=self) + self.main_window.content = toga.MultilineTextInput() + + def read(self): + # Read the document + with self.path.open() as f: + self.main_window.content.value = f.read() + + def write(self): + # Save the document + with self.path.open("w") as f: + f.write(self.main_window.content.value) + + +This document class can then be registered with an app instance. The constructor for +:any:`toga.App` allows you to declare the full list of document types that your app +supports; the first declared document type is treated as the default document type for +your app. + +In the following example, the ``ExampleDocument`` class is set as the default content +type, and is registered as representing documents with extension ``mydoc`` or +``mydocument``. The app will also support documents with the extension ``otherdoc``. The +app is configured as a :ref:`session-based app `; it will used +``ExampleDocument`` (with extension ``mydoc``) as the default document type: + +.. code-block:: python + + import toga + + class ExampleApp(toga.App): + def startup(self): + # Make this a session-based app. + self.main_window = None + + app = ExampleApp( + "Document App", + "com.example.documentapp", + document_types={ + "mydoc": ExampleDocument, + "mydocument": ExampleDocument, + "otherdoc": OtherDocument, + } + ) + + app.main_loop() + +Reference +--------- + +.. autoclass:: toga.Document + :members: + :undoc-members: diff --git a/docs/reference/api/resources/index.rst b/docs/reference/api/resources/index.rst index 0544de2680..d341076cc5 100644 --- a/docs/reference/api/resources/index.rst +++ b/docs/reference/api/resources/index.rst @@ -6,6 +6,7 @@ Resources app_paths fonts command + document icons images sources/source diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index 8e923bd442..639ec4b886 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -34,6 +34,7 @@ Location,Hardware,:class:`~toga.hardware.location.Location`,A sensor that can ca Screen,Hardware,:class:`~toga.screens.Screen`,A representation of a screen attached to a device.,|y|,|y|,|y|,|y|,|y|,|b|,|b| App Paths,Resource,:class:`~toga.paths.Paths`,A mechanism for obtaining platform-appropriate filesystem locations for an application.,|y|,|y|,|y|,|y|,|y|,,|b| Command,Resource,:class:`~toga.Command`,Command,|y|,|y|,|y|,,|y|,, +Document,Resource,:class:`~toga.Document`,A representation of a file on disk that will be displayed in one or more windows.,|y|,|y|,|y|,,,, Font,Resource,:class:`~toga.Font`,A text font,|y|,|y|,|y|,|y|,|y|,, Icon,Resource,:class:`~toga.Icon`,"A small, square image, used to provide easily identifiable visual context to a widget.",|y|,|y|,|y|,|y|,|y|,,|b| Image,Resource,:class:`~toga.Image`,Graphical content of arbitrary size.,|y|,|y|,|y|,|y|,|y|,, diff --git a/examples/documentapp/documentapp/app.py b/examples/documentapp/documentapp/app.py index 684694af82..e9d415c0a0 100644 --- a/examples/documentapp/documentapp/app.py +++ b/examples/documentapp/documentapp/app.py @@ -2,10 +2,9 @@ class ExampleDocument(toga.Document): - def __init__(self, path, app): - super().__init__(path=path, document_type="Example Document", app=app) + document_type = "Example Document" - async def can_close(self): + async def can_close(self, window, **kwargs): return await self.main_window.question_dialog( "Are you sure?", "Do you want to close this document?", @@ -13,20 +12,19 @@ async def can_close(self): def create(self): # Create the main window for the document. - self.main_window = toga.DocumentMainWindow( - doc=self, - title=f"Example: {self.path.name}", - ) + self.main_window = toga.DocumentMainWindow(doc=self, on_close=self.can_close) self.main_window.content = toga.MultilineTextInput() def read(self): with self.path.open() as f: - self.content = f.read() + self.main_window.content.value = f.read() - self.main_window.content.value = self.content + def write(self): + with self.path.open("w") as f: + f.write(self.main_window.content.value) -class ExampleDocumentApp(toga.DocumentApp): +class ExampleDocumentApp(toga.App): def startup(self): # A document-based app is a session app, so it has no main window. A window (or # windows) will be created from the document(s) specified at the command line;