Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finalize API for Document-based apps #2666

Merged
merged 61 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
53d1a5b
Update documentation and examples for document API.
freakboy3742 Jun 25, 2024
d793d60
Update core and core tests to match documentation.
freakboy3742 Jun 27, 2024
f543a52
Add document type registration to example document app.
freakboy3742 Jun 28, 2024
2d377f9
Add document API tests for macOS.
freakboy3742 Jun 29, 2024
8e46ad4
Update document tests for iOS and Android.
freakboy3742 Jun 29, 2024
5d57a38
Initial attempt at GTK and Winforms document API tests.
freakboy3742 Jun 29, 2024
eb65ee9
Use an automated approach to window cleanup.
freakboy3742 Jun 29, 2024
e1dcd2c
Minor cleanups and test fixes.
freakboy3742 Jun 29, 2024
8dc24e3
Mark GTK and Winforms as using default command line handling.
freakboy3742 Jun 29, 2024
0e85f43
Add error handling to GTK/Winforms document handling.
freakboy3742 Jun 29, 2024
34129cb
Add Open menu item for GTK and Winforms.
freakboy3742 Jun 29, 2024
ea2e6e5
Create an empty document on GTK and Winforms startup.
freakboy3742 Jun 29, 2024
0e3d025
Remove need for Document to have an implementation.
freakboy3742 Jul 1, 2024
118b93e
Merge branch 'main' into document-api
freakboy3742 Jul 15, 2024
808b1f7
Merge branch 'main' into document-api
freakboy3742 Jul 18, 2024
699f506
Add factory method for standard commands, and backend implementation …
freakboy3742 Jul 19, 2024
9816d28
Add backend implementations of commands.
freakboy3742 Jul 19, 2024
780e7f3
Remove backend representations for DocumentMainWindow.
freakboy3742 Jul 19, 2024
f8ca850
Fixes for test coverage and platform menus.
freakboy3742 Jul 19, 2024
44dbb54
Add handling for non-defined document commands.
freakboy3742 Jul 19, 2024
f60d39c
Add documentation and core implemenation for new/save/save as/save all.
freakboy3742 Jul 2, 2024
91df79a
Add cocoa implementation of new/save/save as/save all and testbed tests.
freakboy3742 Jul 2, 2024
8c9b7c3
Add GTK and Winforms implementation and tests.
freakboy3742 Jul 2, 2024
e7e0cc5
Add second doctype registration for coverage.
freakboy3742 Jul 2, 2024
48a773d
Additional test fixes and coverage.
freakboy3742 Jul 19, 2024
35df5b5
Add resilience to adding missing commands.
freakboy3742 Jul 19, 2024
84cddeb
Make the file management programmatic API public.
freakboy3742 Jul 19, 2024
5a991ac
Speculative fix for macOS app shutdown issues.
freakboy3742 Jul 19, 2024
6d7a0fa
Add document modification and focus API.
freakboy3742 Jul 23, 2024
098480c
Open the existing representation of a document.
freakboy3742 Jul 23, 2024
b40f013
Minor behavior corrections for Linux paths and focus.
freakboy3742 Jul 23, 2024
833c923
Corrections for Python3.8 path handling on Windows.
freakboy3742 Jul 23, 2024
ea4065a
Open documents into 'existing' windows on GTK and Linux.
freakboy3742 Jul 23, 2024
31c68d1
Merge branch 'main' into document-api
mhsmith Jul 25, 2024
46535b7
Apply suggestions from code review
freakboy3742 Jul 27, 2024
d85f346
Rename is_modified -> modified.
freakboy3742 Jul 27, 2024
7dd5b54
Simplify handling of replacement filenames.
freakboy3742 Jul 27, 2024
9ecc086
Update docstrings to consistently refer to standard commands.
freakboy3742 Jul 27, 2024
eca1987
Rename DocumentMainWindow to DocumentWindow.
freakboy3742 Jul 27, 2024
7ef9991
Make modified a writable document property.
freakboy3742 Jul 27, 2024
013b123
Correct the save_as testbed test.
freakboy3742 Jul 27, 2024
df258ce
Correct widget support data.
freakboy3742 Jul 29, 2024
dc150da
Apply suggestions from code review
freakboy3742 Aug 17, 2024
7f9d456
Merge branch 'main' into document-api
freakboy3742 Aug 17, 2024
facd630
Removed some duplicated details in the command docs.
freakboy3742 Aug 17, 2024
dbf55e5
Corrected some pre-commit issues.
freakboy3742 Aug 17, 2024
8d5945b
Reorganization of document-based classes.
freakboy3742 Aug 17, 2024
2a21108
Correct the expected error type.
freakboy3742 Aug 17, 2024
c3116eb
Correct some documentation markup.
freakboy3742 Aug 17, 2024
e67601c
Refactor the document control commands into DocumentSet.
freakboy3742 Aug 18, 2024
3862233
Correct wording to avoid spelling issue.
freakboy3742 Aug 18, 2024
43236b5
Simplify the API for creating standard commands.
freakboy3742 Aug 18, 2024
84b2860
Add links to default Command actions.
freakboy3742 Aug 19, 2024
46e7ab0
Rename Document.document_type; corrected and simplified open/save logic.
freakboy3742 Aug 20, 2024
8443dd2
Move document types to DocumentSet, and move extension handling to Do…
freakboy3742 Aug 20, 2024
10c8621
Remove document.close
freakboy3742 Aug 20, 2024
ccea366
Simpified cleanup of replacement marker.
freakboy3742 Aug 20, 2024
351028f
Update documentation for new types/extensions format.
freakboy3742 Aug 20, 2024
58089aa
Apply suggestions from code review
freakboy3742 Aug 22, 2024
c181f61
Move deprecated property back to the deprecated class.
freakboy3742 Aug 22, 2024
2589966
Add a hide method to Document for parity.
freakboy3742 Aug 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion changes/2209.removal.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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 ``DocumentApp`` base class is no longer required. Apps can subclass ``App`` directly, passing the document types as a ``list`` of ``Document`` classes, rather than a mapping of extension to document type.

