Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[widget audit] toga.Box #1820

Merged
merged 8 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions android/src/toga_android/widgets/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions android/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from android.view import ViewTreeObserver
from toga.fonts import SYSTEM

from .properties import toga_color


class LayoutListener(dynamic_proxy(ViewTreeObserver.OnGlobalLayoutListener)):
def __init__(self):
Expand Down Expand Up @@ -80,5 +82,9 @@ def height(self):
# Return the value in DP
return self.native.getHeight() / self.scale_factor

@property
def background_color(self):
return toga_color(self.native.getBackground().getColor())

def press(self):
self.native.performClick()
7 changes: 7 additions & 0 deletions android/tests_backend/widgets/box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from java import jclass

from .base import SimpleProbe


class BoxProbe(SimpleProbe):
native_class = jclass("android.widget.RelativeLayout")
4 changes: 0 additions & 4 deletions android/tests_backend/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
1 change: 1 addition & 0 deletions changes/1820.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Box widget now has 100% test coverage, and complete API documentation.
5 changes: 0 additions & 5 deletions cocoa/src/toga_cocoa/widgets/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
13 changes: 13 additions & 0 deletions cocoa/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)
7 changes: 7 additions & 0 deletions cocoa/tests_backend/widgets/box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from toga_cocoa.libs import NSView

from .base import SimpleProbe


class BoxProbe(SimpleProbe):
native_class = NSView
11 changes: 0 additions & 11 deletions cocoa/tests_backend/widgets/label.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from toga.colors import TRANSPARENT
from toga_cocoa.libs import NSTextField

from .base import SimpleProbe
Expand All @@ -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)
Expand Down
34 changes: 10 additions & 24 deletions core/src/toga/widgets/box.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
6 changes: 0 additions & 6 deletions core/tests/test_deprecated_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 28 additions & 11 deletions core/tests/widgets/test_box.py
Original file line number Diff line number Diff line change
@@ -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]
51 changes: 18 additions & 33 deletions docs/reference/api/containers/box.rst
Original file line number Diff line number Diff line change
@@ -1,64 +1,49 @@
Box
===

A generic container for other widgets. Used to construct layouts.

.. rst-class:: widget-support
.. csv-filter:: Availability (:ref:`Key <api-status-key>`)
:header-rows: 1
:file: ../../data/widgets_by_platform.csv
: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
---------
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/data/widgets_by_platform.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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|,,,
Expand Down
18 changes: 1 addition & 17 deletions iOS/src/toga_iOS/widgets/box.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
12 changes: 11 additions & 1 deletion iOS/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
7 changes: 7 additions & 0 deletions iOS/tests_backend/widgets/box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from toga_iOS.libs import UIView

from .base import SimpleProbe


class BoxProbe(SimpleProbe):
native_class = UIView
Loading