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

Restructured widget initialization order #2942

Merged
merged 17 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions changes/2942.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The initialization process for widgets has been internally restructured to avoid unnecessary style reapplications.
2 changes: 0 additions & 2 deletions cocoa/src/toga_cocoa/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ class Widget:
def __init__(self, interface):
super().__init__()
self.interface = interface
self.interface._impl = self
self._container = None
self.constraints = None
self.native = None
self.create()
self.interface.style.reapply()

@abstractmethod
def create(self): ...
Expand Down
50 changes: 27 additions & 23 deletions core/src/toga/style/applicator.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
from __future__ import annotations
import warnings

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from toga.widgets.base import Widget
# Make sure deprecation warnings are shown by default
warnings.filterwarnings("default", category=DeprecationWarning)


class TogaApplicator:
"""Apply styles to a Toga widget."""

def __init__(self, widget: Widget):
self.widget = widget
def __init__(self, widget: None = None):
if widget is not None:
warnings.warn(
"Widget parameter is deprecated. Applicator will be given a reference "
"to its widget when it is assigned as that widget's applicator.",
DeprecationWarning,
stacklevel=2,
)

def refresh(self) -> None:
# print("RE-EVALUATE LAYOUT", self.widget)
self.widget.refresh()
# print("RE-EVALUATE LAYOUT", self.node)
self.node.refresh()