The API for ``Document`` subclasses has also changed:

* A path is no longer provided as an argument to the Document constructor;

* The ``document_type`` is now specified as a class property; and
freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved

* Extensions are now defined as a class property of the ``Document``; and

* The ``can_close()`` handler is no longer honored. Documents now track if they are modified, and have a default ``on_close`` handler that uses the modification status of a document to control whether a document can close. Invoking ``touch()`` on document will mark a document as modified. This modification flag is cleared by saving the document.
2 changes: 1 addition & 1 deletion cocoa/src/toga_cocoa/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def applicationOpenUntitledFile_(self, sender) -> bool:

@objc_method
def applicationShouldOpenUntitledFile_(self, sender) -> bool:
return bool(self.interface.document_types)
return bool(self.interface.documents.types)

@objc_method
def application_openFiles_(self, app, filenames) -> None:
Expand Down
95 changes: 50 additions & 45 deletions core/src/toga/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def __init__(
home_page: str | None = None,
description: str | None = None,
startup: AppStartupMethod | None = None,
document_types: dict[str, type[Document]] | None = None,
document_types: list[type[Document]] | None = None,
on_running: OnRunningHandler | None = None,
on_exit: OnExitHandler | None = None,
id: None = None, # DEPRECATED
Expand Down Expand Up @@ -198,8 +198,8 @@ def __init__(
:param startup: A callable to run before starting the app.
:param on_running: The initial :any:`on_running` handler.
: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 document_types: A list of :any:`Document` classes that this app
can manage.
: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 @@ -315,8 +315,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)
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
Expand Down Expand Up @@ -535,45 +537,34 @@ def _create_standard_commands(self):
]:
self.commands.add(Command.standard(self, cmd_id))

