From 91dd7c9f1141cfb1efdd00ef332da7cddff8bd61 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sun, 31 Dec 2023 13:13:24 -0500 Subject: [PATCH 1/6] Added tabbingIdentifier to Window in Cocoa --- cocoa/src/toga_cocoa/window.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 17b70dd16b..8e1be4754f 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -148,6 +148,9 @@ def __init__(self, interface, title, position, size): self.native.interface = self.interface self.native.impl = self + # This causes windows to only tab together with others of the same class. + self.native.tabbingIdentifier = str(self.__class__) + # Cocoa releases windows when they are closed; this causes havoc with # Toga's widget cleanup because the ObjC runtime thinks there's no # references to the object left. Add a reference that can be released @@ -163,7 +166,7 @@ def __init__(self, interface, title, position, size): self.container = Container(on_refresh=self.content_refreshed) self.native.contentView = self.container.native - # Ensure that the container renders it's background in the same color as the window. + # Ensure that the container renders its background in the same color as the window. self.native.wantsLayer = True self.container.native.backgroundColor = self.native.backgroundColor From 1a715fef58f35b630e0ceff07336494e3eeba560 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sun, 31 Dec 2023 13:38:30 -0500 Subject: [PATCH 2/6] Added changenote --- changes/2311.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2311.feature.rst diff --git a/changes/2311.feature.rst b/changes/2311.feature.rst new file mode 100644 index 0000000000..02addee77a --- /dev/null +++ b/changes/2311.feature.rst @@ -0,0 +1 @@ +On macOS, windows now only tab with others of the same class. From 3405a39586b881b0fbfddf852e3915aee34533be Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sun, 7 Jan 2024 18:42:02 -0500 Subject: [PATCH 3/6] Fixed tabbingIdentifier to use interface class name, not implementation; made tabbing test. --- android/tests_backend/app.py | 4 ++ android/tests_backend/window.py | 7 ++++ cocoa/src/toga_cocoa/window.py | 2 +- cocoa/tests_backend/app.py | 10 ++++- cocoa/tests_backend/window.py | 7 ++++ gtk/tests_backend/app.py | 4 ++ gtk/tests_backend/window.py | 9 +++++ iOS/tests_backend/app.py | 4 ++ iOS/tests_backend/window.py | 7 ++++ testbed/tests/test_window.py | 64 +++++++++++++++++++++++++++++++- winforms/tests_backend/app.py | 4 ++ winforms/tests_backend/window.py | 8 ++++ 12 files changed, 127 insertions(+), 3 deletions(-) diff --git a/android/tests_backend/app.py b/android/tests_backend/app.py index 76365fcc4d..765b608e1d 100644 --- a/android/tests_backend/app.py +++ b/android/tests_backend/app.py @@ -106,3 +106,7 @@ def rotate(self): self.native.findViewById( R.id.content ).getViewTreeObserver().dispatchOnGlobalLayout() + + @property + def tabbing_enabled(self): + xfail("Tabbed windows not implemented for this backend.") diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index bc283f43a3..e3842088b9 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -87,3 +87,10 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled): def press_toolbar_button(self, index): self.native.onOptionsItemSelected(self._toolbar_items()[index]) + + @property + def tabs(self): + pytest.xfail("Tabbed windows not implemented for this backend.") + + def merge_all_windows(self): + pytest.xfail("Tabbed windows not implemented for this backend.") diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 8e1be4754f..296b1fdda0 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -149,7 +149,7 @@ def __init__(self, interface, title, position, size): self.native.impl = self # This causes windows to only tab together with others of the same class. - self.native.tabbingIdentifier = str(self.__class__) + self.native.tabbingIdentifier = str(self.interface.__class__) # Cocoa releases windows when they are closed; this causes havoc with # Toga's widget cleanup because the ObjC runtime thinks there's no diff --git a/cocoa/tests_backend/app.py b/cocoa/tests_backend/app.py index 8c82b68a4f..a95f7c3191 100644 --- a/cocoa/tests_backend/app.py +++ b/cocoa/tests_backend/app.py @@ -24,7 +24,7 @@ def __init__(self, app): super().__init__() self.app = app # Prevents erroneous test fails from secondary windows opening as tabs - NSWindow.allowsAutomaticWindowTabbing = False + self.tabbing_enabled = False assert isinstance(self.app._impl.native, NSApplication) @property @@ -175,3 +175,11 @@ def keystroke(self, combination): keyCode=key_code, ) return toga_key(event) + + @property + def tabbing_enabled(self): + return NSWindow.allowsAutomaticWindowTabbing + + @tabbing_enabled.setter + def tabbing_enabled(self, value): + NSWindow.allowsAutomaticWindowTabbing = value diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 9eeba9ff29..09b311e824 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -261,3 +261,10 @@ def press_toolbar_button(self, index): restype=None, argtypes=[objc_id], ) + + @property + def tabs(self): + return self.native.tabbedWindows + + def merge_all_windows(self): + self.native.mergeAllWindows(self.native) diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index 10dcc058b6..d8f9da9a0d 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -154,3 +154,7 @@ def keystroke(self, combination): event.state = state return toga_key(event) + + @property + def tabbing_enabled(self): + pytest.xfail("Tabbed windows not implemented for this backend.") diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index a90b60f8ef..c4bd6b65a8 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -2,6 +2,8 @@ from pathlib import Path from unittest.mock import Mock +from pytest import xfail + from toga_gtk.libs import Gdk, Gtk from .probe import BaseProbe @@ -251,3 +253,10 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled): def press_toolbar_button(self, index): item = self.impl.native_toolbar.get_nth_item(index) item.emit("clicked") + + @property + def tabs(self): + xfail("Tabbed windows not implemented for this backend.") + + def merge_all_windows(self): + xfail("Tabbed windows not implemented for this backend.") diff --git a/iOS/tests_backend/app.py b/iOS/tests_backend/app.py index 98a4ba0369..2b6631703a 100644 --- a/iOS/tests_backend/app.py +++ b/iOS/tests_backend/app.py @@ -69,3 +69,7 @@ def terminate(self): def rotate(self): self.native = self.app._impl.native self.native.delegate.application(self.native, didChangeStatusBarOrientation=0) + + @property + def tabbing_enabled(self): + pytest.xfail("Tabbed windows not implemented for this backend.") diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index 08f9a34295..6805817f92 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -77,3 +77,10 @@ async def close_select_folder_dialog(self, dialog, result, multiple_select): def has_toolbar(self): pytest.skip("Toolbars not implemented on iOS") + + @property + def tabs(self): + pytest.xfail("Tabbed windows not implemented for this backend.") + + def merge_all_windows(self): + pytest.xfail("Tabbed windows not implemented for this backend.") diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 70067ce9c5..34ac827995 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -17,7 +17,7 @@ def window_probe(app, window): module = import_module("tests_backend.window") - return getattr(module, "WindowProbe")(app, window) + return module.WindowProbe(app, window) @pytest.fixture @@ -216,6 +216,68 @@ async def test_secondary_window_with_args(app, second_window, second_window_prob assert second_window not in app.windows + def probe_for_window_class(app, WindowClass): + window = WindowClass() + window.show() + probe = window_probe(app, window) + return probe + + async def test_window_tabbing(app, app_probe, main_window_probe): + """Windows tab only with others of the same class (only implemented on macOS)""" + + class WindowSubclass(toga.Window): + pass + + base_probes = [probe_for_window_class(app, toga.Window) for _ in range(2)] + subclass_probes = [ + probe_for_window_class(app, WindowSubclass) for _ in range(2) + ] + + try: + base_probe, _ = base_probes + subclass_probe, _ = subclass_probes + + app_probe.tabbing_enabled = True + + # Double check that nothing's tabbed initially. + assert not any( + [main_window_probe.tabs, base_probe.tabs, subclass_probe.tabs] + ) + + main_window_probe.merge_all_windows() + await main_window_probe.wait_for_window( + "Merge All Windows called on MainWindow" + ) + assert not any( + [main_window_probe.tabs, base_probe.tabs, subclass_probe.tabs] + ) + + base_probe.merge_all_windows() + + await main_window_probe.wait_for_window( + "Merge All Windows called on base Window" + ) + assert ( + not main_window_probe.tabs + and len(base_probe.tabs) == 2 + and not subclass_probe.tabs + ) + + subclass_probe.merge_all_windows() + await main_window_probe.wait_for_window( + "Merge All Windows called on Window subclass" + ) + assert ( + not main_window_probe.tabs + and len(base_probe.tabs) == 2 + and len(subclass_probe.tabs) == 2 + ) + + finally: + app_probe.tabbing_enabled = False + for probe in [*base_probes, *subclass_probes]: + probe.close() + async def test_secondary_window_cleanup(app_probe): """Memory for windows is cleaned up when windows are deleted.""" # Create and show a window with content. We can't use the second_window fixture diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index 4a2640e538..c4907c8c2e 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -154,3 +154,7 @@ def activate_menu_minimize(self): def keystroke(self, combination): return winforms_to_toga_key(toga_to_winforms_key(combination)) + + @property + def tabbing_enabled(self): + pytest.xfail("Tabbed windows not implemented for this backend.") diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 5fc6ae3c94..7c908208bd 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -1,6 +1,7 @@ import asyncio from unittest.mock import Mock +from pytest import xfail from System import EventArgs from System.Windows.Forms import ( Form, @@ -148,3 +149,10 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled): def press_toolbar_button(self, index): self._native_toolbar_item(index).OnClick(EventArgs.Empty) + + @property + def tabs(self): + xfail("Tabbed windows not implemented for this backend.") + + def merge_all_windows(self): + xfail("Tabbed windows not implemented for this backend.") From 400ace457528eb99f63f64bf23aefa2037037858 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sun, 7 Jan 2024 19:10:08 -0500 Subject: [PATCH 4/6] Fixed xfail tabbing mode setter in non-macOS backends --- android/tests_backend/app.py | 4 ++++ gtk/tests_backend/app.py | 4 ++++ iOS/tests_backend/app.py | 4 ++++ winforms/tests_backend/app.py | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/android/tests_backend/app.py b/android/tests_backend/app.py index 765b608e1d..b4a977ade1 100644 --- a/android/tests_backend/app.py +++ b/android/tests_backend/app.py @@ -110,3 +110,7 @@ def rotate(self): @property def tabbing_enabled(self): xfail("Tabbed windows not implemented for this backend.") + + @tabbing_enabled.setter + def tabbing_enabled(self, value): + xfail("Tabbed windows not implemented for this backend.") diff --git a/gtk/tests_backend/app.py b/gtk/tests_backend/app.py index d8f9da9a0d..cbdefa407d 100644 --- a/gtk/tests_backend/app.py +++ b/gtk/tests_backend/app.py @@ -158,3 +158,7 @@ def keystroke(self, combination): @property def tabbing_enabled(self): pytest.xfail("Tabbed windows not implemented for this backend.") + + @tabbing_enabled.setter + def tabbing_enabled(self, value): + pytest.xfail("Tabbed windows not implemented for this backend.") diff --git a/iOS/tests_backend/app.py b/iOS/tests_backend/app.py index 2b6631703a..9eaa063bc9 100644 --- a/iOS/tests_backend/app.py +++ b/iOS/tests_backend/app.py @@ -73,3 +73,7 @@ def rotate(self): @property def tabbing_enabled(self): pytest.xfail("Tabbed windows not implemented for this backend.") + + @tabbing_enabled.setter + def tabbing_enabled(self, value): + pytest.xfail("Tabbed windows not implemented for this backend.") diff --git a/winforms/tests_backend/app.py b/winforms/tests_backend/app.py index c4907c8c2e..d0f5eafdd4 100644 --- a/winforms/tests_backend/app.py +++ b/winforms/tests_backend/app.py @@ -158,3 +158,7 @@ def keystroke(self, combination): @property def tabbing_enabled(self): pytest.xfail("Tabbed windows not implemented for this backend.") + + @tabbing_enabled.setter + def tabbing_enabled(self, value): + pytest.xfail("Tabbed windows not implemented for this backend.") From 017b4b7a40f47618bad4f5cbe2f05fb7c13c1c2f Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Sun, 7 Jan 2024 20:59:08 -0500 Subject: [PATCH 5/6] Added merge command, separated assertions, cleaned up test --- cocoa/src/toga_cocoa/app.py | 10 +++++++ cocoa/tests_backend/window.py | 3 +- testbed/tests/test_window.py | 54 ++++++++++++++--------------------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index b072464c95..1b693051bb 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -289,6 +289,12 @@ def _create_app_commands(self): shortcut=toga.Key.MOD_1 + "m", group=toga.Group.WINDOW, ), + toga.Command( + self._menu_merge_all_windows, + "Merge All Windows", + group=toga.Group.WINDOW, + section=10, + ), # ---- Help menu ---------------------------------- toga.Command( self._menu_visit_homepage, @@ -317,6 +323,10 @@ def _menu_minimize(self, command, **kwargs): if self.interface.current_window: self.interface.current_window._impl.native.miniaturize(None) + def _menu_merge_all_windows(self, command, **kwargs): + native_window = self.interface.current_window._impl.native + native_window.mergeAllWindows(native_window) + def _menu_visit_homepage(self, command, **kwargs): self.interface.visit_homepage() diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 09b311e824..a9968a001b 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -267,4 +267,5 @@ def tabs(self): return self.native.tabbedWindows def merge_all_windows(self): - self.native.mergeAllWindows(self.native) + self.app.current_window = self.window + self.app._impl._menu_merge_all_windows(None) diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 34ac827995..d4f9da0b46 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -216,67 +216,57 @@ async def test_secondary_window_with_args(app, second_window, second_window_prob assert second_window not in app.windows - def probe_for_window_class(app, WindowClass): - window = WindowClass() - window.show() - probe = window_probe(app, window) - return probe - - async def test_window_tabbing(app, app_probe, main_window_probe): + async def test_window_tabbing(app, app_probe, main_window, main_window_probe): """Windows tab only with others of the same class (only implemented on macOS)""" class WindowSubclass(toga.Window): pass - base_probes = [probe_for_window_class(app, toga.Window) for _ in range(2)] - subclass_probes = [ - probe_for_window_class(app, WindowSubclass) for _ in range(2) - ] + base_windows = [toga.Window() for _ in range(2)] + subclass_windows = [WindowSubclass() for _ in range(2)] + for window in [*base_windows, *subclass_windows]: + window.show() try: - base_probe, _ = base_probes - subclass_probe, _ = subclass_probes + base_probe = window_probe(app, base_windows[0]) + subclass_probe = window_probe(app, subclass_windows[0]) app_probe.tabbing_enabled = True # Double check that nothing's tabbed initially. - assert not any( - [main_window_probe.tabs, base_probe.tabs, subclass_probe.tabs] - ) + assert not main_window_probe.tabs + assert not base_probe.tabs + assert not subclass_probe.tabs main_window_probe.merge_all_windows() await main_window_probe.wait_for_window( "Merge All Windows called on MainWindow" ) - assert not any( - [main_window_probe.tabs, base_probe.tabs, subclass_probe.tabs] - ) + # There's only one MainWindow, so nothing should have changed. + assert not main_window_probe.tabs + assert not base_probe.tabs + assert not subclass_probe.tabs base_probe.merge_all_windows() - await main_window_probe.wait_for_window( "Merge All Windows called on base Window" ) - assert ( - not main_window_probe.tabs - and len(base_probe.tabs) == 2 - and not subclass_probe.tabs - ) + assert not main_window_probe.tabs + assert len(base_probe.tabs) == 2 + assert not subclass_probe.tabs subclass_probe.merge_all_windows() await main_window_probe.wait_for_window( "Merge All Windows called on Window subclass" ) - assert ( - not main_window_probe.tabs - and len(base_probe.tabs) == 2 - and len(subclass_probe.tabs) == 2 - ) + assert not main_window_probe.tabs + assert len(base_probe.tabs) == 2 + assert len(subclass_probe.tabs) == 2 finally: app_probe.tabbing_enabled = False - for probe in [*base_probes, *subclass_probes]: - probe.close() + for window in [*base_windows, *subclass_windows]: + window.close() async def test_secondary_window_cleanup(app_probe): """Memory for windows is cleaned up when windows are deleted.""" From 4db7b1aee1b25ac7d98e65bdeb44b7d62e5c1ab5 Mon Sep 17 00:00:00 2001 From: Charles Whittington Date: Mon, 8 Jan 2024 21:45:07 -0500 Subject: [PATCH 6/6] Changed Merge All Windows to native handler. Works manually, but hangs in testbed. --- android/tests_backend/window.py | 3 --- cocoa/src/toga_cocoa/app.py | 6 +----- cocoa/tests_backend/window.py | 4 ---- gtk/tests_backend/window.py | 3 --- iOS/tests_backend/window.py | 3 --- testbed/tests/test_window.py | 13 ++++++++++--- winforms/tests_backend/window.py | 3 --- 7 files changed, 11 insertions(+), 24 deletions(-) diff --git a/android/tests_backend/window.py b/android/tests_backend/window.py index e3842088b9..5e684c565b 100644 --- a/android/tests_backend/window.py +++ b/android/tests_backend/window.py @@ -91,6 +91,3 @@ def press_toolbar_button(self, index): @property def tabs(self): pytest.xfail("Tabbed windows not implemented for this backend.") - - def merge_all_windows(self): - pytest.xfail("Tabbed windows not implemented for this backend.") diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 1b693051bb..af30dbbe7b 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -290,7 +290,7 @@ def _create_app_commands(self): group=toga.Group.WINDOW, ), toga.Command( - self._menu_merge_all_windows, + NativeHandler(SEL("mergeAllWindows:")), "Merge All Windows", group=toga.Group.WINDOW, section=10, @@ -323,10 +323,6 @@ def _menu_minimize(self, command, **kwargs): if self.interface.current_window: self.interface.current_window._impl.native.miniaturize(None) - def _menu_merge_all_windows(self, command, **kwargs): - native_window = self.interface.current_window._impl.native - native_window.mergeAllWindows(native_window) - def _menu_visit_homepage(self, command, **kwargs): self.interface.visit_homepage() diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index a9968a001b..bc8c69045a 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -265,7 +265,3 @@ def press_toolbar_button(self, index): @property def tabs(self): return self.native.tabbedWindows - - def merge_all_windows(self): - self.app.current_window = self.window - self.app._impl._menu_merge_all_windows(None) diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index c4bd6b65a8..20aa3cf4e8 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -257,6 +257,3 @@ def press_toolbar_button(self, index): @property def tabs(self): xfail("Tabbed windows not implemented for this backend.") - - def merge_all_windows(self): - xfail("Tabbed windows not implemented for this backend.") diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index 6805817f92..29d39b57ed 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -81,6 +81,3 @@ def has_toolbar(self): @property def tabs(self): pytest.xfail("Tabbed windows not implemented for this backend.") - - def merge_all_windows(self): - pytest.xfail("Tabbed windows not implemented for this backend.") diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index d4f9da0b46..d7d0f20480 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -238,7 +238,10 @@ class WindowSubclass(toga.Window): assert not base_probe.tabs assert not subclass_probe.tabs - main_window_probe.merge_all_windows() + # Merge All Windows command operates based on which window is active. + app.current_window = main_window + await main_window_probe.wait_for_window("Switched to MainWindow") + app_probe._activate_menu_item(["Window", "Merge All Windows"]) await main_window_probe.wait_for_window( "Merge All Windows called on MainWindow" ) @@ -247,7 +250,9 @@ class WindowSubclass(toga.Window): assert not base_probe.tabs assert not subclass_probe.tabs - base_probe.merge_all_windows() + app.current_window = base_probe.window + await main_window_probe.wait_for_window("Switched to base Window") + app_probe._activate_menu_item(["Window", "Merge All Windows"]) await main_window_probe.wait_for_window( "Merge All Windows called on base Window" ) @@ -255,7 +260,9 @@ class WindowSubclass(toga.Window): assert len(base_probe.tabs) == 2 assert not subclass_probe.tabs - subclass_probe.merge_all_windows() + app.current_window = subclass_probe.window + await main_window_probe.wait_for_window("Switched to subclassed Window") + app_probe._activate_menu_item(["Window", "Merge All Windows"]) await main_window_probe.wait_for_window( "Merge All Windows called on Window subclass" ) diff --git a/winforms/tests_backend/window.py b/winforms/tests_backend/window.py index 7c908208bd..0cb63c9931 100644 --- a/winforms/tests_backend/window.py +++ b/winforms/tests_backend/window.py @@ -153,6 +153,3 @@ def press_toolbar_button(self, index): @property def tabs(self): xfail("Tabbed windows not implemented for this backend.") - - def merge_all_windows(self): - xfail("Tabbed windows not implemented for this backend.")