diff --git a/changes/2942.misc.rst b/changes/2942.misc.rst new file mode 100644 index 0000000000..41bb0166c3 --- /dev/null +++ b/changes/2942.misc.rst @@ -0,0 +1 @@ +The initialization process for widgets has been internally restructured to avoid unnecessary style reapplications. diff --git a/changes/2942.removal.rst b/changes/2942.removal.rst new file mode 100644 index 0000000000..365ef380c1 --- /dev/null +++ b/changes/2942.removal.rst @@ -0,0 +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. diff --git a/cocoa/src/toga_cocoa/widgets/base.py b/cocoa/src/toga_cocoa/widgets/base.py index 28da6271fb..06e6264135 100644 --- a/cocoa/src/toga_cocoa/widgets/base.py +++ b/cocoa/src/toga_cocoa/widgets/base.py @@ -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): ... diff --git a/cocoa/src/toga_cocoa/widgets/button.py b/cocoa/src/toga_cocoa/widgets/button.py index 1ef61bcefd..eadb8e2b92 100644 --- a/cocoa/src/toga_cocoa/widgets/button.py +++ b/cocoa/src/toga_cocoa/widgets/button.py @@ -32,7 +32,6 @@ def create(self): self._icon = None self.native.buttonType = NSMomentaryPushInButton - self._set_button_style() self.native.target = self.native self.native.action = SEL("onPress:") diff --git a/core/src/toga/style/applicator.py b/core/src/toga/style/applicator.py index 2b0c17ee75..43674a0d24 100644 --- a/core/src/toga/style/applicator.py +++ b/core/src/toga/style/applicator.py @@ -1,16 +1,34 @@ 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, + ) + + @property + def widget(self) -> Widget: + """The widget to which this applicator is assigned. + + Syntactic sugar over the node attribute set by Travertino. + """ + return self.node def refresh(self) -> None: # print("RE-EVALUATE LAYOUT", self.widget) diff --git a/core/src/toga/widgets/activityindicator.py b/core/src/toga/widgets/activityindicator.py index 102f44d129..4ac5c2ab18 100644 --- a/core/src/toga/widgets/activityindicator.py +++ b/core/src/toga/widgets/activityindicator.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Literal +from typing import Any, Literal from .base import StyleT, Widget @@ -22,11 +22,12 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl = self.factory.ActivityIndicator(interface=self) - if running: self.start() + def _create(self) -> Any: + return self.factory.ActivityIndicator(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/base.py b/core/src/toga/widgets/base.py index 61d135cb9f..0d179ac8ac 100644 --- a/core/src/toga/widgets/base.py +++ b/core/src/toga/widgets/base.py @@ -2,6 +2,7 @@ from builtins import id as identifier from typing import TYPE_CHECKING, Any, TypeVar +from warnings import warn from travertino.declaration import BaseStyle from travertino.node import Node @@ -33,18 +34,70 @@ def __init__( :param style: A style object. If no style is provided, a default style will be applied to the widget. """ - super().__init__( - style=style if style else Pack(), - applicator=TogaApplicator(self), - ) + super().__init__(style=style if style is not None else Pack()) self._id = str(id if id else identifier(self)) self._window: Window | None = None self._app: App | None = None - self._impl: Any = None + # Get factory and assign implementation self.factory = get_platform_factory() + ########################################### + # Backwards compatibility for Toga <= 0.4.8 + ########################################### + + # Just in case we're working with a third-party widget created before + # the _create() mechanism was added, which has already defined its + # implementation. We still want to call _create(), to issue the warning and + # inform users about where they should be creating the implementation, but if + # there already is one, we don't want to do the assignment and thus replace it + # with None. + + impl = self._create() + + if not hasattr(self, "_impl"): + self._impl = impl + + ############################# + # End backwards compatibility + ############################# + + self.applicator = TogaApplicator() + + ############################################## + # Backwards compatibility for Travertino 0.3.0 + ############################################## + + # The below if block will execute when using Travertino 0.3.0. For future + # versions of Travertino, these assignments (and the reapply) will already have + # been handled "automatically" by assigning the applicator above; in that case, + # we want to avoid doing a second, redundant style reapplication. + + # This whole section can be removed as soon as there's a newer version of + # Travertino to set as Toga's minimum requirement. + + if not hasattr(self.applicator, "node"): # pragma: no cover + self.applicator.node = self + self.style._applicator = self.applicator + self.style.reapply() + + ############################# + # End backwards compatibility + ############################# + + def _create(self) -> Any: + """Create a platform-specific implementation of this widget. + + A subclass of Widget should redefine this method to return its implementation. + """ + warn( + "Widgets should create and return their implementation in ._create(). This " + "will be an exception in a future version.", + RuntimeWarning, + stacklevel=2, + ) + def __repr__(self) -> str: return f"<{self.__class__.__name__}:0x{identifier(self):x}>" diff --git a/core/src/toga/widgets/box.py b/core/src/toga/widgets/box.py index 5f84160fb1..48ab114c78 100644 --- a/core/src/toga/widgets/box.py +++ b/core/src/toga/widgets/box.py @@ -24,14 +24,14 @@ 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: self.add(*children) + def _create(self): + return self.factory.Box(interface=self) + @property def enabled(self) -> bool: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/button.py b/core/src/toga/widgets/button.py index f8c2471d9c..0c07a76d15 100644 --- a/core/src/toga/widgets/button.py +++ b/core/src/toga/widgets/button.py @@ -44,9 +44,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 @@ -63,6 +60,9 @@ def __init__( self.on_press = on_press self.enabled = enabled + def _create(self) -> Any: + return self.factory.Button(interface=self) + @property def text(self) -> str: """The text displayed on the button. diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 0941a5d8ce..355b400144 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -1239,14 +1239,10 @@ def __init__( :param on_alt_release: Initial :any:`on_alt_release` handler. :param on_alt_drag: Initial :any:`on_alt_drag` handler. """ - super().__init__(id=id, style=style) 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 @@ -1257,6 +1253,9 @@ def __init__( self.on_alt_release = on_alt_release self.on_alt_drag = on_alt_drag + def _create(self) -> Any: + return self.factory.Canvas(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/dateinput.py b/core/src/toga/widgets/dateinput.py index 7408f38ae2..b277023bf8 100644 --- a/core/src/toga/widgets/dateinput.py +++ b/core/src/toga/widgets/dateinput.py @@ -51,9 +51,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 @@ -61,6 +58,9 @@ def __init__( self.value = value self.on_change = on_change + def _create(self) -> Any: + return self.factory.DateInput(interface=self) + @property def value(self) -> datetime.date: """The currently selected date. A value of ``None`` will be converted into diff --git a/core/src/toga/widgets/detailedlist.py b/core/src/toga/widgets/detailedlist.py index a14f698d31..434c08eab5 100644 --- a/core/src/toga/widgets/detailedlist.py +++ b/core/src/toga/widgets/detailedlist.py @@ -85,6 +85,16 @@ 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) ###################################################################### @@ -104,24 +114,15 @@ 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 self.on_refresh = on_refresh self.on_select = on_select + def _create(self) -> Any: + return self.factory.DetailedList(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/divider.py b/core/src/toga/widgets/divider.py index 4b7edf62c1..8a7db69fe1 100644 --- a/core/src/toga/widgets/divider.py +++ b/core/src/toga/widgets/divider.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Literal +from typing import Any, Literal from toga.constants import Direction @@ -29,10 +29,11 @@ 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 + def _create(self) -> Any: + return self.factory.Divider(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/imageview.py b/core/src/toga/widgets/imageview.py index 663b3d1071..7011e059ab 100644 --- a/core/src/toga/widgets/imageview.py +++ b/core/src/toga/widgets/imageview.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any, Literal from travertino.size import at_least @@ -83,12 +83,16 @@ 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 + def _create(self) -> Any: + return self.factory.ImageView(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 83fb1ba7a6..b448ae5905 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from .base import StyleT, Widget @@ -19,11 +21,11 @@ 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 _create(self) -> Any: + return self.factory.Label(interface=self) + def focus(self) -> None: """No-op; Label cannot accept input focus.""" pass diff --git a/core/src/toga/widgets/mapview.py b/core/src/toga/widgets/mapview.py index dd69651b07..425a5a65ab 100644 --- a/core/src/toga/widgets/mapview.py +++ b/core/src/toga/widgets/mapview.py @@ -155,8 +155,6 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl: Any = self.factory.MapView(interface=self) - self._pins = MapPinSet(self, pins) if location: @@ -169,6 +167,9 @@ def __init__( self.on_select = on_select + def _create(self) -> Any: + return self.factory.MapView(interface=self) + @property def location(self) -> toga.LatLng: """The latitude/longitude where the map is centered. diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index afbc10a2b5..c966ad036b 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -39,12 +39,8 @@ def __init__( :param on_change: A handler that will be invoked when the value of the widget changes. """ - 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 @@ -55,6 +51,9 @@ def __init__( self.placeholder = placeholder self.on_change = on_change + def _create(self) -> Any: + return self.factory.MultilineTextInput(interface=self) + @property def placeholder(self) -> str: """The placeholder text for the widget. diff --git a/core/src/toga/widgets/numberinput.py b/core/src/toga/widgets/numberinput.py index 8a9907fe67..d05edf937f 100644 --- a/core/src/toga/widgets/numberinput.py +++ b/core/src/toga/widgets/numberinput.py @@ -147,7 +147,6 @@ def __init__( self._max: Decimal | None = None self.on_change = None - self._impl = self.factory.NumberInput(interface=self) self.readonly = readonly self.step = step @@ -157,6 +156,9 @@ def __init__( self.on_change = on_change + def _create(self) -> Any: + return self.factory.NumberInput(interface=self) + @property def readonly(self) -> bool: """Can the value of the widget be modified by the user? diff --git a/core/src/toga/widgets/optioncontainer.py b/core/src/toga/widgets/optioncontainer.py index a1a6c1fc26..8a90f64d36 100644 --- a/core/src/toga/widgets/optioncontainer.py +++ b/core/src/toga/widgets/optioncontainer.py @@ -397,8 +397,6 @@ def __init__( self._content = OptionList(self) self.on_select = None - self._impl = self.factory.OptionContainer(interface=self) - if content is not None: for item in content: if isinstance(item, OptionItem): @@ -424,6 +422,9 @@ def __init__( self.on_select = on_select + def _create(self) -> Any: + return self.factory.OptionContainer(interface=self) + @property def enabled(self) -> bool: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/passwordinput.py b/core/src/toga/widgets/passwordinput.py index 48548b4935..b72bcba0f0 100644 --- a/core/src/toga/widgets/passwordinput.py +++ b/core/src/toga/widgets/passwordinput.py @@ -1,10 +1,12 @@ from __future__ import annotations +from typing import Any + from .textinput import TextInput class PasswordInput(TextInput): """Create a new password input widget.""" - def _create(self) -> None: - self._impl = self.factory.PasswordInput(interface=self) + def _create(self) -> Any: + return self.factory.PasswordInput(interface=self) diff --git a/core/src/toga/widgets/progressbar.py b/core/src/toga/widgets/progressbar.py index 49f2e67575..0d8e1cebcb 100644 --- a/core/src/toga/widgets/progressbar.py +++ b/core/src/toga/widgets/progressbar.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Literal, SupportsFloat +from typing import Any, Literal, SupportsFloat from .base import StyleT, Widget @@ -32,14 +32,15 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl = self.factory.ProgressBar(interface=self) - self.max = max self.value = value if running: self.start() + def _create(self) -> Any: + return self.factory.ProgressBar(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/scrollcontainer.py b/core/src/toga/widgets/scrollcontainer.py index 92c7e61722..a69e9d2673 100644 --- a/core/src/toga/widgets/scrollcontainer.py +++ b/core/src/toga/widgets/scrollcontainer.py @@ -45,15 +45,15 @@ def __init__( self._content: Widget | None = None self.on_scroll = None - # Create a platform specific implementation of a Scroll Container - self._impl = self.factory.ScrollContainer(interface=self) - # Set all attributes self.vertical = vertical self.horizontal = horizontal self.content = content self.on_scroll = on_scroll + def _create(self) -> Any: + return self.factory.ScrollContainer(interface=self) + @Widget.app.setter def app(self, app) -> None: # Invoke the superclass property setter diff --git a/core/src/toga/widgets/selection.py b/core/src/toga/widgets/selection.py index 2425c3e716..667877d35b 100644 --- a/core/src/toga/widgets/selection.py +++ b/core/src/toga/widgets/selection.py @@ -69,7 +69,6 @@ def __init__( self._items: SourceT | ListSource self.on_change = None # needed for _impl initialization - self._impl = self.factory.Selection(interface=self) self._accessor = accessor self.items = items @@ -79,6 +78,9 @@ def __init__( self.on_change = on_change self.enabled = enabled + def _create(self) -> Any: + return self.factory.Selection(interface=self) + @property def items(self) -> SourceT | ListSource: """The items to display in the selection. diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index b71f73e2cb..2ebd29fa00 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -39,6 +39,8 @@ def __call__(self, widget: Slider, /, **kwargs: Any) -> object: class Slider(Widget): + _MIN_WIDTH = 100 + def __init__( self, id: str | None = None, @@ -72,7 +74,6 @@ def __init__( :any:`range` of the slider. Defaults to ``(0, 1)``. """ super().__init__(id=id, style=style) - self._impl = self.factory.Slider(interface=self) ###################################################################### # 2023-06: Backwards compatibility @@ -115,7 +116,8 @@ def __init__( self.enabled = enabled - _MIN_WIDTH = 100 + def _create(self) -> Any: + return self.factory.Slider(interface=self) # Backends are inconsistent about when they produce events for programmatic changes, # so we deal with those in the interface layer. diff --git a/core/src/toga/widgets/splitcontainer.py b/core/src/toga/widgets/splitcontainer.py index 5d76f2a24a..29493e856e 100644 --- a/core/src/toga/widgets/splitcontainer.py +++ b/core/src/toga/widgets/splitcontainer.py @@ -2,7 +2,7 @@ import sys from collections.abc import Sequence -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from toga.app import App from toga.constants import Direction @@ -45,13 +45,13 @@ def __init__( super().__init__(id=id, style=style) self._content: list[SplitContainerContentT] = [None, None] - # Create a platform specific implementation of a SplitContainer - self._impl = self.factory.SplitContainer(interface=self) - if content: self.content = content self.direction = direction + def _create(self) -> Any: + return self.factory.SplitContainer(interface=self) + @property def enabled(self) -> bool: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/switch.py b/core/src/toga/widgets/switch.py index 1fd202301f..927e45a8d1 100644 --- a/core/src/toga/widgets/switch.py +++ b/core/src/toga/widgets/switch.py @@ -41,8 +41,6 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl = self.factory.Switch(interface=self) - self.text = text # Set a dummy handler before installing the actual on_change, because we do not @@ -54,6 +52,9 @@ def __init__( self.enabled = enabled + def _create(self) -> Any: + return self.factory.Switch(interface=self) + @property def text(self) -> str: """The text label for the Switch. diff --git a/core/src/toga/widgets/table.py b/core/src/toga/widgets/table.py index d0e991e0fb..f450c77d4d 100644 --- a/core/src/toga/widgets/table.py +++ b/core/src/toga/widgets/table.py @@ -78,8 +78,6 @@ def __init__( defined. :param on_double_click: **DEPRECATED**; use :attr:`on_activate`. """ - super().__init__(id=id, style=style) - ###################################################################### # 2023-06: Backwards compatibility ###################################################################### @@ -119,12 +117,16 @@ def __init__( self.on_activate = None self._data = None - self._impl = self.factory.Table(interface=self) + super().__init__(id=id, style=style) + self.data = data self.on_select = on_select self.on_activate = on_activate + def _create(self) -> Any: + return self.factory.Table(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index cea1d1e44a..20b62f075d 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -81,9 +81,6 @@ def __init__( """ super().__init__(id=id, style=style) - # Create a platform specific implementation of the widget - self._create() - self.placeholder = placeholder self.readonly = readonly @@ -103,8 +100,8 @@ def __init__( self.on_lose_focus = on_lose_focus self.on_gain_focus = on_gain_focus - def _create(self) -> None: - self._impl = self.factory.TextInput(interface=self) + def _create(self) -> Any: + return self.factory.TextInput(interface=self) @property def readonly(self) -> bool: diff --git a/core/src/toga/widgets/timeinput.py b/core/src/toga/widgets/timeinput.py index 0a23987bb0..3ae7e13e3f 100644 --- a/core/src/toga/widgets/timeinput.py +++ b/core/src/toga/widgets/timeinput.py @@ -42,9 +42,6 @@ def __init__( """ super().__init__(id=id, style=style) - # Create a platform specific implementation of a TimeInput - self._impl = self.factory.TimeInput(interface=self) - self.on_change = None self.min = min self.max = max @@ -52,6 +49,9 @@ def __init__( self.value = value self.on_change = on_change + def _create(self) -> Any: + return self.factory.TimeInput(interface=self) + @property def value(self) -> datetime.time: """The currently selected time. A value of ``None`` will be converted into the diff --git a/core/src/toga/widgets/tree.py b/core/src/toga/widgets/tree.py index 4c2b2c616e..a4d5ceb862 100644 --- a/core/src/toga/widgets/tree.py +++ b/core/src/toga/widgets/tree.py @@ -78,8 +78,6 @@ def __init__( defined. :param on_double_click: **DEPRECATED**; use :attr:`on_activate`. """ - super().__init__(id=id, style=style) - ###################################################################### # 2023-06: Backwards compatibility ###################################################################### @@ -117,12 +115,16 @@ def __init__( self.on_activate = None self._data = None - self._impl = self.factory.Tree(interface=self) + super().__init__(id=id, style=style) + self.data = data self.on_select = on_select self.on_activate = on_activate + def _create(self): + return self.factory.Tree(interface=self) + @property def enabled(self) -> Literal[True]: """Is the widget currently enabled? i.e., can the user interact with the widget? diff --git a/core/src/toga/widgets/webview.py b/core/src/toga/widgets/webview.py index 740d830af2..64d5a641bd 100644 --- a/core/src/toga/widgets/webview.py +++ b/core/src/toga/widgets/webview.py @@ -44,13 +44,15 @@ def __init__( """ super().__init__(id=id, style=style) - self._impl = self.factory.WebView(interface=self) self.user_agent = user_agent # Set the load handler before loading the first URL. self.on_webview_load = on_webview_load self.url = url + def _create(self) -> Any: + return self.factory.WebView(interface=self) + def _set_url(self, url: str | None, future: asyncio.Future | None) -> None: # Utility method for validating and setting the URL with a future. if (url is not None) and not url.startswith(("https://", "http://")): diff --git a/core/tests/app/test_widget_registry.py b/core/tests/app/test_widget_registry.py index a5a25d96f8..0e5d567056 100644 --- a/core/tests/app/test_widget_registry.py +++ b/core/tests/app/test_widget_registry.py @@ -1,24 +1,15 @@ import pytest -import toga from toga.app import WidgetRegistry +from ..utils import ExampleLeafWidget + @pytest.fixture def widget_registry(): return WidgetRegistry() -# Create the simplest possible widget with a concrete implementation -class ExampleWidget(toga.Widget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) - - def __repr__(self): - return f"Widget(id={self.id!r})" - - def test_empty_registry(widget_registry): assert len(widget_registry) == 0 assert list(widget_registry) == [] @@ -28,7 +19,7 @@ def test_empty_registry(widget_registry): def test_add_widget(widget_registry): """Widgets can be added to the registry.""" # Add a widget to the registry - widget1 = ExampleWidget(id="widget-1") + widget1 = ExampleLeafWidget(id="widget-1") widget_registry._add(widget1) assert len(widget_registry) == 1 @@ -37,7 +28,7 @@ def test_add_widget(widget_registry): assert widget_registry["widget-1"] == widget1 # Add a second widget - widget2 = ExampleWidget(id="widget-2") + widget2 = ExampleLeafWidget(id="widget-2") widget_registry._add(widget2) assert len(widget_registry) == 2 @@ -48,12 +39,12 @@ def test_add_widget(widget_registry): def test_update_widgets(widget_registry): """The registry can be bulk updated.""" # Add a widget to the registry - widget1 = ExampleWidget(id="widget-1") + widget1 = ExampleLeafWidget(id="widget-1") widget_registry._add(widget1) - widget2 = ExampleWidget(id="widget-2") - widget3 = ExampleWidget(id="widget-3") - widget4 = ExampleWidget(id="widget-4") + widget2 = ExampleLeafWidget(id="widget-2") + widget3 = ExampleLeafWidget(id="widget-3") + widget4 = ExampleLeafWidget(id="widget-4") widget_registry._update({widget2, widget3, widget4}) assert len(widget_registry) == 4 @@ -67,8 +58,8 @@ def test_remove_widget(widget_registry): """A widget can be removed from the repository.""" "Widgets can be added to the registry" # Add a widget to the registry - widget1 = ExampleWidget(id="widget-1") - widget2 = ExampleWidget(id="widget-2") + widget1 = ExampleLeafWidget(id="widget-1") + widget2 = ExampleLeafWidget(id="widget-2") widget_registry._update({widget1, widget2}) assert len(widget_registry) == 2 @@ -82,7 +73,7 @@ def test_remove_widget(widget_registry): def test_add_same_widget_twice(widget_registry): """A widget cannot be added to the same registry twice.""" # Add a widget to the registry - widget1 = ExampleWidget(id="widget-1") + widget1 = ExampleLeafWidget(id="widget-1") widget_registry._add(widget1) assert len(widget_registry) == 1 @@ -102,12 +93,12 @@ def test_add_same_widget_twice(widget_registry): def test_add_duplicate_id(widget_registry): """A widget cannot be added to the same registry twice.""" # Add a widget to the registry - widget1 = ExampleWidget(id="widget-1") + widget1 = ExampleLeafWidget(id="widget-1") widget_registry._add(widget1) assert len(widget_registry) == 1 - new_widget = ExampleWidget(id="widget-1") + new_widget = ExampleLeafWidget(id="widget-1") # Add the widget again; this raises an error with pytest.raises( @@ -123,7 +114,7 @@ def test_add_duplicate_id(widget_registry): def test_setitem(widget_registry): """Widgets cannot be directly assigned to the registry.""" - widget1 = ExampleWidget(id="widget-1") + widget1 = ExampleLeafWidget(id="widget-1") with pytest.raises( TypeError, diff --git a/core/tests/style/pack/utils.py b/core/tests/style/pack/utils.py index 185f2e132f..86c3f1f8d9 100644 --- a/core/tests/style/pack/utils.py +++ b/core/tests/style/pack/utils.py @@ -7,9 +7,23 @@ class ExampleNode(Node): def __init__(self, name, style, size=None, children=None): - super().__init__( - style=style, children=children, applicator=TogaApplicator(self) - ) + self._impl = Mock() + self._children = None + + super().__init__(style=style, children=children, applicator=TogaApplicator()) + + ############################################## + # Backwards compatibility for Travertino 0.3.0 + ############################################## + + if not hasattr(self.applicator, "node"): + self.applicator.node = self + self.style._applicator = self.applicator + self.style.reapply() + + ############################# + # End backwards compatibility + ############################# self.name = name self._impl = Mock() diff --git a/core/tests/style/test_applicator.py b/core/tests/style/test_applicator.py index b8f2e4bb9b..01a0594bf3 100644 --- a/core/tests/style/test_applicator.py +++ b/core/tests/style/test_applicator.py @@ -1,27 +1,12 @@ import pytest -import toga from toga.colors import REBECCAPURPLE from toga.fonts import FANTASY +from toga.style import TogaApplicator from toga.style.pack import HIDDEN, RIGHT, VISIBLE from toga_dummy.utils import assert_action_performed_with - -# Create the simplest possible widget with a concrete implementation that will -# allow children -class ExampleWidget(toga.Widget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) - self._children = [] - - -# Create the simplest possible widget with a concrete implementation that cannot -# have children. -class ExampleLeafWidget(toga.Widget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) +from ..utils import ExampleLeafWidget, ExampleWidget @pytest.fixture @@ -154,3 +139,17 @@ def test_set_background_color(child, widget): widget.applicator.set_background_color(REBECCAPURPLE) assert_action_performed_with(widget, "set background color", color=REBECCAPURPLE) + + +def test_deprecated_widget_argument(widget): + """The widget argument to TogaApplicator is deprecated.""" + with pytest.warns(DeprecationWarning): + TogaApplicator(widget) + + +def test_widget_alias_to_node(widget): + """Applicator.widget is an alias to applicator.node.""" + applicator = widget.applicator + + assert applicator.widget is widget + assert applicator.widget is applicator.node diff --git a/core/tests/utils.py b/core/tests/utils.py new file mode 100644 index 0000000000..1021c67cb7 --- /dev/null +++ b/core/tests/utils.py @@ -0,0 +1,26 @@ +import toga + + +# Create the simplest possible widget with a concrete implementation that will +# allow children +class ExampleWidget(toga.Widget): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._children = [] + + def _create(self): + return self.factory.Widget(interface=self) + + def __repr__(self): + return f"Widget(id={self.id!r})" + + +# Create the simplest possible widget with a concrete implementation that cannot +# have children. +class ExampleLeafWidget(toga.Widget): + def _create(self): + return self.factory.Widget(interface=self) + + def __repr__(self): + return f"Widget(id={self.id!r})" diff --git a/core/tests/widgets/test_base.py b/core/tests/widgets/test_base.py index fb71b92f53..9052bdf095 100644 --- a/core/tests/widgets/test_base.py +++ b/core/tests/widgets/test_base.py @@ -1,6 +1,9 @@ +from unittest.mock import Mock + import pytest import toga +from toga.platform import get_platform_factory from toga.style import Pack from toga_dummy.utils import ( EventLog, @@ -11,22 +14,23 @@ attribute_value, ) +from ..utils import ExampleLeafWidget, ExampleWidget + -# Create the simplest possible widget with a concrete implementation that will -# allow children -class ExampleWidget(toga.Widget): +# Represent a hypothetical user-created widget class that does create and assign a valid +# implementation, but doesn't do so in _create(). This is to assist with migration for +# existing user code written before #2942 reorganized widget initialization. +# +# Right now this only issues a warning. Unfortunately it only works if the _impl is +# set *before* super().__init__; any existing code that does so afterward will raise an +# exception. +class WidgetSubclassWithoutCreate(toga.Widget): def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) - self._children = [] + self.factory = get_platform_factory() + self._impl = self.factory.Widget(interface=self) -# Create the simplest possible widget with a concrete implementation that cannot -# have children. -class ExampleLeafWidget(toga.Widget): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) @pytest.fixture @@ -911,6 +915,7 @@ def test_remove_from_non_parent(widget): other = ExampleWidget(id="other") child = ExampleLeafWidget(id="child_id") other.add(child) + EventLog.reset() assert widget.children == [] assert other.children == [child] @@ -1237,3 +1242,22 @@ def test_tab_index(widget): assert widget.tab_index == 8 assert attribute_value(widget, "tab_index") == tab_index + + +def test_one_reapply_during_init(): + """Style's reapply() should be called exactly once during widget initialization.""" + + class MockedPack(Pack): + reapply = Mock() + + ExampleWidget(style=MockedPack()) + MockedPack.reapply.assert_called_once() + + +def test_widget_with_no_create(): + """Creating a widget with no _create() method issues a warning.""" + with pytest.warns( + RuntimeWarning, + match=r"Widgets should create and return their implementation", + ): + WidgetSubclassWithoutCreate() diff --git a/core/tests/window/__init__.py b/core/tests/window/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/tests/window/test_filtered_widget_registry.py b/core/tests/window/test_filtered_widget_registry.py index d8c4af16c7..29ffc895f9 100644 --- a/core/tests/window/test_filtered_widget_registry.py +++ b/core/tests/window/test_filtered_widget_registry.py @@ -2,15 +2,7 @@ import toga - -# Create the simplest possible widget with a concrete implementation -class ExampleWidget(toga.Widget): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._impl = self.factory.Widget(self) - - def __repr__(self): - return f"Widget(id={self.id!r})" +from ..utils import ExampleLeafWidget # Create a box subclass with a reproducible repr @@ -24,8 +16,8 @@ def make_window(win_id): win.content = ExampleBox( id=f"{win_id}.0", children=[ - ExampleWidget(id=f"{win_id}.1"), - ExampleWidget(id=f"{win_id}.2"), + ExampleLeafWidget(id=f"{win_id}.1"), + ExampleLeafWidget(id=f"{win_id}.2"), ], ) return win @@ -155,7 +147,7 @@ def test_reuse_id(app): # Create a widget with the magic ID. The widget still isn't in the registry, because # it's not part of a window - first = ExampleWidget(id="magic") + first = ExampleLeafWidget(id="magic") assert "magic" not in app.widgets assert "magic" not in win_1.widgets @@ -171,7 +163,7 @@ def test_reuse_id(app): # Create a second widget with the same ID. # The widget exists, but the registry is storing `first`. - second = ExampleWidget(id="magic") + second = ExampleLeafWidget(id="magic") assert "magic" in app.widgets assert "magic" in win_1.widgets diff --git a/dummy/src/toga_dummy/utils.py b/dummy/src/toga_dummy/utils.py index 2cb1ce3358..d5ab2e5f32 100644 --- a/dummy/src/toga_dummy/utils.py +++ b/dummy/src/toga_dummy/utils.py @@ -371,7 +371,14 @@ def assert_action_performed_with(_widget, _action, **test_data): found = False except AttributeError: # No raw attribute; use the provided value as-is - if data[key] != value: + try: + if data[key] != value: + found = False + ######################################################## + # Backwards compatibility for Travertino 0.3.0 + # Font.__eq__ throws an AttributeError against non-Fonts + ######################################################## + except AttributeError: found = False except KeyError: found = False diff --git a/dummy/src/toga_dummy/widgets/base.py b/dummy/src/toga_dummy/widgets/base.py index 93743330f6..186f3fb9a3 100644 --- a/dummy/src/toga_dummy/widgets/base.py +++ b/dummy/src/toga_dummy/widgets/base.py @@ -7,7 +7,6 @@ class Widget(LoggedObject): def __init__(self, interface): super().__init__() self.interface = interface - self.interface._impl = self self.container = None self.create() diff --git a/gtk/src/toga_gtk/widgets/base.py b/gtk/src/toga_gtk/widgets/base.py index c1868f1aa2..7363341459 100644 --- a/gtk/src/toga_gtk/widgets/base.py +++ b/gtk/src/toga_gtk/widgets/base.py @@ -9,7 +9,6 @@ class Widget: def __init__(self, interface): super().__init__() self.interface = interface - self.interface._impl = self self._container = None self.native = None self.style_providers = {} @@ -24,9 +23,6 @@ def __init__(self, interface): self.native.set_name(f"toga-{self.interface.id}") self.native.get_style_context().add_class("toga") - # Ensure initial styles are applied. - self.interface.style.reapply() - @abstractmethod def create(self): ... diff --git a/iOS/src/toga_iOS/widgets/base.py b/iOS/src/toga_iOS/widgets/base.py index 59cb4f241b..4692a08286 100644 --- a/iOS/src/toga_iOS/widgets/base.py +++ b/iOS/src/toga_iOS/widgets/base.py @@ -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): ... diff --git a/textual/src/toga_textual/factory.py b/textual/src/toga_textual/factory.py index b737aed521..d41f6da7fa 100644 --- a/textual/src/toga_textual/factory.py +++ b/textual/src/toga_textual/factory.py @@ -3,8 +3,7 @@ from . import dialogs from .app import App from .command import Command - -# from .fonts import Font +from .fonts import Font from .icons import Icon # from .images import Image @@ -50,7 +49,7 @@ def not_implemented(feature): "not_implemented", "App", "Command", - # "Font", + "Font", "Icon", # "Image", "Paths", diff --git a/textual/src/toga_textual/fonts.py b/textual/src/toga_textual/fonts.py new file mode 100644 index 0000000000..fa9d3f0e6c --- /dev/null +++ b/textual/src/toga_textual/fonts.py @@ -0,0 +1,2 @@ +class Font: + def __init__(*args, **kwargs): ... diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py index ce10ebad3e..dd2f0a6693 100644 --- a/textual/src/toga_textual/widgets/base.py +++ b/textual/src/toga_textual/widgets/base.py @@ -32,7 +32,6 @@ def scale_out_vertical(self, value): class Widget(Scalable): def __init__(self, interface): self.interface = interface - self.interface._impl = self self.container = None self.create() diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index 1f721aade0..e7c9c565d1 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -4,7 +4,6 @@ class Widget: def __init__(self, interface): self.interface = interface - self.interface._impl = self self._container = None self.create() diff --git a/winforms/src/toga_winforms/widgets/base.py b/winforms/src/toga_winforms/widgets/base.py index 3f0d2cdf86..cc683bd00e 100644 --- a/winforms/src/toga_winforms/widgets/base.py +++ b/winforms/src/toga_winforms/widgets/base.py @@ -46,7 +46,6 @@ class Widget(Scalable, ABC): def __init__(self, interface): self.interface = interface - self.interface._impl = self self._container = None self.native = None @@ -64,8 +63,6 @@ def __init__(self, interface): # at System.Windows.Forms.TabControl.GetTabPage(Int32 index) self.native.CreateGraphics().Dispose() - self.interface.style.reapply() - @abstractmethod def create(self): ...