def set_bounds(self) -> None:
# print(" APPLY LAYOUT", self.widget, self.widget.layout)
self.widget._impl.set_bounds(
self.widget.layout.absolute_content_left,
self.widget.layout.absolute_content_top,
self.widget.layout.content_width,
self.widget.layout.content_height,
# print(" APPLY LAYOUT", self.node, self.node.layout)
self.node._impl.set_bounds(
self.node.layout.absolute_content_left,
self.node.layout.absolute_content_top,
self.node.layout.content_width,
self.node.layout.content_height,
)
for child in self.widget.children:
for child in self.node.children:
child.applicator.set_bounds()

def set_text_alignment(self, alignment: str) -> None:
self.widget._impl.set_alignment(alignment)
self.node._impl.set_alignment(alignment)

def set_hidden(self, hidden: bool) -> None:
self.widget._impl.set_hidden(hidden)
for child in self.widget.children:
self.node._impl.set_hidden(hidden)
for child in self.node.children:
# If the parent is hidden, then so are all children. However, if the
# parent is visible, then the child's explicit visibility style is
# taken into account. This visibility cascades into any
Expand All @@ -49,11 +53,11 @@ def set_hidden(self, hidden: bool) -> None:
def set_font(self, font: object) -> None:
# Changing the font of a widget can make the widget change size,
# which in turn means we need to do a re-layout
self.widget._impl.set_font(font)
self.widget.refresh()
self.node._impl.set_font(font)
self.node.refresh()

def set_color(self, color: object) -> None:
self.widget._impl.set_color(color)
self.node._impl.set_color(color)

def set_background_color(self, color: object) -> None:
self.widget._impl.set_background_color(color)
self.node._impl.set_background_color(color)
4 changes: 2 additions & 2 deletions core/src/toga/widgets/activityindicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class ActivityIndicator(Widget):
_IMPL_NAME = "ActivityIndicator"

def __init__(
self,
id: str | None = None,
Expand All @@ -22,8 +24,6 @@ def __init__(
"""
super().__init__(id=id, style=style)

self._impl = self.factory.ActivityIndicator(interface=self)

if running:
self.start()

Expand Down
27 changes: 23 additions & 4 deletions core/src/toga/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from builtins import id as identifier
from typing import TYPE_CHECKING, Any, TypeVar
from typing import TYPE_CHECKING, TypeVar

from travertino.declaration import BaseStyle
from travertino.node import Node
Expand All @@ -17,6 +17,8 @@


class Widget(Node):
_IMPL_NAME = "Widget"

_MIN_WIDTH = 100
_MIN_HEIGHT = 100

Expand All @@ -34,16 +36,33 @@ def __init__(
will be applied to the widget.
"""
super().__init__(
style=style if style else Pack(),
applicator=TogaApplicator(self),
style=style if style is not None else Pack(),
HalfWhitt marked this conversation as resolved.
Show resolved Hide resolved
applicator=None,
HalfWhitt marked this conversation as resolved.
Show resolved Hide resolved
)

self._id = str(id if id else identifier(self))
self._window: Window | None = None
self._app: App | None = None
self._impl: Any = None

self.factory = get_platform_factory()
self._impl = getattr(self.factory, self._IMPL_NAME)(interface=self)

self.applicator = TogaApplicator()

##############################################
# Backwards compatibility for Travertino 0.3.0
##############################################

if not hasattr(self.applicator, "node"):
HalfWhitt marked this conversation as resolved.
Show resolved Hide resolved
self.applicator.node = self
self.style._applicator = self.applicator
self.style.reapply()
else: # pragma: no cover
pass

#############################
# End backwards compatibility
#############################

def __repr__(self) -> str:
return f"<{self.__class__.__name__}:0x{identifier(self):x}>"
Expand Down
5 changes: 2 additions & 3 deletions core/src/toga/widgets/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class Box(Widget):
_IMPL_NAME = "Box"

_MIN_WIDTH = 0
_MIN_HEIGHT = 0

Expand All @@ -24,9 +26,6 @@ def __init__(
"""
super().__init__(id=id, style=style)

# Create a platform specific implementation of a Box
self._impl = self.factory.Box(interface=self)

# Children need to be added *after* the impl has been created.
self._children: list[Widget] = []
if children is not None:
Expand Down
5 changes: 2 additions & 3 deletions core/src/toga/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def __call__(self, widget: Button, /, **kwargs: Any) -> object:


class Button(Widget):
_IMPL_NAME = "Button"

def __init__(
self,
text: str | None = None,
Expand All @@ -44,9 +46,6 @@ def __init__(
"""
super().__init__(id=id, style=style)

# Create a platform specific implementation of a Button
self._impl = self.factory.Button(interface=self)

# Set a dummy handler before installing the actual on_press, because we do not want
# on_press triggered by the initial value being set
self.on_press = None
Expand Down
5 changes: 2 additions & 3 deletions core/src/toga/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,8 @@ def __call__(


class Canvas(Widget):
_IMPL_NAME = "Canvas"

_MIN_WIDTH = 0
_MIN_HEIGHT = 0

Expand Down Expand Up @@ -1241,9 +1243,6 @@ def __init__(

self._context = Context(canvas=self)

# Create a platform specific implementation of Canvas
self._impl = self.factory.Canvas(interface=self)

# Set all the properties
self.on_resize = on_resize
self.on_press = on_press
Expand Down
5 changes: 2 additions & 3 deletions core/src/toga/widgets/dateinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def __call__(self, widget: DateInput, /, **kwargs: Any) -> object:


class DateInput(Widget):
_IMPL_NAME = "DateInput"

_MIN_WIDTH = 200

def __init__(
Expand All @@ -51,9 +53,6 @@ def __init__(
"""
super().__init__(id=id, style=style)

# Create a platform specific implementation of a DateInput
self._impl = self.factory.DateInput(interface=self)

self.on_change = None
self.min = min
self.max = max
Expand Down
22 changes: 11 additions & 11 deletions core/src/toga/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def __call__(self, widget: DetailedList, /, **kwargs: Any) -> object:


class DetailedList(Widget):
_IMPL_NAME = "DetailedList"

def __init__(
self,
id: str | None = None,
Expand Down Expand Up @@ -85,6 +87,15 @@ def __init__(
:param on_refresh: Initial :any:`on_refresh` handler.
:param on_delete: **DEPRECATED**; use ``on_primary_action``.
"""
# Prime the attributes and handlers that need to exist when the widget is created.
self._accessors = accessors
self._missing_value = missing_value
self._primary_action = primary_action
self._secondary_action = secondary_action
self.on_select = None

self._data: SourceT | ListSource = None

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

######################################################################
Expand All @@ -103,17 +114,6 @@ def __init__(
# End backwards compatibility.
######################################################################

# Prime the attributes and handlers that need to exist when the widget is created.
self._accessors = accessors
self._missing_value = missing_value
self._primary_action = primary_action
self._secondary_action = secondary_action
self.on_select = None

self._data: SourceT | ListSource = None

self._impl = self.factory.DetailedList(interface=self)

self.data = data
self.on_primary_action = on_primary_action
self.on_secondary_action = on_secondary_action
Expand Down
4 changes: 2 additions & 2 deletions core/src/toga/widgets/divider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@


class Divider(Widget):
_IMPL_NAME = "Divider"

HORIZONTAL = Direction.HORIZONTAL
VERTICAL = Direction.VERTICAL

Expand All @@ -29,8 +31,6 @@ def __init__(
"""
super().__init__(id=id, style=style)

# Create a platform specific implementation of a Divider
self._impl = self.factory.Divider(interface=self)
self.direction = direction

@property
Expand Down
7 changes: 5 additions & 2 deletions core/src/toga/widgets/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ def rehint_imageview(


class ImageView(Widget):
_IMPL_NAME = "ImageView"

def __init__(
self,
image: ImageContentT | None = None,
Expand All @@ -83,10 +85,11 @@ def __init__(
:param style: A style object. If no style is provided, a default style will be
applied to the widget.
"""
super().__init__(id=id, style=style)
# Prime the image attribute
self._image = None
self._impl = self.factory.ImageView(interface=self)

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

self.image = image

@property
Expand Down
5 changes: 2 additions & 3 deletions core/src/toga/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@


class Label(Widget):
_IMPL_NAME = "Label"

def __init__(
self,
text: str,
Expand All @@ -19,9 +21,6 @@ def __init__(
"""
super().__init__(id=id, style=style)

# Create a platform specific implementation of a Label
self._impl = self.factory.Label(interface=self)

self.text = text

def focus(self) -> None:
Expand Down
6 changes: 4 additions & 2 deletions core/src/toga/widgets/mapview.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@


class MapPin:
_IMPL_NAME = "MapPin"

def __init__(
self,
location: toga.LatLng | tuple[float, float],
Expand Down Expand Up @@ -131,6 +133,8 @@ def __call__(self, widget: MapView, /, *, pin: MapPin, **kwargs: Any) -> object:


class MapView(Widget):
_IMPL_NAME = "MapView"

def __init__(
self,
id: str | None = None,
Expand All @@ -155,8 +159,6 @@ def __init__(
"""
super().__init__(id=id, style=style)

self._impl: Any = self.factory.MapView(interface=self)

self._pins = MapPinSet(self, pins)

if location:
Expand Down
5 changes: 2 additions & 3 deletions core/src/toga/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def __call__(self, widget: MultilineTextInput, /, **kwargs: Any) -> object:


class MultilineTextInput(Widget):
_IMPL_NAME = "MultilineTextInput"

def __init__(
self,
id: str | None = None,
Expand All @@ -42,9 +44,6 @@ def __init__(

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

# Create a platform specific implementation of a MultilineTextInput
self._impl = self.factory.MultilineTextInput(interface=self)

# Set a dummy handler before installing the actual on_change, because we do not want
# on_change triggered by the initial value being set
self.on_change = None
Expand Down
Loading
Loading