Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop explicit sender attribute from messages #1940

Merged
merged 18 commits into from
Mar 6, 2023
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
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
willmcgugan marked this conversation as resolved.
Show resolved Hide resolved


### 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

### Added
Expand Down
8 changes: 4 additions & 4 deletions docs/examples/events/custom01.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ class ColorButton(Static):
class Selected(Message):
"""Color selected message."""

def __init__(self, sender: MessageTarget, color: Color) -> None:
def __init__(self, color: Color) -> None:
self.color = color
super().__init__(sender)
super().__init__()

def __init__(self, color: Color) -> None:
self.color = color
Expand All @@ -24,9 +24,9 @@ def on_mount(self) -> None:
self.styles.background = Color.parse("#ffffff33")
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
await self.post_message(self.Selected(self, self.color))
self.post_message(self.Selected(self.color))

def render(self) -> str:
return str(self.color)
Expand Down
11 changes: 3 additions & 8 deletions docs/guide/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

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

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

Expand Down
3 changes: 1 addition & 2 deletions src/textual/_animator.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ def __init__(self, app: App, frames_per_second: int = 60) -> None:
self._timer = Timer(
app,
1 / frames_per_second,
app,
name="Animator",
callback=self,
pause=True,
Expand All @@ -201,7 +200,7 @@ async def start(self) -> None:
async def stop(self) -> None:
"""Stop the animator task."""
try:
await self._timer.stop()
self._timer.stop()
except asyncio.CancelledError:
pass
finally:
Expand Down
11 changes: 4 additions & 7 deletions src/textual/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,18 @@


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_no_wait(self, message: "Message") -> bool:
def post_message(self, message: "Message") -> bool:
...


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


Expand Down
34 changes: 11 additions & 23 deletions src/textual/_xterm_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@
class XTermParser(Parser[events.Event]):
_re_sgr_mouse = re.compile(r"\x1b\[<(\d+);(\d+);(\d+)([Mm])")

def __init__(
self, sender: MessageTarget, more_data: Callable[[], bool], debug: bool = False
) -> None:
self.sender = sender
def __init__(self, more_data: Callable[[], bool], debug: bool = False) -> None:
self.more_data = more_data
self.last_x = 0
self.last_y = 0
Expand All @@ -47,7 +44,7 @@ def feed(self, data: str) -> Iterable[events.Event]:
self.debug_log(f"FEED {data!r}")
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)
if sgr_match:
_buttons, _x, _y, state = sgr_match.groups()
Expand All @@ -74,7 +71,6 @@ def parse_mouse_code(self, code: str, sender: MessageTarget) -> events.Event | N
button = (buttons + 1) & 3

