Drop explicit sender attribute from messages (#1940)

* remove sender

* removed priority post

* timer fix

* test fixes

* drop async version of post_message

* extended docs

* fix no app

* Added control properties

* changelog

* changelog

* changelog

* fix for stopping timers

* changelog

* added aliases to radio and checkbox

* Drop sender from Message init

* drop time

* drop cast

* Added aliases
This commit is contained in:
Will McGugan
2023-03-06 10:52:34 +00:00
committed by GitHub
parent cb84d9111c
commit 373fc95fc1
41 changed files with 390 additions and 403 deletions

View File

@@ -5,6 +5,24 @@ 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/).
## [0.14.0] - Unreleased
### Changes
- Breaking change: There is now only `post_message` to post events, which is non-async, `post_message_no_wait` was dropped. https://github.com/Textualize/textual/pull/1940
- Breaking change: The Timer class now has just one method to stop it, `Timer.stop` which is non sync https://github.com/Textualize/textual/pull/1940
- Breaking change: Messages don't require a `sender` in their constructor https://github.com/Textualize/textual/pull/1940
- Many messages have grown a `control` property which returns the control they relate to. https://github.com/Textualize/textual/pull/1940
- Dropped `time` attribute from Messages https://github.com/Textualize/textual/pull/1940
### Added
- Added `data_table` attribute to DataTable events https://github.com/Textualize/textual/pull/1940
- Added `list_view` attribute to `ListView` events https://github.com/Textualize/textual/pull/1940
- Added `radio_set` attribute to `RadioSet` events https://github.com/Textualize/textual/pull/1940
- Added `switch` attribute to `Switch` events https://github.com/Textualize/textual/pull/1940
- Breaking change: Added `toggle_button` attribute to RadioButton and Checkbox events, replaces `input` https://github.com/Textualize/textual/pull/1940
## [0.13.0] - 2023-03-02 ## [0.13.0] - 2023-03-02
### Added ### Added

View File

@@ -10,9 +10,9 @@ class ColorButton(Static):
class Selected(Message): class Selected(Message):
"""Color selected message.""" """Color selected message."""
def __init__(self, sender: MessageTarget, color: Color) -> None: def __init__(self, color: Color) -> None:
self.color = color self.color = color
super().__init__(sender) super().__init__()
def __init__(self, color: Color) -> None: def __init__(self, color: Color) -> None:
self.color = color self.color = color
@@ -24,9 +24,9 @@ class ColorButton(Static):
self.styles.background = Color.parse("#ffffff33") self.styles.background = Color.parse("#ffffff33")
self.styles.border = ("tall", self.color) self.styles.border = ("tall", self.color)
async def on_click(self) -> None: def on_click(self) -> None:
# The post_message method sends an event to be handled in the DOM # The post_message method sends an event to be handled in the DOM
await self.post_message(self.Selected(self, self.color)) self.post_message(self.Selected(self.color))
def render(self) -> str: def render(self) -> str:
return str(self.color) return str(self.color)

View File

@@ -107,16 +107,11 @@ The message class is defined within the widget class itself. This is not strictl
- It reduces the amount of imports. If you import `ColorButton`, you have access to the message class via `ColorButton.Selected`. - It reduces the amount of imports. If you import `ColorButton`, you have access to the message class via `ColorButton.Selected`.
- It creates a namespace for the handler. So rather than `on_selected`, the handler name becomes `on_color_button_selected`. This makes it less likely that your chosen name will clash with another message. - It creates a namespace for the handler. So rather than `on_selected`, the handler name becomes `on_color_button_selected`. This makes it less likely that your chosen name will clash with another message.
### Sending messages
## Sending messages To send a message call the [post_message()][textual.message_pump.MessagePump.post_message] method. This will place a message on the widget's message queue and run any message handlers.
In the previous example we used [post_message()][textual.message_pump.MessagePump.post_message] to send an event to its parent. We could also have used [post_message_no_wait()][textual.message_pump.MessagePump.post_message_no_wait] for non async code. Sending messages in this way allows you to write custom widgets without needing to know in what context they will be used.
There are other ways of sending (posting) messages, which you may need to use less frequently.
- [post_message][textual.message_pump.MessagePump.post_message] To post a message to a particular widget.
- [post_message_no_wait][textual.message_pump.MessagePump.post_message_no_wait] The non-async version of `post_message`.
It is common for widgets to send messages to themselves, and allow them to bubble. This is so a base class has an opportunity to handle the message. We do this in the example above, which means a subclass could add a `on_color_button_selected` if it wanted to handle the message itself.
## Preventing messages ## Preventing messages

View File

@@ -182,7 +182,6 @@ class Animator:
self._timer = Timer( self._timer = Timer(
app, app,
1 / frames_per_second, 1 / frames_per_second,
app,
name="Animator", name="Animator",
callback=self, callback=self,
pause=True, pause=True,
@@ -201,7 +200,7 @@ class Animator:
async def stop(self) -> None: async def stop(self) -> None:
"""Stop the animator task.""" """Stop the animator task."""
try: try:
await self._timer.stop() self._timer.stop()
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
finally: finally:

View File

@@ -8,21 +8,18 @@ if TYPE_CHECKING:
class MessageTarget(Protocol): class MessageTarget(Protocol):
async def post_message(self, message: "Message") -> bool: async def _post_message(self, message: "Message") -> bool:
... ...
async def _post_priority_message(self, message: "Message") -> bool: def post_message(self, message: "Message") -> bool:
...
def post_message_no_wait(self, message: "Message") -> bool:
... ...
class EventTarget(Protocol): class EventTarget(Protocol):
async def post_message(self, message: "Message") -> bool: async def _post_message(self, message: "Message") -> bool:
... ...
def post_message_no_wait(self, message: "Message") -> bool: def post_message(self, message: "Message") -> bool:
... ...

View File

@@ -26,10 +26,7 @@ _re_bracketed_paste_end = re.compile(r"^\x1b\[201~$")
class XTermParser(Parser[events.Event]): class XTermParser(Parser[events.Event]):
_re_sgr_mouse = re.compile(r"\x1b\[<(\d+);(\d+);(\d+)([Mm])") _re_sgr_mouse = re.compile(r"\x1b\[<(\d+);(\d+);(\d+)([Mm])")
def __init__( def __init__(self, more_data: Callable[[], bool], debug: bool = False) -> None:
self, sender: MessageTarget, more_data: Callable[[], bool], debug: bool = False
) -> None:
self.sender = sender
self.more_data = more_data self.more_data = more_data
self.last_x = 0 self.last_x = 0
self.last_y = 0 self.last_y = 0
@@ -47,7 +44,7 @@ class XTermParser(Parser[events.Event]):
self.debug_log(f"FEED {data!r}") self.debug_log(f"FEED {data!r}")
return super().feed(data) return super().feed(data)
def parse_mouse_code(self, code: str, sender: MessageTarget) -> events.Event | None: def parse_mouse_code(self, code: str) -> events.Event | None:
sgr_match = self._re_sgr_mouse.match(code) sgr_match = self._re_sgr_mouse.match(code)
if sgr_match: if sgr_match:
_buttons, _x, _y, state = sgr_match.groups() _buttons, _x, _y, state = sgr_match.groups()
@@ -74,7 +71,6 @@ class XTermParser(Parser[events.Event]):
button = (buttons + 1) & 3 button = (buttons + 1) & 3
event = event_class( event = event_class(
sender,
x, x,
y, y,
delta_x, delta_x,
@@ -103,7 +99,7 @@ class XTermParser(Parser[events.Event]):
key_events = sequence_to_key_events(character) key_events = sequence_to_key_events(character)
for event in key_events: for event in key_events:
if event.key == "escape": if event.key == "escape":
event = events.Key(event.sender, "circumflex_accent", "^") event = events.Key("circumflex_accent", "^")
on_token(event) on_token(event)
while not self.is_eof: while not self.is_eof:
@@ -116,9 +112,7 @@ class XTermParser(Parser[events.Event]):
# the full escape code was. # the full escape code was.
pasted_text = "".join(paste_buffer[:-1]) pasted_text = "".join(paste_buffer[:-1])
# Note the removal of NUL characters: https://github.com/Textualize/textual/issues/1661 # Note the removal of NUL characters: https://github.com/Textualize/textual/issues/1661
on_token( on_token(events.Paste(pasted_text.replace("\x00", "")))
events.Paste(self.sender, text=pasted_text.replace("\x00", ""))
)
paste_buffer.clear() paste_buffer.clear()
character = ESC if use_prior_escape else (yield read1()) character = ESC if use_prior_escape else (yield read1())
@@ -145,12 +139,12 @@ class XTermParser(Parser[events.Event]):
peek_buffer = yield self.peek_buffer() peek_buffer = yield self.peek_buffer()
if not peek_buffer: if not peek_buffer:
# An escape arrived without any following characters # An escape arrived without any following characters
on_token(events.Key(self.sender, "escape", "\x1b")) on_token(events.Key("escape", "\x1b"))
continue continue
if peek_buffer and peek_buffer[0] == ESC: if peek_buffer and peek_buffer[0] == ESC:
# There is an escape in the buffer, so ESC ESC has arrived # There is an escape in the buffer, so ESC ESC has arrived
yield read1() yield read1()
on_token(events.Key(self.sender, "escape", "\x1b")) on_token(events.Key("escape", "\x1b"))
# If there is no further data, it is not part of a sequence, # If there is no further data, it is not part of a sequence,
# So we don't need to go in to the loop # So we don't need to go in to the loop
if len(peek_buffer) == 1 and not more_data(): if len(peek_buffer) == 1 and not more_data():
@@ -208,7 +202,7 @@ class XTermParser(Parser[events.Event]):
mouse_match = _re_mouse_event.match(sequence) mouse_match = _re_mouse_event.match(sequence)
if mouse_match is not None: if mouse_match is not None:
mouse_code = mouse_match.group(0) mouse_code = mouse_match.group(0)
event = self.parse_mouse_code(mouse_code, self.sender) event = self.parse_mouse_code(mouse_code)
if event: if event:
on_token(event) on_token(event)
break break
@@ -221,11 +215,7 @@ class XTermParser(Parser[events.Event]):
mode_report_match["mode_id"] == "2026" mode_report_match["mode_id"] == "2026"
and int(mode_report_match["setting_parameter"]) > 0 and int(mode_report_match["setting_parameter"]) > 0
): ):
on_token( on_token(messages.TerminalSupportsSynchronizedOutput())
messages.TerminalSupportsSynchronizedOutput(
self.sender
)
)
break break
else: else:
if not bracketed_paste: if not bracketed_paste:
@@ -247,9 +237,7 @@ class XTermParser(Parser[events.Event]):
keys = ANSI_SEQUENCES_KEYS.get(sequence) keys = ANSI_SEQUENCES_KEYS.get(sequence)
if keys is not None: if keys is not None:
for key in keys: for key in keys:
yield events.Key( yield events.Key(key.value, sequence if len(sequence) == 1 else None)
self.sender, key.value, sequence if len(sequence) == 1 else None
)
elif len(sequence) == 1: elif len(sequence) == 1:
try: try:
if not sequence.isalnum(): if not sequence.isalnum():
@@ -262,6 +250,6 @@ class XTermParser(Parser[events.Event]):
else: else:
name = sequence name = sequence
name = KEY_NAME_REPLACEMENTS.get(name, name) name = KEY_NAME_REPLACEMENTS.get(name, name)
yield events.Key(self.sender, name, sequence) yield events.Key(name, sequence)
except: except:
yield events.Key(self.sender, sequence, sequence) yield events.Key(sequence, sequence)

View File

@@ -525,7 +525,7 @@ class App(Generic[ReturnType], DOMNode):
""" """
self._exit = True self._exit = True
self._return_value = result self._return_value = result
self.post_message_no_wait(messages.ExitApp(sender=self)) self.post_message(messages.ExitApp())
if message: if message:
self._exit_renderables.append(message) self._exit_renderables.append(message)
@@ -878,7 +878,7 @@ class App(Generic[ReturnType], DOMNode):
except KeyError: except KeyError:
char = key if len(key) == 1 else None char = key if len(key) == 1 else None
print(f"press {key!r} (char={char!r})") print(f"press {key!r} (char={char!r})")
key_event = events.Key(app, key, char) key_event = events.Key(key, char)
driver.send_event(key_event) driver.send_event(key_event)
await wait_for_idle(0) await wait_for_idle(0)
@@ -1272,7 +1272,7 @@ class App(Generic[ReturnType], DOMNode):
The screen that was replaced. The screen that was replaced.
""" """
screen.post_message_no_wait(events.ScreenSuspend(self)) screen.post_message(events.ScreenSuspend())
self.log.system(f"{screen} SUSPENDED") self.log.system(f"{screen} SUSPENDED")
if not self.is_screen_installed(screen) and screen not in self._screen_stack: if not self.is_screen_installed(screen) and screen not in self._screen_stack:
screen.remove() screen.remove()
@@ -1288,7 +1288,7 @@ class App(Generic[ReturnType], DOMNode):
""" """
next_screen, await_mount = self._get_screen(screen) next_screen, await_mount = self._get_screen(screen)
self._screen_stack.append(next_screen) self._screen_stack.append(next_screen)
self.screen.post_message_no_wait(events.ScreenResume(self)) self.screen.post_message(events.ScreenResume())
self.log.system(f"{self.screen} is current (PUSHED)") self.log.system(f"{self.screen} is current (PUSHED)")
return await_mount return await_mount
@@ -1303,7 +1303,7 @@ class App(Generic[ReturnType], DOMNode):
self._replace_screen(self._screen_stack.pop()) self._replace_screen(self._screen_stack.pop())
next_screen, await_mount = self._get_screen(screen) next_screen, await_mount = self._get_screen(screen)
self._screen_stack.append(next_screen) self._screen_stack.append(next_screen)
self.screen.post_message_no_wait(events.ScreenResume(self)) self.screen.post_message(events.ScreenResume())
self.log.system(f"{self.screen} is current (SWITCHED)") self.log.system(f"{self.screen} is current (SWITCHED)")
return await_mount return await_mount
return AwaitMount(self.screen, []) return AwaitMount(self.screen, [])
@@ -1382,7 +1382,7 @@ class App(Generic[ReturnType], DOMNode):
) )
previous_screen = self._replace_screen(screen_stack.pop()) previous_screen = self._replace_screen(screen_stack.pop())
self.screen._screen_resized(self.size) self.screen._screen_resized(self.size)
self.screen.post_message_no_wait(events.ScreenResume(self)) self.screen.post_message(events.ScreenResume())
self.log.system(f"{self.screen} is active") self.log.system(f"{self.screen} is active")
return previous_screen return previous_screen
@@ -1395,7 +1395,7 @@ class App(Generic[ReturnType], DOMNode):
""" """
self.screen.set_focus(widget, scroll_visible) self.screen.set_focus(widget, scroll_visible)
async def _set_mouse_over(self, widget: Widget | None) -> None: def _set_mouse_over(self, widget: Widget | None) -> None:
"""Called when the mouse is over another widget. """Called when the mouse is over another widget.
Args: Args:
@@ -1404,16 +1404,16 @@ class App(Generic[ReturnType], DOMNode):
if widget is None: if widget is None:
if self.mouse_over is not None: if self.mouse_over is not None:
try: try:
await self.mouse_over.post_message(events.Leave(self)) self.mouse_over.post_message(events.Leave())
finally: finally:
self.mouse_over = None self.mouse_over = None
else: else:
if self.mouse_over is not widget: if self.mouse_over is not widget:
try: try:
if self.mouse_over is not None: if self.mouse_over is not None:
await self.mouse_over._forward_event(events.Leave(self)) self.mouse_over._forward_event(events.Leave())
if widget is not None: if widget is not None:
await widget._forward_event(events.Enter(self)) widget._forward_event(events.Enter())
finally: finally:
self.mouse_over = widget self.mouse_over = widget
@@ -1426,12 +1426,10 @@ class App(Generic[ReturnType], DOMNode):
if widget == self.mouse_captured: if widget == self.mouse_captured:
return return
if self.mouse_captured is not None: if self.mouse_captured is not None:
self.mouse_captured.post_message_no_wait( self.mouse_captured.post_message(events.MouseRelease(self.mouse_position))
events.MouseRelease(self, self.mouse_position)
)
self.mouse_captured = widget self.mouse_captured = widget
if widget is not None: if widget is not None:
widget.post_message_no_wait(events.MouseCapture(self, self.mouse_position)) widget.post_message(events.MouseCapture(self.mouse_position))
def panic(self, *renderables: RenderableType) -> None: def panic(self, *renderables: RenderableType) -> None:
"""Exits the app then displays a message. """Exits the app then displays a message.
@@ -1544,8 +1542,8 @@ class App(Generic[ReturnType], DOMNode):
with self.batch_update(): with self.batch_update():
try: try:
try: try:
await self._dispatch_message(events.Compose(sender=self)) await self._dispatch_message(events.Compose())
await self._dispatch_message(events.Mount(sender=self)) await self._dispatch_message(events.Mount())
finally: finally:
self._mounted_event.set() self._mounted_event.set()
@@ -1575,11 +1573,11 @@ class App(Generic[ReturnType], DOMNode):
await self.animator.stop() await self.animator.stop()
finally: finally:
for timer in list(self._timers): for timer in list(self._timers):
await timer.stop() timer.stop()
self._running = True self._running = True
try: try:
load_event = events.Load(sender=self) load_event = events.Load()
await self._dispatch_message(load_event) await self._dispatch_message(load_event)
driver: Driver driver: Driver
@@ -1825,7 +1823,7 @@ class App(Generic[ReturnType], DOMNode):
await self._close_all() await self._close_all()
await self._close_messages() await self._close_messages()
await self._dispatch_message(events.Unmount(sender=self)) await self._dispatch_message(events.Unmount())
self._print_error_renderables() self._print_error_renderables()
if self.devtools is not None and self.devtools.is_connected: if self.devtools is not None and self.devtools.is_connected:
@@ -1953,19 +1951,19 @@ class App(Generic[ReturnType], DOMNode):
if isinstance(event, events.MouseEvent): if isinstance(event, events.MouseEvent):
# Record current mouse position on App # Record current mouse position on App
self.mouse_position = Offset(event.x, event.y) self.mouse_position = Offset(event.x, event.y)
await self.screen._forward_event(event) self.screen._forward_event(event)
elif isinstance(event, events.Key): elif isinstance(event, events.Key):
if not await self.check_bindings(event.key, priority=True): if not await self.check_bindings(event.key, priority=True):
forward_target = self.focused or self.screen forward_target = self.focused or self.screen
await forward_target._forward_event(event) forward_target._forward_event(event)
else: else:
await self.screen._forward_event(event) self.screen._forward_event(event)
elif isinstance(event, events.Paste) and not event.is_forwarded: elif isinstance(event, events.Paste) and not event.is_forwarded:
if self.focused is not None: if self.focused is not None:
await self.focused._forward_event(event) self.focused._forward_event(event)
else: else:
await self.screen._forward_event(event) self.screen._forward_event(event)
else: else:
await super().on_event(event) await super().on_event(event)
@@ -2092,7 +2090,7 @@ class App(Generic[ReturnType], DOMNode):
async def _on_resize(self, event: events.Resize) -> None: async def _on_resize(self, event: events.Resize) -> None:
event.stop() event.stop()
await self.screen.post_message(event) self.screen.post_message(event)
def _detach_from_dom(self, widgets: list[Widget]) -> list[Widget]: def _detach_from_dom(self, widgets: list[Widget]) -> list[Widget]:
"""Detach a list of widgets from the DOM. """Detach a list of widgets from the DOM.

