From 40140cd080f53c5c375527431b972122501b1d4b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 21 Mar 2023 13:46:27 +0800 Subject: [PATCH 1/6] Update docs and tests for Box. --- core/src/toga/widgets/box.py | 34 ++++++------------ core/tests/test_deprecated_factory.py | 6 ---- core/tests/widgets/test_box.py | 39 ++++++++++++++------ docs/reference/api/containers/box.rst | 51 ++++++++++----------------- 4 files changed, 56 insertions(+), 74 deletions(-) diff --git a/core/src/toga/widgets/box.py b/core/src/toga/widgets/box.py index 4001127020..97ba28a080 100644 --- a/core/src/toga/widgets/box.py +++ b/core/src/toga/widgets/box.py @@ -1,38 +1,24 @@ -import warnings - from .base import Widget class Box(Widget): - """This is a Widget that contains other widgets, but has no rendering or - interaction of its own. - - Args: - id (str): An identifier for this widget. - style (:class:~colosseum.CSSNode`): An optional style object. If no - style is provided then a new one will be created for the widget. - children (``list`` of :class:`~toga.Widget`): An optional list of child - Widgets that will be in this box. - """ - def __init__( self, id=None, style=None, children=None, - factory=None, # DEPRECATED! ): - super().__init__(id=id, style=style) + """Create a new Box container widget. + + Inherits from :class:`~toga.widgets.base.Widget`. - ###################################################################### - # 2022-09: Backwards compatibility - ###################################################################### - # factory no longer used - if factory: - warnings.warn("The factory argument is no longer used.", DeprecationWarning) - ###################################################################### - # End backwards compatibility. - ###################################################################### + :param text: The text to display on the button. + :param id: The ID for the widget. + :param style: A style object. If no style is provided, a default style + will be applied to the widget. + :param children: An optional list of children for to add to the Box. + """ + super().__init__(id=id, style=style) self._children = [] if children: diff --git a/core/tests/test_deprecated_factory.py b/core/tests/test_deprecated_factory.py index 2a434b873f..4d1713ad96 100644 --- a/core/tests/test_deprecated_factory.py +++ b/core/tests/test_deprecated_factory.py @@ -76,12 +76,6 @@ def test_window(self): self.assertEqual(widget._impl.interface, widget) self.assertNotEqual(widget.factory, self.factory) - def test_box_created(self): - with self.assertWarns(DeprecationWarning): - widget = toga.Box(children=[toga.Widget()], 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/core/tests/widgets/test_box.py b/core/tests/widgets/test_box.py index c0f41a2054..bfd5e2a57d 100644 --- a/core/tests/widgets/test_box.py +++ b/core/tests/widgets/test_box.py @@ -1,16 +1,33 @@ import toga -from toga_dummy.utils import TestCase +from toga_dummy.utils import ( + assert_action_not_performed, + assert_action_performed, +) -class BoxTests(TestCase): - def setUp(self): - super().setUp() - self.children = [toga.Widget()] - self.box = toga.Box(children=self.children) +def test_create_box(): + "A Box can be created." + box = toga.Box() + # Round trip the impl/interface + assert box._impl.interface == box - def test_widget_created(self): - self.assertEqual(self.box._impl.interface, self.box) - self.assertActionPerformed(self.box, "create Box") + assert_action_performed(box, "create Box") + assert_action_not_performed(box, "add child") - def test_children_added(self): - self.assertEqual(self.box._children, self.children) + +def test_create_box_with_children(): + "A Box can be created with children." + child1 = toga.Box() + child2 = toga.Box() + box = toga.Box(children=[child1, child2]) + + # Round trip the impl/interface + assert box._impl.interface == box + + assert_action_performed(box, "create Box") + # The impl-level add-child will not be called, + # because the box hasn't been assigned to a window + assert_action_not_performed(box, "add child") + + # But the box will have children. + assert box.children == [child1, child2] diff --git a/docs/reference/api/containers/box.rst b/docs/reference/api/containers/box.rst index 2b7c8ea2a1..3330d0afa8 100644 --- a/docs/reference/api/containers/box.rst +++ b/docs/reference/api/containers/box.rst @@ -1,6 +1,8 @@ Box === +A generic container for other widgets. Used to construct layouts. + .. rst-class:: widget-support .. csv-filter:: Availability (:ref:`Key `) :header-rows: 1 @@ -8,57 +10,40 @@ Box :included_cols: 4,5,6,7,8,9 :exclude: {0: '(?!(Box|Component))'} -The box is a generic container for widgets, allowing you to construct layouts. - Usage ----- -A box can be instantiated with no children and the children added later: - -.. code-block:: Python - - import toga - - box = toga.Box('box1') - - button = toga.Button('Hello world', on_press=button_handler) - box.add(button) - -To create boxes within boxes, use the children argument: +An empty Box can be constructed without any children, with children added to the +box after construction: .. code-block:: Python import toga - box_a = toga.Box('box_a') - box_b = toga.Box('box_b') + box = toga.Box() - box = toga.Box('box', children=[box_a, box_b]) + label1 = toga.Label('Hello') + label2 = toga.Label('World') -Box Styling ------------ + box.add(label1) + box.add(label2) -Styling of boxes can be done during instantiation of the Box: +Alternatively, children can be specified at the time the box is constructed: .. code-block:: Python import toga - from toga.style import Pack - from toga.style.pack import COLUMN - - box = toga.Box(id='box', style=Pack(direction=COLUMN, padding_top=10)) - -Styles can be also be updated on an existing instance: -.. code-block:: Python - - import toga - from toga.style import Pack - from toga.style.pack import COLUMN + label1 = toga.Label('Hello') + label2 = toga.Label('World') - box = toga.Box(id='box', style=Pack(direction=COLUMN)) + box = toga.Box(children=[label1, label2]) - box.style.update(padding_top=10) +In most apps, a layout is constructed by building a tree of boxes inside boxes, +with concrete widgets (such as :class:`~toga.widgets.label.Label` or +:class:`~toga.widgets.button.Button`) forming the leaf nodes of the tree. Style +directives can be applied to enforce padding around the outside of the box, +direction of child stacking inside the box, and background color of the box. Reference --------- From 3049c3b70317bff51c5e7ee5671978d545c053ad Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 21 Mar 2023 13:46:38 +0800 Subject: [PATCH 2/6] Add Changenote. --- changes/1820.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/1820.feature.rst diff --git a/changes/1820.feature.rst b/changes/1820.feature.rst new file mode 100644 index 0000000000..8bc73e8e69 --- /dev/null +++ b/changes/1820.feature.rst @@ -0,0 +1 @@ +The Box widget now has 100% test coverage, and complete API documentation. From ab622e239cfacee3711e1b8fcd3dadfba53fa30d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 21 Mar 2023 14:17:17 +0800 Subject: [PATCH 3/6] Add testbed tests for Box, plus Cocoa probe. --- cocoa/src/toga_cocoa/widgets/box.py | 5 ---- cocoa/tests_backend/widgets/base.py | 13 +++++++++ cocoa/tests_backend/widgets/box.py | 7 +++++ cocoa/tests_backend/widgets/label.py | 11 ------- testbed/tests/widgets/properties.py | 43 ++++++++++++++++++++++++++++ testbed/tests/widgets/test_box.py | 15 ++++++++++ 6 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 cocoa/tests_backend/widgets/box.py create mode 100644 testbed/tests/widgets/test_box.py diff --git a/cocoa/src/toga_cocoa/widgets/box.py b/cocoa/src/toga_cocoa/widgets/box.py index bc5454dd65..df5ebec834 100644 --- a/cocoa/src/toga_cocoa/widgets/box.py +++ b/cocoa/src/toga_cocoa/widgets/box.py @@ -11,11 +11,6 @@ def isFlipped(self) -> bool: # Default Cocoa coordinate frame is around the wrong way. return True - @objc_method - def display(self) -> None: - self.layer.needsDisplay = True - self.layer.displayIfNeeded() - class Box(Widget): def create(self): diff --git a/cocoa/tests_backend/widgets/base.py b/cocoa/tests_backend/widgets/base.py index b1a83c4462..728dd325fb 100644 --- a/cocoa/tests_backend/widgets/base.py +++ b/cocoa/tests_backend/widgets/base.py @@ -1,7 +1,10 @@ import asyncio +from toga.colors import TRANSPARENT from toga.fonts import CURSIVE, FANTASY, MONOSPACE, SANS_SERIF, SERIF, SYSTEM +from .properties import toga_color + class SimpleProbe: def __init__(self, widget): @@ -55,5 +58,15 @@ def width(self): def height(self): return self.native.frame.size.height + @property + def background_color(self): + if self.native.drawsBackground: + if self.native.backgroundColor: + return toga_color(self.native.backgroundColor) + else: + return None + else: + return TRANSPARENT + def press(self): self.native.performClick(None) diff --git a/cocoa/tests_backend/widgets/box.py b/cocoa/tests_backend/widgets/box.py new file mode 100644 index 0000000000..0bc31e433e --- /dev/null +++ b/cocoa/tests_backend/widgets/box.py @@ -0,0 +1,7 @@ +from toga_cocoa.libs import NSView + +from .base import SimpleProbe + + +class BoxProbe(SimpleProbe): + native_class = NSView diff --git a/cocoa/tests_backend/widgets/label.py b/cocoa/tests_backend/widgets/label.py index b26fdfac6b..d4faa7e066 100644 --- a/cocoa/tests_backend/widgets/label.py +++ b/cocoa/tests_backend/widgets/label.py @@ -1,4 +1,3 @@ -from toga.colors import TRANSPARENT from toga_cocoa.libs import NSTextField from .base import SimpleProbe @@ -16,16 +15,6 @@ def text(self): def color(self): return toga_color(self.native.textColor) - @property - def background_color(self): - if self.native.drawsBackground: - if self.native.backgroundColor: - return toga_color(self.native.backgroundColor) - else: - return None - else: - return TRANSPARENT - @property def font(self): return toga_font(self.native.font) diff --git a/testbed/tests/widgets/properties.py b/testbed/tests/widgets/properties.py index ca0d967170..d5599411c8 100644 --- a/testbed/tests/widgets/properties.py +++ b/testbed/tests/widgets/properties.py @@ -147,6 +147,49 @@ async def test_background_color_transparent(widget, probe): assert_color(probe.background_color, TRANSPARENT) +async def test_flex_widget_size(widget, probe): + "The widget can expand in either axis." + # Container is initially a non-flex row box. Paint it red so we can see it. + widget.style.background_color = RED + await probe.redraw() + + # Check the initial widget size + assert probe.width == 100 + assert probe.height == 100 + + # Drop the fixed height, and make the widget flexible + widget.style.flex = 1 + del widget.style.height + + # Widget should now be 100 pixels wide, but as tall as the container. + await probe.redraw() + assert probe.width == 100 + assert probe.height > 300 + + # Make the parent a COLUMN box + del widget.style.width + widget.parent.style.direction = COLUMN + + # Widget should now be the size of the container + await probe.redraw() + assert probe.width > 300 + assert probe.height > 300 + + # Revert to fixed height + widget.style.height = 150 + + await probe.redraw() + assert probe.width > 300 + assert probe.height == 150 + + # Revert to fixed width + widget.style.width = 150 + + await probe.redraw() + assert probe.width == 150 + assert probe.height == 150 + + async def test_flex_horizontal_widget_size(widget, probe): "Check that a widget that is flexible in the horizontal axis resizes as expected" # Container is initially a non-flex row box. diff --git a/testbed/tests/widgets/test_box.py b/testbed/tests/widgets/test_box.py new file mode 100644 index 0000000000..93efac2b6e --- /dev/null +++ b/testbed/tests/widgets/test_box.py @@ -0,0 +1,15 @@ +import pytest + +import toga +from toga.style import Pack + +from .properties import ( # noqa: F401 + test_background_color, + test_background_color_reset, + test_flex_widget_size, +) + + +@pytest.fixture +async def widget(): + return toga.Box(style=Pack(width=100, height=100)) From aa0988e5baa92c32232c1aa44ed28e7d9cc02c2e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 21 Mar 2023 14:29:52 +0800 Subject: [PATCH 4/6] Add Android Box probe. --- android/src/toga_android/widgets/box.py | 3 --- android/tests_backend/widgets/box.py | 12 ++++++++++++ android/tests_backend/widgets/label.py | 4 ---- testbed/tests/widgets/properties.py | 15 +++++++++------ 4 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 android/tests_backend/widgets/box.py diff --git a/android/src/toga_android/widgets/box.py b/android/src/toga_android/widgets/box.py index ef2c0eae9b..d4f0ad112b 100644 --- a/android/src/toga_android/widgets/box.py +++ b/android/src/toga_android/widgets/box.py @@ -10,9 +10,6 @@ def create(self): self.native = RelativeLayout(MainActivity.singletonThis) def set_child_bounds(self, widget, x, y, width, height): - # Avoid setting child boundaries if `create()` has not been called. - if not widget.native: - return # We assume `widget.native` has already been added to this `RelativeLayout`. # # We use `topMargin` and `leftMargin` to perform absolute layout. Not very diff --git a/android/tests_backend/widgets/box.py b/android/tests_backend/widgets/box.py new file mode 100644 index 0000000000..2ede4ac2f8 --- /dev/null +++ b/android/tests_backend/widgets/box.py @@ -0,0 +1,12 @@ +from java import jclass + +from .base import SimpleProbe +from .properties import toga_color + + +class BoxProbe(SimpleProbe): + native_class = jclass("android.widget.RelativeLayout") + + @property + def background_color(self): + return toga_color(self.native.getBackground().getColor()) diff --git a/android/tests_backend/widgets/label.py b/android/tests_backend/widgets/label.py index 1598da5c77..c8708cef07 100644 --- a/android/tests_backend/widgets/label.py +++ b/android/tests_backend/widgets/label.py @@ -11,10 +11,6 @@ class LabelProbe(SimpleProbe): def color(self): return toga_color(self.native.getCurrentTextColor()) - @property - def background_color(self): - return toga_color(self.native.getBackground().getColor()) - @property def text(self): return str(self.native.getText()) diff --git a/testbed/tests/widgets/properties.py b/testbed/tests/widgets/properties.py index d5599411c8..17671d9d26 100644 --- a/testbed/tests/widgets/properties.py +++ b/testbed/tests/widgets/properties.py @@ -1,3 +1,5 @@ +from pytest import approx + from toga.colors import RED, TRANSPARENT, color as named_color from toga.fonts import BOLD, FANTASY, ITALIC, NORMAL, SERIF, SYSTEM from toga.style.pack import COLUMN @@ -154,8 +156,9 @@ async def test_flex_widget_size(widget, probe): await probe.redraw() # Check the initial widget size - assert probe.width == 100 - assert probe.height == 100 + # Match isn't exact because of pixel scaling on some platforms + assert probe.width == approx(100, rel=0.01) + assert probe.height == approx(100, rel=0.01) # Drop the fixed height, and make the widget flexible widget.style.flex = 1 @@ -163,7 +166,7 @@ async def test_flex_widget_size(widget, probe): # Widget should now be 100 pixels wide, but as tall as the container. await probe.redraw() - assert probe.width == 100 + assert probe.width == approx(100, rel=0.01) assert probe.height > 300 # Make the parent a COLUMN box @@ -180,14 +183,14 @@ async def test_flex_widget_size(widget, probe): await probe.redraw() assert probe.width > 300 - assert probe.height == 150 + assert probe.height == approx(150, rel=0.01) # Revert to fixed width widget.style.width = 150 await probe.redraw() - assert probe.width == 150 - assert probe.height == 150 + assert probe.width == approx(150, rel=0.01) + assert probe.height == approx(150, rel=0.01) async def test_flex_horizontal_widget_size(widget, probe): From 6735c177ebd2d32bc9d2adeec187d7d670e80254 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 21 Mar 2023 14:40:28 +0800 Subject: [PATCH 5/6] Add iOS box probe. --- iOS/src/toga_iOS/widgets/box.py | 18 +----------------- iOS/tests_backend/widgets/base.py | 12 +++++++++++- iOS/tests_backend/widgets/box.py | 7 +++++++ iOS/tests_backend/widgets/label.py | 10 +--------- 4 files changed, 20 insertions(+), 27 deletions(-) create mode 100644 iOS/tests_backend/widgets/box.py diff --git a/iOS/src/toga_iOS/widgets/box.py b/iOS/src/toga_iOS/widgets/box.py index ad83b30e61..fa57d6542c 100644 --- a/iOS/src/toga_iOS/widgets/box.py +++ b/iOS/src/toga_iOS/widgets/box.py @@ -1,28 +1,12 @@ -from rubicon.objc import objc_method, objc_property from travertino.size import at_least from toga_iOS.libs import UIView from toga_iOS.widgets.base import Widget -class TogaView(UIView): - interface = objc_property(object, weak=True) - impl = objc_property(object, weak=True) - - @objc_method - def isFlipped(self) -> bool: - # Default Cocoa coordinate frame is around the wrong way. - return True - - @objc_method - def display(self) -> None: - self.layer.setNeedsDisplay_(True) - self.layer.displayIfNeeded() - - class Box(Widget): def create(self): - self.native = TogaView.alloc().init() + self.native = UIView.alloc().init() self.native.interface = self.interface self.native.impl = self diff --git a/iOS/tests_backend/widgets/base.py b/iOS/tests_backend/widgets/base.py index 3f1afabca3..9116eb063e 100644 --- a/iOS/tests_backend/widgets/base.py +++ b/iOS/tests_backend/widgets/base.py @@ -1,7 +1,10 @@ import asyncio +from toga.colors import TRANSPARENT from toga.fonts import CURSIVE, FANTASY, MONOSPACE, SANS_SERIF, SERIF, SYSTEM -from toga_iOS.libs import NSRunLoop +from toga_iOS.libs import NSRunLoop, UIColor + +from .properties import toga_color # From UIControl.h UIControlEventTouchDown = 1 << 0 @@ -87,5 +90,12 @@ def width(self): def height(self): return self.native.frame.size.height + @property + def background_color(self): + if self.native.backgroundColor == UIColor.clearColor: + return TRANSPARENT + else: + return toga_color(self.native.backgroundColor) + def press(self): self.native.sendActionsForControlEvents(UIControlEventTouchDown) diff --git a/iOS/tests_backend/widgets/box.py b/iOS/tests_backend/widgets/box.py new file mode 100644 index 0000000000..32e111f4e3 --- /dev/null +++ b/iOS/tests_backend/widgets/box.py @@ -0,0 +1,7 @@ +from toga_iOS.libs import UIView + +from .base import SimpleProbe + + +class BoxProbe(SimpleProbe): + native_class = UIView diff --git a/iOS/tests_backend/widgets/label.py b/iOS/tests_backend/widgets/label.py index 2614e80f84..ab06ec2da0 100644 --- a/iOS/tests_backend/widgets/label.py +++ b/iOS/tests_backend/widgets/label.py @@ -1,5 +1,4 @@ -from toga.colors import TRANSPARENT -from toga_iOS.libs import UIColor, UILabel +from toga_iOS.libs import UILabel from .base import SimpleProbe from .properties import toga_alignment, toga_color, toga_font @@ -16,13 +15,6 @@ def text(self): def color(self): return toga_color(self.native.textColor) - @property - def background_color(self): - if self.native.backgroundColor == UIColor.clearColor: - return TRANSPARENT - else: - return toga_color(self.native.backgroundColor) - @property def font(self): return toga_font(self.native.font) From 5acf022bb53c5857907a6924a8532ddc623a9174 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 21 Mar 2023 14:59:58 +0800 Subject: [PATCH 6/6] Update widget support chart. --- docs/reference/data/widgets_by_platform.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index afd52e24f6..58876dc1c2 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -23,7 +23,7 @@ TimePicker,General Widget,:class:`~toga.widgets.timepicker.TimePicker`,An input Tree,General Widget,:class:`~toga.widgets.tree.Tree`,Tree of data,|b|,|b|,|b|,,, WebView,General Widget,:class:`~toga.widgets.webview.WebView`,A panel for displaying HTML,|b|,|b|,|b|,|b|,|b|, Widget,General Widget,:class:`~toga.widgets.base.Widget`,The base widget,|b|,|b|,|b|,|b|,|b|,|b| -Box,Layout Widget,:class:`~toga.widgets.box.Box`,Container for components,|b|,|b|,|b|,|b|,|b|,|b| +Box,Layout Widget,:class:`~toga.widgets.box.Box`,Container for components,|y|,|y|,|y|,|y|,|y|,|b| ScrollContainer,Layout Widget,:class:`~toga.widgets.scrollcontainer.ScrollContainer`,Scrollable Container,|b|,|b|,|b|,|b|,|b|, SplitContainer,Layout Widget,:class:`~toga.widgets.splitcontainer.SplitContainer`,Split Container,|b|,|b|,|b|,,, OptionContainer,Layout Widget,:class:`~toga.widgets.optioncontainer.OptionContainer`,Option Container,|b|,|b|,|b|,,,