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 4 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
2 changes: 1 addition & 1 deletion cocoa/src/toga_cocoa/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def create(self):
self._icon = None

self.native.buttonType = NSMomentaryPushInButton
self._set_button_style()
HalfWhitt marked this conversation as resolved.
Show resolved Hide resolved
# self._set_button_style()

self.native.target = self.native
self.native.action = SEL("onPress:")
Expand Down
26 changes: 24 additions & 2 deletions core/src/toga/style/applicator.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
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.
"""
HalfWhitt marked this conversation as resolved.
Show resolved Hide resolved
return self.node

@widget.setter
def widget(self, widget: Widget):
self.node = widget

def refresh(self) -> None:
# print("RE-EVALUATE LAYOUT", self.widget)
Expand Down
7 changes: 5 additions & 2 deletions core/src/toga/widgets/activityindicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import Literal

from toga.platform import get_platform_factory

from .base import StyleT, Widget


Expand All @@ -20,10 +22,11 @@ def __init__(
:param running: Describes whether the indicator is running at the
time it is created.
"""
super().__init__(id=id, style=style)

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

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

if running:
self.start()

Expand Down
33 changes: 27 additions & 6 deletions core/src/toga/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
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

from toga.platform import get_platform_factory
from toga.style import Pack, TogaApplicator

if TYPE_CHECKING:
Expand All @@ -17,6 +16,8 @@


class Widget(Node):
_IMPL_NAME = "Widget"

_MIN_WIDTH = 100
_MIN_HEIGHT = 100

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

from collections.abc import Iterable

from toga.platform import get_platform_factory

from .base import StyleT, Widget


Expand All @@ -22,11 +24,11 @@ def __init__(
will be applied to the widget.
:param children: An optional list of children for to add to the Box.
"""
super().__init__(id=id, style=style)

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

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

# Children need to be added *after* the impl has been created.
self._children: list[Widget] = []
if children is not None:
Expand Down
7 changes: 4 additions & 3 deletions core/src/toga/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import toga
from toga.handlers import wrapped_handler
from toga.platform import get_platform_factory

from .base import StyleT, Widget

Expand Down Expand Up @@ -42,11 +43,11 @@ def __init__(
:param enabled: Is the button enabled (i.e., can it be pressed?). Optional; by
default, buttons are created in an enabled state.
"""
super().__init__(id=id, style=style)

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

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

# 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
6 changes: 3 additions & 3 deletions core/src/toga/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
Font,
)
from toga.handlers import wrapped_handler
from toga.platform import get_platform_factory

from .base import StyleT, Widget

Expand Down Expand Up @@ -1236,14 +1237,13 @@ def __init__(
:param on_alt_release: Initial :any:`on_alt_release` handler.
:param on_alt_drag: Initial :any:`on_alt_drag` handler.
"""
self.factory = get_platform_factory()
self._impl = self.factory.Canvas(interface=self)

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 Down
9 changes: 6 additions & 3 deletions core/src/toga/widgets/dateinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import toga
from toga.handlers import wrapped_handler
from toga.platform import get_platform_factory

from .base import StyleT, Widget

Expand All @@ -27,6 +28,8 @@ def __call__(self, widget: DateInput, /, **kwargs: Any) -> object:


class DateInput(Widget):
_IMPL_NAME = "DateInput"

_MIN_WIDTH = 200

def __init__(
Expand All @@ -49,11 +52,11 @@ def __init__(
:param max: The latest date (inclusive) that can be selected.
:param on_change: A handler that will be invoked when the value changes.
"""
super().__init__(id=id, style=style)

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

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

self.on_change = None
self.min = min
self.max = max
Expand Down
24 changes: 13 additions & 11 deletions core/src/toga/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import toga
from toga.handlers import wrapped_handler
from toga.platform import get_platform_factory
from toga.sources import ListSource, Row, Source

from .base import StyleT, Widget
Expand Down Expand Up @@ -85,6 +86,18 @@ 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

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

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

######################################################################
Expand All @@ -103,17 +116,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
6 changes: 4 additions & 2 deletions core/src/toga/widgets/divider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Literal

from toga.constants import Direction
from toga.platform import get_platform_factory

from .base import StyleT, Widget

Expand All @@ -27,10 +28,11 @@ def __init__(
:attr:`~toga.constants.Direction.VERTICAL`; defaults to
:attr:`~toga.constants.Direction.HORIZONTAL`
"""
self.factory = get_platform_factory()
self._impl = self.factory.Divider(interface=self)

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: 6 additions & 1 deletion core/src/toga/widgets/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from travertino.size import at_least

import toga
from toga.platform import get_platform_factory
from toga.style.pack import NONE
from toga.widgets.base import StyleT, Widget

Expand Down Expand Up @@ -83,10 +84,14 @@ 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.factory = get_platform_factory()
self._impl = self.factory.ImageView(interface=self)

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

self.image = image

@property
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 toga.platform import get_platform_factory

from .base import StyleT, Widget


Expand All @@ -17,11 +19,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)

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

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

self.text = text

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

import toga
from toga.handlers import wrapped_handler
from toga.platform import get_platform_factory

from .base import StyleT, Widget


class MapPin:
_IMPL_NAME = "MapPin"

def __init__(
self,
location: toga.LatLng | tuple[float, float],
Expand Down Expand Up @@ -153,9 +156,10 @@ def __init__(
:param on_select: A handler that will be invoked when the user selects a map
pin.
"""
super().__init__(id=id, style=style)
self.factory = get_platform_factory()
self._impl = self.factory.MapView(interface=self)

self._impl: Any = self.factory.MapView(interface=self)
super().__init__(id=id, style=style)

self._pins = MapPinSet(self, pins)

Expand Down
Loading
Loading