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 all 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.
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