diff --git a/changes/2438.feature.rst b/changes/2438.feature.rst new file mode 100644 index 0000000000..de78f132c3 --- /dev/null +++ b/changes/2438.feature.rst @@ -0,0 +1 @@ +Windows now have a refresh lock to stop refreshing window layout happening after every add() diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 4233ac1607..68e1f84814 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -242,6 +242,9 @@ def enabled(self, value: bool) -> None: self._impl.set_enabled(bool(value)) def refresh(self) -> None: + if self.window is not None and self.window.locked: + return + self._impl.refresh() # Refresh the layout diff --git a/core/src/toga/window.py b/core/src/toga/window.py index 04c7e6b2b6..0cb9b3c835 100644 --- a/core/src/toga/window.py +++ b/core/src/toga/window.py @@ -28,6 +28,8 @@ from toga.screens import Screen from toga.widgets.base import Widget +from contextlib import contextmanager + class FilteredWidgetRegistry: # A class that exposes a mapping lookup interface, filtered to widgets from a single @@ -174,6 +176,7 @@ def __init__( self._content = None self._is_full_screen = False self._closed = False + self._locked = False self._resizable = resizable self._closable = closable @@ -346,6 +349,29 @@ def size(self, size: tuple[int, int]) -> None: if self.content: self.content.refresh() + @property + def locked(self) -> bool: + """Is the window locked from refreshes?""" + return self._locked + + @locked.setter + def locked(self, locked: bool) -> None: + """Lock or unlock window refresh. + When window is unlocked, refresh the content + """ + self._locked = locked + if not locked: + self.content.refresh() + + @contextmanager + def refresh_lock(self, *args, **kwargs): + """Obtain a refresh lock on the window. Unlocks + automatically when context manager leaves scope. + """ + self.locked = True + yield + self.locked = False + ###################################################################### # Window position ###################################################################### diff --git a/core/tests/test_window.py b/core/tests/test_window.py index 5ea0a05ac4..76b189e96c 100644 --- a/core/tests/test_window.py +++ b/core/tests/test_window.py @@ -169,6 +169,42 @@ def test_change_content(window, app): assert content1.window is None +def test_change_content_locked(window, app): + """The refresh of a window can be locked""" + with window.refresh_lock(): + 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 not (yet) refreshed + assert content1.app == app + assert content1.window == window + assert_action_performed_with(window, "set content", widget=content1._impl) + assert_action_not_performed(content1, "refresh") + + # Set the content of the window to something new + content2 = toga.Box() + window.content = content2 + + # The content has been assigned and not (yet) refreshed + assert content2.app == app + assert content2.window == window + assert_action_performed_with(window, "set content", widget=content2._impl) + assert_action_not_performed(content2, "refresh") + + # The original content has been removed + assert content1.window is None + + # Action refresh must not have been performed on content1 + assert_action_not_performed(content1, "refresh") + + # Action refresh must have been performed on content2 + assert_action_performed(content2, "refresh") + + def test_set_position(window): """The position of the window can be set.""" window.position = (123, 456)