From b031b18edf0b36d33cd35ccab33d684707aa1c67 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 22 Oct 2024 00:12:26 -0700 Subject: [PATCH 01/34] Fixed macOS --- cocoa/src/toga_cocoa/app.py | 10 +++++++++- testbed/tests/app/test_desktop.py | 32 +++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 321a4dc21d..5ad3f3d012 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -6,6 +6,7 @@ SEL, NSMutableDictionary, NSObject, + ObjCClass, objc_method, objc_property, ) @@ -34,6 +35,8 @@ ) from .screens import Screen as ScreenImpl +NSPanel = ObjCClass("NSPanel") + class AppDelegate(NSObject): interface = objc_property(object, weak=True) @@ -366,7 +369,12 @@ def show_cursor(self): ###################################################################### def get_current_window(self): - return self.native.keyWindow + focused_window = self.native.keyWindow + if isinstance(focused_window, NSPanel): # If the focus is on a dialog + sheet_parent = focused_window.sheetParent + return sheet_parent if sheet_parent else self.native.mainWindow + else: # If the focus is on a window + return focused_window def set_current_window(self, window): window._impl.native.makeKeyAndOrderFront(window._impl.native) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 5e36ee03a0..d91617214d 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -346,7 +346,7 @@ async def test_show_hide_cursor(app, app_probe): assert app_probe.is_cursor_visible -async def test_current_window(app, app_probe, main_window): +async def test_current_window(app, app_probe, main_window, main_window_probe): """The current window can be retrieved""" try: if app_probe.supports_current_window_assignment: @@ -370,25 +370,41 @@ async def test_current_window(app, app_probe, main_window): window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) window3.content = toga.Box(style=Pack(background_color=FIREBRICK)) - # We don't need to probe anything window specific; we just need - # a window probe to enforce appropriate delays. - window1_probe = window_probe(app, window1) - window1.show() window2.show() window3.show() - await window1_probe.wait_for_window("Extra windows added") + await main_window_probe.wait_for_window("Extra windows added") + + info_dialog = toga.InfoDialog("Info", "Some info") + app_probe.setup_info_dialog_result(info_dialog) + + # When a window without any dialog is made the current_window, + # then app.current_window should return the specified window. + app.current_window = window1 + await main_window_probe.wait_for_window("Window 1 is current") + if app_probe.supports_current_window_assignment: + assert app.current_window == window1 + # When a dialog is in focus, app.current_window should + # return the window from which the dialog was initiated. app.current_window = window2 - await window1_probe.wait_for_window("Window 2 is current") + dialog_task = app.loop.create_task(window2.dialog(info_dialog)) + await main_window_probe.wait_for_window("Window 2 is current") if app_probe.supports_current_window_assignment: assert app.current_window == window2 + await app_probe.redraw("select 'OK") + # Cancel the task to avoid dangling + dialog_task.cancel() app.current_window = window3 - await window1_probe.wait_for_window("Window 3 is current") + dialog_task = app.loop.create_task(window3.dialog(info_dialog)) + await main_window_probe.wait_for_window("Window 3 is current") if app_probe.supports_current_window_assignment: assert app.current_window == window3 + await app_probe.redraw("select 'OK") + # Cancel the task to avoid dangling + dialog_task.cancel() async def test_session_based_app( From a8405fcf842c328bcb032f7b0f98b761703fbba8 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 22 Oct 2024 00:53:21 -0700 Subject: [PATCH 02/34] Added changelog --- changes/2926.bugfix.rst | 1 + testbed/tests/app/test_desktop.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changes/2926.bugfix.rst diff --git a/changes/2926.bugfix.rst b/changes/2926.bugfix.rst new file mode 100644 index 0000000000..038d65af12 --- /dev/null +++ b/changes/2926.bugfix.rst @@ -0,0 +1 @@ +On macOS, when a dialog is in focus, `App.current_window` now returns the window from which the dialog was initiated. diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index d91617214d..f4ca815be2 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -380,13 +380,13 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): app_probe.setup_info_dialog_result(info_dialog) # When a window without any dialog is made the current_window, - # then app.current_window should return the specified window. + # then `app.current_window` should return the specified window. app.current_window = window1 await main_window_probe.wait_for_window("Window 1 is current") if app_probe.supports_current_window_assignment: assert app.current_window == window1 - # When a dialog is in focus, app.current_window should + # When a dialog is in focus, `app.current_window` should # return the window from which the dialog was initiated. app.current_window = window2 dialog_task = app.loop.create_task(window2.dialog(info_dialog)) From c32d164705874ba2a59eaedf36a6a5a61b5e32cf Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 22 Oct 2024 05:13:02 -0400 Subject: [PATCH 03/34] Separated dialogs for both windows --- testbed/tests/app/test_desktop.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index f4ca815be2..9279723ccf 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -376,8 +376,7 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): await main_window_probe.wait_for_window("Extra windows added") - info_dialog = toga.InfoDialog("Info", "Some info") - app_probe.setup_info_dialog_result(info_dialog) + # When a window without any dialog is made the current_window, # then `app.current_window` should return the specified window. @@ -388,23 +387,30 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): # When a dialog is in focus, `app.current_window` should # return the window from which the dialog was initiated. + + window2_info_dialog = toga.InfoDialog("Info", "Some info") + app_probe.setup_info_dialog_result(window2_info_dialog) + app.current_window = window2 - dialog_task = app.loop.create_task(window2.dialog(info_dialog)) + window2_dialog_task = app.loop.create_task(window2.dialog(window2_info_dialog)) await main_window_probe.wait_for_window("Window 2 is current") if app_probe.supports_current_window_assignment: assert app.current_window == window2 - await app_probe.redraw("select 'OK") + await app_probe.redraw("select 'OK'") # Cancel the task to avoid dangling - dialog_task.cancel() + window2_dialog_task.cancel() + + window3_info_dialog = toga.InfoDialog("Info", "Some info") + app_probe.setup_info_dialog_result(window3_info_dialog) app.current_window = window3 - dialog_task = app.loop.create_task(window3.dialog(info_dialog)) + window3_dialog_task = app.loop.create_task(window3.dialog(window3_info_dialog)) await main_window_probe.wait_for_window("Window 3 is current") if app_probe.supports_current_window_assignment: assert app.current_window == window3 - await app_probe.redraw("select 'OK") + await app_probe.redraw("select 'OK'") # Cancel the task to avoid dangling - dialog_task.cancel() + window3_dialog_task.cancel() async def test_session_based_app( From 02bc50abcb953a53b55f589b8b736fbf72f14562 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 22 Oct 2024 05:14:05 -0400 Subject: [PATCH 04/34] pre-commit fix --- testbed/tests/app/test_desktop.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 9279723ccf..eb73c5f655 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -376,8 +376,6 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): await main_window_probe.wait_for_window("Extra windows added") - - # When a window without any dialog is made the current_window, # then `app.current_window` should return the specified window. app.current_window = window1 From d143d192557a6a4233ac2ac6ce35c3deb60d9fde Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 22 Oct 2024 06:13:00 -0400 Subject: [PATCH 05/34] Modified testbed::app::test_desktop::test_current_window --- testbed/tests/app/test_desktop.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index eb73c5f655..2d6557035e 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -352,10 +352,8 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): if app_probe.supports_current_window_assignment: assert app.current_window == main_window - # When all windows are hidden, WinForms and Cocoa return None, while GTK - # returns the last active window. main_window.hide() - assert app.current_window in [None, main_window] + assert app.current_window is None main_window.show() assert app.current_window == main_window @@ -386,7 +384,7 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): # When a dialog is in focus, `app.current_window` should # return the window from which the dialog was initiated. - window2_info_dialog = toga.InfoDialog("Info", "Some info") + window2_info_dialog = toga.InfoDialog("Second Window Info", "Some info") app_probe.setup_info_dialog_result(window2_info_dialog) app.current_window = window2 @@ -398,7 +396,7 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): # Cancel the task to avoid dangling window2_dialog_task.cancel() - window3_info_dialog = toga.InfoDialog("Info", "Some info") + window3_info_dialog = toga.InfoDialog("Third Window Info", "Some info") app_probe.setup_info_dialog_result(window3_info_dialog) app.current_window = window3 From 8a42a913b84ea4207e616eeac41f41c5ff5bb50c Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Tue, 22 Oct 2024 07:07:52 -0400 Subject: [PATCH 06/34] Restart CI for intermittent iOS failure From c667c3e571f51366b76a6f3035d6d7517f17811e Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 22 Oct 2024 19:25:48 -0700 Subject: [PATCH 07/34] Test Run 1 --- testbed/tests/app/test_desktop.py | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 2d6557035e..3a8786d625 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -374,39 +374,39 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): await main_window_probe.wait_for_window("Extra windows added") - # When a window without any dialog is made the current_window, - # then `app.current_window` should return the specified window. - app.current_window = window1 - await main_window_probe.wait_for_window("Window 1 is current") - if app_probe.supports_current_window_assignment: - assert app.current_window == window1 - - # When a dialog is in focus, `app.current_window` should - # return the window from which the dialog was initiated. - - window2_info_dialog = toga.InfoDialog("Second Window Info", "Some info") - app_probe.setup_info_dialog_result(window2_info_dialog) - app.current_window = window2 - window2_dialog_task = app.loop.create_task(window2.dialog(window2_info_dialog)) await main_window_probe.wait_for_window("Window 2 is current") if app_probe.supports_current_window_assignment: assert app.current_window == window2 - await app_probe.redraw("select 'OK'") - # Cancel the task to avoid dangling - window2_dialog_task.cancel() - - window3_info_dialog = toga.InfoDialog("Third Window Info", "Some info") - app_probe.setup_info_dialog_result(window3_info_dialog) app.current_window = window3 - window3_dialog_task = app.loop.create_task(window3.dialog(window3_info_dialog)) await main_window_probe.wait_for_window("Window 3 is current") if app_probe.supports_current_window_assignment: assert app.current_window == window3 - await app_probe.redraw("select 'OK'") - # Cancel the task to avoid dangling - window3_dialog_task.cancel() + + # Test current window when dialog is in focus + window1_probe = window_probe(app, window1) + + app.current_window = window1 + await main_window_probe.wait_for_window("Window 1 is current") + assert app.current_window == window1 + + info_dialog = toga.InfoDialog("Info", "Some info") + window1_probe.setup_info_dialog_result(info_dialog) + + await window1_probe.redraw("Display window1 modal info dialog") + await window1.dialog(info_dialog) + + if app_probe.supports_current_window_assignment: + assert app.current_window == window1 + + # On the native backend, the dialog should be in focus, instead of the window + if toga.platform.current_platform == "macOS": + assert "_NSAlertPanel" in str( + app._impl.native.keyWindow + ), "The dialog is not in focus" + + await window1_probe.redraw("select 'OK'") async def test_session_based_app( From 75f6e41d33e057426cddfc3c4d36a861b2815d21 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 22 Oct 2024 19:42:07 -0700 Subject: [PATCH 08/34] Test Run 2 --- testbed/tests/app/test_desktop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 3a8786d625..446a8e30d8 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -392,7 +392,7 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): assert app.current_window == window1 info_dialog = toga.InfoDialog("Info", "Some info") - window1_probe.setup_info_dialog_result(info_dialog) + app_probe.setup_info_dialog_result(info_dialog) await window1_probe.redraw("Display window1 modal info dialog") await window1.dialog(info_dialog) From 1e04814c16867ce1b99f1aea680af5bfe799781c Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 22 Oct 2024 19:51:00 -0700 Subject: [PATCH 09/34] Test Run 3 Test Run 3 --- testbed/tests/app/test_desktop.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 446a8e30d8..bb1a2f8bb3 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -384,29 +384,22 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): if app_probe.supports_current_window_assignment: assert app.current_window == window3 - # Test current window when dialog is in focus - window1_probe = window_probe(app, window1) - - app.current_window = window1 - await main_window_probe.wait_for_window("Window 1 is current") - assert app.current_window == window1 - info_dialog = toga.InfoDialog("Info", "Some info") app_probe.setup_info_dialog_result(info_dialog) - await window1_probe.redraw("Display window1 modal info dialog") - await window1.dialog(info_dialog) - + app.current_window = window1 + await main_window_probe.wait_for_window("Window 1 is current") + dialog_task = app.loop.create_task(window1.dialog(info_dialog)) + await main_window_probe.wait_for_window("Displayed window1 modal info dialog") if app_probe.supports_current_window_assignment: assert app.current_window == window1 - - # On the native backend, the dialog should be in focus, instead of the window if toga.platform.current_platform == "macOS": assert "_NSAlertPanel" in str( app._impl.native.keyWindow ), "The dialog is not in focus" - - await window1_probe.redraw("select 'OK'") + await app_probe.redraw("select 'OK'") + # Cancel the task to avoid dangling + dialog_task.cancel() async def test_session_based_app( From 43d10055719a84a4302c95eb39dce66b15acc370 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 24 Oct 2024 18:32:45 -0700 Subject: [PATCH 10/34] Cleaning up --- testbed/tests/app/test_desktop.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index bb1a2f8bb3..f3ac14817d 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -374,6 +374,8 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): await main_window_probe.wait_for_window("Extra windows added") + # When a window without any dialog is made the current_window, + # then `app.current_window` should return the specified window. app.current_window = window2 await main_window_probe.wait_for_window("Window 2 is current") if app_probe.supports_current_window_assignment: @@ -384,6 +386,8 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): if app_probe.supports_current_window_assignment: assert app.current_window == window3 + # When a dialog is in focus, `app.current_window` should + # return the window from which the dialog was initiated. info_dialog = toga.InfoDialog("Info", "Some info") app_probe.setup_info_dialog_result(info_dialog) @@ -391,8 +395,10 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): await main_window_probe.wait_for_window("Window 1 is current") dialog_task = app.loop.create_task(window1.dialog(info_dialog)) await main_window_probe.wait_for_window("Displayed window1 modal info dialog") + # The public API should report that current window is the specified window if app_probe.supports_current_window_assignment: assert app.current_window == window1 + # But, the backend should be reporting that the current window is the dialog if toga.platform.current_platform == "macOS": assert "_NSAlertPanel" in str( app._impl.native.keyWindow @@ -401,6 +407,8 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): # Cancel the task to avoid dangling dialog_task.cancel() + assert app.current_window == window1 + async def test_session_based_app( monkeypatch, From a373f26c3f6241f1bd58f6840cf96e3c2ad49235 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 1 Nov 2024 04:06:38 -0700 Subject: [PATCH 11/34] Modified setup_info_dialog_result --- android/tests_backend/app.py | 3 +++ cocoa/src/toga_cocoa/app.py | 4 +--- cocoa/src/toga_cocoa/libs/appkit.py | 1 + cocoa/tests_backend/app.py | 16 +++++++++++-- cocoa/tests_backend/dialogs.py | 6 +++-- cocoa/tests_backend/window.py | 5 ++++- gtk/tests_backend/app.py | 3 +++ iOS/tests_backend/app.py | 3 +++ testbed/tests/app/test_desktop.py | 35 ++++++++++++++--------------- winforms/tests_backend/app.py | 3 +++ 10 files changed, 53 insertions(+), 26 deletions(-) diff --git a/android/tests_backend/app.py b/android/tests_backend/app.py index 2d82b1e7f9..b226b0a4f2 100644 --- a/android/tests_backend/app.py +++ b/android/tests_backend/app.py @@ -42,6 +42,9 @@ def logs_path(self): def assert_app_icon(self, icon): pytest.xfail("Android apps don't have app icons at runtime") + def assert_dialog_in_focus(self, dialog): + pass + def _menu_item(self, path): menu = self.main_window_probe._native_menu() for i_path, label in enumerate(path): diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 5ad3f3d012..6d60142dbe 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -6,7 +6,6 @@ SEL, NSMutableDictionary, NSObject, - ObjCClass, objc_method, objc_property, ) @@ -31,12 +30,11 @@ NSMenu, NSMenuItem, NSNumber, + NSPanel, NSScreen, ) from .screens import Screen as ScreenImpl -NSPanel = ObjCClass("NSPanel") - class AppDelegate(NSObject): interface = objc_property(object, weak=True) diff --git a/cocoa/src/toga_cocoa/libs/appkit.py b/cocoa/src/toga_cocoa/libs/appkit.py index 2e656e8b45..15cb62662b 100644 --- a/cocoa/src/toga_cocoa/libs/appkit.py +++ b/cocoa/src/toga_cocoa/libs/appkit.py @@ -545,6 +545,7 @@ class NSLineBreakMode(Enum): ###################################################################### # NSPanel.h +NSPanel = ObjCClass("NSPanel") NSUtilityWindowMask = 1 << 4 diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index 3390ba4de6..a8151d4943 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -10,13 +10,13 @@ NSEvent, NSEventModifierFlagShift, NSEventType, + NSPanel, NSWindow, ) from .dialogs import DialogsMixin from .probe import BaseProbe, NSRunLoop -NSPanel = ObjCClass("NSPanel") NSDate = ObjCClass("NSDate") @@ -190,6 +190,14 @@ def activate_menu_close_all_windows(self): def activate_menu_minimize(self): self._activate_menu_item(["Window", "Minimize"]) + def assert_dialog_in_focus(self, dialog): + # Cannot directly compare `dialog._impl.native`(`NSAlert`) and + # `self.app._impl.native.keyWindow`(`_NSAlertPanel`) objects. + # Hence, do a string comparison instead. + assert str(dialog._impl.native.objc_class.__name__) in str( + self.app._impl.native.keyWindow.objc_class.__name__ + ), "The dialog is not in focus" + def assert_menu_item(self, path, enabled): item = self._menu_item(path) assert item.isEnabled() == enabled @@ -247,7 +255,7 @@ async def restore_standard_app(self): self.app._impl.native.activateIgnoringOtherApps(True) await self.redraw("Restore to standard app", delay=0.1) - def _setup_alert_dialog_result(self, dialog, result): + def _setup_alert_dialog_result(self, dialog, result, pre_close_test=None): # Replace the dialog polling mechanism with an implementation that polls # 5 times, then returns the required result. _poll_modal_session = dialog._impl._poll_modal_session @@ -258,6 +266,10 @@ def auto_poll_modal_session(nsapp, session): if count < 5: count += 1 return _poll_modal_session(nsapp, session) + + if pre_close_test: + pre_close_test() + return result dialog._impl._poll_modal_session = auto_poll_modal_session diff --git a/cocoa/tests_backend/dialogs.py b/cocoa/tests_backend/dialogs.py index f7170b8d72..fabb1cc450 100644 --- a/cocoa/tests_backend/dialogs.py +++ b/cocoa/tests_backend/dialogs.py @@ -16,8 +16,10 @@ class DialogsMixin: supports_multiple_select_folder = True - def setup_info_dialog_result(self, dialog): - self._setup_alert_dialog_result(dialog, NSAlertFirstButtonReturn) + def setup_info_dialog_result(self, dialog, pre_close_test_method=None): + self._setup_alert_dialog_result( + dialog, NSAlertFirstButtonReturn, pre_close_test_method + ) def setup_question_dialog_result(self, dialog, result): if result: diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index ce2036d1b2..8da2010ed9 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -91,7 +91,7 @@ def press_toolbar_button(self, index): argtypes=[objc_id], ) - def _setup_alert_dialog_result(self, dialog, result): + def _setup_alert_dialog_result(self, dialog, result, pre_close_test=None): # Install an overridden show method that invokes the original, # but then closes the open dialog. orig_show = dialog._impl.show @@ -99,6 +99,9 @@ def _setup_alert_dialog_result(self, dialog, result): def automated_show(host_window, future): orig_show(host_window, future) + if pre_close_test: + pre_close_test() + dialog._impl.host_window.endSheet( dialog._impl.host_window.attachedSheet, returnCode=result, diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index e00d9b205c..1375569a91 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -73,6 +73,9 @@ def assert_app_icon(self, icon): mid_color = img.getpixel((img.size[0] // 2, img.size[1] // 2)) assert mid_color == (149, 119, 73, 255) + def assert_dialog_in_focus(self, dialog): + pass + def _menu_item(self, path): main_menu = self.app._impl.native.get_menubar() menu = main_menu diff --git a/iOS/tests_backend/app.py b/iOS/tests_backend/app.py index d0810d7224..c56adbf4dc 100644 --- a/iOS/tests_backend/app.py +++ b/iOS/tests_backend/app.py @@ -48,6 +48,9 @@ def logs_path(self): def assert_app_icon(self, icon): pytest.xfail("iOS apps don't have app icons at runtime") + def assert_dialog_in_focus(self, dialog): + pass + def assert_system_menus(self): pytest.skip("Menus not implemented on iOS") diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index f3ac14817d..40f0b2373d 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -386,27 +386,26 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): if app_probe.supports_current_window_assignment: assert app.current_window == window3 - # When a dialog is in focus, `app.current_window` should - # return the window from which the dialog was initiated. + # When a dialog is in focus, `app.current_window` should return the window from + # which the dialog was initiated. info_dialog = toga.InfoDialog("Info", "Some info") - app_probe.setup_info_dialog_result(info_dialog) - app.current_window = window1 - await main_window_probe.wait_for_window("Window 1 is current") - dialog_task = app.loop.create_task(window1.dialog(info_dialog)) - await main_window_probe.wait_for_window("Displayed window1 modal info dialog") - # The public API should report that current window is the specified window - if app_probe.supports_current_window_assignment: - assert app.current_window == window1 - # But, the backend should be reporting that the current window is the dialog - if toga.platform.current_platform == "macOS": - assert "_NSAlertPanel" in str( - app._impl.native.keyWindow - ), "The dialog is not in focus" - await app_probe.redraw("select 'OK'") - # Cancel the task to avoid dangling - dialog_task.cancel() + def test_current_window_in_presence_of_dialog(): + # The public API should report that current window is the specified window + if app_probe.supports_current_window_assignment: + assert app.current_window == window1 + + # But, the backend should be reporting that the current window is the dialog + app_probe.assert_dialog_in_focus(info_dialog) + + main_window_probe.setup_info_dialog_result( + info_dialog, + pre_close_test_method=test_current_window_in_presence_of_dialog, + ) + await main_window_probe.wait_for_window("Display window1 modal info dialog") + await window1.dialog(info_dialog) + # After the dialog exits, window1 should still be the current window. assert app.current_window == window1 diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 8985cffc57..8932158a8b 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -157,6 +157,9 @@ async def close_about_dialog(self): def activate_menu_visit_homepage(self): self._activate_menu_item(["Help", "Visit homepage"]) + def assert_dialog_in_focus(self, dialog): + pass + def assert_menu_item(self, path, *, enabled=True): item = self._menu_item(path) assert item.Enabled == enabled From 67d2fdeed021e37f8582ffcefa38f3c32aca7987 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 1 Nov 2024 04:19:40 -0700 Subject: [PATCH 12/34] Modified setup_info_dialog_result --- android/tests_backend/dialogs.py | 2 +- gtk/tests_backend/dialogs.py | 2 +- iOS/tests_backend/dialogs.py | 2 +- winforms/tests_backend/dialogs.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/tests_backend/dialogs.py b/android/tests_backend/dialogs.py index 2a99bd533a..e84e109d13 100644 --- a/android/tests_backend/dialogs.py +++ b/android/tests_backend/dialogs.py @@ -28,7 +28,7 @@ async def _close_dialog(): dialog._impl.show = automated_show - def setup_info_dialog_result(self, dialog): + def setup_info_dialog_result(self, dialog, pre_close_test_method=None): self._setup_alert_dialog_result(dialog, ["OK"], 0) def setup_question_dialog_result(self, dialog, result): diff --git a/gtk/tests_backend/dialogs.py b/gtk/tests_backend/dialogs.py index eebdf188f7..58f0e98fab 100644 --- a/gtk/tests_backend/dialogs.py +++ b/gtk/tests_backend/dialogs.py @@ -36,7 +36,7 @@ def automated_show(host_window, future): dialog._impl.show = automated_show - def setup_info_dialog_result(self, dialog): + def setup_info_dialog_result(self, dialog, pre_close_test_method=None): self._setup_dialog_result(dialog, Gtk.ResponseType.OK) def setup_question_dialog_result(self, dialog, result): diff --git a/iOS/tests_backend/dialogs.py b/iOS/tests_backend/dialogs.py index c2b3517675..c2a2f9dea3 100644 --- a/iOS/tests_backend/dialogs.py +++ b/iOS/tests_backend/dialogs.py @@ -28,7 +28,7 @@ def automated_show(host_window, future): dialog._impl.show = automated_show - def setup_info_dialog_result(self, dialog): + def setup_info_dialog_result(self, dialog, pre_close_test_method=None): self._setup_alert_dialog(dialog, 0) def setup_question_dialog_result(self, dialog, result): diff --git a/winforms/tests_backend/dialogs.py b/winforms/tests_backend/dialogs.py index 483c05f704..77f7a14d94 100644 --- a/winforms/tests_backend/dialogs.py +++ b/winforms/tests_backend/dialogs.py @@ -32,7 +32,7 @@ async def _close_dialog(): dialog._impl.show = automated_show - def setup_info_dialog_result(self, dialog): + def setup_info_dialog_result(self, dialog, pre_close_test_method=None): self._setup_dialog_result(dialog, "\n") def setup_question_dialog_result(self, dialog, result): From 6307ac607ed3253d780f08153b884716e21cb036 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 1 Nov 2024 04:41:30 -0700 Subject: [PATCH 13/34] Modified setup_info_dialog_result --- testbed/tests/app/test_desktop.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 40f0b2373d..8dc187a1fb 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -405,8 +405,6 @@ def test_current_window_in_presence_of_dialog(): await main_window_probe.wait_for_window("Display window1 modal info dialog") await window1.dialog(info_dialog) - # After the dialog exits, window1 should still be the current window. - assert app.current_window == window1 async def test_session_based_app( From 33a96944416a680df37023430eee9101134d1e38 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Fri, 1 Nov 2024 22:21:40 -0700 Subject: [PATCH 14/34] Added note to docs --- core/src/toga/app.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 7b54c9d4d4..65b77abe0d 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -827,7 +827,12 @@ def show_cursor(self) -> None: @property def current_window(self) -> Window | None: - """Return the currently active window.""" + """The window currently in active focus. + + On macOS, when a :any:`Dialog` is dismissed, the window from which the dialog + was initiated becomes the :any:`App.current_window`, regardless of whether the + :any:`App.current_window` property was modified. + """ window = self._impl.get_current_window() if window is None: return None From beb30110547e178e5b301721eacd88381eded6d4 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 5 Nov 2024 07:49:27 -0800 Subject: [PATCH 15/34] Fixed bug and probe on winforms --- cocoa/tests_backend/app.py | 6 +++--- cocoa/tests_backend/window.py | 6 +++--- winforms/src/toga_winforms/app.py | 11 +++++++++++ winforms/src/toga_winforms/dialogs.py | 14 +++++++++++++- winforms/tests_backend/app.py | 12 +++++++++++- winforms/tests_backend/dialogs.py | 9 +++++++-- 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index a8151d4943..65d9abd712 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -255,7 +255,7 @@ async def restore_standard_app(self): self.app._impl.native.activateIgnoringOtherApps(True) await self.redraw("Restore to standard app", delay=0.1) - def _setup_alert_dialog_result(self, dialog, result, pre_close_test=None): + def _setup_alert_dialog_result(self, dialog, result, pre_close_test_method=None): # Replace the dialog polling mechanism with an implementation that polls # 5 times, then returns the required result. _poll_modal_session = dialog._impl._poll_modal_session @@ -267,8 +267,8 @@ def auto_poll_modal_session(nsapp, session): count += 1 return _poll_modal_session(nsapp, session) - if pre_close_test: - pre_close_test() + if pre_close_test_method: + pre_close_test_method() return result diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 8da2010ed9..dfdd165f8d 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -91,7 +91,7 @@ def press_toolbar_button(self, index): argtypes=[objc_id], ) - def _setup_alert_dialog_result(self, dialog, result, pre_close_test=None): + def _setup_alert_dialog_result(self, dialog, result, pre_close_test_method=None): # Install an overridden show method that invokes the original, # but then closes the open dialog. orig_show = dialog._impl.show @@ -99,8 +99,8 @@ def _setup_alert_dialog_result(self, dialog, result, pre_close_test=None): def automated_show(host_window, future): orig_show(host_window, future) - if pre_close_test: - pre_close_test() + if pre_close_test_method: + pre_close_test_method() dialog._impl.host_window.endSheet( dialog._impl.host_window.attachedSheet, diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 54194a20a5..cf03fdc66f 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -251,6 +251,17 @@ def get_current_window(self): for window in self.interface.windows: if WinForms.Form.ActiveForm == window._impl.native: return window._impl + + # If the focus is on a dialog, then return its host window + active_window_handle = windll.user32.GetForegroundWindow() + dialog_impl = getattr(window._impl, "dialog_impl", None) + if dialog_impl: + dialog_title = getattr(dialog_impl, "title", None) + if active_window_handle == windll.user32.FindWindowW( + None, dialog_title if dialog_title else dialog_impl.native.Title + ): + return window._impl + return None def set_current_window(self, window): diff --git a/winforms/src/toga_winforms/dialogs.py b/winforms/src/toga_winforms/dialogs.py index a6a9fc3a14..951cfbd812 100644 --- a/winforms/src/toga_winforms/dialogs.py +++ b/winforms/src/toga_winforms/dialogs.py @@ -17,7 +17,9 @@ class BaseDialog: def show(self, host_window, future): self.future = future - + self.host_window = host_window + if host_window: + self.host_window._impl.dialog_impl = self # Don't differentiate between app and window modal dialogs # Show the dialog using an inner loop. asyncio.get_event_loop().start_inner_loop(self._show) @@ -50,6 +52,8 @@ def _show(self): self.future.set_result(return_value == self.success_result) else: self.future.set_result(None) + if self.host_window: + del self.host_window._impl.dialog_impl class InfoDialog(MessageDialog): @@ -177,14 +181,20 @@ def winforms_FormClosing(self, sender, event): def winforms_Click_quit(self, sender, event): self.future.set_result(False) self.native.Close() + if self.host_window: + del self.host_window._impl.dialog_impl def winforms_Click_retry(self, sender, event): self.future.set_result(True) self.native.Close() + if self.host_window: + del self.host_window._impl.dialog_impl def winforms_Click_accept(self, sender, event): self.future.set_result(None) self.native.Close() + if self.host_window: + del self.host_window._impl.dialog_impl class FileDialog(BaseDialog): @@ -224,6 +234,8 @@ def _show(self): self.future.set_result(self._get_filenames()) else: self.future.set_result(None) + if self.host_window: + del self.host_window._impl.dialog_impl def _set_title(self, title): self.native.Title = title diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 8932158a8b..65ba215167 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -158,7 +158,17 @@ def activate_menu_visit_homepage(self): self._activate_menu_item(["Help", "Visit homepage"]) def assert_dialog_in_focus(self, dialog): - pass + active_window_handle = ctypes.windll.user32.GetForegroundWindow() + # Cannot directly get the handle from the native WinForms object + # as the MessageBox dialog doesn't expose a Handle property. + # Hence, use user32 to get the hwnd for comparison. + dialog_title = getattr(dialog._impl, "title", None) + expected_dialog_handle = ctypes.windll.user32.FindWindowW( + None, dialog_title if dialog_title else dialog._impl.native.Title + ) + assert ( + expected_dialog_handle == active_window_handle + ), "The dialog is not in focus" def assert_menu_item(self, path, *, enabled=True): item = self._menu_item(path) diff --git a/winforms/tests_backend/dialogs.py b/winforms/tests_backend/dialogs.py index 77f7a14d94..29cf2a1243 100644 --- a/winforms/tests_backend/dialogs.py +++ b/winforms/tests_backend/dialogs.py @@ -7,7 +7,7 @@ class DialogsMixin: supports_multiple_select_folder = False - def _setup_dialog_result(self, dialog, char, alt=False): + def _setup_dialog_result(self, dialog, char, alt=False, pre_close_test_method=None): # Install an overridden show method that invokes the original, # but then closes the open dialog. orig_show = dialog._impl.show @@ -21,6 +21,9 @@ async def _close_dialog(): # sleep(0), but the file dialogs require it to be positive for some reason. await asyncio.sleep(0.001) + if pre_close_test_method: + pre_close_test_method() + await self.type_character(char, alt=alt) except Exception as e: @@ -33,7 +36,9 @@ async def _close_dialog(): dialog._impl.show = automated_show def setup_info_dialog_result(self, dialog, pre_close_test_method=None): - self._setup_dialog_result(dialog, "\n") + self._setup_dialog_result( + dialog, "\n", pre_close_test_method=pre_close_test_method + ) def setup_question_dialog_result(self, dialog, result): self._setup_dialog_result(dialog, "y" if result else "n") From 9c6e99e55003337fa52b8db126def82dafca9478 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 5 Nov 2024 07:54:55 -0800 Subject: [PATCH 16/34] Removed unnecessary note for macOS. --- core/src/toga/app.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 65b77abe0d..7b54c9d4d4 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -827,12 +827,7 @@ def show_cursor(self) -> None: @property def current_window(self) -> Window | None: - """The window currently in active focus. - - On macOS, when a :any:`Dialog` is dismissed, the window from which the dialog - was initiated becomes the :any:`App.current_window`, regardless of whether the - :any:`App.current_window` property was modified. - """ + """Return the currently active window.""" window = self._impl.get_current_window() if window is None: return None From 3d475999df12b4f016cf376e77bbbaa3f4ac6aa1 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 5 Nov 2024 08:19:51 -0800 Subject: [PATCH 17/34] Added pragma no branch --- winforms/src/toga_winforms/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index cf03fdc66f..ba02788ec8 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -259,7 +259,10 @@ def get_current_window(self): dialog_title = getattr(dialog_impl, "title", None) if active_window_handle == windll.user32.FindWindowW( None, dialog_title if dialog_title else dialog_impl.native.Title - ): + ): # pragma: no branch + # Marking as no branch, since a window having dialog_impl + # will have its dialog in focus, as all dialogs are shown + # in modal style. return window._impl return None From dc62b343953ea5314b3a564261e6e53506756192 Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Tue, 5 Nov 2024 08:32:22 -0800 Subject: [PATCH 18/34] Restart CI for intermittent readthedocs failures From 28880fa679971f737dce1088d45be8fda56a94cc Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 6 Nov 2024 07:41:37 -0500 Subject: [PATCH 19/34] Implemented on GTK --- gtk/src/toga_gtk/app.py | 13 ++++++++++++- gtk/src/toga_gtk/dialogs.py | 6 ++++++ gtk/tests_backend/dialogs.py | 11 +++++++++-- testbed/tests/app/test_desktop.py | 6 +++--- winforms/src/toga_winforms/dialogs.py | 2 +- 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 467614e6d8..161831519e 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -236,7 +236,18 @@ def show_cursor(self): def get_current_window(self): # pragma: no-cover-if-linux-wayland current_window = self.native.get_active_window()._impl - return current_window if current_window.interface.visible else None + if current_window.interface.visible: + # If the focus is on a dialog, then return its host window + for window in self.interface.windows: + dialog_impl = getattr(window._impl, "dialog_impl", None) + if dialog_impl: + if dialog_impl.native.is_visible(): # pragma: no branch + # Marking as no branch, since a window having dialog_impl + # will have its dialog visible, as all dialogs are shown + # in modal style. + return window._impl + return current_window + return None def set_current_window(self, window): window._impl.native.present() diff --git a/gtk/src/toga_gtk/dialogs.py b/gtk/src/toga_gtk/dialogs.py index 968c1d2a38..e6954fce45 100644 --- a/gtk/src/toga_gtk/dialogs.py +++ b/gtk/src/toga_gtk/dialogs.py @@ -9,6 +9,8 @@ def show(self, host_window, future): # If this is a modal dialog, set the window as transient to the host window. if host_window: + self.host_window = host_window + self.host_window._impl.dialog_impl = self self.native.set_transient_for(host_window._impl.native) else: self.native.set_transient_for(None) @@ -52,6 +54,8 @@ def gtk_response(self, dialog, response): self.future.set_result(result) self.native.destroy() + if self.host_window: + del self.host_window._impl.dialog_impl class InfoDialog(MessageDialog): @@ -209,6 +213,8 @@ def gtk_response(self, dialog, response): self.future.set_result(result) self.native.destroy() + if self.host_window: + del self.host_window._impl.dialog_impl class SaveFileDialog(FileDialog): diff --git a/gtk/tests_backend/dialogs.py b/gtk/tests_backend/dialogs.py index 58f0e98fab..eee1cc4bd2 100644 --- a/gtk/tests_backend/dialogs.py +++ b/gtk/tests_backend/dialogs.py @@ -21,7 +21,9 @@ def _default_close_handler(self, dialog, gtk_result): dialog._impl.native.response(gtk_result) self._wait_for_dialog("Wait for dialog to disappear") - def _setup_dialog_result(self, dialog, gtk_result, close_handler=None): + def _setup_dialog_result( + self, dialog, gtk_result, close_handler=None, pre_close_test_method=None + ): # Install an overridden show method that invokes the original, # but then closes the open dialog. orig_show = dialog._impl.show @@ -29,6 +31,9 @@ def _setup_dialog_result(self, dialog, gtk_result, close_handler=None): def automated_show(host_window, future): orig_show(host_window, future) + if pre_close_test_method: + pre_close_test_method() + if close_handler: close_handler(dialog, gtk_result) else: @@ -37,7 +42,9 @@ def automated_show(host_window, future): dialog._impl.show = automated_show def setup_info_dialog_result(self, dialog, pre_close_test_method=None): - self._setup_dialog_result(dialog, Gtk.ResponseType.OK) + self._setup_dialog_result( + dialog, Gtk.ResponseType.OK, pre_close_test_method=pre_close_test_method + ) def setup_question_dialog_result(self, dialog, result): self._setup_dialog_result( diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 8dc187a1fb..56e709e0da 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -360,9 +360,9 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): finally: main_window.show() - window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200)) - window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200)) - window3 = toga.Window("Test Window 3", position=(300, 400), size=(200, 200)) + window1 = toga.Window(title="Test Window 1", position=(150, 150), size=(200, 200)) + window2 = toga.Window(title="Test Window 2", position=(400, 150), size=(200, 200)) + window3 = toga.Window(title="Test Window 3", position=(300, 400), size=(200, 200)) window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE)) window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) diff --git a/winforms/src/toga_winforms/dialogs.py b/winforms/src/toga_winforms/dialogs.py index 951cfbd812..55e28e9726 100644 --- a/winforms/src/toga_winforms/dialogs.py +++ b/winforms/src/toga_winforms/dialogs.py @@ -17,8 +17,8 @@ class BaseDialog: def show(self, host_window, future): self.future = future - self.host_window = host_window if host_window: + self.host_window = host_window self.host_window._impl.dialog_impl = self # Don't differentiate between app and window modal dialogs # Show the dialog using an inner loop. From f4020d57678dff1bf7d9229fe542d80b587dbba7 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 7 Nov 2024 02:25:00 -0800 Subject: [PATCH 20/34] Implemented on Android --- android/src/toga_android/dialogs.py | 2 +- android/tests_backend/app.py | 4 +++- android/tests_backend/dialogs.py | 11 +++++++++-- testbed/tests/app/test_mobile.py | 21 ++++++++++++++++++++- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/android/src/toga_android/dialogs.py b/android/src/toga_android/dialogs.py index 56c3366bd0..e6d430214c 100644 --- a/android/src/toga_android/dialogs.py +++ b/android/src/toga_android/dialogs.py @@ -22,7 +22,7 @@ def show(self, host_window, future): if self.native: # Show the dialog. Don't differentiate between app and window modal dialogs. - self.native.show() + self.native_alert_dialog = self.native.show() else: # Dialog doesn't have an implementation. This can't be covered, as # the testbed shortcuts the test before showing the dialog. diff --git a/android/tests_backend/app.py b/android/tests_backend/app.py index b226b0a4f2..aa6d0fed08 100644 --- a/android/tests_backend/app.py +++ b/android/tests_backend/app.py @@ -43,7 +43,9 @@ def assert_app_icon(self, icon): pytest.xfail("Android apps don't have app icons at runtime") def assert_dialog_in_focus(self, dialog): - pass + assert ( + dialog._impl.native_alert_dialog.isShowing() is True + ), "The dialog is not in focus" def _menu_item(self, path): menu = self.main_window_probe._native_menu() diff --git a/android/tests_backend/dialogs.py b/android/tests_backend/dialogs.py index e84e109d13..d0d83ea52b 100644 --- a/android/tests_backend/dialogs.py +++ b/android/tests_backend/dialogs.py @@ -4,7 +4,9 @@ class DialogsMixin: - def _setup_alert_dialog_result(self, dialog, buttons, selected_index): + def _setup_alert_dialog_result( + self, dialog, buttons, selected_index, pre_close_test_method=None + ): # Install an overridden show method that invokes the original, # but then closes the open dialog. orig_show = dialog._impl.show @@ -16,6 +18,9 @@ async def _close_dialog(): # Inject a small pause without blocking the event loop await asyncio.sleep(1.0 if self.app.run_slow else 0.2) try: + if pre_close_test_method: + pre_close_test_method() + dialog_view = self.get_dialog_view() self.assert_dialog_buttons(dialog_view, buttons) await self.press_dialog_button(dialog_view, buttons[selected_index]) @@ -29,7 +34,9 @@ async def _close_dialog(): dialog._impl.show = automated_show def setup_info_dialog_result(self, dialog, pre_close_test_method=None): - self._setup_alert_dialog_result(dialog, ["OK"], 0) + self._setup_alert_dialog_result( + dialog, ["OK"], 0, pre_close_test_method=pre_close_test_method + ) def setup_question_dialog_result(self, dialog, result): self._setup_alert_dialog_result(dialog, ["No", "Yes"], 1 if result else 0) diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index d019c70f88..c30ac53846 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -43,7 +43,7 @@ async def test_full_screen(app): app.exit_full_screen() -async def test_current_window(app, main_window, main_window_probe): +async def test_current_window(app, app_probe, main_window, main_window_probe): """The current window can be retrieved""" assert app.current_window == main_window @@ -52,6 +52,25 @@ async def test_current_window(app, main_window, main_window_probe): await main_window_probe.wait_for_window("Main window is still current") assert app.current_window == main_window + # When a dialog is in focus, `app.current_window` should return the window from + # which the dialog was initiated. + info_dialog = toga.InfoDialog("Info", "Some info") + + def test_current_window_in_presence_of_dialog(): + # The public API should report that current window is the specified window + assert app.current_window == main_window + + # But, the backend should be reporting that the focus is on the dialog + app_probe.assert_dialog_in_focus(info_dialog) + + main_window_probe.setup_info_dialog_result( + info_dialog, + pre_close_test_method=test_current_window_in_presence_of_dialog, + ) + + await main_window_probe.wait_for_window("Display window1 modal info dialog") + await main_window.dialog(info_dialog) + async def test_app_lifecycle(app, app_probe): """Application lifecycle can be exercised""" From 53d6f712592245cee517a6445664a043cd7ab312 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Thu, 7 Nov 2024 03:09:06 -0800 Subject: [PATCH 21/34] Implemented on iOS --- gtk/tests_backend/app.py | 2 +- iOS/tests_backend/app.py | 5 ++++- iOS/tests_backend/dialogs.py | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index 1375569a91..4ec6de7a62 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -74,7 +74,7 @@ def assert_app_icon(self, icon): assert mid_color == (149, 119, 73, 255) def assert_dialog_in_focus(self, dialog): - pass + assert dialog._impl.native.is_visible() is True, "The dialog is not in focus" def _menu_item(self, path): main_menu = self.app._impl.native.get_menubar() diff --git a/iOS/tests_backend/app.py b/iOS/tests_backend/app.py index c56adbf4dc..42e6e91201 100644 --- a/iOS/tests_backend/app.py +++ b/iOS/tests_backend/app.py @@ -49,7 +49,10 @@ def assert_app_icon(self, icon): pytest.xfail("iOS apps don't have app icons at runtime") def assert_dialog_in_focus(self, dialog): - pass + root_view_controller = self.native.keyWindow.rootViewController + assert ( + root_view_controller.presentedViewController == dialog._impl.native + ), "The dialog is not in focus" def assert_system_menus(self): pytest.skip("Menus not implemented on iOS") diff --git a/iOS/tests_backend/dialogs.py b/iOS/tests_backend/dialogs.py index c2a2f9dea3..e4e7f9f0ae 100644 --- a/iOS/tests_backend/dialogs.py +++ b/iOS/tests_backend/dialogs.py @@ -8,7 +8,7 @@ class DialogsMixin: def dialog_view_controller(self): return self.app.current_window._impl.native.rootViewController - def _setup_alert_dialog(self, dialog, action_index): + def _setup_alert_dialog(self, dialog, action_index, pre_close_test_method=None): # Install an overridden show method that invokes the original, # but then closes the open dialog. orig_show = dialog._impl.show @@ -20,6 +20,10 @@ def automated_show(host_window, future): NSRunLoop.currentRunLoop.runUntilDate( NSDate.dateWithTimeIntervalSinceNow(1.0 if self.app.run_slow else 0.2) ) + + if pre_close_test_method: + pre_close_test_method() + # Close the dialog and trigger the completion handler self.dialog_view_controller.dismissViewControllerAnimated( False, completion=None @@ -29,7 +33,7 @@ def automated_show(host_window, future): dialog._impl.show = automated_show def setup_info_dialog_result(self, dialog, pre_close_test_method=None): - self._setup_alert_dialog(dialog, 0) + self._setup_alert_dialog(dialog, 0, pre_close_test_method=pre_close_test_method) def setup_question_dialog_result(self, dialog, result): self._setup_alert_dialog(dialog, 0 if result else 1) From a479725e3a882ffe752268da62d6d178c9fd05eb Mon Sep 17 00:00:00 2001 From: proneon267 <45512885+proneon267@users.noreply.github.com> Date: Wed, 6 Nov 2024 19:30:28 -0800 Subject: [PATCH 22/34] Update 2926.bugfix.rst --- changes/2926.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/2926.bugfix.rst b/changes/2926.bugfix.rst index 038d65af12..5e9ee6393b 100644 --- a/changes/2926.bugfix.rst +++ b/changes/2926.bugfix.rst @@ -1 +1 @@ -On macOS, when a dialog is in focus, `App.current_window` now returns the window from which the dialog was initiated. +On desktop backends, when a dialog is in focus, `App.current_window` now returns the window from which the dialog was initiated. From 39190677cbcf0f489387ec6f806f7f2cce757ba9 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sat, 9 Nov 2024 18:11:58 -0800 Subject: [PATCH 23/34] Reimplemented on Winforms --- winforms/src/toga_winforms/app.py | 36 +++++++++++++++------------ winforms/src/toga_winforms/dialogs.py | 25 ++++++++----------- winforms/tests_backend/app.py | 8 +++--- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index ba02788ec8..b8cd54965a 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -68,6 +68,10 @@ def __init__(self, interface): # boolean to allow us to avoid building a deep stack. self._cursor_visible = True + # The currently visible dialog. On winforms, all dialogs are modal by nature + # and the active focus can't be changed unless the modal dialog is dismissed. + self._current_dialog = None + self.loop = WinformsProactorEventLoop() asyncio.set_event_loop(self.loop) @@ -248,23 +252,23 @@ def show_cursor(self): ###################################################################### def get_current_window(self): - for window in self.interface.windows: - if WinForms.Form.ActiveForm == window._impl.native: - return window._impl - - # If the focus is on a dialog, then return its host window - active_window_handle = windll.user32.GetForegroundWindow() - dialog_impl = getattr(window._impl, "dialog_impl", None) - if dialog_impl: - dialog_title = getattr(dialog_impl, "title", None) - if active_window_handle == windll.user32.FindWindowW( - None, dialog_title if dialog_title else dialog_impl.native.Title - ): # pragma: no branch - # Marking as no branch, since a window having dialog_impl - # will have its dialog in focus, as all dialogs are shown - # in modal style. + # There can be only 1 dialog visible at a time, as all dialogs are modal + if self._current_dialog: + active_window_hwnd = windll.user32.GetForegroundWindow() + # The window class name for dialog boxes is "#32770": + # https://learn.microsoft.com/en-us/windows/win32/winauto/dialog-box + current_dialog_hwnd = windll.user32.FindWindowW( + "#32770", self._current_dialog.title + ) + return ( + self._current_dialog.host_window_impl + if active_window_hwnd == current_dialog_hwnd + else None + ) + else: + for window in self.interface.windows: + if WinForms.Form.ActiveForm == window._impl.native: return window._impl - return None def set_current_window(self, window): diff --git a/winforms/src/toga_winforms/dialogs.py b/winforms/src/toga_winforms/dialogs.py index 55e28e9726..392d397dd2 100644 --- a/winforms/src/toga_winforms/dialogs.py +++ b/winforms/src/toga_winforms/dialogs.py @@ -11,15 +11,16 @@ ) from System.Windows.Forms import DialogResult, MessageBoxButtons, MessageBoxIcon +from toga import App + from .libs.wrapper import WeakrefCallable class BaseDialog: def show(self, host_window, future): self.future = future - if host_window: - self.host_window = host_window - self.host_window._impl.dialog_impl = self + self.host_window_impl = getattr(host_window, "_impl", None) + App.app._impl._current_dialog = self # Don't differentiate between app and window modal dialogs # Show the dialog using an inner loop. asyncio.get_event_loop().start_inner_loop(self._show) @@ -52,8 +53,7 @@ def _show(self): self.future.set_result(return_value == self.success_result) else: self.future.set_result(None) - if self.host_window: - del self.host_window._impl.dialog_impl + App.app._impl._current_dialog = None class InfoDialog(MessageDialog): @@ -102,6 +102,7 @@ class StackTraceDialog(BaseDialog): def __init__(self, title, message, content, retry): super().__init__() + self.title = title self.native = WinForms.Form() self.native.MinimizeBox = False self.native.FormBorderStyle = self.native.FormBorderStyle.FixedSingle @@ -181,20 +182,17 @@ def winforms_FormClosing(self, sender, event): def winforms_Click_quit(self, sender, event): self.future.set_result(False) self.native.Close() - if self.host_window: - del self.host_window._impl.dialog_impl + App.app._impl._current_dialog = None def winforms_Click_retry(self, sender, event): self.future.set_result(True) self.native.Close() - if self.host_window: - del self.host_window._impl.dialog_impl + App.app._impl._current_dialog = None def winforms_Click_accept(self, sender, event): self.future.set_result(None) self.native.Close() - if self.host_window: - del self.host_window._impl.dialog_impl + App.app._impl._current_dialog = None class FileDialog(BaseDialog): @@ -208,6 +206,7 @@ def __init__( file_types=None, ): super().__init__() + self.title = title self.native = native self._set_title(title) @@ -234,8 +233,7 @@ def _show(self): self.future.set_result(self._get_filenames()) else: self.future.set_result(None) - if self.host_window: - del self.host_window._impl.dialog_impl + App.app._impl._current_dialog = None def _set_title(self, title): self.native.Title = title @@ -293,7 +291,6 @@ def __init__(self, title, initial_directory, multiple_select): title, initial_directory, ) - # The native dialog doesn't support multiple selection, so the only effect # this has is to change whether we return a list. self.multiple_select = multiple_select diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 65ba215167..c24ade1f39 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -159,12 +159,10 @@ def activate_menu_visit_homepage(self): def assert_dialog_in_focus(self, dialog): active_window_handle = ctypes.windll.user32.GetForegroundWindow() - # Cannot directly get the handle from the native WinForms object - # as the MessageBox dialog doesn't expose a Handle property. - # Hence, use user32 to get the hwnd for comparison. - dialog_title = getattr(dialog._impl, "title", None) + # The window class name for dialog boxes is "#32770": + # https://learn.microsoft.com/en-us/windows/win32/winauto/dialog-box expected_dialog_handle = ctypes.windll.user32.FindWindowW( - None, dialog_title if dialog_title else dialog._impl.native.Title + "#32770", dialog._impl.title ) assert ( expected_dialog_handle == active_window_handle From a21c30f63fda1924a2d084114e2539c4d23dc4d0 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 10 Nov 2024 03:06:26 -0800 Subject: [PATCH 24/34] Correct probe method on cocoa --- cocoa/tests_backend/app.py | 7 ++----- winforms/src/toga_winforms/dialogs.py | 2 ++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index 65d9abd712..d20c28a694 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -191,11 +191,8 @@ def activate_menu_minimize(self): self._activate_menu_item(["Window", "Minimize"]) def assert_dialog_in_focus(self, dialog): - # Cannot directly compare `dialog._impl.native`(`NSAlert`) and - # `self.app._impl.native.keyWindow`(`_NSAlertPanel`) objects. - # Hence, do a string comparison instead. - assert str(dialog._impl.native.objc_class.__name__) in str( - self.app._impl.native.keyWindow.objc_class.__name__ + assert ( + dialog._impl.native.window == self.app._impl.native.keyWindow ), "The dialog is not in focus" def assert_menu_item(self, path, enabled): diff --git a/winforms/src/toga_winforms/dialogs.py b/winforms/src/toga_winforms/dialogs.py index 392d397dd2..8add609d9f 100644 --- a/winforms/src/toga_winforms/dialogs.py +++ b/winforms/src/toga_winforms/dialogs.py @@ -19,6 +19,7 @@ class BaseDialog: def show(self, host_window, future): self.future = future + self.host_window_impl = getattr(host_window, "_impl", None) App.app._impl._current_dialog = self # Don't differentiate between app and window modal dialogs @@ -291,6 +292,7 @@ def __init__(self, title, initial_directory, multiple_select): title, initial_directory, ) + # The native dialog doesn't support multiple selection, so the only effect # this has is to change whether we return a list. self.multiple_select = multiple_select From dd97a815476c3ba5749ff2d22c64b95ebe402427 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 10 Nov 2024 07:34:02 -0500 Subject: [PATCH 25/34] Reimplemented on gtk --- gtk/src/toga_gtk/app.py | 19 +++++++------------ gtk/src/toga_gtk/dialogs.py | 14 ++++++++------ gtk/tests_backend/app.py | 8 +++++++- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 161831519e..386ca547a6 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -45,6 +45,8 @@ def __init__(self, interface): self.actions = None + self._app_modal_dialog_shown = False + def gtk_activate(self, data=None): pass @@ -236,18 +238,11 @@ def show_cursor(self): def get_current_window(self): # pragma: no-cover-if-linux-wayland current_window = self.native.get_active_window()._impl - if current_window.interface.visible: - # If the focus is on a dialog, then return its host window - for window in self.interface.windows: - dialog_impl = getattr(window._impl, "dialog_impl", None) - if dialog_impl: - if dialog_impl.native.is_visible(): # pragma: no branch - # Marking as no branch, since a window having dialog_impl - # will have its dialog visible, as all dialogs are shown - # in modal style. - return window._impl - return current_window - return None + return ( + current_window + if current_window.interface.visible and not self._app_modal_dialog_shown + else None + ) def set_current_window(self, window): window._impl.native.present() diff --git a/gtk/src/toga_gtk/dialogs.py b/gtk/src/toga_gtk/dialogs.py index e6954fce45..abc318fd89 100644 --- a/gtk/src/toga_gtk/dialogs.py +++ b/gtk/src/toga_gtk/dialogs.py @@ -1,5 +1,7 @@ from pathlib import Path +from toga import App + from .libs import Gtk @@ -9,14 +11,16 @@ def show(self, host_window, future): # If this is a modal dialog, set the window as transient to the host window. if host_window: - self.host_window = host_window - self.host_window._impl.dialog_impl = self self.native.set_transient_for(host_window._impl.native) + host_window._impl.native.present() else: self.native.set_transient_for(None) + App.app._impl._app_modal_dialog_shown = True # Show the dialog. self.native.show() + while Gtk.events_pending(): + Gtk.main_iteration_do(False) class MessageDialog(BaseDialog): @@ -54,8 +58,7 @@ def gtk_response(self, dialog, response): self.future.set_result(result) self.native.destroy() - if self.host_window: - del self.host_window._impl.dialog_impl + App.app._impl._app_modal_dialog_shown = False class InfoDialog(MessageDialog): @@ -213,8 +216,7 @@ def gtk_response(self, dialog, response): self.future.set_result(result) self.native.destroy() - if self.host_window: - del self.host_window._impl.dialog_impl + App.app._impl._app_modal_dialog_shown = False class SaveFileDialog(FileDialog): diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index 4ec6de7a62..938774ff14 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -74,7 +74,13 @@ def assert_app_icon(self, icon): assert mid_color == (149, 119, 73, 255) def assert_dialog_in_focus(self, dialog): - assert dialog._impl.native.is_visible() is True, "The dialog is not in focus" + # Gtk.Dialog's methods - is_active(), has_focus() both return False, even + # when the dialog is in focus. Hence,they cannot be used to determine focus. + assert ( + dialog._impl.native.get_transient_for() + == self.app._impl.native.get_active_window() + and dialog._impl.native.is_visible() + ), "The dialog is not in focus" def _menu_item(self, path): main_menu = self.app._impl.native.get_menubar() From fe182768926064c603310f0b47fe0abf55181adc Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 10 Nov 2024 07:42:51 -0500 Subject: [PATCH 26/34] Correct behavior on cocoa --- cocoa/src/toga_cocoa/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 6d60142dbe..e30d2195d2 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -370,7 +370,7 @@ def get_current_window(self): focused_window = self.native.keyWindow if isinstance(focused_window, NSPanel): # If the focus is on a dialog sheet_parent = focused_window.sheetParent - return sheet_parent if sheet_parent else self.native.mainWindow + return sheet_parent if sheet_parent else None else: # If the focus is on a window return focused_window From d3bb9154e6270b4a14c5ce4c2983586f12581b74 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 10 Nov 2024 11:13:28 -0500 Subject: [PATCH 27/34] Correct behavior on gtk --- gtk/src/toga_gtk/dialogs.py | 2 -- gtk/tests_backend/app.py | 14 +++++++++----- gtk/tests_backend/dialogs.py | 19 +++++++++++++------ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/gtk/src/toga_gtk/dialogs.py b/gtk/src/toga_gtk/dialogs.py index abc318fd89..25510d1fdf 100644 --- a/gtk/src/toga_gtk/dialogs.py +++ b/gtk/src/toga_gtk/dialogs.py @@ -19,8 +19,6 @@ def show(self, host_window, future): # Show the dialog. self.native.show() - while Gtk.events_pending(): - Gtk.main_iteration_do(False) class MessageDialog(BaseDialog): diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index 938774ff14..1f9bf72064 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -76,11 +76,15 @@ def assert_app_icon(self, icon): def assert_dialog_in_focus(self, dialog): # Gtk.Dialog's methods - is_active(), has_focus() both return False, even # when the dialog is in focus. Hence,they cannot be used to determine focus. - assert ( - dialog._impl.native.get_transient_for() - == self.app._impl.native.get_active_window() - and dialog._impl.native.is_visible() - ), "The dialog is not in focus" + if IS_WAYLAND: + assert dialog._impl.native.is_visible(), "The dialog is not in focus" + else: + # Gtk.Dialog.get_transient_for() doesn't work on wayland and will crash. + assert ( + dialog._impl.native.get_transient_for() + == self.app._impl.native.get_active_window() + and dialog._impl.native.is_visible() + ), "The dialog is not in focus" def _menu_item(self, path): main_menu = self.app._impl.native.get_menubar() diff --git a/gtk/tests_backend/dialogs.py b/gtk/tests_backend/dialogs.py index eee1cc4bd2..90cbadafe2 100644 --- a/gtk/tests_backend/dialogs.py +++ b/gtk/tests_backend/dialogs.py @@ -1,3 +1,4 @@ +import asyncio from datetime import datetime, timedelta from pathlib import Path from unittest.mock import Mock @@ -31,13 +32,19 @@ def _setup_dialog_result( def automated_show(host_window, future): orig_show(host_window, future) - if pre_close_test_method: - pre_close_test_method() + async def _close_dialog(): + # Add a slight delay for the dialog to show up + await self.redraw("Running pre-close test", delay=0.05) - if close_handler: - close_handler(dialog, gtk_result) - else: - self._default_close_handler(dialog, gtk_result) + if pre_close_test_method: + pre_close_test_method() + + if close_handler: + close_handler(dialog, gtk_result) + else: + self._default_close_handler(dialog, gtk_result) + + asyncio.create_task(_close_dialog(), name="close-dialog") dialog._impl.show = automated_show From e43f923c1c18dbcc9c737d3828c674c2dc30cf32 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Sun, 10 Nov 2024 17:46:29 -0800 Subject: [PATCH 28/34] Correct behavior on winforms --- winforms/src/toga_winforms/app.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index b8cd54965a..8e970ea377 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -68,8 +68,8 @@ def __init__(self, interface): # boolean to allow us to avoid building a deep stack. self._cursor_visible = True - # The currently visible dialog. On winforms, all dialogs are modal by nature - # and the active focus can't be changed unless the modal dialog is dismissed. + # The currently visible dialog. On winforms, all dialogs are + # modal in nature. Only one dialog will be visible at a time. self._current_dialog = None self.loop = WinformsProactorEventLoop() @@ -252,24 +252,30 @@ def show_cursor(self): ###################################################################### def get_current_window(self): - # There can be only 1 dialog visible at a time, as all dialogs are modal - if self._current_dialog: + current_window = next( + ( + window._impl + for window in self.interface.windows + if WinForms.Form.ActiveForm == window._impl.native + ), + None, + ) + + if not current_window and self._current_dialog: + # There can be only 1 dialog visible at a time, as all dialogs are modal active_window_hwnd = windll.user32.GetForegroundWindow() # The window class name for dialog boxes is "#32770": # https://learn.microsoft.com/en-us/windows/win32/winauto/dialog-box current_dialog_hwnd = windll.user32.FindWindowW( "#32770", self._current_dialog.title ) - return ( + current_window = ( self._current_dialog.host_window_impl if active_window_hwnd == current_dialog_hwnd else None ) - else: - for window in self.interface.windows: - if WinForms.Form.ActiveForm == window._impl.native: - return window._impl - return None + + return current_window def set_current_window(self, window): window._impl.native.Activate() From 85ad15d488d32ca238c8292888badb07d2b70ee0 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 11 Nov 2024 09:24:32 -0800 Subject: [PATCH 29/34] Reimplemented across platforms --- android/src/toga_android/dialogs.py | 19 ++++++++++------- android/tests_backend/app.py | 4 +--- android/tests_backend/dialogs.py | 20 +++++++++++------- cocoa/src/toga_cocoa/app.py | 14 ++++++++----- cocoa/src/toga_cocoa/dialogs.py | 5 +++++ cocoa/src/toga_cocoa/libs/appkit.py | 1 - cocoa/tests_backend/app.py | 10 ++++----- cocoa/tests_backend/window.py | 21 ++++++++++++------- gtk/src/toga_gtk/app.py | 8 +------ gtk/src/toga_gtk/dialogs.py | 6 ------ gtk/tests_backend/app.py | 12 ++--------- gtk/tests_backend/dialogs.py | 24 +++++++++++++-------- iOS/tests_backend/dialogs.py | 25 ++++++++++++++-------- testbed/tests/app/test_desktop.py | 30 +++++++++++++++++---------- winforms/src/toga_winforms/app.py | 25 +++++++--------------- winforms/src/toga_winforms/dialogs.py | 13 ++++++------ winforms/tests_backend/dialogs.py | 22 ++++++++++---------- 17 files changed, 134 insertions(+), 125 deletions(-) diff --git a/android/src/toga_android/dialogs.py b/android/src/toga_android/dialogs.py index e6d430214c..9aa610f4d3 100644 --- a/android/src/toga_android/dialogs.py +++ b/android/src/toga_android/dialogs.py @@ -22,7 +22,7 @@ def show(self, host_window, future): if self.native: # Show the dialog. Don't differentiate between app and window modal dialogs. - self.native_alert_dialog = self.native.show() + self.native.show() else: # Dialog doesn't have an implementation. This can't be covered, as # the testbed shortcuts the test before showing the dialog. @@ -40,14 +40,16 @@ def __init__( ): super().__init__() - self.native = AlertDialog.Builder(toga.App.app.current_window._impl.app.native) - self.native.setCancelable(False) - self.native.setTitle(title) - self.native.setMessage(message) + self.native_builder = AlertDialog.Builder( + toga.App.app.current_window._impl.app.native + ) + self.native_builder.setCancelable(False) + self.native_builder.setTitle(title) + self.native_builder.setMessage(message) if icon is not None: - self.native.setIcon(icon) + self.native_builder.setIcon(icon) - self.native.setPositiveButton( + self.native_builder.setPositiveButton( positive_text, OnClickListener( self.completion_handler, @@ -55,9 +57,10 @@ def __init__( ), ) if negative_text is not None: - self.native.setNegativeButton( + self.native_builder.setNegativeButton( negative_text, OnClickListener(self.completion_handler, False) ) + self.native = self.native_builder.create() def completion_handler(self, return_value: bool) -> None: self.future.set_result(return_value) diff --git a/android/tests_backend/app.py b/android/tests_backend/app.py index aa6d0fed08..9c6d6ad2f9 100644 --- a/android/tests_backend/app.py +++ b/android/tests_backend/app.py @@ -43,9 +43,7 @@ def assert_app_icon(self, icon): pytest.xfail("Android apps don't have app icons at runtime") def assert_dialog_in_focus(self, dialog): - assert ( - dialog._impl.native_alert_dialog.isShowing() is True - ), "The dialog is not in focus" + assert dialog._impl.native.isShowing() is True, "The dialog is not in focus" def _menu_item(self, path): menu = self.main_window_probe._native_menu() diff --git a/android/tests_backend/dialogs.py b/android/tests_backend/dialogs.py index d0d83ea52b..6c89d8b4a6 100644 --- a/android/tests_backend/dialogs.py +++ b/android/tests_backend/dialogs.py @@ -17,17 +17,21 @@ def automated_show(host_window, future): async def _close_dialog(): # Inject a small pause without blocking the event loop await asyncio.sleep(1.0 if self.app.run_slow else 0.2) + try: if pre_close_test_method: pre_close_test_method() - - dialog_view = self.get_dialog_view() - self.assert_dialog_buttons(dialog_view, buttons) - await self.press_dialog_button(dialog_view, buttons[selected_index]) - except Exception as e: - # An error occurred closing the dialog; that means the dialog - # isn't what as expected, so record that in the future. - future.set_exception(e) + finally: + try: + dialog_view = self.get_dialog_view() + self.assert_dialog_buttons(dialog_view, buttons) + await self.press_dialog_button( + dialog_view, buttons[selected_index] + ) + except Exception as e: + # An error occurred closing the dialog; that means the dialog + # isn't what as expected, so record that in the future. + future.set_exception(e) asyncio.create_task(_close_dialog(), name="close-dialog") diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index e30d2195d2..38e96e83bf 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -30,8 +30,8 @@ NSMenu, NSMenuItem, NSNumber, - NSPanel, NSScreen, + NSWindow, ) from .screens import Screen as ScreenImpl @@ -110,6 +110,7 @@ def __init__(self, interface): # Create the lookup table for commands and menu items self._menu_items = {} + self._active_window_before_dialog = None # Populate the main window as soon as the event loop is running. self.loop.call_soon_threadsafe(self.interface._startup) @@ -368,10 +369,13 @@ def show_cursor(self): def get_current_window(self): focused_window = self.native.keyWindow - if isinstance(focused_window, NSPanel): # If the focus is on a dialog - sheet_parent = focused_window.sheetParent - return sheet_parent if sheet_parent else None - else: # If the focus is on a window + if not isinstance(focused_window, NSWindow): + return ( + self._active_window_before_dialog._impl.native + if self._active_window_before_dialog + else None + ) + else: return focused_window def set_current_window(self, window): diff --git a/cocoa/src/toga_cocoa/dialogs.py b/cocoa/src/toga_cocoa/dialogs.py index 969ec60ac2..d8226128aa 100644 --- a/cocoa/src/toga_cocoa/dialogs.py +++ b/cocoa/src/toga_cocoa/dialogs.py @@ -23,6 +23,7 @@ class BaseDialog: def show(self, host_window, future): self.future = future + toga.App.app._impl._active_window_before_dialog = host_window if host_window: # Begin the panel window-modal. @@ -61,9 +62,11 @@ def build_dialog(self): def completion_handler(self, return_value: int) -> None: self.future.set_result(None) + toga.App.app._impl._active_window_before_dialog = None def bool_completion_handler(self, return_value: int) -> None: self.future.set_result(return_value == NSAlertFirstButtonReturn) + toga.App.app._impl._active_window_before_dialog = None def _poll_modal_session(self, nsapp, session): # This is factored out so it can be mocked for testing purposes. @@ -230,6 +233,7 @@ def single_path_completion_handler(self, return_value: int) -> None: result = None self.future.set_result(result) + toga.App.app._impl._active_window_before_dialog = None def multi_path_completion_handler(self, return_value: int) -> None: if return_value == NSModalResponseOK: @@ -238,6 +242,7 @@ def multi_path_completion_handler(self, return_value: int) -> None: result = None self.future.set_result(result) + toga.App.app._impl._active_window_before_dialog = None def run_app_modal(self): self.native.beginWithCompletionHandler(self.completion_handler) diff --git a/cocoa/src/toga_cocoa/libs/appkit.py b/cocoa/src/toga_cocoa/libs/appkit.py index 15cb62662b..2e656e8b45 100644 --- a/cocoa/src/toga_cocoa/libs/appkit.py +++ b/cocoa/src/toga_cocoa/libs/appkit.py @@ -545,7 +545,6 @@ class NSLineBreakMode(Enum): ###################################################################### # NSPanel.h -NSPanel = ObjCClass("NSPanel") NSUtilityWindowMask = 1 << 4 diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index d20c28a694..d929b40e80 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -263,11 +263,11 @@ def auto_poll_modal_session(nsapp, session): if count < 5: count += 1 return _poll_modal_session(nsapp, session) - - if pre_close_test_method: - pre_close_test_method() - - return result + try: + if pre_close_test_method: + pre_close_test_method() + finally: + return result dialog._impl._poll_modal_session = auto_poll_modal_session diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index dfdd165f8d..019435eaf5 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -98,14 +98,19 @@ def _setup_alert_dialog_result(self, dialog, result, pre_close_test_method=None) def automated_show(host_window, future): orig_show(host_window, future) - - if pre_close_test_method: - pre_close_test_method() - - dialog._impl.host_window.endSheet( - dialog._impl.host_window.attachedSheet, - returnCode=result, - ) + try: + if pre_close_test_method: + pre_close_test_method() + finally: + try: + dialog._impl.host_window.endSheet( + dialog._impl.host_window.attachedSheet, + returnCode=result, + ) + except Exception as e: + # An error occurred closing the dialog; that means the dialog + # isn't what as expected, so record that in the future. + future.set_exception(e) dialog._impl.show = automated_show diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 386ca547a6..467614e6d8 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -45,8 +45,6 @@ def __init__(self, interface): self.actions = None - self._app_modal_dialog_shown = False - def gtk_activate(self, data=None): pass @@ -238,11 +236,7 @@ def show_cursor(self): def get_current_window(self): # pragma: no-cover-if-linux-wayland current_window = self.native.get_active_window()._impl - return ( - current_window - if current_window.interface.visible and not self._app_modal_dialog_shown - else None - ) + return current_window if current_window.interface.visible else None def set_current_window(self, window): window._impl.native.present() diff --git a/gtk/src/toga_gtk/dialogs.py b/gtk/src/toga_gtk/dialogs.py index 25510d1fdf..968c1d2a38 100644 --- a/gtk/src/toga_gtk/dialogs.py +++ b/gtk/src/toga_gtk/dialogs.py @@ -1,7 +1,5 @@ from pathlib import Path -from toga import App - from .libs import Gtk @@ -12,10 +10,8 @@ def show(self, host_window, future): # If this is a modal dialog, set the window as transient to the host window. if host_window: self.native.set_transient_for(host_window._impl.native) - host_window._impl.native.present() else: self.native.set_transient_for(None) - App.app._impl._app_modal_dialog_shown = True # Show the dialog. self.native.show() @@ -56,7 +52,6 @@ def gtk_response(self, dialog, response): self.future.set_result(result) self.native.destroy() - App.app._impl._app_modal_dialog_shown = False class InfoDialog(MessageDialog): @@ -214,7 +209,6 @@ def gtk_response(self, dialog, response): self.future.set_result(result) self.native.destroy() - App.app._impl._app_modal_dialog_shown = False class SaveFileDialog(FileDialog): diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index 1f9bf72064..dc23baf8a1 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -75,16 +75,8 @@ def assert_app_icon(self, icon): def assert_dialog_in_focus(self, dialog): # Gtk.Dialog's methods - is_active(), has_focus() both return False, even - # when the dialog is in focus. Hence,they cannot be used to determine focus. - if IS_WAYLAND: - assert dialog._impl.native.is_visible(), "The dialog is not in focus" - else: - # Gtk.Dialog.get_transient_for() doesn't work on wayland and will crash. - assert ( - dialog._impl.native.get_transient_for() - == self.app._impl.native.get_active_window() - and dialog._impl.native.is_visible() - ), "The dialog is not in focus" + # when the dialog is in focus. Hence, they cannot be used to determine focus. + assert dialog._impl.native.is_visible(), "The dialog is not in focus" def _menu_item(self, path): main_menu = self.app._impl.native.get_menubar() diff --git a/gtk/tests_backend/dialogs.py b/gtk/tests_backend/dialogs.py index 90cbadafe2..ccd78e557d 100644 --- a/gtk/tests_backend/dialogs.py +++ b/gtk/tests_backend/dialogs.py @@ -34,15 +34,21 @@ def automated_show(host_window, future): async def _close_dialog(): # Add a slight delay for the dialog to show up - await self.redraw("Running pre-close test", delay=0.05) - - if pre_close_test_method: - pre_close_test_method() - - if close_handler: - close_handler(dialog, gtk_result) - else: - self._default_close_handler(dialog, gtk_result) + await asyncio.sleep(0.05) + try: + if pre_close_test_method: + pre_close_test_method() + finally: + # Attempt to close the dialog regardless of any previous exceptions + try: + if close_handler: + close_handler(dialog, gtk_result) + else: + self._default_close_handler(dialog, gtk_result) + except Exception as e: + # An error occurred closing the dialog; that means the dialog + # isn't what as expected, so record that in the future. + future.set_exception(e) asyncio.create_task(_close_dialog(), name="close-dialog") diff --git a/iOS/tests_backend/dialogs.py b/iOS/tests_backend/dialogs.py index e4e7f9f0ae..676ab6c783 100644 --- a/iOS/tests_backend/dialogs.py +++ b/iOS/tests_backend/dialogs.py @@ -20,15 +20,22 @@ def automated_show(host_window, future): NSRunLoop.currentRunLoop.runUntilDate( NSDate.dateWithTimeIntervalSinceNow(1.0 if self.app.run_slow else 0.2) ) - - if pre_close_test_method: - pre_close_test_method() - - # Close the dialog and trigger the completion handler - self.dialog_view_controller.dismissViewControllerAnimated( - False, completion=None - ) - dialog._impl.native.actions[action_index].handler(dialog._impl.native) + try: + if pre_close_test_method: + pre_close_test_method() + finally: + try: + # Close the dialog and trigger the completion handler + self.dialog_view_controller.dismissViewControllerAnimated( + False, completion=None + ) + dialog._impl.native.actions[action_index].handler( + dialog._impl.native + ) + except Exception as e: + # An error occurred closing the dialog; that means the dialog + # isn't what as expected, so record that in the future. + future.set_exception(e) dialog._impl.show = automated_show diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index 56e709e0da..2f1173a34f 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -386,25 +386,33 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): if app_probe.supports_current_window_assignment: assert app.current_window == window3 - # When a dialog is in focus, `app.current_window` should return the window from - # which the dialog was initiated. - info_dialog = toga.InfoDialog("Info", "Some info") - + # When a dialog is in focus, app.current_window should return the + # previously active window(window3). def test_current_window_in_presence_of_dialog(): - # The public API should report that current window is the specified window + # The public API should report that the previous window is the current window if app_probe.supports_current_window_assignment: - assert app.current_window == window1 + assert app.current_window == window3 - # But, the backend should be reporting that the current window is the dialog - app_probe.assert_dialog_in_focus(info_dialog) + # But, the backend should be reporting that the dialog is in focus + app_probe.assert_dialog_in_focus(window_modal_info_dialog) + # Test in presence of window modal dialog + window_modal_info_dialog = toga.InfoDialog("Window Modal Dialog Info", "Some info") main_window_probe.setup_info_dialog_result( - info_dialog, + window_modal_info_dialog, pre_close_test_method=test_current_window_in_presence_of_dialog, ) - await main_window_probe.wait_for_window("Display window1 modal info dialog") - await window1.dialog(info_dialog) + await window1.dialog(window_modal_info_dialog) + + # Test in presence of app modal dialog + app_modal_info_dialog = toga.InfoDialog("App Modal Dialog Info", "Some info") + main_window_probe.setup_info_dialog_result( + app_modal_info_dialog, + pre_close_test_method=test_current_window_in_presence_of_dialog, + ) + await main_window_probe.wait_for_window("Display app modal info dialog") + await app.dialog(app_modal_info_dialog) async def test_session_based_app( diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 8e970ea377..5d0942b3d6 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -68,9 +68,7 @@ def __init__(self, interface): # boolean to allow us to avoid building a deep stack. self._cursor_visible = True - # The currently visible dialog. On winforms, all dialogs are - # modal in nature. Only one dialog will be visible at a time. - self._current_dialog = None + self._active_window_before_dialog = None self.loop = WinformsProactorEventLoop() asyncio.set_event_loop(self.loop) @@ -252,7 +250,7 @@ def show_cursor(self): ###################################################################### def get_current_window(self): - current_window = next( + current_window_impl = next( ( window._impl for window in self.interface.windows @@ -261,21 +259,14 @@ def get_current_window(self): None, ) - if not current_window and self._current_dialog: - # There can be only 1 dialog visible at a time, as all dialogs are modal - active_window_hwnd = windll.user32.GetForegroundWindow() - # The window class name for dialog boxes is "#32770": - # https://learn.microsoft.com/en-us/windows/win32/winauto/dialog-box - current_dialog_hwnd = windll.user32.FindWindowW( - "#32770", self._current_dialog.title - ) - current_window = ( - self._current_dialog.host_window_impl - if active_window_hwnd == current_dialog_hwnd + if not current_window_impl: + return ( + self._active_window_before_dialog._impl + if self._active_window_before_dialog else None ) - - return current_window + else: + return current_window_impl def set_current_window(self, window): window._impl.native.Activate() diff --git a/winforms/src/toga_winforms/dialogs.py b/winforms/src/toga_winforms/dialogs.py index 8add609d9f..0b01825956 100644 --- a/winforms/src/toga_winforms/dialogs.py +++ b/winforms/src/toga_winforms/dialogs.py @@ -20,8 +20,7 @@ class BaseDialog: def show(self, host_window, future): self.future = future - self.host_window_impl = getattr(host_window, "_impl", None) - App.app._impl._current_dialog = self + App.app._impl._active_window_before_dialog = App.app.current_window # Don't differentiate between app and window modal dialogs # Show the dialog using an inner loop. asyncio.get_event_loop().start_inner_loop(self._show) @@ -54,7 +53,7 @@ def _show(self): self.future.set_result(return_value == self.success_result) else: self.future.set_result(None) - App.app._impl._current_dialog = None + App.app._impl._active_window_before_dialog = None class InfoDialog(MessageDialog): @@ -183,17 +182,17 @@ def winforms_FormClosing(self, sender, event): def winforms_Click_quit(self, sender, event): self.future.set_result(False) self.native.Close() - App.app._impl._current_dialog = None + App.app._impl._active_window_before_dialog = None def winforms_Click_retry(self, sender, event): self.future.set_result(True) self.native.Close() - App.app._impl._current_dialog = None + App.app._impl._active_window_before_dialog = None def winforms_Click_accept(self, sender, event): self.future.set_result(None) self.native.Close() - App.app._impl._current_dialog = None + App.app._impl._active_window_before_dialog = None class FileDialog(BaseDialog): @@ -234,7 +233,7 @@ def _show(self): self.future.set_result(self._get_filenames()) else: self.future.set_result(None) - App.app._impl._current_dialog = None + App.app._impl._active_window_before_dialog = None def _set_title(self, title): self.native.Title = title diff --git a/winforms/tests_backend/dialogs.py b/winforms/tests_backend/dialogs.py index 29cf2a1243..323c6d206e 100644 --- a/winforms/tests_backend/dialogs.py +++ b/winforms/tests_backend/dialogs.py @@ -16,20 +16,20 @@ def automated_show(host_window, future): orig_show(host_window, future) async def _close_dialog(): - try: - # Give the inner event loop a chance to start. The MessageBox dialogs work with - # sleep(0), but the file dialogs require it to be positive for some reason. - await asyncio.sleep(0.001) + # Give the inner event loop a chance to start. The MessageBox dialogs work with + # sleep(0), but the file dialogs require it to be positive for some reason. + await asyncio.sleep(0.001) + try: if pre_close_test_method: pre_close_test_method() - - await self.type_character(char, alt=alt) - - except Exception as e: - # An error occurred closing the dialog; that means the dialog - # isn't what as expected, so record that in the future. - future.set_exception(e) + finally: + try: + await self.type_character(char, alt=alt) + except Exception as e: + # An error occurred closing the dialog; that means the dialog + # isn't what as expected, so record that in the future. + future.set_exception(e) asyncio.create_task(_close_dialog(), name="close-dialog") From a1cd1564edc286e37441e0d6ec8ca3224425d068 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 11 Nov 2024 09:48:43 -0800 Subject: [PATCH 30/34] Minor fix --- cocoa/src/toga_cocoa/libs/appkit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cocoa/src/toga_cocoa/libs/appkit.py b/cocoa/src/toga_cocoa/libs/appkit.py index 2e656e8b45..15cb62662b 100644 --- a/cocoa/src/toga_cocoa/libs/appkit.py +++ b/cocoa/src/toga_cocoa/libs/appkit.py @@ -545,6 +545,7 @@ class NSLineBreakMode(Enum): ###################################################################### # NSPanel.h +NSPanel = ObjCClass("NSPanel") NSUtilityWindowMask = 1 << 4 From 4c81695778b520d2a201ba5288f7a64fbca5a2ce Mon Sep 17 00:00:00 2001 From: proneon267 Date: Mon, 11 Nov 2024 19:49:51 -0800 Subject: [PATCH 31/34] Correct test setup on cocoa --- cocoa/src/toga_cocoa/app.py | 12 +++--------- cocoa/src/toga_cocoa/dialogs.py | 2 +- testbed/tests/app/test_desktop.py | 24 +++++++++++++++++------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 38e96e83bf..404a3c48f8 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -31,7 +31,6 @@ NSMenuItem, NSNumber, NSScreen, - NSWindow, ) from .screens import Screen as ScreenImpl @@ -368,15 +367,10 @@ def show_cursor(self): ###################################################################### def get_current_window(self): - focused_window = self.native.keyWindow - if not isinstance(focused_window, NSWindow): - return ( - self._active_window_before_dialog._impl.native - if self._active_window_before_dialog - else None - ) + if self._active_window_before_dialog: + return self._active_window_before_dialog._impl.native else: - return focused_window + return self.native.keyWindow def set_current_window(self, window): window._impl.native.makeKeyAndOrderFront(window._impl.native) diff --git a/cocoa/src/toga_cocoa/dialogs.py b/cocoa/src/toga_cocoa/dialogs.py index d8226128aa..17d5283168 100644 --- a/cocoa/src/toga_cocoa/dialogs.py +++ b/cocoa/src/toga_cocoa/dialogs.py @@ -23,7 +23,7 @@ class BaseDialog: def show(self, host_window, future): self.future = future - toga.App.app._impl._active_window_before_dialog = host_window + toga.App.app._impl._active_window_before_dialog = toga.App.app.current_window if host_window: # Begin the panel window-modal. diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index bc480ee175..f7b99df9a8 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -389,29 +389,39 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): assert app.current_window == window3 # When a dialog is in focus, app.current_window should return the - # previously active window(window3). - def test_current_window_in_presence_of_dialog(): + # previously active window. + def test_current_window_in_presence_of_dialog(dialog, previous_active_window): # The public API should report that the previous window is the current window if app_probe.supports_current_window_assignment: - assert app.current_window == window3 + assert app.current_window == previous_active_window # But, the backend should be reporting that the dialog is in focus - app_probe.assert_dialog_in_focus(window_modal_info_dialog) + app_probe.assert_dialog_in_focus(dialog) # Test in presence of window modal dialog + previous_active_window = app.current_window window_modal_info_dialog = toga.InfoDialog("Window Modal Dialog Info", "Some info") main_window_probe.setup_info_dialog_result( window_modal_info_dialog, - pre_close_test_method=test_current_window_in_presence_of_dialog, + pre_close_test_method=partial( + test_current_window_in_presence_of_dialog, + window_modal_info_dialog, + previous_active_window, + ), ) await main_window_probe.wait_for_window("Display window1 modal info dialog") await window1.dialog(window_modal_info_dialog) # Test in presence of app modal dialog + previous_active_window = app.current_window app_modal_info_dialog = toga.InfoDialog("App Modal Dialog Info", "Some info") - main_window_probe.setup_info_dialog_result( + app_probe.setup_info_dialog_result( app_modal_info_dialog, - pre_close_test_method=test_current_window_in_presence_of_dialog, + pre_close_test_method=partial( + test_current_window_in_presence_of_dialog, + app_modal_info_dialog, + previous_active_window, + ), ) await main_window_probe.wait_for_window("Display app modal info dialog") await app.dialog(app_modal_info_dialog) From 378b5ce06a420ba12ba7510b1e31fe3aa541c950 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 12 Nov 2024 08:05:08 -0800 Subject: [PATCH 32/34] Modified mobile test setup --- testbed/tests/app/test_mobile.py | 44 ++++++++++++++++++++++--------- winforms/src/toga_winforms/app.py | 26 +++++++----------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index c30ac53846..3d043bbd44 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -1,3 +1,5 @@ +from functools import partial + import pytest import toga @@ -52,24 +54,40 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): await main_window_probe.wait_for_window("Main window is still current") assert app.current_window == main_window - # When a dialog is in focus, `app.current_window` should return the window from - # which the dialog was initiated. - info_dialog = toga.InfoDialog("Info", "Some info") - - def test_current_window_in_presence_of_dialog(): - # The public API should report that current window is the specified window - assert app.current_window == main_window + # When a dialog is in focus, app.current_window should return the + # previously active window(main_window on mobile platforms). + def test_current_window_in_presence_of_dialog(dialog, previous_active_window): + # The public API should report that the previous window is the current window + assert app.current_window == previous_active_window - # But, the backend should be reporting that the focus is on the dialog - app_probe.assert_dialog_in_focus(info_dialog) + # But, the backend should be reporting that the dialog is in focus + app_probe.assert_dialog_in_focus(dialog) + # Test in presence of window modal dialog + window_modal_info_dialog = toga.InfoDialog("Window Modal Dialog Info", "Some info") main_window_probe.setup_info_dialog_result( - info_dialog, - pre_close_test_method=test_current_window_in_presence_of_dialog, + window_modal_info_dialog, + pre_close_test_method=partial( + test_current_window_in_presence_of_dialog, + window_modal_info_dialog, + main_window, + ), ) - await main_window_probe.wait_for_window("Display window1 modal info dialog") - await main_window.dialog(info_dialog) + await main_window.dialog(window_modal_info_dialog) + + # Test in presence of app modal dialog + app_modal_info_dialog = toga.InfoDialog("App Modal Dialog Info", "Some info") + app_probe.setup_info_dialog_result( + app_modal_info_dialog, + pre_close_test_method=partial( + test_current_window_in_presence_of_dialog, + app_modal_info_dialog, + main_window, + ), + ) + await main_window_probe.wait_for_window("Display app modal info dialog") + await app.dialog(app_modal_info_dialog) async def test_app_lifecycle(app, app_probe): diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 98c88fb3a4..80f302b168 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -251,23 +251,17 @@ def show_cursor(self): ###################################################################### def get_current_window(self): - current_window_impl = next( - ( - window._impl - for window in self.interface.windows - if WinForms.Form.ActiveForm == window._impl.native - ), - None, - ) - - if not current_window_impl: - return ( - self._active_window_before_dialog._impl - if self._active_window_before_dialog - else None - ) + if self._active_window_before_dialog: + return self._active_window_before_dialog._impl else: - return current_window_impl + return next( + ( + window._impl + for window in self.interface.windows + if WinForms.Form.ActiveForm == window._impl.native + ), + None, + ) def set_current_window(self, window): window._impl.native.Activate() From f4d6c64766169cbc292701f9701997eb07b9063b Mon Sep 17 00:00:00 2001 From: proneon267 Date: Tue, 12 Nov 2024 05:13:45 -0800 Subject: [PATCH 33/34] Modified changenote --- changes/2926.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/2926.bugfix.rst b/changes/2926.bugfix.rst index 5e9ee6393b..4cf1f2e5c9 100644 --- a/changes/2926.bugfix.rst +++ b/changes/2926.bugfix.rst @@ -1 +1 @@ -On desktop backends, when a dialog is in focus, `App.current_window` now returns the window from which the dialog was initiated. +On desktop backends, when a dialog is in focus, `App.current_window` now returns the last active window. From f10bdc80b4aa4398740a9eb9de7c15a64ba9b403 Mon Sep 17 00:00:00 2001 From: proneon267 Date: Wed, 13 Nov 2024 22:55:52 -0800 Subject: [PATCH 34/34] Fix implementation on macOS --- changes/2926.bugfix.rst | 2 +- cocoa/src/toga_cocoa/app.py | 9 +++++---- cocoa/src/toga_cocoa/dialogs.py | 5 ----- cocoa/tests_backend/app.py | 2 +- cocoa/tests_backend/dialogs.py | 4 +++- cocoa/tests_backend/window.py | 2 +- gtk/tests_backend/dialogs.py | 3 +-- iOS/tests_backend/dialogs.py | 2 +- testbed/tests/app/test_desktop.py | 24 ++++++------------------ testbed/tests/app/test_mobile.py | 22 ++++++---------------- winforms/src/toga_winforms/app.py | 17 ++++------------- winforms/src/toga_winforms/dialogs.py | 8 -------- winforms/tests_backend/dialogs.py | 2 +- 13 files changed, 30 insertions(+), 72 deletions(-) diff --git a/changes/2926.bugfix.rst b/changes/2926.bugfix.rst index 4cf1f2e5c9..b0f31b3ec6 100644 --- a/changes/2926.bugfix.rst +++ b/changes/2926.bugfix.rst @@ -1 +1 @@ -On desktop backends, when a dialog is in focus, `App.current_window` now returns the last active window. +On macOS, when a dialog is in focus, `App.current_window` now returns the host window, instead of raising an `AttributeError`. diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 404a3c48f8..d2cff2172f 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -30,6 +30,7 @@ NSMenu, NSMenuItem, NSNumber, + NSPanel, NSScreen, ) from .screens import Screen as ScreenImpl @@ -109,7 +110,6 @@ def __init__(self, interface): # Create the lookup table for commands and menu items self._menu_items = {} - self._active_window_before_dialog = None # Populate the main window as soon as the event loop is running. self.loop.call_soon_threadsafe(self.interface._startup) @@ -367,10 +367,11 @@ def show_cursor(self): ###################################################################### def get_current_window(self): - if self._active_window_before_dialog: - return self._active_window_before_dialog._impl.native + key_window = self.native.keyWindow + if isinstance(key_window, NSPanel): + return key_window.sheetParent else: - return self.native.keyWindow + return key_window def set_current_window(self, window): window._impl.native.makeKeyAndOrderFront(window._impl.native) diff --git a/cocoa/src/toga_cocoa/dialogs.py b/cocoa/src/toga_cocoa/dialogs.py index 17d5283168..969ec60ac2 100644 --- a/cocoa/src/toga_cocoa/dialogs.py +++ b/cocoa/src/toga_cocoa/dialogs.py @@ -23,7 +23,6 @@ class BaseDialog: def show(self, host_window, future): self.future = future - toga.App.app._impl._active_window_before_dialog = toga.App.app.current_window if host_window: # Begin the panel window-modal. @@ -62,11 +61,9 @@ def build_dialog(self): def completion_handler(self, return_value: int) -> None: self.future.set_result(None) - toga.App.app._impl._active_window_before_dialog = None def bool_completion_handler(self, return_value: int) -> None: self.future.set_result(return_value == NSAlertFirstButtonReturn) - toga.App.app._impl._active_window_before_dialog = None def _poll_modal_session(self, nsapp, session): # This is factored out so it can be mocked for testing purposes. @@ -233,7 +230,6 @@ def single_path_completion_handler(self, return_value: int) -> None: result = None self.future.set_result(result) - toga.App.app._impl._active_window_before_dialog = None def multi_path_completion_handler(self, return_value: int) -> None: if return_value == NSModalResponseOK: @@ -242,7 +238,6 @@ def multi_path_completion_handler(self, return_value: int) -> None: result = None self.future.set_result(result) - toga.App.app._impl._active_window_before_dialog = None def run_app_modal(self): self.native.beginWithCompletionHandler(self.completion_handler) diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index d929b40e80..60f0693929 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -265,7 +265,7 @@ def auto_poll_modal_session(nsapp, session): return _poll_modal_session(nsapp, session) try: if pre_close_test_method: - pre_close_test_method() + pre_close_test_method(dialog) finally: return result diff --git a/cocoa/tests_backend/dialogs.py b/cocoa/tests_backend/dialogs.py index fabb1cc450..6b7abeb5d9 100644 --- a/cocoa/tests_backend/dialogs.py +++ b/cocoa/tests_backend/dialogs.py @@ -18,7 +18,9 @@ class DialogsMixin: def setup_info_dialog_result(self, dialog, pre_close_test_method=None): self._setup_alert_dialog_result( - dialog, NSAlertFirstButtonReturn, pre_close_test_method + dialog, + NSAlertFirstButtonReturn, + pre_close_test_method=pre_close_test_method, ) def setup_question_dialog_result(self, dialog, result): diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 019435eaf5..6ef29b7c49 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -100,7 +100,7 @@ def automated_show(host_window, future): orig_show(host_window, future) try: if pre_close_test_method: - pre_close_test_method() + pre_close_test_method(dialog) finally: try: dialog._impl.host_window.endSheet( diff --git a/gtk/tests_backend/dialogs.py b/gtk/tests_backend/dialogs.py index ccd78e557d..cf0c7df61f 100644 --- a/gtk/tests_backend/dialogs.py +++ b/gtk/tests_backend/dialogs.py @@ -37,9 +37,8 @@ async def _close_dialog(): await asyncio.sleep(0.05) try: if pre_close_test_method: - pre_close_test_method() + pre_close_test_method(dialog) finally: - # Attempt to close the dialog regardless of any previous exceptions try: if close_handler: close_handler(dialog, gtk_result) diff --git a/iOS/tests_backend/dialogs.py b/iOS/tests_backend/dialogs.py index 676ab6c783..f28b7dc608 100644 --- a/iOS/tests_backend/dialogs.py +++ b/iOS/tests_backend/dialogs.py @@ -22,7 +22,7 @@ def automated_show(host_window, future): ) try: if pre_close_test_method: - pre_close_test_method() + pre_close_test_method(dialog) finally: try: # Close the dialog and trigger the completion handler diff --git a/testbed/tests/app/test_desktop.py b/testbed/tests/app/test_desktop.py index f7b99df9a8..a8f9bc1869 100644 --- a/testbed/tests/app/test_desktop.py +++ b/testbed/tests/app/test_desktop.py @@ -390,38 +390,26 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): # When a dialog is in focus, app.current_window should return the # previously active window. - def test_current_window_in_presence_of_dialog(dialog, previous_active_window): - # The public API should report that the previous window is the current window - if app_probe.supports_current_window_assignment: - assert app.current_window == previous_active_window - - # But, the backend should be reporting that the dialog is in focus + def test_current_window_in_presence_of_dialog(dialog): app_probe.assert_dialog_in_focus(dialog) + # Accessing current_window in presence of dialog shouldn't raise any exceptions. + _ = app.current_window + # Test in presence of window modal dialog - previous_active_window = app.current_window window_modal_info_dialog = toga.InfoDialog("Window Modal Dialog Info", "Some info") main_window_probe.setup_info_dialog_result( window_modal_info_dialog, - pre_close_test_method=partial( - test_current_window_in_presence_of_dialog, - window_modal_info_dialog, - previous_active_window, - ), + pre_close_test_method=test_current_window_in_presence_of_dialog, ) await main_window_probe.wait_for_window("Display window1 modal info dialog") await window1.dialog(window_modal_info_dialog) # Test in presence of app modal dialog - previous_active_window = app.current_window app_modal_info_dialog = toga.InfoDialog("App Modal Dialog Info", "Some info") app_probe.setup_info_dialog_result( app_modal_info_dialog, - pre_close_test_method=partial( - test_current_window_in_presence_of_dialog, - app_modal_info_dialog, - previous_active_window, - ), + pre_close_test_method=test_current_window_in_presence_of_dialog, ) await main_window_probe.wait_for_window("Display app modal info dialog") await app.dialog(app_modal_info_dialog) diff --git a/testbed/tests/app/test_mobile.py b/testbed/tests/app/test_mobile.py index 3d043bbd44..c62c86a637 100644 --- a/testbed/tests/app/test_mobile.py +++ b/testbed/tests/app/test_mobile.py @@ -1,5 +1,3 @@ -from functools import partial - import pytest import toga @@ -56,22 +54,18 @@ async def test_current_window(app, app_probe, main_window, main_window_probe): # When a dialog is in focus, app.current_window should return the # previously active window(main_window on mobile platforms). - def test_current_window_in_presence_of_dialog(dialog, previous_active_window): - # The public API should report that the previous window is the current window - assert app.current_window == previous_active_window - + def test_current_window_in_presence_of_dialog(dialog): # But, the backend should be reporting that the dialog is in focus app_probe.assert_dialog_in_focus(dialog) + # Accessing current_window in presence of dialog shouldn't raise any exceptions. + _ = app.current_window + # Test in presence of window modal dialog window_modal_info_dialog = toga.InfoDialog("Window Modal Dialog Info", "Some info") main_window_probe.setup_info_dialog_result( window_modal_info_dialog, - pre_close_test_method=partial( - test_current_window_in_presence_of_dialog, - window_modal_info_dialog, - main_window, - ), + pre_close_test_method=test_current_window_in_presence_of_dialog, ) await main_window_probe.wait_for_window("Display window1 modal info dialog") await main_window.dialog(window_modal_info_dialog) @@ -80,11 +74,7 @@ def test_current_window_in_presence_of_dialog(dialog, previous_active_window): app_modal_info_dialog = toga.InfoDialog("App Modal Dialog Info", "Some info") app_probe.setup_info_dialog_result( app_modal_info_dialog, - pre_close_test_method=partial( - test_current_window_in_presence_of_dialog, - app_modal_info_dialog, - main_window, - ), + pre_close_test_method=test_current_window_in_presence_of_dialog, ) await main_window_probe.wait_for_window("Display app modal info dialog") await app.dialog(app_modal_info_dialog) diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 80f302b168..f01610667c 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -68,8 +68,6 @@ def __init__(self, interface): # boolean to allow us to avoid building a deep stack. self._cursor_visible = True - self._active_window_before_dialog = None - self.loop = WinformsProactorEventLoop() asyncio.set_event_loop(self.loop) @@ -251,17 +249,10 @@ def show_cursor(self): ###################################################################### def get_current_window(self): - if self._active_window_before_dialog: - return self._active_window_before_dialog._impl - else: - return next( - ( - window._impl - for window in self.interface.windows - if WinForms.Form.ActiveForm == window._impl.native - ), - None, - ) + for window in self.interface.windows: + if WinForms.Form.ActiveForm == window._impl.native: + return window._impl + return None def set_current_window(self, window): window._impl.native.Activate() diff --git a/winforms/src/toga_winforms/dialogs.py b/winforms/src/toga_winforms/dialogs.py index 10e96aa17f..6f090a0268 100644 --- a/winforms/src/toga_winforms/dialogs.py +++ b/winforms/src/toga_winforms/dialogs.py @@ -9,8 +9,6 @@ ) from System.Windows.Forms import DialogResult, MessageBoxButtons, MessageBoxIcon -from toga import App - from .libs.user32 import DPI_AWARENESS_CONTEXT_UNAWARE, SetThreadDpiAwarenessContext from .libs.wrapper import WeakrefCallable @@ -19,7 +17,6 @@ class BaseDialog: def show(self, host_window, future): self.future = future - App.app._impl._active_window_before_dialog = App.app.current_window # Don't differentiate between app and window modal dialogs # Show the dialog using an inner loop. asyncio.get_event_loop().start_inner_loop(self._show) @@ -52,7 +49,6 @@ def _show(self): self.future.set_result(return_value == self.success_result) else: self.future.set_result(None) - App.app._impl._active_window_before_dialog = None class InfoDialog(MessageDialog): @@ -210,17 +206,14 @@ def winforms_FormClosing(self, sender, event): def winforms_Click_quit(self, sender, event): self.future.set_result(False) self.native.Close() - App.app._impl._active_window_before_dialog = None def winforms_Click_retry(self, sender, event): self.future.set_result(True) self.native.Close() - App.app._impl._active_window_before_dialog = None def winforms_Click_accept(self, sender, event): self.future.set_result(None) self.native.Close() - App.app._impl._active_window_before_dialog = None class FileDialog(BaseDialog): @@ -261,7 +254,6 @@ def _show(self): self.future.set_result(self._get_filenames()) else: self.future.set_result(None) - App.app._impl._active_window_before_dialog = None def _set_title(self, title): self.native.Title = title diff --git a/winforms/tests_backend/dialogs.py b/winforms/tests_backend/dialogs.py index 323c6d206e..072be6ba2f 100644 --- a/winforms/tests_backend/dialogs.py +++ b/winforms/tests_backend/dialogs.py @@ -22,7 +22,7 @@ async def _close_dialog(): try: if pre_close_test_method: - pre_close_test_method() + pre_close_test_method(dialog) finally: try: await self.type_character(char, alt=alt)