Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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 assign 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()``. Existing user code inheriting from Widget that creates 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()
Comment thread
HalfWhitt marked this conversation as resolved.

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.
"""
Comment thread
HalfWhitt marked this conversation as resolved.
return self.node

def refresh(self) -> None:
# print("RE-EVALUATE LAYOUT", self.widget)
Expand Down
13 changes: 10 additions & 3 deletions core/src/toga/style/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,22 @@ def apply(self, prop: str, value: object) -> None:
"font_variant",
"font_weight",
):
self._applicator.set_font(
Font(
# For other properties, a backend that doesn't support it will simply do
# a no-op when instructed to set it. But for font, we need to know
# up-front, because just creating a Font object will fail.
Comment thread
HalfWhitt marked this conversation as resolved.
Outdated
try:
font = Font(
self.font_family,
self.font_size,
style=self.font_style,
variant=self.font_variant,
weight=self.font_weight,
)
)
except NotImplementedError: # pragma: no cover
font = None

if font: # pragma: no branch
self._applicator.set_font(font)
else:
# Any other style change will cause a change in layout geometry,
# so perform a refresh.
Expand Down
8 changes: 6 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 @@ -22,11 +24,13 @@ def __init__(
"""
super().__init__(id=id, style=style)

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

if running:
self.start()

def _create(self) -> None:
self.factory = get_platform_factory()
Comment thread
HalfWhitt marked this conversation as resolved.
Outdated
self._impl = 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
48 changes: 42 additions & 6 deletions core/src/toga/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from __future__ import annotations

from builtins import id as identifier
from typing import TYPE_CHECKING, Any, TypeVar
from typing import TYPE_CHECKING, TypeVar
from warnings import warn

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 Down Expand Up @@ -34,16 +34,52 @@ 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(),
Comment thread
HalfWhitt marked this conversation as resolved.
Outdated
applicator=None,
Comment thread
HalfWhitt marked this conversation as resolved.
Outdated
)

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()
# Create and assign _impl
self._create()

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) -> None:
"""Create and store a platform-specific implementation of this widget.

A subclass of Widget should redefine this method to create an implementation
and assign it to self._impl.
"""
warn(
"Widgets should create their implementation and assign it to self._impl 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
9 changes: 6 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 @@ -24,14 +26,15 @@ 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) -> None:
self.factory = get_platform_factory()
self._impl = 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
8 changes: 5 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 @@ -44,9 +45,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 +61,10 @@ def __init__(
self.on_press = on_press
self.enabled = enabled

def _create(self) -> None:
self.factory = get_platform_factory()
self._impl = self.factory.Button(interface=self)

@property
def text(self) -> str:
"""The text displayed on the button.
Expand Down
9 changes: 5 additions & 4 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,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 @@ -1254,6 +1251,10 @@ def __init__(
self.on_alt_release = on_alt_release
self.on_alt_drag = on_alt_drag

def _create(self) -> None:
self.factory = get_platform_factory()
self._impl = 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
8 changes: 5 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 Down Expand Up @@ -51,16 +52,17 @@ 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) -> None:
self.factory = get_platform_factory()
self._impl = 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: 14 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,15 @@ 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 @@ -103,23 +113,16 @@ 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) -> None:
self.factory = get_platform_factory()
self._impl = 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: 5 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 @@ -29,10 +30,12 @@ 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) -> None:
self.factory = get_platform_factory()
self._impl = 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
Loading