if self.document_types:
default_document_type = list(self.document_types.values())[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(set(self.document_types.values())) == 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+
known_document_classes = set()
for i, (extension, document_class) in enumerate(
self.document_types.items()
):
if document_class not in known_document_classes:
command = Command.standard(
self,
Command.NEW,
action=simple_handler(
self.documents.new, document_class
),
)
command.text = (
command.text + f" {document_class.document_type}"
)
if i > 0:
command.shortcut = None
command._id = f"{command.id}:{extension}"
command.order = command.order + len(
known_document_classes
)

self.commands.add(command)
known_document_classes.add(document_class)
for i, document_class in enumerate(self.documents.types):
command = Command.standard(
self,
Command.NEW,
action=simple_handler(self.documents.new, document_class),
)
command.text = command.text + f" {document_class.description}"
if i > 0:
command.shortcut = None
command._id = f"{command.id}:{document_class.extensions[0]}"
command.order = command.order + i

self.commands.add(command)

for cmd_id in [
Command.OPEN,
Expand All @@ -595,7 +586,7 @@ def _create_initial_windows(self):
if self._impl.HANDLES_COMMAND_LINE:
return
doc_count = len(self.windows)
if self.document_types:
if self.documents.types:
for filename in sys.argv[1:]:
if self._open_initial_document(filename):
doc_count += 1
Expand All @@ -604,9 +595,9 @@ def _create_initial_windows(self):
if self.main_window is None and doc_count == 0:
try:
# Pass in the first document type as the default
default_doc_type = next(iter(self.document_types.values()))
default_doc_type = self.documents.types[0]
self.documents.new(default_doc_type)
except StopIteration:
except IndexError:
# No document types defined.
raise RuntimeError(
"App didn't create any windows, or register any document types."
Expand Down Expand Up @@ -681,16 +672,6 @@ 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) -> DocumentSet:
"""The list of documents associated with this app."""
Expand Down Expand Up @@ -896,6 +877,26 @@ def add_background_task(self, handler: BackgroundTask) -> None:

self.loop.call_soon_threadsafe(wrapped_handler(self, handler))

######################################################################
# 2024-08: Backwards compatibility
######################################################################

@property
def document_types(self) -> dict[str, type[Document]]:
"""**DEPRECATED** - Use ``documents.types``; extensions can be
obtained from the individual document classes itself.
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
"""
warnings.warn(
"App.document_types is deprecated. Use App.documents.types",
DeprecationWarning,
stacklevel=2,
)
return {
extension: doc_type
for doc_type in self.documents.types
for extension in doc_type.extensions
}

######################################################################
# End backwards compatibility
######################################################################
Expand All @@ -911,4 +912,8 @@ def __init__(self, *args, **kwargs):
DeprecationWarning,
stacklevel=2,
)
# Convert document types from dictionary format to list format.
# The old API guaranteed that document_types was provided
kwargs["document_types"] = list(kwargs["document_types"].values())

super().__init__(*args, **kwargs)
28 changes: 19 additions & 9 deletions core/src/toga/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,32 +161,42 @@ def __call__(self, command: Command, **kwargs) -> bool:

class Command:
#: An identifier for the standard "About" menu item. This command is always
#: installed by default.
#: installed by default. Uses :meth:`toga.App.about` as the default action.
ABOUT: str = "about"
#: An identifier for the standard "Exit" menu item. This command may be installed by
#: default, depending on platform requirements.
#: default, depending on platform requirements. Uses :meth:`toga.App.request_exit`
#: as the default action.
EXIT: str = "request_exit"
#: An identifier for the standard "New" menu item. This constant will be used for
#: the default document type for your app; if you specify more than one document
#: type, the command for the subsequent commands will have a colon and the first
#: extension for that data type appended to the ID.
#: extension for that data type appended to the ID. Uses
#: :meth:`toga.documents.DocumentSet.new` as the default action.
NEW: str = "documents.new"
#: An identifier for the standard "Open" menu item. This command will be
#: automatically installed if your app declares any document types.
#: automatically installed if your app declares any document types. Uses
#: :meth:`toga.documents.DocumentSet.request_open` as the default action.
OPEN: str = "documents.request_open"
#: An identifier for the standard "Preferences" menu item.
#: An identifier for the standard "Preferences" menu item. The Preferences item is
#: not installed by default. If you install it manually, it will attempt to use
#: ``toga.App.preferences()`` as the default action; your app will need to define
#: this method, or provide an explicit value for the action.
PREFERENCES: str = "preferences"
#: An identifier for the standard "Save" menu item. This command will be
#: automatically installed if your app declares any document types.
#: automatically installed if your app declares any document types. Uses
#: :meth:`toga.documents.DocumentSet.save` as the default action.
SAVE: str = "documents.save"
#: An identifier for the standard "Save As..." menu item. This command will be
#: automatically installed if your app declares any document types.
#: automatically installed if your app declares any document types. Uses
#: :meth:`toga.documents.DocumentSet.save_as` as the default action.
SAVE_AS: str = "documents.save_as"
#: An identifier for the standard "Save All" menu item. This command will be
#: automatically installed if your app declares any document types.
#: automatically installed if your app declares any document types. Uses
#: :meth:`toga.documents.DocumentSet.save_all` as the default action.
SAVE_ALL: str = "documents.save_all"
#: An identifier for the standard "Visit Homepage" menu item. This command may be
#: installed by default, depending on platform requirements.
#: installed by default, depending on platform requirements. Uses
#: :meth:`toga.App.visit_homepage` as the default action.
VISIT_HOMEPAGE: str = "visit_homepage"

def __init__(
Expand Down
Loading