mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #2205 from davep/bug/2203/radioset
Rework `RadioSet` so it no longer leans on the DOM for state
This commit is contained in:
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### [Fixed]
|
||||||
|
|
||||||
|
- `RadioSet` is now far less likely to report `pressed_button` as `None` https://github.com/Textualize/textual/issues/2203
|
||||||
|
|
||||||
## [0.17.3] - 2023-04-02
|
## [0.17.3] - 2023-04-02
|
||||||
|
|
||||||
### [Fixed]
|
### [Fixed]
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import rich.repr
|
||||||
|
|
||||||
from ..containers import Container
|
from ..containers import Container
|
||||||
from ..css.query import DOMQuery, QueryError
|
|
||||||
from ..message import Message
|
from ..message import Message
|
||||||
from ._radio_button import RadioButton
|
from ._radio_button import RadioButton
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ class RadioSet(Container):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rich.repr.auto
|
||||||
class Changed(Message, bubble=True):
|
class Changed(Message, bubble=True):
|
||||||
"""Posted when the pressed button in the set changes.
|
"""Posted when the pressed button in the set changes.
|
||||||
|
|
||||||
@@ -46,14 +48,14 @@ class RadioSet(Container):
|
|||||||
"""A reference to the `RadioSet` that was changed."""
|
"""A reference to the `RadioSet` that was changed."""
|
||||||
self.pressed = pressed
|
self.pressed = pressed
|
||||||
"""The `RadioButton` that was pressed to make the change."""
|
"""The `RadioButton` that was pressed to make the change."""
|
||||||
# Note: it would be cleaner to use `sender.pressed_index` here,
|
self.index = radio_set.pressed_index
|
||||||
# but we can't be 100% sure all of the updates have happened at
|
|
||||||
# this point, and so we can't go looking for the index of the
|
|
||||||
# pressed button via the normal route. So here we go under the
|
|
||||||
# hood.
|
|
||||||
self.index = radio_set._nodes.index(pressed)
|
|
||||||
"""The index of the `RadioButton` that was pressed to make the change."""
|
"""The index of the `RadioButton` that was pressed to make the change."""
|
||||||
|
|
||||||
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
|
yield "radio_set", self.radio_set
|
||||||
|
yield "pressed", self.pressed
|
||||||
|
yield "index", self.index
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*buttons: str | RadioButton,
|
*buttons: str | RadioButton,
|
||||||
@@ -76,6 +78,8 @@ class RadioSet(Container):
|
|||||||
[RadioButton][textual.widgets.RadioButton] will be created from
|
[RadioButton][textual.widgets.RadioButton] will be created from
|
||||||
it.
|
it.
|
||||||
"""
|
"""
|
||||||
|
self._pressed_button: RadioButton | None = None
|
||||||
|
"""Holds the radio buttons we're responsible for."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
*[
|
*[
|
||||||
(button if isinstance(button, RadioButton) else RadioButton(button))
|
(button if isinstance(button, RadioButton) else RadioButton(button))
|
||||||
@@ -87,22 +91,22 @@ class RadioSet(Container):
|
|||||||
disabled=disabled,
|
disabled=disabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def _buttons(self) -> DOMQuery[RadioButton]:
|
|
||||||
"""The buttons within the set."""
|
|
||||||
return self.query(RadioButton)
|
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""Perform some processing once mounted in the DOM."""
|
"""Perform some processing once mounted in the DOM."""
|
||||||
|
|
||||||
# It's possible for the user to pass in a collection of radio
|
# It's possible for the user to pass in a collection of radio
|
||||||
# buttons, with more than one set to on. So here we check for that
|
# buttons, with more than one set to on; they shouldn't, but we
|
||||||
# and, for want of a better approach, we keep the first one on and
|
# can't stop them. So here we check for that and, for want of a
|
||||||
# turn all the others off.
|
# better approach, we keep the first one on and turn all the others
|
||||||
switched_on = self._buttons.filter(".-on")
|
# off.
|
||||||
if len(switched_on) > 1:
|
switched_on = [button for button in self.query(RadioButton) if button.value]
|
||||||
with self.prevent(RadioButton.Changed):
|
with self.prevent(RadioButton.Changed):
|
||||||
for button in switched_on[1:]:
|
for button in switched_on[1:]:
|
||||||
button.value = False
|
button.value = False
|
||||||
|
|
||||||
|
# Keep track of which button is initially pressed.
|
||||||
|
if switched_on:
|
||||||
|
self._pressed_button = switched_on[0]
|
||||||
|
|
||||||
def on_radio_button_changed(self, event: RadioButton.Changed) -> None:
|
def on_radio_button_changed(self, event: RadioButton.Changed) -> None:
|
||||||
"""Respond to the value of a button in the set being changed.
|
"""Respond to the value of a button in the set being changed.
|
||||||
@@ -110,39 +114,39 @@ class RadioSet(Container):
|
|||||||
Args:
|
Args:
|
||||||
event: The event.
|
event: The event.
|
||||||
"""
|
"""
|
||||||
# If the button is changing to be the pressed button...
|
# We're going to consume the underlying radio button events, making
|
||||||
if event.radio_button.value:
|
# it appear as if they don't emit their own, as far as the caller is
|
||||||
# ...send off a message to say that the pressed state has
|
# concerned. As such, stop the event bubbling and also prohibit the
|
||||||
# changed.
|
# same event being sent out if/when we make a value change in here.
|
||||||
self.post_message(self.Changed(self, event.radio_button))
|
event.stop()
|
||||||
# ...then look for the button that was previously the pressed
|
with self.prevent(RadioButton.Changed):
|
||||||
# one and unpress it.
|
# If the message pertains to a button being clicked to on...
|
||||||
for button in self._buttons.filter(".-on"):
|
if event.radio_button.value:
|
||||||
if button != event.radio_button:
|
# If there's a button pressed right now and it's not really a
|
||||||
button.value = False
|
# case of the user mashing on the same button...
|
||||||
break
|
if (
|
||||||
else:
|
self._pressed_button is not None
|
||||||
# If this leaves us with no buttons checked, disallow that. Note
|
and self._pressed_button != event.radio_button
|
||||||
# that we stop the current event and (see below) we also prevent
|
):
|
||||||
# another Changed event being emitted. This should all be seen
|
self._pressed_button.value = False
|
||||||
# as a non-operation.
|
# Make the pressed button this new button.
|
||||||
event.stop()
|
self._pressed_button = event.radio_button
|
||||||
if not self._buttons.filter(".-on"):
|
# Emit a message to say our state has changed.
|
||||||
with self.prevent(RadioButton.Changed):
|
self.post_message(self.Changed(self, event.radio_button))
|
||||||
event.radio_button.value = True
|
else:
|
||||||
|
# We're being clicked off, we don't want that.
|
||||||
|
event.radio_button.value = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pressed_button(self) -> RadioButton | None:
|
def pressed_button(self) -> RadioButton | None:
|
||||||
"""The currently-pressed button, or `None` if none are pressed."""
|
"""The currently-pressed button, or `None` if none are pressed."""
|
||||||
try:
|
return self._pressed_button
|
||||||
return self.query_one("RadioButton.-on", RadioButton)
|
|
||||||
except QueryError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pressed_index(self) -> int:
|
def pressed_index(self) -> int:
|
||||||
"""The index of the currently-pressed button, or -1 if none are pressed."""
|
"""The index of the currently-pressed button, or -1 if none are pressed."""
|
||||||
try:
|
return (
|
||||||
return self._nodes.index(self.pressed_button)
|
self._nodes.index(self._pressed_button)
|
||||||
except ValueError:
|
if self._pressed_button is not None
|
||||||
return -1
|
else -1
|
||||||
|
)
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ async def test_radio_sets_initial_state():
|
|||||||
async def test_radio_sets_toggle():
|
async def test_radio_sets_toggle():
|
||||||
"""Test the status of the radio sets after they've been toggled."""
|
"""Test the status of the radio sets after they've been toggled."""
|
||||||
async with RadioSetApp().run_test() as pilot:
|
async with RadioSetApp().run_test() as pilot:
|
||||||
pilot.app.query_one("#from_buttons", RadioSet)._buttons[0].toggle()
|
pilot.app.query_one("#from_buttons", RadioSet)._nodes[0].toggle()
|
||||||
pilot.app.query_one("#from_strings", RadioSet)._buttons[2].toggle()
|
pilot.app.query_one("#from_strings", RadioSet)._nodes[2].toggle()
|
||||||
await pilot.pause()
|
await pilot.pause()
|
||||||
assert pilot.app.query_one("#from_buttons", RadioSet).pressed_index == 0
|
assert pilot.app.query_one("#from_buttons", RadioSet).pressed_index == 0
|
||||||
assert pilot.app.query_one("#from_buttons", RadioSet).pressed_button is not None
|
assert pilot.app.query_one("#from_buttons", RadioSet).pressed_button is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user