Skip to content

Commit

Permalink
Allow style properties to be accessed directly on the widget (#3107)
Browse files Browse the repository at this point in the history
* Allow style properties to be accessed directly on the widget

* Add **kwargs to all widget constructors

* Update "layout" and "resize" examples to use style-less syntax

* Add tests to verify kwarg passthrough on widgets.

* Additional tests to verify Pack/descriptor parity.

---------

Co-authored-by: Russell Keith-Magee <[email protected]>
  • Loading branch information
mhsmith and freakboy3742 authored Jan 15, 2025
1 parent ebeb83f commit fbedd3f
Show file tree
Hide file tree
Showing 56 changed files with 477 additions and 83 deletions.
1 change: 1 addition & 0 deletions changes/3011.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Style properties can now be passed directly to a widget's constructor, or accessed as attributes, without explicitly using a ``style`` object.
28 changes: 28 additions & 0 deletions core/src/toga/style/mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class StyleProperty:
def __set_name__(self, mixin_cls, name):
self.name = name

def __get__(self, widget, mixin_cls):
return self if widget is None else getattr(widget.style, self.name)

def __set__(self, widget, value):
setattr(widget.style, self.name, value)

def __delete__(self, widget):
delattr(widget.style, self.name)


def style_mixin(style_cls):
mixin_dict = {
"__doc__": f"""
Allows accessing the {style_cls.__name__} {style_cls._doc_link} directly on
the widget. For example, instead of ``widget.style.color``, you can simply
write ``widget.color``.
"""
}

for name in dir(style_cls):
if not name.startswith("_") and isinstance(getattr(style_cls, name), property):
mixin_dict[name] = StyleProperty()

return type(style_cls.__name__ + "Mixin", (), mixin_dict)
2 changes: 2 additions & 0 deletions core/src/toga/style/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@


class Pack(BaseStyle):
_doc_link = ":doc:`style properties </reference/style/pack>`"

class Box(BaseBox):
pass

Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/activityindicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def __init__(
id: str | None = None,
style: StyleT | None = None,
running: bool = False,
**kwargs,
):
"""Create a new ActivityIndicator widget.
Expand All @@ -19,8 +20,9 @@ def __init__(
will be applied to the widget.
:param running: Describes whether the indicator is running at the
time it is created.
:param kwargs: Initial style properties.
"""
super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

if running:
self.start()
Expand Down
13 changes: 11 additions & 2 deletions core/src/toga/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,25 @@

from toga.platform import get_platform_factory
from toga.style import Pack, TogaApplicator
from toga.style.mixin import style_mixin

if TYPE_CHECKING:
from toga.app import App
from toga.window import Window

StyleT = TypeVar("StyleT", bound=BaseStyle)
PackMixin = style_mixin(Pack)


class Widget(Node):
class Widget(Node, PackMixin):
_MIN_WIDTH = 100
_MIN_HEIGHT = 100

def __init__(
self,
id: str | None = None,
style: StyleT | None = None,
**kwargs,
):
"""Create a base Toga widget.
Expand All @@ -33,8 +36,14 @@ def __init__(
: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 kwargs: Initial style properties.
"""
super().__init__(style=style if style is not None else Pack())
if style is None:
style = Pack(**kwargs)
elif kwargs:
style = style.copy()
style.update(**kwargs)
super().__init__(style)

self._id = str(id if id else identifier(self))
self._window: Window | None = None
Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ def __init__(
id: str | None = None,
style: StyleT | None = None,
children: Iterable[Widget] | None = None,
**kwargs,
):
"""Create a new Box container widget.
: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.
:param kwargs: Initial style properties.
"""
super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

# Children need to be added *after* the impl has been created.
self._children: list[Widget] = []
Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def __init__(
style: StyleT | None = None,
on_press: toga.widgets.button.OnPressHandler | None = None,
enabled: bool = True,
**kwargs,
):
"""Create a new button widget.
Expand All @@ -41,8 +42,9 @@ def __init__(
:param on_press: A handler that will be invoked when the button is pressed.
:param enabled: Is the button enabled (i.e., can it be pressed?). Optional; by
default, buttons are created in an enabled state.
:param kwargs: Initial style properties.
"""
super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

# Set a dummy handler before installing the actual on_press, because we do not
# want on_press triggered by the initial value being set
Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/canvas/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def __init__(
on_alt_press: OnTouchHandler | None = None,
on_alt_release: OnTouchHandler | None = None,
on_alt_drag: OnTouchHandler | None = None,
**kwargs,
):
"""Create a new Canvas widget.
Expand All @@ -84,10 +85,11 @@ def __init__(
:param on_alt_press: Initial :any:`on_alt_press` handler.
:param on_alt_release: Initial :any:`on_alt_release` handler.
:param on_alt_drag: Initial :any:`on_alt_drag` handler.
:param kwargs: Initial style properties.
"""
self._context = Context(canvas=self)

super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

# Set all the properties
self.on_resize = on_resize
Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/dateinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(
min: datetime.date | None = None,
max: datetime.date | None = None,
on_change: toga.widgets.dateinput.OnChangeHandler | None = None,
**kwargs,
):
"""Create a new DateInput widget.
Expand All @@ -47,8 +48,9 @@ def __init__(
:param min: The earliest date (inclusive) that can be selected.
:param max: The latest date (inclusive) that can be selected.
:param on_change: A handler that will be invoked when the value changes.
:param kwargs: Initial style properties.
"""
super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

self.on_change = None
self.min = min
Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(
on_secondary_action: OnSecondaryActionHandler | None = None,
on_refresh: OnRefreshHandler | None = None,
on_select: toga.widgets.detailedlist.OnSelectHandler | None = None,
**kwargs,
):
"""Create a new DetailedList widget.
Expand All @@ -81,6 +82,7 @@ def __init__(
:param secondary_action: The name for the secondary action.
:param on_secondary_action: Initial :any:`on_secondary_action` handler.
:param on_refresh: Initial :any:`on_refresh` handler.
:param kwargs: Initial style properties.
"""
# Prime the attributes and handlers that need to exist when the widget is
# created.
Expand All @@ -92,7 +94,7 @@ def __init__(

self._data: SourceT | ListSource = None

super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

self.data = data
self.on_primary_action = on_primary_action
Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/divider.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def __init__(
id: str | None = None,
style: StyleT | None = None,
direction: Direction = HORIZONTAL,
**kwargs,
):
"""Create a new divider line.
Expand All @@ -26,8 +27,9 @@ def __init__(
:attr:`~toga.constants.Direction.HORIZONTAL` or
:attr:`~toga.constants.Direction.VERTICAL`; defaults to
:attr:`~toga.constants.Direction.HORIZONTAL`
:param kwargs: Initial style properties.
"""
super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

self.direction = direction

Expand Down
7 changes: 4 additions & 3 deletions core/src/toga/widgets/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,21 @@ def __init__(
image: ImageContentT | None = None,
id: str | None = None,
style: StyleT | None = None,
**kwargs,
):
"""
Create a new image view.
"""Create a new image view.
:param image: The image to display. Can be any valid :any:`image content
<ImageContentT>` type; or :any:`None` to display no image.
: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 kwargs: Initial style properties.
"""
# Prime the image attribute
self._image = None

super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

self.image = image

Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ def __init__(
text: str,
id: str | None = None,
style: StyleT | None = None,
**kwargs,
):
"""Create a new text label.
:param text: Text of the label.
: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 kwargs: Initial style properties.
"""
super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

self.text = text

Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/mapview.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def __init__(
zoom: int = 11,
pins: Iterable[MapPin] | None = None,
on_select: toga.widgets.mapview.OnSelectHandler | None = None,
**kwargs,
):
"""Create a new MapView widget.
Expand All @@ -152,8 +153,9 @@ def __init__(
:param pins: The initial pins to display on the map.
:param on_select: A handler that will be invoked when the user selects a map
pin.
:param kwargs: Initial style properties.
"""
super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

self._pins = MapPinSet(self, pins)

Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def __init__(
readonly: bool = False,
placeholder: str | None = None,
on_change: toga.widgets.multilinetextinput.OnChangeHandler | None = None,
**kwargs,
):
"""Create a new multi-line text input widget.
Expand All @@ -38,8 +39,9 @@ def __init__(
there is no user content to display.
:param on_change: A handler that will be invoked when the value of
the widget changes.
:param kwargs: Initial style properties.
"""
super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

# Set a dummy handler before installing the actual on_change, because we do not
# want on_change triggered by the initial value being set
Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/numberinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def __init__(
value: NumberInputT | None = None,
readonly: bool = False,
on_change: toga.widgets.numberinput.OnChangeHandler | None = None,
**kwargs,
):
"""Create a new number input widget.
Expand All @@ -107,6 +108,7 @@ def __init__(
:param readonly: Can the value of the widget be modified by the user?
:param on_change: A handler that will be invoked when the value of the widget
changes.
:param kwargs: Initial style properties.
"""
# The initial setting of min requires calling get_value(),
# which in turn interrogates min. Prime those values with
Expand All @@ -116,7 +118,7 @@ def __init__(

self.on_change = None

super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

self.readonly = readonly
self.step = step
Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ def __init__(
style: StyleT | None = None,
content: Iterable[OptionContainerContentT] | None = None,
on_select: toga.widgets.optioncontainer.OnSelectHandler | None = None,
**kwargs,
):
"""Create a new OptionContainer.
Expand All @@ -392,11 +393,12 @@ def __init__(
:param content: The initial :any:`OptionContainer content
<OptionContainerContentT>` to display in the OptionContainer.
:param on_select: Initial :any:`on_select` handler.
:param kwargs: Initial style properties.
"""
self._content = OptionList(self)
self.on_select = None

super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

if content is not None:
for item in content:
Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/progressbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def __init__(
max: str | SupportsFloat = 1.0,
value: str | SupportsFloat = 0.0,
running: bool = False,
**kwargs,
):
"""Create a new Progress Bar widget.
Expand All @@ -29,8 +30,9 @@ def __init__(
clipped. Defaults to 0.0.
:param running: Describes whether the indicator is running at the time
it is created. Default is False.
:param kwargs: Initial style properties.
"""
super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

self.max = max
self.value = value
Expand Down
4 changes: 3 additions & 1 deletion core/src/toga/widgets/scrollcontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def __init__(
vertical: bool = True,
on_scroll: OnScrollHandler | None = None,
content: Widget | None = None,
**kwargs,
):
"""Create a new Scroll Container.
Expand All @@ -39,12 +40,13 @@ def __init__(
:param vertical: Should horizontal scrolling be permitted?
:param on_scroll: Initial :any:`on_scroll` handler.
:param content: The content to display in the scroll window.
:param kwargs: Initial style properties.
"""

self._content: Widget | None = None
self.on_scroll = None

super().__init__(id=id, style=style)
super().__init__(id, style, **kwargs)

# Set all attributes
self.vertical = vertical
Expand Down
Loading

0 comments on commit fbedd3f

Please sign in to comment.