From d3fd16d2362288e4f10e5ee43b5e65197944ba55 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 31 Jul 2023 14:18:57 +0800 Subject: [PATCH 01/27] Update docstrings and documentation for Window and MainWindow. --- core/src/toga/app.py | 46 +++---- core/src/toga/window.py | 136 ++++++++++---------- core/tests/test_deprecated_factory.py | 12 -- docs/reference/api/app.rst | 3 + docs/reference/api/index.rst | 10 +- docs/reference/api/mainwindow.rst | 23 +++- docs/reference/api/window.rst | 47 ++++--- docs/reference/data/widgets_by_platform.csv | 4 +- docs/reference/images/MainWindow.png | Bin 0 -> 130451 bytes docs/reference/images/Window.png | Bin 0 -> 100954 bytes 10 files changed, 143 insertions(+), 138 deletions(-) create mode 100644 docs/reference/images/MainWindow.png create mode 100644 docs/reference/images/Window.png diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 90ea3a24e8..2f09e33476 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -127,43 +127,45 @@ def __init__( title: str | None = None, position: tuple[int, int] = (100, 100), size: tuple[int, int] = (640, 480), - toolbar: list[Widget] | None = None, resizeable: bool = True, minimizable: bool = True, - factory: None = None, # DEPRECATED ! - on_close: None = None, - ) -> None: - ###################################################################### - # 2022-09: Backwards compatibility - ###################################################################### - # factory no longer used - if factory: - warnings.warn("The factory argument is no longer used.", DeprecationWarning) - ###################################################################### - # End backwards compatibility. - ###################################################################### + ): + """Create a new application Main Window. + + :param id: The ID of the window. + :param title: Title for the window. + :param position: Position of the window, as a tuple of ``(x, y)`` coordinates. + :param size: Size of the window, as a tuple of ``(width, height)``, in pixels. + :param resizeable: Can the window be manually resized by the user? + :param minimizable: Can the window be minimized by the user? + """ super().__init__( id=id, title=title, position=position, size=size, - toolbar=toolbar, resizeable=resizeable, closeable=True, minimizable=minimizable, - on_close=on_close, ) - @Window.on_close.setter - def on_close(self, handler): - """Raise an exception. ``on_exit`` for the app should be used instead of ``on_close`` on - main window. + @property + def on_close(self) -> None: + """The handler to invoke before the window is closed in response to a user + action. + + Always returns ``None``. Main windows should use :meth:`toga.App.on_exit`, + rather than ``on_close``. - Args: - handler (:obj:`callable`): The handler passed. + :raises ValueError: if an attempt is made to set the ``on_close`` handler for an + App. """ + return None + + @on_close.setter + def on_close(self, handler: Any): if handler: - raise AttributeError( + raise ValueError( "Cannot set on_close handler for the main window. Use the app on_exit handler instead" ) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 5d9996a0a4..56d65a86cf 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -1,6 +1,5 @@ from __future__ import annotations -import warnings from builtins import id as identifier from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, overload @@ -60,45 +59,30 @@ def __init__(self, window: Window): class Window: - """The top level container of an application. - - Args: - id: The ID of the window. - title: Title for the window. - position: Position of the window, as x,y coordinates. - size: Size of the window, as (width, height) sizes, in pixels. - toolbar: (Deprecated, will have no effect) - resizeable: Toggle if the window is resizable by the user. - closeable: Toggle if the window is closable by the user. - minimizable: Toggle if the window is minimizable by the user. - on_close: A callback to invoke when the user makes a request to close the window. - """ - _WINDOW_CLASS = "Window" def __init__( self, id: str | None = None, - title: str | None = None, + title: str = "Toga", position: tuple[int, int] = (100, 100), size: tuple[int, int] = (640, 480), - toolbar: list[Widget | None] = None, resizeable: bool = True, closeable: bool = True, minimizable: bool = True, - factory: None = None, # DEPRECATED ! on_close: OnCloseHandler | None = None, ) -> None: - ###################################################################### - # 2022-09: Backwards compatibility - ###################################################################### - # factory no longer used - if factory: - warnings.warn("The factory argument is no longer used.", DeprecationWarning) - ###################################################################### - # End backwards compatibility. - ###################################################################### - + """Create a new Window. + + :param id: The ID of the window. + :param title: Title for the window. + :param position: Position of the window, as a tuple of ``(x, y)`` coordinates. + :param size: Size of the window, as a tuple of ``(width, height)``, in pixels. + :param resizeable: Can the window be manually resized by the user? + :param closeable: Should the window provide the option to be manually closed? + :param minimizable: Can the window be minimized by the user? + :param on_close: The initial ``on_close`` handler. + """ self.widgets = WidgetRegistry() self._id = str(id if id else identifier(self)) @@ -114,7 +98,7 @@ def __init__( self.factory = get_platform_factory() self._impl = getattr(self.factory, self._WINDOW_CLASS)( interface=self, - title="Toga" if title is None else title, + title=title, position=position, size=size, ) @@ -125,28 +109,21 @@ def __init__( @property def id(self) -> str: - """The DOM identifier for the window. - - This id can be used to target CSS directives. - """ + """The DOM identifier for the window.""" return self._id @property def app(self) -> App | None: """Instance of the :class:`toga.App` that this window belongs to. - Returns: - The app that it belongs to :class:`toga.App`. - - Raises: - Exception: If the window already is associated with another app. - """ + :raises ValueError: If a window is already assigned to an app, and an attempt is made + to assign the window to a new app.""" return self._app @app.setter def app(self, app: App) -> None: if self._app: - raise Exception("Window is already associated with an App") + raise ValueError("Window is already associated with an App") self._app = app self._impl.set_app(app._impl) @@ -156,7 +133,7 @@ def app(self, app: App) -> None: @property def title(self) -> str: - """Title of the window. If no title is given it defaults to ``"Toga"``.""" + """Title of the window. If no title is provided, the title will default to ``"Toga"``.""" return self._impl.get_title() @title.setter @@ -219,23 +196,32 @@ def position(self, position: tuple[int, int]) -> None: self._impl.set_position(position) def show(self) -> None: - """Show window, if hidden.""" + """Show the window, if hidden. + + :raises ValueError: if the window hasn't been associated with an""" if self.app is None: - raise AttributeError( - "Can't show a window that doesn't have an associated app" - ) + raise ValueError("Can't show a window that doesn't have an associated app") self._impl.show() def hide(self) -> None: - """Hide window, if shown.""" + """Hide window, if shown. + + :raises ValueError: if the window hasn't been associated with an app.""" if self.app is None: - raise AttributeError( - "Can't hide a window that doesn't have an associated app" - ) + raise ValueError("Can't hide a window that doesn't have an associated app") self._impl.hide() @property def full_screen(self) -> bool: + """Is the window in full screen mode? + + .. note:: + Full screen mode is *not* the same as "maximized". A full screen window + has no title bar, tool bar or window control widgets; some or all of these + controls may be visible on a maximized app. A good example of "full screen" + mode is a slideshow app in presentation mode - the only visible content is + the slide. + """ return self._is_full_screen @full_screen.setter @@ -245,6 +231,7 @@ def full_screen(self, is_full_screen: bool) -> None: @property def visible(self) -> bool: + "Is the window visible?" return self._impl.get_visible() @visible.setter @@ -256,7 +243,12 @@ def visible(self, visible: bool) -> None: @property def on_close(self) -> OnCloseHandler: - """The handler to invoke before the window is closed.""" + """The handler to invoke before the window is closed in response to a user + action. + + If the handler returns ``False``, the request to close the window will be + cancelled. + """ return self._on_close @on_close.setter @@ -268,6 +260,10 @@ def cleanup(window: Window, should_close: bool) -> None: self._on_close = wrapped_handler(self, handler, cleanup=cleanup) def close(self) -> None: + """Close the window. + + This *does not* invoke the ``on_close`` handler; the window will be immediately + and unconditionally closed.""" self.app.windows -= self self._impl.close() @@ -283,7 +279,7 @@ def info_dialog( ) -> Dialog: """Ask the user to acknowledge some information. - Presents as a dialog with a single 'OK' button to close the dialog. + Presents as a dialog with a single "OK" button to close the dialog. :param title: The title of the dialog window. :param message: The message to display. @@ -306,15 +302,15 @@ def question_dialog( ) -> Dialog: """Ask the user a yes/no question. - Presents as a dialog with a 'YES' and 'NO' button. + Presents as a dialog with "Yes" and "No" buttons. :param title: The title of the dialog window. :param message: The question to be answered. :param on_result: A callback that will be invoked when the user selects an option on the dialog. :returns: An awaitable Dialog object. The Dialog object returns - ``True`` when the 'YES' button was pressed, ``False`` when - the 'NO' button was pressed. + ``True`` when the "Yes" button was pressed, ``False`` when + the "No" button was pressed. """ dialog = Dialog(self) self.factory.dialogs.QuestionDialog( @@ -330,16 +326,16 @@ def confirm_dialog( ) -> Dialog: """Ask the user to confirm if they wish to proceed with an action. - Presents as a dialog with 'Cancel' and 'OK' buttons (or whatever labels - are appropriate on the current platform) + Presents as a dialog with "Cancel" and "OK" buttons (or whatever labels + are appropriate on the current platform). :param title: The title of the dialog window. :param message: A message describing the action to be confirmed. :param on_result: A callback that will be invoked when the user selects an option on the dialog. :returns: An awaitable Dialog object. The Dialog object returns - ``True`` when the 'OK' button was pressed, ``False`` when - the 'CANCEL' button was pressed. + ``True`` when the "OK" button was pressed, ``False`` when + the "CANCEL" button was pressed. """ dialog = Dialog(self) self.factory.dialogs.ConfirmDialog( @@ -355,14 +351,14 @@ def error_dialog( ) -> Dialog: """Ask the user to acknowledge an error state. - Presents as an error dialog with a 'OK' button to close the dialog. + Presents as an error dialog with a "OK" button to close the dialog. :param title: The title of the dialog window. :param message: The error message to display. :param on_result: A callback that will be invoked when the user selects an option on the dialog. :returns: An awaitable Dialog object. The Dialog object returns - ``None`` after the user pressed the 'OK' button. + ``None`` after the user pressed the "OK" button. """ dialog = Dialog(self) self.factory.dialogs.ErrorDialog( @@ -448,18 +444,18 @@ def save_file_dialog( Presents the user a system-native "Save file" dialog. - This opens a native dialog where the user can select a place to save a file. - It is possible to suggest a filename and force the user to use a specific file extension. - If no path is returned (e.g. dialog is canceled), a ValueError is raised. + This opens a native dialog where the user can select a place to save a file. It + is possible to suggest a filename, and constrain the list of allowed file + extensions. :param title: The title of the dialog window :param suggested_filename: A default filename :param file_types: A list of strings with the allowed file extensions. - :param on_result: A callback that will be invoked when the user - selects an option on the dialog. - :returns: An awaitable Dialog object. The Dialog object returns - a path object for the selected file location, or ``None`` if - the user cancelled the save operation. + :param on_result: A callback that will be invoked when the user selects an + option on the dialog. + :returns: An awaitable Dialog object. The Dialog object returns a path object + for the selected file location, or ``None`` if the user cancelled the save + operation. """ dialog = Dialog(self) # Convert suggested filename to a path (if it isn't already), @@ -528,10 +524,10 @@ def open_file_dialog( :param title: The title of the dialog window :param initial_directory: The initial folder in which to open the dialog. If ``None``, use the default location provided by the operating system - (which will often be "last used location") + (which will often be the last used location) :param file_types: A list of strings with the allowed file extensions. :param multiselect: If True, the user will be able to select multiple - files; if False, the selection will be restricted to a single file/ + files; if False, the selection will be restricted to a single file. :param on_result: A callback that will be invoked when the user selects an option on the dialog. :returns: An awaitable Dialog object. The Dialog object returns diff --git a/core/tests/test_deprecated_factory.py b/core/tests/test_deprecated_factory.py index afc49d689c..47f6ee59ef 100644 --- a/core/tests/test_deprecated_factory.py +++ b/core/tests/test_deprecated_factory.py @@ -30,12 +30,6 @@ def test_document_app(self): self.assertEqual(widget._impl.interface, widget) self.assertNotEqual(widget.factory, self.factory) - def test_main_window(self): - with self.assertWarns(DeprecationWarning): - widget = toga.MainWindow(factory=self.factory) - self.assertEqual(widget._impl.interface, widget) - self.assertNotEqual(widget.factory, self.factory) - def test_command(self): with self.assertWarns(DeprecationWarning): widget = toga.Command(self.callback, "Test", factory=self.factory) @@ -62,12 +56,6 @@ def test_icon(self): self.assertEqual(widget._impl.interface, widget) self.assertNotEqual(widget.factory, self.factory) - def test_window(self): - with self.assertWarns(DeprecationWarning): - widget = toga.Window(factory=self.factory) - self.assertEqual(widget._impl.interface, widget) - self.assertNotEqual(widget.factory, self.factory) - def test_canvas_created(self): with self.assertWarns(DeprecationWarning): widget = toga.Canvas(factory=self.factory) diff --git a/docs/reference/api/app.rst b/docs/reference/api/app.rst index ab3b036f3f..dc9b7d000e 100644 --- a/docs/reference/api/app.rst +++ b/docs/reference/api/app.rst @@ -48,6 +48,9 @@ Alternatively, you can subclass App and implement the startup method app = MyApp('First App', 'org.beeware.helloworld') app.main_loop() +All App instances must have a main window. This main window must exist at the conclusion +of the ``startup()`` method. + Reference --------- diff --git a/docs/reference/api/index.rst b/docs/reference/api/index.rst index 56ee42a770..41968d8568 100644 --- a/docs/reference/api/index.rst +++ b/docs/reference/api/index.rst @@ -7,13 +7,13 @@ API Reference Core application components --------------------------- -=============================================== ======================== +=============================================== =================================================== Component Description -=============================================== ======================== +=============================================== =================================================== :doc:`Application ` The application itself - :doc:`Window ` Window object - :doc:`MainWindow ` Main Window -=============================================== ======================== + :doc:`Window ` An operating system-managed container of widgets. + :doc:`MainWindow ` The main window of the application. +=============================================== =================================================== General widgets --------------- diff --git a/docs/reference/api/mainwindow.rst b/docs/reference/api/mainwindow.rst index caedcd55a4..1bd8ddbede 100644 --- a/docs/reference/api/mainwindow.rst +++ b/docs/reference/api/mainwindow.rst @@ -1,6 +1,12 @@ MainWindow ========== +The main window of the application. + +.. figure:: /reference/images/MainWindow.png + :align: center + :width: 300px + .. rst-class:: widget-support .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 @@ -8,20 +14,25 @@ MainWindow :included_cols: 4,5,6,7,8,9 :exclude: {0: '(?!(MainWindow|Component))'} -A window for displaying components to the user - Usage ----- -A MainWindow is used for desktop applications, where components need to be shown within a window-manager. Windows can be configured on -instantiation and support displaying multiple widgets, toolbars and resizing. +The Main Window of an application is a normal :class:`toga.Window`, with one exception - +when the Main Window is closed, the application exits. .. code-block:: python import toga - window = toga.MainWindow('id-window', title='This is a window!') - window.show() + main_window = toga.MainWindow(title='My Application') + + self.toga.App.main_window = main_window + main_window.show() + +As the main window is closely bound to the App, a main window *cannot* define an +``on_close`` handler. Instead, if you want to prevent the main window from exiting, you +should use an ``on_exit`` handler on the :class:`toga.App` that the main window is +associated with. Reference --------- diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index 0b02584aaa..167c481ea4 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -1,6 +1,12 @@ Window ====== +An operating system-managed container of widgets. + +.. figure:: /reference/images/Window.png + :align: center + :width: 300px + .. rst-class:: widget-support .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 @@ -8,38 +14,37 @@ Window :included_cols: 4,5,6,7,8,9 :exclude: {0: '(?!(Window|Component))'} -A window for displaying components to the user - Usage ----- -The window class is used for desktop applications, where components need to be shown within a window-manager. Windows can be configured on -instantiation and support displaying multiple widgets, toolbars and resizing. +A window is the top-level container that the operating system uses to contain widgets. +The window has content, which will usually be a container widget of some kind. A window +may also have other decorations, such as a title bar or toolbar. + +By default, a window is not visible. A window must be associated with an application +before it can be displayed. The content of the window can be changed by re-assigning the +content of the window to a new widget. .. code-block:: python import toga + window = toga.Window() + window.content = toga.Box(children=[...]) + toga.App.app.windows += window + window.show() - class ExampleWindow(toga.App): - def startup(self): - self.label = toga.Label('Hello World') - outer_box = toga.Box( - children=[self.label] - ) - self.window = toga.Window() - self.window.content = outer_box - - self.window.show() - - - def main(): - return ExampleWindow('Window', 'org.beeware.window') + # Change the window's content to something new + window.content = toga.Box(children=[...]) +The operating system may provide controls that allow the user to resize, reposition, +minimize or maximize the the window. However, the availability of these controls is +entirely operating system dependent. - if __name__ == '__main__': - app = main() - app.main_loop() +If the operating system provides a way to close the window, Toga will call the +``on_close`` handler. This handler must return a Boolean confirming whether the close is +permitted. This can be used to implement protections against closing a window with +unsaved changes. Reference --------- diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index c2faab48df..7f7804cbf3 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -1,7 +1,7 @@ Component,Type,Component,Description,macOS,GTK,Windows,iOS,Android,Web Application,Core Component,:class:`~toga.App`,The application itself,|b|,|b|,|b|,|b|,|b|,|b| -Window,Core Component,:class:`~toga.Window`,Window object,|b|,|b|,|b|,|b|,|b|,|b| -MainWindow,Core Component,:class:`~toga.MainWindow`,Main window of the application,|b|,|b|,|b|,|b|,|b|,|b| +Window,Core Component,:class:`~toga.Window`,An operating system-managed container of widgets.,|b|,|b|,|b|,|b|,|b|,|b| +MainWindow,Core Component,:class:`~toga.MainWindow`,The main window of the application.,|b|,|b|,|b|,|b|,|b|,|b| ActivityIndicator,General Widget,:class:`~toga.ActivityIndicator`,A spinning activity animation,|y|,|y|,,,,|b| Button,General Widget,:class:`~toga.Button`,Basic clickable Button,|y|,|y|,|y|,|y|,|y|,|b| Canvas,General Widget,:class:`~toga.Canvas`,Area you can draw on,|b|,|b|,|b|,|b|,, diff --git a/docs/reference/images/MainWindow.png b/docs/reference/images/MainWindow.png new file mode 100644 index 0000000000000000000000000000000000000000..b8d72e6fd0592596061770b4b6295c79b83583f0 GIT binary patch literal 130451 zcmeFZdpwiP!^UjC%l$drpU3a_`2G3)@4Lt2dR%YU`+8s3^?toyhv)0?mUza+ zUiSChzi-;KN%r)q6K6MVlE~P!Y0EyTZNQb7%KMU=Hf{Fwx3xWU+SYc@nOniv{R4eB zZQB0?^|;<)i|fvZVtreOSIWEg*%Bod@@sWPlj`I*oO;w-PP(MJRwF7COIMfBl)i_o4!9pu`i_O35M!! z% z@{##P?^gQoK8?bh6Wb1c`>@(ZMS*gL9CHd#ek08Nx{nD?G2RA5l3 zMU=JH-*;F5?TyP2tv!F=5*A>s@Q8TN*@Bjbw{PV{D8R`DdNb@6y|99m7^!&d^ zo)7iCWgCnFUKs}cXSx18_$-(BG{f|6Mca@A_)lKEN`n z`=5ZH1I_@GZTvQGFwp*goq_h|ZHn@lx#pWT{jurv37d0Ko2Tm}@1QQ87n6B^4#)i3 zetQ32$b)@hC-k?U+U_8!|8r4NDi^l*mBY^bH>daiE~S( zzSMJj_BNKK_bxarFbJUx&hRn4mEmgR(89V)2hBgNc*PVi4XxFz^!a618}l4apWgQL z?zKItr~dEh{|pJzEF^wNZ2l3^y&|O(9yr0uMUj`!AF~Pe0QaL1$bHc1uoKX6yARLA zgVtEnUVX#`DB*Me|ne)N*%H1sdo)`^poNJnRZyhgrzlRr!5=fz&TX%JqKn9Xm7#ZY4Hnuq_e ze>)s|!XrE4eD_4IrbaGd4W~%N!I>*&59FisQ{AFp7rWHFa}asde%Y=SJZtJeG4W)J zre^f{HxaI%d&izsSj@aTu!2Yenc0lBuG=NjAO0A`)>&@_&mMGGd13YQLgAJ3`M|vY zI8s`wIzO^-H~^o9k++^Idoq#aQWsm6SmYJ6T7EH|UwSLxNvOjV3Oep??OIk2*U)(i zkl>Eqz7<4#{PM*Wh5Yc9OJi;8ugMkF;ss&A9B4UVO=ouRii^RJeQM7DcMv(r?d^ev zr=DVR1XRtdggrG=0><+z80QV4xtgXjvS-O=%4Rp1{-qxbZke#YQ#=ry`BSncmWU#A zmB4luwckM~fh#CF7yOEb8AyE+Jh{<>98~#%3GYZzImOfy&&sbz_znhFi**}@My&EW6RD~RCW`lPI{xZ1OY^3ChLqjyVg>A+EzAQNtb+mJK<%Uw*uIt{}n#HBGs$h zVy_{IzlXl3fA__@*hGSmBbpk54C`@+^V(vOzpNXrUt3#D()SGe_1o2?oP_Ac_6MZ% zqeYF@Uqs&k=7rZ5R^e+A?XI*n`3tu>iJm8OER>@~QSf@%@Dn-AI!>&geqo2^&~Z98 zSziaxauK%MtX^AXS-sEA@$Y0nlS^&z6jDmT$}!QBZOAmv%%>f+R5qi87g6A%A^D9d z-Cfgh7{+RtSV0DjZ?Mc+s+~QZ6>x?Xc0m{pBf=Jh%{=O-yX!l2Xbd^EVaSt08&2>T zWNL#iPpQna3zFqeE;+@0Hie?5cP#b$D6Q?zTYVt0E0!#A67iifXPr8p|MTbga|=cD zG`GcO@rj_3-3$-5<|kGsiCLBXpu-00ncn7J>fNSdUizWm6Iq`91W?4(A8$u!YhVsS8oKsmg!X` zUC6=`BmMGEDS%{FRN!nZm;3OB+?d+$(plfLYh7HnD4E!@a`0Nn0$i91D3%QEZA_d-FedoV_)%H#4|i&B1(fo~bY61!>by<7 zd#G+8qj$y#Upuac|22|v^c>Qv_B@WMctoGf# z&=g=%LcQ$Me^GCFR(dWm;yDMByx4X|Fmc0jWyew!yXkYpB}2t+{nE9k9@g!PR|T^x z&ut5mGibNZdwMUg?@P_6^h3BW-D6Ww~j+G8|>^y?Apfu(lmeYX^c#jto`N& zus3LS*D@}q*}``iTl$y-&mm{8=@4MqYj;Zo!e80Xp%+IQjgJw?HIbtF%Y_@Owo~Qs ztJjOqwhncK+{opuw7=0_y@B`gm?YvzC_JL9sI{pns!Rc?i&-FS$b6g3>)0F$PdDe_ z)l6 z(QnaxrXnMvrTjUO3-8`_^c9SKB^+^JMn*ls^3P~pT$X2n)t>D`que-8#qOqwbX9af zxx#cRWiD3fH^ZvORpN0q>%qPPO>_Lx82J+_3)fnhvJy)BOUKjelPe5u$h>;EG9g`KsYrz4Ua2GWiZ~c{y{i07wmC6_P7j)?^;~xc2H{PS6-Plj=R&C~ls5mOVP^*i*dP1xA zd57!D)vK*k3wgj9*DVbFhn%gZ!I~AQW;RCVo5rZqhfz->gI)Hg8!6 z7K&3lACN8#1M}$1hx3ZXfg|>T zlPTE}ai4j4i#uM0IW1511`pEhYMR;b_y3~HY-TNBW}A7HE3L25A}2I-j91wJaI%Ib zU4B@bji}K3L4Fs&`}iy85r!Qy85m%hAU4Yh8Rg-+Q!ib=^T6f#m2FX_q1yVx24!!x z2akSuK=|5G@PTw8^R@>b!c81c+7&)fZlR36S;r4bH~q{{z6+;NG`E7mJ}%*+zimvB zY1@aG__h6WxL{rPcmcU#z;!=A+E-MH&Vlb_`DI@8;*NY(3WWH3ho2Lh$Zu9G{W$wbidMCfF3Jb zerb*ls-$AQp9!cf3ndPtWh-FP?4_v>+i7Tmc%nu2?LmNRaL>k7RIj?8ZKC~Q^`>h3 zSl?&U(_JwV(a-$jH23N<^|B^9y!{4mOeMXD?S3ENHy!0x!NtK7tr;5jW-sf@2~eXi zCrX#x?AF>n3a{lfu!Dub67@l<#Rs;ZUl9G^>CqTy5g$DkxLEyy#$YG32$8*^UqQqS zKUcyJVN$Gx2AY}xJK`aL5bJMcb-6WB?nuK5ZaP{d%f|-&iI^QpKUmTCB}6H}t-DpX z@EoT#Uk-LNS%H=O2yU?cmI|`wQX&WGuL4*xZa-lMNc5E05A%(Q{I>Y&y`k1(%G`A3 z7;FVGjNQ58UUbYc3%rooPzp6~AmbnEb_D`R9L% z`nZ$h{bh|xj+q99&6`dXv?Om1YOgXH-}6G#-dl43~avr#1e zDo4*ofpELlrtw#N$sq`Vk~<_%!1$bP`wg~w?^rJ%B~l_+eh4Lsnz2KR=WE5G;&Bnr zvvn4VO*El9V_K@b{b7mzM)nY;# z^m%sV+%b@v5YePaFo6`H(86|Y(z&z2tUOza z5_S0+pTf3WV14y7IK^D)`5o$+zPzd@20-=Stb}lB&Y2fxwDD+C0%0=;U#%G9vloV? zJ{LQOi$iBmpm`^m$xp?P?DeT#_I2X200Nehz%O&NySB%Wl3?iAFGHdS1|ARI6xJ0h z)lq2Je=fYY4Rma^I{CEBD&=i=0ii+-;eqTdfJ!m=-LDgE6jiy2Q#k8gXlfkIX`YqT z8aD4|0dQ5FyN)=Ift>|hu4pkwV7k6xyrV^{xMH-(pq3*~tHua8RXJFS_s}78`F=t0 zdD#{$Q6oacOp?vjP=Quxj24X`C$aE^Gska3v;{={HDHB}5g)&k z)iS=$G2P45B>YaIF#_AG%P;$hsz!YmosYQ}HpEA0v%m5BHA z=_(APDY7(a05&OH0&fK;10q@tfCJDV3~(1@qOfD@ru~8`Q8TZw601DPi&ywe<_UlB z@O2~W1ATRM>qdX^@WMdiptvP;P{6syGu7Lsmh&w@zuM~HpXCbSgM_>>@+vES0-gxF zNGWMeHIaDQ+v8WmLmsR>yf?^jGVp*&x>o#5W)enDNp3f4{}N7ZXB{Y`t6JfGXN1A< zp7Y&Ov8N~?CE?7HPm5Fq6A9V2?#VpUf3fI_^-|wg$!9;*Vo3AjR@|7Fwpb(36hRm{ z8#jop7F+RnfcNvD4+BW@GmiYqfR8mc+77NKMz-Y`voc_;3EZJX!E?FUq2pawwWlb^}T9nO0T{Fzd~fh5A8v( z60>CxmdrDpd5Ru0bpI<`11b*g$sNC^_*`0OCZ-V9mNtL`XPse=iu6@pzoWI`v zn)vEOauFdyfdGXL7yiZobjP4O-ym zNfssD)`2P4kd7C8UGZ`h4(E6exr0_D2TS9J=291B`>~5L&J}=))+utBB?G;4V;lOA zM)+kdwvN%~2{m%CWdzuCb>l73rw z`OcH~m`S#&$rf8gI;gxxFV-k6GaQHVFWy@qZ-OiI8HlXG=aB%NXCFv=fxja1AL^je~& z4Az1N-U?f&eLSxX>AWItv=Ubtnu~u8iK|7=2(S>sq1S7$l!GgP#?N0CW=^foT{@l9 z2@zj0jYDiDMrtVdm9yHVi?LP6%vjq00S!}ov@I)Bd z=DBj8!wAdyvKv zROP*LiY(j<@sWaWTSMBN{X~=}1Fqb1Q14keReKLld+P8kqjnzpMJJ+%5X(qOtP!0kLPb}+T6DMAGrOK+MJz0y{OxTm7fNMyWhu6$I}Evv=7 zsjJJGFj4~1u0D9p=^>mPddtB0x%eEnF_4C6yZ&!LLJlp6hK#K3;D`CU@co5ElQpf(s-r>nDj5f zROr>cgW@b>mKoNpb5~W0p!eDe82tFKoUp3kDfV7baIwje^Jq4HBtB~StRk!l8R$Bp zwu{VgN4_6=C#Uata&S|STtBzqIE`6D>^mCt@G{C|2MTfSP2~g7fuNnpcF4A<)x5cQ z{2?RtgEMAc>lz+|bpCwIV`Ic||7OG1;EDF@9+jN zHHd&v_m3l#D6j{7RL=*z5PfUxx4*83uA2uGyueln{LqxEC?>DSB(oxx-YCRlVmq`u z2+$_`#9>7tYU!(_wU*tYJ$wKvZ0x-#!#aUoOHkU1Mx-twv!qZVfpKJyq5=1+@a0V_WF5hF+J#4X%FmeYwL#T%*;HYc_Md|dF%hQERq zJ$~lAEIfv2Nb@sTdl-|9PBaPs?n)qwW?5m&;0(9M62n&a0RRs1Td3%4a~D4mJ2kCI zEma(M_bUE|y*@N{GEa7C=$8giQ`syN|#(Ij+hG4MZcxW=E;i^P$ByjSWOkTy*9;< z#<(TOKSU~);7XuOSB{TC8{sUPJ#H3I?hzT-Fkg2`D~Rk4%&&w$0jMJyLfwHvd{c-! zV3W#CO}^Qq{vX%%X#Yk9B2MtXJ<6D-C>M&cHUTCOjy_ileG&r0Msu@*L=El$ z!;Quz-xe818Vc=NVPkJTJ533S$m566fja_g;}3OsR1%hh4m#b6+{tNVM%#Xzuh75Z z9;~}IBH^)^pw?8OCQ)a$7=je|0BdnIPR??&A|QM}iCD^es3)fdHEEb{oQ`J%izpE( z>+>^7{K*0fu?dv^MC{6qZzW_b90FAg@GG+6FyY$;SP+`3EFk<40HDlbbv?d#4W$mq zOnk=}z-Dcqd_Ho(F*72A3D(E{ zT9fxMQja_z&O@vXVmn6}RkM*yrQQma{tmc?li>+;Q_%>ApO}B9P#(OBO1gMnY2JVC zf|h?x_v^`PQK|`Q+J>LiDAk|l``tc=4DJlt!PKW$UnCm5pLZNH^I(Gj!wb*a$uxWltREEn^_w*F7)Q}wLU$VvoaXq&ArsaEHG-s zbRfM|h3#liik;y}ULmgss{=V74Y{GMJV_bfR9>$zEOp)9votH-m!jvjla-gWoC1y|^Jr%5 z`4H4Zo(4;yK>)Zs75YzZK!US#hDM5>4aye$bgJK4Fsl1Jal^2(;aFYTx6H|>51m*H|d{ZybV&hD*XORk1T7M4QMCbq&Bihu|d?PrYWCLsgc8n*6@SI6-Jy4HdZ z@RRXLT$eQLg=bHA@TP!3p{^x%#!n`hY@KGq>y?u^A@&v`ZLLlKjINP1G;xqJ>%z=6 z(gO_(I_%04*MknPMeljnkb?_I9R8t_$}M=?`awP%>D@~r`oO|h*G82jWQh6-NSY*+ z_k`xnPgWXO7IIH!NrZ>xwGpCLsgfa70$+a$_{`aBadm(Z=hI+O ztvpT-jbMfCQ;in2^$G^OvCD#$VSL@vHM#V>zq-nHm3Bw@^ zM$n^40nJeg>@oUV;|K=|$)%{US9e)W!h=)7$TigY(-rR1J95R0z0>qw5@^ZLFhVNU z({*tF#G4r8RgUf#0(_ewdY%q}M$qkRBWfZdxhxeeDAxx)IT*(J8ku$M+Qrq;PQV8V zu$&aujFcZS6q+z*DngHg^T8yZO89zWUsFJ3eHW1Lkl81;HtYwGA-SUsHg|B&gEmfo z=sR;ec?E`DbQC=kyXKH*53V@Rl09JZJW=MVpnnoX#D%_{B@z~E-*H*QKd{= zo_B6_bPCodn3C3~vnY|83Jg#6?Ddq;bLg2YOC6$?Br&+7>p6XPPfNqe$XD6vNw0^U z|Mx*dxMkFnR*=S3LRSvL*O?brYTk7XmRf zMlp@hS1aZLffxt?!Ze?8XwzA+1qc6>t~PAbx=>P@IGrw*u@EoTV?XkGVaHFS*?Z&w zfR|t!`%!=d<%6Lr6r?gafdFd zfwL=?Wxw{yy|>|~XamVW;pA}gNi_$K0;)7Xdfm8DZ(Hv*{xa3b>0$7ZSHW2z(i3Ua z{=A_*B2{~)JVHE14wb8_WRkxBTeyrbv31}kBXLF%#aOSWF)taC z0tEal_lp+=u-{204YSzglc^X%x+rd84ilO)DTuT7>4WH6tPltLoA5;h#CeF9cW%-p z6|yvSOJn-6ffh*6iN&@|i{mok!%h=#ib}c*Tn6U)Un+rRpbFXrAeIe46G>}dn2?Te z-fctO#jW2Xmu?!N#)!V~?0O*O4~pJH3l6--8cS8>vV_{u4W*Ca;*#W?sGUuIPM&!q z#vE5}W0B!dLm5NpCr!4-4ul@Va&L*MMbqXhwg5n_{#N&7MLeBFo~_)7VfSUXftQaR z&&hk7elulH+yZzm{%sC<%!rL~YsRYUA$LCz&vd(h9oY4|c(D0$)uUKWe0EkDH{jyM zZP{f0Vgp_PX4&j-{2G5-4WUlG=)Bt%wqKleK%DXx7wmzaQK#nW>)h^;F_TFp+B2`9 z!#oY1swJ4L1Gd|~thhBOYr<9ohE6~ld|$TH%E|OOf;N?f(zRB1^kR&*jZRUcC??m{ zD=i#+%&{Cl^;k+FSoQu~#&{gxrz4)6)seza0a^PPlxjg&o9Br}!*BQ@@4I8|_;Gx& z34p6+{@UPSr&;kv=HLep>*K;Q4r$*&w@DnpAsW4W_Nh|zb(e9WcA9`t{j-!);-2cs zf0M+Y)k7{1U`Gzm+VGn+QpIn34VlJ}Yk*@j!n-vcm3UVr2@{h1K+ef9424GH04Kry zLuSbS_*O6?_PvrNGY@g@>uXS6)sDw=?5l`c{G3&SHWaDMFqipCy?_Ww&V$?*+Bf!sVMwQ4;F#Ed>|vfY z5cA3w@E`=3BTMk3?k7b2{QGL}jvb}o%?gY-P|>EdEZflg0U@%Vw&9gr z!7VmWsZ}cFKw5;QeiR$45Ad6DzyClE_SjSstt#>EL_oF)=&K4b{~UvaA$TFp0UTD!5cN2Dx1Wy@dO^#1-Qb1kmWzf?A8T#s}b^SeUD+!6Tnz_x4G<%}w3D&v~x24*I%k8bA7h_jvVO4vcOL=9mSF+QR8VqUuAqVVvxif~r)$ z?AJp1j^WS5KsP(#gneDM*c%ooq68zCGszK@Bz}hpGaxw7QTR4ge}<&eX+}SbII5^u z)h>zJGMh0(F+8D8HkokWf$|@S2ZyiS_+&J|owvtVMZd9!^*@SY(hE-H=2(DYDeM}{ zl%fl2uY%8^kwzvb#U>=PHf`oqfGQVPOb98@FP8n>Jg*T-u`ir}tEnNA7{PUA_4Zf? z3cZaC#!x@<&SNikHb%auoC3l?GrTcz&k8pFRa@^MR*43iL(RQ)oyDTOj{;Rbr0=|v zPoRzPV7##j8=P(VnipGGMh|a z%p@c83AZVc!lq;ntOA7|=>G-J{LB}+7YQPZBYY3=%@Y0Yc=Pc5Rdbhh{VQ;4vfSEM z$kVzJ$;*-x?t9g8b?CB8-7tj-yh(Xq88Hd+J3a5yYkY>`0izpg7t#Gvs6Z`}1@)={ zdXUmjQ5y(x+C_eteFvg9p=zrJb}F3+V0AWmfdjL!#SzuJk78%^>ypQXdJRAf#&y2_ zrl?_FRcU>l&5#Fs{02L`trc4_uk8htMTNjhO<4kCwKW)XT=Zidv|;;d#cBNl+u=JI z_AT8)^MS%p(JegFUgVz={~=qA;TRO z-!24xx=NhQMS`a2`V&{e!c+TewpZB*9nbpQfDgzb37E2KdOP#Xfz@);6xIPi-ZYt69Xmpjw!*O)?LrVOl2a_ueC5>J`C znJ!#eKC>~&1>rBTkT=-EOLxoF14>H90a3$BJXc z3i6h$lOisT)xE7v0cjx_)?^H~jla>^WJ)6XDKYAPs=9KDSbwsRMJ_P|#1f6psCB^T z#E2>qovlgYZ#cg6himQ9SN3RxaQF3ZeR^#vwWA_L&O%wL9%6W7@s(z9SAw=Ar?6ISvj`$<5GufZZua}@KMpljlEKhe&ty1SkXOdw_wO!x5ysUBg`rt2 zMh@Ai-hSRx8MUfS5>3=ir<1>txf-8D6uW7H97zP=c41x{jH@>*FUXu-1yT3sDlKFwPt2ga=xo(ezi8?B2uqIG`tO!6vYhVI~&!1|jy@K^w%*0YE5u(In$ykxO za`pS}?JI@6VBXw|N8R#-j1DqG$kkKU0V#&_Z=OU4DrcGTx0;q7-gA1{?os9zV@|u5 z=PKPP@EpqES3^!?L|!6Gw*oTB4UIcvKaJ=6+?%uO?vd5OENZ9m`Dg9B+*Y=|N|9;c zl+*$}ROMIK)EDF6PJx4^sV&c-*N&hAvm{9~0w9^1Pw`6mWv?ffA6wo(q2UM<5dkyT zJYSy9a-F}D6vDKmQV8hvXV?gom?ya&whTBh%`|M)l4owt3KpWDs`ef5(>NSJH%5yO z;nwihch6MtU^xMu(IPb}J#P@3Yy+EKn}LPut<7(4;6RYa9s(7U9QSYL3W0*ig9@>w zwpvh(*T@-30&!1S{3uc>I!taXa>Q8fzFCO+L>|Em5ZH91Kg$oc#7FV%gb|_{Vd7nD z6uOov%r=e%;4c?h7jekIdnCzbANa^ykI!aLgEGx!KO#Hm`xfnEKo#$5)P9NpRJQv* z+yG5%XtLm!;Sfcw1zisnL!9`pLWk4%d6l%N8SbBwh95tB6-_qER{7OMb)0BdkhV5j zv<0Hsj+}94JR2PXYWP9EN-$LjVv*WODb|1wINozynK)N(Fi% z%6h6Wl7ExN7G(<72`h-XK_qPNM(%e^(4aX#Sl+@b45ckvVym(JR`q+coR5iB!^F|o zmm2U>;}(f{>!l2cYSFtIfxHC~X~o&KmUuD~>To!^n$`cYsJJZyC`W0^9fo27|E$Bj z#0s37#Q;y_5TPvGIH9fm!5*5N17;gkd}rI)aLZ|AnUGmXv- zDtmuMkhuoEx+E{ zAKrak@a}w3&kT{Oc`)se$AzHklL`H9`IDK7*5MyAgGtrZ7v8&j%}l+F5S&p&a@w+e z8-IW3y1lr<$S&_aqU@Mq1W-y-lRl^y@}C^rM%mRf8e;hmHaIjL+np6eU7i^*?dB}B z9r7>v@7fo(Qfvp7v0Iu_Gx!TA82!7Bd*jriETOA^Ec$tnNO4`p z)T2RTXL|7LOl`E^rRrP0Zy}iv)PE+|sCH_`1d)og=(1?_3+?Sjs@(@q{ zlE-|DBYcc#?U$I!M6UnFO=EX{UDmVRLOXdWzT?{mc?-^#BS!zbFCpXNwzT6S792Em z7WT!Rguc{Q>(4ue5PZJ~6@CTfpxHXg0Xp z_Qx+?&HrqSRNU8oa>BmU#NhXE&TG=U&(f+>%TxA&rrXr6?|>TZ2qY$jBy=?CK|!$z znGQ7on(2^_xgP;T-yFe%L#^k_I$bQeub;s&eV@Z)hVFvQG54=Q5eKF+K7ZUPN1wg+ z>FGxG{(lzV_o_Y{hW_6CgZ)s})N?r!yH**DSt>%TH##&PTk5|I!GtOAjYn}9Y&M%* zffS)!oRi=5D%V9HUGEwYn66^@tFBt>3t}7oD${R>+oAj{qbt4+Ylfww)R($rN2TIC z61LPoo#F3GHDRT>gOV{O52fw=ftWdtz3prXocfLf5*TR$B*Hy}GV@kCokFxQi)s(& zR7&@f=&3E2)F(%-exmi42gr-QQK1n(8?Z~Gv|%EWqbU?=Gn_Akg&BdJiGq`&E2SW67jUarjSBK=WTQ8p5N+vWX>6jZtqjvHT3<;pz zMsE%$fMM8;M6d?2I(Jx@sUpRlz;FM=U({wQh3-V73+@3vGa!~W5EAQ_h<`{WQOR8@ z?&;B{uey(qo&t)9H7s@34Q^$6t|sSMhdfrH0bV)~abjxsES=ON5k9~BbToxOp@7_> zhA>hmARlE+ly;_Pc(x3Xf2&!U35u*~fUWmgop8Xkr#;(PEr3Q@iUO#StPz1y` z4hWYeC#X)1P~9o4l|#Mf%%{nssiG3|*|tc`(U_aBgl2&n1I5?gf=es4_U-yF7al1c zFn%n6JNFCX{y3%q71#1$F<-Z@z{*9t`Uw}dlGA_bC5-iqHF$P4$F-81V_Nf@=`=}opbLOV>9E3)gm9Cl6DAF;C(_n0u1jG0Ov-+dh{Iy9zI; zSt66*)qC#Wh3tEB)8W)Tv!;pMlRt|-7xl_(NZ%P@9iMlY;tO9{eum&w!7E`YK35aN&8u`JMb9CZm%m4(Y(C*G4r4na_f_#&mEcCnb^*` zm^cBfV!}s+LO7+i_8~>iRzPmC-J8)=h(IF_53nNT(kRFK`Ar>!h(%XuzCwJ)57ntjXX&uc3XRHQ7p zM9w3=&$;kOw=!;{0t42atXZFv6r!={hh6r=M>m4kxtUXQ9^{qvvt|FdGR zRlW%eD+h(Kk9~hESB*nNLRm;mYk(IIQqK8&Tk!K#kR~rUG*F`5Z&p2=&0sehiF+ht z7W{eGMK*i&RUV8jF7K17@I;}*M%JDVwuuw{H6fVu$B6qI3&Nb*>|xW7ZTQ*4(ZcT| zphthacwOUOrjd8T_Q?I8y~-wk2tRCI2ZDh3VG|v$=QUTH?@Bw1? z)sg=g#pGvqTd=wxs{{K!e&jZ(L27#IX1VWw7}iYNm#1S7KC@CWLZk@D)#cGFLH!BszOwCnuI(wMrk*VQRT5 z;-QBV;|!Udc$I+=#e+;%oNNa31W*_2Lbl=8wl8q1D6c4Z2F`{%%)bK;(WABG2c!QV5m&m2Ra zXlpO-?RR})-)V2jW{mZfJ;@NObt=MOwU7861eGR5h7fFxDSOMZ&weVc07ir*TQaHK z-VfX7z-=(52Q|WrV$&)ewaHgK<>c8Lwjb5LR!HBsv;f^PSq2iyUoTCnfem(1YfU_r@NofQfG1nG+dxn>@GAMs8DPVG(T+RV>4RJM58RcqDk;$l~}F^b!+ z-850g{#B%up~v5L!Dr7C3$5rL{h4QGiDmc-$n}XG;rufkABB1*pnB*ZpCmk}dW%W| zo2nV*!ddR*1XCG?k>y4#>T0_ll-S!fwakFc)sn3jzU9r4GoW?vS#9vcd-rbaiTd|S zrR|gu-0Qt4 zC~|VI+Gb<6W~&{y6eb`Otz$hEfz2aNZp`UrO#CX8H!$ROc*j$SZ}Hq|5CYtvqSIf= z9SEg>V=ya<;=0}v1U5ai&3evl$*=d~xb$`3eg89yr+2ru{6V@Z=arRh85y{ikEmTi zVixnjT;xaLGadA!(@=F@ATPjcxg}4(wHKLkrTdDJ*iK<47F)`IGVk1d$+JDc;sxFD z0`l?Po#@j16N}mpMH9d5IqI?Em+~4$NWzQ?jr%GS6!Mg}+3>XCbkXr@{>Em4#T(l)=`om47CBkw%_O zyqG@w0{cw->|iD~Q}5#Wyh3ojq-Fn%d)ZRn*rN(SpGW+@J~lbCBG0-SFDtP4DUTRP z0Q1CrL+@l=`iCffC6^nLmA2Y)srd`rjgo$~X^W9Tf<=Ge?!ULXHZO{GVhBrTc4`r?KDp^|9^gaLD9Kilv}fW%#Vl~6Q(SG+ii&spSCq084SlrzUR z*U&E*P&5C*x5mDEc!W_I%rZi}R6upN&ii`R5Bt#MJN;1v`JSj!2LW>jDjnep&`^y`_HlQ^euEZpjzJC%$ zJFO~tSntke$^%;+?^iXBJ1xFFu?oEM+alWUPAlhY$J{+il`BL*-%H7ZJu;anHHKC& z^ElM=1N_zH25PTp*=zv$rT_=*1<0WvR_>R?j!Gj$osyyj2|_I$V^xu2jg?eVJo`Vo z?X(e~+Zxi>uq07A9=iBS-n{to6)(uGK%Vcd8v|^yQArBqUU%4WlEwh()QiGr=?8oC z>yL>G0dqUTW7RAx{RruqybrQjVvAAe>0MlZ7Kozv(*TQT-cP7$zn?jk=ql zKzT=sm#3p}B1y}{0$plTc*vrk4}?#UZ*@ZthRla14etHz);_PV`>Xd6uT+5cq`43f*Wbmk)UY$Vy1Eb8~*qPqY4W=e5i7=+{4VRVORWu;0t^(Fs*gc7z-1 zq9!L*^}?4($*vzac^bYS`<$_;|I}r`!ibWXd6=2x+#KnUfJ=bCPk<9l7WKO{ELpjR zm@G95u7+CvYI!+Ht>Gr=YIkGYhV8LZyVyj zee5DBs5XQJ4~`s&BU~speB0Y|RRi{RA6OT!d(lK(;GE+ua`qR&vwxubg)dZow-{LN zECiEAfbzA&z}^fY!R%4U4NF5G6*#&-Za&Wo&6N8JlFT&UrFX3GWw{r%uGSrW35q<@DrCTj|!}&yz4F~mPc`i)XR1;_gt;6 zr${E5G0!Zpi0FIOqW-BMe!BI_An%Uw;K#pk8(Tg9Yh#qn02^Z-b@!WJWzf*=85_x2 zG3C#2?>c^=gAd6W`1U^ZWor~K*l{5Mx;>vJx;(=cj}02Z^pv6e(7mbL>^l|JWFxMv zJ=mgO3PZx4u#WIa3>Gn(Oil7b0?IA*mQd(<3Kiq>TYI%~Vz}#<_?;kIpY}`CZMSwP z|M62_C$(yEZ*8h8m4M;+XDc$^fEe^LB-%$S@Ql~I4{S97PGJPYTkH`(V`(UPlfRBU1nzV!9i3+2o+h_AdSPS zt^ac0Y4LVl%s1%@SNKu&&k2m+bDroUeW!fO2c~6u`nh!PJQHXv{I-C0WBn&Yu z#QG4nIMz}D54lfWY=lJX@=(Kv0DVYiijSMvM#hs0c1u($t}=xFae#h8`k(*U=>Lm8 z0dD$Mp^kwxGrBrI+uvDJlx3uK z$Et&~ro@p!McU#4jvPL5Vv70$E>3-3!2)hzQjCl zIppbn>cppW46BgkrdPM&d!r{qv8geihmZS(_#X+U^@k6%5R9Xn>P8 zvg#k!pp`r9jyup!{qJ;`I?fJfgr`?%Ad9Yhs1`~OWs^^Xn3ar?mVJI|89{YMP|$fc z!f@$nQC3t_0PV$wF!iP`xUYPOPx}I&FTnsgczeqH$nn?dA+T!-nXtWPjV3iyp_6ZZ z=-cJ1sCZ5fh@@VV{|uO{d%0_#4&4Q}2vOHOW)TQi!tyPUJ{%z2Y0-_)`_>AW$x3G; zXRI;y@7y~V`D!vG|Ib*bK;W_TY(7JIb5i~if5_Ak!pDxB2V{Cz1+m<1jK~78Q<3I* z3~t;mL1~KD_pS4nhI(|AI z*o8&yEtbg;RQy#?|le}dlPJ;-?Y>y)cr96|gp)FZQbHPx*tF_n*eS>^3 z6&^R$C;5J=G21u7oXF0;I1N+k<=qm@aL9#UxY-feaf2Da}gaTW~I(v&7GR)yF`(($xWMA!q3yR7CM7~^4uq3a}U zCgep)gmF4_RRHmk-0O#Xp+lsqK@S-8W?{t7lPU=|u}2R_=oLSee{6Y3+7(>x8RJAM z%&0!9r;IzU*!+>Y<}I*`xQZJNH-Cc{rPOv(Ttx;(uh+-ff|?( z5D9!};M}~N1qWG#`b#bLNO_m`|7my~>g%z!^8=JvEb}(KRTpL*tv;V5ayw-)sPpUOEo7J&sZ;og5!TEZ0d2r@AY7r; zhEDjaBIJIJ*@EtKNP%n{u=OQVbz@~nDyk|R8SLPD6!Jw`+jJ+jGrvezSL;6Xa2BTW z13dr)DQ~Dxd=$JAr+BssD!jO;#32-dp(VMQ|Ii&bmf^bo9$9DFwn~yI_V1T@VYA{yO z0N840TChXYZ6t`4=#Cq?Ed0VVPD$NHh`{3=_rH)){U7@&0QGtvIBJ4@2Bn4@tH4ki zi+4W3o1M{0sc54=_`^lEjl7hPSolMB1BOL*L;*DYWl1Muj9 zVd_y-;&!-kK#2NmB!663M!-zzY)9Fwf;)m8J3bj?KjpZVq760;mj5LLNt)X@egfjV zjUJtAQ(V}S>Yp1%dHZ=g-+v33ZM~L^CupO6*?c%eA66^98u4?W09HOVYb-vd2DO`s z7zT1xP*kK$eFka=AgEP94+%-T8kWxW7{$MQT+^%Q8^Z5TLfQy*bb1t2h4?Z_kULmz>NL3+Qoc4+T!LKj0GjurBnmidFC(^DDYLWd$T@Jh3Z~Mgj26SJ9k_ACOQD2%9k`t{C z^VGj`yaIbLG*?c3v(7lWZCkc66EyJ@;)U4qy5D4(QXQz}yM=CIlEuUU!|&dj7+iaA zZg%)wX_N>xwaW@FEC{`Kz|sMlgfjo}9nbKk;a|(9L3YA~;g$%+Bs@Yf8kMl>dEt;D5^;Z6}obJ;K6lC2R;B!>ZWxKa-1kcn@bI|Ixnvru2WNkkxD+W{XmW zHM^Tb@APUC(Cs%?ggwOV*KNd;gSxzlCuFp~Fo`)(^R9pecs$FD{FrYN?Pk8NQ{#2> zn%C^S8)ZgTC0-wmG7RiEp&QQ{%$IkB7k?e!YlUCxMmOkn!jZ_f2xIx&aw{wMUm{&l z?(y<*ip0MBK0E6mQIkw5piG7G%L7!Yv3c2xO|Z~tsk4bXg#wW!b6OIKCRpD`>PvSZ z=E|}$<3{M8v{0kq0_#A#sF$8~w4<@(BDO2%=Lm|b<%akwm8_uU`H_bhZH8ZT6rZO2 zcerK9Vh)CCjsfJZMkr%E>|`^S{3O~Ug2vy$Qi$)Z$`QER`|%B;P$ zhh4bMWiPhWP~w<4Drf6^j#l0H!6g3a-ne)7Rx<=ulor@B)-H$&L{4MVhaZ!#c~nxf zz1CWuO+F4S7_ATs3X>#49v>V`?xQ-vA~VBG1te#?=+8HHrzYmDB+$sE(2;#=hV=w>2+Mv$E z8uDDf|3B8RK2CTFE1*t^BSUn>1D^+WAS6s+hL!_N+l*M$hJX9)2HL4=Y70a>12P*# zUwD4U+(adQb|dHko!aewDbcz{*Bs~7{i~)(izdOHh(1`4r&Pdqmc4*6zGQX0sw55I z*(qxtNJQYK)nJ0oat@XXY76e?alkOh35-AVZ5Nfn08sk?$S-bo9fjRu#=^&$vC`m! z&16c1BeFWr0+)q4UHxmE4&{fS{g#*P*Ph`jaB3H;(nmD%EykF9^T)3LT~!AOo9nln zZ;6|08m48~paGYO!)j|$CYG))rC5KCEY0UPH(uJdvZPab2g9341ID+IRo93@aL~b$ zcl1=IP-EbeC%{(NJ6N}A3s}^>`V)}5ccv&*jht0v7$${hhF!dEapJ2X?2Vx&7i;)F zbobs%H$TK(_|9=f$!WXm>ANSXbxnZRGC~iX2RezqEk-aHX(34et#|>a*t4yRivGpn zn-|(9H$=wSYyZDF;jaDE77+s32q2K-#K$V)$;?jjRWf6*g2+8deP7Hj4^Gsa+>W96 zufP+nN*oO7JNS@awJZqSZ2jO>SUe!#HH$;8Y`j4IV%rbx<5;`)tgkLaw~eh_O7d=O zPd9Sio1%oJJ)mbv4#sJUJ;DnaIrJO9@N0~aCSN31BOR-lXw`&m$e3wC9wgnrZg$j* z)sott!s3_3AZL`?XC*jq*Einpoh*Cc+qSl2>M$gegnw2C`8AYn^>A3v`p1u0{(}qm zqC50OH2@^t@$Dm+{o2>Wy*4_q>o$uBJ!Jw&_YW>D zMJR3x5Ik2fP==kpMery>sDU6+$+ctuLGK=y+MrauilDu1PmP`!_Tuf&7cJ90cZzNo zy5;-nWnrQ6b}z%bPs*nD3-45Y8kQ_CyJ;+LadEl%#=_IzN@s&qF9)r(^r*DVC!Sq@ z%xUcwdQo*r3Vqw}p}Ym3#(!SEjVj_5p2>JWgvYV@=0_+JyC$vP?Ac7peEdP2KdTX@ zikGcI5Tc6%x97Lg7PN9|mkd3wtPB-;puV3KmDu|&NNmV0fCPj6HoR7g{N=$M!hRfF zbNY92cl8rNXjNKVaSGLSvO1GFD};VpPOkl?wXSC@3ke<}5^r*gEB{=%rEN9*XgjC18!f&ni9Wmm|K%-G~xrb zLD`>x(O}UQWw?TZe5Bp{y7=6S`kftdZ(v|i?`w2{aX)fDnILm z|46JY$Eu7fx$89u1&3H2XY?F=y9b-et}m#&83kDVd;!_%^5TcX(uxfN*v}|xbKbAJ zVPI+lfxkD0w2ZtEAqR+vZjJyuZZNexz06zYL4@(vF>h+VdG!C_M?fCk{_|Ht_fQ_2 zkxG*m?sz}MNR^#c;8+dJ61w}-INk%fV$m4^tVeo>7h@?Wu@JfD^6%{MJP=R|S!Ga5 za}qSyOS00CO9@`IhFs#2^`O*wN^zTO_PsNK4ZgdvL=VeH?ZME^7)Ig?p?zW&$=KcA zuGFhuCP^bz@Bn4Y=XT9?(lEnK!c82$jb`qA?XqXXbFBn)k176+@~m%&#(nUZbO-w} z{-ps&XCl5vZQt;~&e~nwQ4(SRXdD(ygqI4s^eL z>q8FyJG*tQBuCKpCgWm7CWKfZ)S>$rb8_Wh7)2-+u?@X}>fqBM5Jl-Qd}#O=@BL;C zB+gMdG+>@X3up#T##6F`F5Z7ZXI~oZoS!kd(2{c$P&6JNeBnRN+AJFq3ZBv6ZZB_A z+nJ6K&Ws{2iIdD^w6k9d{<)X(y+=>n*30va{F~>>cXuOjI~F-RYjD^-yU>TfTxLUs zV2b^Or{zUDmnH(5O8IanpXEkPt$_C%&W6uQB~Zn%Zio^0G=JoIF2;uqwSWe#V)mJl zZUQ`mOsTl^k$Ken;Z+m9{}PurCloiS7-SxEmYn`fA^@X9tUw3YSNW~97rWBiiwCTL zq4GXW3JGqEQ)4al2|+sze8D?ZmLy|2OdN50apBwA0k@)4gmE~;?}INj>Ro!#B_#fo z84U4YMsRjThy3(ieTkG2506a!_a9@9OTtdN-_G--vt#asEJ|jdd}lO6_0)Nw8!01S zzWC25YqPmwFzPKPh}F}(p(QDyAs^CI285Ie=mdK@fw15h?P7Kewqy&#d@TKcnSvIc zP2Hf$z0t&Yf&@BXn>(O<2;Ml&PtE(mQJF5b8flOIRa4a~CrkXb6}C&+k};cjDuE&| zE4>#oU`KTtGxwiRlpI<3I~>g<&zqmeT3e4BNgLDUo3bgeAA=#B`{-6WHP ztq*cYMoDN}r`(cGYsu_MEL{^hJe>WwX@^7t@PzVSu-Jj!)t1mn;h85-hR9H#VZa<9 zKt7WO%JLh2L}(eFA*uQuc2zlc?OcbRUa{a=_@4nKX`j-nbFU*3Wrea$0oh}C?8VvJ zA z$_Ztys%?yWk^FHF%K`PD{g{+cd}?9!w`4+bD8F3pAlTe=5d9%=05YDFSGzJXZ;u)) zWMGTewm6qmt#8bKAEkz*{Ys8O7qGfaW+0Dhve*9Nt`EeVyksGN)AvMgQ zc#ahgxi&%x8{$-2;wW2y1)dzPIX@34afRrFFWcs9| zZ%$wnLlT(Pwm!KB7qbg&O&vPw9bpH_UIS0ed}p<$PZ*~S!qevXlS3ADz^-gw=$rhX zoCGo{gB|HUo_Fzx{_EG?z}&J`>!@ADYzf!xPrTn3d*wRl{P~CX?xh%Hon4Ef)1x`LY=Bv(Is~vkUO~&a#V+HV&e8>-AZg#y57foF= z8;fHt&yCED50|)A{U+gA5?{no!)v2EQDBYLwwm3-ffJQQhE^fW3%JUJ;Wu|cp3ude zOiaMqKK9p(i(RngBp9ygyo6s<%*p!~XZ)2Bj4Sm?e+-W|ei4v$q> z1l!3N9r7@@)7>&;hRccYB_D=rilWJLNg*S{QKQG%i%mJ}m6w+m2+!$8Tq>niP3NR( zkpcqns{m&Tx0;ijFOfrxod)tKx`9e&S5uXME@%6z~h|5r( ze6HzGmQ|I@Qu)TEv1QmbD-Lg;SLge5`-xuTE5Q=1o#N$knGW{Ul|N`WD{8A(A8HMy z#u_IU?-v&btt}USi}tufIX&`Fee5B@pES=fky4$s%m^S565w#ocQ2;fI%x-z4OvLr zuG?w^vwv&O%|>e$j03u`jdJcMsLjyrf3dT%HGojdB-X9(o}AcIy8=j9RNJxmY2Jwk z`&He@6xc+eT`_9SA$P8O7QLIx8Q|^K-gCk4y>+j#QZGbhF4S&!ul&Nm*l)cK#>lv_ z=re4aV-Vkm61%Os@xH;-*aL3}`ei$j*enDeKgNzw#r+iXY!aB*`*CJ|chyn5#0BR* zQN4xRM}HaXx_|BWKf;yM??LT?68no=Ul3+-q5BKLBiD<5aw1!$%-k;xxmEtm8}qhT zXO8F=)fsP}^wkB0xB&dC1aP618I^u?twk#zWLc0;Tz~6OgXtnHX-k4*d zMy&>vK!MCwgO}Aj3OG}{*}Nd)Yta5>eDJt;yEg_km>20%{sy;!bd2i`sm9&g_)LJV zds0Ta*>b5zh)SF#Fi{uA=ym6W^9Mf-WN*WJ+%TEfMo!K>XUz&X{uUOpet9#nS(g3s zO&gg#LFQ+AmZ;XUeLLYF`={HC*}Gdijys&4^%^qEC~$2l$K@I42Vu*ry822Mcm zO=OPKH;;%y*>o7>Dn_0Cq1d6!xtGW3S9bw%dBX<9LGR0f51L>@9vRW+=Xu^Mxev|x zDc@a@Nn(F+Ww{6LQ)0BlY)w9ykfdcL1-}{UtsCH6{|f6;*&AInc_J9{LB{|g*v*gm zpo682@_nQXH(?M>VJB_#?7lR15BKl-8%*hTjJmE~24orkV4p6Rh0_*Wt{m!4P!I?& zd3)o8ckvWy<7toN>E6AV$F7@-5^l>o`ab~rujPVD#BB=!Fj$^Cal6RP(wC-jmKq+B zj`(`iSR9e%1}i$ns1nQF9*9<%*|1@+*wK%A&vN!Bi>Zz^zC%eIH zTY7W?N#$O>XJC+E?FE0Lb$5t6Y(->61-Tz(->6D{5UnxO^N927hKJPqC`^&3+nA|* zE$w4*H>B=cUd_;DB?*1}ArI}1HA>V1u1xG@(|LQ!FGly5tzda3%qFBWSQ?Rw2ba{b zL5@S#A@=GNcgST(6|s%=2XfB}XJdp0QfW{JRo$0Cs_v^?fmD6#XA-n}Zoe^8zd?`N zS_>5!R^_hKpk{G^6F{`P!1sev1>5&dGChGzFZH!jlFV?2y(C;=;qzM4v%h(m%mo+( z60mO4{yMiJ4m_f+{|-4G*XH1e3oG z1}=73qG+TQV*A)6zLqZMkskQ_*xAGG#=piTK8^EPim*>pGjh$SlEp%UL}P_Eblb!k zczNra)}L*eBd_{HVilV;o{R^u2Tjz%)6?qt+v>i

|7tBL>)-GmlDn;Aeh987`fPI1U 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

_$>>0=`MfJjc`*SVy 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 Date: Mon, 31 Jul 2023 14:38:32 +0800 Subject: [PATCH 02/27] Add changenote. --- changes/2058.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2058.feature.rst diff --git a/changes/2058.feature.rst b/changes/2058.feature.rst new file mode 100644 index 0000000000..e50fbe1a5f --- /dev/null +++ b/changes/2058.feature.rst @@ -0,0 +1 @@ +Window and MainWindow now have 100% test coverage, and complete API documentation. From d2eec8110a26c8c3b5cb615f0be5478d1b46afa2 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 1 Aug 2023 09:39:21 +0800 Subject: [PATCH 03/27] Migrated core tests to pytest. --- changes/2058.removal.1.rst | 1 + core/src/toga/app.py | 1 - core/src/toga/command.py | 3 + core/src/toga/handlers.py | 7 +- core/src/toga/window.py | 47 ++- core/tests/test_window.py | 666 ++++++++++++++------------------- docs/reference/api/window.rst | 7 +- dummy/src/toga_dummy/window.py | 13 +- examples/window/window/app.py | 3 - 9 files changed, 342 insertions(+), 406 deletions(-) create mode 100644 changes/2058.removal.1.rst diff --git a/changes/2058.removal.1.rst b/changes/2058.removal.1.rst new file mode 100644 index 0000000000..b0775e2a9a --- /dev/null +++ b/changes/2058.removal.1.rst @@ -0,0 +1 @@ +Windows no longer need to be explicitly added to the app's window list. When a window is shown, it will be automatically added to the windows for the currently running app. diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 2f09e33476..343efc25ad 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -494,7 +494,6 @@ def main_window(self) -> MainWindow: @main_window.setter def main_window(self, window: MainWindow) -> None: self._main_window = window - self.windows += window self._impl.set_main_window(window) @property diff --git a/core/src/toga/command.py b/core/src/toga/command.py index 8a1f1cd2d9..e8ff62a632 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -392,6 +392,9 @@ def add(self, *commands): if self.on_change: self.on_change() + def __len__(self): + return len(self._commands) + def __iter__(self): prev_cmd = None for cmd in sorted(self._commands): diff --git a/core/src/toga/handlers.py b/core/src/toga/handlers.py index c434a3454b..5d22ed279b 100644 --- a/core/src/toga/handlers.py +++ b/core/src/toga/handlers.py @@ -99,7 +99,12 @@ def _handler(widget, *args, **kwargs): else: # A dummy no-op handler def _handler(widget, *args, **kwargs): - pass + try: + if cleanup: + cleanup(interface, None) + except Exception as e: + print("Error in handler cleanup:", e, file=sys.stderr) + traceback.print_exc() _handler._raw = None diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 56d65a86cf..1e3502cef6 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -91,9 +91,9 @@ def __init__( self._content = None self._is_full_screen = False - self.resizeable = resizeable - self.closeable = closeable - self.minimizable = minimizable + self._resizeable = resizeable + self._closeable = closeable + self._minimizable = minimizable self.factory = get_platform_factory() self._impl = getattr(self.factory, self._WINDOW_CLASS)( @@ -141,7 +141,22 @@ def title(self, title: str) -> None: if not title: title = "Toga" - self._impl.set_title(title) + self._impl.set_title(str(title).split("\n")[0]) + + @property + def resizeable(self) -> bool: + """Is the window resizeable?""" + return self._resizeable + + @property + def closeable(self) -> bool: + """Can the window be closed by a user action?""" + return self._closeable + + @property + def minimizable(self) -> bool: + """Can the window be minimized?""" + return self._minimizable @property def toolbar(self) -> CommandSet: @@ -196,19 +211,24 @@ def position(self, position: tuple[int, int]) -> None: self._impl.set_position(position) def show(self) -> None: - """Show the window, if hidden. + """Show the window, if hidden.""" - :raises ValueError: if the window hasn't been associated with an""" if self.app is None: - raise ValueError("Can't show a window that doesn't have an associated app") + # Needs to be a late import to avoid circular dependencies. + from toga import App + + App.app.windows += self + self._impl.show() def hide(self) -> None: - """Hide window, if shown. - - :raises ValueError: if the window hasn't been associated with an app.""" + """Hide window, if shown.""" if self.app is None: - raise ValueError("Can't hide a window that doesn't have an associated app") + # Needs to be a late import to avoid circular dependencies. + from toga import App + + App.app.windows += self + self._impl.hide() @property @@ -254,7 +274,7 @@ def on_close(self) -> OnCloseHandler: @on_close.setter def on_close(self, handler: OnCloseHandler | None) -> None: def cleanup(window: Window, should_close: bool) -> None: - if should_close: + if should_close or handler is None: window.close() self._on_close = wrapped_handler(self, handler, cleanup=cleanup) @@ -263,7 +283,8 @@ def close(self) -> None: """Close the window. This *does not* invoke the ``on_close`` handler; the window will be immediately - and unconditionally closed.""" + and unconditionally closed. + """ self.app.windows -= self self._impl.close() diff --git a/core/tests/test_window.py b/core/tests/test_window.py index 83d689ecb7..c7ed6ad506 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -1,379 +1,291 @@ -from pathlib import Path -from unittest.mock import MagicMock, Mock, call, patch +from unittest.mock import Mock + +import pytest import toga -from toga.command import CommandSet -from toga.widgets.base import WidgetRegistry -from toga_dummy.utils import TestCase - - -class TestWindow(TestCase): - def setUp(self): - super().setUp() - self.window = toga.Window() - self.app = toga.App("test_name", "id.app") - - def test_window_widgets_registry_on_constructor(self): - self.assertTrue(isinstance(self.window.widgets, WidgetRegistry)) - self.assertEqual(len(self.window.widgets), 0) - - def test_show_is_not_called_in_constructor(self): - self.assertActionNotPerformed(self.window, "show") - - def test_show_raises_error_when_app_not_set(self): - with self.assertRaisesRegex( - AttributeError, "^Can't show a window that doesn't have an associated app$" - ): - self.window.show() - - def test_window_show_with_app_set(self): - self.window.app = self.app - self.window.show() - self.assertActionPerformed(self.window, "show") - self.assertTrue(self.window.visible) - self.assertValueSet(self.window, "visible", True) - - def test_hide_raises_error_when_app_not_set(self): - with self.assertRaisesRegex( - AttributeError, "^Can't hide a window that doesn't have an associated app$" - ): - self.window.hide() - - def test_window_hide_with_app_set(self): - self.window.app = self.app - self.window.hide() - self.assertActionPerformed(self.window, "hide") - self.assertFalse(self.window.visible) - self.assertValueSet(self.window, "visible", False) - - def test_window_show_by_setting_visible_to_true(self): - self.window.app = self.app - self.window.visible = True - self.assertActionPerformed(self.window, "show") - self.assertTrue(self.window.visible) - self.assertValueSet(self.window, "visible", True) - - def test_window_show_by_setting_visible_to_false(self): - self.window.app = self.app - self.window.visible = False - self.assertActionPerformed(self.window, "hide") - self.assertFalse(self.window.visible) - self.assertValueSet(self.window, "visible", False) - - def test_set_window_application_twice(self): - self.assertIsNotNone(self.window.id) - new_app = toga.App("error_name", "id.error") - self.window.app = self.app - with self.assertRaisesRegex( - Exception, "^Window is already associated with an App$" - ): - self.window.app = new_app - - def test_window_title(self): - # Assert default value - title = self.window.title - self.assertEqual(title, "Toga") - self.assertValueGet(self.window, "title") - - # Set a new window title - self.window.title = "New title" - self.assertValueSet(self.window, "title", "New title") - - # New window title can be retrieved - title = self.window.title - self.assertValueGet(self.window, "title") - self.assertEqual(title, "New title") - - # Set a default window title - self.window.title = None - self.assertValueSet(self.window, "title", "Toga") - - # New window title can be retrieved - title = self.window.title - self.assertValueGet(self.window, "title") - self.assertEqual(title, "Toga") - - def test_toolbar(self): - toolbar = self.window.toolbar - self.assertIsInstance(toolbar, CommandSet) - - def test_set_content_without_app(self): - content = MagicMock() - - self.window.content = content - self.assertEqual(content.window, self.window) - self.assertIsNone(content.app) - - def test_set_content_with_app(self): - content = MagicMock() - - self.window.app = self.app - self.window.content = content - - self.assertEqual(content.window, self.window) - self.assertEqual(content.app, self.app) - - def test_set_app_after_content(self): - content = MagicMock() - - self.window.content = content - self.window.app = self.app - - self.assertEqual(content.window, self.window) - self.assertEqual(content.app, self.app) - - def test_set_app_adds_window_widgets_to_app(self): - id0, id1, id2, id3 = "id0", "id1", "id2", "id3" - widget1, widget2, widget3 = ( - toga.Label(id=id1, text="label 1"), - toga.Label(id=id2, text="label 1"), - toga.Label(id=id3, text="label 1"), - ) - content = toga.Box(id=id0, children=[widget1, widget2, widget3]) - - self.window.content = content - - # The window has widgets in it's repository - self.assertEqual(len(self.window.widgets), 4) - self.assertEqual(self.window.widgets[id0], content) - self.assertEqual(self.window.widgets[id1], widget1) - self.assertEqual(self.window.widgets[id2], widget2) - self.assertEqual(self.window.widgets[id3], widget3) - - # The app doesn't know about the widgets - self.assertEqual(len(self.app.widgets), 0) - - # Assign the window to the app - self.window.app = self.app - - # The window's content widgets are now known to the app. - self.assertEqual(len(self.app.widgets), 4) - self.assertEqual(self.app.widgets[id0], content) - self.assertEqual(self.app.widgets[id1], widget1) - self.assertEqual(self.app.widgets[id2], widget2) - self.assertEqual(self.app.widgets[id3], widget3) - - def test_size(self): - # Add some content - content = MagicMock() - self.window.content = content - - # Confirm defaults - self.assertEqual(self.window.size, (640, 480)) - self.assertValueGet(self.window, "size") - - content.refresh.assert_called_once_with() - - def test_set_size(self): - # Add some content - content = MagicMock() - self.window.content = content - - # A new size can be assigned - new_size = (1200, 40) - self.window.size = new_size - self.assertValueSet(self.window, "size", new_size) - - # Side effect of setting window size is a refresh on window content - self.assertEqual(content.refresh.call_args_list, [call(), call()]) - - # New size can be retrieved - self.assertEqual(self.window.size, new_size) - self.assertValueGet(self.window, "size") - - def test_position(self): - # Confirm defaults - self.assertEqual(self.window.position, (100, 100)) - - # A new position can be assigned - new_position = (40, 79) - self.window.position = new_position - self.assertValueSet(self.window, "position", new_position) - - # New position can be retrieved - self.assertEqual(self.window.position, new_position) - self.assertValueGet(self.window, "position") - - def test_full_screen_set(self): - self.assertFalse(self.window.full_screen) - with patch.object(self.window, "_impl"): - self.window.full_screen = True - self.assertTrue(self.window.full_screen) - self.window._impl.set_full_screen.assert_called_once_with(True) - - def test_on_close(self): - with patch.object(self.window, "_impl"): - self.app.windows += self.window - self.assertIsNone(self.window.on_close._raw) - - # set a new callback - def callback(window, **extra): - return f"called {type(window)} with {extra}" - - self.window.on_close = callback - self.assertEqual(self.window.on_close._raw, callback) - self.assertEqual( - self.window.on_close(None, a=1), - "called with {'a': 1}", - ) - - def test_on_close_at_create(self): - def callback(window, **extra): - return f"called {type(window)} with {extra}" - - window = toga.Window(on_close=callback) - self.app.windows += window - - self.assertEqual(window.on_close._raw, callback) - self.assertEqual( - window.on_close(None, a=1), - "called with {'a': 1}", - ) - - self.assertActionPerformed(window, "close") - - def test_close(self): - # Ensure the window is associated with an app - self.app.windows += self.window - with patch.object(self.window, "_impl"): - self.window.close() - self.window._impl.close.assert_called_once_with() - - def test_question_dialog(self): - title = "question_dialog_test" - message = "sample_text" - - self.window.question_dialog(title, message) - - self.assertActionPerformedWith( - self.window, "question_dialog", title=title, message=message - ) - - def test_confirm_dialog(self): - title = "confirm_dialog_test" - message = "sample_text" - - self.window.confirm_dialog(title, message) - - self.assertActionPerformedWith( - self.window, "confirm_dialog", title=title, message=message - ) - - def test_error_dialog(self): - title = "error_dialog_test" - message = "sample_text" - - self.window.error_dialog(title, message) - - self.assertActionPerformedWith( - self.window, "error_dialog", title=title, message=message - ) - - def test_info_dialog(self): - title = "info_dialog_test" - message = "sample_text" - - self.window.info_dialog(title, message) - - self.assertActionPerformedWith( - self.window, "info_dialog", title=title, message=message - ) - - def test_stack_trace_dialog(self): - title = "stack_trace_dialog_test" - message = "sample_text" - content = "sample_content" - retry = True - - self.window.stack_trace_dialog(title, message, content, retry) - - self.assertActionPerformedWith( - self.window, - "stack_trace_dialog", - title=title, - message=message, - content=content, - retry=retry, - ) - - def test_save_file_dialog_with_initial_directory(self): - title = "save_file_dialog_test" - suggested_filename = "/path/to/initial_filename.doc" - file_types = ["test"] - - self.window.save_file_dialog(title, suggested_filename, file_types) - - self.assertActionPerformedWith( - self.window, - "save_file_dialog", - title=title, - filename="initial_filename.doc", - initial_directory=Path("/path/to"), - file_types=file_types, - ) - - def test_save_file_dialog_with_self_as_initial_directory(self): - title = "save_file_dialog_test" - suggested_filename = "./initial_filename.doc" - file_types = ["test"] - - self.window.save_file_dialog(title, suggested_filename, file_types) - - self.assertActionPerformedWith( - self.window, - "save_file_dialog", - title=title, - filename="initial_filename.doc", - initial_directory=None, - file_types=file_types, - ) - - def test_open_file_dialog(self): - title = "title_test" - initial_directory = "/path/to/initial_directory" - file_types = ["test"] - multiselect = True - - self.window.open_file_dialog(title, initial_directory, file_types, multiselect) - - self.assertActionPerformedWith( - self.window, - "open_file_dialog", - title=title, - initial_directory=Path(initial_directory), - file_types=file_types, - multiselect=multiselect, - ) - - def test_select_folder_dialog(self): - title = "" - initial_directory = "/path/to/initial_directory" - multiselect = True - - self.window.select_folder_dialog(title, initial_directory, multiselect) - - self.assertActionPerformedWith( - self.window, - "select_folder_dialog", - title=title, - initial_directory=Path(initial_directory), - multiselect=multiselect, - ) - - def test_window_set_content_once(self): - content = Mock() - self.window.content = content - - self.assertEqual(content.window, self.window) - - self.assertActionPerformed(self.window, "set content") - - def test_window_set_content_twice(self): - content1, content2 = Mock(), Mock() - self.window.content = content1 - self.window.content = content2 - - self.assertEqual(content1.window, None) - self.assertEqual(content2.window, self.window) - - self.assertActionPerformed(self.window, "set content") +from toga_dummy.utils import ( + assert_action_not_performed, + assert_action_performed, + assert_action_performed_with, +) + + +@pytest.fixture +def app(): + return toga.App("Test App", "org.beeware.toga.window") + + +@pytest.fixture +def window(): + return toga.Window() + + +def test_window_created(): + "A Window can be created with minimal arguments" + window = toga.Window() + + assert window.app is None + assert window.content is None + + assert window._impl.interface == window + assert_action_performed(window, "create Window") + + # 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 == (100, 100) + assert window.size == (640, 480) + assert window.resizeable + assert window.closeable + assert window.minimizable + assert len(window.toolbar) == 0 + assert window.on_close._raw is None + + +def test_window_created_explicit(): + "Explicit arguments at construction are stored" + on_close_handler = Mock() + + window = toga.Window( + id="my-window", + title="My Window", + position=(10, 20), + size=(200, 300), + resizeable=False, + closeable=False, + minimizable=False, + on_close=on_close_handler, + ) + + assert window.app is None + assert window.content is None + + assert window._impl.interface == window + assert_action_performed(window, "create Window") + + assert window.id == "my-window" + assert window.title == "My Window" + assert window.position == (10, 20) + assert window.size == (200, 300) + assert not window.resizeable + assert not window.closeable + assert not window.minimizable + assert len(window.toolbar) == 0 + assert window.on_close._raw == on_close_handler + + +def test_set_app(window, app): + """A window can be assigned to an app""" + assert window.app is None + + window.app = app + + assert window.app == app + + app2 = toga.App("Test App 2", "org.beeware.toga.window2") + with pytest.raises(ValueError, match=r"Window is already associated with an App"): + window.app = app2 + + +def test_set_app_with_content(window, app): + """If a window has content, the content is assigned to the app""" + content = toga.Box() + window.content = content + + assert window.app is None + assert content.app is None + + window.app = app + + assert window.app == app + assert content.app == app + + +@pytest.mark.parametrize( + "value, expected", + [ + ("New Text", "New Text"), + ("", "Toga"), + (None, "Toga"), + (12345, "12345"), + ("Contains\nnewline", "Contains"), + ], +) +def test_title(window, value, expected): + """The title of the window can be changed""" + window.title = value + assert window.title == expected + + +def test_change_content(window, app): + """The content of a window can be changed""" + window.app = app + assert window.content is None + assert window.app == app + + # Set the content of the window + content1 = toga.Box() + window.content = content1 + + # The content has been assigned and refreshed + assert content1.app == app + assert content1.window == window + assert_action_performed_with(window, "set content", widget=content1._impl) + assert_action_performed(content1, "refresh") + + # Set the content of the window to something new + content2 = toga.Box() + window.content = content2 + + # The content has been assigned and refreshed + assert content2.app == app + assert content2.window == window + assert_action_performed_with(window, "set content", widget=content2._impl) + assert_action_performed(content2, "refresh") + + # The original content has been removed + assert content1.window is None + + +def test_set_position(window): + """The position of the window can be set.""" + window.position = (123, 456) + + assert window.position == (123, 456) + + +def test_set_size(window): + """The size of the window can be set.""" + window.size = (123, 456) + + assert window.size == (123, 456) + + +def test_set_size_with_content(window): + """The size of the window can be set.""" + content = toga.Box() + window.content = content + + window.size = (123, 456) + + assert window.size == (123, 456) + assert_action_performed(content, "refresh") + + +def test_show_hide(window, app): + """The window can be shown and hidden.""" + assert window.app is None + + window.show() + + # The window has been assigned to the app, and is visible + assert window.app == app + assert window in app.windows + assert_action_performed(window, "show") + assert window.visible + + # Hide with an explicit call + window.hide() + + # Window is still assigned to the app, but is not visible + assert window.app == app + assert window in app.windows + assert_action_performed(window, "hide") + assert not window.visible + + +def test_hide_show(window, app): + """The window can be hidden then shown.""" + assert window.app is None + + window.hide() + + # The window has been assigned to the app, and is not visible + assert window.app == app + assert window in app.windows + assert_action_performed(window, "hide") + assert not window.visible + + # Show with an explicit call + window.show() + + # Window is still assigned to the app, but is not visible + assert window.app == app + assert window in app.windows + assert_action_performed(window, "show") + assert window.visible + + +def test_visibility(window, app): + """The window can be shown and hidden using the visible property.""" + assert window.app is None + + window.visible = True + + # The window has been assigned to the app, and is visible + assert window.app == app + assert window in app.windows + assert_action_performed(window, "show") + assert window.visible + + # Hide with an explicit call + window.visible = False + + # Window is still assigned to the app, but is not visible + assert window.app == app + assert window in app.windows + assert_action_performed(window, "hide") + assert not window.visible + + +def test_close_no_handler(window, app): + """A window without a close handler can be closed""" + window.show() + assert window.app == app + assert window in app.windows + + # Close the window + window._impl.simulate_close() + + # Window has been closed, and is no longer in the app's list of windows. + assert window.app == app + assert window not in app.windows + assert_action_performed(window, "close") + + +def test_close_sucessful_handler(window, app): + """A window with a successful close handler can be closed""" + on_close_handler = Mock(return_value=True) + window.on_close = on_close_handler + + window.show() + assert window.app == app + assert window in app.windows + + # Close the window + window._impl.simulate_close() + + # Window has been closed, and is no longer in the app's list of windows. + assert window.app == app + assert window not in app.windows + assert_action_performed(window, "close") + on_close_handler.assert_called_once_with(window) + + +def test_close_rejected_handler(window, app): + """A window can have a close handler that rejects closing""" + on_close_handler = Mock(return_value=False) + window.on_close = on_close_handler + + window.show() + assert window.app == app + assert window in app.windows + + # Close the window + window._impl.simulate_close() + + # Window has been closed, and is no longer in the app's list of windows. + assert window.app == app + assert window in app.windows + assert_action_not_performed(window, "close") + on_close_handler.assert_called_once_with(window) diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index 167c481ea4..4c3a0f57aa 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -21,9 +21,9 @@ A window is the top-level container that the operating system uses to contain wi The window has content, which will usually be a container widget of some kind. A window may also have other decorations, such as a title bar or toolbar. -By default, a window is not visible. A window must be associated with an application -before it can be displayed. The content of the window can be changed by re-assigning the -content of the window to a new widget. +By default, a window is not visible. When the window is shown, it will be associated +with the currently active application. The content of the window can be changed by +re-assigning the content of the window to a new widget. .. code-block:: python @@ -31,7 +31,6 @@ content of the window to a new widget. window = toga.Window() window.content = toga.Box(children=[...]) - toga.App.app.windows += window window.show() # Change the window's content to something new diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index ed5d263c3e..adb4f6b642 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -1,4 +1,4 @@ -from .utils import LoggedObject, not_required, not_required_on +from .utils import LoggedObject, not_required @not_required @@ -37,9 +37,11 @@ def refreshed(self): self.content.refresh() +@not_required class Window(LoggedObject): def __init__(self, interface, title, position, size): super().__init__() + self._action("create Window") self.interface = interface self.container = Container() @@ -50,8 +52,6 @@ def __init__(self, interface, title, position, size): def create_toolbar(self): self._action("create toolbar") - # Some platforms inherit this method from a base class. - @not_required_on("android", "winforms") def set_content(self, widget): self.container.content = widget self._action("set content", widget=widget) @@ -91,11 +91,10 @@ def get_visible(self): def close(self): self._action("close") + self._set_value("visible", False) - @not_required_on("mobile") def set_full_screen(self, is_full_screen): self._set_value("is_full_screen", is_full_screen) - @not_required - def toga_on_close(self): - self._action("handle Window on_close") + def simulate_close(self): + self.interface.on_close(None) diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 2e770f561d..85effd9e3d 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -43,7 +43,6 @@ def do_new_windows(self, widget, **kwargs): non_resize_window.content = toga.Box( children=[toga.Label("This window is not resizable")] ) - self.app.windows += non_resize_window non_resize_window.show() non_close_window = toga.Window( @@ -55,7 +54,6 @@ def do_new_windows(self, widget, **kwargs): non_close_window.content = toga.Box( children=[toga.Label("This window is not closeable")] ) - self.app.windows += non_close_window non_close_window.show() no_close_handler_window = toga.Window( @@ -66,7 +64,6 @@ def do_new_windows(self, widget, **kwargs): no_close_handler_window.content = toga.Box( children=[toga.Label("This window has no close handler")] ) - self.app.windows += no_close_handler_window no_close_handler_window.show() async def do_current_window_cycling(self, widget, **kwargs): From 7ee636910274be5dedf5c2b2822ec11984f6e956 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 1 Aug 2023 11:10:25 +0800 Subject: [PATCH 04/27] Complete coverage of core dialogs. --- core/src/toga/window.py | 65 ++++- core/tests/test_app.py | 28 +- core/tests/test_handlers.py | 88 +++--- core/tests/test_window.py | 477 +++++++++++++++++++++++++++++++- dummy/src/toga_dummy/app.py | 19 +- dummy/src/toga_dummy/dialogs.py | 64 +++-- dummy/src/toga_dummy/window.py | 2 +- pyproject.toml | 3 +- 8 files changed, 643 insertions(+), 103 deletions(-) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 1e3502cef6..b2a67b5b5e 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from builtins import id as identifier from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, overload @@ -503,8 +504,9 @@ def open_file_dialog( title: str, initial_directory: Path | str | None = None, file_types: list[str] | None = None, - multiselect: Literal[False] = False, + multiple_select: Literal[False] = False, on_result: DialogResultHandler[Path | None] | None = None, + multiselect=None, # DEPRECATED ) -> Dialog: ... @@ -514,8 +516,9 @@ def open_file_dialog( title: str, initial_directory: Path | str | None = None, file_types: list[str] | None = None, - multiselect: Literal[True] = True, + multiple_select: Literal[True] = True, on_result: DialogResultHandler[list[Path] | None] | None = None, + multiselect=None, # DEPRECATED ) -> Dialog: ... @@ -525,8 +528,9 @@ def open_file_dialog( title: str, initial_directory: Path | str | None = None, file_types: list[str] | None = None, - multiselect: bool = False, + multiple_select: bool = False, on_result: DialogResultHandler[list[Path] | Path | None] | None = None, + multiselect=None, # DEPRECATED ) -> Dialog: ... @@ -535,8 +539,9 @@ def open_file_dialog( title: str, initial_directory: Path | str | None = None, file_types: list[str] | None = None, - multiselect: bool = False, + multiple_select: bool = False, on_result: DialogResultHandler[list[Path] | Path | None] | None = None, + multiselect=None, # DEPRECATED ) -> Dialog: """Ask the user to select a file (or files) to open. @@ -547,22 +552,36 @@ def open_file_dialog( If ``None``, use the default location provided by the operating system (which will often be the last used location) :param file_types: A list of strings with the allowed file extensions. - :param multiselect: If True, the user will be able to select multiple + :param multiple_select: If True, the user will be able to select multiple files; if False, the selection will be restricted to a single file. :param on_result: A callback that will be invoked when the user selects an option on the dialog. + :param multiselect: **DEPRECATED** Use ``multiple_select``. :returns: An awaitable Dialog object. The Dialog object returns - a list of ``Path`` objects if ``multiselect`` is ``True``, or a single + a list of ``Path`` objects if ``multiple_select`` is ``True``, or a single ``Path`` otherwise. Returns ``None`` if the open operation is cancelled by the user. """ + ###################################################################### + # 2023-08: Backwards compatibility + ###################################################################### + if multiselect is not None: + warnings.warn( + "open_file_dialog(multiselect) has been renamed multiple_select", + DeprecationWarning, + ) + multiple_select = multiselect + ###################################################################### + # End Backwards compatibility + ###################################################################### + dialog = Dialog(self) self.factory.dialogs.OpenFileDialog( dialog, title, initial_directory=Path(initial_directory) if initial_directory else None, file_types=file_types, - multiselect=multiselect, + multiple_select=multiple_select, on_result=wrapped_handler(self, on_result), ) return dialog @@ -572,8 +591,9 @@ def select_folder_dialog( self, title: str, initial_directory: Path | str | None = None, - multiselect: Literal[False] = False, + multiple_select: Literal[False] = False, on_result: DialogResultHandler[Path | None] | None = None, + multiselect=None, # DEPRECATED ) -> Dialog: ... @@ -582,8 +602,9 @@ def select_folder_dialog( self, title: str, initial_directory: Path | str | None = None, - multiselect: Literal[True] = True, + multiple_select: Literal[True] = True, on_result: DialogResultHandler[list[Path] | None] | None = None, + multiselect=None, # DEPRECATED ) -> Dialog: ... @@ -592,8 +613,9 @@ def select_folder_dialog( self, title: str, initial_directory: Path | str | None = None, - multiselect: bool = False, + multiple_select: bool = False, on_result: DialogResultHandler[list[Path] | Path | None] | None = None, + multiselect=None, # DEPRECATED ) -> Dialog: ... @@ -601,8 +623,9 @@ def select_folder_dialog( self, title: str, initial_directory: Path | str | None = None, - multiselect: bool = False, + multiple_select: bool = False, on_result: DialogResultHandler[list[Path] | Path | None] | None = None, + multiselect=None, # DEPRECATED ) -> Dialog: """Ask the user to select a directory/folder (or folders) to open. @@ -612,21 +635,35 @@ def select_folder_dialog( :param initial_directory: The initial folder in which to open the dialog. If ``None``, use the default location provided by the operating system (which will often be "last used location") - :param multiselect: If True, the user will be able to select multiple + :param multiple_select: If True, the user will be able to select multiple files; if False, the selection will be restricted to a single file/ :param on_result: A callback that will be invoked when the user selects an option on the dialog. + :param multiselect: **DEPRECATED** Use ``multiple_select``. :returns: An awaitable Dialog object. The Dialog object returns - a list of ``Path`` objects if ``multiselect`` is ``True``, or a single + a list of ``Path`` objects if ``multiple_select`` is ``True``, or a single ``Path`` otherwise. Returns ``None`` if the open operation is cancelled by the user. """ + ###################################################################### + # 2023-08: Backwards compatibility + ###################################################################### + if multiselect is not None: + warnings.warn( + "select_folder_dialog(multiselect) has been renamed multiple_select", + DeprecationWarning, + ) + multiple_select = multiselect + ###################################################################### + # End Backwards compatibility + ###################################################################### + dialog = Dialog(self) self.factory.dialogs.SelectFolderDialog( dialog, title, initial_directory=Path(initial_directory) if initial_directory else None, - multiselect=multiselect, + multiple_select=multiple_select, on_result=wrapped_handler(self, on_result), ) return dialog diff --git a/core/tests/test_app.py b/core/tests/test_app.py index e6ff1b747d..0f646be4cf 100644 --- a/core/tests/test_app.py +++ b/core/tests/test_app.py @@ -1,4 +1,5 @@ -from unittest.mock import MagicMock +import asyncio +from unittest.mock import Mock import toga from toga.widgets.base import WidgetRegistry @@ -13,7 +14,7 @@ def setUp(self): self.app_id = "org.beeware.test-app" self.id = "dom-id" - self.content = MagicMock() + self.content = Mock() self.content_id = "content-id" self.content.id = self.content_id @@ -161,16 +162,19 @@ def test_beep(self): self.assertActionPerformed(self.app, "beep") def test_add_background_task(self): + thing = Mock() + async def test_handler(sender): - pass + thing() self.app.add_background_task(test_handler) - self.assertActionPerformedWith( - self.app, - "loop:call_soon_threadsafe", - handler=test_handler, - args=(None,), - ) + + async def run_test(): + # Give the background task time to run. + await asyncio.sleep(0.1) + thing.assert_called_once() + + self.app._impl.loop.run_until_complete(run_test()) def test_override_startup(self): class BadApp(toga.App): @@ -196,19 +200,19 @@ def setUp(self): self.app_id = "beeware.org" self.id = "id" - self.content = MagicMock() + self.content = Mock() self.app = toga.DocumentApp(self.name, self.app_id, id=self.id) def test_app_documents(self): self.assertEqual(self.app.documents, []) - doc = MagicMock() + doc = Mock() self.app._documents.append(doc) self.assertEqual(self.app.documents, [doc]) def test_override_startup(self): - mock = MagicMock() + mock = Mock() class DocApp(toga.DocumentApp): def startup(self): diff --git a/core/tests/test_handlers.py b/core/tests/test_handlers.py index 302bb4e095..32dbdcc818 100644 --- a/core/tests/test_handlers.py +++ b/core/tests/test_handlers.py @@ -18,6 +18,44 @@ def test_noop_handler(): wrapped("arg1", "arg2", kwarg1=3, kwarg2=4) +def test_noop_handler_with_cleanup(): + """cleanup is still performed when a no-op handler is used""" + obj = Mock() + cleanup = Mock() + + wrapped = wrapped_handler(obj, None, cleanup=cleanup) + + assert wrapped._raw is None + + # This does nothing, but doesn't raise an error. + wrapped("arg1", "arg2", kwarg1=3, kwarg2=4) + + # Cleanup method was invoked + cleanup.assert_called_once_with(obj, None) + + +def test_noop_handler_with_cleanup_error(capsys): + """If cleanup on a no-op handler raises an error, it is logged""" + obj = Mock() + cleanup = Mock(side_effect=Exception("Problem in cleanup")) + + wrapped = wrapped_handler(obj, None, cleanup=cleanup) + + assert wrapped._raw is None + + # This does nothing, but doesn't raise an error. + wrapped("arg1", "arg2", kwarg1=3, kwarg2=4) + + # Cleanup method was invoked + cleanup.assert_called_once_with(obj, None) + + # Evidence of the handler cleanup error is in the log. + assert ( + "Error in handler cleanup: Problem in cleanup\nTraceback (most recent call last):\n" + in capsys.readouterr().err + ) + + def test_function_handler(): """A function can be used as a handler""" obj = Mock() @@ -136,13 +174,11 @@ def handler(*args, **kwargs): ) -def test_generator_handler(): +def test_generator_handler(event_loop): """A generator can be used as a handler""" obj = Mock() handler_call = {} - loop = asyncio.new_event_loop() - def handler(*args, **kwargs): handler_call["args"] = args handler_call["kwargs"] = kwargs @@ -164,7 +200,7 @@ async def waiter(): await asyncio.sleep(0.01) count += 1 - loop.run_until_complete(waiter()) + event_loop.run_until_complete(waiter()) # Handler arguments are as expected. assert handler_call == { @@ -175,13 +211,11 @@ async def waiter(): } -def test_generator_handler_error(capsys): +def test_generator_handler_error(event_loop, capsys): """A generator can raise an error""" obj = Mock() handler_call = {} - loop = asyncio.new_event_loop() - def handler(*args, **kwargs): handler_call["args"] = args handler_call["kwargs"] = kwargs @@ -201,7 +235,7 @@ async def waiter(): await asyncio.sleep(0.01) count += 1 - loop.run_until_complete(waiter()) + event_loop.run_until_complete(waiter()) # Handler arguments are as expected. assert handler_call == { @@ -216,14 +250,12 @@ async def waiter(): ) -def test_generator_handler_with_cleanup(): +def test_generator_handler_with_cleanup(event_loop): """A generator can have cleanup""" obj = Mock() cleanup = Mock() handler_call = {} - loop = asyncio.new_event_loop() - def handler(*args, **kwargs): handler_call["args"] = args handler_call["kwargs"] = kwargs @@ -246,7 +278,7 @@ async def waiter(): await asyncio.sleep(0.01) count += 1 - loop.run_until_complete(waiter()) + event_loop.run_until_complete(waiter()) # Handler arguments are as expected. assert handler_call == { @@ -260,14 +292,12 @@ async def waiter(): cleanup.assert_called_once_with(obj, 42) -def test_generator_handler_with_cleanup_error(capsys): +def test_generator_handler_with_cleanup_error(event_loop, capsys): """A generator can raise an error during cleanup""" obj = Mock() cleanup = Mock(side_effect=Exception("Problem in cleanup")) handler_call = {} - loop = asyncio.new_event_loop() - def handler(*args, **kwargs): handler_call["args"] = args handler_call["kwargs"] = kwargs @@ -290,7 +320,7 @@ async def waiter(): await asyncio.sleep(0.01) count += 1 - loop.run_until_complete(waiter()) + event_loop.run_until_complete(waiter()) # Handler arguments are as expected. assert handler_call == { @@ -310,13 +340,11 @@ async def waiter(): ) -def test_coroutine_handler(): +def test_coroutine_handler(event_loop): """A coroutine can be used as a handler""" obj = Mock() handler_call = {} - loop = asyncio.new_event_loop() - async def handler(*args, **kwargs): handler_call["args"] = args handler_call["kwargs"] = kwargs @@ -336,7 +364,7 @@ async def waiter(): await asyncio.sleep(0.01) count += 1 - loop.run_until_complete(waiter()) + event_loop.run_until_complete(waiter()) # Handler arguments are as expected. assert handler_call == { @@ -346,13 +374,11 @@ async def waiter(): } -def test_coroutine_handler_error(capsys): +def test_coroutine_handler_error(event_loop, capsys): """A coroutine can raise an error""" obj = Mock() handler_call = {} - loop = asyncio.new_event_loop() - async def handler(*args, **kwargs): handler_call["args"] = args handler_call["kwargs"] = kwargs @@ -372,7 +398,7 @@ async def waiter(): await asyncio.sleep(0.01) count += 1 - loop.run_until_complete(waiter()) + event_loop.run_until_complete(waiter()) # Handler arguments are as expected. assert handler_call == { @@ -387,14 +413,12 @@ async def waiter(): ) -def test_coroutine_handler_with_cleanup(): +def test_coroutine_handler_with_cleanup(event_loop): """A coroutine can have cleanup""" obj = Mock() cleanup = Mock() handler_call = {} - loop = asyncio.new_event_loop() - async def handler(*args, **kwargs): handler_call["args"] = args handler_call["kwargs"] = kwargs @@ -415,7 +439,7 @@ async def waiter(): await asyncio.sleep(0.01) count += 1 - loop.run_until_complete(waiter()) + event_loop.run_until_complete(waiter()) # Handler arguments are as expected. assert handler_call == { @@ -428,14 +452,12 @@ async def waiter(): cleanup.assert_called_once_with(obj, 42) -def test_coroutine_handler_with_cleanup_error(capsys): +def test_coroutine_handler_with_cleanup_error(event_loop, capsys): """A coroutine can raise an error during cleanup""" obj = Mock() cleanup = Mock(side_effect=Exception("Problem in cleanup")) handler_call = {} - loop = asyncio.new_event_loop() - async def handler(*args, **kwargs): handler_call["args"] = args handler_call["kwargs"] = kwargs @@ -456,7 +478,7 @@ async def waiter(): await asyncio.sleep(0.01) count += 1 - loop.run_until_complete(waiter()) + event_loop.run_until_complete(waiter()) # Handler arguments are as expected. assert handler_call == { @@ -488,7 +510,7 @@ def test_native_handler(): assert wrapped == native_method -def test_async_result(): +def test_async_result(event_loop): class TestAsyncResult(AsyncResult): RESULT_TYPE = "Test" diff --git a/core/tests/test_window.py b/core/tests/test_window.py index c7ed6ad506..4c175e9984 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -1,3 +1,4 @@ +from pathlib import Path from unittest.mock import Mock import pytest @@ -11,7 +12,7 @@ @pytest.fixture -def app(): +def app(event_loop): return toga.App("Test App", "org.beeware.toga.window") @@ -238,6 +239,19 @@ def test_visibility(window, app): assert not window.visible +def test_full_screen(window, app): + """A window can be set full screen.""" + assert not window.full_screen + + window.full_screen = True + assert window.full_screen + assert_action_performed_with(window, "set full screen", full_screen=True) + + window.full_screen = False + assert not window.full_screen + assert_action_performed_with(window, "set full screen", full_screen=False) + + def test_close_no_handler(window, app): """A window without a close handler can be closed""" window.show() @@ -289,3 +303,464 @@ def test_close_rejected_handler(window, app): assert window in app.windows assert_action_not_performed(window, "close") on_close_handler.assert_called_once_with(window) + + +def test_info_dialog(window, app): + """An info dialog can be shown""" + window.app = app + on_result_handler = Mock() + dialog = window.info_dialog("Title", "Body", on_result=on_result_handler) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + async def run_dialog(dialog): + dialog._impl.simulate_result(None) + assert await dialog is None + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show info dialog", + title="Title", + message="Body", + ) + on_result_handler.assert_called_once_with(window, None) + + +def test_question_dialog(window, app): + """A question dialog can be shown""" + window.app = app + on_result_handler = Mock() + dialog = window.question_dialog("Title", "Body", on_result=on_result_handler) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + async def run_dialog(dialog): + dialog._impl.simulate_result(True) + assert await dialog is True + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show question dialog", + title="Title", + message="Body", + ) + on_result_handler.assert_called_once_with(window, True) + + +def test_confirm_dialog(window, app): + """A confirm dialog can be shown""" + window.app = app + on_result_handler = Mock() + dialog = window.confirm_dialog("Title", "Body", on_result=on_result_handler) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + async def run_dialog(dialog): + dialog._impl.simulate_result(True) + assert await dialog is True + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show confirm dialog", + title="Title", + message="Body", + ) + on_result_handler.assert_called_once_with(window, True) + + +def test_error_dialog(window, app): + """An error dialog can be shown""" + window.app = app + on_result_handler = Mock() + dialog = window.error_dialog("Title", "Body", on_result=on_result_handler) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + async def run_dialog(dialog): + dialog._impl.simulate_result(None) + assert await dialog is None + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show error dialog", + title="Title", + message="Body", + ) + on_result_handler.assert_called_once_with(window, None) + + +def test_stack_trace_dialog(window, app): + """A stack trace dialog can be shown""" + window.app = app + on_result_handler = Mock() + dialog = window.stack_trace_dialog( + "Title", + "Body", + "The error", + on_result=on_result_handler, + ) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + async def run_dialog(dialog): + dialog._impl.simulate_result(None) + assert await dialog is None + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show stack trace dialog", + title="Title", + message="Body", + content="The error", + retry=False, + ) + on_result_handler.assert_called_once_with(window, None) + + +def test_save_file_dialog(window, app): + """A save file dialog can be shown""" + window.app = app + on_result_handler = Mock() + dialog = window.save_file_dialog( + "Title", + Path("/path/to/initial_file.txt"), + on_result=on_result_handler, + ) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + saved_file = Path("/saved/path/filename.txt") + + async def run_dialog(dialog): + dialog._impl.simulate_result(saved_file) + assert await dialog is saved_file + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show save file dialog", + title="Title", + filename="initial_file.txt", + initial_directory=Path("/path/to"), + file_types=None, + ) + on_result_handler.assert_called_once_with(window, saved_file) + + +def test_save_file_dialog_default_directory(window, app): + """If no path is provided, a save file dialog will use the default directory""" + window.app = app + on_result_handler = Mock() + dialog = window.save_file_dialog( + "Title", + "initial_file.txt", + file_types=[".txt", ".pdf"], + on_result=on_result_handler, + ) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + saved_file = Path("/saved/path/filename.txt") + + async def run_dialog(dialog): + dialog._impl.simulate_result(saved_file) + assert await dialog is saved_file + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show save file dialog", + title="Title", + filename="initial_file.txt", + initial_directory=None, + file_types=[".txt", ".pdf"], + ) + on_result_handler.assert_called_once_with(window, saved_file) + + +def test_open_file_dialog(window, app): + """A open file dialog can be shown""" + window.app = app + on_result_handler = Mock() + dialog = window.open_file_dialog( + "Title", + "/path/to/folder", + on_result=on_result_handler, + ) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + opened_file = Path("/opened/path/filename.txt") + + async def run_dialog(dialog): + dialog._impl.simulate_result(opened_file) + assert await dialog is opened_file + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show open file dialog", + title="Title", + initial_directory=Path("/path/to/folder"), + file_types=None, + multiple_select=False, + ) + on_result_handler.assert_called_once_with(window, opened_file) + + +def test_open_file_dialog_default_directory(window, app): + """If no path is provided, a open file dialog will use the default directory""" + window.app = app + on_result_handler = Mock() + dialog = window.open_file_dialog( + "Title", + file_types=[".txt", ".pdf"], + multiple_select=True, + on_result=on_result_handler, + ) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + opened_files = [ + Path("/opened/path/filename.txt"), + Path("/other/path/filename2.txt"), + ] + + async def run_dialog(dialog): + dialog._impl.simulate_result(opened_files) + assert await dialog is opened_files + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show open file dialog", + title="Title", + initial_directory=None, + file_types=[".txt", ".pdf"], + multiple_select=True, + ) + on_result_handler.assert_called_once_with(window, opened_files) + + +def test_select_folder_dialog(window, app): + """A select folder dialog can be shown""" + window.app = app + on_result_handler = Mock() + dialog = window.select_folder_dialog( + "Title", + Path("/path/to/folder"), + on_result=on_result_handler, + ) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + opened_file = Path("/opened/path/filename.txt") + + async def run_dialog(dialog): + dialog._impl.simulate_result(opened_file) + assert await dialog is opened_file + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show select folder dialog", + title="Title", + initial_directory=Path("/path/to/folder"), + multiple_select=False, + ) + on_result_handler.assert_called_once_with(window, opened_file) + + +def test_select_folder_dialog_default_directory(window, app): + """If no path is provided, a select folder dialog will use the default directory""" + window.app = app + on_result_handler = Mock() + dialog = window.select_folder_dialog( + "Title", + multiple_select=True, + on_result=on_result_handler, + ) + + assert dialog.window == window + assert dialog.app == app + + with pytest.raises( + RuntimeError, + match=r"Can't check dialog result directly; use await or an on_result handler", + ): + # Perform a synchronous comparison; this will raise a runtime error + dialog == 1 + + opened_files = [ + Path("/opened/path/filename.txt"), + Path("/other/path/filename2.txt"), + ] + + async def run_dialog(dialog): + dialog._impl.simulate_result(opened_files) + assert await dialog is opened_files + + app._impl.loop.run_until_complete(run_dialog(dialog)) + + assert_action_performed_with( + window, + "show select folder dialog", + title="Title", + initial_directory=None, + multiple_select=True, + ) + on_result_handler.assert_called_once_with(window, opened_files) + + +def test_deprecated_names_open_file_dialog(window, app): + "Deprecated names still work on open file dialogs" + window.app = app + on_result_handler = Mock() + with pytest.warns( + DeprecationWarning, + match=r"open_file_dialog\(multiselect\) has been renamed multiple_select", + ): + dialog = window.open_file_dialog( + "Title", + "/path/to/folder", + multiselect=True, + on_result=on_result_handler, + ) + + opened_files = [Path("/opened/path/filename.txt")] + + dialog._impl.simulate_result(opened_files) + + assert_action_performed_with( + window, + "show open file dialog", + title="Title", + initial_directory=Path("/path/to/folder"), + file_types=None, + multiple_select=True, + ) + on_result_handler.assert_called_once_with(window, opened_files) + + +def test_deprecated_names_select_folder_dialog(window, app): + "Deprecated names still work on open file dialogs" + window.app = app + on_result_handler = Mock() + with pytest.warns( + DeprecationWarning, + match=r"select_folder_dialog\(multiselect\) has been renamed multiple_select", + ): + dialog = window.select_folder_dialog( + "Title", + "/path/to/folder", + multiselect=True, + on_result=on_result_handler, + ) + + opened_files = [Path("/opened/path")] + + dialog._impl.simulate_result(opened_files) + + assert_action_performed_with( + window, + "show select folder dialog", + title="Title", + initial_directory=Path("/path/to/folder"), + multiple_select=True, + ) + on_result_handler.assert_called_once_with(window, opened_files) diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 9a5fd67fe4..796b490518 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -1,27 +1,18 @@ -from .utils import LoggedObject, not_required, not_required_on +import asyncio + +from .utils import LoggedObject, not_required_on from .window import Window class MainWindow(Window): - @not_required - def toga_on_close(self): - self.action("handle MainWindow on_close") - - -@not_required -class EventLoop: - def __init__(self, app): - self.app = app - - def call_soon_threadsafe(self, handler, *args): - self.app._action("loop:call_soon_threadsafe", handler=handler, args=args) + pass class App(LoggedObject): def __init__(self, interface): super().__init__() self.interface = interface - self.loop = EventLoop(self) + self.loop = asyncio.new_event_loop() def create(self): self._action("create") diff --git a/dummy/src/toga_dummy/dialogs.py b/dummy/src/toga_dummy/dialogs.py index 40323cffb1..06229b2ae0 100644 --- a/dummy/src/toga_dummy/dialogs.py +++ b/dummy/src/toga_dummy/dialogs.py @@ -1,50 +1,63 @@ class BaseDialog: - def __init__(self, interface): + def __init__(self, interface, on_result): self.interface = interface self.interface._impl = self + self.on_result = on_result + + def simulate_result(self, result): + self.on_result(None, result) + self.interface.future.set_result(result) class InfoDialog(BaseDialog): def __init__(self, interface, title, message, on_result=None): - super().__init__(interface) + super().__init__(interface, on_result=on_result) interface.window._impl._action( - "info_dialog", title=title, message=message, on_result=on_result + "show info dialog", + title=title, + message=message, ) class QuestionDialog(BaseDialog): def __init__(self, interface, title, message, on_result=None): - super().__init__(interface) + super().__init__(interface, on_result=on_result) interface.window._impl._action( - "question_dialog", title=title, message=message, on_result=on_result + "show question dialog", + title=title, + message=message, ) class ConfirmDialog(BaseDialog): def __init__(self, interface, title, message, on_result=None): - super().__init__(interface) + super().__init__(interface, on_result=on_result) interface.window._impl._action( - "confirm_dialog", title=title, message=message, on_result=on_result + "show confirm dialog", + title=title, + message=message, ) class ErrorDialog(BaseDialog): def __init__(self, interface, title, message, on_result=None): - super().__init__(interface) + super().__init__(interface, on_result=on_result) interface.window._impl._action( - "error_dialog", title=title, message=message, on_result=on_result + "show error dialog", + title=title, + message=message, ) class StackTraceDialog(BaseDialog): - def __init__(self, interface, title, message, on_result=None, **kwargs): - super().__init__(interface) + def __init__(self, interface, title, message, content, retry, on_result=None): + super().__init__(interface, on_result=on_result) interface.window._impl._action( - "stack_trace_dialog", + "show stack trace dialog", title=title, message=message, - on_result=on_result, - **kwargs + content=content, + retry=retry, ) @@ -58,14 +71,13 @@ def __init__( file_types=None, on_result=None, ): - super().__init__(interface) + super().__init__(interface, on_result=on_result) interface.window._impl._action( - "save_file_dialog", + "show save file dialog", title=title, filename=filename, initial_directory=initial_directory, file_types=file_types, - on_result=on_result, ) @@ -76,17 +88,16 @@ def __init__( title, initial_directory, file_types, - multiselect, + multiple_select, on_result=None, ): - super().__init__(interface) + super().__init__(interface, on_result=on_result) interface.window._impl._action( - "open_file_dialog", + "show open file dialog", title=title, initial_directory=initial_directory, file_types=file_types, - multiselect=multiselect, - on_result=on_result, + multiple_select=multiple_select, ) @@ -96,14 +107,13 @@ def __init__( interface, title, initial_directory, - multiselect, + multiple_select, on_result=None, ): - super().__init__(interface) + super().__init__(interface, on_result=on_result) interface.window._impl._action( - "select_folder_dialog", + "show select folder dialog", title=title, initial_directory=initial_directory, - multiselect=multiselect, - on_result=on_result, + multiple_select=multiple_select, ) diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index adb4f6b642..2eb9ec0fa8 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -94,7 +94,7 @@ def close(self): self._set_value("visible", False) def set_full_screen(self, is_full_screen): - self._set_value("is_full_screen", is_full_screen) + self._action("set full screen", full_screen=is_full_screen) def simulate_close(self): self.interface.on_close(None) diff --git a/pyproject.toml b/pyproject.toml index 5cccb3446e..aa3fb03160 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,8 @@ exclude_lines = [ "@(abc\\.)?abstractmethod", "NotImplementedError\\(\\)", "if TYPE_CHECKING:", - "class .+?\\(Protocol\\):", + "class .+?\\(Protocol.*\\):", + "@overload", ] [tool.isort] From 3e6bf8645f5a3c2e4075fe40f28f7b8273f15d1c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 1 Aug 2023 11:11:03 +0800 Subject: [PATCH 05/27] Remove GTK window tests. --- gtk/tests/widgets/test_window.py | 59 -------------------------------- 1 file changed, 59 deletions(-) delete mode 100644 gtk/tests/widgets/test_window.py diff --git a/gtk/tests/widgets/test_window.py b/gtk/tests/widgets/test_window.py deleted file mode 100644 index 0d67dd65c7..0000000000 --- a/gtk/tests/widgets/test_window.py +++ /dev/null @@ -1,59 +0,0 @@ -import unittest - -try: - import gi - - gi.require_version("Gtk", "3.0") - from gi.repository import Gtk -except ImportError: - import sys - - # If we're on Linux, Gtk *should* be available. If it isn't, make - # Gtk an object... but in such a way that every test will fail, - # because the object isn't actually the Gtk interface. - if sys.platform == "linux": - Gtk = object() - else: - Gtk = None - -import toga - - -def handle_events(): - while Gtk.events_pending(): - Gtk.main_iteration_do(blocking=False) - - -@unittest.skipIf( - Gtk is None, "Can't run GTK implementation tests on a non-Linux platform" -) -class TogaAppForWindowDemo(toga.App): - pass - - -class TestGtkWindow(unittest.TestCase): - def setUp(self): - self.box1 = toga.Box() - self.box2 = toga.Box() - self.app = TogaAppForWindowDemo("Test", "org.beeware.toga-gtk-tests") - self.app.main_window = toga.MainWindow("test window") - self.window = self.app.main_window - - def test_set_content_visibility_effects(self): - # Window is not showing, boxes cannot be drawn - self.assertEqual(self.window._impl.get_visible(), False) - self.assertEqual(self.box1._impl.native.is_drawable(), False) - self.assertEqual(self.box2._impl.native.is_drawable(), False) - - self.window.content = self.box1 - self.assertEqual(self.window.content._impl.native.is_drawable(), False) - - self.window.content = self.box2 - self.assertEqual(self.window.content._impl.native.is_drawable(), False) - - self.window.show() - self.assertEqual(self.window._impl.get_visible(), True) - self.assertEqual(self.window.content._impl.native.is_drawable(), True) - - self.window.content = self.box1 - self.assertEqual(self.window._impl.get_visible(), True) From cec300d940c1385e64a5373bab5daf25a852deab Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 1 Aug 2023 11:33:15 +0800 Subject: [PATCH 06/27] Correct a spelling issue, and document an API rename. --- changes/2058.removal.2.rst | 1 + docs/spelling_wordlist | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changes/2058.removal.2.rst diff --git a/changes/2058.removal.2.rst b/changes/2058.removal.2.rst new file mode 100644 index 0000000000..3499758242 --- /dev/null +++ b/changes/2058.removal.2.rst @@ -0,0 +1 @@ +The ``multiselect`` argument to Open File and Select Folder dialogs has been renamed ``multiple_select``, for consistency with other widgets that have multiple selection capability. diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 05a1116ab9..c837487f6c 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -53,7 +53,7 @@ Refactored rehint rehinted Ren -resizable +resizeable reStructuredText runtime scrollable From a13683bfe142b3a6bb548f1a59b33f8a66d471ff Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 1 Aug 2023 11:38:03 +0800 Subject: [PATCH 07/27] Exclude some clases from interface tests. --- dummy/src/toga_dummy/dialogs.py | 12 ++++++++++++ dummy/src/toga_dummy/window.py | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/dummy/src/toga_dummy/dialogs.py b/dummy/src/toga_dummy/dialogs.py index 06229b2ae0..9bd760065b 100644 --- a/dummy/src/toga_dummy/dialogs.py +++ b/dummy/src/toga_dummy/dialogs.py @@ -1,3 +1,7 @@ +from .utils import not_required + + +@not_required # Testbed coverage is complete. class BaseDialog: def __init__(self, interface, on_result): self.interface = interface @@ -9,6 +13,7 @@ def simulate_result(self, result): self.interface.future.set_result(result) +@not_required # Testbed coverage is complete. class InfoDialog(BaseDialog): def __init__(self, interface, title, message, on_result=None): super().__init__(interface, on_result=on_result) @@ -19,6 +24,7 @@ def __init__(self, interface, title, message, on_result=None): ) +@not_required # Testbed coverage is complete. class QuestionDialog(BaseDialog): def __init__(self, interface, title, message, on_result=None): super().__init__(interface, on_result=on_result) @@ -29,6 +35,7 @@ def __init__(self, interface, title, message, on_result=None): ) +@not_required # Testbed coverage is complete. class ConfirmDialog(BaseDialog): def __init__(self, interface, title, message, on_result=None): super().__init__(interface, on_result=on_result) @@ -39,6 +46,7 @@ def __init__(self, interface, title, message, on_result=None): ) +@not_required # Testbed coverage is complete. class ErrorDialog(BaseDialog): def __init__(self, interface, title, message, on_result=None): super().__init__(interface, on_result=on_result) @@ -49,6 +57,7 @@ def __init__(self, interface, title, message, on_result=None): ) +@not_required # Testbed coverage is complete. class StackTraceDialog(BaseDialog): def __init__(self, interface, title, message, content, retry, on_result=None): super().__init__(interface, on_result=on_result) @@ -61,6 +70,7 @@ def __init__(self, interface, title, message, content, retry, on_result=None): ) +@not_required # Testbed coverage is complete. class SaveFileDialog(BaseDialog): def __init__( self, @@ -81,6 +91,7 @@ def __init__( ) +@not_required # Testbed coverage is complete. class OpenFileDialog(BaseDialog): def __init__( self, @@ -101,6 +112,7 @@ def __init__( ) +@not_required # Testbed coverage is complete. class SelectFolderDialog(BaseDialog): def __init__( self, diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 2eb9ec0fa8..97ffbb920e 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -1,7 +1,7 @@ from .utils import LoggedObject, not_required -@not_required +@not_required # not part of the formal API spec class Container: def __init__(self, content=None): self.baseline_dpi = 96 @@ -37,7 +37,7 @@ def refreshed(self): self.content.refresh() -@not_required +@not_required # Testbed coverage is complete class Window(LoggedObject): def __init__(self, interface, title, position, size): super().__init__() From 32097ca798b1460cd8040f9613bb3050183ac2e0 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 1 Aug 2023 15:43:47 +0800 Subject: [PATCH 08/27] Cocoa GUI tests for Window (excluding dialogs and toolbars. --- cocoa/src/toga_cocoa/window.py | 56 +++----- cocoa/tests_backend/window.py | 50 +++++++ core/src/toga/command.py | 5 +- testbed/tests/test_window.py | 233 +++++++++++++++++++++++++++++++++ testbed/tests/testbed.py | 1 + 5 files changed, 304 insertions(+), 41 deletions(-) create mode 100644 cocoa/tests_backend/window.py create mode 100644 testbed/tests/test_window.py diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 52de33dd7f..aee7a8643c 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -7,7 +7,6 @@ NSMakeRect, NSMiniaturizableWindowMask, NSMutableArray, - NSObject, NSPoint, NSResizableWindowMask, NSScreen, @@ -25,7 +24,7 @@ def toolbar_identifier(cmd): return "ToolbarItem-%s" % id(cmd) -class WindowDelegate(NSObject): +class TogaWindow(NSWindow): interface = objc_property(object, weak=True) impl = objc_property(object, weak=True) @@ -36,17 +35,8 @@ def windowShouldClose_(self, notification) -> bool: @objc_method def windowDidResize_(self, notification) -> None: if self.interface.content: - # print() - # print("Window resize", ( - # notification.object.contentView.frame.size.width, - # notification.object.contentView.frame.size.height - # )) - if ( - notification.object.contentView.frame.size.width > 0.0 - and notification.object.contentView.frame.size.height > 0.0 - ): - # Set the window to the new size - self.interface.content.refresh() + # Set the window to the new size + self.interface.content.refresh() ###################################################################### # Toolbar delegate methods @@ -115,11 +105,6 @@ def onToolbarButtonPress_(self, obj) -> None: item.action(obj) -class TogaWindow(NSWindow): - interface = objc_property(object, weak=True) - impl = objc_property(object, weak=True) - - class Window: def __init__(self, interface, title, position, size): self.interface = interface @@ -146,19 +131,23 @@ def __init__(self, interface, title, position, size): self.native.interface = self.interface self.native.impl = self + # 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 an explicit reference to the window. + self.native.retain() + self.set_title(title) self.set_size(size) self.set_position(position) - self.delegate = WindowDelegate.alloc().init() - self.delegate.interface = self.interface - self.delegate.impl = self - - self.native.delegate = self.delegate + self.native.delegate = self.native self.container = Container(on_refresh=self.content_refreshed) self.native.contentView = self.container.native + def __del__(self): + self.native.release() + def create_toolbar(self): self._toolbar_items = {} for cmd in self.interface.toolbar: @@ -200,10 +189,6 @@ def set_title(self, title): self.native.title = title def get_position(self): - # If there is no active screen, we can't get a position - if len(NSScreen.screens) == 0: - return 0, 0 - # The "primary" screen has index 0 and origin (0, 0). primary_screen = NSScreen.screens[0].frame window_frame = self.native.frame @@ -217,10 +202,6 @@ def get_position(self): ) def set_position(self, position): - # If there is no active screen, we can't set a position - if len(NSScreen.screens) == 0: - return - # The "primary" screen has index 0 and origin (0, 0). primary_screen = NSScreen.screens[0].frame @@ -256,14 +237,11 @@ def set_full_screen(self, is_full_screen): self.interface.factory.not_implemented("Window.set_full_screen()") def cocoa_windowShouldClose(self): - if self.interface.on_close._raw: - # The on_close handler has a cleanup method that will enforce - # the close if the on_close handler requests it; this initial - # "should close" request can always return False. - self.interface.on_close(self) - return False - else: - return True + # The on_close handler has a cleanup method that will enforce + # the close if the on_close handler requests it; this initial + # "should close" request can always return False. + self.interface.on_close(None) + return False def close(self): self.native.close() diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py new file mode 100644 index 0000000000..ba39ccb9b4 --- /dev/null +++ b/cocoa/tests_backend/window.py @@ -0,0 +1,50 @@ +from toga_cocoa.libs import ( + NSClosableWindowMask, + NSMiniaturizableWindowMask, + NSResizableWindowMask, + NSWindow, +) + +from .probe import BaseProbe + + +class WindowProbe(BaseProbe): + def __init__(self, app, window): + super().__init__() + self.app = app + self.window = window + self.impl = window._impl + self.native = window._impl.native + assert isinstance(self.native, NSWindow) + + def close(self): + self.native.performClose(None) + + @property + def content_size(self): + return ( + self.native.contentView.frame.size.width, + self.native.contentView.frame.size.height, + ) + + @property + def is_resizable(self): + return bool(self.native.styleMask & NSResizableWindowMask) + + @property + def is_closeable(self): + return bool(self.native.styleMask & NSClosableWindowMask) + + @property + def is_minimizable(self): + return bool(self.native.styleMask & NSMiniaturizableWindowMask) + + @property + def is_minimized(self): + return bool(self.native.isMiniaturized) + + def minimize(self): + self.native.performMiniaturize(None) + + def unminimize(self): + self.native.deminiaturize(None) diff --git a/core/src/toga/command.py b/core/src/toga/command.py index e8ff62a632..4f66dabd83 100644 --- a/core/src/toga/command.py +++ b/core/src/toga/command.py @@ -248,8 +248,6 @@ def __init__( ################################################################## # End backwards compatibility. ################################################################## - orig_action = action - self.action = wrapped_handler(self, action) self.text = text self.shortcut = shortcut @@ -260,6 +258,9 @@ def __init__( self.section = section if section else 0 self.order = order if order else 0 + orig_action = action + self.action = wrapped_handler(self, action) + self.factory = get_platform_factory() self._impl = self.factory.Command(interface=self) diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py new file mode 100644 index 0000000000..9ff7cec766 --- /dev/null +++ b/testbed/tests/test_window.py @@ -0,0 +1,233 @@ +from importlib import import_module +from unittest.mock import Mock + +import toga +from toga.colors import CORNFLOWERBLUE, GOLDENROD, REBECCAPURPLE +from toga.style.pack import COLUMN, Pack + + +def window_probe(app, window): + module = import_module("tests_backend.window") + return getattr(module, "WindowProbe")(app, window) + + +async def test_secondary_window(app): + """A secondary window can be created""" + new_window = toga.Window() + probe = window_probe(app, new_window) + + new_window.show() + await probe.redraw("New window has been shown") + + assert new_window.app == app + assert new_window in app.windows + + assert new_window.title == "Toga" + assert new_window.size == (640, 480) + assert new_window.position == (100, 100) + assert probe.is_resizable + assert probe.is_closeable + assert probe.is_minimizable + + new_window.close() + await probe.redraw("New window has been closed") + + assert new_window not in app.windows + + +async def test_secondary_window_with_args(app): + """A secondary window can be created with a specific size and position.""" + on_close_handler = Mock(return_value=False) + + new_window = toga.Window( + title="New Window", + position=(200, 300), + size=(300, 200), + on_close=on_close_handler, + ) + probe = window_probe(app, new_window) + + new_window.show() + await probe.redraw("New window has been shown") + + assert new_window.app == app + assert new_window in app.windows + + assert new_window.title == "New Window" + assert new_window.size == (300, 200) + assert new_window.position == (200, 300) + + probe.close() + await probe.redraw("Attempt to close second window that is rejected") + on_close_handler.assert_called_once_with(new_window) + + assert new_window in app.windows + + # Reset, and try again, this time allowing the + on_close_handler.reset_mock() + on_close_handler.return_value = True + + probe.close() + await probe.redraw("Attempt to close second window that succeeds") + on_close_handler.assert_called_once_with(new_window) + + assert new_window not in app.windows + + +async def test_non_resizable(app): + """A non-resizable window can be created""" + new_window = toga.Window( + title="Not Resizable", resizeable=False, position=(150, 150) + ) + + new_window.show() + + probe = window_probe(app, new_window) + await probe.redraw("Non resizable window has been shown") + + assert new_window.visible + assert not probe.is_resizable + + # Clean up + new_window.close() + + +async def test_non_closeable(app): + """A non-closeable window can be created""" + new_window = toga.Window( + title="Not Closeable", closeable=False, position=(150, 150) + ) + + new_window.show() + + probe = window_probe(app, new_window) + await probe.redraw("Non-closeable window has been shown") + + assert new_window.visible + assert not probe.is_closeable + + # Do a UI close on the window + probe.close() + await probe.redraw("Close request was ignored") + assert new_window.visible + + # Do an explicit close on the window + new_window.close() + await probe.redraw("Explicit close was honored") + + assert not new_window.visible + + +async def test_non_minimizable(app): + """A non-minimizable window can be created""" + new_window = toga.Window( + title="Not Minimizable", minimizable=False, position=(150, 150) + ) + + new_window.show() + + probe = window_probe(app, new_window) + await probe.redraw("Non-minimizable window has been shown") + assert new_window.visible + assert not probe.is_minimizable + + probe.minimize() + await probe.redraw("Minimize request has been ignored") + assert not probe.is_minimized + + # Clean up + new_window.close() + + +async def test_visibility(app): + """Visibility of a window can be controlled""" + new_window = toga.Window(title="New Window", position=(200, 250)) + probe = window_probe(app, new_window) + + new_window.show() + await probe.redraw("New window has been shown") + + assert new_window.app == app + assert new_window in app.windows + + assert new_window.visible + assert new_window.size == (640, 480) + assert new_window.position == (200, 250) + + new_window.hide() + await probe.redraw("New window has been hidden") + + assert not new_window.visible + + # Move and resie the window while offscreen + new_window.size = (250, 200) + new_window.position = (300, 150) + + new_window.show() + await probe.redraw("New window has been made visible again") + + assert new_window.visible + assert new_window.size == (250, 200) + assert new_window.position == (300, 150) + + probe.minimize() + # Delay is required to account for "genie" animations + await probe.redraw("Window has been minimized", delay=0.5) + + assert probe.is_minimized + + probe.unminimize() + # Delay is required to account for "genie" animations + await probe.redraw("Window has been unminimized", delay=0.5) + + assert not probe.is_minimized + + probe.close() + await probe.redraw("New window has been closed") + + assert new_window not in app.windows + + +async def test_move_and_resize(app): + """A window can be moved and resized.""" + new_window = toga.Window(title="New Window") + probe = window_probe(app, new_window) + new_window.show() + await probe.redraw("New window has been shown") + + # Determine + extra_width = new_window.size[0] - probe.content_size[0] + extra_height = new_window.size[1] - probe.content_size[1] + + new_window.position = (150, 50) + await probe.redraw("New window has been moved") + assert new_window.position == (150, 50) + + new_window.size = (200, 150) + await probe.redraw("New window has been resized") + assert new_window.size == (200, 150) + assert probe.content_size == (200 - extra_width, 150 - extra_height) + + box1 = toga.Box(style=Pack(background_color=REBECCAPURPLE, width=10, height=10)) + box2 = toga.Box(style=Pack(background_color=GOLDENROD, width=10, height=200)) + new_window.content = toga.Box( + children=[box1, box2], + style=Pack(direction=COLUMN, background_color=CORNFLOWERBLUE), + ) + await probe.redraw("New window has had height adjusted due to content") + assert new_window.size == (200 + extra_width, 210 + extra_height) + assert probe.content_size == (200, 210) + + # Alter the content width to exceed window size + box1.style.width = 250 + await probe.redraw("New window has had width adjusted due to content") + assert new_window.size == (250 + extra_width, 210 + extra_height) + assert probe.content_size == (250, 210) + + # Try to resize to a size less than the content size + new_window.size = (200, 150) + await probe.redraw("New window forced resize fails") + assert new_window.size == (250 + extra_width, 210 + extra_height) + assert probe.content_size == (250, 210) + + new_window.close() diff --git a/testbed/tests/testbed.py b/testbed/tests/testbed.py index fd8da6b187..185d480f21 100644 --- a/testbed/tests/testbed.py +++ b/testbed/tests/testbed.py @@ -137,6 +137,7 @@ def get_terminal_size(*args, **kwargs): else: report_coverage = False + report_coverage = True thread = Thread( target=partial( run_tests, From 212fb8959cb8db1a2f0fc3b7dd93fe8158e163a0 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 1 Aug 2023 16:04:57 +0800 Subject: [PATCH 09/27] Add menu items and keyboard shortcuts for close/minimize on macOS. --- cocoa/src/toga_cocoa/app.py | 41 +++++++++++++++++++++++++++++++--- cocoa/src/toga_cocoa/window.py | 2 +- core/src/toga/app.py | 5 ++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index c983720d4d..251d7af696 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -140,7 +140,7 @@ def create(self): self.interface.commands.add( # ---- App menu ----------------------------------- toga.Command( - lambda _: self.interface.about(), + lambda _, **kwargs: self.interface.about(), "About " + formal_name, group=toga.Group.APP, ), @@ -176,12 +176,36 @@ def create(self): ), # Quit should always be the last item, in a section on its own toga.Command( - lambda _: self.interface.exit(), + lambda _, **kwargs: self.interface.exit(), "Quit " + formal_name, shortcut=toga.Key.MOD_1 + "q", group=toga.Group.APP, section=sys.maxsize, ), + # ---- File menu ---------------------------------- + toga.Command( + lambda _, **kwargs: self.interface.current_window._impl.native.performClose( + None + ) + if self.interface.current_window + else None, + "Close Window", + shortcut=toga.Key.MOD_1 + "W", + group=toga.Group.FILE, + order=1, + section=50, + ), + toga.Command( + lambda _, **kwargs: [ + window._impl.native.performClose(None) + for window in set(self.interface.windows) + ], + "Close All Windows", + shortcut=toga.Key.MOD_2 + toga.Key.MOD_1 + "W", + group=toga.Group.FILE, + order=2, + section=50, + ), # ---- Edit menu ---------------------------------- toga.Command( NativeHandler(SEL("undo:")), @@ -244,9 +268,20 @@ def create(self): section=10, order=60, ), + # ---- Edit menu ---------------------------------- + toga.Command( + lambda _, **kwargs: self.interface.current_window._impl.native.miniaturize( + None + ) + if self.interface.current_window + else None, + "Minimize", + shortcut=toga.Key.MOD_1 + "m", + group=toga.Group.WINDOW, + ), # ---- Help menu ---------------------------------- toga.Command( - lambda _: self.interface.visit_homepage(), + lambda _, **kwargs: self.interface.visit_homepage(), "Visit homepage", enabled=self.interface.home_page is not None, group=toga.Group.HELP, diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index aee7a8643c..c27b340ecc 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -157,7 +157,7 @@ def create_toolbar(self): self._toolbar_native = NSToolbar.alloc().initWithIdentifier( "Toolbar-%s" % id(self) ) - self._toolbar_native.setDelegate(self.delegate) + self._toolbar_native.setDelegate(self.native) self.native.setToolbar(self._toolbar_native) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 343efc25ad..0e01d3e17b 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -499,7 +499,10 @@ def main_window(self, window: MainWindow) -> None: @property def current_window(self): """Return the currently active content window.""" - return self._impl.get_current_window().interface + window = self._impl.get_current_window() + if window is None: + return window + return window.interface @current_window.setter def current_window(self, window): From f3bffde9e02884cd16098ad0dc98a0fdaadf2652 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 2 Aug 2023 09:02:30 +0800 Subject: [PATCH 10/27] Add GUI test of full screen mode. --- cocoa/src/toga_cocoa/container.py | 3 +-- cocoa/src/toga_cocoa/libs/appkit.py | 20 ++++++++++++---- cocoa/src/toga_cocoa/window.py | 17 +++++++------ cocoa/tests_backend/window.py | 17 +++++++------ examples/window/window/app.py | 17 +++++++++---- testbed/tests/test_window.py | 37 +++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 29 deletions(-) diff --git a/cocoa/src/toga_cocoa/container.py b/cocoa/src/toga_cocoa/container.py index b48698ad68..8cef990686 100644 --- a/cocoa/src/toga_cocoa/container.py +++ b/cocoa/src/toga_cocoa/container.py @@ -103,8 +103,7 @@ def content(self, widget): widget.container = self def refreshed(self): - if self.on_refresh: - self.on_refresh(self) + self.on_refresh(self) @property def width(self): diff --git a/cocoa/src/toga_cocoa/libs/appkit.py b/cocoa/src/toga_cocoa/libs/appkit.py index 7e30335a8b..445f172355 100644 --- a/cocoa/src/toga_cocoa/libs/appkit.py +++ b/cocoa/src/toga_cocoa/libs/appkit.py @@ -758,11 +758,21 @@ def NSTextAlignment(alignment): NSWindow = ObjCClass("NSWindow") NSWindow.declare_property("frame") -NSBorderlessWindowMask = 0 -NSTitledWindowMask = 1 << 0 -NSClosableWindowMask = 1 << 1 -NSMiniaturizableWindowMask = 1 << 2 -NSResizableWindowMask = 1 << 3 + +class NSWindowStyleMask(IntEnum): + Borderless = 0 + Titled = 1 << 0 + Closable = 1 << 1 + Miniaturizable = 1 << 2 + Resizable = 1 << 3 + UnifiedTitleAndToolbar = 1 << 12 + FullScreen = 1 << 14 + FullSizeContentView = 1 << 15 + UtilityWindow = 1 << 4 + DocModalWindow = 1 << 6 + NonactivatingPanel = 1 << 7 + HUDWindow = 1 << 13 + # NSCompositingOperationXXX is equivalent to NSCompositeXXX NSCompositingOperationClear = 0 diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index c27b340ecc..63167e5a16 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -3,18 +3,15 @@ from toga_cocoa.libs import ( SEL, NSBackingStoreBuffered, - NSClosableWindowMask, NSMakeRect, - NSMiniaturizableWindowMask, NSMutableArray, NSPoint, - NSResizableWindowMask, NSScreen, NSSize, - NSTitledWindowMask, NSToolbar, NSToolbarItem, NSWindow, + NSWindowStyleMask, objc_method, objc_property, ) @@ -110,15 +107,15 @@ def __init__(self, interface, title, position, size): self.interface = interface self.interface._impl = self - mask = NSTitledWindowMask + mask = NSWindowStyleMask.Titled if self.interface.closeable: - mask |= NSClosableWindowMask + mask |= NSWindowStyleMask.Closable if self.interface.resizeable: - mask |= NSResizableWindowMask + mask |= NSWindowStyleMask.Resizable if self.interface.minimizable: - mask |= NSMiniaturizableWindowMask + mask |= NSWindowStyleMask.Miniaturizable # Create the window with a default frame; # we'll update size and position later. @@ -234,7 +231,9 @@ def get_visible(self): return bool(self.native.isVisible) def set_full_screen(self, is_full_screen): - self.interface.factory.not_implemented("Window.set_full_screen()") + current_state = bool(self.native.styleMask & NSWindowStyleMask.FullScreen) + if is_full_screen != current_state: + self.native.toggleFullScreen(self.native) def cocoa_windowShouldClose(self): # The on_close handler has a cleanup method that will enforce diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index ba39ccb9b4..4c0590b788 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -1,9 +1,4 @@ -from toga_cocoa.libs import ( - NSClosableWindowMask, - NSMiniaturizableWindowMask, - NSResizableWindowMask, - NSWindow, -) +from toga_cocoa.libs import NSWindow, NSWindowStyleMask from .probe import BaseProbe @@ -27,17 +22,21 @@ def content_size(self): self.native.contentView.frame.size.height, ) + @property + def is_full_screen(self): + return bool(self.native.styleMask & NSWindowStyleMask.FullScreen) + @property def is_resizable(self): - return bool(self.native.styleMask & NSResizableWindowMask) + return bool(self.native.styleMask & NSWindowStyleMask.Resizable) @property def is_closeable(self): - return bool(self.native.styleMask & NSClosableWindowMask) + return bool(self.native.styleMask & NSWindowStyleMask.Closable) @property def is_minimizable(self): - return bool(self.native.styleMask & NSMiniaturizableWindowMask) + return bool(self.native.styleMask & NSWindowStyleMask.Miniaturizable) @property def is_minimized(self): diff --git a/examples/window/window/app.py b/examples/window/window/app.py index 85effd9e3d..a1afadcfe1 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -23,12 +23,15 @@ def do_small(self, widget, **kwargs): def do_large(self, widget, **kwargs): self.main_window.size = (1500, 1000) - def do_full_screen(self, widget, **kwargs): + def do_app_full_screen(self, widget, **kwargs): if self.is_full_screen: self.exit_full_screen() else: self.set_full_screen(self.main_window) + def do_window_full_screen(self, widget, **kwargs): + self.main_window.full_screen = not self.main_window.full_screen + def do_title(self, widget, **kwargs): self.main_window.title = f"Time is {datetime.now()}" @@ -140,8 +143,13 @@ def startup(self): btn_do_large = toga.Button( "Become large", on_press=self.do_large, style=btn_style ) - btn_do_full_screen = toga.Button( - "Become full screen", on_press=self.do_full_screen, style=btn_style + btn_do_app_full_screen = toga.Button( + "Make app full screen", on_press=self.do_app_full_screen, style=btn_style + ) + btn_do_window_full_screen = toga.Button( + "Make window full screen", + on_press=self.do_window_full_screen, + style=btn_style, ) btn_do_title = toga.Button( "Change title", on_press=self.do_title, style=btn_style @@ -172,7 +180,8 @@ def startup(self): btn_do_right, btn_do_small, btn_do_large, - btn_do_full_screen, + btn_do_app_full_screen, + btn_do_window_full_screen, btn_do_title, btn_do_new_windows, btn_do_current_window_cycling, diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 9ff7cec766..6c0c94bf85 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -231,3 +231,40 @@ async def test_move_and_resize(app): assert probe.content_size == (250, 210) new_window.close() + + +async def test_full_screen(app): + """Window can be made full screen""" + new_window = toga.Window(title="New Window", size=(400, 300), position=(150, 150)) + new_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + probe = window_probe(app, new_window) + new_window.show() + await probe.redraw("New window has been shown") + assert not probe.is_full_screen + initial_content_size = probe.content_size + + new_window.full_screen = True + # A short delay to allow for genie animations + await probe.redraw("New window is full screen", delay=1) + assert probe.is_full_screen + assert probe.content_size[0] > initial_content_size[0] + assert probe.content_size[1] > initial_content_size[1] + + new_window.full_screen = True + await probe.redraw("New window is still full screen") + assert probe.is_full_screen + assert probe.content_size[0] > initial_content_size[0] + assert probe.content_size[1] > initial_content_size[1] + + new_window.full_screen = False + # A short delay to allow for genie animations + await probe.redraw("New window is not full screen", delay=1) + assert not probe.is_full_screen + assert probe.content_size == initial_content_size + + new_window.full_screen = False + await probe.redraw("New window is still not full screen") + assert not probe.is_full_screen + assert probe.content_size == initial_content_size + + new_window.close() From 86e91aeb7bb9af84c3ce17b503d1da8eacb510c3 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 2 Aug 2023 09:24:03 +0800 Subject: [PATCH 11/27] Converge on US spelling for resizable, closable --- changes/2058.removal.3.rst | 1 + cocoa/src/toga_cocoa/window.py | 4 +- cocoa/tests_backend/window.py | 2 +- core/src/toga/app.py | 6 +-- core/src/toga/window.py | 68 +++++++++++++++++++++++----- core/tests/test_window.py | 46 +++++++++++++++---- docs/spelling_wordlist | 4 +- examples/window/window/app.py | 6 +-- gtk/src/toga_gtk/window.py | 6 +-- testbed/tests/test_window.py | 16 +++---- winforms/src/toga_winforms/window.py | 4 +- 11 files changed, 117 insertions(+), 46 deletions(-) create mode 100644 changes/2058.removal.3.rst diff --git a/changes/2058.removal.3.rst b/changes/2058.removal.3.rst new file mode 100644 index 0000000000..4c6d52f106 --- /dev/null +++ b/changes/2058.removal.3.rst @@ -0,0 +1 @@ +``Window.resizeable`` and ``Window.closeable`` have been renamed ``Window.resizable`` and ``Window.closable``, to adhere to US spelling conventions. diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 63167e5a16..3ac7332313 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -108,10 +108,10 @@ def __init__(self, interface, title, position, size): self.interface._impl = self mask = NSWindowStyleMask.Titled - if self.interface.closeable: + if self.interface.closable: mask |= NSWindowStyleMask.Closable - if self.interface.resizeable: + if self.interface.resizable: mask |= NSWindowStyleMask.Resizable if self.interface.minimizable: diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 4c0590b788..4ae718698e 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -31,7 +31,7 @@ def is_resizable(self): return bool(self.native.styleMask & NSWindowStyleMask.Resizable) @property - def is_closeable(self): + def is_closable(self): return bool(self.native.styleMask & NSWindowStyleMask.Closable) @property diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 0e01d3e17b..3b2efc2765 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -127,7 +127,7 @@ def __init__( title: str | None = None, position: tuple[int, int] = (100, 100), size: tuple[int, int] = (640, 480), - resizeable: bool = True, + resizable: bool = True, minimizable: bool = True, ): """Create a new application Main Window. @@ -144,8 +144,8 @@ def __init__( title=title, position=position, size=size, - resizeable=resizeable, - closeable=True, + resizable=resizable, + closable=True, minimizable=minimizable, ) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index b2a67b5b5e..8bb475193a 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -68,10 +68,12 @@ def __init__( title: str = "Toga", position: tuple[int, int] = (100, 100), size: tuple[int, int] = (640, 480), - resizeable: bool = True, - closeable: bool = True, + resizable: bool = True, + closable: bool = True, minimizable: bool = True, on_close: OnCloseHandler | None = None, + resizeable=None, # DEPRECATED + closeable=None, # DEPRECATED ) -> None: """Create a new Window. @@ -79,11 +81,33 @@ def __init__( :param title: Title for the window. :param position: Position of the window, as a tuple of ``(x, y)`` coordinates. :param size: Size of the window, as a tuple of ``(width, height)``, in pixels. - :param resizeable: Can the window be manually resized by the user? - :param closeable: Should the window provide the option to be manually closed? + :param resizable: Can the window be manually resized by the user? + :param closable: Should the window provide the option to be manually closed? :param minimizable: Can the window be minimized by the user? :param on_close: The initial ``on_close`` handler. + :param resizeable: **DEPRECATED** - Use ``resizable``. + :param closeable: **DEPRECATED** - Use ``closable``. """ + ###################################################################### + # 2023-08: Backwards compatibility + ###################################################################### + if resizeable is not None: + warnings.warn( + "Window.resizeable has been renamed Window.resizable", + DeprecationWarning, + ) + resizable = resizeable + + if closeable is not None: + warnings.warn( + "Window.closeable has been renamed Window.closable", + DeprecationWarning, + ) + closable = closeable + ###################################################################### + # End backwards compatibility + ###################################################################### + self.widgets = WidgetRegistry() self._id = str(id if id else identifier(self)) @@ -92,8 +116,8 @@ def __init__( self._content = None self._is_full_screen = False - self._resizeable = resizeable - self._closeable = closeable + self._resizable = resizable + self._closable = closable self._minimizable = minimizable self.factory = get_platform_factory() @@ -145,14 +169,14 @@ def title(self, title: str) -> None: self._impl.set_title(str(title).split("\n")[0]) @property - def resizeable(self) -> bool: - """Is the window resizeable?""" - return self._resizeable + def resizable(self) -> bool: + """Is the window resizable?""" + return self._resizable @property - def closeable(self) -> bool: + def closable(self) -> bool: """Can the window be closed by a user action?""" - return self._closeable + return self._closable @property def minimizable(self) -> bool: @@ -667,3 +691,25 @@ def select_folder_dialog( on_result=wrapped_handler(self, on_result), ) return dialog + + ###################################################################### + # 2023-08: Backwards compatibility + ###################################################################### + + @property + def resizeable(self) -> bool: + """**DEPRECATED** Use :attr:`resizable`""" + warnings.warn( + "Window.resizeable has been renamed Window.resizable", + DeprecationWarning, + ) + return self._resizable + + @property + def closeable(self) -> bool: + """**DEPRECATED** Use :attr:`closable`""" + warnings.warn( + "Window.closeable has been renamed Window.closable", + DeprecationWarning, + ) + return self._closable diff --git a/core/tests/test_window.py b/core/tests/test_window.py index 4c175e9984..7e65c61505 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -36,8 +36,8 @@ def test_window_created(): assert window.title == "Toga" assert window.position == (100, 100) assert window.size == (640, 480) - assert window.resizeable - assert window.closeable + assert window.resizable + assert window.closable assert window.minimizable assert len(window.toolbar) == 0 assert window.on_close._raw is None @@ -52,8 +52,8 @@ def test_window_created_explicit(): title="My Window", position=(10, 20), size=(200, 300), - resizeable=False, - closeable=False, + resizable=False, + closable=False, minimizable=False, on_close=on_close_handler, ) @@ -68,8 +68,8 @@ def test_window_created_explicit(): assert window.title == "My Window" assert window.position == (10, 20) assert window.size == (200, 300) - assert not window.resizeable - assert not window.closeable + assert not window.resizable + assert not window.closable assert not window.minimizable assert len(window.toolbar) == 0 assert window.on_close._raw == on_close_handler @@ -708,7 +708,7 @@ async def run_dialog(dialog): def test_deprecated_names_open_file_dialog(window, app): - "Deprecated names still work on open file dialogs" + """Deprecated names still work on open file dialogs.""" window.app = app on_result_handler = Mock() with pytest.warns( @@ -738,7 +738,7 @@ def test_deprecated_names_open_file_dialog(window, app): def test_deprecated_names_select_folder_dialog(window, app): - "Deprecated names still work on open file dialogs" + """Deprecated names still work on open file dialogs.""" window.app = app on_result_handler = Mock() with pytest.warns( @@ -764,3 +764,33 @@ def test_deprecated_names_select_folder_dialog(window, app): multiple_select=True, ) on_result_handler.assert_called_once_with(window, opened_files) + + +def test_deprecated_names_resizeable(): + """Deprecated spelling of resizable still works""" + with pytest.warns( + DeprecationWarning, + match=r"Window.resizeable has been renamed Window.resizable", + ): + window = toga.Window(title="Deprecated", resizeable=True) + + with pytest.warns( + DeprecationWarning, + match=r"Window.resizeable has been renamed Window.resizable", + ): + assert window.resizeable + + +def test_deprecated_names_closeable(): + """Deprecated spelling of closable still works""" + with pytest.warns( + DeprecationWarning, + match=r"Window.closeable has been renamed Window.closable", + ): + window = toga.Window(title="Deprecated", closeable=True) + + with pytest.warns( + DeprecationWarning, + match=r"Window.closeable has been renamed Window.closable", + ): + assert window.closeable diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index c837487f6c..27372e7b9e 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -12,7 +12,6 @@ Bugfixes Cairo cancelled clickable -closable codepoint coroutine CSS @@ -33,7 +32,6 @@ KDE linters macOS Manjaro -minimizable Monetization namespace OpenStreetMap @@ -53,7 +51,7 @@ Refactored rehint rehinted Ren -resizeable +resizable reStructuredText runtime scrollable diff --git a/examples/window/window/app.py b/examples/window/window/app.py index a1afadcfe1..ca7732ba2a 100644 --- a/examples/window/window/app.py +++ b/examples/window/window/app.py @@ -40,7 +40,7 @@ def do_new_windows(self, widget, **kwargs): "Non-resizable Window", position=(200, 200), size=(300, 300), - resizeable=False, + resizable=False, on_close=self.close_handler, ) non_resize_window.content = toga.Box( @@ -52,10 +52,10 @@ def do_new_windows(self, widget, **kwargs): "Non-closeable Window", position=(300, 300), size=(300, 300), - closeable=False, + closable=False, ) non_close_window.content = toga.Box( - children=[toga.Label("This window is not closeable")] + children=[toga.Label("This window is not closable")] ) non_close_window.show() diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 5771ebdd73..1eed6c178a 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -26,12 +26,12 @@ def __init__(self, interface, title, position, size): self.set_title(title) self.set_position(position) - # Set the window deletable/closeable. - self.native.set_deletable(self.interface.closeable) + # Set the window deletable/closable. + self.native.set_deletable(self.interface.closable) # Added to set Window Resizable - removes Window Maximize button from # Window Decorator when resizable == False - self.native.set_resizable(self.interface.resizeable) + self.native.set_resizable(self.interface.resizable) self.toolbar_native = None self.toolbar_items = None diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 6c0c94bf85..904d033099 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -26,7 +26,7 @@ async def test_secondary_window(app): assert new_window.size == (640, 480) assert new_window.position == (100, 100) assert probe.is_resizable - assert probe.is_closeable + assert probe.is_closable assert probe.is_minimizable new_window.close() @@ -92,19 +92,16 @@ async def test_non_resizable(app): new_window.close() -async def test_non_closeable(app): - """A non-closeable window can be created""" - new_window = toga.Window( - title="Not Closeable", closeable=False, position=(150, 150) - ) - +async def test_non_closable(app): + """A non-closable window can be created""" + new_window = toga.Window(title="Not Closeable", closable=False, position=(150, 150)) new_window.show() probe = window_probe(app, new_window) - await probe.redraw("Non-closeable window has been shown") + await probe.redraw("Non-closable window has been shown") assert new_window.visible - assert not probe.is_closeable + assert not probe.is_closable # Do a UI close on the window probe.close() @@ -123,7 +120,6 @@ async def test_non_minimizable(app): new_window = toga.Window( title="Not Minimizable", minimizable=False, position=(150, 150) ) - new_window.show() probe = window_probe(app, new_window) diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 422a1a4df8..3de0803483 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -35,7 +35,7 @@ def __init__(self, interface, title, position, size): self.native.Resize += lambda sender, args: self.resize_content() self.resize_content() # Store initial size - if not self.native.interface.resizeable: + if not self.native.interface.resizable: self.native.FormBorderStyle = self.native.FormBorderStyle.FixedSingle self.native.MaximizeBox = False @@ -130,7 +130,7 @@ def winforms_FormClosing(self, sender, event): # If the app is exiting, or a manual close has been requested, # don't get confirmation; just close. if not self.interface.app._impl._is_exiting and not self._is_closing: - if not self.interface.closeable: + if not self.interface.closable: # Closeability is implemented by shortcutting the close handler. event.Cancel = True elif self.interface.on_close._raw: From 9f719f6b8e0d6bc431bf4ff4addeb2de3bfecd48 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 2 Aug 2023 13:18:21 +0800 Subject: [PATCH 12/27] Cocoa dialogs 100% coverage. --- cocoa/src/toga_cocoa/dialogs.py | 90 +++++----- cocoa/src/toga_cocoa/libs/appkit.py | 5 +- cocoa/tests_backend/window.py | 166 ++++++++++++++++++- testbed/tests/test_window.py | 244 +++++++++++++++++++++++++++- 4 files changed, 460 insertions(+), 45 deletions(-) diff --git a/cocoa/src/toga_cocoa/dialogs.py b/cocoa/src/toga_cocoa/dialogs.py index 7951d81f8c..d2e5fd70ed 100644 --- a/cocoa/src/toga_cocoa/dialogs.py +++ b/cocoa/src/toga_cocoa/dialogs.py @@ -7,9 +7,9 @@ NSAlertFirstButtonReturn, NSAlertStyle, NSBezelBorder, - NSFileHandlingPanelOKButton, NSFont, NSMakeRect, + NSModalResponseOK, NSOpenPanel, NSSavePanel, NSScrollView, @@ -20,7 +20,7 @@ class BaseDialog(ABC): def __init__(self, interface): self.interface = interface - self.interface.impl = self + self.interface._impl = self class NSAlertDialog(BaseDialog): @@ -46,21 +46,22 @@ def __init__( self.build_dialog(**kwargs) self.native.beginSheetModalForWindow( - interface.window._impl.native, completionHandler=completion_handler + interface.window._impl.native, + completionHandler=completion_handler, ) def build_dialog(self): pass def completion_handler(self, return_value: int) -> None: - self.on_result(self, None) + self.on_result(None, None) self.interface.future.set_result(None) def bool_completion_handler(self, return_value: int) -> None: result = return_value == NSAlertFirstButtonReturn - self.on_result(self, result) + self.on_result(None, result) self.interface.future.set_result(result) @@ -169,55 +170,62 @@ def __init__( filename, initial_directory, file_types, - multiselect, + multiple_select, on_result=None, ): super().__init__(interface=interface) self.on_result = on_result # Create the panel - self.create_panel(multiselect) + self.create_panel(multiple_select) - # Set all the - self.panel.title = title + # Set the title of the panel + self.native.title = title if filename: - self.panel.nameFieldStringValue = filename + self.native.nameFieldStringValue = filename if initial_directory: - self.panel.directoryURL = NSURL.URLWithString( + self.native.directoryURL = NSURL.URLWithString( str(initial_directory.as_uri()) ) - self.panel.allowedFileTypes = file_types + self.native.allowedFileTypes = file_types - if multiselect: + if multiple_select: handler = self.multi_path_completion_handler else: handler = self.single_path_completion_handler - self.panel.beginSheetModalForWindow( + self.native.beginSheetModalForWindow( interface.window._impl.native, completionHandler=handler, ) + # Provided as a stub that can be mocked in test conditions + def selected_path(self): + return self.native.URL + + # Provided as a stub that can be mocked in test conditions + def selected_paths(self): + return self.native.URLs + def single_path_completion_handler(self, return_value: int) -> None: - if return_value == NSFileHandlingPanelOKButton: - result = Path(self.panel.URL.path) + if return_value == NSModalResponseOK: + result = Path(str(self.selected_path().path)) else: result = None - self.on_result(self, result) - + self.on_result(None, result) self.interface.future.set_result(result) def multi_path_completion_handler(self, return_value: int) -> None: - if return_value == NSFileHandlingPanelOKButton: - result = [Path(url.path) for url in self.panel.URLs] + if return_value == NSModalResponseOK: + result = [Path(url.path) for url in self.selected_paths()] else: result = None - self.on_result(self, result) + self.on_result(None, result) self.interface.future.set_result(result) @@ -238,12 +246,12 @@ def __init__( filename=filename, initial_directory=initial_directory, file_types=None, # File types aren't offered by Cocoa save panels. - multiselect=False, + multiple_select=False, on_result=on_result, ) - def create_panel(self, multiselect): - self.panel = NSSavePanel.alloc().init() + def create_panel(self, multiple_select): + self.native = NSSavePanel.alloc().init() class OpenFileDialog(FileDialog): @@ -253,7 +261,7 @@ def __init__( title, initial_directory, file_types, - multiselect, + multiple_select, on_result=None, ): super().__init__( @@ -262,16 +270,16 @@ def __init__( filename=None, initial_directory=initial_directory, file_types=file_types, - multiselect=multiselect, + multiple_select=multiple_select, on_result=on_result, ) - def create_panel(self, multiselect): - self.panel = NSOpenPanel.alloc().init() - self.panel.allowsMultipleSelection = multiselect - self.panel.canChooseDirectories = False - self.panel.canCreateDirectories = False - self.panel.canChooseFiles = True + def create_panel(self, multiple_select): + self.native = NSOpenPanel.alloc().init() + self.native.allowsMultipleSelection = multiple_select + self.native.canChooseDirectories = False + self.native.canCreateDirectories = False + self.native.canChooseFiles = True class SelectFolderDialog(FileDialog): @@ -280,7 +288,7 @@ def __init__( interface, title, initial_directory, - multiselect, + multiple_select, on_result=None, ): super().__init__( @@ -289,14 +297,14 @@ def __init__( filename=None, initial_directory=initial_directory, file_types=None, - multiselect=multiselect, + multiple_select=multiple_select, on_result=on_result, ) - def create_panel(self, multiselect): - self.panel = NSOpenPanel.alloc().init() - self.panel.allowsMultipleSelection = multiselect - self.panel.canChooseDirectories = True - self.panel.canCreateDirectories = True - self.panel.canChooseFiles = False - self.panel.resolvesAliases = True + def create_panel(self, multiple_select): + self.native = NSOpenPanel.alloc().init() + self.native.allowsMultipleSelection = multiple_select + self.native.canChooseDirectories = True + self.native.canCreateDirectories = True + self.native.canChooseFiles = False + self.native.resolvesAliases = True diff --git a/cocoa/src/toga_cocoa/libs/appkit.py b/cocoa/src/toga_cocoa/libs/appkit.py index 445f172355..4386f44dc0 100644 --- a/cocoa/src/toga_cocoa/libs/appkit.py +++ b/cocoa/src/toga_cocoa/libs/appkit.py @@ -589,8 +589,6 @@ class NSLineBreakMode(Enum): # NSSavePanel.h NSSavePanel = ObjCClass("NSSavePanel") -NSFileHandlingPanelOKButton = 1 - ###################################################################### # NSScreen.h NSScreen = ObjCClass("NSScreen") @@ -774,6 +772,9 @@ class NSWindowStyleMask(IntEnum): HUDWindow = 1 << 13 +NSModalResponseOK = 1 +NSModalResponseCancel = 0 + # NSCompositingOperationXXX is equivalent to NSCompositeXXX NSCompositingOperationClear = 0 NSCompositingOperationCopy = 1 diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 4ae718698e..7e7428f041 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -1,4 +1,16 @@ -from toga_cocoa.libs import NSWindow, NSWindowStyleMask +from unittest.mock import Mock + +from toga_cocoa.libs import ( + NSURL, + NSAlertFirstButtonReturn, + NSAlertSecondButtonReturn, + NSModalResponseCancel, + NSModalResponseOK, + NSOpenPanel, + NSSavePanel, + NSWindow, + NSWindowStyleMask, +) from .probe import BaseProbe @@ -47,3 +59,155 @@ def minimize(self): def unminimize(self): self.native.deminiaturize(None) + + async def close_info_dialog(self, dialog): + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSAlertFirstButtonReturn, + ) + await self.redraw("Info dialog dismissed") + + async def close_question_dialog(self, dialog, result): + if result: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSAlertFirstButtonReturn, + ) + else: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSAlertSecondButtonReturn, + ) + await self.redraw(f"Question dialog ({'YES' if result else 'NO'}) dismissed") + + async def close_confirm_dialog(self, dialog, result): + if result: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSAlertFirstButtonReturn, + ) + else: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSAlertSecondButtonReturn, + ) + + await self.redraw(f"Question dialog ({'OK' if result else 'CANCEL'}) dismissed") + + async def close_error_dialog(self, dialog): + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSAlertFirstButtonReturn, + ) + await self.redraw("Error dialog dismissed") + + async def close_stack_trace_dialog(self, dialog, result): + if result is None: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSAlertFirstButtonReturn, + ) + await self.redraw("Stack trace dialog dismissed") + else: + if result: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSAlertFirstButtonReturn, + ) + else: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSAlertSecondButtonReturn, + ) + + await self.redraw( + f"Stack trace dialog ({'RETRY' if result else 'QUIT'}) dismissed" + ) + + async def close_save_file_dialog(self, dialog, result): + assert isinstance(dialog.native, NSSavePanel) + + if result: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSModalResponseOK, + ) + else: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSModalResponseCancel, + ) + + await self.redraw( + f"Save file dialog ({'SAVE' if result else 'CANCEL'}) dismissed" + ) + + async def close_open_file_dialog(self, dialog, result, multiple_select): + assert isinstance(dialog.native, NSOpenPanel) + + if result is not None: + if multiple_select: + if result: + dialog.selected_paths = Mock( + return_value=[ + NSURL.fileURLWithPath(str(path), isDirectory=False) + for path in result + ] + ) + else: + dialog.selected_path = Mock( + return_value=NSURL.fileURLWithPath( + str(result), + isDirectory=False, + ) + ) + + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSModalResponseOK, + ) + else: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSModalResponseCancel, + ) + + await self.redraw( + f"Open {'multiselect ' if multiple_select else ''}file dialog " + f"({'SAVE' if result else 'CANCEL'}) dismissed" + ) + + async def close_select_folder_dialog(self, dialog, result, multiple_select): + assert isinstance(dialog.native, NSOpenPanel) + + if result is not None: + if multiple_select: + if result: + dialog.selected_paths = Mock( + return_value=[ + NSURL.fileURLWithPath(str(path), isDirectory=True) + for path in result + ] + ) + else: + dialog.selected_path = Mock( + return_value=NSURL.fileURLWithPath( + str(result), + isDirectory=True, + ) + ) + + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSModalResponseOK, + ) + else: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSModalResponseCancel, + ) + + await self.redraw( + f"{'Multiselect' if multiple_select else ' Select'} folder dialog " + f"({'SAVE' if result else 'CANCEL'}) dismissed" + ) diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 904d033099..2dc629bdf2 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -1,6 +1,11 @@ +import io +import traceback from importlib import import_module +from pathlib import Path from unittest.mock import Mock +import pytest + import toga from toga.colors import CORNFLOWERBLUE, GOLDENROD, REBECCAPURPLE from toga.style.pack import COLUMN, Pack @@ -11,6 +16,11 @@ def window_probe(app, window): return getattr(module, "WindowProbe")(app, window) +@pytest.fixture +async def main_window_probe(app, main_window): + yield window_probe(app, main_window) + + async def test_secondary_window(app): """A secondary window can be created""" new_window = toga.Window() @@ -77,7 +87,7 @@ async def test_secondary_window_with_args(app): async def test_non_resizable(app): """A non-resizable window can be created""" new_window = toga.Window( - title="Not Resizable", resizeable=False, position=(150, 150) + title="Not Resizable", resizable=False, position=(150, 150) ) new_window.show() @@ -264,3 +274,235 @@ async def test_full_screen(app): assert probe.content_size == initial_content_size new_window.close() + + +async def test_info_dialog(main_window, main_window_probe): + """An info dialog can be displayed and acknowledged.""" + on_result_handler = Mock() + dialog_result = main_window.info_dialog( + "Info", "Some info", on_result=on_result_handler + ) + await main_window_probe.redraw("Info dialog displayed") + await main_window_probe.close_info_dialog(dialog_result._impl) + + on_result_handler.assert_called_once_with(main_window, None) + assert await dialog_result is 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() + 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) + + on_result_handler.assert_called_once_with(main_window, result) + assert await dialog_result is 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() + 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) + + on_result_handler.assert_called_once_with(main_window, result) + assert await dialog_result is result + + +async def test_error_dialog(main_window, main_window_probe): + """An error dialog can be displayed and acknowledged.""" + on_result_handler = Mock() + 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) + + on_result_handler.assert_called_once_with(main_window, None) + assert await dialog_result is 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) + 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) + + on_result_handler.assert_called_once_with(main_window, result) + assert await dialog_result is 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() + 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) + + if 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. + on_result_handler.call_count == 1 + assert on_result_handler.mock_calls[0].args[0] == main_window + assert on_result_handler.mock_calls[0].args[1].name == Path(filename).name + assert (await dialog_result).name == Path(filename).name + else: + on_result_handler.assert_called_once_with(main_window, None) + assert await dialog_result is None + + +@pytest.mark.parametrize( + "initial_directory, file_types, multiple_select, result", + [ + # Successful single select + (Path(__file__).parent, None, False, Path("/path/to/file1.txt")), + # Cancelled single select + (Path(__file__).parent, None, False, None), + # Successful single select with no initial directory + (None, None, False, Path("/path/to/file1.txt")), + # Successful single select with file types + (Path(__file__).parent, [".txt", ".doc"], False, Path("/path/to/file1.txt")), + # Successful multiple selection + ( + Path(__file__).parent, + None, + True, + [Path("/path/to/file1.txt"), Path("/path/to/file2.txt")], + ), + # Successful multiple selection of no items + (Path(__file__).parent, None, True, []), + # Cancelled multiple selection + (Path(__file__).parent, None, True, None), + # Successful multiple selection with no initial directory + (None, None, True, [Path("/path/to/file1.txt"), Path("/path/to/file2.txt")]), + # Successful multiple selection with file types + ( + Path(__file__).parent, + [".txt", ".doc"], + True, + [Path("/path/to/file1.txt"), Path("/path/to/file2.txt")], + ), + ], +) +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() + 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=multiple_select, + ) + + if result is not None: + on_result_handler.assert_called_once_with(main_window, result) + assert await dialog_result == result + else: + print(dialog_result._impl, on_result_handler, on_result_handler.mock_calls) + on_result_handler.assert_called_once_with(main_window, None) + assert await dialog_result is None + + +@pytest.mark.parametrize( + "initial_directory, multiple_select, result", + [ + # Successful single select + (Path(__file__).parent, False, Path("/path/to/dir1")), + # Cancelled single select + (Path(__file__).parent, False, None), + # Successful single select with no initial directory + (None, False, Path("/path/to/dir1")), + # Successful multiple selection + (Path(__file__).parent, True, [Path("/path/to/dir1"), Path("/path/to/dir2")]), + # Successful multiple selection with no items + (Path(__file__).parent, True, []), + # Cancelled multiple selection + (Path(__file__).parent, 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() + 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=multiple_select, + ) + + if result is not None: + on_result_handler.assert_called_once_with(main_window, result) + assert await dialog_result == result + else: + on_result_handler.assert_called_once_with(main_window, None) + assert await dialog_result is None From e3613f2e8bd7bde63a5db9ab24c830edc1cde751 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 2 Aug 2023 14:36:51 +0800 Subject: [PATCH 13/27] 100% coverage for Window on iOS. --- docs/reference/api/window.rst | 8 + iOS/src/toga_iOS/app.py | 2 +- iOS/src/toga_iOS/dialogs.py | 11 +- iOS/src/toga_iOS/window.py | 11 + iOS/tests_backend/window.py | 68 ++++ testbed/tests/test_window.py | 591 ++++++++++++++++++++-------------- 6 files changed, 449 insertions(+), 242 deletions(-) create mode 100644 iOS/tests_backend/window.py diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index 4c3a0f57aa..3a7e5c1c5e 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -45,6 +45,14 @@ If the operating system provides a way to close the window, Toga will call the permitted. This can be used to implement protections against closing a window with unsaved changes. +Notes +----- + +* A mobile application can only have a single window (the :class:`~toga.MainWindow`), + and that window cannot be moved, resized, hidden, or made full screen. Toga will raise + an exception if you attempt to create a secondary window on a mobile platform. If you try + to modify the size, position, or visibility of the main window, the request will be ignored. + Reference --------- diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index c137e2e07c..48c4f7f653 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -8,7 +8,7 @@ class MainWindow(Window): - pass + _is_main_window = True class PythonAppDelegate(UIResponder): diff --git a/iOS/src/toga_iOS/dialogs.py b/iOS/src/toga_iOS/dialogs.py index 1448bf8fa6..300a52e76a 100644 --- a/iOS/src/toga_iOS/dialogs.py +++ b/iOS/src/toga_iOS/dialogs.py @@ -1,4 +1,4 @@ -from abc import ABC +from abc import ABC, abstractmethod from rubicon.objc import Block from rubicon.objc.runtime import objc_id @@ -14,7 +14,7 @@ class BaseDialog(ABC): def __init__(self, interface): self.interface = interface - self.interface.impl = self + self.interface._impl = self class AlertDialog(BaseDialog): @@ -34,8 +34,9 @@ def __init__(self, interface, title, message, on_result=None): completion=None, ) + @abstractmethod def populate_dialog(self, native): - pass + ... def response(self, value): self.on_result(self, value) @@ -139,7 +140,7 @@ def __init__( title, initial_directory, file_types, - multiselect, + multiple_select, on_result=None, ): super().__init__(interface=interface) @@ -152,7 +153,7 @@ def __init__( interface, title, initial_directory, - multiselect, + multiple_select, on_result=None, ): super().__init__(interface=interface) diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index 5efbb66099..c25135119a 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -7,10 +7,17 @@ class Window: + _is_main_window = False + def __init__(self, interface, title, position, size): self.interface = interface self.interface._impl = self + if not self._is_main_window: + raise RuntimeError( + "Secondary windows cannot be created on mobile platforms" + ) + self.native = UIWindow.alloc().initWithFrame(UIScreen.mainScreen.bounds) # Set up a container for the window's content @@ -84,5 +91,9 @@ def get_visible(self): # The window is always visible return True + def set_full_screen(self, is_full_screen): + # Windows are always full screen + pass + def close(self): pass diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py new file mode 100644 index 0000000000..316d9491e0 --- /dev/null +++ b/iOS/tests_backend/window.py @@ -0,0 +1,68 @@ +import pytest + +from toga_iOS.libs import UIWindow + +from .probe import BaseProbe + + +class WindowProbe(BaseProbe): + def __init__(self, app, window): + super().__init__() + self.app = app + self.window = window + self.impl = window._impl + self.native = window._impl.native + assert isinstance(self.native, UIWindow) + + @property + def content_size(self): + return ( + self.native.contentView.frame.size.width, + self.native.contentView.frame.size.height, + ) + + async def close_info_dialog(self, dialog): + self.native.rootViewController.dismissViewControllerAnimated( + False, completion=None + ) + dialog.native.actions[0].handler(dialog.native) + await self.redraw("Info dialog dismissed") + + async def close_question_dialog(self, dialog, result): + self.native.rootViewController.dismissViewControllerAnimated( + False, completion=None + ) + if result: + dialog.native.actions[0].handler(dialog.native) + else: + dialog.native.actions[1].handler(dialog.native) + await self.redraw(f"Question dialog ({'YES' if result else 'NO'}) dismissed") + + async def close_confirm_dialog(self, dialog, result): + self.native.rootViewController.dismissViewControllerAnimated( + False, completion=None + ) + if result: + dialog.native.actions[0].handler(dialog.native) + else: + dialog.native.actions[1].handler(dialog.native) + await self.redraw(f"Question dialog ({'OK' if result else 'CANCEL'}) dismissed") + + async def close_error_dialog(self, dialog): + self.native.rootViewController.dismissViewControllerAnimated( + False, completion=None + ) + dialog.native.actions[0].handler(dialog.native) + await self.redraw("Error dialog dismissed") + + async def close_stack_trace_dialog(self, dialog, result): + pytest.skip("Stack Trace dialog not implemented on iOS") + + async def close_save_file_dialog(self, dialog, result): + pytest.skip("Save File dialog not implemented on iOS") + + async def close_open_file_dialog(self, dialog, result, multiple_select): + pytest.skip("Open File dialog not implemented on iOS") + + async def close_select_folder_dialog(self, dialog, result, multiple_select): + pytest.skip("Select Folder dialog not implemented on iOS") diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 2dc629bdf2..6c87201087 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -21,259 +21,378 @@ async def main_window_probe(app, main_window): yield window_probe(app, main_window) -async def test_secondary_window(app): - """A secondary window can be created""" - new_window = toga.Window() - probe = window_probe(app, new_window) +async def test_title(main_window, main_window_probe): + """The title of a window can be changed""" + original_title = main_window.title + assert original_title == "Toga Testbed" + await main_window_probe.redraw("Window title can be retrieved") + + try: + main_window.title = "A Different Title" + assert main_window.title == "A Different Title" + await main_window_probe.redraw("Window title can be changed") + finally: + main_window.title = original_title + assert main_window.title == "Toga Testbed" + await main_window_probe.redraw("Window title can be reverted") + + +# Mobile platforms have different windowing characterics, so they have different tests. +if toga.platform.current_platform in {"iOS", "android"}: + #################################################################################### + # Mobile platform tests + #################################################################################### + + async def test_visibility(main_window, main_window_probe): + """Hide and close are no-ops on mobile""" + assert main_window.visible + + main_window.hide() + await main_window_probe.redraw("Window.hide is a no-op") + assert main_window.visible + + main_window.close() + await main_window_probe.redraw("Window.close is a no-op") + assert main_window.visible + + async def test_secondary_window(app): + """A secondary window cannot be created""" + with pytest.raises( + RuntimeError, + match=r"Secondary windows cannot be created on mobile platforms", + ): + toga.Window() + + async def test_move_and_resize(main_window, main_window_probe, capsys): + """Move and resize are no-ops on mobile.""" + initial_size = main_window.size + content_size = main_window_probe.content_size + assert initial_size[0] > 300 + assert initial_size[1] > 500 + + assert main_window.position == (0, 0) + + main_window.position = (150, 50) + await main_window_probe.redraw("Main window can't be moved") + assert main_window.size == initial_size + assert main_window.position == (0, 0) + + main_window.size = (200, 150) + await main_window_probe.redraw("Main window cannot be resized") + assert main_window.size == initial_size + assert main_window.position == (0, 0) + + box1 = toga.Box(style=Pack(background_color=REBECCAPURPLE, width=10, height=10)) + box2 = toga.Box(style=Pack(background_color=GOLDENROD, width=10, height=20)) + main_window.content = toga.Box( + children=[box1, box2], + style=Pack(direction=COLUMN, background_color=CORNFLOWERBLUE), + ) + await main_window_probe.redraw("Main window content has been set") + assert main_window.size == initial_size + assert main_window_probe.content_size == content_size + + # Alter the content width to exceed window width + box1.style.width = 1000 + await main_window_probe.redraw("Content is too wide for the window") + assert main_window.size == initial_size + assert main_window_probe.content_size == content_size + + assert ( + "**WARNING** Window content exceeds available space" + in capsys.readouterr().out + ) + + # Resize content to fit + box1.style.width = 100 + await main_window_probe.redraw("Content fits in window") + assert main_window.size == initial_size + assert main_window_probe.content_size == content_size + + assert ( + "**WARNING** Window content exceeds available space" + not in capsys.readouterr().out + ) + + # Alter the content width to exceed window height + box1.style.height = 2000 + await main_window_probe.redraw("Content is too tall for the window") + assert main_window.size == initial_size + assert main_window_probe.content_size == content_size + + assert ( + "**WARNING** Window content exceeds available space" + in capsys.readouterr().out + ) + + async def test_full_screen(main_window, main_window_probe): + """Window can be made full screen""" + main_window.full_screen = True + await main_window_probe.redraw("Full screen is a no-op") + + main_window.full_screen = False + await main_window_probe.redraw("Full screen is a no-op") + +else: + #################################################################################### + # Desktop platform tests + #################################################################################### + + async def test_secondary_window(app): + """A secondary window can be created""" + new_window = toga.Window() + probe = window_probe(app, new_window) + + new_window.show() + await probe.redraw("New window has been shown") + + assert new_window.app == app + assert new_window in app.windows + + assert new_window.title == "Toga" + assert new_window.size == (640, 480) + assert new_window.position == (100, 100) + assert probe.is_resizable + assert probe.is_closable + assert probe.is_minimizable + + new_window.close() + await probe.redraw("New window has been closed") + + assert new_window not in app.windows + + async def test_secondary_window_with_args(app): + """A secondary window can be created with a specific size and position.""" + on_close_handler = Mock(return_value=False) + + new_window = toga.Window( + title="New Window", + position=(200, 300), + size=(300, 200), + on_close=on_close_handler, + ) + probe = window_probe(app, new_window) + + new_window.show() + await probe.redraw("New window has been shown") + + assert new_window.app == app + assert new_window in app.windows + + assert new_window.title == "New Window" + assert new_window.size == (300, 200) + assert new_window.position == (200, 300) + + probe.close() + await probe.redraw("Attempt to close second window that is rejected") + on_close_handler.assert_called_once_with(new_window) + + assert new_window in app.windows + + # Reset, and try again, this time allowing the + on_close_handler.reset_mock() + on_close_handler.return_value = True + + probe.close() + await probe.redraw("Attempt to close second window that succeeds") + on_close_handler.assert_called_once_with(new_window) - new_window.show() - await probe.redraw("New window has been shown") + assert new_window not in app.windows - assert new_window.app == app - assert new_window in app.windows + async def test_non_resizable(app): + """A non-resizable window can be created""" + new_window = toga.Window( + title="Not Resizable", resizable=False, position=(150, 150) + ) - assert new_window.title == "Toga" - assert new_window.size == (640, 480) - assert new_window.position == (100, 100) - assert probe.is_resizable - assert probe.is_closable - assert probe.is_minimizable + new_window.show() - new_window.close() - await probe.redraw("New window has been closed") + probe = window_probe(app, new_window) + await probe.redraw("Non resizable window has been shown") - assert new_window not in app.windows + assert new_window.visible + assert not probe.is_resizable + # Clean up + new_window.close() -async def test_secondary_window_with_args(app): - """A secondary window can be created with a specific size and position.""" - on_close_handler = Mock(return_value=False) - - new_window = toga.Window( - title="New Window", - position=(200, 300), - size=(300, 200), - on_close=on_close_handler, - ) - probe = window_probe(app, new_window) - - new_window.show() - await probe.redraw("New window has been shown") - - assert new_window.app == app - assert new_window in app.windows - - assert new_window.title == "New Window" - assert new_window.size == (300, 200) - assert new_window.position == (200, 300) - - probe.close() - await probe.redraw("Attempt to close second window that is rejected") - on_close_handler.assert_called_once_with(new_window) - - assert new_window in app.windows - - # Reset, and try again, this time allowing the - on_close_handler.reset_mock() - on_close_handler.return_value = True - - probe.close() - await probe.redraw("Attempt to close second window that succeeds") - on_close_handler.assert_called_once_with(new_window) - - assert new_window not in app.windows - - -async def test_non_resizable(app): - """A non-resizable window can be created""" - new_window = toga.Window( - title="Not Resizable", resizable=False, position=(150, 150) - ) - - new_window.show() - - probe = window_probe(app, new_window) - await probe.redraw("Non resizable window has been shown") - - assert new_window.visible - assert not probe.is_resizable - - # Clean up - new_window.close() + async def test_non_closable(app): + """A non-closable window can be created""" + new_window = toga.Window( + title="Not Closeable", closable=False, position=(150, 150) + ) + new_window.show() + probe = window_probe(app, new_window) + await probe.redraw("Non-closable window has been shown") -async def test_non_closable(app): - """A non-closable window can be created""" - new_window = toga.Window(title="Not Closeable", closable=False, position=(150, 150)) - new_window.show() + assert new_window.visible + assert not probe.is_closable - probe = window_probe(app, new_window) - await probe.redraw("Non-closable window has been shown") + # Do a UI close on the window + probe.close() + await probe.redraw("Close request was ignored") + assert new_window.visible - assert new_window.visible - assert not probe.is_closable + # Do an explicit close on the window + new_window.close() + await probe.redraw("Explicit close was honored") - # Do a UI close on the window - probe.close() - await probe.redraw("Close request was ignored") - assert new_window.visible - - # Do an explicit close on the window - new_window.close() - await probe.redraw("Explicit close was honored") - - assert not new_window.visible - - -async def test_non_minimizable(app): - """A non-minimizable window can be created""" - new_window = toga.Window( - title="Not Minimizable", minimizable=False, position=(150, 150) - ) - new_window.show() + assert not new_window.visible - probe = window_probe(app, new_window) - await probe.redraw("Non-minimizable window has been shown") - assert new_window.visible - assert not probe.is_minimizable + async def test_non_minimizable(app): + """A non-minimizable window can be created""" + new_window = toga.Window( + title="Not Minimizable", minimizable=False, position=(150, 150) + ) + new_window.show() - probe.minimize() - await probe.redraw("Minimize request has been ignored") - assert not probe.is_minimized + probe = window_probe(app, new_window) + await probe.redraw("Non-minimizable window has been shown") + assert new_window.visible + assert not probe.is_minimizable - # Clean up - new_window.close() + probe.minimize() + await probe.redraw("Minimize request has been ignored") + assert not probe.is_minimized + # Clean up + new_window.close() -async def test_visibility(app): - """Visibility of a window can be controlled""" - new_window = toga.Window(title="New Window", position=(200, 250)) - probe = window_probe(app, new_window) - - new_window.show() - await probe.redraw("New window has been shown") - - assert new_window.app == app - assert new_window in app.windows - - assert new_window.visible - assert new_window.size == (640, 480) - assert new_window.position == (200, 250) - - new_window.hide() - await probe.redraw("New window has been hidden") - - assert not new_window.visible - - # Move and resie the window while offscreen - new_window.size = (250, 200) - new_window.position = (300, 150) - - new_window.show() - await probe.redraw("New window has been made visible again") - - assert new_window.visible - assert new_window.size == (250, 200) - assert new_window.position == (300, 150) - - probe.minimize() - # Delay is required to account for "genie" animations - await probe.redraw("Window has been minimized", delay=0.5) - - assert probe.is_minimized - - probe.unminimize() - # Delay is required to account for "genie" animations - await probe.redraw("Window has been unminimized", delay=0.5) - - assert not probe.is_minimized - - probe.close() - await probe.redraw("New window has been closed") - - assert new_window not in app.windows - - -async def test_move_and_resize(app): - """A window can be moved and resized.""" - new_window = toga.Window(title="New Window") - probe = window_probe(app, new_window) - new_window.show() - await probe.redraw("New window has been shown") - - # Determine - extra_width = new_window.size[0] - probe.content_size[0] - extra_height = new_window.size[1] - probe.content_size[1] - - new_window.position = (150, 50) - await probe.redraw("New window has been moved") - assert new_window.position == (150, 50) - - new_window.size = (200, 150) - await probe.redraw("New window has been resized") - assert new_window.size == (200, 150) - assert probe.content_size == (200 - extra_width, 150 - extra_height) - - box1 = toga.Box(style=Pack(background_color=REBECCAPURPLE, width=10, height=10)) - box2 = toga.Box(style=Pack(background_color=GOLDENROD, width=10, height=200)) - new_window.content = toga.Box( - children=[box1, box2], - style=Pack(direction=COLUMN, background_color=CORNFLOWERBLUE), - ) - await probe.redraw("New window has had height adjusted due to content") - assert new_window.size == (200 + extra_width, 210 + extra_height) - assert probe.content_size == (200, 210) - - # Alter the content width to exceed window size - box1.style.width = 250 - await probe.redraw("New window has had width adjusted due to content") - assert new_window.size == (250 + extra_width, 210 + extra_height) - assert probe.content_size == (250, 210) - - # Try to resize to a size less than the content size - new_window.size = (200, 150) - await probe.redraw("New window forced resize fails") - assert new_window.size == (250 + extra_width, 210 + extra_height) - assert probe.content_size == (250, 210) - - new_window.close() - - -async def test_full_screen(app): - """Window can be made full screen""" - new_window = toga.Window(title="New Window", size=(400, 300), position=(150, 150)) - new_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - probe = window_probe(app, new_window) - new_window.show() - await probe.redraw("New window has been shown") - assert not probe.is_full_screen - initial_content_size = probe.content_size - - new_window.full_screen = True - # A short delay to allow for genie animations - await probe.redraw("New window is full screen", delay=1) - assert probe.is_full_screen - assert probe.content_size[0] > initial_content_size[0] - assert probe.content_size[1] > initial_content_size[1] - - new_window.full_screen = True - await probe.redraw("New window is still full screen") - assert probe.is_full_screen - assert probe.content_size[0] > initial_content_size[0] - assert probe.content_size[1] > initial_content_size[1] - - new_window.full_screen = False - # A short delay to allow for genie animations - await probe.redraw("New window is not full screen", delay=1) - assert not probe.is_full_screen - assert probe.content_size == initial_content_size - - new_window.full_screen = False - await probe.redraw("New window is still not full screen") - assert not probe.is_full_screen - assert probe.content_size == initial_content_size - - new_window.close() + async def test_visibility(app): + """Visibility of a window can be controlled""" + new_window = toga.Window(title="New Window", position=(200, 250)) + probe = window_probe(app, new_window) + + new_window.show() + await probe.redraw("New window has been shown") + + assert new_window.app == app + assert new_window in app.windows + + assert new_window.visible + assert new_window.size == (640, 480) + assert new_window.position == (200, 250) + + new_window.hide() + await probe.redraw("New window has been hidden") + + assert not new_window.visible + + # Move and resie the window while offscreen + new_window.size = (250, 200) + new_window.position = (300, 150) + + new_window.show() + await probe.redraw("New window has been made visible again") + + assert new_window.visible + assert new_window.size == (250, 200) + assert new_window.position == (300, 150) + + probe.minimize() + # Delay is required to account for "genie" animations + await probe.redraw("Window has been minimized", delay=0.5) + + assert probe.is_minimized + + probe.unminimize() + # Delay is required to account for "genie" animations + await probe.redraw("Window has been unminimized", delay=0.5) + + assert not probe.is_minimized + + probe.close() + await probe.redraw("New window has been closed") + + assert new_window not in app.windows + + async def test_move_and_resize(app): + """A window can be moved and resized.""" + new_window = toga.Window(title="New Window") + probe = window_probe(app, new_window) + new_window.show() + await probe.redraw("New window has been shown") + + # Determine the extra width consumed by window chrome (e.g., title bars, borders etc) + extra_width = new_window.size[0] - probe.content_size[0] + extra_height = new_window.size[1] - probe.content_size[1] + + new_window.position = (150, 50) + await probe.redraw("New window has been moved") + assert new_window.position == (150, 50) + + new_window.size = (200, 150) + await probe.redraw("New window has been resized") + assert new_window.size == (200, 150) + assert probe.content_size == (200 - extra_width, 150 - extra_height) + + box1 = toga.Box(style=Pack(background_color=REBECCAPURPLE, width=10, height=10)) + box2 = toga.Box(style=Pack(background_color=GOLDENROD, width=10, height=200)) + new_window.content = toga.Box( + children=[box1, box2], + style=Pack(direction=COLUMN, background_color=CORNFLOWERBLUE), + ) + await probe.redraw("New window has had height adjusted due to content") + assert new_window.size == (200 + extra_width, 210 + extra_height) + assert probe.content_size == (200, 210) + + # Alter the content width to exceed window size + box1.style.width = 250 + await probe.redraw("New window has had width adjusted due to content") + assert new_window.size == (250 + extra_width, 210 + extra_height) + assert probe.content_size == (250, 210) + + # Try to resize to a size less than the content size + new_window.size = (200, 150) + await probe.redraw("New window forced resize fails") + assert new_window.size == (250 + extra_width, 210 + extra_height) + assert probe.content_size == (250, 210) + + new_window.close() + + async def test_full_screen(app): + """Window can be made full screen""" + new_window = toga.Window( + title="New Window", size=(400, 300), position=(150, 150) + ) + new_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) + probe = window_probe(app, new_window) + new_window.show() + await probe.redraw("New window has been shown") + assert not probe.is_full_screen + initial_content_size = probe.content_size + + new_window.full_screen = True + # A short delay to allow for genie animations + await probe.redraw("New window is full screen", delay=1) + assert probe.is_full_screen + assert probe.content_size[0] > initial_content_size[0] + assert probe.content_size[1] > initial_content_size[1] + + new_window.full_screen = True + await probe.redraw("New window is still full screen") + assert probe.is_full_screen + assert probe.content_size[0] > initial_content_size[0] + assert probe.content_size[1] > initial_content_size[1] + + new_window.full_screen = False + # A short delay to allow for genie animations + await probe.redraw("New window is not full screen", delay=1) + assert not probe.is_full_screen + assert probe.content_size == initial_content_size + + new_window.full_screen = False + await probe.redraw("New window is still not full screen") + assert not probe.is_full_screen + assert probe.content_size == initial_content_size + + new_window.close() + + +######################################################################################## +# Dialog tests +######################################################################################## async def test_info_dialog(main_window, main_window_probe): From 7cd3fde6fedf1661ca9977fbf1cf4986ba725ef8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 2 Aug 2023 14:57:23 +0800 Subject: [PATCH 14/27] Minor coverage tweaks. --- docs/reference/api/window.rst | 5 +- iOS/src/toga_iOS/container.py | 5 +- testbed/tests/test_window.py | 91 +++++++++++++++++++---------------- testbed/tests/testbed.py | 1 - 4 files changed, 54 insertions(+), 48 deletions(-) diff --git a/docs/reference/api/window.rst b/docs/reference/api/window.rst index 3a7e5c1c5e..6b4251eb94 100644 --- a/docs/reference/api/window.rst +++ b/docs/reference/api/window.rst @@ -50,8 +50,9 @@ Notes * A mobile application can only have a single window (the :class:`~toga.MainWindow`), and that window cannot be moved, resized, hidden, or made full screen. Toga will raise - an exception if you attempt to create a secondary window on a mobile platform. If you try - to modify the size, position, or visibility of the main window, the request will be ignored. + an exception if you attempt to create a secondary window on a mobile platform. If you + try to modify the size, position, or visibility of the main window, the request will + be ignored. Reference --------- diff --git a/iOS/src/toga_iOS/container.py b/iOS/src/toga_iOS/container.py index d23e6c4b73..f2d7e0c25c 100644 --- a/iOS/src/toga_iOS/container.py +++ b/iOS/src/toga_iOS/container.py @@ -39,7 +39,7 @@ def content(self): @content.setter def content(self, widget): - if self._content: + if self.content: self._content.container = None self._content = widget @@ -47,8 +47,7 @@ def content(self, widget): widget.container = self def refreshed(self): - if self.on_refresh: - self.on_refresh(self) + self.on_refresh(self) class Container(BaseContainer): diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 6c87201087..59dbff7480 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -82,48 +82,55 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): assert main_window.size == initial_size assert main_window.position == (0, 0) - box1 = toga.Box(style=Pack(background_color=REBECCAPURPLE, width=10, height=10)) - box2 = toga.Box(style=Pack(background_color=GOLDENROD, width=10, height=20)) - main_window.content = toga.Box( - children=[box1, box2], - style=Pack(direction=COLUMN, background_color=CORNFLOWERBLUE), - ) - await main_window_probe.redraw("Main window content has been set") - assert main_window.size == initial_size - assert main_window_probe.content_size == content_size - - # Alter the content width to exceed window width - box1.style.width = 1000 - await main_window_probe.redraw("Content is too wide for the window") - assert main_window.size == initial_size - assert main_window_probe.content_size == content_size - - assert ( - "**WARNING** Window content exceeds available space" - in capsys.readouterr().out - ) - - # Resize content to fit - box1.style.width = 100 - await main_window_probe.redraw("Content fits in window") - assert main_window.size == initial_size - assert main_window_probe.content_size == content_size - - assert ( - "**WARNING** Window content exceeds available space" - not in capsys.readouterr().out - ) - - # Alter the content width to exceed window height - box1.style.height = 2000 - await main_window_probe.redraw("Content is too tall for the window") - assert main_window.size == initial_size - assert main_window_probe.content_size == content_size - - assert ( - "**WARNING** Window content exceeds available space" - in capsys.readouterr().out - ) + try: + orig_content = main_window.content + + box1 = toga.Box( + style=Pack(background_color=REBECCAPURPLE, width=10, height=10) + ) + box2 = toga.Box(style=Pack(background_color=GOLDENROD, width=10, height=20)) + main_window.content = toga.Box( + children=[box1, box2], + style=Pack(direction=COLUMN, background_color=CORNFLOWERBLUE), + ) + await main_window_probe.redraw("Main window content has been set") + assert main_window.size == initial_size + assert main_window_probe.content_size == content_size + + # Alter the content width to exceed window width + box1.style.width = 1000 + await main_window_probe.redraw("Content is too wide for the window") + assert main_window.size == initial_size + assert main_window_probe.content_size == content_size + + assert ( + "**WARNING** Window content exceeds available space" + in capsys.readouterr().out + ) + + # Resize content to fit + box1.style.width = 100 + await main_window_probe.redraw("Content fits in window") + assert main_window.size == initial_size + assert main_window_probe.content_size == content_size + + assert ( + "**WARNING** Window content exceeds available space" + not in capsys.readouterr().out + ) + + # Alter the content width to exceed window height + box1.style.height = 2000 + await main_window_probe.redraw("Content is too tall for the window") + assert main_window.size == initial_size + assert main_window_probe.content_size == content_size + + assert ( + "**WARNING** Window content exceeds available space" + in capsys.readouterr().out + ) + finally: + main_window.content = orig_content async def test_full_screen(main_window, main_window_probe): """Window can be made full screen""" diff --git a/testbed/tests/testbed.py b/testbed/tests/testbed.py index 185d480f21..fd8da6b187 100644 --- a/testbed/tests/testbed.py +++ b/testbed/tests/testbed.py @@ -137,7 +137,6 @@ def get_terminal_size(*args, **kwargs): else: report_coverage = False - report_coverage = True thread = Thread( target=partial( run_tests, From c867585f95c55b87dc13de868500bdc5abb75619 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 3 Aug 2023 13:36:30 +0800 Subject: [PATCH 15/27] GTK Window and Dialogs to 100%. --- cocoa/tests_backend/window.py | 56 ++++- core/src/toga/window.py | 2 +- gtk/src/toga_gtk/dialogs.py | 124 +++++++--- gtk/src/toga_gtk/window.py | 4 +- gtk/tests_backend/window.py | 216 +++++++++++++++++ iOS/tests_backend/window.py | 3 + testbed/tests/test_window.py | 438 ++++++++++++++++++---------------- 7 files changed, 597 insertions(+), 246 deletions(-) create mode 100644 gtk/tests_backend/window.py diff --git a/cocoa/tests_backend/window.py b/cocoa/tests_backend/window.py index 7e7428f041..55ec6208df 100644 --- a/cocoa/tests_backend/window.py +++ b/cocoa/tests_backend/window.py @@ -1,5 +1,7 @@ from unittest.mock import Mock +from rubicon.objc.collections import ObjCListInstance + from toga_cocoa.libs import ( NSURL, NSAlertFirstButtonReturn, @@ -16,6 +18,10 @@ class WindowProbe(BaseProbe): + supports_minimize_control = True + supports_move_while_hidden = True + supports_unminimize = True + def __init__(self, app, window): super().__init__() self.app = app @@ -24,6 +30,12 @@ def __init__(self, app, window): self.native = window._impl.native assert isinstance(self.native, NSWindow) + async def wait_for_window(self, message, minimize=False, full_screen=False): + await self.redraw( + message, + delay=0.75 if full_screen else 0.5 if minimize else None, + ) + def close(self): self.native.performClose(None) @@ -148,6 +160,11 @@ async def close_open_file_dialog(self, dialog, result, multiple_select): if result is not None: if multiple_select: if result: + # Since we are mocking selected_path(), it's never actually invoked + # under test conditions. Call it just to confirm that it returns the + # type we think it does. + assert isinstance(dialog.selected_paths(), ObjCListInstance) + dialog.selected_paths = Mock( return_value=[ NSURL.fileURLWithPath(str(path), isDirectory=False) @@ -162,10 +179,17 @@ async def close_open_file_dialog(self, dialog, result, multiple_select): ) ) - self.native.endSheet( - self.native.attachedSheet, - returnCode=NSModalResponseOK, - ) + # If there's nothing selected, you can't press OK. + if result: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSModalResponseOK, + ) + else: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSModalResponseCancel, + ) else: self.native.endSheet( self.native.attachedSheet, @@ -174,7 +198,7 @@ async def close_open_file_dialog(self, dialog, result, multiple_select): await self.redraw( f"Open {'multiselect ' if multiple_select else ''}file dialog " - f"({'SAVE' if result else 'CANCEL'}) dismissed" + f"({'OPEN' if result else 'CANCEL'}) dismissed" ) async def close_select_folder_dialog(self, dialog, result, multiple_select): @@ -183,6 +207,11 @@ async def close_select_folder_dialog(self, dialog, result, multiple_select): if result is not None: if multiple_select: if result: + # Since we are mocking selected_path(), it's never actually invoked + # under test conditions. Call it just to confirm that it returns the + # type we think it does. + assert isinstance(dialog.selected_paths(), ObjCListInstance) + dialog.selected_paths = Mock( return_value=[ NSURL.fileURLWithPath(str(path), isDirectory=True) @@ -197,10 +226,17 @@ async def close_select_folder_dialog(self, dialog, result, multiple_select): ) ) - self.native.endSheet( - self.native.attachedSheet, - returnCode=NSModalResponseOK, - ) + # If there's nothing selected, you can't press OK. + if result: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSModalResponseOK, + ) + else: + self.native.endSheet( + self.native.attachedSheet, + returnCode=NSModalResponseCancel, + ) else: self.native.endSheet( self.native.attachedSheet, @@ -209,5 +245,5 @@ async def close_select_folder_dialog(self, dialog, result, multiple_select): await self.redraw( f"{'Multiselect' if multiple_select else ' Select'} folder dialog " - f"({'SAVE' if result else 'CANCEL'}) dismissed" + f"({'OPEN' if result else 'CANCEL'}) dismissed" ) diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 8bb475193a..e95b2c5f3d 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -472,7 +472,7 @@ def stack_trace_dialog( self.factory.dialogs.StackTraceDialog( dialog, title, - message, + message=message, content=content, retry=retry, on_result=wrapped_handler(self, on_result), diff --git a/gtk/src/toga_gtk/dialogs.py b/gtk/src/toga_gtk/dialogs.py index 74e3a81350..7b0395b5ee 100644 --- a/gtk/src/toga_gtk/dialogs.py +++ b/gtk/src/toga_gtk/dialogs.py @@ -7,7 +7,7 @@ class BaseDialog(ABC): def __init__(self, interface): self.interface = interface - self.interface.impl = self + self.interface._impl = self class MessageDialog(BaseDialog): @@ -15,14 +15,15 @@ def __init__( self, interface, title, - message, message_type, buttons, success_result=None, on_result=None, + **kwargs, ): super().__init__(interface=interface) self.on_result = on_result + self.success_result = success_result self.native = Gtk.MessageDialog( transient_for=interface.window._impl.native, @@ -31,22 +32,25 @@ def __init__( buttons=buttons, text=title, ) - self.native.format_secondary_text(message) + self.build_dialog(**kwargs) - return_value = self.native.run() - self.native.destroy() + self.native.connect("response", self.gtk_response) + self.native.show() - if success_result: - result = return_value == success_result + def build_dialog(self, message): + self.native.format_secondary_text(message) + + def gtk_response(self, dialog, response): + if self.success_result: + result = response == self.success_result else: result = None - # def completion_handler(self, return_value: bool) -> None: - if self.on_result: - self.on_result(self, result) - + self.on_result(self, result) self.interface.future.set_result(result) + self.native.destroy() + class InfoDialog(MessageDialog): def __init__(self, interface, title, message, on_result=None): @@ -98,10 +102,59 @@ def __init__(self, interface, title, message, on_result=None): ) -class StackTraceDialog(BaseDialog): - def __init__(self, interface, title, message, on_result=None, **kwargs): - super().__init__(interface=interface) - interface.window.factory.not_implemented("Window.stack_trace_dialog()") +class StackTraceDialog(MessageDialog): + def __init__(self, interface, title, on_result=None, **kwargs): + super().__init__( + interface=interface, + title=title, + message_type=Gtk.MessageType.ERROR, + buttons=( + Gtk.ButtonsType.CANCEL if kwargs.get("retry") else Gtk.ButtonsType.OK + ), + success_result=Gtk.ResponseType.OK if kwargs.get("retry") else None, + on_result=on_result, + **kwargs, + ) + + def build_dialog(self, message, content, retry): + container = self.native.get_message_area() + + self.native.format_secondary_text(message) + + # Create a scrolling readonly text area, in monospace font, to contain the stack trace. + buffer = Gtk.TextBuffer() + buffer.set_text(content) + + trace = Gtk.TextView() + trace.set_buffer(buffer) + trace.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + trace.set_property("editable", False) + trace.set_property("cursor-visible", False) + + trace.get_style_context().add_class("toga") + trace.get_style_context().add_class("stacktrace") + trace.get_style_context().add_class("dialog") + + style_provider = Gtk.CssProvider() + style_provider.load_from_data(b".toga.stacktrace {font-family: monospace;}") + + trace.get_style_context().add_provider( + style_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, + ) + + scroll = Gtk.ScrolledWindow() + scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + scroll.set_size_request(500, 200) + scroll.add(trace) + + container.pack_end(scroll, False, False, 0) + + container.show_all() + + # If this is a retry dialog, add a retry button (which maps to OK). + if retry: + self.native.add_button("Retry", Gtk.ResponseType.OK) class FileDialog(BaseDialog): @@ -112,7 +165,7 @@ def __init__( filename, initial_directory, file_types, - multiselect, + multiple_select, action, ok_icon, on_result=None, @@ -141,26 +194,35 @@ def __init__( filter_filetype.add_pattern("*." + file_type) self.native.add_filter(filter_filetype) - if multiselect: + self.multiple_select = multiple_select + if self.multiple_select: self.native.set_select_multiple(True) - response = self.native.run() + self.native.connect("response", self.gtk_response) + self.native.show() + + # Provided as a stub that can be mocked in test conditions + def selected_path(self): + return self.native.get_filename() + # Provided as a stub that can be mocked in test conditions + def selected_paths(self): + return self.native.get_filenames() + + def gtk_response(self, dialog, response): if response == Gtk.ResponseType.OK: - if multiselect: - result = [Path(filename) for filename in self.native.get_filenames()] + if self.multiple_select: + result = [Path(filename) for filename in self.selected_paths()] else: - result = Path(self.native.get_filename()) + result = Path(self.selected_path()) else: result = None - self.native.destroy() - - if self.on_result: - self.on_result(self, result) - + self.on_result(self, result) self.interface.future.set_result(result) + self.native.destroy() + class SaveFileDialog(FileDialog): def __init__( @@ -178,7 +240,7 @@ def __init__( filename=filename, initial_directory=initial_directory, file_types=file_types, - multiselect=False, + multiple_select=False, action=Gtk.FileChooserAction.SAVE, ok_icon=Gtk.STOCK_SAVE, on_result=on_result, @@ -192,7 +254,7 @@ def __init__( title, initial_directory, file_types, - multiselect, + multiple_select, on_result=None, ): super().__init__( @@ -201,7 +263,7 @@ def __init__( filename=None, initial_directory=initial_directory, file_types=file_types, - multiselect=multiselect, + multiple_select=multiple_select, action=Gtk.FileChooserAction.OPEN, ok_icon=Gtk.STOCK_OPEN, on_result=on_result, @@ -214,7 +276,7 @@ def __init__( interface, title, initial_directory, - multiselect, + multiple_select, on_result=None, ): super().__init__( @@ -223,7 +285,7 @@ def __init__( filename=None, initial_directory=initial_directory, file_types=None, - multiselect=multiselect, + multiple_select=multiple_select, action=Gtk.FileChooserAction.SELECT_FOLDER, ok_icon=Gtk.STOCK_OPEN, on_result=on_result, diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 1eed6c178a..208ee63ffe 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -103,10 +103,8 @@ def get_visible(self): def gtk_delete_event(self, widget, data): if self._is_closing: should_close = True - elif self.interface.on_close._raw: - should_close = self.interface.on_close(self.interface.app) else: - should_close = True + should_close = self.interface.on_close(self.interface.app) # Return value of the GTK on_close handler indicates # whether the event has been fully handled. Returning diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py new file mode 100644 index 0000000000..160f9c464e --- /dev/null +++ b/gtk/tests_backend/window.py @@ -0,0 +1,216 @@ +import asyncio +from pathlib import Path +from unittest.mock import Mock + +import pytest + +from toga_gtk.libs import Gdk, Gtk + +from .probe import BaseProbe + + +class WindowProbe(BaseProbe): + # GTK defers a lot of window behavior to the window manager, which means some features + # either don't exist, or we can't guarantee they behave the way Toga would like. + # 1. No way to create a window without a minimize button + supports_minimize_control = False + # 2. Window manager may not honor changes in position while the window isn't visible + supports_move_while_hidden = False + # 3. Deiconify (i.e., unminimize) isn't guaranteed to actually unminimize the window + supports_unminimize = False + + def __init__(self, app, window): + super().__init__() + self.app = app + self.window = window + self.impl = window._impl + self.native = window._impl.native + assert isinstance(self.native, Gtk.Window) + + async def wait_for_window(self, message, minimize=False, full_screen=False): + await self.redraw(message, delay=0.5 if full_screen or minimize else 0.1) + + def close(self): + if self.is_closable: + self.native.close() + + @property + def content_size(self): + content_allocation = self.impl.container.get_allocation() + return (content_allocation.width, content_allocation.height) + + @property + def is_full_screen(self): + return bool(self.native.get_window().get_state() & Gdk.WindowState.FULLSCREEN) + + @property + def is_resizable(self): + return self.native.get_resizable() + + @property + def is_closable(self): + return self.native.get_deletable() + + @property + def is_minimizable(self): + pytest.xfail("GTK doesn't support disabling minimization") + + @property + def is_minimized(self): + return bool(self.native.get_window().get_state() & Gdk.WindowState.ICONIFIED) + + def minimize(self): + self.native.iconify() + + def unminimize(self): + self.native.deiconify() + + async def wait_for_dialog(self, dialog, message): + # It can take a moment for the dialog to disappear and the response to be + # handled. However, the delay can be variable; use the completion of the future + # as a proxy for "the dialog is done", with a safety catch that will prevent an + # indefinite wait. + await self.redraw(message, delay=0.1) + count = 0 + while dialog.native.get_visible() and count < 20: + await asyncio.sleep(0.1) + count += 1 + + async def close_info_dialog(self, dialog): + dialog.native.response(Gtk.ResponseType.OK) + await self.wait_for_dialog(dialog, "Info dialog dismissed") + + async def close_question_dialog(self, dialog, result): + if result: + dialog.native.response(Gtk.ResponseType.YES) + else: + dialog.native.response(Gtk.ResponseType.NO) + + await self.wait_for_dialog( + dialog, + f"Question dialog ({'YES' if result else 'NO'}) dismissed", + ) + + async def close_confirm_dialog(self, dialog, result): + if result: + dialog.native.response(Gtk.ResponseType.OK) + else: + dialog.native.response(Gtk.ResponseType.CANCEL) + + await self.wait_for_dialog( + dialog, + f"Question dialog ({'OK' if result else 'CANCEL'}) dismissed", + ) + + async def close_error_dialog(self, dialog): + dialog.native.response(Gtk.ResponseType.CANCEL) + await self.wait_for_dialog(dialog, "Error dialog dismissed") + + async def close_stack_trace_dialog(self, dialog, result): + if result is None: + dialog.native.response(Gtk.ResponseType.OK) + await self.wait_for_dialog(dialog, "Stack trace dialog dismissed") + else: + if result: + dialog.native.response(Gtk.ResponseType.OK) + else: + dialog.native.response(Gtk.ResponseType.CANCEL) + + await self.wait_for_dialog( + dialog, + f"Stack trace dialog ({'RETRY' if result else 'QUIT'}) dismissed", + ) + + async def close_save_file_dialog(self, dialog, result): + assert isinstance(dialog.native, Gtk.FileChooserDialog) + + if result: + dialog.native.response(Gtk.ResponseType.OK) + else: + dialog.native.response(Gtk.ResponseType.CANCEL) + + await self.wait_for_dialog( + dialog, + f"Save file dialog ({'SAVE' if result else 'CANCEL'}) dismissed", + ) + + async def close_open_file_dialog(self, dialog, result, multiple_select): + assert isinstance(dialog.native, Gtk.FileChooserDialog) + + # GTK's file dialog shows folders first; but if a folder is selected + # when the "open" button is pressed, it opens that folder. So, we need + # to ensure that a file (any file) is selected so this doesn't happen. + # If it's a multi-select dialog, unselect everythong. + if result == []: + dialog.native.unselect_all() + await self.redraw("All files unselected") + else: + # Find the first file in the folder + for path in Path(dialog.native.get_current_folder()).glob("*"): + if path.is_file(): + break + dialog.native.select_filename(str(path)) + await self.redraw("Selected a single (arbitrary) file") + + if result is not None: + if multiple_select: + if result: + # Since we are mocking selected_path(), it's never actually invoked + # under test conditions. Call it just to confirm that it returns the + # type we think it does. + assert isinstance(dialog.selected_paths(), list) + + dialog.selected_paths = Mock( + return_value=[str(path) for path in result] + ) + else: + dialog.selected_path = Mock(return_value=str(result)) + + # If there's nothing selected, you can't press OK. + if result: + dialog.native.response(Gtk.ResponseType.OK) + else: + dialog.native.response(Gtk.ResponseType.CANCEL) + else: + dialog.native.response(Gtk.ResponseType.CANCEL) + + await self.wait_for_dialog( + dialog, + ( + f"Open {'multiselect ' if multiple_select else ''}file dialog " + f"({'OPEN' if result else 'CANCEL'}) dismissed" + ), + ) + + async def close_select_folder_dialog(self, dialog, result, multiple_select): + assert isinstance(dialog.native, Gtk.FileChooserDialog) + + if result is not None: + if multiple_select: + if result: + # Since we are mocking selected_path(), it's never actually invoked + # under test conditions. Call it just to confirm that it returns the + # type we think it does. + assert isinstance(dialog.selected_paths(), list) + + dialog.selected_paths = Mock( + return_value=[str(path) for path in result] + ) + else: + dialog.selected_path = Mock(return_value=str(result)) + + # If there's nothing selected, you can't press OK. + if result: + dialog.native.response(Gtk.ResponseType.OK) + else: + dialog.native.response(Gtk.ResponseType.CANCEL) + else: + dialog.native.response(Gtk.ResponseType.CANCEL) + + await self.wait_for_dialog( + dialog, + ( + f"{'Multiselect' if multiple_select else ' Select'} folder dialog " + f"({'OPEN' if result else 'CANCEL'}) dismissed" + ), + ) diff --git a/iOS/tests_backend/window.py b/iOS/tests_backend/window.py index 316d9491e0..199353464c 100644 --- a/iOS/tests_backend/window.py +++ b/iOS/tests_backend/window.py @@ -14,6 +14,9 @@ def __init__(self, app, window): self.native = window._impl.native assert isinstance(self.native, UIWindow) + async def wait_for_window(self, message, minimize=False, full_screen=False): + await self.redraw(message) + @property def content_size(self): return ( diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index 59dbff7480..fb2b8f436c 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -16,6 +16,21 @@ def window_probe(app, window): return getattr(module, "WindowProbe")(app, window) +@pytest.fixture +async def second_window(second_window_kwargs): + yield toga.Window(**second_window_kwargs) + + +@pytest.fixture +async def second_window_probe(app, second_window): + second_window.show() + probe = window_probe(app, second_window) + await probe.wait_for_window(f"Window ({second_window.title}) has been created") + yield probe + if second_window in app.windows: + second_window.close() + + @pytest.fixture async def main_window_probe(app, main_window): yield window_probe(app, main_window) @@ -25,16 +40,16 @@ async def test_title(main_window, main_window_probe): """The title of a window can be changed""" original_title = main_window.title assert original_title == "Toga Testbed" - await main_window_probe.redraw("Window title can be retrieved") + await main_window_probe.wait_for_window("Window title can be retrieved") try: main_window.title = "A Different Title" assert main_window.title == "A Different Title" - await main_window_probe.redraw("Window title can be changed") + await main_window_probe.wait_for_window("Window title can be changed") finally: main_window.title = original_title assert main_window.title == "Toga Testbed" - await main_window_probe.redraw("Window title can be reverted") + await main_window_probe.wait_for_window("Window title can be reverted") # Mobile platforms have different windowing characterics, so they have different tests. @@ -48,14 +63,14 @@ async def test_visibility(main_window, main_window_probe): assert main_window.visible main_window.hide() - await main_window_probe.redraw("Window.hide is a no-op") + await main_window_probe.wait_for_window("Window.hide is a no-op") assert main_window.visible main_window.close() - await main_window_probe.redraw("Window.close is a no-op") + await main_window_probe.wait_for_window("Window.close is a no-op") assert main_window.visible - async def test_secondary_window(app): + async def test_secondary_window(): """A secondary window cannot be created""" with pytest.raises( RuntimeError, @@ -73,12 +88,12 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): assert main_window.position == (0, 0) main_window.position = (150, 50) - await main_window_probe.redraw("Main window can't be moved") + await main_window_probe.wait_for_window("Main window can't be moved") assert main_window.size == initial_size assert main_window.position == (0, 0) main_window.size = (200, 150) - await main_window_probe.redraw("Main window cannot be resized") + await main_window_probe.wait_for_window("Main window cannot be resized") assert main_window.size == initial_size assert main_window.position == (0, 0) @@ -93,13 +108,15 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): children=[box1, box2], style=Pack(direction=COLUMN, background_color=CORNFLOWERBLUE), ) - await main_window_probe.redraw("Main window content has been set") + await main_window_probe.wait_for_window("Main window content has been set") assert main_window.size == initial_size assert main_window_probe.content_size == content_size # Alter the content width to exceed window width box1.style.width = 1000 - await main_window_probe.redraw("Content is too wide for the window") + await main_window_probe.wait_for_window( + "Content is too wide for the window" + ) assert main_window.size == initial_size assert main_window_probe.content_size == content_size @@ -110,7 +127,7 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): # Resize content to fit box1.style.width = 100 - await main_window_probe.redraw("Content fits in window") + await main_window_probe.wait_for_window("Content fits in window") assert main_window.size == initial_size assert main_window_probe.content_size == content_size @@ -121,7 +138,9 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): # Alter the content width to exceed window height box1.style.height = 2000 - await main_window_probe.redraw("Content is too tall for the window") + await main_window_probe.wait_for_window( + "Content is too tall for the window" + ) assert main_window.size == initial_size assert main_window_probe.content_size == content_size @@ -135,266 +154,284 @@ async def test_move_and_resize(main_window, main_window_probe, capsys): async def test_full_screen(main_window, main_window_probe): """Window can be made full screen""" main_window.full_screen = True - await main_window_probe.redraw("Full screen is a no-op") + await main_window_probe.wait_for_window("Full screen is a no-op") main_window.full_screen = False - await main_window_probe.redraw("Full screen is a no-op") + await main_window_probe.wait_for_window("Full screen is a no-op") else: #################################################################################### # Desktop platform tests #################################################################################### - async def test_secondary_window(app): + @pytest.mark.parametrize("second_window_kwargs", [{}]) + async def test_secondary_window(app, second_window, second_window_probe): """A secondary window can be created""" - new_window = toga.Window() - probe = window_probe(app, new_window) - - new_window.show() - await probe.redraw("New window has been shown") - - assert new_window.app == app - assert new_window in app.windows + assert second_window.app == app + assert second_window in app.windows - assert new_window.title == "Toga" - assert new_window.size == (640, 480) - assert new_window.position == (100, 100) - assert probe.is_resizable - assert probe.is_closable - assert probe.is_minimizable + assert second_window.title == "Toga" + assert second_window.size == (640, 480) + assert second_window.position == (100, 100) + assert second_window_probe.is_resizable + assert second_window_probe.is_closable + if second_window_probe.supports_minimize_control: + assert second_window_probe.is_minimizable - new_window.close() - await probe.redraw("New window has been closed") + second_window.close() + await second_window_probe.wait_for_window("Secondary window has been closed") - assert new_window not in app.windows + assert second_window not in app.windows - async def test_secondary_window_with_args(app): + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Secondary Window", position=(200, 300), size=(300, 200))], + ) + async def test_secondary_window_with_args(app, second_window, second_window_probe): """A secondary window can be created with a specific size and position.""" on_close_handler = Mock(return_value=False) + second_window.on_close = on_close_handler - new_window = toga.Window( - title="New Window", - position=(200, 300), - size=(300, 200), - on_close=on_close_handler, - ) - probe = window_probe(app, new_window) - - new_window.show() - await probe.redraw("New window has been shown") + second_window.show() + await second_window_probe.wait_for_window("Secondary window has been shown") - assert new_window.app == app - assert new_window in app.windows + assert second_window.app == app + assert second_window in app.windows - assert new_window.title == "New Window" - assert new_window.size == (300, 200) - assert new_window.position == (200, 300) + assert second_window.title == "Secondary Window" + assert second_window.size == (300, 200) + assert second_window.position == (200, 300) - probe.close() - await probe.redraw("Attempt to close second window that is rejected") - on_close_handler.assert_called_once_with(new_window) + second_window_probe.close() + await second_window_probe.wait_for_window( + "Attempt to close second window that is rejected" + ) + on_close_handler.assert_called_once_with(second_window) - assert new_window in app.windows + assert second_window in app.windows # Reset, and try again, this time allowing the on_close_handler.reset_mock() on_close_handler.return_value = True - probe.close() - await probe.redraw("Attempt to close second window that succeeds") - on_close_handler.assert_called_once_with(new_window) - - assert new_window not in app.windows - - async def test_non_resizable(app): - """A non-resizable window can be created""" - new_window = toga.Window( - title="Not Resizable", resizable=False, position=(150, 150) + second_window_probe.close() + await second_window_probe.wait_for_window( + "Attempt to close second window that succeeds" ) + on_close_handler.assert_called_once_with(second_window) - new_window.show() + assert second_window not in app.windows - probe = window_probe(app, new_window) - await probe.redraw("Non resizable window has been shown") - - assert new_window.visible - assert not probe.is_resizable - - # Clean up - new_window.close() + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Not Resizable", resizable=False, position=(200, 150))], + ) + async def test_non_resizable(second_window, second_window_probe): + """A non-resizable window can be created""" + assert second_window.visible + assert not second_window_probe.is_resizable - async def test_non_closable(app): + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Not Closeable", closable=False, position=(200, 150))], + ) + async def test_non_closable(second_window, second_window_probe): """A non-closable window can be created""" - new_window = toga.Window( - title="Not Closeable", closable=False, position=(150, 150) - ) - new_window.show() - - probe = window_probe(app, new_window) - await probe.redraw("Non-closable window has been shown") - - assert new_window.visible - assert not probe.is_closable + assert second_window.visible + assert not second_window_probe.is_closable # Do a UI close on the window - probe.close() - await probe.redraw("Close request was ignored") - assert new_window.visible + second_window_probe.close() + await second_window_probe.wait_for_window("Close request was ignored") + assert second_window.visible # Do an explicit close on the window - new_window.close() - await probe.redraw("Explicit close was honored") + second_window.close() + await second_window_probe.wait_for_window("Explicit close was honored") - assert not new_window.visible + assert not second_window.visible - async def test_non_minimizable(app): + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Not Minimizable", minimizable=False, position=(200, 150))], + ) + async def test_non_minimizable(second_window, second_window_probe): """A non-minimizable window can be created""" - new_window = toga.Window( - title="Not Minimizable", minimizable=False, position=(150, 150) - ) - new_window.show() + assert second_window.visible + assert not second_window_probe.is_minimizable - probe = window_probe(app, new_window) - await probe.redraw("Non-minimizable window has been shown") - assert new_window.visible - assert not probe.is_minimizable + second_window_probe.minimize() + await second_window_probe.wait_for_window("Minimize request has been ignored") + assert not second_window_probe.is_minimized - probe.minimize() - await probe.redraw("Minimize request has been ignored") - assert not probe.is_minimized + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Secondary Window", position=(200, 150))], + ) + async def test_visibility(app, second_window, second_window_probe): + """Visibility of a window can be controlled""" + assert second_window.app == app + assert second_window in app.windows - # Clean up - new_window.close() + assert second_window.visible + assert second_window.size == (640, 480) + assert second_window.position == (200, 150) - async def test_visibility(app): - """Visibility of a window can be controlled""" - new_window = toga.Window(title="New Window", position=(200, 250)) - probe = window_probe(app, new_window) + # Move the window + second_window.position = (250, 200) - new_window.show() - await probe.redraw("New window has been shown") + await second_window_probe.wait_for_window("Secondary window has been moved") + assert second_window.size == (640, 480) + assert second_window.position == (250, 200) - assert new_window.app == app - assert new_window in app.windows + # Resize the window + second_window.size = (300, 250) - assert new_window.visible - assert new_window.size == (640, 480) - assert new_window.position == (200, 250) + await second_window_probe.wait_for_window( + "Secondary window has been resized; position has not changed" + ) + + assert second_window.size == (300, 250) + # We can't confirm position here, because it may have changed. macOS rescales + # windows relative to the bottom-left corner, which means the position of the + # window has changed relative to the Toga coordinate frame. - new_window.hide() - await probe.redraw("New window has been hidden") + second_window.hide() + await second_window_probe.wait_for_window("Secondary window has been hidden") - assert not new_window.visible + assert not second_window.visible - # Move and resie the window while offscreen - new_window.size = (250, 200) - new_window.position = (300, 150) + # Move and resize the window while offscreen + second_window.size = (250, 200) + second_window.position = (300, 150) - new_window.show() - await probe.redraw("New window has been made visible again") + second_window.show() + await second_window_probe.wait_for_window( + "Secondary window has been made visible again; window has moved" + ) - assert new_window.visible - assert new_window.size == (250, 200) - assert new_window.position == (300, 150) + assert second_window.visible + assert second_window.size == (250, 200) + if second_window_probe.supports_move_while_hidden: + assert second_window.position == (300, 150) - probe.minimize() + second_window_probe.minimize() # Delay is required to account for "genie" animations - await probe.redraw("Window has been minimized", delay=0.5) + await second_window_probe.wait_for_window( + "Window has been minimized", + minimize=True, + ) - assert probe.is_minimized + assert second_window_probe.is_minimized - probe.unminimize() - # Delay is required to account for "genie" animations - await probe.redraw("Window has been unminimized", delay=0.5) + if second_window_probe.supports_unminimize: + second_window_probe.unminimize() + # Delay is required to account for "genie" animations + await second_window_probe.wait_for_window( + "Window has been unminimized", + minimize=True, + ) - assert not probe.is_minimized + assert not second_window_probe.is_minimized - probe.close() - await probe.redraw("New window has been closed") + second_window_probe.close() + await second_window_probe.wait_for_window("Secondary window has been closed") - assert new_window not in app.windows + assert second_window not in app.windows - async def test_move_and_resize(app): + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Secondary Window", position=(200, 150))], + ) + async def test_move_and_resize(second_window, second_window_probe): """A window can be moved and resized.""" - new_window = toga.Window(title="New Window") - probe = window_probe(app, new_window) - new_window.show() - await probe.redraw("New window has been shown") # Determine the extra width consumed by window chrome (e.g., title bars, borders etc) - extra_width = new_window.size[0] - probe.content_size[0] - extra_height = new_window.size[1] - probe.content_size[1] - - new_window.position = (150, 50) - await probe.redraw("New window has been moved") - assert new_window.position == (150, 50) - - new_window.size = (200, 150) - await probe.redraw("New window has been resized") - assert new_window.size == (200, 150) - assert probe.content_size == (200 - extra_width, 150 - extra_height) + extra_width = second_window.size[0] - second_window_probe.content_size[0] + extra_height = second_window.size[1] - second_window_probe.content_size[1] + + second_window.position = (150, 50) + await second_window_probe.wait_for_window("Secondary window has been moved") + assert second_window.position == (150, 50) + + second_window.size = (200, 150) + await second_window_probe.wait_for_window("Secondary window has been resized") + assert second_window.size == (200, 150) + assert second_window_probe.content_size == ( + 200 - extra_width, + 150 - extra_height, + ) box1 = toga.Box(style=Pack(background_color=REBECCAPURPLE, width=10, height=10)) box2 = toga.Box(style=Pack(background_color=GOLDENROD, width=10, height=200)) - new_window.content = toga.Box( + second_window.content = toga.Box( children=[box1, box2], style=Pack(direction=COLUMN, background_color=CORNFLOWERBLUE), ) - await probe.redraw("New window has had height adjusted due to content") - assert new_window.size == (200 + extra_width, 210 + extra_height) - assert probe.content_size == (200, 210) + await second_window_probe.wait_for_window( + "Secondary window has had height adjusted due to content" + ) + assert second_window.size == (200 + extra_width, 210 + extra_height) + assert second_window_probe.content_size == (200, 210) # Alter the content width to exceed window size box1.style.width = 250 - await probe.redraw("New window has had width adjusted due to content") - assert new_window.size == (250 + extra_width, 210 + extra_height) - assert probe.content_size == (250, 210) + await second_window_probe.wait_for_window( + "Secondary window has had width adjusted due to content" + ) + assert second_window.size == (250 + extra_width, 210 + extra_height) + assert second_window_probe.content_size == (250, 210) # Try to resize to a size less than the content size - new_window.size = (200, 150) - await probe.redraw("New window forced resize fails") - assert new_window.size == (250 + extra_width, 210 + extra_height) - assert probe.content_size == (250, 210) - - new_window.close() + second_window.size = (200, 150) + await second_window_probe.wait_for_window( + "Secondary window forced resize fails" + ) + assert second_window.size == (250 + extra_width, 210 + extra_height) + assert second_window_probe.content_size == (250, 210) - async def test_full_screen(app): + @pytest.mark.parametrize( + "second_window_kwargs", + [dict(title="Secondary Window", position=(200, 150))], + ) + async def test_full_screen(second_window, second_window_probe): """Window can be made full screen""" - new_window = toga.Window( - title="New Window", size=(400, 300), position=(150, 150) + assert not second_window_probe.is_full_screen + initial_content_size = second_window_probe.content_size + + second_window.full_screen = True + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is full screen", + full_screen=True, ) - new_window.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE)) - probe = window_probe(app, new_window) - new_window.show() - await probe.redraw("New window has been shown") - assert not probe.is_full_screen - initial_content_size = probe.content_size - - new_window.full_screen = True - # A short delay to allow for genie animations - await probe.redraw("New window is full screen", delay=1) - assert probe.is_full_screen - assert probe.content_size[0] > initial_content_size[0] - assert probe.content_size[1] > initial_content_size[1] - - new_window.full_screen = True - await probe.redraw("New window is still full screen") - assert probe.is_full_screen - assert probe.content_size[0] > initial_content_size[0] - assert probe.content_size[1] > initial_content_size[1] - - new_window.full_screen = False - # A short delay to allow for genie animations - await probe.redraw("New window is not full screen", delay=1) - assert not probe.is_full_screen - assert probe.content_size == initial_content_size - - new_window.full_screen = False - await probe.redraw("New window is still not full screen") - assert not probe.is_full_screen - assert probe.content_size == initial_content_size - - new_window.close() + assert second_window_probe.is_full_screen + assert second_window_probe.content_size[0] > initial_content_size[0] + assert second_window_probe.content_size[1] > initial_content_size[1] + + second_window.full_screen = True + await second_window_probe.wait_for_window( + "Secondary window is still full screen" + ) + assert second_window_probe.is_full_screen + assert second_window_probe.content_size[0] > initial_content_size[0] + assert second_window_probe.content_size[1] > initial_content_size[1] + + second_window.full_screen = False + # A longer delay to allow for genie animations + await second_window_probe.wait_for_window( + "Secondary window is not full screen", + full_screen=True, + ) + assert not second_window_probe.is_full_screen + assert second_window_probe.content_size == initial_content_size + + second_window.full_screen = False + await second_window_probe.wait_for_window( + "Secondary window is still not full screen" + ) + assert not second_window_probe.is_full_screen + assert second_window_probe.content_size == initial_content_size ######################################################################################## @@ -578,11 +615,10 @@ async def test_open_file_dialog( multiple_select=multiple_select, ) - if result is not None: + if result: on_result_handler.assert_called_once_with(main_window, result) assert await dialog_result == result else: - print(dialog_result._impl, on_result_handler, on_result_handler.mock_calls) on_result_handler.assert_called_once_with(main_window, None) assert await dialog_result is None @@ -626,7 +662,7 @@ async def test_select_folder_dialog( multiple_select=multiple_select, ) - if result is not None: + if result: on_result_handler.assert_called_once_with(main_window, result) assert await dialog_result == result else: From 066a3840d6e08f6da67e036bc3b5a03c44209dd4 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 3 Aug 2023 13:48:32 +0800 Subject: [PATCH 16/27] Tweak visibility handling to ensure complete coverage in testbed conditions. --- gtk/src/toga_gtk/container.py | 5 +---- gtk/src/toga_gtk/widgets/base.py | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/gtk/src/toga_gtk/container.py b/gtk/src/toga_gtk/container.py index 4072e7f098..9210eb1c8a 100644 --- a/gtk/src/toga_gtk/container.py +++ b/gtk/src/toga_gtk/container.py @@ -178,10 +178,7 @@ def do_size_allocate(self, allocation): # the Toga widget. Toga maintains a tree of children; all nodes # in that tree are direct children of the container. for widget in self.get_children(): - if not widget.get_visible(): - # print(" not visible {widget.interface}") - pass - else: + if widget.get_visible(): # Set the size of the child widget to the computed layout size. # print(f" allocate child {widget.interface}: {widget.interface.layout}") widget_allocation = Gdk.Rectangle() diff --git a/gtk/src/toga_gtk/widgets/base.py b/gtk/src/toga_gtk/widgets/base.py index a9404932b9..dad77145f0 100644 --- a/gtk/src/toga_gtk/widgets/base.py +++ b/gtk/src/toga_gtk/widgets/base.py @@ -152,6 +152,8 @@ def set_alignment(self, alignment): def set_hidden(self, hidden): self.native.set_visible(not hidden) + if self.container: + self.container.make_dirty() def set_color(self, color): self.apply_css("color", get_color_css(color)) From 3bd0b05c1b6feaa26c9633412fca8fb2cc875694 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 4 Aug 2023 09:55:57 +0800 Subject: [PATCH 17/27] Use blackbox as a testing WM. --- .github/workflows/ci.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bba4ea2b7..d9fee1b115 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -207,10 +207,15 @@ jobs: - backend: linux runs-on: ubuntu-22.04 # The package list should be the same as in tutorial-0.rst, and the BeeWare - # tutorial, plus flwm to provide a window manager + # tutorial, plus blackbox to provide a window manager. We need a window + # manager that is reasonably lightweight, honors full screen mode, and + # treats the window position as the top-left corner of the *window*, not the + # top-left corner of the window *content*. The default GNOME window managers of + # most distros meet these requirementt, but they're heavyweight; flwm doesn't + # work either. Blackbox is the lightest WM we've found that works. pre-command: | sudo apt update -y - sudo apt install -y flwm pkg-config python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-webkit2-4.0 + sudo apt install -y blackbox pkg-config python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-webkit2-4.0 # Start Virtual X server echo "Start X server..." @@ -219,7 +224,7 @@ jobs: # Start Window manager echo "Start window manager..." - DISPLAY=:99 flwm & + DISPLAY=:99 blackbox & sleep 1 briefcase-run-prefix: 'DISPLAY=:99' From 054658d6773ab0bc62178d798dcd95d940a63033 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 4 Aug 2023 10:47:35 +0800 Subject: [PATCH 18/27] Rework file dialog setup handling to avoid CI failure. --- gtk/tests_backend/window.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index 160f9c464e..a27d11c2d7 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -1,5 +1,4 @@ import asyncio -from pathlib import Path from unittest.mock import Mock import pytest @@ -75,6 +74,7 @@ async def wait_for_dialog(self, dialog, message): while dialog.native.get_visible() and count < 20: await asyncio.sleep(0.1) count += 1 + assert not dialog.native.get_visible(), "Dialog didn't close" async def close_info_dialog(self, dialog): dialog.native.response(Gtk.ResponseType.OK) @@ -137,20 +137,22 @@ async def close_save_file_dialog(self, dialog, result): async def close_open_file_dialog(self, dialog, result, multiple_select): assert isinstance(dialog.native, Gtk.FileChooserDialog) - # GTK's file dialog shows folders first; but if a folder is selected - # when the "open" button is pressed, it opens that folder. So, we need - # to ensure that a file (any file) is selected so this doesn't happen. - # If it's a multi-select dialog, unselect everythong. - if result == []: - dialog.native.unselect_all() - await self.redraw("All files unselected") - else: - # Find the first file in the folder - for path in Path(dialog.native.get_current_folder()).glob("*"): - if path.is_file(): - break - dialog.native.select_filename(str(path)) + # GTK's file dialog shows folders first; but if a folder is selected when the + # "open" button is pressed, it opens that folder. To prevent this, if we're + # expecting this dialog to return a result, ensure a file is selected. We don't + # care which file it is, as we're mocking the return value of the dialog. + if result: + dialog.native.select_filename(__file__) + # We don't know how long it will take for the GUI to update, so iterate + # for a while until the change has been applied. await self.redraw("Selected a single (arbitrary) file") + count = 0 + while dialog.native.get_filename() != __file__ and count < 10: + await asyncio.sleep(0.1) + count += 1 + assert ( + dialog.native.get_filename() == __file__ + ), "Dialog didn't select dummy file" if result is not None: if multiple_select: From e18c5b1c88fe0b2682515305ebad96f329e98d4f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 4 Aug 2023 11:07:47 +0800 Subject: [PATCH 19/27] More test updates for CI's benefit. --- gtk/tests_backend/window.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/gtk/tests_backend/window.py b/gtk/tests_backend/window.py index a27d11c2d7..44fc078214 100644 --- a/gtk/tests_backend/window.py +++ b/gtk/tests_backend/window.py @@ -1,4 +1,5 @@ import asyncio +from pathlib import Path from unittest.mock import Mock import pytest @@ -187,6 +188,25 @@ async def close_open_file_dialog(self, dialog, result, multiple_select): async def close_select_folder_dialog(self, dialog, result, multiple_select): assert isinstance(dialog.native, Gtk.FileChooserDialog) + # GTK's file dialog might open on default location that doesn't have anything + # that can be selected, which alters closing behavior. To provide consistent + # test conditions, select an arbitrary folder that we know has subfolders. We + # don't care which folder it is, as we're mocking the return value of the + # dialog. + if result: + folder = str(Path(__file__).parent.parent) + dialog.native.set_current_folder(folder) + # We don't know how long it will take for the GUI to update, so iterate + # for a while until the change has been applied. + await self.redraw("Selected a single (arbitrary) folder") + count = 0 + while dialog.native.get_current_folder() != folder and count < 10: + await asyncio.sleep(0.1) + count += 1 + assert ( + dialog.native.get_current_folder() == folder + ), "Dialog didn't select dummy folder" + if result is not None: if multiple_select: if result: From 59cfb50672c440424847157f99074dfd1d068efa Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 5 Aug 2023 11:38:27 +0800 Subject: [PATCH 20/27] Restructure menu creation for cocoa. --- cocoa/src/toga_cocoa/app.py | 65 ++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 251d7af696..54bbd2a2e3 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -135,12 +135,21 @@ def create(self): self.appDelegate.native = self.native self.native.setDelegate_(self.appDelegate) - formal_name = self.interface.formal_name + self._create_app_commands() + + # Call user code to populate the main window + self.interface._startup() + + # Create the lookup table of menu items, + # then force the creation of the menus. + self.create_menus() + def _create_app_commands(self): + formal_name = self.interface.formal_name self.interface.commands.add( # ---- App menu ----------------------------------- toga.Command( - lambda _, **kwargs: self.interface.about(), + self._menu_about, "About " + formal_name, group=toga.Group.APP, ), @@ -176,19 +185,21 @@ def create(self): ), # Quit should always be the last item, in a section on its own toga.Command( - lambda _, **kwargs: self.interface.exit(), + self._menu_exit, "Quit " + formal_name, shortcut=toga.Key.MOD_1 + "q", group=toga.Group.APP, section=sys.maxsize, ), # ---- File menu ---------------------------------- + # This is a bit of an oddity. Safari has 2 distinct "Close Window" and + # "Close All Windows" menu items (partially to differentiate from "Close + # Tab"). Most other Apple HIG apps have a "Close" item that becomes + # "Close All" when you press Option (MOD_2). That behavior isn't something + # we're currently set up to implement, so we live with a separate menu item + # for now. toga.Command( - lambda _, **kwargs: self.interface.current_window._impl.native.performClose( - None - ) - if self.interface.current_window - else None, + self._menu_close_window, "Close Window", shortcut=toga.Key.MOD_1 + "W", group=toga.Group.FILE, @@ -196,10 +207,7 @@ def create(self): section=50, ), toga.Command( - lambda _, **kwargs: [ - window._impl.native.performClose(None) - for window in set(self.interface.windows) - ], + self._menu_close_all_windows, "Close All Windows", shortcut=toga.Key.MOD_2 + toga.Key.MOD_1 + "W", group=toga.Group.FILE, @@ -268,13 +276,9 @@ def create(self): section=10, order=60, ), - # ---- Edit menu ---------------------------------- + # ---- Window menu ---------------------------------- toga.Command( - lambda _, **kwargs: self.interface.current_window._impl.native.miniaturize( - None - ) - if self.interface.current_window - else None, + self._menu_minimize, "Minimize", shortcut=toga.Key.MOD_1 + "m", group=toga.Group.WINDOW, @@ -287,18 +291,24 @@ def create(self): group=toga.Group.HELP, ), ) - self._create_app_commands() - # Call user code to populate the main window - self.interface._startup() + def _menu_about(self, app, **kwargs): + self.interface.about() - # Create the lookup table of menu items, - # then force the creation of the menus. - self.create_menus() + def _menu_exit(self, app, **kwargs): + self.interface.exit() - def _create_app_commands(self): - # No extra commands - pass + def _menu_close_window(self, app, **kwargs): + if self.interface.current_window: + self.interface.current_window._impl.native.performClose(None) + + def _menu_close_all_windows(self, app, **kwargs): + for window in self.interface.windows: + window._impl.native.performClose(None) + + def _menu_minimize(self, app, **kwargs): + if self.interface.current_window: + self.interface.current_window._impl.native.miniaturize(None) def create_menus(self): # Recreate the menu @@ -462,6 +472,7 @@ def select_file(self, **kwargs): class DocumentApp(App): def _create_app_commands(self): + super()._create_app_commands() self.interface.commands.add( toga.Command( lambda _: self.select_file(), From 4fffdbe86d62cf171b88468b6575da37bbc743f5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 5 Aug 2023 11:51:06 +0800 Subject: [PATCH 21/27] Rework default title handling so MainWindow defaults to the formal name as a title. --- core/src/toga/app.py | 6 +++++- core/src/toga/window.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 3b2efc2765..69cf7a320d 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -133,7 +133,7 @@ def __init__( """Create a new application Main Window. :param id: The ID of the window. - :param title: Title for the window. + :param title: Title for the window. Defaults to the formal name of the app. :param position: Position of the window, as a tuple of ``(x, y)`` coordinates. :param size: Size of the window, as a tuple of ``(width, height)``, in pixels. :param resizeable: Can the window be manually resized by the user? @@ -149,6 +149,10 @@ def __init__( minimizable=minimizable, ) + @property + def _default_title(self) -> str: + return App.app.formal_name + @property def on_close(self) -> None: """The handler to invoke before the window is closed in response to a user diff --git a/core/src/toga/window.py b/core/src/toga/window.py index e95b2c5f3d..bd1c6b6ecf 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -65,7 +65,7 @@ class Window: def __init__( self, id: str | None = None, - title: str = "Toga", + title: str | None = None, position: tuple[int, int] = (100, 100), size: tuple[int, int] = (640, 480), resizable: bool = True, @@ -78,7 +78,7 @@ def __init__( """Create a new Window. :param id: The ID of the window. - :param title: Title for the window. + :param title: Title for the window. Defaults to "Toga". :param position: Position of the window, as a tuple of ``(x, y)`` coordinates. :param size: Size of the window, as a tuple of ``(width, height)``, in pixels. :param resizable: Can the window be manually resized by the user? @@ -123,7 +123,7 @@ def __init__( self.factory = get_platform_factory() self._impl = getattr(self.factory, self._WINDOW_CLASS)( interface=self, - title=title, + title=title if title else self._default_title, position=position, size=size, ) @@ -156,6 +156,10 @@ def app(self, app: App) -> None: if self.content: self.content.app = app + @property + def _default_title(self) -> str: + return "Toga" + @property def title(self) -> str: """Title of the window. If no title is provided, the title will default to ``"Toga"``.""" @@ -164,7 +168,7 @@ def title(self) -> str: @title.setter def title(self, title: str) -> None: if not title: - title = "Toga" + title = self._default_title self._impl.set_title(str(title).split("\n")[0]) From ce4130d7c744ad48b5f4c5a647c699f2f4d98a53 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 11 Aug 2023 12:53:36 +0800 Subject: [PATCH 22/27] Add a test of closing window explicitly. --- core/tests/test_window.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/core/tests/test_window.py b/core/tests/test_window.py index 7e65c61505..a5f38bd3ee 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -252,6 +252,25 @@ def test_full_screen(window, app): assert_action_performed_with(window, "set full screen", full_screen=False) +def test_close_direct(window, app): + """A window can be closed directly""" + on_close_handler = Mock(return_value=True) + window.on_close = on_close_handler + + window.show() + assert window.app == app + assert window in app.windows + + # Close the window directly + window.close() + + # Window has been closed, but the close handler has *not* been invoked. + assert window.app == app + assert window not in app.windows + assert_action_performed(window, "close") + on_close_handler.assert_not_called() + + def test_close_no_handler(window, app): """A window without a close handler can be closed""" window.show() @@ -298,7 +317,7 @@ def test_close_rejected_handler(window, app): # Close the window window._impl.simulate_close() - # Window has been closed, and is no longer in the app's list of windows. + # Window has *not* been closed assert window.app == app assert window in app.windows assert_action_not_performed(window, "close") From cc2df8ba1c4434c0e811ad8d467439bbd0107f20 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 16 Aug 2023 10:48:46 +0800 Subject: [PATCH 23/27] Insert a pause on app exit to make sure Briefcase gets all the app logs. --- testbed/src/testbed/app.py | 4 ---- testbed/tests/testbed.py | 15 ++++++--------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/testbed/src/testbed/app.py b/testbed/src/testbed/app.py index bce5ac1eda..20ffd097fe 100644 --- a/testbed/src/testbed/app.py +++ b/testbed/src/testbed/app.py @@ -3,10 +3,6 @@ class Testbed(toga.App): def startup(self): - # A flag that controls whether the test suite should slow down - # so that changes are observable - self.run_slow = False - # Set a default return code for the app, so that a value is # available if the app exits for a reason other than the test # suite exiting/crashing. diff --git a/testbed/tests/testbed.py b/testbed/tests/testbed.py index fd8da6b187..244294b5e4 100644 --- a/testbed/tests/testbed.py +++ b/testbed/tests/testbed.py @@ -2,6 +2,7 @@ import os import sys import tempfile +import time import traceback from functools import partial from pathlib import Path @@ -15,7 +16,7 @@ def run_tests(app, cov, args, report_coverage, run_slow): try: - # Control the run speed of the + # Control the run speed of the test app. app.run_slow = run_slow project_path = Path(__file__).parent.parent @@ -75,6 +76,9 @@ def run_tests(app, cov, args, report_coverage, run_slow): traceback.print_exc() app.returncode = 1 finally: + print(f">>>>>>>>>> EXIT {app.returncode} <<<<<<<<<<") + # Add a short pause to make sure any log tailing gets a chance to flush + time.sleep(0.5) app.add_background_task(lambda app, **kwargs: app.exit()) @@ -147,14 +151,7 @@ def get_terminal_size(*args, **kwargs): report_coverage=report_coverage, ) ) - app.add_background_task(lambda app, *kwargs: thread.start()) - - # Add an on_exit handler that will terminate the test suite. - def exit_suite(app, **kwargs): - print(f">>>>>>>>>> EXIT {app.returncode} <<<<<<<<<<") - return True - - app.on_exit = exit_suite + thread.start() # Start the test app. app.main_loop() From f8a8773d0efeeb5cae0e84296ef3f82c3cfb2ba1 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 4 Aug 2023 13:48:58 +0800 Subject: [PATCH 24/27] Make app and window back references weak. --- 1215.bugfix.rst | 1 + cocoa/src/toga_cocoa/app.py | 10 +++++++++- cocoa/src/toga_cocoa/container.py | 12 +++++++----- cocoa/src/toga_cocoa/widgets/optioncontainer.py | 2 +- cocoa/src/toga_cocoa/widgets/scrollcontainer.py | 2 +- cocoa/src/toga_cocoa/widgets/splitcontainer.py | 5 +---- cocoa/src/toga_cocoa/window.py | 14 ++++++++++++-- core/src/toga/widgets/base.py | 15 ++++++++------- 8 files changed, 40 insertions(+), 21 deletions(-) create mode 100644 1215.bugfix.rst diff --git a/1215.bugfix.rst b/1215.bugfix.rst new file mode 100644 index 0000000000..a77fc7e1ca --- /dev/null +++ b/1215.bugfix.rst @@ -0,0 +1 @@ +A memory leak associated with creation and deletion of windows has been resolved. diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index 54bbd2a2e3..aa556c8f58 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -2,6 +2,7 @@ import inspect import os import sys +import weakref from urllib.parse import unquote, urlparse from rubicon.objc.eventloop import CocoaLifecycle, EventLoopPolicy @@ -112,13 +113,20 @@ class App: def __init__(self, interface): self.interface = interface - self.interface._impl = self self._cursor_visible = True asyncio.set_event_loop_policy(EventLoopPolicy()) self.loop = asyncio.new_event_loop() + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def create(self): self.native = NSApplication.sharedApplication self.native.setActivationPolicy(NSApplicationActivationPolicyRegular) diff --git a/cocoa/src/toga_cocoa/container.py b/cocoa/src/toga_cocoa/container.py index 8cef990686..592769ba6e 100644 --- a/cocoa/src/toga_cocoa/container.py +++ b/cocoa/src/toga_cocoa/container.py @@ -1,3 +1,5 @@ +import weakref + from rubicon.objc import objc_method from .libs import ( @@ -32,7 +34,7 @@ def __init__( min_width=100, min_height=100, layout_native=None, - on_refresh=None, + parent=None, ): """A container for layouts. @@ -45,11 +47,11 @@ def __init__( itself; however, for widgets like ScrollContainer where the layout needs to be computed based on a different size to what will be rendered, the source of the size can be different. - :param on_refresh: The callback to be notified when this container's layout is - refreshed. + :param parent: The parent of this container; this is the object that will be + notified when this container's layout is refreshed. """ self._content = None - self.on_refresh = on_refresh + self.parent = weakref.ref(parent) self.native = TogaView.alloc().init() self.layout_native = self.native if layout_native is None else layout_native @@ -103,7 +105,7 @@ def content(self, widget): widget.container = self def refreshed(self): - self.on_refresh(self) + self.parent().content_refreshed(self) @property def width(self): diff --git a/cocoa/src/toga_cocoa/widgets/optioncontainer.py b/cocoa/src/toga_cocoa/widgets/optioncontainer.py index 66566470ea..5aa04e1b7c 100644 --- a/cocoa/src/toga_cocoa/widgets/optioncontainer.py +++ b/cocoa/src/toga_cocoa/widgets/optioncontainer.py @@ -68,7 +68,7 @@ def content_refreshed(self, container): def add_content(self, index, text, widget): # Create the container for the widget - container = Container(on_refresh=self.content_refreshed) + container = Container(parent=self) container.content = widget self.sub_containers.insert(index, container) diff --git a/cocoa/src/toga_cocoa/widgets/scrollcontainer.py b/cocoa/src/toga_cocoa/widgets/scrollcontainer.py index 5c9226e22b..073ca8b8a7 100644 --- a/cocoa/src/toga_cocoa/widgets/scrollcontainer.py +++ b/cocoa/src/toga_cocoa/widgets/scrollcontainer.py @@ -47,7 +47,7 @@ def create(self): # of the contentView if scrolling is enabled in that axis. self.document_container = Container( layout_native=self.native.contentView, - on_refresh=self.content_refreshed, + parent=self, ) self.native.documentView = self.document_container.native diff --git a/cocoa/src/toga_cocoa/widgets/splitcontainer.py b/cocoa/src/toga_cocoa/widgets/splitcontainer.py index 13948dacd2..82bed9846d 100644 --- a/cocoa/src/toga_cocoa/widgets/splitcontainer.py +++ b/cocoa/src/toga_cocoa/widgets/splitcontainer.py @@ -45,10 +45,7 @@ def create(self): self.native.impl = self self.native.delegate = self.native - self.sub_containers = [ - Container(on_refresh=self.content_refreshed), - Container(on_refresh=self.content_refreshed), - ] + self.sub_containers = [Container(parent=self), Container(parent=self)] self.native.addSubview(self.sub_containers[0].native) self.native.addSubview(self.sub_containers[1].native) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index 3ac7332313..ca17460273 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -1,3 +1,5 @@ +import weakref + from toga.command import Command as BaseCommand from toga_cocoa.container import Container from toga_cocoa.libs import ( @@ -105,7 +107,6 @@ def onToolbarButtonPress_(self, obj) -> None: class Window: def __init__(self, interface, title, position, size): self.interface = interface - self.interface._impl = self mask = NSWindowStyleMask.Titled if self.interface.closable: @@ -139,10 +140,19 @@ def __init__(self, interface, title, position, size): self.native.delegate = self.native - self.container = Container(on_refresh=self.content_refreshed) + self.container = Container(parent=self) self.native.contentView = self.container.native + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def __del__(self): + print("DEL WINDOW IMPL") self.native.release() def create_toolbar(self): diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 68af53e3de..e535951fd3 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -1,5 +1,6 @@ from __future__ import annotations +import weakref from builtins import id as identifier from typing import TYPE_CHECKING, Iterator, NoReturn @@ -201,20 +202,20 @@ def app(self) -> App | None: :raises ValueError: If this widget is already associated with another app. """ - return self._app + return self._app() if self._app else None @app.setter def app(self, app: App | None) -> None: # If the widget is already assigned to an app - if self._app: - if self._app == app: + if self.app: + if self.app == app: # If app is the same as the previous app, return return # Deregister the widget from the old app - self._app.widgets.remove(self.id) + self.app.widgets.remove(self.id) - self._app = app + self._app = weakref.ref(app) if app else None self._impl.set_app(app) for child in self.children: child.app = app @@ -230,7 +231,7 @@ def window(self) -> Window | None: When setting the window for a widget, all children of this widget will be recursively assigned to the same window. """ - return self._window + return self._window() if self._window else None @window.setter def window(self, window: Window | None) -> None: @@ -238,7 +239,7 @@ def window(self, window: Window | None) -> None: if self.window is not None: self.window.widgets.remove(self.id) - self._window = window + self._window = weakref.ref(window) if window else None self._impl.set_window(window) for child in self.children: From b6fdd2dc1139b1ee3b364d410a42c7231217abc0 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 7 Aug 2023 10:03:22 +0800 Subject: [PATCH 25/27] Roll out weakref change across other backends. --- android/src/toga_android/app.py | 10 +++++++++- android/src/toga_android/window.py | 11 ++++++++++- cocoa/src/toga_cocoa/window.py | 1 - gtk/src/toga_gtk/app.py | 10 +++++++++- gtk/src/toga_gtk/window.py | 11 ++++++++++- iOS/src/toga_iOS/app.py | 11 ++++++++++- iOS/src/toga_iOS/container.py | 12 +++++++----- iOS/src/toga_iOS/window.py | 13 +++++++++++-- web/src/toga_web/app.py | 11 ++++++++++- web/src/toga_web/window.py | 11 ++++++++++- winforms/src/toga_winforms/app.py | 10 +++++++++- winforms/src/toga_winforms/window.py | 11 ++++++++++- 12 files changed, 105 insertions(+), 17 deletions(-) diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 269bf49171..5a94967730 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -1,4 +1,5 @@ import asyncio +import weakref from rubicon.java import android_events @@ -173,7 +174,6 @@ def native(self): class App: def __init__(self, interface): self.interface = interface - self.interface._impl = self self._listener = None self.loop = android_events.AndroidEventLoop() @@ -182,6 +182,14 @@ def __init__(self, interface): def native(self): return self._listener.native if self._listener else None + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def create(self): # The `_listener` listens for activity event callbacks. For simplicity, # the app's `.native` is the listener's native Java class. diff --git a/android/src/toga_android/window.py b/android/src/toga_android/window.py index 71d7ea1970..03e6ce9fba 100644 --- a/android/src/toga_android/window.py +++ b/android/src/toga_android/window.py @@ -1,3 +1,5 @@ +import weakref + from decimal import ROUND_UP from .container import Container @@ -23,9 +25,16 @@ class Window(Container): def __init__(self, interface, title, position, size): super().__init__() self.interface = interface - self.interface._impl = self # self.set_title(title) + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def set_app(self, app): self.app = app native_parent = app.native.findViewById(R__id.content) diff --git a/cocoa/src/toga_cocoa/window.py b/cocoa/src/toga_cocoa/window.py index ca17460273..aba092dac6 100644 --- a/cocoa/src/toga_cocoa/window.py +++ b/cocoa/src/toga_cocoa/window.py @@ -152,7 +152,6 @@ def interface(self, value): self._interface = weakref.ref(value) def __del__(self): - print("DEL WINDOW IMPL") self.native.release() def create_toolbar(self): diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 5f7dee07c6..956c3b67eb 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -3,6 +3,7 @@ import os.path import signal import sys +import weakref from urllib.parse import unquote, urlparse import gbulb @@ -55,13 +56,20 @@ class App: def __init__(self, interface): self.interface = interface - self.interface._impl = self gbulb.install(gtk=True) self.loop = asyncio.new_event_loop() self.create() + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def create(self): # Stimulate the build of the app self.native = Gtk.Application( diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 208ee63ffe..2898b4cd6d 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -1,3 +1,5 @@ +import weakref + from toga.command import GROUP_BREAK, SECTION_BREAK from toga.handlers import wrapped_handler @@ -10,7 +12,6 @@ class Window: def __init__(self, interface, title, position, size): self.interface = interface - self.interface._impl = self self._is_closing = False @@ -47,6 +48,14 @@ def __init__(self, interface, title, position, size): self.layout.pack_end(self.container, expand=True, fill=True, padding=0) self.native.add(self.layout) + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def get_title(self): return self.native.get_title() diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index 48c4f7f653..8fa0f55e88 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -1,4 +1,5 @@ import asyncio +import weakref from rubicon.objc import objc_method from rubicon.objc.eventloop import EventLoopPolicy, iOSLifecycle @@ -53,7 +54,7 @@ def application_didChangeStatusBarOrientation_( class App: def __init__(self, interface): self.interface = interface - self.interface._impl = self + # Native instance doesn't exist until the lifecycle completes. self.native = None @@ -63,6 +64,14 @@ def __init__(self, interface): asyncio.set_event_loop_policy(EventLoopPolicy()) self.loop = asyncio.new_event_loop() + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def create(self): """Calls the startup method on the interface.""" self.interface._startup() diff --git a/iOS/src/toga_iOS/container.py b/iOS/src/toga_iOS/container.py index f2d7e0c25c..4dd1800190 100644 --- a/iOS/src/toga_iOS/container.py +++ b/iOS/src/toga_iOS/container.py @@ -1,3 +1,5 @@ +import weakref + from .libs import ( UIApplication, UINavigationController, @@ -15,15 +17,15 @@ class BaseContainer: - def __init__(self, content=None, on_refresh=None): + def __init__(self, content=None, parent=None): """A base class for iOS containers. :param content: The widget impl that is the container's initial content. - :param on_refresh: The callback to be notified when this container's layout is - refreshed. + :param parent: The parent of this container; this is the object that will be + notified when this container's layout is refreshed. """ self._content = content - self.on_refresh = on_refresh + self.parent = weakref.ref(parent) @property def content(self): @@ -47,7 +49,7 @@ def content(self, widget): widget.container = self def refreshed(self): - self.on_refresh(self) + self.parent().content_refreshed(self) class Container(BaseContainer): diff --git a/iOS/src/toga_iOS/window.py b/iOS/src/toga_iOS/window.py index c25135119a..c39e2515c9 100644 --- a/iOS/src/toga_iOS/window.py +++ b/iOS/src/toga_iOS/window.py @@ -1,3 +1,5 @@ +import weakref + from toga_iOS.container import RootContainer from toga_iOS.libs import ( UIColor, @@ -11,7 +13,6 @@ class Window: def __init__(self, interface, title, position, size): self.interface = interface - self.interface._impl = self if not self._is_main_window: raise RuntimeError( @@ -22,7 +23,7 @@ def __init__(self, interface, title, position, size): # Set up a container for the window's content # RootContainer provides a titlebar for the window. - self.container = RootContainer(on_refresh=self.content_refreshed) + self.container = RootContainer(parent=self) # Set the size of the content to the size of the window self.container.native.frame = self.native.bounds @@ -40,6 +41,14 @@ def __init__(self, interface, title, position, size): self.set_title(title) + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def set_content(self, widget): self.container.content = widget diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index fdb71cba10..9f7ce025cc 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -1,3 +1,5 @@ +import weakref + import toga from toga_web.libs import create_element, js from toga_web.window import Window @@ -11,7 +13,14 @@ def on_close(self, *args): class App: def __init__(self, interface): self.interface = interface - self.interface._impl = self + + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) def create(self): # self.resource_path = os.path.dirname(os.path.dirname(NSBundle.mainBundle.bundlePath)) diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 6e437cda4e..aeca0922c3 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -1,10 +1,11 @@ +import weakref + from toga_web.libs import create_element, js class Window: def __init__(self, interface, title, position, size): self.interface = interface - self.interface._impl = self self.native = create_element( "main", @@ -18,6 +19,14 @@ def __init__(self, interface, title, position, size): self.set_title(title) + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def get_title(self): return js.document.title diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index 7a50715fc0..78a6df22bb 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -2,6 +2,7 @@ import re import sys import threading +import weakref import toga from toga import Key @@ -39,7 +40,6 @@ class App: def __init__(self, interface): self.interface = interface - self.interface._impl = self # Winforms app exit is tightly bound to the close of the MainWindow. # The FormClosing message on MainWindow triggers the "on_exit" handler @@ -58,6 +58,14 @@ def __init__(self, interface): self.loop = WinformsProactorEventLoop() asyncio.set_event_loop(self.loop) + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def create(self): self.native = WinForms.Application self.app_context = WinForms.ApplicationContext() diff --git a/winforms/src/toga_winforms/window.py b/winforms/src/toga_winforms/window.py index 506c24f511..af7c01e46d 100644 --- a/winforms/src/toga_winforms/window.py +++ b/winforms/src/toga_winforms/window.py @@ -1,3 +1,5 @@ +import weakref + from toga import GROUP_BREAK, SECTION_BREAK from .container import Container @@ -8,7 +10,6 @@ class Window(Container, Scalable): def __init__(self, interface, title, position, size): self.interface = interface - self.interface._impl = self # Winforms close handling is caught on the FormClosing handler. To allow # for async close handling, we need to be able to abort this close @@ -41,6 +42,14 @@ def __init__(self, interface, title, position, size): self.native.FormBorderStyle = self.native.FormBorderStyle.FixedSingle self.native.MaximizeBox = False + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def create_toolbar(self): if self.interface.toolbar: if self.toolbar_native: From c9ed0f844aa227087601ce6756bc714b8a40ac95 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 7 Aug 2023 11:58:25 +0800 Subject: [PATCH 26/27] Add weakrefs to dummy, and a test of GC on the live backends. --- dummy/src/toga_dummy/app.py | 9 +++++++++ dummy/src/toga_dummy/window.py | 10 ++++++++++ testbed/tests/test_window.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 796b490518..50aef337b6 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -1,4 +1,5 @@ import asyncio +import weakref from .utils import LoggedObject, not_required_on from .window import Window @@ -14,6 +15,14 @@ def __init__(self, interface): self.interface = interface self.loop = asyncio.new_event_loop() + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def create(self): self._action("create") self.interface._startup() diff --git a/dummy/src/toga_dummy/window.py b/dummy/src/toga_dummy/window.py index 97ffbb920e..6762b84e3a 100644 --- a/dummy/src/toga_dummy/window.py +++ b/dummy/src/toga_dummy/window.py @@ -1,3 +1,5 @@ +import weakref + from .utils import LoggedObject, not_required @@ -49,6 +51,14 @@ def __init__(self, interface, title, position, size): self.set_position(position) self.set_size(size) + @property + def interface(self): + return self._interface() + + @interface.setter + def interface(self, value): + self._interface = weakref.ref(value) + def create_toolbar(self): self._action("create toolbar") diff --git a/testbed/tests/test_window.py b/testbed/tests/test_window.py index fb2b8f436c..6b29778b85 100644 --- a/testbed/tests/test_window.py +++ b/testbed/tests/test_window.py @@ -1,5 +1,7 @@ +import gc import io import traceback +import weakref from importlib import import_module from pathlib import Path from unittest.mock import Mock @@ -183,6 +185,32 @@ async def test_secondary_window(app, second_window, second_window_probe): assert second_window not in app.windows + 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 + # because the fixture will retain a reference, preventing garbage collection. + second_window = toga.Window() + second_window.content = toga.Box() + second_window.show() + await app_probe.redraw("Secondary Window has been created") + + # Retain a weak reference to the window to check garbage collection + window_ref = weakref.ref(second_window) + impl_ref = weakref.ref(second_window._impl) + + second_window.close() + await app_probe.redraw("Secondary window has been closed") + + # Clear the local reference to the window (which should be the last reference), + # and force a garbage collection pass. This should cause deletion of both the + # interface and impl of the window. + second_window = None + gc.collect() + + # Assert that the weak references are now dead. + assert window_ref() is None + assert impl_ref() is None + @pytest.mark.parametrize( "second_window_kwargs", [dict(title="Secondary Window", position=(200, 300), size=(300, 200))], From 73714f606ccb9a0515053cb0dfe5df5545153ab3 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 10 Aug 2023 08:41:39 +0800 Subject: [PATCH 27/27] Corrected some inheritance issues with iOS containers. --- iOS/src/toga_iOS/container.py | 16 ++++++++-------- iOS/src/toga_iOS/widgets/scrollcontainer.py | 5 +---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/iOS/src/toga_iOS/container.py b/iOS/src/toga_iOS/container.py index 4dd1800190..9f0f260a3d 100644 --- a/iOS/src/toga_iOS/container.py +++ b/iOS/src/toga_iOS/container.py @@ -53,7 +53,7 @@ def refreshed(self): class Container(BaseContainer): - def __init__(self, content=None, layout_native=None, on_refresh=None): + def __init__(self, content=None, layout_native=None, parent=None): """ :param content: The widget impl that is the container's initial content. :param layout_native: The native widget that should be used to provide size @@ -61,10 +61,10 @@ def __init__(self, content=None, layout_native=None, on_refresh=None): however, for widgets like ScrollContainer where the layout needs to be computed based on a different size to what will be rendered, the source of the size can be different. - :param on_refresh: The callback to be notified when this container's layout is - refreshed. + :param parent: The parent of this container; this is the object that will be + notified when this container's layout is refreshed. """ - super().__init__(content=content, on_refresh=on_refresh) + super().__init__(content=content, parent=parent) self.native = UIView.alloc().init() self.native.translatesAutoresizingMaskIntoConstraints = True @@ -88,7 +88,7 @@ def __init__( self, content=None, layout_native=None, - on_refresh=None, + parent=None, ): """ :param content: The widget impl that is the container's initial content. @@ -97,13 +97,13 @@ def __init__( itself; however, for widgets like ScrollContainer where the layout needs to be computed based on a different size to what will be rendered, the source of the size can be different. - :param on_refresh: The callback to be notified when this container's layout is - refreshed. + :param parent: The parent of this container; this is the object that will be + notified when this container's layout is refreshed. """ super().__init__( content=content, layout_native=layout_native, - on_refresh=on_refresh, + parent=parent, ) # Construct a NavigationController that provides a navigation bar, and diff --git a/iOS/src/toga_iOS/widgets/scrollcontainer.py b/iOS/src/toga_iOS/widgets/scrollcontainer.py index cb32c84c66..4dcf78a567 100644 --- a/iOS/src/toga_iOS/widgets/scrollcontainer.py +++ b/iOS/src/toga_iOS/widgets/scrollcontainer.py @@ -34,10 +34,7 @@ def create(self): self._allow_horizontal = True self._allow_vertical = True - self.document_container = Container( - layout_native=self.native, - on_refresh=self.content_refreshed, - ) + self.document_container = Container(layout_native=self.native, parent=self) self.native.addSubview(self.document_container.native) self.add_constraints()