View File

@@ -163,7 +163,7 @@ class DOMNode(MessagePump):
@auto_refresh.setter @auto_refresh.setter
def auto_refresh(self, interval: float | None) -> None: def auto_refresh(self, interval: float | None) -> None:
if self._auto_refresh_timer is not None: if self._auto_refresh_timer is not None:
self._auto_refresh_timer.stop_no_wait() self._auto_refresh_timer.stop()
self._auto_refresh_timer = None self._auto_refresh_timer = None
if interval is not None: if interval is not None:
self._auto_refresh_timer = self.set_interval( self._auto_refresh_timer = self.set_interval(

View File

@@ -34,7 +34,7 @@ class Driver(ABC):
def send_event(self, event: events.Event) -> None: def send_event(self, event: events.Event) -> None:
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
self._target.post_message(event), loop=self._loop self._target._post_message(event), loop=self._loop
) )
def process_event(self, event: events.Event) -> None: def process_event(self, event: events.Event) -> None:

View File

@@ -39,9 +39,9 @@ class HeadlessDriver(Driver):
terminal_size = self._get_terminal_size() terminal_size = self._get_terminal_size()
width, height = terminal_size width, height = terminal_size
textual_size = Size(width, height) textual_size = Size(width, height)
event = events.Resize(self._target, textual_size, textual_size) event = events.Resize(textual_size, textual_size)
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
self._target.post_message(event), self._target._post_message(event),
loop=loop, loop=loop,
) )

View File

@@ -97,9 +97,9 @@ class LinuxDriver(Driver):
terminal_size = self._get_terminal_size() terminal_size = self._get_terminal_size()
width, height = terminal_size width, height = terminal_size
textual_size = Size(width, height) textual_size = Size(width, height)
event = events.Resize(self._target, textual_size, textual_size) event = events.Resize(textual_size, textual_size)
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
self._target.post_message(event), self._target._post_message(event),
loop=loop, loop=loop,
) )
@@ -217,7 +217,7 @@ class LinuxDriver(Driver):
return True return True
return False return False
parser = XTermParser(self._target, more_data, self._debug) parser = XTermParser(more_data, self._debug)
feed = parser.feed feed = parser.feed
utf8_decoder = getincrementaldecoder("utf-8")().decode utf8_decoder = getincrementaldecoder("utf-8")().decode

View File

@@ -224,7 +224,7 @@ class EventMonitor(threading.Thread):
def run(self) -> None: def run(self) -> None:
exit_requested = self.exit_event.is_set exit_requested = self.exit_event.is_set
parser = XTermParser(self.target, lambda: False) parser = XTermParser(lambda: False)
try: try:
read_count = wintypes.DWORD(0) read_count = wintypes.DWORD(0)

View File

