diff --git a/changes/1893.feature.rst b/changes/1893.feature.rst new file mode 100644 index 0000000000..a3d87b4ea9 --- /dev/null +++ b/changes/1893.feature.rst @@ -0,0 +1 @@ +Widgets now have a `.clear()` method to remove all child widgets. diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 64d4e252bb..68b61e2ed8 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -154,12 +154,17 @@ def remove(self, *children): Any nominated child widget that is not a child of this widget will not have any change in parentage. + Refreshes the widget after removal if any children were removed. + Raises ``ValueError`` if this widget cannot have children. :param children: The child nodes to remove. """ + removed = False + for child in children: if child.parent is self: + removed = True super().remove(child) child.app = None @@ -167,9 +172,18 @@ def remove(self, *children): self._impl.remove_child(child._impl) - if self.window: + if self.window and removed: self.window.content.refresh() + def clear(self): + """Remove all child widgets of this node. + + Refreshes the widget after removal if any children were removed. + + Raises ``ValueError`` if this widget cannot have children. + """ + self.remove(*self.children) + @property def app(self): """The App to which this widget belongs. diff --git a/core/tests/widgets/test_base.py b/core/tests/widgets/test_base.py index 8a1f8fa061..28259230b6 100644 --- a/core/tests/widgets/test_base.py +++ b/core/tests/widgets/test_base.py @@ -617,6 +617,92 @@ def test_remove_multiple_children(widget): window.content.refresh.assert_called_once_with() +def test_clear_all_children(widget): + "All children can be simultaneously removed from a widget" + # Add children to the widget + child1 = TestLeafWidget(id="child1_id") + child2 = TestLeafWidget(id="child2_id") + child3 = TestLeafWidget(id="child3_id") + widget.add(child1, child2, child3) + + app = toga.App("Test", "com.example.test") + window = Mock() + widget.app = app + widget.window = window + + assert widget.children == [child1, child2, child3] + for child in widget.children: + assert child.parent == widget + assert child.app == app + assert child.window == window + + # Clear children + widget.clear() + + # Parent doesn't know about the removed children, and vice versa + assert widget.children == [] + assert child1.parent is None + assert child2.parent is None + assert child3.parent is None + + # App and window have been reset on the removed widgets + assert child1.app is None + assert child1.window is None + + assert child2.app is None + assert child2.window is None + + assert child3.app is None + assert child3.window is None + + # The impl's remove_child has been invoked thrice + assert_action_performed_with(widget, "remove child", child=child1._impl) + assert_action_performed_with(widget, "remove child", child=child2._impl) + assert_action_performed_with(widget, "remove child", child=child3._impl) + + # The window layout has been refreshed once + window.content.refresh.assert_called_once_with() + + +def test_clear_no_children(widget): + "No changes are made (no-op) if widget has no children" + app = toga.App("Test", "com.example.test") + window = Mock() + widget.app = app + widget.window = window + + assert widget.children == [] + + # Clear children + widget.clear() + + # Parent doesn't have any children still + assert widget.children == [] + + # The window layout has not been refreshed + window.content.refresh.assert_not_called() + + +def test_clear_leaf_node(): + "No changes are made to leaf node that cannot have children" + leaf = TestLeafWidget() + app = toga.App("Test", "com.example.test") + window = Mock() + leaf.app = app + leaf.window = window + + assert leaf.children == [] + + # Clear children + leaf.clear() + + # Parent doesn't have any children still + assert leaf.children == [] + + # The window layout has not been refreshed + window.content.refresh.assert_not_called() + + def test_remove_from_non_parent(widget): "Trying to remove a child from a widget other than it's parent is a no-op" # Create a second parent widget, and add a child to it