diff --git a/cocoa/src/toga_cocoa/widgets/splitcontainer.py b/cocoa/src/toga_cocoa/widgets/splitcontainer.py index 416d1113a5..30d8ddd349 100644 --- a/cocoa/src/toga_cocoa/widgets/splitcontainer.py +++ b/cocoa/src/toga_cocoa/widgets/splitcontainer.py @@ -56,7 +56,8 @@ def create(self): def set_bounds(self, x, y, width, height): super().set_bounds(x, y, width, height) for container in self.sub_containers: - container.content.interface.refresh() + if container.content: + container.content.interface.refresh() # Apply any pending split self.native.performSelector( @@ -72,15 +73,19 @@ def set_content(self, content, flex): for index, widget in enumerate(content): # Compute the minimum layout for the content - widget.interface.style.layout(widget.interface, MinimumContainer()) - min_width = widget.interface.layout.width - min_height = widget.interface.layout.height + if widget: + widget.interface.style.layout(widget.interface, MinimumContainer()) + min_width = widget.interface.layout.width + min_height = widget.interface.layout.height - # Create a container with that minimum size, and assign the widget as content - self.sub_containers[index].min_width = min_width - self.sub_containers[index].min_height = min_height + # Create a container with that minimum size, and assign the widget as content + self.sub_containers[index].min_width = min_width + self.sub_containers[index].min_height = min_height - self.sub_containers[index].content = widget + self.sub_containers[index].content = widget + else: + self.sub_containers[index].min_width = 0 + self.sub_containers[index].min_height = 0 # We now know the initial positions of the split. However, we can't *set* the # because Cocoa requires a pixel position, and the widget isn't visible yet. diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index 694b39e4fc..b9111c4df3 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -14,7 +14,10 @@ def __init__( id=None, style=None, direction: Direction = Direction.VERTICAL, - content: list[Widget | tuple[Widget, int]] | None = None, + content: tuple[Widget | tuple[Widget, int], Widget | tuple[Widget, int]] = ( + None, + None, + ), ): """Create a new SplitContainer. @@ -28,7 +31,7 @@ def __init__( :attr:`~toga.constants.Direction.VERTICAL`; defaults to :attr:`~toga.constants.Direction.VERTICAL` :param content: The content that will fill the panels of the SplitContainer. A - list with 2 elements; each item in the list is either: + tuple with 2 elements; each item in the tuple is either: * A tuple consisting of a widget and the initial flex value to apply to that widget in the split. @@ -37,13 +40,10 @@ def __init__( """ super().__init__(id=id, style=style) - self._content = [] - # Create a platform specific implementation of a SplitContainer self._impl = self.factory.SplitContainer(interface=self) - if content: - self.content = content + self.content = content self.direction = direction @property @@ -64,7 +64,7 @@ def focus(self): pass @property - def content(self) -> list[Widget]: + def content(self) -> tuple[Widget, Widget]: """The widgets displayed in the SplitContainer. When retrieved, only the list of widgets is returned. @@ -80,10 +80,15 @@ def content(self) -> list[Widget]: return self._content @content.setter - def content(self, content): - if content is None or len(content) != 2: + def content( + self, content: tuple[Widget | tuple[Widget, int], Widget | tuple[Widget, int]] + ): + try: + if len(content) != 2: + raise TypeError() + except TypeError: raise ValueError( - "SplitContainer content must be a list with exactly 2 elements" + "SplitContainer content must be a sequence with exactly 2 elements" ) _content = [] @@ -92,8 +97,10 @@ def content(self, content): if isinstance(item, tuple): if len(item) == 2: widget, flex_value = item - if flex_value < 1: - flex_value = 1 + if flex_value <= 0: + raise ValueError( + "The flex value for an item in a SplitContainer must be >0" + ) else: raise ValueError( "An item in SplitContainer content must be a 2-tuple " @@ -107,11 +114,15 @@ def content(self, content): _content.append(widget) flex.append(flex_value) - widget.app = self.app - widget.window = self.window + if widget: + widget.app = self.app + widget.window = self.window - self._impl.set_content([w._impl for w in _content], flex) - self._content = _content + self._impl.set_content( + tuple(w._impl if w is not None else None for w in _content), + flex, + ) + self._content = tuple(_content) self.refresh() @Widget.app.setter @@ -120,8 +131,8 @@ def app(self, app): Widget.app.fset(self, app) # Also assign the app to the content in the container - if self.content: - for content in self.content: + for content in self.content: + if content: content.app = app @Widget.window.setter @@ -130,8 +141,8 @@ def window(self, window): Widget.window.fset(self, window) # Also assign the window to the content in the container - if self._content: - for content in self._content: + for content in self._content: + if content: content.window = window @property diff --git a/core/tests/widgets/test_splitcontainer.py b/core/tests/widgets/test_splitcontainer.py index cf34aca923..25433a822c 100644 --- a/core/tests/widgets/test_splitcontainer.py +++ b/core/tests/widgets/test_splitcontainer.py @@ -44,7 +44,7 @@ def test_widget_created(): assert splitcontainer._impl.interface == splitcontainer assert_action_performed(splitcontainer, "create SplitContainer") - assert splitcontainer.content == [] + assert splitcontainer.content == (None, None) assert splitcontainer.direction == toga.SplitContainer.VERTICAL @@ -57,14 +57,14 @@ def test_widget_created_with_values(content1, content2): assert splitcontainer._impl.interface == splitcontainer assert_action_performed(splitcontainer, "create SplitContainer") - assert splitcontainer.content == [content1, content2] + assert splitcontainer.content == (content1, content2) assert splitcontainer.direction == toga.SplitContainer.HORIZONTAL # The content has been assigned to the widget assert_action_performed_with( splitcontainer, "set content", - content=[content1._impl, content2._impl], + content=(content1._impl, content2._impl), flex=[1, 1], ) @@ -72,8 +72,25 @@ def test_widget_created_with_values(content1, content2): assert_action_performed(splitcontainer, "refresh") -def test_assign_to_app(app, splitcontainer, content1, content2): +@pytest.mark.parametrize( + "include_left, include_right", + [ + (False, False), + (True, True), + (False, True), + (True, False), + (True, True), + ], +) +def test_assign_to_app(app, content1, content2, include_left, include_right): """If the widget is assigned to an app, the content is also assigned""" + splitcontainer = toga.SplitContainer( + content=[ + content1 if include_left else None, + content2 if include_right else None, + ] + ) + # Split container is initially unassigned assert splitcontainer.app is None @@ -84,8 +101,11 @@ def test_assign_to_app(app, splitcontainer, content1, content2): assert splitcontainer.app == app # Content is also on the app - assert content1.app == app - assert content2.app == app + if include_left: + assert content1.app == app + + if include_right: + assert content2.app == app def test_assign_to_app_no_content(app): @@ -102,8 +122,25 @@ def test_assign_to_app_no_content(app): assert splitcontainer.app == app -def test_assign_to_window(window, splitcontainer, content1, content2): +@pytest.mark.parametrize( + "include_left, include_right", + [ + (False, False), + (True, True), + (False, True), + (True, False), + (True, True), + ], +) +def test_assign_to_window(window, content1, content2, include_left, include_right): """If the widget is assigned to a window, the content is also assigned""" + splitcontainer = toga.SplitContainer( + content=[ + content1 if include_left else None, + content2 if include_right else None, + ] + ) + # Split container is initially unassigned assert splitcontainer.window is None @@ -112,9 +149,13 @@ def test_assign_to_window(window, splitcontainer, content1, content2): # Split container is on the window assert splitcontainer.window == window + # Content is also on the window - assert content1.window == window - assert content2.window == window + if include_left: + assert content1.window == window + + if include_right: + assert content2.window == window def test_assign_to_window_no_content(window): @@ -150,19 +191,37 @@ def test_focus_noop(splitcontainer): assert_action_not_performed(splitcontainer, "focus") +@pytest.mark.parametrize( + "include_left, include_right", + [ + (False, False), + (True, True), + (False, True), + (True, False), + (True, True), + ], +) def test_set_content_widgets( splitcontainer, content1, content2, content3, + include_left, + include_right, ): """Widget content can be set to a list of widgets""" - splitcontainer.content = [content2, content3] + splitcontainer.content = [ + content2 if include_left else None, + content3 if include_right else None, + ] assert_action_performed_with( splitcontainer, "set content", - content=[content2._impl, content3._impl], + content=( + content2._impl if include_left else None, + content3._impl if include_right else None, + ), flex=[1, 1], ) @@ -170,14 +229,36 @@ def test_set_content_widgets( assert_action_performed(splitcontainer, "refresh") -def test_set_content_flex(splitcontainer, content2, content3): +@pytest.mark.parametrize( + "include_left, include_right", + [ + (False, False), + (True, True), + (False, True), + (True, False), + (True, True), + ], +) +def test_set_content_flex( + splitcontainer, + content2, + content3, + include_left, + include_right, +): """Widget content can be set to a list of widgets with flex values""" - splitcontainer.content = [(content2, 2), (content3, 3)] + splitcontainer.content = [ + (content2 if include_left else None, 2), + (content3 if include_right else None, 3), + ] assert_action_performed_with( splitcontainer, "set content", - content=[content2._impl, content3._impl], + content=( + content2._impl if include_left else None, + content3._impl if include_right else None, + ), flex=[2, 3], ) @@ -185,15 +266,37 @@ def test_set_content_flex(splitcontainer, content2, content3): assert_action_performed(splitcontainer, "refresh") -def test_set_content_flex_altered(splitcontainer, content2, content3): - """Flex values will be manipulated if out of range, and defaulted if missing""" - splitcontainer.content = [content2, (content3, 0)] +@pytest.mark.parametrize( + "include_left, include_right", + [ + (False, False), + (True, True), + (False, True), + (True, False), + (True, True), + ], +) +def test_set_content_flex_mixed( + splitcontainer, + content2, + content3, + include_left, + include_right, +): + """Flex values will be defaulted if missing""" + splitcontainer.content = [ + content2 if include_left else None, + (content3 if include_right else None, 3), + ] assert_action_performed_with( splitcontainer, "set content", - content=[content2._impl, content3._impl], - flex=[1, 1], + content=( + content2._impl if include_left else None, + content3._impl if include_right else None, + ), + flex=[1, 3], ) # The split container has been refreshed @@ -205,19 +308,19 @@ def test_set_content_flex_altered(splitcontainer, content2, content3): [ ( None, - r"SplitContainer content must be a list with exactly 2 elements", + r"SplitContainer content must be a sequence with exactly 2 elements", ), ( [], - r"SplitContainer content must be a list with exactly 2 elements", + r"SplitContainer content must be a sequence with exactly 2 elements", ), ( [toga.Box()], - r"SplitContainer content must be a list with exactly 2 elements", + r"SplitContainer content must be a sequence with exactly 2 elements", ), ( [toga.Box(), toga.Box(), toga.Box()], - r"SplitContainer content must be a list with exactly 2 elements", + r"SplitContainer content must be a sequence with exactly 2 elements", ), ( [toga.Box(), (toga.Box(),)], @@ -229,6 +332,14 @@ def test_set_content_flex_altered(splitcontainer, content2, content3): r"An item in SplitContainer content must be a 2-tuple containing " r"the widget, and the flex weight to assign to that widget.", ), + ( + [toga.Box(), (toga.Box(), 0)], + r"The flex value for an item in a SplitContainer must be >0", + ), + ( + [toga.Box(), (toga.Box(), -1)], + r"The flex value for an item in a SplitContainer must be >0", + ), ], ) def test_set_content_invalid(splitcontainer, content, message):