From 46a5968407dd729434d688df4312b346b5340339 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 22 Sep 2024 07:48:42 -0400 Subject: [PATCH 1/6] Fixed macOS document mode --- cocoa/src/toga_cocoa/app.py | 5 ++++- core/src/toga/app.py | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index e2443796a6..321a4dc21d 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -54,7 +54,10 @@ def applicationOpenUntitledFile_(self, sender) -> bool: @objc_method def applicationShouldOpenUntitledFile_(self, sender) -> bool: - return bool(self.interface.documents.types) + if self.interface._main_window == toga.App._UNDEFINED: + return False + else: + return bool(self.interface.documents.types) @objc_method def application_openFiles_(self, app, filenames) -> None: diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 24a15b6b91..11cec73550 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -659,6 +659,15 @@ def _startup(self) -> None: # Create any initial windows self._create_initial_windows() + # If the app has document types but has no windows, then show an "Open Document" + # Dialog for the user to select a document. + if ( + self.main_window is None + and len(self.windows) == 0 + and self.documents.types != [] + ): + asyncio.create_task(self.documents.request_open()) + # Manifest the initial state of the menus. This will cascade down to all # open windows if the platform has window-based menus. Then install the # on-change handler for menus to respond to any future changes. From 6c80d8c09d2a862c18495e3263fd16b1cebc49d3 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 22 Sep 2024 07:53:29 -0400 Subject: [PATCH 2/6] Added a changelog --- changes/2860.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2860.bugfix.rst diff --git a/changes/2860.bugfix.rst b/changes/2860.bugfix.rst new file mode 100644 index 0000000000..64ebd10286 --- /dev/null +++ b/changes/2860.bugfix.rst @@ -0,0 +1 @@ +Apps that specify both `document_types` and a `main_window` no longer display the "Document Selection" dialog on startup in macOS. From d424c325c46775c079d38020725c93c5c5a54008 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 23 Sep 2024 06:42:18 -0400 Subject: [PATCH 3/6] Fixed macOS document mode --- core/src/toga/app.py | 50 +++++++++++++---------------- core/tests/app/test_document_app.py | 34 ++++++++++++++++++-- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 11cec73550..dabdf07a99 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -614,26 +614,29 @@ def _create_initial_windows(self): If document types are defined, try to open every argument on the command line as a document (unless the backend manages the command line arguments). """ - # If the backend handles the command line, don't do any command line processing. - if self._impl.HANDLES_COMMAND_LINE: - return - doc_count = len(self.windows) - if self.documents.types: - for filename in sys.argv[1:]: - if self._open_initial_document(filename): - doc_count += 1 - # Safety check: Do we have at least one document? - if self.main_window is None and doc_count == 0: - try: - # Pass in the first document type as the default - default_doc_type = self.documents.types[0] - self.documents.new(default_doc_type) - except IndexError: - # No document types defined. - raise RuntimeError( - "App didn't create any windows, or register any document types." - ) + if not (self._impl.HANDLES_COMMAND_LINE and self._impl.CLOSE_ON_LAST_WINDOW): + doc_count = len(self.windows) + + # Process command line arguments if the backend doesn't handle them + if not self._impl.HANDLES_COMMAND_LINE and self.documents.types: + for filename in sys.argv[1:]: + if self._open_initial_document(filename): + doc_count += 1 + + # Ensure there is at least one document or window + if self.main_window is None and doc_count == 0: + if self.documents.types: + if self._impl.CLOSE_ON_LAST_WINDOW: + # Pass in the first document type as the default + self.documents.new(self.documents.types[0]) + else: + self.loop.run_until_complete(self.documents.request_open()) + else: + # No document types defined. + raise RuntimeError( + "App didn't create any windows, or register any document types." + ) def _startup(self) -> None: # Wrap the platform's event loop's task factory for task tracking @@ -659,15 +662,6 @@ def _startup(self) -> None: # Create any initial windows self._create_initial_windows() - # If the app has document types but has no windows, then show an "Open Document" - # Dialog for the user to select a document. - if ( - self.main_window is None - and len(self.windows) == 0 - and self.documents.types != [] - ): - asyncio.create_task(self.documents.request_open()) - # Manifest the initial state of the menus. This will cascade down to all # open windows if the platform has window-based menus. Then install the # on-change handler for menus to respond to any future changes. diff --git a/core/tests/app/test_document_app.py b/core/tests/app/test_document_app.py index 1cae91a399..c7f8358bb9 100644 --- a/core/tests/app/test_document_app.py +++ b/core/tests/app/test_document_app.py @@ -1,5 +1,5 @@ import sys -from unittest.mock import Mock +from unittest.mock import AsyncMock, MagicMock, Mock import pytest @@ -151,7 +151,7 @@ def test_create_no_cmdline(monkeypatch): assert toga.Command.SAVE_ALL in app.commands -def test_create_no_cmdline_default_handling(monkeypatch): +def test_create_no_cmdline_default_handling_close_on_last_window(monkeypatch): """If the backend uses the app's command line handling, no error is raised for an empty command line.""" monkeypatch.setattr(sys, "argv", ["app-exe"]) @@ -175,6 +175,36 @@ def test_create_no_cmdline_default_handling(monkeypatch): assert len(app.windows) == 0 +def test_create_no_cmdline_default_handling_no_close_on_last_window(monkeypatch): + """If the backend handles the app's command line and the app doesn't exit when + the last window closes, no error is raised for an empty command line and the + app shows a document selection dialog on startup.""" + + monkeypatch.setattr(sys, "argv", ["app-exe"]) + + # Monkeypatch the property that makes the backend handle command line arguments + # and not close the app when the last window closes. + monkeypatch.setattr(DummyApp, "HANDLES_COMMAND_LINE", True) + monkeypatch.setattr(DummyApp, "CLOSE_ON_LAST_WINDOW", False) + + # Mock request_open() because OpenFileDialog's response can't be set before the + # app creation. Since request_open() is called during app initialization, we can't + # set the dialog response in time, leading to an unexpected dialog response error. + monkeypatch.setattr(ExampleDocumentApp, "documents", MagicMock()) + mock_request_open = AsyncMock() + monkeypatch.setattr(ExampleDocumentApp.documents, "request_open", mock_request_open) + + # Create the app instance + _ = ExampleDocumentApp( + "Test App", + "org.beeware.document-app", + document_types=[ExampleDocument], + ) + + # Check that request_open was called + mock_request_open.assert_called_once() + + def test_create_with_cmdline(monkeypatch, example_file): """If a document is specified at the command line, it is opened.""" monkeypatch.setattr(sys, "argv", ["app-exe", str(example_file)]) From d7f78235057117f71aea38b80deb0f140d1ebcfb Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Mon, 23 Sep 2024 07:00:40 -0400 Subject: [PATCH 4/6] Update changes/2860.bugfix.rst Co-authored-by: Russell Keith-Magee --- changes/2860.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/2860.bugfix.rst b/changes/2860.bugfix.rst index 64ebd10286..4a223c464d 100644 --- a/changes/2860.bugfix.rst +++ b/changes/2860.bugfix.rst @@ -1 +1 @@ -Apps that specify both `document_types` and a `main_window` no longer display the "Document Selection" dialog on startup in macOS. +On macOS, apps that specify both `document_types` and a `main_window` no longer display the document selection dialog on startup. From d02f76adcb0efd963abd6d72282d20520d44b3d8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 23 Sep 2024 07:43:00 -0700 Subject: [PATCH 5/6] Refactor initial document handling to separate command line from default document handling. --- core/src/toga/app.py | 49 ++++++++++++++--------------- core/tests/app/test_document_app.py | 25 ++++++++++----- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index dabdf07a99..7f3f5ad864 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -608,35 +608,34 @@ def _create_standard_commands(self): 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 its own - command-line handling interface. + arguments. - If document types are defined, try to open every argument on the command line as - a document (unless the backend manages the command line arguments). - """ - - if not (self._impl.HANDLES_COMMAND_LINE and self._impl.CLOSE_ON_LAST_WINDOW): - doc_count = len(self.windows) + If document types are defined, and the backend doesn't have native command line + handling, try to open every argument on the command line as a document (unless + the backend manages the command line arguments). - # Process command line arguments if the backend doesn't handle them - if not self._impl.HANDLES_COMMAND_LINE and self.documents.types: + If, after processing all command line arguments, the app doesn't have at least + one window, the app's default initial document handling will be triggered. + """ + # Process command line arguments if the backend doesn't handle them + if not self._impl.HANDLES_COMMAND_LINE: + if self.documents.types: for filename in sys.argv[1:]: - if self._open_initial_document(filename): - doc_count += 1 - - # Ensure there is at least one document or window - if self.main_window is None and doc_count == 0: - if self.documents.types: - if self._impl.CLOSE_ON_LAST_WINDOW: - # Pass in the first document type as the default - self.documents.new(self.documents.types[0]) - else: - self.loop.run_until_complete(self.documents.request_open()) + 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.documents.types: + if self._impl.CLOSE_ON_LAST_WINDOW: + # Pass in the first document type as the default + self.documents.new(self.documents.types[0]) else: - # No document types defined. - raise RuntimeError( - "App didn't create any windows, or register any document types." - ) + self.loop.run_until_complete(self.documents.request_open()) + else: + # No document types defined. + raise RuntimeError( + "App didn't create any windows, or register any document types." + ) def _startup(self) -> None: # Wrap the platform's event loop's task factory for task tracking diff --git a/core/tests/app/test_document_app.py b/core/tests/app/test_document_app.py index c7f8358bb9..d05ba49e8f 100644 --- a/core/tests/app/test_document_app.py +++ b/core/tests/app/test_document_app.py @@ -1,9 +1,10 @@ import sys -from unittest.mock import AsyncMock, MagicMock, Mock +from unittest.mock import AsyncMock, Mock import pytest import toga +from toga.documents import DocumentSet from toga_dummy.app import App as DummyApp from toga_dummy.command import Command as DummyCommand from toga_dummy.utils import ( @@ -170,9 +171,9 @@ def test_create_no_cmdline_default_handling_close_on_last_window(monkeypatch): assert app.documents.types == [ExampleDocument] - # No documents or windows exist - assert len(app.documents) == 0 - assert len(app.windows) == 0 + # A default document has been created + assert len(app.documents) == 1 + assert len(app.windows) == 1 def test_create_no_cmdline_default_handling_no_close_on_last_window(monkeypatch): @@ -190,18 +191,26 @@ def test_create_no_cmdline_default_handling_no_close_on_last_window(monkeypatch) # Mock request_open() because OpenFileDialog's response can't be set before the # app creation. Since request_open() is called during app initialization, we can't # set the dialog response in time, leading to an unexpected dialog response error. - monkeypatch.setattr(ExampleDocumentApp, "documents", MagicMock()) mock_request_open = AsyncMock() - monkeypatch.setattr(ExampleDocumentApp.documents, "request_open", mock_request_open) + monkeypatch.setattr(DocumentSet, "request_open", mock_request_open) # Create the app instance - _ = ExampleDocumentApp( + app = ExampleDocumentApp( "Test App", "org.beeware.document-app", document_types=[ExampleDocument], ) - # Check that request_open was called + assert app._impl.interface == app + assert_action_performed(app, "create App") + + assert app.documents.types == [ExampleDocument] + + # No documents have been created + assert len(app.documents) == 0 + assert len(app.windows) == 0 + + # ...but request_open was called mock_request_open.assert_called_once() From bc683c2f84f0ef23e897384b2876e997797566d8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 23 Sep 2024 07:49:03 -0700 Subject: [PATCH 6/6] Add some missing python version coverage exclusions. --- core/src/toga/app.py | 8 ++++---- pyproject.toml | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 7f3f5ad864..2cf27e5c95 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -493,14 +493,14 @@ def _install_task_factory_wrapper(self): def factory(loop, coro, context=None): if platform_task_factory is not None: - if sys.version_info < (3, 11): + if sys.version_info < (3, 11): # pragma: no-cover-if-gte-py311 task = platform_task_factory(loop, coro) - else: + else: # pragma: no-cover-if-lt-py311 task = platform_task_factory(loop, coro, context=context) else: - if sys.version_info < (3, 11): + if sys.version_info < (3, 11): # pragma: no-cover-if-gte-py311 task = asyncio.Task(coro, loop=loop) - else: + else: # pragma: no-cover-if-lt-py311 task = asyncio.Task(coro, loop=loop, context=context) self._running_tasks.add(task) diff --git a/pyproject.toml b/pyproject.toml index 9123dc689a..c0171adebe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,8 @@ exclude_lines = [ no-cover-if-missing-setuptools_scm = "not is_installed('setuptools_scm')" no-cover-if-missing-PIL = "not is_installed('PIL')" no-cover-if-PIL-installed = "is_installed('PIL')" +no-cover-if-lt-py311 = "sys_version_info < (3, 11) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" +no-cover-if-gte-py311 = "sys_version_info > (3, 11) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" no-cover-if-lt-py310 = "sys_version_info < (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" no-cover-if-gte-py310 = "sys_version_info > (3, 10) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'" no-cover-if-lt-py39 = "sys_version_info < (3, 9) and os_environ.get('COVERAGE_EXCLUDE_PYTHON_VERSION') != 'disable'"