event = event_class(
sender,
x,
y,
delta_x,
Expand Down Expand Up @@ -103,7 +99,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None:
key_events = sequence_to_key_events(character)
for event in key_events:
if event.key == "escape":
event = events.Key(event.sender, "circumflex_accent", "^")
event = events.Key("circumflex_accent", "^")
on_token(event)

while not self.is_eof:
Expand All @@ -116,9 +112,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None:
# the full escape code was.
pasted_text = "".join(paste_buffer[:-1])
# Note the removal of NUL characters: https://github.com/Textualize/textual/issues/1661
on_token(
events.Paste(self.sender, text=pasted_text.replace("\x00", ""))
)
on_token(events.Paste(pasted_text.replace("\x00", "")))
paste_buffer.clear()

character = ESC if use_prior_escape else (yield read1())
Expand All @@ -145,12 +139,12 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None:
peek_buffer = yield self.peek_buffer()
if not peek_buffer:
# An escape arrived without any following characters
on_token(events.Key(self.sender, "escape", "\x1b"))
on_token(events.Key("escape", "\x1b"))
continue
if peek_buffer and peek_buffer[0] == ESC:
# There is an escape in the buffer, so ESC ESC has arrived
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,
# So we don't need to go in to the loop
if len(peek_buffer) == 1 and not more_data():
Expand Down Expand Up @@ -208,7 +202,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None:
mouse_match = _re_mouse_event.match(sequence)
if mouse_match is not None:
mouse_code = mouse_match.group(0)
event = self.parse_mouse_code(mouse_code, self.sender)
event = self.parse_mouse_code(mouse_code)
if event:
on_token(event)
break
Expand All @@ -221,11 +215,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None:
mode_report_match["mode_id"] == "2026"
and int(mode_report_match["setting_parameter"]) > 0
):
on_token(
messages.TerminalSupportsSynchronizedOutput(
self.sender
)
)
on_token(messages.TerminalSupportsSynchronizedOutput())
break
else:
if not bracketed_paste:
Expand All @@ -247,9 +237,7 @@ def _sequence_to_key_events(
keys = ANSI_SEQUENCES_KEYS.get(sequence)
if keys is not None:
for key in keys:
yield events.Key(
self.sender, key.value, sequence if len(sequence) == 1 else None
)
yield events.Key(key.value, sequence if len(sequence) == 1 else None)
elif len(sequence) == 1:
try:
if not sequence.isalnum():
Expand All @@ -262,6 +250,6 @@ def _sequence_to_key_events(
else:
name = sequence
name = KEY_NAME_REPLACEMENTS.get(name, name)
yield events.Key(self.sender, name, sequence)
yield events.Key(name, sequence)
except:
yield events.Key(self.sender, sequence, sequence)
yield events.Key(sequence, sequence)
48 changes: 23 additions & 25 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ def exit(
"""
self._exit = True
self._return_value = result
self.post_message_no_wait(messages.ExitApp(sender=self))
self.post_message(messages.ExitApp())
if message:
self._exit_renderables.append(message)

Expand Down Expand Up @@ -878,7 +878,7 @@ async def _press_keys(self, keys: Iterable[str]) -> None:
except KeyError:
char = key if len(key) == 1 else None
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)
await wait_for_idle(0)

Expand Down Expand Up @@ -1272,7 +1272,7 @@ def _replace_screen(self, screen: Screen) -> Screen:
The screen that was replaced.

"""
screen.post_message_no_wait(events.ScreenSuspend(self))
screen.post_message(events.ScreenSuspend())
self.log.system(f"{screen} SUSPENDED")
if not self.is_screen_installed(screen) and screen not in self._screen_stack:
screen.remove()
Expand All @@ -1288,7 +1288,7 @@ def push_screen(self, screen: Screen | str) -> AwaitMount:
"""
next_screen, await_mount = self._get_screen(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)")
return await_mount

Expand All @@ -1303,7 +1303,7 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount:
self._replace_screen(self._screen_stack.pop())
next_screen, await_mount = self._get_screen(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)")
return await_mount
return AwaitMount(self.screen, [])
Expand Down Expand Up @@ -1382,7 +1382,7 @@ def pop_screen(self) -> Screen:
)
previous_screen = self._replace_screen(screen_stack.pop())
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")
return previous_screen

Expand All @@ -1395,7 +1395,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None:
"""
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.

Args:
Expand All @@ -1404,16 +1404,16 @@ async def _set_mouse_over(self, widget: Widget | None) -> None:
if widget is None:
if self.mouse_over is not None:
try:
await self.mouse_over.post_message(events.Leave(self))
self.mouse_over.post_message(events.Leave())
finally:
self.mouse_over = None
else:
if self.mouse_over is not widget:
try:
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:
await widget._forward_event(events.Enter(self))
widget._forward_event(events.Enter())
finally:
self.mouse_over = widget

Expand All @@ -1426,12 +1426,10 @@ def capture_mouse(self, widget: Widget | None) -> None:
if widget == self.mouse_captured:
return
if self.mouse_captured is not None:
self.mouse_captured.post_message_no_wait(
events.MouseRelease(self, self.mouse_position)
)
self.mouse_captured.post_message(events.MouseRelease(self.mouse_position))
self.mouse_captured = widget
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:
"""Exits the app then displays a message.
Expand Down Expand Up @@ -1544,8 +1542,8 @@ async def invoke_ready_callback() -> None:
with self.batch_update():
try:
try:
await self._dispatch_message(events.Compose(sender=self))
await self._dispatch_message(events.Mount(sender=self))
await self._dispatch_message(events.Compose())
await self._dispatch_message(events.Mount())
finally:
self._mounted_event.set()

Expand Down Expand Up @@ -1575,11 +1573,11 @@ async def invoke_ready_callback() -> None:
await self.animator.stop()
finally:
for timer in list(self._timers):
await timer.stop()
timer.stop()

self._running = True
try:
load_event = events.Load(sender=self)
load_event = events.Load()
await self._dispatch_message(load_event)

driver: Driver
Expand Down Expand Up @@ -1825,7 +1823,7 @@ async def _shutdown(self) -> None:
await self._close_all()
await self._close_messages()

await self._dispatch_message(events.Unmount(sender=self))
await self._dispatch_message(events.Unmount())

self._print_error_renderables()
if self.devtools is not None and self.devtools.is_connected:
Expand Down Expand Up @@ -1953,19 +1951,19 @@ async def on_event(self, event: events.Event) -> None:
if isinstance(event, events.MouseEvent):
# Record current mouse position on App
self.mouse_position = Offset(event.x, event.y)
await self.screen._forward_event(event)
self.screen._forward_event(event)
elif isinstance(event, events.Key):
if not await self.check_bindings(event.key, priority=True):
forward_target = self.focused or self.screen
await forward_target._forward_event(event)
forward_target._forward_event(event)
else:
await self.screen._forward_event(event)
self.screen._forward_event(event)

elif isinstance(event, events.Paste) and not event.is_forwarded:
if self.focused is not None:
await self.focused._forward_event(event)
self.focused._forward_event(event)
else:
await self.screen._forward_event(event)
self.screen._forward_event(event)
else:
await super().on_event(event)

Expand Down Expand Up @@ -2092,7 +2090,7 @@ async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None:

async def _on_resize(self, event: events.Resize) -> None:
event.stop()
await self.screen.post_message(event)
self.screen.post_message(event)

def _detach_from_dom(self, widgets: list[Widget]) -> list[Widget]:
"""Detach a list of widgets from the DOM.
Expand Down
2 changes: 1 addition & 1 deletion src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def auto_refresh(self) -> float | None:
@auto_refresh.setter
def auto_refresh(self, interval: float | None) -> 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
if interval is not None:
self._auto_refresh_timer = self.set_interval(
Expand Down
Loading