@@ -1,11 +1,11 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Awaitable, Callable, Type, TypeVar from typing import TYPE_CHECKING, Type, TypeVar
import rich.repr import rich.repr
from rich.style import Style from rich.style import Style
from ._types import CallbackType, MessageTarget from ._types import CallbackType
from .geometry import Offset, Size from .geometry import Offset, Size
from .keys import _get_key_aliases from .keys import _get_key_aliases
from .message import Message from .message import Message
@@ -28,9 +28,9 @@ class Event(Message):
@rich.repr.auto @rich.repr.auto
class Callback(Event, bubble=False, verbose=True): class Callback(Event, bubble=False, verbose=True):
def __init__(self, sender: MessageTarget, callback: CallbackType) -> None: def __init__(self, callback: CallbackType) -> None:
self.callback = callback self.callback = callback
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "callback", self.callback yield "callback", self.callback
@@ -71,8 +71,8 @@ class Idle(Event, bubble=False):
class Action(Event): class Action(Event):
__slots__ = ["action"] __slots__ = ["action"]
def __init__(self, sender: MessageTarget, action: str) -> None: def __init__(self, action: str) -> None:
super().__init__(sender) super().__init__()
self.action = action self.action = action
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
@@ -82,7 +82,6 @@ class Action(Event):
class Resize(Event, bubble=False): class Resize(Event, bubble=False):
"""Sent when the app or widget has been resized. """Sent when the app or widget has been resized.
Args: Args:
sender: The sender of the event (the Screen).
size: The new size of the Widget. size: The new size of the Widget.
virtual_size: The virtual size (scrollable size) of the Widget. virtual_size: The virtual size (scrollable size) of the Widget.
container_size: The size of the Widget's container widget. Defaults to None. container_size: The size of the Widget's container widget. Defaults to None.
@@ -93,7 +92,6 @@ class Resize(Event, bubble=False):
def __init__( def __init__(
self, self,
sender: MessageTarget,
size: Size, size: Size,
virtual_size: Size, virtual_size: Size,
container_size: Size | None = None, container_size: Size | None = None,
@@ -101,7 +99,7 @@ class Resize(Event, bubble=False):
self.size = size self.size = size
self.virtual_size = virtual_size self.virtual_size = virtual_size
self.container_size = size if container_size is None else container_size self.container_size = size if container_size is None else container_size
super().__init__(sender) super().__init__()
def can_replace(self, message: "Message") -> bool: def can_replace(self, message: "Message") -> bool:
return isinstance(message, Resize) return isinstance(message, Resize)
@@ -149,13 +147,12 @@ class MouseCapture(Event, bubble=False):
Args: Args:
sender: The sender of the event, (in this case the app).
mouse_position: The position of the mouse when captured. mouse_position: The position of the mouse when captured.
""" """
def __init__(self, sender: MessageTarget, mouse_position: Offset) -> None: def __init__(self, mouse_position: Offset) -> None:
super().__init__(sender) super().__init__()
self.mouse_position = mouse_position self.mouse_position = mouse_position
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
@@ -167,12 +164,11 @@ class MouseRelease(Event, bubble=False):
"""Mouse has been released. """Mouse has been released.
Args: Args:
sender: The sender of the event, (in this case the app).
mouse_position: The position of the mouse when released. mouse_position: The position of the mouse when released.
""" """
def __init__(self, sender: MessageTarget, mouse_position: Offset) -> None: def __init__(self, mouse_position: Offset) -> None:
super().__init__(sender) super().__init__()
self.mouse_position = mouse_position self.mouse_position = mouse_position
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
@@ -188,7 +184,6 @@ class Key(InputEvent):
"""Sent when the user hits a key on the keyboard. """Sent when the user hits a key on the keyboard.
Args: Args:
sender: The sender of the event (always the App).
key: The key that was pressed. key: The key that was pressed.
character: A printable character or ``None`` if it is not printable. character: A printable character or ``None`` if it is not printable.
@@ -198,8 +193,8 @@ class Key(InputEvent):
__slots__ = ["key", "character", "aliases"] __slots__ = ["key", "character", "aliases"]
def __init__(self, sender: MessageTarget, key: str, character: str | None) -> None: def __init__(self, key: str, character: str | None) -> None:
super().__init__(sender) super().__init__()
self.key = key self.key = key
self.character = ( self.character = (
(key if len(key) == 1 else None) if character is None else character (key if len(key) == 1 else None) if character is None else character
@@ -245,7 +240,6 @@ class MouseEvent(InputEvent, bubble=True):
"""Sent in response to a mouse event. """Sent in response to a mouse event.
Args: Args:
sender: The sender of the event.
x: The relative x coordinate. x: The relative x coordinate.
y: The relative y coordinate. y: The relative y coordinate.
delta_x: Change in x since the last message. delta_x: Change in x since the last message.
@@ -276,7 +270,6 @@ class MouseEvent(InputEvent, bubble=True):
def __init__( def __init__(
self, self,
sender: MessageTarget,
x: int, x: int,
y: int, y: int,
delta_x: int, delta_x: int,
@@ -289,7 +282,7 @@ class MouseEvent(InputEvent, bubble=True):
screen_y: int | None = None, screen_y: int | None = None,
style: Style | None = None, style: Style | None = None,
) -> None: ) -> None:
super().__init__(sender) super().__init__()
self.x = x self.x = x
self.y = y self.y = y
self.delta_x = delta_x self.delta_x = delta_x
@@ -305,7 +298,6 @@ class MouseEvent(InputEvent, bubble=True):
@classmethod @classmethod
def from_event(cls: Type[MouseEventT], event: MouseEvent) -> MouseEventT: def from_event(cls: Type[MouseEventT], event: MouseEvent) -> MouseEventT:
new_event = cls( new_event = cls(
event.sender,
event.x, event.x,
event.y, event.y,
event.delta_x, event.delta_x,
@@ -387,7 +379,6 @@ class MouseEvent(InputEvent, bubble=True):
def _apply_offset(self, x: int, y: int) -> MouseEvent: def _apply_offset(self, x: int, y: int) -> MouseEvent:
return self.__class__( return self.__class__(
self.sender,
x=self.x + x, x=self.x + x,
y=self.y + y, y=self.y + y,
delta_x=self.delta_x, delta_x=self.delta_x,
@@ -437,13 +428,12 @@ class Timer(Event, bubble=False, verbose=True):
def __init__( def __init__(
self, self,
sender: MessageTarget,
timer: "TimerClass", timer: "TimerClass",
time: float, time: float,
count: int = 0, count: int = 0,
callback: TimerCallback | None = None, callback: TimerCallback | None = None,
) -> None: ) -> None:
super().__init__(sender) super().__init__()
self.timer = timer self.timer = timer
self.time = time self.time = time
self.count = count self.count = count
@@ -486,12 +476,11 @@ class Paste(Event, bubble=True):
and disable it when the app shuts down. and disable it when the app shuts down.
Args: Args:
sender: The sender of the event, (in this case the app).
text: The text that has been pasted. text: The text that has been pasted.
""" """
def __init__(self, sender: MessageTarget, text: str) -> None: def __init__(self, text: str) -> None:
super().__init__(sender) super().__init__()
self.text = text self.text = text
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:

View File

@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, ClassVar
import rich.repr import rich.repr
from . import _clock from . import _clock
from ._context import active_message_pump
from ._types import MessageTarget as MessageTarget from ._types import MessageTarget as MessageTarget
from .case import camel_to_snake from .case import camel_to_snake
@@ -14,19 +15,10 @@ if TYPE_CHECKING:
@rich.repr.auto @rich.repr.auto
class Message: class Message:
"""Base class for a message. """Base class for a message."""
Args:
sender: The sender of the message / event.
Attributes:
sender: The sender of the message.
time: The time when the message was sent.
"""
__slots__ = [ __slots__ = [
"sender", "_sender",
"time",
"_forwarded", "_forwarded",
"_no_default_action", "_no_default_action",
"_stop_propagation", "_stop_propagation",
@@ -34,16 +26,13 @@ class Message:
"_prevent", "_prevent",
] ]
sender: MessageTarget
bubble: ClassVar[bool] = True # Message will bubble to parent bubble: ClassVar[bool] = True # Message will bubble to parent
verbose: ClassVar[bool] = False # Message is verbose verbose: ClassVar[bool] = False # Message is verbose
no_dispatch: ClassVar[bool] = False # Message may not be handled by client code no_dispatch: ClassVar[bool] = False # Message may not be handled by client code
namespace: ClassVar[str] = "" # Namespace to disambiguate messages namespace: ClassVar[str] = "" # Namespace to disambiguate messages
def __init__(self, sender: MessageTarget) -> None: def __init__(self) -> None:
self.sender: MessageTarget = sender self._sender: MessageTarget | None = active_message_pump.get(None)
self.time: float = _clock.get_time_no_wait()
self._forwarded = False self._forwarded = False
self._no_default_action = False self._no_default_action = False
self._stop_propagation = False self._stop_propagation = False
@@ -55,7 +44,7 @@ class Message:
super().__init__() super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield self.sender yield from ()
def __init_subclass__( def __init_subclass__(
cls, cls,
@@ -73,6 +62,12 @@ class Message:
if namespace is not None: if namespace is not None:
cls.namespace = namespace cls.namespace = namespace
@property
def sender(self) -> MessageTarget:
"""The sender of the message."""
assert self._sender is not None
return self._sender
@property @property
def is_forwarded(self) -> bool: def is_forwarded(self) -> bool:
return self._forwarded return self._forwarded
@@ -118,10 +113,10 @@ class Message:
self._stop_propagation = stop self._stop_propagation = stop
return self return self
async def _bubble_to(self, widget: MessagePump) -> None: def _bubble_to(self, widget: MessagePump) -> None:
"""Bubble to a widget (typically the parent). """Bubble to a widget (typically the parent).
Args: Args:
widget: Target of bubble. widget: Target of bubble.
""" """
await widget.post_message(self) widget.post_message(self)

View File

@@ -287,7 +287,6 @@ class MessagePump(metaclass=MessagePumpMeta):
timer = Timer( timer = Timer(
self, self,
delay, delay,
self,
name=name or f"set_timer#{Timer._timer_count}", name=name or f"set_timer#{Timer._timer_count}",
callback=callback, callback=callback,
repeat=0, repeat=0,
@@ -321,7 +320,6 @@ class MessagePump(metaclass=MessagePumpMeta):
timer = Timer( timer = Timer(
self, self,
interval, interval,
self,
name=name or f"set_interval#{Timer._timer_count}", name=name or f"set_interval#{Timer._timer_count}",
callback=callback, callback=callback,
repeat=repeat or None, repeat=repeat or None,
@@ -341,8 +339,8 @@ class MessagePump(metaclass=MessagePumpMeta):
# We send the InvokeLater message to ourselves first, to ensure we've cleared # We send the InvokeLater message to ourselves first, to ensure we've cleared
# out anything already pending in our own queue. # out anything already pending in our own queue.
message = messages.InvokeLater(self, partial(callback, *args, **kwargs)) message = messages.InvokeLater(partial(callback, *args, **kwargs))
self.post_message_no_wait(message) self.post_message(message)
def call_later(self, callback: Callable, *args, **kwargs) -> None: def call_later(self, callback: Callable, *args, **kwargs) -> None:
"""Schedule a callback to run after all messages are processed in this object. """Schedule a callback to run after all messages are processed in this object.
@@ -353,8 +351,8 @@ class MessagePump(metaclass=MessagePumpMeta):
*args: Positional arguments to pass to the callable. *args: Positional arguments to pass to the callable.
**kwargs: Keyword arguments to pass to the callable. **kwargs: Keyword arguments to pass to the callable.
""" """
message = events.Callback(self, callback=partial(callback, *args, **kwargs)) message = events.Callback(callback=partial(callback, *args, **kwargs))
self.post_message_no_wait(message) self.post_message(message)
def call_next(self, callback: Callable, *args, **kwargs) -> None: def call_next(self, callback: Callable, *args, **kwargs) -> None:
"""Schedule a callback to run immediately after processing the current message. """Schedule a callback to run immediately after processing the current message.
@@ -372,7 +370,7 @@ class MessagePump(metaclass=MessagePumpMeta):
def _close_messages_no_wait(self) -> None: def _close_messages_no_wait(self) -> None:
"""Request the message queue to immediately exit.""" """Request the message queue to immediately exit."""
self._message_queue.put_nowait(messages.CloseMessages(sender=self)) self._message_queue.put_nowait(messages.CloseMessages())
async def _on_close_messages(self, message: messages.CloseMessages) -> None: async def _on_close_messages(self, message: messages.CloseMessages) -> None:
await self._close_messages() await self._close_messages()
@@ -384,9 +382,9 @@ class MessagePump(metaclass=MessagePumpMeta):
self._closing = True self._closing = True
stop_timers = list(self._timers) stop_timers = list(self._timers)
for timer in stop_timers: for timer in stop_timers:
await timer.stop() timer.stop()
self._timers.clear() self._timers.clear()
await self._message_queue.put(events.Unmount(sender=self)) await self._message_queue.put(events.Unmount())
Reactive._reset_object(self) Reactive._reset_object(self)
await self._message_queue.put(None) await self._message_queue.put(None)
if wait and self._task is not None and asyncio.current_task() != self._task: if wait and self._task is not None and asyncio.current_task() != self._task:
@@ -421,15 +419,15 @@ class MessagePump(metaclass=MessagePumpMeta):
finally: finally:
self._running = False self._running = False
for timer in list(self._timers): for timer in list(self._timers):
await timer.stop() timer.stop()
async def _pre_process(self) -> None: async def _pre_process(self) -> None:
"""Procedure to run before processing messages.""" """Procedure to run before processing messages."""
# Dispatch compose and mount messages without going through loop # Dispatch compose and mount messages without going through loop
# These events must occur in this order, and at the start. # These events must occur in this order, and at the start.
try: try:
await self._dispatch_message(events.Compose(sender=self)) await self._dispatch_message(events.Compose())
await self._dispatch_message(events.Mount(sender=self)) await self._dispatch_message(events.Mount())
self._post_mount() self._post_mount()
except Exception as error: except Exception as error:
self.app._handle_exception(error) self.app._handle_exception(error)
@@ -489,7 +487,7 @@ class MessagePump(metaclass=MessagePumpMeta):
): ):
self._last_idle = current_time self._last_idle = current_time
if not self._closed: if not self._closed:
event = events.Idle(self) event = events.Idle()
for _cls, method in self._get_dispatch_methods( for _cls, method in self._get_dispatch_methods(
"on_idle", event "on_idle", event
): ):
@@ -581,20 +579,22 @@ class MessagePump(metaclass=MessagePumpMeta):
# Bubble messages up the DOM (if enabled on the message) # Bubble messages up the DOM (if enabled on the message)
if message.bubble and self._parent and not message._stop_propagation: if message.bubble and self._parent and not message._stop_propagation:
if message.sender == self._parent: if message._sender is not None and message._sender == self._parent:
# parent is sender, so we stop propagation after parent # parent is sender, so we stop propagation after parent
message.stop() message.stop()
if self.is_parent_active and not self._parent._closing: if self.is_parent_active and not self._parent._closing:
await message._bubble_to(self._parent) message._bubble_to(self._parent)
def check_idle(self) -> None: def check_idle(self) -> None:
"""Prompt the message pump to call idle if the queue is empty.""" """Prompt the message pump to call idle if the queue is empty."""
if self._message_queue.empty(): if self._message_queue.empty():
self.post_message_no_wait(messages.Prompt(sender=self)) self.post_message(messages.Prompt())
async def post_message(self, message: Message) -> bool: async def _post_message(self, message: Message) -> bool:
"""Post a message or an event to this message pump. """Post a message or an event to this message pump.
This is an internal method for use where a coroutine is required.
Args: Args:
message: A message object. message: A message object.
@@ -602,40 +602,9 @@ class MessagePump(metaclass=MessagePumpMeta):
True if the messages was posted successfully, False if the message was not posted True if the messages was posted successfully, False if the message was not posted
(because the message pump was in the process of closing). (because the message pump was in the process of closing).
""" """
return self.post_message(message)
if self._closing or self._closed: def post_message(self, message: Message) -> bool:
return False
if not self.check_message_enabled(message):
return True
# Add a copy of the prevented message types to the message
# This is so that prevented messages are honoured by the event's handler
message._prevent.update(self._get_prevented_messages())
await self._message_queue.put(message)
return True
# TODO: This may not be needed, or may only be needed by the timer
# Consider removing or making private
async def _post_priority_message(self, message: Message) -> bool:
"""Post a "priority" messages which will be processes prior to regular messages.
Note that you should rarely need this in a regular app. It exists primarily to allow
timer messages to skip the queue, so that they can be more regular.
Args:
message: A message.
Returns:
True if the messages was processed, False if it wasn't.
"""
# TODO: Allow priority messages to jump the queue
if self._closing or self._closed:
return False
if not self.check_message_enabled(message):
return False
await self._message_queue.put(message)
return True
def post_message_no_wait(self, message: Message) -> bool:
"""Posts a message on the queue. """Posts a message on the queue.
Args: Args:
@@ -654,16 +623,6 @@ class MessagePump(metaclass=MessagePumpMeta):
self._message_queue.put_nowait(message) self._message_queue.put_nowait(message)
return True return True
async def _post_message_from_child(self, message: Message) -> bool:
if self._closing or self._closed:
return False
return await self.post_message(message)
def _post_message_from_child_no_wait(self, message: Message) -> bool:
if self._closing or self._closed:
return False
return self.post_message_no_wait(message)
async def on_callback(self, event: events.Callback) -> None: async def on_callback(self, event: events.Callback) -> None:
await invoke(event.callback) await invoke(event.callback)

View File

@@ -9,7 +9,6 @@ from .geometry import Region
from .message import Message from .message import Message
if TYPE_CHECKING: if TYPE_CHECKING:
from .message_pump import MessagePump
from .widget import Widget from .widget import Widget
@@ -25,12 +24,11 @@ class ExitApp(Message, verbose=True):
@rich.repr.auto @rich.repr.auto
class Update(Message, verbose=True): class Update(Message, verbose=True):
def __init__(self, sender: MessagePump, widget: Widget): def __init__(self, widget: Widget):
super().__init__(sender) super().__init__()
self.widget = widget self.widget = widget
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield self.sender
yield self.widget yield self.widget
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
@@ -63,9 +61,9 @@ class UpdateScroll(Message, verbose=True):
class InvokeLater(Message, verbose=True, bubble=False): class InvokeLater(Message, verbose=True, bubble=False):
"""Sent by Textual to invoke a callback.""" """Sent by Textual to invoke a callback."""
def __init__(self, sender: MessagePump, callback: CallbackType) -> None: def __init__(self, callback: CallbackType) -> None:
self.callback = callback self.callback = callback
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "callback", self.callback yield "callback", self.callback
@@ -75,9 +73,9 @@ class InvokeLater(Message, verbose=True, bubble=False):
class ScrollToRegion(Message, bubble=False): class ScrollToRegion(Message, bubble=False):
"""Ask the parent to scroll a given region in to view.""" """Ask the parent to scroll a given region in to view."""
def __init__(self, sender: MessagePump, region: Region) -> None: def __init__(self, region: Region) -> None:
self.region = region self.region = region
super().__init__(sender) super().__init__()
class Prompt(Message, no_dispatch=True): class Prompt(Message, no_dispatch=True):

View File

@@ -210,9 +210,7 @@ class Reactive(Generic[ReactiveType]):
_rich_traceback_omit = True _rich_traceback_omit = True
await awaitable await awaitable
# Watcher may have changed the state, so run compute again # Watcher may have changed the state, so run compute again
obj.post_message_no_wait( obj.post_message(events.Callback(callback=partial(Reactive._compute, obj)))
events.Callback(sender=obj, callback=partial(Reactive._compute, obj))
)
def invoke_watcher( def invoke_watcher(
watch_function: Callable, old_value: object, value: object watch_function: Callable, old_value: object, value: object
@@ -235,10 +233,8 @@ class Reactive(Generic[ReactiveType]):
watch_result = watch_function() watch_result = watch_function()
if isawaitable(watch_result): if isawaitable(watch_result):
# Result is awaitable, so we need to await it within an async context # Result is awaitable, so we need to await it within an async context
obj.post_message_no_wait( obj.post_message(
events.Callback( events.Callback(callback=partial(await_watcher, watch_result))
sender=obj, callback=partial(await_watcher, watch_result)
)
) )
watch_function = getattr(obj, f"watch_{name}", None) watch_function = getattr(obj, f"watch_{name}", None)

View File

@@ -330,20 +330,20 @@ class Screen(Widget):
if widget is None: if widget is None:
# No focus, so blur currently focused widget if it exists # No focus, so blur currently focused widget if it exists
if self.focused is not None: if self.focused is not None:
self.focused.post_message_no_wait(events.Blur(self)) self.focused.post_message(events.Blur())
self.focused = None self.focused = None
self.log.debug("focus was removed") self.log.debug("focus was removed")
elif widget.focusable: elif widget.focusable:
if self.focused != widget: if self.focused != widget:
if self.focused is not None: if self.focused is not None:
# Blur currently focused widget # Blur currently focused widget
self.focused.post_message_no_wait(events.Blur(self)) self.focused.post_message(events.Blur())
# Change focus # Change focus
self.focused = widget self.focused = widget
# Send focus event # Send focus event
if scroll_visible: if scroll_visible:
self.screen.scroll_to_widget(widget) self.screen.scroll_to_widget(widget)
widget.post_message_no_wait(events.Focus(self)) widget.post_message(events.Focus())
self.log.debug(widget, "was focused") self.log.debug(widget, "was focused")
async def _on_idle(self, event: events.Idle) -> None: async def _on_idle(self, event: events.Idle) -> None:
@@ -381,7 +381,7 @@ class Screen(Widget):
self.app._display(self, self._compositor.render()) self.app._display(self, self._compositor.render())
self._dirty_widgets.clear() self._dirty_widgets.clear()
if self._callbacks: if self._callbacks:
self.post_message_no_wait(events.InvokeCallbacks(self)) self.post_message(events.InvokeCallbacks())
self.update_timer.pause() self.update_timer.pause()
@@ -439,9 +439,9 @@ class Screen(Widget):
if widget._size_updated( if widget._size_updated(
region.size, virtual_size, container_size, layout=False region.size, virtual_size, container_size, layout=False
): ):
widget.post_message_no_wait( widget.post_message(
ResizeEvent( ResizeEvent(
self, region.size, virtual_size, container_size region.size, virtual_size, container_size
) )
) )
@@ -451,7 +451,7 @@ class Screen(Widget):
Show = events.Show Show = events.Show
for widget in hidden: for widget in hidden:
widget.post_message_no_wait(Hide(self)) widget.post_message(Hide())
# We want to send a resize event to widgets that were just added or change since last layout # We want to send a resize event to widgets that were just added or change since last layout
send_resize = shown | resized send_resize = shown | resized
@@ -467,12 +467,12 @@ class Screen(Widget):
) in layers: ) in layers:
widget._size_updated(region.size, virtual_size, container_size) widget._size_updated(region.size, virtual_size, container_size)
if widget in send_resize: if widget in send_resize:
widget.post_message_no_wait( widget.post_message(
ResizeEvent(self, region.size, virtual_size, container_size) ResizeEvent(region.size, virtual_size, container_size)
) )
for widget in shown: for widget in shown:
widget.post_message_no_wait(Show(self)) widget.post_message(Show())
except Exception as error: except Exception as error:
self.app._handle_exception(error) self.app._handle_exception(error)
@@ -480,7 +480,7 @@ class Screen(Widget):
display_update = self._compositor.render(full=full) display_update = self._compositor.render(full=full)
self.app._display(self, display_update) self.app._display(self, display_update)
if not self.app._dom_ready: if not self.app._dom_ready:
self.app.post_message_no_wait(events.Ready(self)) self.app.post_message(events.Ready())
self.app._dom_ready = True self.app._dom_ready = True
async def _on_update(self, message: messages.Update) -> None: async def _on_update(self, message: messages.Update) -> None:
@@ -516,7 +516,7 @@ class Screen(Widget):
event.stop() event.stop()
self._screen_resized(event.size) self._screen_resized(event.size)
async def _handle_mouse_move(self, event: events.MouseMove) -> None: def _handle_mouse_move(self, event: events.MouseMove) -> None:
try: try:
if self.app.mouse_captured: if self.app.mouse_captured:
widget = self.app.mouse_captured widget = self.app.mouse_captured
@@ -524,11 +524,10 @@ class Screen(Widget):
else: else:
widget, region = self.get_widget_at(event.x, event.y) widget, region = self.get_widget_at(event.x, event.y)
except errors.NoWidget: except errors.NoWidget:
await self.app._set_mouse_over(None) self.app._set_mouse_over(None)
else: else:
await self.app._set_mouse_over(widget) self.app._set_mouse_over(widget)
mouse_event = events.MouseMove( mouse_event = events.MouseMove(
self,
event.x - region.x, event.x - region.x,
event.y - region.y, event.y - region.y,
event.delta_x, event.delta_x,
@@ -543,18 +542,18 @@ class Screen(Widget):
) )
widget.hover_style = event.style widget.hover_style = event.style
mouse_event._set_forwarded() mouse_event._set_forwarded()
await widget._forward_event(mouse_event) widget._forward_event(mouse_event)
async def _forward_event(self, event: events.Event) -> None: def _forward_event(self, event: events.Event) -> None:
if event.is_forwarded: if event.is_forwarded:
return return
event._set_forwarded() event._set_forwarded()
if isinstance(event, (events.Enter, events.Leave)): if isinstance(event, (events.Enter, events.Leave)):
await self.post_message(event) self.post_message(event)
elif isinstance(event, events.MouseMove): elif isinstance(event, events.MouseMove):
event.style = self.get_style_at(event.screen_x, event.screen_y) event.style = self.get_style_at(event.screen_x, event.screen_y)
await self._handle_mouse_move(event) self._handle_mouse_move(event)
elif isinstance(event, events.MouseEvent): elif isinstance(event, events.MouseEvent):
try: try:
@@ -574,11 +573,9 @@ class Screen(Widget):
event.style = self.get_style_at(event.screen_x, event.screen_y) event.style = self.get_style_at(event.screen_x, event.screen_y)
if widget is self: if widget is self:
event._set_forwarded() event._set_forwarded()
await self.post_message(event) self.post_message(event)
else: else:
await widget._forward_event( widget._forward_event(event._apply_offset(-region.x, -region.y))
event._apply_offset(-region.x, -region.y)
)
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)): elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
try: try:
@@ -588,8 +585,8 @@ class Screen(Widget):
scroll_widget = widget scroll_widget = widget
if scroll_widget is not None: if scroll_widget is not None:
if scroll_widget is self: if scroll_widget is self:
await self.post_message(event) self.post_message(event)
else: else:
await scroll_widget._forward_event(event) scroll_widget._forward_event(event)
else: else:
await self.post_message(event) self.post_message(event)

View File

@@ -10,7 +10,6 @@ from rich.segment import Segment, Segments
from rich.style import Style, StyleType from rich.style import Style, StyleType
from . import events from . import events
from ._types import MessageTarget
from .geometry import Offset from .geometry import Offset
from .message import Message from .message import Message
from .reactive import Reactive from .reactive import Reactive
@@ -47,7 +46,6 @@ class ScrollTo(ScrollMessage, verbose=True):
def __init__( def __init__(
self, self,
sender: MessageTarget,
x: float | None = None, x: float | None = None,
y: float | None = None, y: float | None = None,
animate: bool = True, animate: bool = True,
@@ -55,7 +53,7 @@ class ScrollTo(ScrollMessage, verbose=True):
self.x = x self.x = x
self.y = y self.y = y
self.animate = animate self.animate = animate
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "x", self.x, None yield "x", self.x, None
@@ -301,12 +299,10 @@ class ScrollBar(Widget):
self.mouse_over = False self.mouse_over = False
def action_scroll_down(self) -> None: def action_scroll_down(self) -> None:
self.post_message_no_wait( self.post_message(ScrollDown() if self.vertical else ScrollRight())
ScrollDown(self) if self.vertical else ScrollRight(self)
)
def action_scroll_up(self) -> None: def action_scroll_up(self) -> None:
self.post_message_no_wait(ScrollUp(self) if self.vertical else ScrollLeft(self)) self.post_message(ScrollUp() if self.vertical else ScrollLeft())
def action_grab(self) -> None: def action_grab(self) -> None:
self.capture_mouse() self.capture_mouse()
@@ -359,7 +355,7 @@ class ScrollBar(Widget):
* (virtual_size / self.window_size) * (virtual_size / self.window_size)
) )
) )
await self.post_message(ScrollTo(self, x=x, y=y)) self.post_message(ScrollTo(x=x, y=y))
event.stop() event.stop()
async def _on_click(self, event: events.Click) -> None: async def _on_click(self, event: events.Click) -> None:

View File

@@ -34,7 +34,6 @@ class Timer:
Args: Args:
event_target: The object which will receive the timer events. event_target: The object which will receive the timer events.
interval: The time between timer events, in seconds. interval: The time between timer events, in seconds.
sender: The sender of the event.
name: A name to assign the event (for debugging). Defaults to None. name: A name to assign the event (for debugging). Defaults to None.
callback: A optional callback to invoke when the event is handled. Defaults to None. callback: A optional callback to invoke when the event is handled. Defaults to None.
repeat: The number of times to repeat the timer, or None to repeat forever. Defaults to None. repeat: The number of times to repeat the timer, or None to repeat forever. Defaults to None.
@@ -48,7 +47,6 @@ class Timer:
self, self,
event_target: MessageTarget, event_target: MessageTarget,
interval: float, interval: float,
sender: MessageTarget,
*, *,
name: str | None = None, name: str | None = None,
callback: TimerCallback | None = None, callback: TimerCallback | None = None,
@@ -59,7 +57,6 @@ class Timer:
self._target_repr = repr(event_target) self._target_repr = repr(event_target)
self._target = weakref.ref(event_target) self._target = weakref.ref(event_target)
self._interval = interval self._interval = interval
self.sender = sender
self.name = f"Timer#{self._timer_count}" if name is None else name self.name = f"Timer#{self._timer_count}" if name is None else name
self._timer_count += 1 self._timer_count += 1
self._callback = callback self._callback = callback
@@ -92,14 +89,8 @@ class Timer:
self._task = create_task(self._run_timer(), name=self.name) self._task = create_task(self._run_timer(), name=self.name)
return self._task return self._task
def stop_no_wait(self) -> None: def stop(self) -> None:
"""Stop the timer.""" """Stop the timer."""
if self._task is not None:
self._task.cancel()
self._task = None
async def stop(self) -> None:
"""Stop the timer, and block until it exits."""
if self._task is not None: if self._task is not None:
self._active.set() self._active.set()
self._task.cancel() self._task.cancel()
@@ -170,10 +161,9 @@ class Timer:
app._handle_exception(error) app._handle_exception(error)
else: else:
event = events.Timer( event = events.Timer(
self.sender,
timer=self, timer=self,
time=next_timer, time=next_timer,
count=count, count=count,
callback=self._callback, callback=self._callback,
) )
await self.target._post_priority_message(event) await self.target.post_message(event)

View File

@@ -42,7 +42,7 @@ from ._arrange import DockArrangeResult, arrange
from ._asyncio import create_task from ._asyncio import create_task
from ._cache import FIFOCache from ._cache import FIFOCache
from ._compose import compose from ._compose import compose
from ._context import active_app from ._context import NoActiveAppError, active_app
from ._easing import DEFAULT_SCROLL_EASING from ._easing import DEFAULT_SCROLL_EASING
from ._layout import Layout from ._layout import Layout
from ._segment_tools import align_lines from ._segment_tools import align_lines
@@ -2491,9 +2491,9 @@ class Widget(DOMNode):
return Style() return Style()
return self.screen.get_style_at(*screen_offset) return self.screen.get_style_at(*screen_offset)
async def _forward_event(self, event: events.Event) -> None: def _forward_event(self, event: events.Event) -> None:
event._set_forwarded() event._set_forwarded()
await self.post_message(event) self.post_message(event)
def _refresh_scroll(self) -> None: def _refresh_scroll(self) -> None:
"""Refreshes the scroll position.""" """Refreshes the scroll position."""
@@ -2579,7 +2579,7 @@ class Widget(DOMNode):
""" """
await self.app.action(action, self) await self.app.action(action, self)
async def post_message(self, message: Message) -> bool: def post_message(self, message: Message) -> bool:
"""Post a message to this widget. """Post a message to this widget.
Args: Args:
@@ -2588,11 +2588,13 @@ class Widget(DOMNode):
Returns: Returns:
True if the message was posted, False if this widget was closed / closing. True if the message was posted, False if this widget was closed / closing.
""" """
if not self.check_message_enabled(message):
return True
if not self.is_running: if not self.is_running:
self.log.warning(self, f"IS NOT RUNNING, {message!r} not sent") try:
return await super().post_message(message) self.log.warning(self, f"IS NOT RUNNING, {message!r} not sent")
except NoActiveAppError:
pass
return super().post_message(message)
async def _on_idle(self, event: events.Idle) -> None: async def _on_idle(self, event: events.Idle) -> None:
"""Called when there are no more events on the queue. """Called when there are no more events on the queue.
@@ -2608,13 +2610,13 @@ class Widget(DOMNode):
else: else:
if self._scroll_required: if self._scroll_required:
self._scroll_required = False self._scroll_required = False
screen.post_message_no_wait(messages.UpdateScroll(self)) screen.post_message(messages.UpdateScroll())
if self._repaint_required: if self._repaint_required:
self._repaint_required = False self._repaint_required = False
screen.post_message_no_wait(messages.Update(self, self)) screen.post_message(messages.Update(self))
if self._layout_required: if self._layout_required:
self._layout_required = False self._layout_required = False
screen.post_message_no_wait(messages.Layout(self)) screen.post_message(messages.Layout())
def focus(self, scroll_visible: bool = True) -> None: def focus(self, scroll_visible: bool = True) -> None:
"""Give focus to this widget. """Give focus to this widget.
@@ -2729,12 +2731,12 @@ class Widget(DOMNode):
def _on_focus(self, event: events.Focus) -> None: def _on_focus(self, event: events.Focus) -> None:
self.has_focus = True self.has_focus = True
self.refresh() self.refresh()
self.post_message_no_wait(events.DescendantFocus(self)) self.post_message(events.DescendantFocus())
def _on_blur(self, event: events.Blur) -> None: def _on_blur(self, event: events.Blur) -> None:
self.has_focus = False self.has_focus = False
self.refresh() self.refresh()
self.post_message_no_wait(events.DescendantBlur(self)) self.post_message(events.DescendantBlur())
def _on_descendant_blur(self, event: events.DescendantBlur) -> None: def _on_descendant_blur(self, event: events.DescendantBlur) -> None:
if self._has_focus_within: if self._has_focus_within:

View File

@@ -165,9 +165,14 @@ class Button(Static, can_focus=True):
button: The button that was pressed. button: The button that was pressed.
""" """
def __init__(self, button: Button) -> None:
self.button = button
super().__init__()
@property @property
def button(self) -> Button: def control(self) -> Button:
return cast(Button, self.sender) """Alias for the button."""
return self.button
def __init__( def __init__(
self, self,
@@ -235,7 +240,7 @@ class Button(Static, can_focus=True):
# Manage the "active" effect: # Manage the "active" effect:
self._start_active_affect() self._start_active_affect()
# ...and let other components know that we've just been clicked: # ...and let other components know that we've just been clicked:
self.post_message_no_wait(Button.Pressed(self)) self.post_message(Button.Pressed(self))
def _start_active_affect(self) -> None: def _start_active_affect(self) -> None:
"""Start a small animation to show the button was clicked.""" """Start a small animation to show the button was clicked."""
@@ -247,7 +252,7 @@ class Button(Static, can_focus=True):
async def _on_key(self, event: events.Key) -> None: async def _on_key(self, event: events.Key) -> None:
if event.key == "enter" and not self.disabled: if event.key == "enter" and not self.disabled:
self._start_active_affect() self._start_active_affect()
await self.post_message(Button.Pressed(self)) self.post_message(Button.Pressed(self))
@classmethod @classmethod
def success( def success(

View File

@@ -1,5 +1,7 @@
"""Provides a check box widget.""" """Provides a check box widget."""
from __future__ import annotations
from ._toggle_button import ToggleButton from ._toggle_button import ToggleButton
@@ -14,3 +16,14 @@ class Checkbox(ToggleButton):
# https://github.com/Textualize/textual/issues/1814 # https://github.com/Textualize/textual/issues/1814
namespace = "checkbox" namespace = "checkbox"
@property
def checkbox(self) -> Checkbox:
"""The checkbox that was changed."""
assert isinstance(self._toggle_button, Checkbox)
return self._toggle_button
@property
def control(self) -> Checkbox:
"""An alias for self.checkbox"""
return self.checkbox

View File

@@ -319,25 +319,31 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
def __init__( def __init__(
self, self,
sender: DataTable, data_table: DataTable,
value: CellType, value: CellType,
coordinate: Coordinate, coordinate: Coordinate,
cell_key: CellKey, cell_key: CellKey,
) -> None: ) -> None:
self.data_table = data_table
"""The data table."""
self.value: CellType = value self.value: CellType = value
"""The value in the highlighted cell.""" """The value in the highlighted cell."""
self.coordinate: Coordinate = coordinate self.coordinate: Coordinate = coordinate
"""The coordinate of the highlighted cell.""" """The coordinate of the highlighted cell."""
self.cell_key: CellKey = cell_key self.cell_key: CellKey = cell_key
"""The key for the highlighted cell.""" """The key for the highlighted cell."""
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "sender", self.sender
yield "value", self.value yield "value", self.value
yield "coordinate", self.coordinate yield "coordinate", self.coordinate
yield "cell_key", self.cell_key yield "cell_key", self.cell_key
@property
def control(self) -> DataTable:
"""Alias for the data table."""
return self.data_table
class CellSelected(Message, bubble=True): class CellSelected(Message, bubble=True):
"""Posted by the `DataTable` widget when a cell is selected. """Posted by the `DataTable` widget when a cell is selected.
@@ -348,25 +354,31 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
def __init__( def __init__(
self, self,
sender: DataTable, data_table: DataTable,
value: CellType, value: CellType,
coordinate: Coordinate, coordinate: Coordinate,
cell_key: CellKey, cell_key: CellKey,
) -> None: ) -> None:
self.data_table = data_table
"""The data table."""
self.value: CellType = value self.value: CellType = value
"""The value in the cell that was selected.""" """The value in the cell that was selected."""
self.coordinate: Coordinate = coordinate self.coordinate: Coordinate = coordinate
"""The coordinate of the cell that was selected.""" """The coordinate of the cell that was selected."""
self.cell_key: CellKey = cell_key self.cell_key: CellKey = cell_key
"""The key for the selected cell.""" """The key for the selected cell."""
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "sender", self.sender
yield "value", self.value yield "value", self.value
yield "coordinate", self.coordinate yield "coordinate", self.coordinate
yield "cell_key", self.cell_key yield "cell_key", self.cell_key
@property
def control(self) -> DataTable:
"""Alias for the data table."""
return self.data_table
class RowHighlighted(Message, bubble=True): class RowHighlighted(Message, bubble=True):
"""Posted when a row is highlighted. """Posted when a row is highlighted.
@@ -376,18 +388,26 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
widget in the DOM. widget in the DOM.
""" """
def __init__(self, sender: DataTable, cursor_row: int, row_key: RowKey) -> None: def __init__(
self, data_table: DataTable, cursor_row: int, row_key: RowKey
) -> None:
self.data_table = data_table
"""The data table."""
self.cursor_row: int = cursor_row self.cursor_row: int = cursor_row
"""The y-coordinate of the cursor that highlighted the row.""" """The y-coordinate of the cursor that highlighted the row."""
self.row_key: RowKey = row_key self.row_key: RowKey = row_key
"""The key of the row that was highlighted.""" """The key of the row that was highlighted."""
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "sender", self.sender
yield "cursor_row", self.cursor_row yield "cursor_row", self.cursor_row
yield "row_key", self.row_key yield "row_key", self.row_key
@property
def control(self) -> DataTable:
"""Alias for the data table."""
return self.data_table
class RowSelected(Message, bubble=True): class RowSelected(Message, bubble=True):
"""Posted when a row is selected. """Posted when a row is selected.
@@ -397,18 +417,26 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
widget in the DOM. widget in the DOM.
""" """
def __init__(self, sender: DataTable, cursor_row: int, row_key: RowKey) -> None: def __init__(
self, data_table: DataTable, cursor_row: int, row_key: RowKey
) -> None:
self.data_table = data_table
"""The data table."""
self.cursor_row: int = cursor_row self.cursor_row: int = cursor_row
"""The y-coordinate of the cursor that made the selection.""" """The y-coordinate of the cursor that made the selection."""
self.row_key: RowKey = row_key self.row_key: RowKey = row_key
"""The key of the row that was selected.""" """The key of the row that was selected."""
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "sender", self.sender
yield "cursor_row", self.cursor_row yield "cursor_row", self.cursor_row
yield "row_key", self.row_key yield "row_key", self.row_key
@property
def control(self) -> DataTable:
"""Alias for the data table."""
return self.data_table
class ColumnHighlighted(Message, bubble=True): class ColumnHighlighted(Message, bubble=True):
"""Posted when a column is highlighted. """Posted when a column is highlighted.
@@ -419,19 +447,25 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
""" """
def __init__( def __init__(
self, sender: DataTable, cursor_column: int, column_key: ColumnKey self, data_table: DataTable, cursor_column: int, column_key: ColumnKey
) -> None: ) -> None:
self.data_table = data_table
"""The data table."""
self.cursor_column: int = cursor_column self.cursor_column: int = cursor_column
"""The x-coordinate of the column that was highlighted.""" """The x-coordinate of the column that was highlighted."""
self.column_key = column_key self.column_key = column_key
"""The key of the column that was highlighted.""" """The key of the column that was highlighted."""
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "sender", self.sender
yield "cursor_column", self.cursor_column yield "cursor_column", self.cursor_column
yield "column_key", self.column_key yield "column_key", self.column_key
@property
def control(self) -> DataTable:
"""Alias for the data table."""
return self.data_table
class ColumnSelected(Message, bubble=True): class ColumnSelected(Message, bubble=True):
"""Posted when a column is selected. """Posted when a column is selected.
@@ -442,67 +476,85 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
""" """
def __init__( def __init__(
self, sender: DataTable, cursor_column: int, column_key: ColumnKey self, data_table: DataTable, cursor_column: int, column_key: ColumnKey
) -> None: ) -> None:
self.data_table = data_table
"""The data table."""
self.cursor_column: int = cursor_column self.cursor_column: int = cursor_column
"""The x-coordinate of the column that was selected.""" """The x-coordinate of the column that was selected."""
self.column_key = column_key self.column_key = column_key
"""The key of the column that was selected.""" """The key of the column that was selected."""
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "sender", self.sender
yield "cursor_column", self.cursor_column yield "cursor_column", self.cursor_column
yield "column_key", self.column_key yield "column_key", self.column_key
@property
def control(self) -> DataTable:
"""Alias for the data table."""
return self.data_table
class HeaderSelected(Message, bubble=True): class HeaderSelected(Message, bubble=True):
"""Posted when a column header/label is clicked.""" """Posted when a column header/label is clicked."""
def __init__( def __init__(
self, self,
sender: DataTable, data_table: DataTable,
column_key: ColumnKey, column_key: ColumnKey,
column_index: int, column_index: int,
label: Text, label: Text,
): ):
self.data_table = data_table
"""The data table."""
self.column_key = column_key self.column_key = column_key
"""The key for the column.""" """The key for the column."""
self.column_index = column_index self.column_index = column_index
"""The index for the column.""" """The index for the column."""
self.label = label self.label = label
"""The text of the label.""" """The text of the label."""
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "sender", self.sender
yield "column_key", self.column_key yield "column_key", self.column_key
yield "column_index", self.column_index yield "column_index", self.column_index
yield "label", self.label.plain yield "label", self.label.plain
@property
def control(self) -> DataTable:
"""Alias for the data table."""
return self.data_table
class RowLabelSelected(Message, bubble=True): class RowLabelSelected(Message, bubble=True):
"""Posted when a row label is clicked.""" """Posted when a row label is clicked."""
def __init__( def __init__(
self, self,
sender: DataTable, data_table: DataTable,
row_key: RowKey, row_key: RowKey,
row_index: int, row_index: int,
label: Text, label: Text,
): ):
self.data_table = data_table
"""The data table."""
self.row_key = row_key self.row_key = row_key
"""The key for the column.""" """The key for the column."""
self.row_index = row_index self.row_index = row_index
"""The index for the column.""" """The index for the column."""
self.label = label self.label = label
"""The text of the label.""" """The text of the label."""
super().__init__(sender) super().__init__()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "sender", self.sender
yield "row_key", self.row_key yield "row_key", self.row_key
yield "row_index", self.row_index yield "row_index", self.row_index
yield "label", self.label.plain yield "label", self.label.plain
@property
def control(self) -> DataTable:
"""Alias for the data table."""
return self.data_table
def __init__( def __init__(
self, self,
*, *,
@@ -896,7 +948,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
return return
else: else:
cell_key = self.coordinate_to_cell_key(coordinate) cell_key = self.coordinate_to_cell_key(coordinate)
self.post_message_no_wait( self.post_message(
DataTable.CellHighlighted( DataTable.CellHighlighted(
self, cell_value, coordinate=coordinate, cell_key=cell_key self, cell_value, coordinate=coordinate, cell_key=cell_key
) )
@@ -927,16 +979,14 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
is_valid_row = row_index < len(self._data) is_valid_row = row_index < len(self._data)
if is_valid_row: if is_valid_row:
row_key = self._row_locations.get_key(row_index) row_key = self._row_locations.get_key(row_index)
self.post_message_no_wait( self.post_message(DataTable.RowHighlighted(self, row_index, row_key))
DataTable.RowHighlighted(self, row_index, row_key)
)
def _highlight_column(self, column_index: int) -> None: def _highlight_column(self, column_index: int) -> None:
"""Apply highlighting to the column at the given index, and post event.""" """Apply highlighting to the column at the given index, and post event."""
self.refresh_column(column_index) self.refresh_column(column_index)
if column_index < len(self.columns): if column_index < len(self.columns):
column_key = self._column_locations.get_key(column_index) column_key = self._column_locations.get_key(column_index)
self.post_message_no_wait( self.post_message(
DataTable.ColumnHighlighted(self, column_index, column_key) DataTable.ColumnHighlighted(self, column_index, column_key)
) )
@@ -1837,13 +1887,13 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
message = DataTable.HeaderSelected( message = DataTable.HeaderSelected(
self, column.key, column_index, label=column.label self, column.key, column_index, label=column.label
) )
self.post_message_no_wait(message) self.post_message(message)
elif is_row_label_click: elif is_row_label_click:
row = self.ordered_rows[row_index] row = self.ordered_rows[row_index]
message = DataTable.RowLabelSelected( message = DataTable.RowLabelSelected(
self, row.key, row_index, label=row.label self, row.key, row_index, label=row.label
) )
self.post_message_no_wait(message) self.post_message(message)
elif self.show_cursor and self.cursor_type != "none": elif self.show_cursor and self.cursor_type != "none":
# Only post selection events if there is a visible row/col/cell cursor. # Only post selection events if there is a visible row/col/cell cursor.
self.cursor_coordinate = Coordinate(row_index, column_index) self.cursor_coordinate = Coordinate(row_index, column_index)
@@ -1900,7 +1950,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
cursor_type = self.cursor_type cursor_type = self.cursor_type
cell_key = self.coordinate_to_cell_key(cursor_coordinate) cell_key = self.coordinate_to_cell_key(cursor_coordinate)
if cursor_type == "cell": if cursor_type == "cell":
self.post_message_no_wait( self.post_message(
DataTable.CellSelected( DataTable.CellSelected(
self, self,
self.get_cell_at(cursor_coordinate), self.get_cell_at(cursor_coordinate),
@@ -1911,10 +1961,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
elif cursor_type == "row": elif cursor_type == "row":
row_index, _ = cursor_coordinate row_index, _ = cursor_coordinate
row_key, _ = cell_key row_key, _ = cell_key
self.post_message_no_wait(DataTable.RowSelected(self, row_index, row_key)) self.post_message(DataTable.RowSelected(self, row_index, row_key))
elif cursor_type == "column": elif cursor_type == "column":
_, column_index = cursor_coordinate _, column_index = cursor_coordinate
_, column_key = cell_key _, column_key = cell_key
self.post_message_no_wait( self.post_message(DataTable.ColumnSelected(self, column_index, column_key))
DataTable.ColumnSelected(self, column_index, column_key)
)

View File

@@ -77,9 +77,9 @@ class DirectoryTree(Tree[DirEntry]):
path: The path of the file that was selected. path: The path of the file that was selected.
""" """
def __init__(self, sender: MessageTarget, path: str) -> None: def __init__(self, path: str) -> None:
self.path: str = path self.path: str = path
super().__init__(sender) super().__init__()
def __init__( def __init__(
self, self,
@@ -176,7 +176,7 @@ class DirectoryTree(Tree[DirEntry]):
if not dir_entry.loaded: if not dir_entry.loaded:
self.load_directory(event.node) self.load_directory(event.node)
else: else:
self.post_message_no_wait(self.FileSelected(self, dir_entry.path)) self.post_message(self.FileSelected(dir_entry.path))
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
event.stop() event.stop()
@@ -184,4 +184,4 @@ class DirectoryTree(Tree[DirEntry]):
if dir_entry is None: if dir_entry is None:
return return
if not dir_entry.is_dir: if not dir_entry.is_dir:
self.post_message_no_wait(self.FileSelected(self, dir_entry.path)) self.post_message(self.FileSelected(dir_entry.path))

View File

@@ -146,10 +146,15 @@ class Input(Widget, can_focus=True):
input: The `Input` widget that was changed. input: The `Input` widget that was changed.
""" """
def __init__(self, sender: Input, value: str) -> None: def __init__(self, input: Input, value: str) -> None:
super().__init__(sender) super().__init__()
self.input: Input = input
self.value: str = value self.value: str = value
self.input: Input = sender
@property
def control(self) -> Input:
"""Alias for self.input."""
return self.input
class Submitted(Message, bubble=True): class Submitted(Message, bubble=True):
"""Posted when the enter key is pressed within an `Input`. """Posted when the enter key is pressed within an `Input`.
@@ -162,10 +167,15 @@ class Input(Widget, can_focus=True):
input: The `Input` widget that is being submitted. input: The `Input` widget that is being submitted.
""" """
def __init__(self, sender: Input, value: str) -> None: def __init__(self, input: Input, value: str) -> None:
super().__init__(sender) super().__init__()
self.input: Input = input
self.value: str = value self.value: str = value
self.input: Input = sender
@property
def control(self) -> Input:
"""Alias for self.input."""
return self.input
def __init__( def __init__(
self, self,
@@ -243,7 +253,7 @@ class Input(Widget, can_focus=True):
async def watch_value(self, value: str) -> None: async def watch_value(self, value: str) -> None:
if self.styles.auto_dimensions: if self.styles.auto_dimensions:
self.refresh(layout=True) self.refresh(layout=True)
await self.post_message(self.Changed(self, value)) self.post_message(self.Changed(self, value))
@property @property
def cursor_width(self) -> int: def cursor_width(self) -> int:
@@ -479,4 +489,4 @@ class Input(Widget, can_focus=True):
async def action_submit(self) -> None: async def action_submit(self) -> None:
"""Handle a submit action (normally the user hitting Enter in the input).""" """Handle a submit action (normally the user hitting Enter in the input)."""
await self.post_message(self.Submitted(self, self.value)) self.post_message(self.Submitted(self, self.value))

View File

@@ -1,5 +1,7 @@
"""Provides a list item widget for use with `ListView`.""" """Provides a list item widget for use with `ListView`."""
from __future__ import annotations
from textual import events from textual import events
from textual.message import Message from textual.message import Message
from textual.reactive import reactive from textual.reactive import reactive
@@ -41,10 +43,12 @@ class ListItem(Widget, can_focus=False):
class _ChildClicked(Message): class _ChildClicked(Message):
"""For informing with the parent ListView that we were clicked""" """For informing with the parent ListView that we were clicked"""
sender: "ListItem" def __init__(self, item: ListItem) -> None:
self.item = item
super().__init__()
def on_click(self, event: events.Click) -> None: def on_click(self, event: events.Click) -> None:
self.post_message_no_wait(self._ChildClicked(self)) self.post_message(self._ChildClicked(self))
def watch_highlighted(self, value: bool) -> None: def watch_highlighted(self, value: bool) -> None:
self.set_class(value, "--highlight") self.set_class(value, "--highlight")

View File

@@ -48,8 +48,9 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
item: The highlighted item, if there is one highlighted. item: The highlighted item, if there is one highlighted.
""" """
def __init__(self, sender: ListView, item: ListItem | None) -> None: def __init__(self, list_view: ListView, item: ListItem | None) -> None:
super().__init__(sender) super().__init__()
self.list_view = list_view
self.item: ListItem | None = item self.item: ListItem | None = item
class Selected(Message, bubble=True): class Selected(Message, bubble=True):
@@ -62,8 +63,9 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
item: The selected item. item: The selected item.
""" """
def __init__(self, sender: ListView, item: ListItem) -> None: def __init__(self, list_view: ListView, item: ListItem) -> None:
super().__init__(sender) super().__init__()
self.list_view = list_view
self.item: ListItem = item self.item: ListItem = item
def __init__( def __init__(
@@ -143,7 +145,7 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
new_child = None new_child = None
self._scroll_highlighted_region() self._scroll_highlighted_region()
self.post_message_no_wait(self.Highlighted(self, new_child)) self.post_message(self.Highlighted(self, new_child))
def append(self, item: ListItem) -> AwaitMount: def append(self, item: ListItem) -> AwaitMount:
"""Append a new ListItem to the end of the ListView. """Append a new ListItem to the end of the ListView.
@@ -176,7 +178,7 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
selected_child = self.highlighted_child selected_child = self.highlighted_child
if selected_child is None: if selected_child is None:
return return
self.post_message_no_wait(self.Selected(self, selected_child)) self.post_message(self.Selected(self, selected_child))
def action_cursor_down(self) -> None: def action_cursor_down(self) -> None:
"""Highlight the next item in the list.""" """Highlight the next item in the list."""
@@ -194,8 +196,8 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
def on_list_item__child_clicked(self, event: ListItem._ChildClicked) -> None: def on_list_item__child_clicked(self, event: ListItem._ChildClicked) -> None:
self.focus() self.focus()
self.index = self._nodes.index(event.sender) self.index = self._nodes.index(event.item)
self.post_message_no_wait(self.Selected(self, event.sender)) self.post_message(self.Selected(self, event.item))
def _scroll_highlighted_region(self) -> None: def _scroll_highlighted_region(self) -> None:
"""Used to keep the highlighted index within vision""" """Used to keep the highlighted index within vision"""

View File

@@ -100,7 +100,7 @@ class MarkdownBlock(Static):
async def action_link(self, href: str) -> None: async def action_link(self, href: str) -> None:
"""Called on link click.""" """Called on link click."""
await self.post_message(Markdown.LinkClicked(href, sender=self)) self.post_message(Markdown.LinkClicked(href))
class MarkdownHeader(MarkdownBlock): class MarkdownHeader(MarkdownBlock):
@@ -524,26 +524,24 @@ class Markdown(Widget):
class TableOfContentsUpdated(Message, bubble=True): class TableOfContentsUpdated(Message, bubble=True):
"""The table of contents was updated.""" """The table of contents was updated."""
def __init__( def __init__(self, table_of_contents: TableOfContentsType) -> None:
self, table_of_contents: TableOfContentsType, *, sender: Widget super().__init__()
) -> None:
super().__init__(sender=sender)
self.table_of_contents: TableOfContentsType = table_of_contents self.table_of_contents: TableOfContentsType = table_of_contents
"""Table of contents.""" """Table of contents."""
class TableOfContentsSelected(Message, bubble=True): class TableOfContentsSelected(Message, bubble=True):
"""An item in the TOC was selected.""" """An item in the TOC was selected."""
def __init__(self, block_id: str, *, sender: Widget) -> None: def __init__(self, block_id: str) -> None:
super().__init__(sender=sender) super().__init__()
self.block_id = block_id self.block_id = block_id
"""ID of the block that was selected.""" """ID of the block that was selected."""
class LinkClicked(Message, bubble=True): class LinkClicked(Message, bubble=True):
"""A link in the document was clicked.""" """A link in the document was clicked."""
def __init__(self, href: str, *, sender: Widget) -> None: def __init__(self, href: str) -> None:
super().__init__(sender=sender) super().__init__()
self.href: str = href self.href: str = href
"""The link that was selected.""" """The link that was selected."""
@@ -702,9 +700,7 @@ class Markdown(Widget):
) )
) )
await self.post_message( self.post_message(Markdown.TableOfContentsUpdated(table_of_contents))
Markdown.TableOfContentsUpdated(table_of_contents, sender=self)
)
with self.app.batch_update(): with self.app.batch_update():
await self.query("MarkdownBlock").remove() await self.query("MarkdownBlock").remove()
await self.mount_all(output) await self.mount_all(output)
@@ -760,8 +756,8 @@ class MarkdownTableOfContents(Widget, can_focus_children=True):
async def on_tree_node_selected(self, message: Tree.NodeSelected) -> None: async def on_tree_node_selected(self, message: Tree.NodeSelected) -> None:
node_data = message.node.data node_data = message.node.data
if node_data is not None: if node_data is not None:
await self.post_message( await self._post_message(
Markdown.TableOfContentsSelected(node_data["block_id"], sender=self) Markdown.TableOfContentsSelected(node_data["block_id"])
) )

View File

@@ -1,5 +1,7 @@
"""Provides a radio button widget.""" """Provides a radio button widget."""
from __future__ import annotations
from ._toggle_button import ToggleButton from ._toggle_button import ToggleButton
@@ -21,3 +23,14 @@ class RadioButton(ToggleButton):
# https://github.com/Textualize/textual/issues/1814 # https://github.com/Textualize/textual/issues/1814
namespace = "radio_button" namespace = "radio_button"
@property
def radio_button(self) -> RadioButton:
"""The radio button that was changed."""
assert isinstance(self._toggle_button, RadioButton)
return self._toggle_button
@property
def control(self) -> RadioButton:
"""Alias for self.radio_button"""
return self.radio_button

View File

@@ -37,15 +37,14 @@ class RadioSet(Container):
This message can be handled using an `on_radio_set_changed` method. This message can be handled using an `on_radio_set_changed` method.
""" """
def __init__(self, sender: RadioSet, pressed: RadioButton) -> None: def __init__(self, radio_set: RadioSet, pressed: RadioButton) -> None:
"""Initialise the message. """Initialise the message.
Args: Args:
sender: The radio set sending the message.
pressed: The radio button that was pressed. pressed: The radio button that was pressed.
""" """
super().__init__(sender) super().__init__()
self.input = sender self.radio_set = radio_set
"""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."""
@@ -54,7 +53,7 @@ class RadioSet(Container):
# this point, and so we can't go looking for the index of the # 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 # pressed button via the normal route. So here we go under the
# hood. # hood.
self.index = sender._nodes.index(pressed) 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 __init__( def __init__(
@@ -114,16 +113,14 @@ class RadioSet(Container):
event: The event. event: The event.
""" """
# If the button is changing to be the pressed button... # If the button is changing to be the pressed button...
if event.input.value: if event.radio_button.value:
# ...send off a message to say that the pressed state has # ...send off a message to say that the pressed state has
# changed. # changed.
self.post_message_no_wait( self.post_message(self.Changed(self, event.radio_button))
self.Changed(self, cast(RadioButton, event.input))
)
# ...then look for the button that was previously the pressed # ...then look for the button that was previously the pressed
# one and unpress it. # one and unpress it.
for button in self._buttons.filter(".-on"): for button in self._buttons.filter(".-on"):
if button != event.input: if button != event.radio_button:
button.value = False button.value = False
break break
else: else:
@@ -134,7 +131,7 @@ class RadioSet(Container):
event.stop() event.stop()
if not self._buttons.filter(".-on"): if not self._buttons.filter(".-on"):
with self.prevent(RadioButton.Changed): with self.prevent(RadioButton.Changed):
event.input.value = True event.radio_button.value = True
@property @property
def pressed_button(self) -> RadioButton | None: def pressed_button(self) -> RadioButton | None:

View File

@@ -87,10 +87,15 @@ class Switch(Widget, can_focus=True):
input: The `Switch` widget that was changed. input: The `Switch` widget that was changed.
""" """
def __init__(self, sender: Switch, value: bool) -> None: def __init__(self, switch: Switch, value: bool) -> None:
super().__init__(sender) super().__init__()
self.value: bool = value self.value: bool = value
self.input: Switch = sender self.switch: Switch = switch
@property
def control(self) -> Switch:
"""Alias for self.switch."""
return self.switch
def __init__( def __init__(
self, self,
@@ -124,7 +129,7 @@ class Switch(Widget, can_focus=True):
self.animate("slider_pos", target_slider_pos, duration=0.3) self.animate("slider_pos", target_slider_pos, duration=0.3)
else: else:
self.slider_pos = target_slider_pos self.slider_pos = target_slider_pos
self.post_message_no_wait(self.Changed(self, self.value)) self.post_message(self.Changed(self, self.value))
def watch_slider_pos(self, slider_pos: float) -> None: def watch_slider_pos(self, slider_pos: float) -> None:
self.set_class(slider_pos == 1, "-on") self.set_class(slider_pos == 1, "-on")

View File

@@ -218,15 +218,15 @@ class ToggleButton(Static, can_focus=True):
class Changed(Message, bubble=True): class Changed(Message, bubble=True):
"""Posted when the value of the toggle button changes.""" """Posted when the value of the toggle button changes."""
def __init__(self, sender: ToggleButton, value: bool) -> None: def __init__(self, toggle_button: ToggleButton, value: bool) -> None:
"""Initialise the message. """Initialise the message.
Args: Args:
sender: The toggle button sending the message. toggle_button: The toggle button sending the message.
value: The value of the toggle button. value: The value of the toggle button.
""" """
super().__init__(sender) super().__init__()
self.input = sender self._toggle_button = toggle_button
"""A reference to the toggle button that was changed.""" """A reference to the toggle button that was changed."""
self.value = value self.value = value
"""The value of the toggle button after the change.""" """The value of the toggle button after the change."""
@@ -239,4 +239,4 @@ class ToggleButton(Static, can_focus=True):
`False`. Subsequently a related `Changed` event will be posted. `False`. Subsequently a related `Changed` event will be posted.
""" """
self.set_class(self.value, "-on") self.set_class(self.value, "-on")
self.post_message_no_wait(self.Changed(self, self.value)) self.post_message(self.Changed(self, self.value))

View File

@@ -14,7 +14,6 @@ from .._cache import LRUCache
from .._immutable_sequence_view import ImmutableSequenceView from .._immutable_sequence_view import ImmutableSequenceView
from .._loop import loop_last from .._loop import loop_last
from .._segment_tools import line_pad from .._segment_tools import line_pad
from .._types import MessageTarget
from ..binding import Binding, BindingType from ..binding import Binding, BindingType
from ..geometry import Region, Size, clamp from ..geometry import Region, Size, clamp
from ..message import Message from ..message import Message
@@ -436,11 +435,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
node: The node that was collapsed. node: The node that was collapsed.
""" """
def __init__( def __init__(self, node: TreeNode[EventTreeDataType]) -> None:
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
) -> None:
self.node: TreeNode[EventTreeDataType] = node self.node: TreeNode[EventTreeDataType] = node
super().__init__(sender) super().__init__()
class NodeExpanded(Generic[EventTreeDataType], Message, bubble=True): class NodeExpanded(Generic[EventTreeDataType], Message, bubble=True):
"""Event sent when a node is expanded. """Event sent when a node is expanded.
@@ -452,11 +449,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
node: The node that was expanded. node: The node that was expanded.
""" """
def __init__( def __init__(self, node: TreeNode[EventTreeDataType]) -> None:
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
) -> None:
self.node: TreeNode[EventTreeDataType] = node self.node: TreeNode[EventTreeDataType] = node
super().__init__(sender) super().__init__()
class NodeHighlighted(Generic[EventTreeDataType], Message, bubble=True): class NodeHighlighted(Generic[EventTreeDataType], Message, bubble=True):
"""Event sent when a node is highlighted. """Event sent when a node is highlighted.
@@ -468,11 +463,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
node: The node that was highlighted. node: The node that was highlighted.
""" """
def __init__( def __init__(self, node: TreeNode[EventTreeDataType]) -> None:
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
) -> None:
self.node: TreeNode[EventTreeDataType] = node self.node: TreeNode[EventTreeDataType] = node
super().__init__(sender) super().__init__()
class NodeSelected(Generic[EventTreeDataType], Message, bubble=True): class NodeSelected(Generic[EventTreeDataType], Message, bubble=True):
"""Event sent when a node is selected. """Event sent when a node is selected.
@@ -484,11 +477,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
node: The node that was selected. node: The node that was selected.
""" """
def __init__( def __init__(self, node: TreeNode[EventTreeDataType]) -> None:
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
) -> None:
self.node: TreeNode[EventTreeDataType] = node self.node: TreeNode[EventTreeDataType] = node
super().__init__(sender) super().__init__()
def __init__( def __init__(
self, self,
@@ -779,7 +770,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
node._selected = True node._selected = True
self._cursor_node = node self._cursor_node = node
if previous_node != node: if previous_node != node:
self.post_message_no_wait(self.NodeHighlighted(self, node)) self.post_message(self.NodeHighlighted(node))
def watch_guide_depth(self, guide_depth: int) -> None: def watch_guide_depth(self, guide_depth: int) -> None:
self._invalidate() self._invalidate()
@@ -1027,10 +1018,10 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
return return
if node.is_expanded: if node.is_expanded:
node.collapse() node.collapse()
self.post_message_no_wait(self.NodeCollapsed(self, node)) self.post_message(self.NodeCollapsed(node))
else: else:
node.expand() node.expand()
self.post_message_no_wait(self.NodeExpanded(self, node)) self.post_message(self.NodeExpanded(node))
async def _on_click(self, event: events.Click) -> None: async def _on_click(self, event: events.Click) -> None:
meta = event.style.meta meta = event.style.meta
@@ -1117,4 +1108,4 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
node = line.path[-1] node = line.path[-1]
if self.auto_expand: if self.auto_expand:
self._toggle_node(node) self._toggle_node(node)
self.post_message_no_wait(self.NodeSelected(self, node)) self.post_message(self.NodeSelected(node))

View File

@@ -10,7 +10,6 @@ from textual.app import App
from textual.coordinate import Coordinate from textual.coordinate import Coordinate
from textual.events import Click, MouseMove from textual.events import Click, MouseMove
from textual.message import Message from textual.message import Message
from textual.message_pump import MessagePump
from textual.widgets import DataTable from textual.widgets import DataTable
from textual.widgets.data_table import ( from textual.widgets.data_table import (
CellDoesNotExist, CellDoesNotExist,
@@ -556,9 +555,8 @@ async def test_coordinate_to_cell_key_invalid_coordinate():
table.coordinate_to_cell_key(Coordinate(9999, 9999)) table.coordinate_to_cell_key(Coordinate(9999, 9999))
def make_click_event(sender: MessagePump): def make_click_event():
return Click( return Click(
sender=sender,
x=1, x=1,
y=2, y=2,
delta_x=0, delta_x=0,
@@ -577,7 +575,7 @@ async def test_datatable_on_click_cell_cursor():
app = DataTableApp() app = DataTableApp()
async with app.run_test() as pilot: async with app.run_test() as pilot:
table = app.query_one(DataTable) table = app.query_one(DataTable)
click = make_click_event(app) click = make_click_event()
column_key = table.add_column("ABC") column_key = table.add_column("ABC")
table.add_row("123") table.add_row("123")
row_key = table.add_row("456") row_key = table.add_row("456")
@@ -591,13 +589,11 @@ async def test_datatable_on_click_cell_cursor():
"CellSelected", "CellSelected",
] ]
cell_highlighted_event: DataTable.CellHighlighted = app.messages[1] cell_highlighted_event: DataTable.CellHighlighted = app.messages[1]
assert cell_highlighted_event.sender is table
assert cell_highlighted_event.value == "456" assert cell_highlighted_event.value == "456"
assert cell_highlighted_event.cell_key == CellKey(row_key, column_key) assert cell_highlighted_event.cell_key == CellKey(row_key, column_key)
assert cell_highlighted_event.coordinate == Coordinate(1, 0) assert cell_highlighted_event.coordinate == Coordinate(1, 0)
cell_selected_event: DataTable.CellSelected = app.messages[2] cell_selected_event: DataTable.CellSelected = app.messages[2]
assert cell_selected_event.sender is table
assert cell_selected_event.value == "456" assert cell_selected_event.value == "456"
assert cell_selected_event.cell_key == CellKey(row_key, column_key) assert cell_selected_event.cell_key == CellKey(row_key, column_key)
assert cell_selected_event.coordinate == Coordinate(1, 0) assert cell_selected_event.coordinate == Coordinate(1, 0)
@@ -610,7 +606,7 @@ async def test_on_click_row_cursor():
async with app.run_test(): async with app.run_test():
table = app.query_one(DataTable) table = app.query_one(DataTable)
table.cursor_type = "row" table.cursor_type = "row"
click = make_click_event(app) click = make_click_event()
table.add_column("ABC") table.add_column("ABC")
table.add_row("123") table.add_row("123")
row_key = table.add_row("456") row_key = table.add_row("456")
@@ -619,12 +615,11 @@ async def test_on_click_row_cursor():
assert app.message_names == ["RowHighlighted", "RowHighlighted", "RowSelected"] assert app.message_names == ["RowHighlighted", "RowHighlighted", "RowSelected"]
row_highlighted: DataTable.RowHighlighted = app.messages[1] row_highlighted: DataTable.RowHighlighted = app.messages[1]
assert row_highlighted.sender is table
assert row_highlighted.row_key == row_key assert row_highlighted.row_key == row_key
assert row_highlighted.cursor_row == 1 assert row_highlighted.cursor_row == 1
row_selected: DataTable.RowSelected = app.messages[2] row_selected: DataTable.RowSelected = app.messages[2]
assert row_selected.sender is table
assert row_selected.row_key == row_key assert row_selected.row_key == row_key
assert row_highlighted.cursor_row == 1 assert row_highlighted.cursor_row == 1
@@ -639,7 +634,7 @@ async def test_on_click_column_cursor():
column_key = table.add_column("ABC") column_key = table.add_column("ABC")
table.add_row("123") table.add_row("123")
table.add_row("456") table.add_row("456")
click = make_click_event(app) click = make_click_event()
table.on_click(event=click) table.on_click(event=click)
await wait_for_idle(0) await wait_for_idle(0)
assert app.message_names == [ assert app.message_names == [
@@ -648,12 +643,10 @@ async def test_on_click_column_cursor():
"ColumnSelected", "ColumnSelected",
] ]
column_highlighted: DataTable.ColumnHighlighted = app.messages[1] column_highlighted: DataTable.ColumnHighlighted = app.messages[1]
assert column_highlighted.sender is table
assert column_highlighted.column_key == column_key assert column_highlighted.column_key == column_key
assert column_highlighted.cursor_column == 0 assert column_highlighted.cursor_column == 0
column_selected: DataTable.ColumnSelected = app.messages[2] column_selected: DataTable.ColumnSelected = app.messages[2]
assert column_selected.sender is table
assert column_selected.column_key == column_key assert column_selected.column_key == column_key
assert column_highlighted.cursor_column == 0 assert column_highlighted.cursor_column == 0
@@ -669,7 +662,6 @@ async def test_hover_coordinate():
assert table.hover_coordinate == Coordinate(0, 0) assert table.hover_coordinate == Coordinate(0, 0)
mouse_move = MouseMove( mouse_move = MouseMove(
sender=app,
x=1, x=1,
y=2, y=2,
delta_x=0, delta_x=0,
@@ -694,7 +686,6 @@ async def test_header_selected():
column_key = table.add_column("number") column_key = table.add_column("number")
table.add_row(3) table.add_row(3)
click_event = Click( click_event = Click(
sender=table,
x=3, x=3,
y=0, y=0,
delta_x=0, delta_x=0,
@@ -708,7 +699,6 @@ async def test_header_selected():
table.on_click(click_event) table.on_click(click_event)
await pilot.pause() await pilot.pause()
message: DataTable.HeaderSelected = app.messages[-1] message: DataTable.HeaderSelected = app.messages[-1]
assert message.sender is table
assert message.label == Text("number") assert message.label == Text("number")
assert message.column_index == 0 assert message.column_index == 0
assert message.column_key == column_key assert message.column_key == column_key
@@ -729,7 +719,6 @@ async def test_row_label_selected():
table.add_column("number") table.add_column("number")
row_key = table.add_row(3, label="A") row_key = table.add_row(3, label="A")
click_event = Click( click_event = Click(
sender=table,
x=1, x=1,
y=1, y=1,
delta_x=0, delta_x=0,
@@ -743,7 +732,6 @@ async def test_row_label_selected():
table.on_click(click_event) table.on_click(click_event)
await pilot.pause() await pilot.pause()
message: DataTable.RowLabelSelected = app.messages[-1] message: DataTable.RowLabelSelected = app.messages[-1]
assert message.sender is table
assert message.label == Text("A") assert message.label == Text("A")
assert message.row_index == 0 assert message.row_index == 0
assert message.row_key == row_key assert message.row_key == row_key

View File

@@ -19,7 +19,7 @@ class ValidWidget(Widget):
async def test_dispatch_key_valid_key(): async def test_dispatch_key_valid_key():
widget = ValidWidget() widget = ValidWidget()
result = await widget.dispatch_key(Key(widget, key="x", character="x")) result = await widget.dispatch_key(Key(key="x", character="x"))
assert result is True assert result is True
assert widget.called_by == widget.key_x assert widget.called_by == widget.key_x
@@ -28,7 +28,7 @@ async def test_dispatch_key_valid_key_alias():
"""When you press tab or ctrl+i, it comes through as a tab key event, but handlers for """When you press tab or ctrl+i, it comes through as a tab key event, but handlers for
tab and ctrl+i are both considered valid.""" tab and ctrl+i are both considered valid."""
widget = ValidWidget() widget = ValidWidget()
result = await widget.dispatch_key(Key(widget, key="tab", character="\t")) result = await widget.dispatch_key(Key(key="tab", character="\t"))
assert result is True assert result is True
assert widget.called_by == widget.key_ctrl_i assert widget.called_by == widget.key_ctrl_i
@@ -54,7 +54,7 @@ async def test_dispatch_key_raises_when_conflicting_handler_aliases():
In the terminal, they're the same thing, so we fail fast via exception here.""" In the terminal, they're the same thing, so we fail fast via exception here."""
widget = DuplicateHandlersWidget() widget = DuplicateHandlersWidget()
with pytest.raises(DuplicateKeyHandlers): with pytest.raises(DuplicateKeyHandlers):
await widget.dispatch_key(Key(widget, key="tab", character="\t")) await widget.dispatch_key(Key(key="tab", character="\t"))
assert widget.called_by == widget.key_tab assert widget.called_by == widget.key_tab

View File

@@ -11,7 +11,7 @@ async def test_paste_app():
app = PasteApp() app = PasteApp()
async with app.run_test() as pilot: async with app.run_test() as pilot:
await app.post_message(events.Paste(sender=app, text="Hello")) app.post_message(events.Paste(text="Hello"))
await pilot.pause(0) await pilot.pause(0)
assert len(paste_events) == 1 assert len(paste_events) == 1

View File

@@ -34,7 +34,7 @@ def chunks(data, size):
@pytest.fixture @pytest.fixture
def parser(): def parser():
return XTermParser(sender=mock.sentinel, more_data=lambda: False) return XTermParser(more_data=lambda: False)
@pytest.mark.parametrize("chunk_size", [2, 3, 4, 5, 6]) @pytest.mark.parametrize("chunk_size", [2, 3, 4, 5, 6])
@@ -65,7 +65,6 @@ def test_bracketed_paste(parser):
assert len(events) == 1 assert len(events) == 1
assert isinstance(events[0], Paste) assert isinstance(events[0], Paste)
assert events[0].text == pasted_text assert events[0].text == pasted_text
assert events[0].sender == mock.sentinel
def test_bracketed_paste_content_contains_escape_codes(parser): def test_bracketed_paste_content_contains_escape_codes(parser):
@@ -302,7 +301,6 @@ def test_terminal_mode_reporting_synchronized_output_supported(parser):
events = list(parser.feed(sequence)) events = list(parser.feed(sequence))
assert len(events) == 1 assert len(events) == 1
assert isinstance(events[0], TerminalSupportsSynchronizedOutput) assert isinstance(events[0], TerminalSupportsSynchronizedOutput)
assert events[0].sender == mock.sentinel
def test_terminal_mode_reporting_synchronized_output_not_supported(parser): def test_terminal_mode_reporting_synchronized_output_not_supported(parser):

View File

@@ -15,7 +15,7 @@ class CheckboxApp(App[None]):
yield Checkbox(value=True, id="cb3") yield Checkbox(value=True, id="cb3")
def on_checkbox_changed(self, event: Checkbox.Changed) -> None: def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
self.events_received.append((event.input.id, event.input.value)) self.events_received.append((event.checkbox.id, event.checkbox.value))
async def test_checkbox_initial_state() -> None: async def test_checkbox_initial_state() -> None:

View File

@@ -15,7 +15,7 @@ class RadioButtonApp(App[None]):
yield RadioButton(value=True, id="rb3") yield RadioButton(value=True, id="rb3")
def on_radio_button_changed(self, event: RadioButton.Changed) -> None: def on_radio_button_changed(self, event: RadioButton.Changed) -> None:
self.events_received.append((event.input.id, event.input.value)) self.events_received.append((event.radio_button.id, event.radio_button.value))
async def test_radio_button_initial_state() -> None: async def test_radio_button_initial_state() -> None:

View File

@@ -19,9 +19,9 @@ class RadioSetApp(App[None]):
def on_radio_set_changed(self, event: RadioSet.Changed) -> None: def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
self.events_received.append( self.events_received.append(
( (
event.input.id, event.radio_set.id,
event.index, event.index,
[button.value for button in event.input.query(RadioButton)], [button.value for button in event.radio_set.query(RadioButton)],
) )
) )