Skip to content

Commit

Permalink
Merge pull request #2942 from HalfWhitt/widget_initialization
Browse files Browse the repository at this point in the history
Restructured widget initialization order
  • Loading branch information
mhsmith authored Dec 4, 2024
2 parents 1c8b7d4 + a3f9a54 commit df65161
Show file tree
Hide file tree
Showing 47 changed files with 305 additions and 171 deletions.
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.
1 change: 1 addition & 0 deletions changes/2942.removal.rst
Original file line number Diff line number Diff line change
@@ -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.
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
1 change: 0 additions & 1 deletion cocoa/src/toga_cocoa/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:")
Expand Down
22 changes: 20 additions & 2 deletions core/src/toga/style/applicator.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
7 changes: 4 additions & 3 deletions core/src/toga/widgets/activityindicator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Literal
from typing import Any, Literal

from .base import StyleT, Widget

Expand All @@ -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?
Expand Down
63 changes: 58 additions & 5 deletions core/src/toga/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}>"

Expand Down
6 changes: 3 additions & 3 deletions core/src/toga/widgets/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
6 changes: 3 additions & 3 deletions core/src/toga/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
7 changes: 3 additions & 4 deletions core/src/toga/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?
Expand Down
6 changes: 3 additions & 3 deletions core/src/toga/widgets/dateinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,16 @@ 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

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
Expand Down
25 changes: 13 additions & 12 deletions core/src/toga/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

######################################################################
Expand All @@ -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?
Expand Down
7 changes: 4 additions & 3 deletions core/src/toga/widgets/divider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Literal
from typing import Any, Literal

from toga.constants import Direction

Expand Down Expand Up @@ -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?
Expand Down
10 changes: 7 additions & 3 deletions core/src/toga/widgets/imageview.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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?
Expand Down
8 changes: 5 additions & 3 deletions core/src/toga/widgets/label.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import Any

from .base import StyleT, Widget


Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions core/src/toga/widgets/mapview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
Loading

0 comments on commit df65161

Please sign in to comment.