Skip to content

Commit df65161

Browse files
authored
Merge pull request #2942 from HalfWhitt/widget_initialization
Restructured widget initialization order
2 parents 1c8b7d4 + a3f9a54 commit df65161

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+305
-171
lines changed

changes/2942.misc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The initialization process for widgets has been internally restructured to avoid unnecessary style reapplications.

changes/2942.removal.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Widgets now create and return their implementations via a ``_create()`` method. A user-created custom widget that inherits from an existing Toga widget and uses its same implementation will require no changes; any user-created widgets that need to specify their own implementation should do so in ``_create()`` and return it. Existing user code inheriting from Widget that assigns its implementation before calling ``super().__init__()`` will continue to function, but give a RuntimeWarning; unfortunately, this change breaks any existing code that doesn't create its implementation until afterward. Such usage will now raise an exception.

cocoa/src/toga_cocoa/widgets/base.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@ class Widget:
99
def __init__(self, interface):
1010
super().__init__()
1111
self.interface = interface
12-
self.interface._impl = self
1312
self._container = None
1413
self.constraints = None
1514
self.native = None
1615
self.create()
17-
self.interface.style.reapply()
1816

1917
@abstractmethod
2018
def create(self): ...

cocoa/src/toga_cocoa/widgets/button.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ def create(self):
3232
self._icon = None
3333

3434
self.native.buttonType = NSMomentaryPushInButton
35-
self._set_button_style()
3635

3736
self.native.target = self.native
3837
self.native.action = SEL("onPress:")

core/src/toga/style/applicator.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,34 @@
11
from __future__ import annotations
22

3+
import warnings
34
from typing import TYPE_CHECKING
45

56
if TYPE_CHECKING:
67
from toga.widgets.base import Widget
78

9+
# Make sure deprecation warnings are shown by default
10+
warnings.filterwarnings("default", category=DeprecationWarning)
11+
812

913
class TogaApplicator:
1014
"""Apply styles to a Toga widget."""
1115

12-
def __init__(self, widget: Widget):
13-
self.widget = widget
16+
def __init__(self, widget: None = None):
17+
if widget is not None:
18+
warnings.warn(
19+
"Widget parameter is deprecated. Applicator will be given a reference "
20+
"to its widget when it is assigned as that widget's applicator.",
21+
DeprecationWarning,
22+
stacklevel=2,
23+
)
24+
25+
@property
26+
def widget(self) -> Widget:
27+
"""The widget to which this applicator is assigned.
28+
29+
Syntactic sugar over the node attribute set by Travertino.
30+
"""
31+
return self.node
1432

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

core/src/toga/widgets/activityindicator.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Literal
3+
from typing import Any, Literal
44

55
from .base import StyleT, Widget
66

