diff --git a/changes/2023.feature.rst b/changes/2023.feature.rst new file mode 100644 index 0000000000..ee83fb8c52 --- /dev/null +++ b/changes/2023.feature.rst @@ -0,0 +1 @@ +The initial position of each newly created window is now different, cascading down the screen as windows are created. diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index d41da811df..16442cafbe 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -2,6 +2,7 @@ from toga.command import Command, Separator from toga.types import Position, Size +from toga.window import _initial_position from toga_cocoa.container import Container from toga_cocoa.libs import ( SEL, @@ -161,7 +162,7 @@ def __init__(self, interface, title, position, size): self.set_title(title) self.set_size(size) - self.set_position(position) + self.set_position(position if position is not None else _initial_position()) self.native.delegate = self.native diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 65ab6fb08d..2d41d8481a 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -193,7 +193,7 @@ def __init__( self, id: str | None = None, title: str | None = None, - position: PositionT = Position(100, 100), + position: PositionT | None = None, size: SizeT = Size(640, 480), resizable: bool = True, minimizable: bool = True, diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 725e5c4a9e..2995eadc9f 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -27,6 +27,29 @@ from toga.widgets.base import Widget +_window_count = -1 + + +def _initial_position() -> Position: + """Compute a cascading initial position for platforms that don't have a native + implementation. + + This is a stateful method; each time it is invoked, it will yield a new initial + position. + + :returns: The position for the new window. + """ + # Each new window created without an explicit position is positioned + # 50px down and to the right from the previous window, with the first + # window positioned at (100, 100). Every 15 windows, move back to a + # y coordinate of 100, and start from 50 pixels further right. + global _window_count + _window_count += 1 + + pos = 100 + (_window_count % 15) * 50 + return Position(pos + (_window_count // 15 * 50), pos) + + class FilteredWidgetRegistry: # A class that exposes a mapping lookup interface, filtered to widgets from a single # window. The underlying data store is on the app. @@ -117,7 +140,7 @@ def __init__( self, id: str | None = None, title: str | None = None, - position: PositionT = Position(100, 100), + position: PositionT | None = None, size: SizeT = Size(640, 480), resizable: bool = True, closable: bool = True, @@ -178,13 +201,11 @@ def __init__( self._minimizable = minimizable self.factory = get_platform_factory() - position = Position(*position) - size = Size(*size) self._impl = getattr(self.factory, self._WINDOW_CLASS)( interface=self, title=title if title else self._default_title, - position=position, - size=size, + position=None if position is None else Position(*position), + size=Size(*size), ) # Add the window to the app diff --git a/core/tests/app/test_mainwindow.py b/core/tests/app/test_mainwindow.py index 18381b6fdd..7112ba1942 100644 --- a/core/tests/app/test_mainwindow.py +++ b/core/tests/app/test_mainwindow.py @@ -20,7 +20,8 @@ def test_create(app): assert isinstance(window.id, str) # Window title is the app title. assert window.title == "Test App" - assert window.position == (100, 100) + # The app has created a main window, so this will be the second window. + assert window.position == (150, 150) assert window.size == (640, 480) assert window.resizable assert window.closable diff --git a/core/tests/conftest.py b/core/tests/conftest.py index 20ea402501..4997342a20 100644 --- a/core/tests/conftest.py +++ b/core/tests/conftest.py @@ -3,12 +3,16 @@ import pytest import toga +from toga import window as toga_window from toga_dummy.utils import EventLog @pytest.fixture(autouse=True) -def reset_event_log(): +def reset_global_state(): + # Clear the testing event log EventLog.reset() + # Reset the global window count + toga_window._window_count = -1 @pytest.fixture(autouse=True) diff --git a/core/tests/window/test_window.py b/core/tests/window/test_window.py index eba2c70abe..784af9562d 100644 --- a/core/tests/window/test_window.py +++ b/core/tests/window/test_window.py @@ -29,7 +29,8 @@ def test_window_created(app): # We can't know what the ID is, but it must be a string. assert isinstance(window.id, str) assert window.title == "Toga" - assert window.position == toga.Position(100, 100) + # The app has created a main window, so this will be the second window. + assert window.position == toga.Position(150, 150) assert window.size == toga.Size(640, 480) assert window.resizable assert window.closable @@ -207,6 +208,48 @@ def test_set_position(window): assert window.position == toga.Position(123, 456) +def test_position_cascade(app): + """The initial position of windows will cascade.""" + windows = [app.main_window] + + for i in range(0, 14): + win = toga.Window(title=f"Window {i}") + # The for the first 14 new windows (the app creates the first window) + # the x and y coordinates must be the same + assert win.position[0] == win.position[1] + # The position of the window should cascade down + assert win.position[0] > windows[-1].position[0] + assert win.position[1] > windows[-1].position[1] + + windows.append(win) + + # The 15th window will come back to the y origin, but shift along the x axis. + win = toga.Window(title=f"Window {i}") + assert win.position[0] > windows[0].position[0] + assert win.position[1] == windows[0].position[1] + + windows.append(win) + + # Cascade another 15 windows + for i in range(16, 30): + win = toga.Window(title=f"Window {i}") + # The position of the window should cascade down + assert win.position[0] > windows[-1].position[0] + assert win.position[1] > windows[-1].position[1] + + # The y coordinate of these windows should be the same + # as 15 windows ago; the x coordinate is shifted right + assert win.position[0] > windows[i - 15].position[0] + assert win.position[1] == windows[i - 15].position[1] + + windows.append(win) + + # The 30 window will come back to the y origin, but shift along the x axis. + win = toga.Window(title=f"Window {i}") + assert win.position[0] > windows[15].position[0] + assert win.position[1] == windows[15].position[1] + + def test_set_size(window): """The size of the window can be set.""" window.size = (123, 456) @@ -392,9 +435,11 @@ def test_screen(window, app): # window between the screens. # `window.screen` will return `Secondary Screen` assert window.screen == app.screens[1] - assert window.position == toga.Position(100, 100) + # The app has created a main window; the secondary window will be at second + # position. + assert window.position == toga.Position(150, 150) window.screen = app.screens[0] - assert window.position == toga.Position(1466, 868) + assert window.position == toga.Position(1516, 918) def test_screen_position(window, app): diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 11b961d390..bebf0332b5 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -2,6 +2,7 @@ import toga_dummy from toga.types import Size +from toga.window import _initial_position from .screens import Screen as ScreenImpl from .utils import LoggedObject @@ -50,7 +51,7 @@ def __init__(self, interface, title, position, size): self.container = Container() self.set_title(title) - self.set_position(position) + self.set_position(position if position is not None else _initial_position()) self.set_size(size) def create_toolbar(self): diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 5562ba8b81..b35569a135 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -4,6 +4,7 @@ from toga.command import Separator from toga.types import Position, Size +from toga.window import _initial_position from .container import TogaContainer from .libs import Gdk, Gtk @@ -30,7 +31,7 @@ def __init__(self, interface, title, position, size): self.native.set_default_size(size[0], size[1]) self.set_title(title) - self.set_position(position) + self.set_position(position if position is not None else _initial_position()) # Set the window deletable/closable. self.native.set_deletable(self.interface.closable) diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 2798debc84..795ab60cb8 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -6,7 +6,7 @@ from toga.colors import CORNFLOWERBLUE, FIREBRICK, REBECCAPURPLE from toga.style.pack import Pack -from ..test_window import window_probe +from ..window.test_window import window_probe @pytest.fixture diff --git a/testbed/tests/window/__init__.py b/testbed/tests/window/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testbed/tests/window/test_dialogs.py b/testbed/tests/window/test_dialogs.py new file mode 100644 index 0000000000..0af54aa590 --- /dev/null +++ b/testbed/tests/window/test_dialogs.py @@ -0,0 +1,261 @@ +import io +import traceback +from asyncio import wait_for +from pathlib import Path +from unittest.mock import Mock + +import pytest + +TESTS_DIR = Path(__file__).parent.parent + + +async def assert_dialog_result(window, dialog, on_result, expected): + actual = await wait_for(dialog, timeout=1) + if callable(expected): + assert expected(actual) + else: + assert actual == expected + + on_result.assert_called_once_with(window, actual) + + +async def test_info_dialog(main_window, main_window_probe): + """An info dialog can be displayed and acknowledged.""" + on_result_handler = Mock() + with pytest.warns( + DeprecationWarning, + match=r"Synchronous `on_result` handlers have been deprecated;", + ): + dialog_result = main_window.info_dialog( + "Info", "Some info", on_result=on_result_handler + ) + await main_window_probe.redraw("Info dialog displayed") + assert main_window_probe.is_modal_dialog(dialog_result._impl) + await main_window_probe.close_info_dialog(dialog_result._impl) + await assert_dialog_result(main_window, dialog_result, on_result_handler, None) + + +@pytest.mark.parametrize("result", [False, True]) +async def test_question_dialog(main_window, main_window_probe, result): + """An question dialog can be displayed and acknowledged.""" + on_result_handler = Mock() + with pytest.warns( + DeprecationWarning, + match=r"Synchronous `on_result` handlers have been deprecated;", + ): + dialog_result = main_window.question_dialog( + "Question", + "Some question", + on_result=on_result_handler, + ) + await main_window_probe.redraw("Question dialog displayed") + await main_window_probe.close_question_dialog(dialog_result._impl, result) + await assert_dialog_result(main_window, dialog_result, on_result_handler, result) + + +@pytest.mark.parametrize("result", [False, True]) +async def test_confirm_dialog(main_window, main_window_probe, result): + """A confirmation dialog can be displayed and acknowledged.""" + on_result_handler = Mock() + with pytest.warns( + DeprecationWarning, + match=r"Synchronous `on_result` handlers have been deprecated;", + ): + dialog_result = main_window.confirm_dialog( + "Confirm", + "Some confirmation", + on_result=on_result_handler, + ) + await main_window_probe.redraw("Confirmation dialog displayed") + await main_window_probe.close_confirm_dialog(dialog_result._impl, result) + await assert_dialog_result(main_window, dialog_result, on_result_handler, result) + + +async def test_error_dialog(main_window, main_window_probe): + """An error dialog can be displayed and acknowledged.""" + on_result_handler = Mock() + with pytest.warns( + DeprecationWarning, + match=r"Synchronous `on_result` handlers have been deprecated;", + ): + dialog_result = main_window.error_dialog( + "Error", "Some error", on_result=on_result_handler + ) + await main_window_probe.redraw("Error dialog displayed") + await main_window_probe.close_error_dialog(dialog_result._impl) + await assert_dialog_result(main_window, dialog_result, on_result_handler, None) + + +@pytest.mark.parametrize("result", [None, False, True]) +async def test_stack_trace_dialog(main_window, main_window_probe, result): + """A confirmation dialog can be displayed and acknowledged.""" + on_result_handler = Mock() + stack = io.StringIO() + traceback.print_stack(file=stack) + with pytest.warns( + DeprecationWarning, + match=r"Synchronous `on_result` handlers have been deprecated;", + ): + dialog_result = main_window.stack_trace_dialog( + "Stack Trace", + "Some stack trace", + stack.getvalue(), + retry=result is not None, + on_result=on_result_handler, + ) + await main_window_probe.redraw( + f"Stack trace dialog (with{'out' if result is None else ''} retry) displayed" + ) + await main_window_probe.close_stack_trace_dialog(dialog_result._impl, result) + await assert_dialog_result(main_window, dialog_result, on_result_handler, result) + + +@pytest.mark.parametrize( + "filename, file_types, result", + [ + ("/path/to/file.txt", None, Path("/path/to/file.txt")), + ("/path/to/file.txt", None, None), + ("/path/to/file.txt", ["txt", "doc"], Path("/path/to/file.txt")), + ("/path/to/file.txt", ["txt", "doc"], None), + ], +) +async def test_save_file_dialog( + main_window, + main_window_probe, + filename, + file_types, + result, +): + """A file open dialog can be displayed and acknowledged.""" + on_result_handler = Mock() + with pytest.warns( + DeprecationWarning, + match=r"Synchronous `on_result` handlers have been deprecated;", + ): + dialog_result = main_window.save_file_dialog( + "Save file", + suggested_filename=filename, + file_types=file_types, + on_result=on_result_handler, + ) + await main_window_probe.redraw("Save File dialog displayed") + await main_window_probe.close_save_file_dialog(dialog_result._impl, result) + + # The directory where the file dialog is opened can't be 100% predicted + # so we need to modify the check to only inspect the filename. + await assert_dialog_result( + main_window, + dialog_result, + on_result_handler, + None if result is None else (lambda actual: actual.name == result.name), + ) + + +@pytest.mark.parametrize( + "initial_directory, file_types, multiple_select, result", + [ + # Successful single select + (TESTS_DIR, None, False, TESTS_DIR / "data.py"), + # Cancelled single select + (TESTS_DIR, None, False, None), + # Successful single select with no initial directory + (None, None, False, TESTS_DIR / "data.py"), + # Successful single select with file types + (TESTS_DIR, ["txt"], False, TESTS_DIR / "data.py"), + # Successful multiple selection + ( + TESTS_DIR, + None, + True, + [TESTS_DIR / "conftest.py", TESTS_DIR / "data.py"], + ), + # Successful multiple selection of one item + (TESTS_DIR, None, True, [TESTS_DIR / "data.py"]), + # Cancelled multiple selection + (TESTS_DIR, None, True, None), + # Successful multiple selection with no initial directory + (None, None, True, [TESTS_DIR / "conftest.py", TESTS_DIR / "data.py"]), + # Successful multiple selection with file types + ( + TESTS_DIR, + ["txt", "doc"], + True, + [TESTS_DIR / "conftest.py", TESTS_DIR / "data.py"], + ), + ], +) +async def test_open_file_dialog( + main_window, + main_window_probe, + initial_directory, + file_types, + multiple_select, + result, +): + """A file open dialog can be displayed and acknowledged.""" + on_result_handler = Mock() + with pytest.warns( + DeprecationWarning, + match=r"Synchronous `on_result` handlers have been deprecated;", + ): + dialog_result = main_window.open_file_dialog( + "Open file", + initial_directory=initial_directory, + file_types=file_types, + multiple_select=multiple_select, + on_result=on_result_handler, + ) + await main_window_probe.redraw("Open File dialog displayed") + await main_window_probe.close_open_file_dialog( + dialog_result._impl, result, multiple_select + ) + await assert_dialog_result(main_window, dialog_result, on_result_handler, result) + + +@pytest.mark.parametrize( + "initial_directory, multiple_select, result", + [ + # Successful single select + (TESTS_DIR, False, TESTS_DIR / "widgets"), + # Cancelled single select + (TESTS_DIR, False, None), + # Successful single select with no initial directory + (None, False, TESTS_DIR / "widgets"), + # Successful multiple selection + (TESTS_DIR, True, [TESTS_DIR, TESTS_DIR / "widgets"]), + # Successful multiple selection with one item + (TESTS_DIR, True, [TESTS_DIR / "widgets"]), + # Cancelled multiple selection + (TESTS_DIR, True, None), + ], +) +async def test_select_folder_dialog( + main_window, + main_window_probe, + initial_directory, + multiple_select, + result, +): + """A folder selection dialog can be displayed and acknowledged.""" + on_result_handler = Mock() + with pytest.warns( + DeprecationWarning, + match=r"Synchronous `on_result` handlers have been deprecated;", + ): + dialog_result = main_window.select_folder_dialog( + "Select folder", + initial_directory=initial_directory, + multiple_select=multiple_select, + on_result=on_result_handler, + ) + await main_window_probe.redraw("Select Folder dialog displayed") + await main_window_probe.close_select_folder_dialog( + dialog_result._impl, result, multiple_select + ) + + if ( + isinstance(result, list) + and not main_window_probe.supports_multiple_select_folder + ): + result = result[-1:] + await assert_dialog_result(main_window, dialog_result, on_result_handler, result) diff --git a/testbed/tests/test_window.py b/testbed/tests/window/test_window.py similarity index 69% rename from testbed/tests/test_window.py rename to testbed/tests/window/test_window.py index 557a1da2cd..70d20016e7 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/window/test_window.py @@ -1,11 +1,7 @@ import gc -import io import re -import traceback import weakref -from asyncio import wait_for from importlib import import_module -from pathlib import Path from unittest.mock import Mock import pytest @@ -175,7 +171,10 @@ async def test_secondary_window(app, second_window, second_window_probe): assert second_window.title == "Toga" assert second_window.size == (640, 480) - assert second_window.position == (100, 100) + # Position should be cascaded; the exact position depends on the platform, + # and how many windows have been created. As long as it's not at (100,100). + assert second_window.position != (100, 100) + assert second_window_probe.is_resizable if second_window_probe.supports_closable: assert second_window_probe.is_closable @@ -550,263 +549,3 @@ async def test_as_image(main_window, main_window_probe): main_window_probe.content_size, screen=main_window.screen, ) - - -######################################################################################## -# Dialog tests -######################################################################################## - - -TESTS_DIR = Path(__file__).parent - - -async def assert_dialog_result(window, dialog, on_result, expected): - actual = await wait_for(dialog, timeout=1) - if callable(expected): - assert expected(actual) - else: - assert actual == expected - - on_result.assert_called_once_with(window, actual) - - -async def test_info_dialog(main_window, main_window_probe): - """An info dialog can be displayed and acknowledged.""" - on_result_handler = Mock() - with pytest.warns( - DeprecationWarning, - match=r"Synchronous `on_result` handlers have been deprecated;", - ): - dialog_result = main_window.info_dialog( - "Info", "Some info", on_result=on_result_handler - ) - await main_window_probe.redraw("Info dialog displayed") - assert main_window_probe.is_modal_dialog(dialog_result._impl) - await main_window_probe.close_info_dialog(dialog_result._impl) - await assert_dialog_result(main_window, dialog_result, on_result_handler, None) - - -@pytest.mark.parametrize("result", [False, True]) -async def test_question_dialog(main_window, main_window_probe, result): - """An question dialog can be displayed and acknowledged.""" - on_result_handler = Mock() - with pytest.warns( - DeprecationWarning, - match=r"Synchronous `on_result` handlers have been deprecated;", - ): - dialog_result = main_window.question_dialog( - "Question", - "Some question", - on_result=on_result_handler, - ) - await main_window_probe.redraw("Question dialog displayed") - await main_window_probe.close_question_dialog(dialog_result._impl, result) - await assert_dialog_result(main_window, dialog_result, on_result_handler, result) - - -@pytest.mark.parametrize("result", [False, True]) -async def test_confirm_dialog(main_window, main_window_probe, result): - """A confirmation dialog can be displayed and acknowledged.""" - on_result_handler = Mock() - with pytest.warns( - DeprecationWarning, - match=r"Synchronous `on_result` handlers have been deprecated;", - ): - dialog_result = main_window.confirm_dialog( - "Confirm", - "Some confirmation", - on_result=on_result_handler, - ) - await main_window_probe.redraw("Confirmation dialog displayed") - await main_window_probe.close_confirm_dialog(dialog_result._impl, result) - await assert_dialog_result(main_window, dialog_result, on_result_handler, result) - - -async def test_error_dialog(main_window, main_window_probe): - """An error dialog can be displayed and acknowledged.""" - on_result_handler = Mock() - with pytest.warns( - DeprecationWarning, - match=r"Synchronous `on_result` handlers have been deprecated;", - ): - dialog_result = main_window.error_dialog( - "Error", "Some error", on_result=on_result_handler - ) - await main_window_probe.redraw("Error dialog displayed") - await main_window_probe.close_error_dialog(dialog_result._impl) - await assert_dialog_result(main_window, dialog_result, on_result_handler, None) - - -@pytest.mark.parametrize("result", [None, False, True]) -async def test_stack_trace_dialog(main_window, main_window_probe, result): - """A confirmation dialog can be displayed and acknowledged.""" - on_result_handler = Mock() - stack = io.StringIO() - traceback.print_stack(file=stack) - with pytest.warns( - DeprecationWarning, - match=r"Synchronous `on_result` handlers have been deprecated;", - ): - dialog_result = main_window.stack_trace_dialog( - "Stack Trace", - "Some stack trace", - stack.getvalue(), - retry=result is not None, - on_result=on_result_handler, - ) - await main_window_probe.redraw( - f"Stack trace dialog (with{'out' if result is None else ''} retry) displayed" - ) - await main_window_probe.close_stack_trace_dialog(dialog_result._impl, result) - await assert_dialog_result(main_window, dialog_result, on_result_handler, result) - - -@pytest.mark.parametrize( - "filename, file_types, result", - [ - ("/path/to/file.txt", None, Path("/path/to/file.txt")), - ("/path/to/file.txt", None, None), - ("/path/to/file.txt", ["txt", "doc"], Path("/path/to/file.txt")), - ("/path/to/file.txt", ["txt", "doc"], None), - ], -) -async def test_save_file_dialog( - main_window, - main_window_probe, - filename, - file_types, - result, -): - """A file open dialog can be displayed and acknowledged.""" - on_result_handler = Mock() - with pytest.warns( - DeprecationWarning, - match=r"Synchronous `on_result` handlers have been deprecated;", - ): - dialog_result = main_window.save_file_dialog( - "Save file", - suggested_filename=filename, - file_types=file_types, - on_result=on_result_handler, - ) - await main_window_probe.redraw("Save File dialog displayed") - await main_window_probe.close_save_file_dialog(dialog_result._impl, result) - - # The directory where the file dialog is opened can't be 100% predicted - # so we need to modify the check to only inspect the filename. - await assert_dialog_result( - main_window, - dialog_result, - on_result_handler, - None if result is None else (lambda actual: actual.name == result.name), - ) - - -@pytest.mark.parametrize( - "initial_directory, file_types, multiple_select, result", - [ - # Successful single select - (TESTS_DIR, None, False, TESTS_DIR / "data.py"), - # Cancelled single select - (TESTS_DIR, None, False, None), - # Successful single select with no initial directory - (None, None, False, TESTS_DIR / "data.py"), - # Successful single select with file types - (TESTS_DIR, ["txt"], False, TESTS_DIR / "data.py"), - # Successful multiple selection - ( - TESTS_DIR, - None, - True, - [TESTS_DIR / "conftest.py", TESTS_DIR / "data.py"], - ), - # Successful multiple selection of one item - (TESTS_DIR, None, True, [TESTS_DIR / "data.py"]), - # Cancelled multiple selection - (TESTS_DIR, None, True, None), - # Successful multiple selection with no initial directory - (None, None, True, [TESTS_DIR / "conftest.py", TESTS_DIR / "data.py"]), - # Successful multiple selection with file types - ( - TESTS_DIR, - ["txt", "doc"], - True, - [TESTS_DIR / "conftest.py", TESTS_DIR / "data.py"], - ), - ], -) -async def test_open_file_dialog( - main_window, - main_window_probe, - initial_directory, - file_types, - multiple_select, - result, -): - """A file open dialog can be displayed and acknowledged.""" - on_result_handler = Mock() - with pytest.warns( - DeprecationWarning, - match=r"Synchronous `on_result` handlers have been deprecated;", - ): - dialog_result = main_window.open_file_dialog( - "Open file", - initial_directory=initial_directory, - file_types=file_types, - multiple_select=multiple_select, - on_result=on_result_handler, - ) - await main_window_probe.redraw("Open File dialog displayed") - await main_window_probe.close_open_file_dialog( - dialog_result._impl, result, multiple_select - ) - await assert_dialog_result(main_window, dialog_result, on_result_handler, result) - - -@pytest.mark.parametrize( - "initial_directory, multiple_select, result", - [ - # Successful single select - (TESTS_DIR, False, TESTS_DIR / "widgets"), - # Cancelled single select - (TESTS_DIR, False, None), - # Successful single select with no initial directory - (None, False, TESTS_DIR / "widgets"), - # Successful multiple selection - (TESTS_DIR, True, [TESTS_DIR, TESTS_DIR / "widgets"]), - # Successful multiple selection with one item - (TESTS_DIR, True, [TESTS_DIR / "widgets"]), - # Cancelled multiple selection - (TESTS_DIR, True, None), - ], -) -async def test_select_folder_dialog( - main_window, - main_window_probe, - initial_directory, - multiple_select, - result, -): - """A folder selection dialog can be displayed and acknowledged.""" - on_result_handler = Mock() - with pytest.warns( - DeprecationWarning, - match=r"Synchronous `on_result` handlers have been deprecated;", - ): - dialog_result = main_window.select_folder_dialog( - "Select folder", - initial_directory=initial_directory, - multiple_select=multiple_select, - on_result=on_result_handler, - ) - await main_window_probe.redraw("Select Folder dialog displayed") - await main_window_probe.close_select_folder_dialog( - dialog_result._impl, result, multiple_select - ) - - if ( - isinstance(result, list) - and not main_window_probe.supports_multiple_select_folder - ): - result = result[-1:] - await assert_dialog_result(main_window, dialog_result, on_result_handler, result) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 75ccdb6231..753f016c3a 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -40,7 +40,10 @@ def __init__(self, interface, title, position, size): self.set_title(title) self.set_size(size) - self.set_position(position) + # Winforms does window cascading by default; use that behavior, rather than + # Toga's re-implementation. + if position: + self.set_position(position) self.toolbar_native = None