Skip to content

Commit

Permalink
Core API and documentation changes for finalized Document interface.
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed Jun 18, 2024
1 parent 958f536 commit 018608a
Show file tree
Hide file tree
Showing 14 changed files with 352 additions and 277 deletions.
1 change: 1 addition & 0 deletions changes/2209.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The API for Documents and document types has been finalized.
8 changes: 8 additions & 0 deletions changes/2209.removal.rst
Original file line number Diff line number Diff line change
@@ -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``.
176 changes: 87 additions & 89 deletions core/src/toga/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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)

######################################################################
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
2 changes: 2 additions & 0 deletions core/src/toga/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 018608a

Please sign in to comment.