@@ -22,11 +22,12 @@ def __init__(
2222
"""
2323
super().__init__(id=id, style=style)
2424

25-
self._impl = self.factory.ActivityIndicator(interface=self)
26-
2725
if running:
2826
self.start()
2927

28+
def _create(self) -> Any:
29+
return self.factory.ActivityIndicator(interface=self)
30+
3031
@property
3132
def enabled(self) -> Literal[True]:
3233
"""Is the widget currently enabled? i.e., can the user interact with the widget?

core/src/toga/widgets/base.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from builtins import id as identifier
44
from typing import TYPE_CHECKING, Any, TypeVar
5+
from warnings import warn
56

67
from travertino.declaration import BaseStyle
78
from travertino.node import Node
@@ -33,18 +34,70 @@ def __init__(
3334
:param style: A style object. If no style is provided, a default style
3435
will be applied to the widget.
3536
"""
36-
super().__init__(
37-
style=style if style else Pack(),
38-
applicator=TogaApplicator(self),
39-
)
37+
super().__init__(style=style if style is not None else Pack())
4038

4139
self._id = str(id if id else identifier(self))
4240
self._window: Window | None = None
4341
self._app: App | None = None
44-
self._impl: Any = None
4542

43+
# Get factory and assign implementation
4644
self.factory = get_platform_factory()
4745

46+
###########################################
47+
# Backwards compatibility for Toga <= 0.4.8
48+
###########################################
49+
50+
# Just in case we're working with a third-party widget created before
51+
# the _create() mechanism was added, which has already defined its
52+
# implementation. We still want to call _create(), to issue the warning and
53+
# inform users about where they should be creating the implementation, but if
54+
# there already is one, we don't want to do the assignment and thus replace it
55+
# with None.
56+
57+
impl = self._create()
58+
59+
if not hasattr(self, "_impl"):
60+
self._impl = impl
61+
62+
#############################
63+
# End backwards compatibility
64+
#############################
65+
66+
self.applicator = TogaApplicator()
67+
68+
##############################################
69+
# Backwards compatibility for Travertino 0.3.0
70+
##############################################
71+
72+
# The below if block will execute when using Travertino 0.3.0. For future
73+
# versions of Travertino, these assignments (and the reapply) will already have
74+
# been handled "automatically" by assigning the applicator above; in that case,
75+
# we want to avoid doing a second, redundant style reapplication.
76+
77+
# This whole section can be removed as soon as there's a newer version of
78+
# Travertino to set as Toga's minimum requirement.
79+
80+
if not hasattr(self.applicator, "node"): # pragma: no cover
81+
self.applicator.node = self
82+
self.style._applicator = self.applicator
83+
self.style.reapply()
84+
85+
#############################
86+
# End backwards compatibility
87+
#############################
88+
89+
def _create(self) -> Any:
90+
"""Create a platform-specific implementation of this widget.
91+
92+
A subclass of Widget should redefine this method to return its implementation.
93+
"""
94+
warn(
95+
"Widgets should create and return their implementation in ._create(). This "
96+
"will be an exception in a future version.",
97+
RuntimeWarning,
98+
stacklevel=2,
99+
)
100+
48101
def __repr__(self) -> str:
49102
return f"<{self.__class__.__name__}:0x{identifier(self):x}>"
50103

core/src/toga/widgets/box.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ def __init__(
2424
"""
2525
super().__init__(id=id, style=style)
2626

27-
# Create a platform specific implementation of a Box
28-
self._impl = self.factory.Box(interface=self)
29-
3027
# Children need to be added *after* the impl has been created.
3128
self._children: list[Widget] = []
3229
if children is not None:
3330
self.add(*children)
3431

32+
def _create(self):
33+
return self.factory.Box(interface=self)
34+
3535
@property
3636
def enabled(self) -> bool:
3737
"""Is the widget currently enabled? i.e., can the user interact with the widget?

core/src/toga/widgets/button.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@ def __init__(
4444
"""
4545
super().__init__(id=id, style=style)
4646

47-
# Create a platform specific implementation of a Button
48-
self._impl = self.factory.Button(interface=self)
49-
5047
# Set a dummy handler before installing the actual on_press, because we do not
5148
# want on_press triggered by the initial value being set
5249
self.on_press = None
@@ -63,6 +60,9 @@ def __init__(
6360
self.on_press = on_press
6461
self.enabled = enabled
6562

63+
def _create(self) -> Any:
64+
return self.factory.Button(interface=self)
65+
6666
@property
6767
def text(self) -> str:
6868
"""The text displayed on the button.

core/src/toga/widgets/canvas.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,14 +1239,10 @@ def __init__(
12391239
:param on_alt_release: Initial :any:`on_alt_release` handler.
12401240
:param on_alt_drag: Initial :any:`on_alt_drag` handler.
12411241
"""
1242-
12431242
super().__init__(id=id, style=style)
12441243

12451244
self._context = Context(canvas=self)
12461245

1247-
# Create a platform specific implementation of Canvas
1248-
self._impl = self.factory.Canvas(interface=self)
1249-
12501246
# Set all the properties
12511247
self.on_resize = on_resize
12521248
self.on_press = on_press
@@ -1257,6 +1253,9 @@ def __init__(
12571253
self.on_alt_release = on_alt_release
12581254
self.on_alt_drag = on_alt_drag
12591255

1256+
def _create(self) -> Any:
1257+
return self.factory.Canvas(interface=self)
1258+
12601259
@property
12611260
def enabled(self) -> Literal[True]:
12621261
"""Is the widget currently enabled? i.e., can the user interact with the widget?

core/src/toga/widgets/dateinput.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,16 @@ def __init__(
5151
"""
5252
super().__init__(id=id, style=style)
5353

54-
# Create a platform specific implementation of a DateInput
55-
self._impl = self.factory.DateInput(interface=self)
56-
5754
self.on_change = None
5855
self.min = min
5956
self.max = max
6057

6158
self.value = value
6259
self.on_change = on_change
6360

61+
def _create(self) -> Any:
62+
return self.factory.DateInput(interface=self)
63+
6464
@property
6565
def value(self) -> datetime.date:
6666
"""The currently selected date. A value of ``None`` will be converted into

core/src/toga/widgets/detailedlist.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ def __init__(
8585
:param on_refresh: Initial :any:`on_refresh` handler.
8686
:param on_delete: **DEPRECATED**; use ``on_primary_action``.
8787
"""
88+
# Prime the attributes and handlers that need to exist when the widget is
89+
# created.
90+
self._accessors = accessors
91+
self._missing_value = missing_value
92+
self._primary_action = primary_action
93+
self._secondary_action = secondary_action
94+
self.on_select = None
95+
96+
self._data: SourceT | ListSource = None
97+
8898
super().__init__(id=id, style=style)
8999

90100
######################################################################
@@ -104,24 +114,15 @@ def __init__(
104114
# End backwards compatibility.
105115
######################################################################
106116

107-
# Prime the attributes and handlers that need to exist when the
108-
# widget is created.
109-
self._accessors = accessors
110-
self._missing_value = missing_value
111-
self._primary_action = primary_action
112-
self._secondary_action = secondary_action
113-
self.on_select = None
114-
115-
self._data: SourceT | ListSource = None
116-
117-
self._impl = self.factory.DetailedList(interface=self)
118-
119117
self.data = data
120118
self.on_primary_action = on_primary_action
121119
self.on_secondary_action = on_secondary_action
122120
self.on_refresh = on_refresh
123121
self.on_select = on_select
124122

123+
def _create(self) -> Any:
124+
return self.factory.DetailedList(interface=self)
125+
125126
@property
126127
def enabled(self) -> Literal[True]:
127128
"""Is the widget currently enabled? i.e., can the user interact with the widget?

core/src/toga/widgets/divider.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Literal
3+
from typing import Any, Literal
44

55
from toga.constants import Direction
66

@@ -29,10 +29,11 @@ def __init__(
2929
"""
3030
super().__init__(id=id, style=style)
3131

32-
# Create a platform specific implementation of a Divider
33-
self._impl = self.factory.Divider(interface=self)
3432
self.direction = direction
3533

34+
def _create(self) -> Any:
35+
return self.factory.Divider(interface=self)
36+
3637
@property
3738
def enabled(self) -> Literal[True]:
3839
"""Is the widget currently enabled? i.e., can the user interact with the widget?

core/src/toga/widgets/imageview.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Literal
3+
from typing import TYPE_CHECKING, Any, Literal
44

55
from travertino.size import at_least
66

@@ -83,12 +83,16 @@ def __init__(
8383
:param style: A style object. If no style is provided, a default style will be
8484
applied to the widget.
8585
"""
86-
super().__init__(id=id, style=style)
8786
# Prime the image attribute
8887
self._image = None
89-
self._impl = self.factory.ImageView(interface=self)
88+
89+
super().__init__(id=id, style=style)
90+
9091
self.image = image
9192

93+
def _create(self) -> Any:
94+
return self.factory.ImageView(interface=self)
95+
9296
@property
9397
def enabled(self) -> Literal[True]:
9498
"""Is the widget currently enabled? i.e., can the user interact with the widget?

core/src/toga/widgets/label.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from typing import Any
4+
35
from .base import StyleT, Widget
46

57

@@ -19,11 +21,11 @@ def __init__(
1921
"""
2022
super().__init__(id=id, style=style)
2123

22-
# Create a platform specific implementation of a Label
23-
self._impl = self.factory.Label(interface=self)
24-
2524
self.text = text
2625

26+
def _create(self) -> Any:
27+
return self.factory.Label(interface=self)
28+
2729
def focus(self) -> None:
2830
"""No-op; Label cannot accept input focus."""
2931
pass

core/src/toga/widgets/mapview.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,6 @@ def __init__(
155155
"""
156156
super().__init__(id=id, style=style)
157157

158-
self._impl: Any = self.factory.MapView(interface=self)
159-
160158
self._pins = MapPinSet(self, pins)
161159

162160
if location:
@@ -169,6 +167,9 @@ def __init__(
169167

170168
self.on_select = on_select
171169

170+
def _create(self) -> Any:
171+
return self.factory.MapView(interface=self)
172+
172173
@property
173174
def location(self) -> toga.LatLng:
174175
"""The latitude/longitude where the map is centered.

0 commit comments

Comments
 (0)