mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
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:
18
CHANGELOG.md
18
CHANGELOG.md
@@ -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
|
||||
- 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
|
||||
|
||||
### Added
|
||||
|
||||
@@ -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
|
||||
@@ -24,9 +24,9 @@ class ColorButton(Static):
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -182,7 +182,6 @@ class Animator:
|
||||
self._timer = Timer(
|
||||
app,
|
||||
1 / frames_per_second,
|
||||
app,
|
||||
name="Animator",
|
||||
callback=self,
|
||||
pause=True,
|
||||
@@ -201,7 +200,7 @@ class Animator:
|
||||
async def stop(self) -> None:
|
||||
"""Stop the animator task."""
|
||||
try:
|
||||
await self._timer.stop()
|
||||
self._timer.stop()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
|
||||
@@ -8,21 +8,18 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
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:
|
||||
...
|
||||
|
||||
|
||||
|
||||
@@ -26,10 +26,7 @@ _re_bracketed_paste_end = re.compile(r"^\x1b\[201~$")
|
||||
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
|
||||
@@ -47,7 +44,7 @@ class XTermParser(Parser[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()
|
||||
@@ -74,7 +71,6 @@ class XTermParser(Parser[events.Event]):
|
||||
button = (buttons + 1) & 3
|
||||
|
||||
event = event_class(
|
||||
sender,
|
||||
x,
|
||||
y,
|
||||
delta_x,
|
||||
@@ -103,7 +99,7 @@ class XTermParser(Parser[events.Event]):
|
||||
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:
|
||||
@@ -116,9 +112,7 @@ class XTermParser(Parser[events.Event]):
|
||||
# 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())
|
||||
@@ -145,12 +139,12 @@ class XTermParser(Parser[events.Event]):
|
||||
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():
|
||||
@@ -208,7 +202,7 @@ class XTermParser(Parser[events.Event]):
|
||||
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
|
||||
@@ -221,11 +215,7 @@ class XTermParser(Parser[events.Event]):
|
||||
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:
|
||||
@@ -247,9 +237,7 @@ class XTermParser(Parser[events.Event]):
|
||||
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():
|
||||
@@ -262,6 +250,6 @@ class XTermParser(Parser[events.Event]):
|
||||
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)
|
||||
|
||||
@@ -525,7 +525,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
"""
|
||||
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)
|
||||
|
||||
@@ -878,7 +878,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
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)
|
||||
|
||||
@@ -1272,7 +1272,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
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()
|
||||
@@ -1288,7 +1288,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -1303,7 +1303,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
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, [])
|
||||
@@ -1382,7 +1382,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
)
|
||||
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
|
||||
|
||||
@@ -1395,7 +1395,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
"""
|
||||
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:
|
||||
@@ -1404,16 +1404,16 @@ class App(Generic[ReturnType], DOMNode):
|
||||
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
|
||||
|
||||
@@ -1426,12 +1426,10 @@ class App(Generic[ReturnType], DOMNode):
|
||||
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.
|
||||
@@ -1544,8 +1542,8 @@ class App(Generic[ReturnType], DOMNode):
|
||||
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()
|
||||
|
||||
@@ -1575,11 +1573,11 @@ class App(Generic[ReturnType], DOMNode):
|
||||
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
|
||||
@@ -1825,7 +1823,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
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:
|
||||
@@ -1953,19 +1951,19 @@ class App(Generic[ReturnType], DOMNode):
|
||||
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)
|
||||
|
||||
@@ -2092,7 +2090,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
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.
|
||||
|
||||
@@ -163,7 +163,7 @@ class DOMNode(MessagePump):
|
||||
@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(
|
||||
|
||||
@@ -34,7 +34,7 @@ class Driver(ABC):
|
||||
|
||||
def send_event(self, event: events.Event) -> None:
|
||||
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:
|
||||
|
||||
@@ -39,9 +39,9 @@ class HeadlessDriver(Driver):
|
||||
terminal_size = self._get_terminal_size()
|
||||
width, height = terminal_size
|
||||
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(
|
||||
self._target.post_message(event),
|
||||
self._target._post_message(event),
|
||||
loop=loop,
|
||||
)
|
||||
|
||||
|
||||
@@ -97,9 +97,9 @@ class LinuxDriver(Driver):
|
||||
terminal_size = self._get_terminal_size()
|
||||
width, height = terminal_size
|
||||
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(
|
||||
self._target.post_message(event),
|
||||
self._target._post_message(event),
|
||||
loop=loop,
|
||||
)
|
||||
|
||||
@@ -217,7 +217,7 @@ class LinuxDriver(Driver):
|
||||
return True
|
||||
return False
|
||||
|
||||
parser = XTermParser(self._target, more_data, self._debug)
|
||||
parser = XTermParser(more_data, self._debug)
|
||||
feed = parser.feed
|
||||
|
||||
utf8_decoder = getincrementaldecoder("utf-8")().decode
|
||||
|
||||
@@ -224,7 +224,7 @@ class EventMonitor(threading.Thread):
|
||||
|
||||
def run(self) -> None:
|
||||
exit_requested = self.exit_event.is_set
|
||||
parser = XTermParser(self.target, lambda: False)
|
||||
parser = XTermParser(lambda: False)
|
||||
|
||||
try:
|
||||
read_count = wintypes.DWORD(0)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Awaitable, Callable, Type, TypeVar
|
||||
from typing import TYPE_CHECKING, Type, TypeVar
|
||||
|
||||
import rich.repr
|
||||
from rich.style import Style
|
||||
|
||||
from ._types import CallbackType, MessageTarget
|
||||
from ._types import CallbackType
|
||||
from .geometry import Offset, Size
|
||||
from .keys import _get_key_aliases
|
||||
from .message import Message
|
||||
@@ -28,9 +28,9 @@ class Event(Message):
|
||||
|
||||
@rich.repr.auto
|
||||
class Callback(Event, bubble=False, verbose=True):
|
||||
def __init__(self, sender: MessageTarget, callback: CallbackType) -> None:
|
||||
def __init__(self, callback: CallbackType) -> None:
|
||||
self.callback = callback
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "callback", self.callback
|
||||
@@ -71,8 +71,8 @@ class Idle(Event, bubble=False):
|
||||
class Action(Event):
|
||||
__slots__ = ["action"]
|
||||
|
||||
def __init__(self, sender: MessageTarget, action: str) -> None:
|
||||
super().__init__(sender)
|
||||
def __init__(self, action: str) -> None:
|
||||
super().__init__()
|
||||
self.action = action
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
@@ -82,7 +82,6 @@ class Action(Event):
|
||||
class Resize(Event, bubble=False):
|
||||
"""Sent when the app or widget has been resized.
|
||||
Args:
|
||||
sender: The sender of the event (the Screen).
|
||||
size: The new 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.
|
||||
@@ -93,7 +92,6 @@ class Resize(Event, bubble=False):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sender: MessageTarget,
|
||||
size: Size,
|
||||
virtual_size: Size,
|
||||
container_size: Size | None = None,
|
||||
@@ -101,7 +99,7 @@ class Resize(Event, bubble=False):
|
||||
self.size = size
|
||||
self.virtual_size = virtual_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:
|
||||
return isinstance(message, Resize)
|
||||
@@ -149,13 +147,12 @@ class MouseCapture(Event, bubble=False):
|
||||
|
||||
|
||||
Args:
|
||||
sender: The sender of the event, (in this case the app).
|
||||
mouse_position: The position of the mouse when captured.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, sender: MessageTarget, mouse_position: Offset) -> None:
|
||||
super().__init__(sender)
|
||||
def __init__(self, mouse_position: Offset) -> None:
|
||||
super().__init__()
|
||||
self.mouse_position = mouse_position
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
@@ -167,12 +164,11 @@ class MouseRelease(Event, bubble=False):
|
||||
"""Mouse has been released.
|
||||
|
||||
Args:
|
||||
sender: The sender of the event, (in this case the app).
|
||||
mouse_position: The position of the mouse when released.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: MessageTarget, mouse_position: Offset) -> None:
|
||||
super().__init__(sender)
|
||||
def __init__(self, mouse_position: Offset) -> None:
|
||||
super().__init__()
|
||||
self.mouse_position = mouse_position
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
@@ -188,7 +184,6 @@ class Key(InputEvent):
|
||||
"""Sent when the user hits a key on the keyboard.
|
||||
|
||||
Args:
|
||||
sender: The sender of the event (always the App).
|
||||
key: The key that was pressed.
|
||||
character: A printable character or ``None`` if it is not printable.
|
||||
|
||||
@@ -198,8 +193,8 @@ class Key(InputEvent):
|
||||
|
||||
__slots__ = ["key", "character", "aliases"]
|
||||
|
||||
def __init__(self, sender: MessageTarget, key: str, character: str | None) -> None:
|
||||
super().__init__(sender)
|
||||
def __init__(self, key: str, character: str | None) -> None:
|
||||
super().__init__()
|
||||
self.key = key
|
||||
self.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.
|
||||
|
||||
Args:
|
||||
sender: The sender of the event.
|
||||
x: The relative x coordinate.
|
||||
y: The relative y coordinate.
|
||||
delta_x: Change in x since the last message.
|
||||
@@ -276,7 +270,6 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sender: MessageTarget,
|
||||
x: int,
|
||||
y: int,
|
||||
delta_x: int,
|
||||
@@ -289,7 +282,7 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
screen_y: int | None = None,
|
||||
style: Style | None = None,
|
||||
) -> None:
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.delta_x = delta_x
|
||||
@@ -305,7 +298,6 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
@classmethod
|
||||
def from_event(cls: Type[MouseEventT], event: MouseEvent) -> MouseEventT:
|
||||
new_event = cls(
|
||||
event.sender,
|
||||
event.x,
|
||||
event.y,
|
||||
event.delta_x,
|
||||
@@ -387,7 +379,6 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
|
||||
def _apply_offset(self, x: int, y: int) -> MouseEvent:
|
||||
return self.__class__(
|
||||
self.sender,
|
||||
x=self.x + x,
|
||||
y=self.y + y,
|
||||
delta_x=self.delta_x,
|
||||
@@ -437,13 +428,12 @@ class Timer(Event, bubble=False, verbose=True):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sender: MessageTarget,
|
||||
timer: "TimerClass",
|
||||
time: float,
|
||||
count: int = 0,
|
||||
callback: TimerCallback | None = None,
|
||||
) -> None:
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
self.timer = timer
|
||||
self.time = time
|
||||
self.count = count
|
||||
@@ -486,12 +476,11 @@ class Paste(Event, bubble=True):
|
||||
and disable it when the app shuts down.
|
||||
|
||||
Args:
|
||||
sender: The sender of the event, (in this case the app).
|
||||
text: The text that has been pasted.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: MessageTarget, text: str) -> None:
|
||||
super().__init__(sender)
|
||||
def __init__(self, text: str) -> None:
|
||||
super().__init__()
|
||||
self.text = text
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, ClassVar
|
||||
import rich.repr
|
||||
|
||||
from . import _clock
|
||||
from ._context import active_message_pump
|
||||
from ._types import MessageTarget as MessageTarget
|
||||
from .case import camel_to_snake
|
||||
|
||||
@@ -14,19 +15,10 @@ if TYPE_CHECKING:
|
||||
|
||||
@rich.repr.auto
|
||||
class 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.
|
||||
"""
|
||||
"""Base class for a message."""
|
||||
|
||||
__slots__ = [
|
||||
"sender",
|
||||
"time",
|
||||
"_sender",
|
||||
"_forwarded",
|
||||
"_no_default_action",
|
||||
"_stop_propagation",
|
||||
@@ -34,16 +26,13 @@ class Message:
|
||||
"_prevent",
|
||||
]
|
||||
|
||||
sender: MessageTarget
|
||||
bubble: ClassVar[bool] = True # Message will bubble to parent
|
||||
verbose: ClassVar[bool] = False # Message is verbose
|
||||
no_dispatch: ClassVar[bool] = False # Message may not be handled by client code
|
||||
namespace: ClassVar[str] = "" # Namespace to disambiguate messages
|
||||
|
||||
def __init__(self, sender: MessageTarget) -> None:
|
||||
self.sender: MessageTarget = sender
|
||||
|
||||
self.time: float = _clock.get_time_no_wait()
|
||||
def __init__(self) -> None:
|
||||
self._sender: MessageTarget | None = active_message_pump.get(None)
|
||||
self._forwarded = False
|
||||
self._no_default_action = False
|
||||
self._stop_propagation = False
|
||||
@@ -55,7 +44,7 @@ class Message:
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield self.sender
|
||||
yield from ()
|
||||
|
||||
def __init_subclass__(
|
||||
cls,
|
||||
@@ -73,6 +62,12 @@ class Message:
|
||||
if namespace is not None:
|
||||
cls.namespace = namespace
|
||||
|
||||
@property
|
||||
def sender(self) -> MessageTarget:
|
||||
"""The sender of the message."""
|
||||
assert self._sender is not None
|
||||
return self._sender
|
||||
|
||||
@property
|
||||
def is_forwarded(self) -> bool:
|
||||
return self._forwarded
|
||||
@@ -118,10 +113,10 @@ class Message:
|
||||
self._stop_propagation = stop
|
||||
return self
|
||||
|
||||
async def _bubble_to(self, widget: MessagePump) -> None:
|
||||
def _bubble_to(self, widget: MessagePump) -> None:
|
||||
"""Bubble to a widget (typically the parent).
|
||||
|
||||
Args:
|
||||
widget: Target of bubble.
|
||||
"""
|
||||
await widget.post_message(self)
|
||||
widget.post_message(self)
|
||||
|
||||
@@ -287,7 +287,6 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
timer = Timer(
|
||||
self,
|
||||
delay,
|
||||
self,
|
||||
name=name or f"set_timer#{Timer._timer_count}",
|
||||
callback=callback,
|
||||
repeat=0,
|
||||
@@ -321,7 +320,6 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
timer = Timer(
|
||||
self,
|
||||
interval,
|
||||
self,
|
||||
name=name or f"set_interval#{Timer._timer_count}",
|
||||
callback=callback,
|
||||
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
|
||||
# out anything already pending in our own queue.
|
||||
|
||||
message = messages.InvokeLater(self, partial(callback, *args, **kwargs))
|
||||
self.post_message_no_wait(message)
|
||||
message = messages.InvokeLater(partial(callback, *args, **kwargs))
|
||||
self.post_message(message)
|
||||
|
||||
def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
"""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.
|
||||
**kwargs: Keyword arguments to pass to the callable.
|
||||
"""
|
||||
message = events.Callback(self, callback=partial(callback, *args, **kwargs))
|
||||
self.post_message_no_wait(message)
|
||||
message = events.Callback(callback=partial(callback, *args, **kwargs))
|
||||
self.post_message(message)
|
||||
|
||||
def call_next(self, callback: Callable, *args, **kwargs) -> None:
|
||||
"""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:
|
||||
"""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:
|
||||
await self._close_messages()
|
||||
@@ -384,9 +382,9 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
self._closing = True
|
||||
stop_timers = list(self._timers)
|
||||
for timer in stop_timers:
|
||||
await timer.stop()
|
||||
timer.stop()
|
||||
self._timers.clear()
|
||||
await self._message_queue.put(events.Unmount(sender=self))
|
||||
await self._message_queue.put(events.Unmount())
|
||||
Reactive._reset_object(self)
|
||||
await self._message_queue.put(None)
|
||||
if wait and self._task is not None and asyncio.current_task() != self._task:
|
||||
@@ -421,15 +419,15 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
finally:
|
||||
self._running = False
|
||||
for timer in list(self._timers):
|
||||
await timer.stop()
|
||||
timer.stop()
|
||||
|
||||
async def _pre_process(self) -> None:
|
||||
"""Procedure to run before processing messages."""
|
||||
# Dispatch compose and mount messages without going through loop
|
||||
# These events must occur in this order, and at the start.
|
||||
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())
|
||||
self._post_mount()
|
||||
except Exception as error:
|
||||
self.app._handle_exception(error)
|
||||
@@ -489,7 +487,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
):
|
||||
self._last_idle = current_time
|
||||
if not self._closed:
|
||||
event = events.Idle(self)
|
||||
event = events.Idle()
|
||||
for _cls, method in self._get_dispatch_methods(
|
||||
"on_idle", event
|
||||
):
|
||||
@@ -581,20 +579,22 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
|
||||
# Bubble messages up the DOM (if enabled on the message)
|
||||
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
|
||||
message.stop()
|
||||
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:
|
||||
"""Prompt the message pump to call idle if the queue is 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.
|
||||
|
||||
This is an internal method for use where a coroutine is required.
|
||||
|
||||
Args:
|
||||
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
|
||||
(because the message pump was in the process of closing).
|
||||
"""
|
||||
return self.post_message(message)
|
||||
|
||||
if self._closing or self._closed:
|
||||
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:
|
||||
def post_message(self, message: Message) -> bool:
|
||||
"""Posts a message on the queue.
|
||||
|
||||
Args:
|
||||
@@ -654,16 +623,6 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
self._message_queue.put_nowait(message)
|
||||
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:
|
||||
await invoke(event.callback)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from .geometry import Region
|
||||
from .message import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .message_pump import MessagePump
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
@@ -25,12 +24,11 @@ class ExitApp(Message, verbose=True):
|
||||
|
||||
@rich.repr.auto
|
||||
class Update(Message, verbose=True):
|
||||
def __init__(self, sender: MessagePump, widget: Widget):
|
||||
super().__init__(sender)
|
||||
def __init__(self, widget: Widget):
|
||||
super().__init__()
|
||||
self.widget = widget
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield self.sender
|
||||
yield self.widget
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
@@ -63,9 +61,9 @@ class UpdateScroll(Message, verbose=True):
|
||||
class InvokeLater(Message, verbose=True, bubble=False):
|
||||
"""Sent by Textual to invoke a callback."""
|
||||
|
||||
def __init__(self, sender: MessagePump, callback: CallbackType) -> None:
|
||||
def __init__(self, callback: CallbackType) -> None:
|
||||
self.callback = callback
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "callback", self.callback
|
||||
@@ -75,9 +73,9 @@ class InvokeLater(Message, verbose=True, bubble=False):
|
||||
class ScrollToRegion(Message, bubble=False):
|
||||
"""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
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
|
||||
class Prompt(Message, no_dispatch=True):
|
||||
|
||||
@@ -210,9 +210,7 @@ class Reactive(Generic[ReactiveType]):
|
||||
_rich_traceback_omit = True
|
||||
await awaitable
|
||||
# Watcher may have changed the state, so run compute again
|
||||
obj.post_message_no_wait(
|
||||
events.Callback(sender=obj, callback=partial(Reactive._compute, obj))
|
||||
)
|
||||
obj.post_message(events.Callback(callback=partial(Reactive._compute, obj)))
|
||||
|
||||
def invoke_watcher(
|
||||
watch_function: Callable, old_value: object, value: object
|
||||
@@ -235,10 +233,8 @@ class Reactive(Generic[ReactiveType]):
|
||||
watch_result = watch_function()
|
||||
if isawaitable(watch_result):
|
||||
# Result is awaitable, so we need to await it within an async context
|
||||
obj.post_message_no_wait(
|
||||
events.Callback(
|
||||
sender=obj, callback=partial(await_watcher, watch_result)
|
||||
)
|
||||
obj.post_message(
|
||||
events.Callback(callback=partial(await_watcher, watch_result))
|
||||
)
|
||||
|
||||
watch_function = getattr(obj, f"watch_{name}", None)
|
||||
|
||||
@@ -330,20 +330,20 @@ class Screen(Widget):
|
||||
if widget is None:
|
||||
# No focus, so blur currently focused widget if it exists
|
||||
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.log.debug("focus was removed")
|
||||
elif widget.focusable:
|
||||
if self.focused != widget:
|
||||
if self.focused is not None:
|
||||
# Blur currently focused widget
|
||||
self.focused.post_message_no_wait(events.Blur(self))
|
||||
self.focused.post_message(events.Blur())
|
||||
# Change focus
|
||||
self.focused = widget
|
||||
# Send focus event
|
||||
if scroll_visible:
|
||||
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")
|
||||
|
||||
async def _on_idle(self, event: events.Idle) -> None:
|
||||
@@ -381,7 +381,7 @@ class Screen(Widget):
|
||||
self.app._display(self, self._compositor.render())
|
||||
self._dirty_widgets.clear()
|
||||
if self._callbacks:
|
||||
self.post_message_no_wait(events.InvokeCallbacks(self))
|
||||
self.post_message(events.InvokeCallbacks())
|
||||
|
||||
self.update_timer.pause()
|
||||
|
||||
@@ -439,9 +439,9 @@ class Screen(Widget):
|
||||
if widget._size_updated(
|
||||
region.size, virtual_size, container_size, layout=False
|
||||
):
|
||||
widget.post_message_no_wait(
|
||||
widget.post_message(
|
||||
ResizeEvent(
|
||||
self, region.size, virtual_size, container_size
|
||||
region.size, virtual_size, container_size
|
||||
)
|
||||
)
|
||||
|
||||
@@ -451,7 +451,7 @@ class Screen(Widget):
|
||||
Show = events.Show
|
||||
|
||||
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
|
||||
send_resize = shown | resized
|
||||
@@ -467,12 +467,12 @@ class Screen(Widget):
|
||||
) in layers:
|
||||
widget._size_updated(region.size, virtual_size, container_size)
|
||||
if widget in send_resize:
|
||||
widget.post_message_no_wait(
|
||||
ResizeEvent(self, region.size, virtual_size, container_size)
|
||||
widget.post_message(
|
||||
ResizeEvent(region.size, virtual_size, container_size)
|
||||
)
|
||||
|
||||
for widget in shown:
|
||||
widget.post_message_no_wait(Show(self))
|
||||
widget.post_message(Show())
|
||||
|
||||
except Exception as error:
|
||||
self.app._handle_exception(error)
|
||||
@@ -480,7 +480,7 @@ class Screen(Widget):
|
||||
display_update = self._compositor.render(full=full)
|
||||
self.app._display(self, display_update)
|
||||
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
|
||||
|
||||
async def _on_update(self, message: messages.Update) -> None:
|
||||
@@ -516,7 +516,7 @@ class Screen(Widget):
|
||||
event.stop()
|
||||
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:
|
||||
if self.app.mouse_captured:
|
||||
widget = self.app.mouse_captured
|
||||
@@ -524,11 +524,10 @@ class Screen(Widget):
|
||||
else:
|
||||
widget, region = self.get_widget_at(event.x, event.y)
|
||||
except errors.NoWidget:
|
||||
await self.app._set_mouse_over(None)
|
||||
self.app._set_mouse_over(None)
|
||||
else:
|
||||
await self.app._set_mouse_over(widget)
|
||||
self.app._set_mouse_over(widget)
|
||||
mouse_event = events.MouseMove(
|
||||
self,
|
||||
event.x - region.x,
|
||||
event.y - region.y,
|
||||
event.delta_x,
|
||||
@@ -543,18 +542,18 @@ class Screen(Widget):
|
||||
)
|
||||
widget.hover_style = event.style
|
||||
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:
|
||||
return
|
||||
event._set_forwarded()
|
||||
if isinstance(event, (events.Enter, events.Leave)):
|
||||
await self.post_message(event)
|
||||
self.post_message(event)
|
||||
|
||||
elif isinstance(event, events.MouseMove):
|
||||
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):
|
||||
try:
|
||||
@@ -574,11 +573,9 @@ class Screen(Widget):
|
||||
event.style = self.get_style_at(event.screen_x, event.screen_y)
|
||||
if widget is self:
|
||||
event._set_forwarded()
|
||||
await self.post_message(event)
|
||||
self.post_message(event)
|
||||
else:
|
||||
await widget._forward_event(
|
||||
event._apply_offset(-region.x, -region.y)
|
||||
)
|
||||
widget._forward_event(event._apply_offset(-region.x, -region.y))
|
||||
|
||||
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
|
||||
try:
|
||||
@@ -588,8 +585,8 @@ class Screen(Widget):
|
||||
scroll_widget = widget
|
||||
if scroll_widget is not None:
|
||||
if scroll_widget is self:
|
||||
await self.post_message(event)
|
||||
self.post_message(event)
|
||||
else:
|
||||
await scroll_widget._forward_event(event)
|
||||
scroll_widget._forward_event(event)
|
||||
else:
|
||||
await self.post_message(event)
|
||||
self.post_message(event)
|
||||
|
||||
@@ -10,7 +10,6 @@ from rich.segment import Segment, Segments
|
||||
from rich.style import Style, StyleType
|
||||
|
||||
from . import events
|
||||
from ._types import MessageTarget
|
||||
from .geometry import Offset
|
||||
from .message import Message
|
||||
from .reactive import Reactive
|
||||
@@ -47,7 +46,6 @@ class ScrollTo(ScrollMessage, verbose=True):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sender: MessageTarget,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
animate: bool = True,
|
||||
@@ -55,7 +53,7 @@ class ScrollTo(ScrollMessage, verbose=True):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.animate = animate
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "x", self.x, None
|
||||
@@ -301,12 +299,10 @@ class ScrollBar(Widget):
|
||||
self.mouse_over = False
|
||||
|
||||
def action_scroll_down(self) -> None:
|
||||
self.post_message_no_wait(
|
||||
ScrollDown(self) if self.vertical else ScrollRight(self)
|
||||
)
|
||||
self.post_message(ScrollDown() if self.vertical else ScrollRight())
|
||||
|
||||
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:
|
||||
self.capture_mouse()
|
||||
@@ -359,7 +355,7 @@ class ScrollBar(Widget):
|
||||
* (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()
|
||||
|
||||
async def _on_click(self, event: events.Click) -> None:
|
||||
|
||||
@@ -34,7 +34,6 @@ class Timer:
|
||||
Args:
|
||||
event_target: The object which will receive the timer events.
|
||||
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.
|
||||
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.
|
||||
@@ -48,7 +47,6 @@ class Timer:
|
||||
self,
|
||||
event_target: MessageTarget,
|
||||
interval: float,
|
||||
sender: MessageTarget,
|
||||
*,
|
||||
name: str | None = None,
|
||||
callback: TimerCallback | None = None,
|
||||
@@ -59,7 +57,6 @@ class Timer:
|
||||
self._target_repr = repr(event_target)
|
||||
self._target = weakref.ref(event_target)
|
||||
self._interval = interval
|
||||
self.sender = sender
|
||||
self.name = f"Timer#{self._timer_count}" if name is None else name
|
||||
self._timer_count += 1
|
||||
self._callback = callback
|
||||
@@ -92,14 +89,8 @@ class Timer:
|
||||
self._task = create_task(self._run_timer(), name=self.name)
|
||||
return self._task
|
||||
|
||||
def stop_no_wait(self) -> None:
|
||||
def stop(self) -> None:
|
||||
"""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:
|
||||
self._active.set()
|
||||
self._task.cancel()
|
||||
@@ -170,10 +161,9 @@ class Timer:
|
||||
app._handle_exception(error)
|
||||
else:
|
||||
event = events.Timer(
|
||||
self.sender,
|
||||
timer=self,
|
||||
time=next_timer,
|
||||
count=count,
|
||||
callback=self._callback,
|
||||
)
|
||||
await self.target._post_priority_message(event)
|
||||
await self.target.post_message(event)
|
||||
|
||||
@@ -42,7 +42,7 @@ from ._arrange import DockArrangeResult, arrange
|
||||
from ._asyncio import create_task
|
||||
from ._cache import FIFOCache
|
||||
from ._compose import compose
|
||||
from ._context import active_app
|
||||
from ._context import NoActiveAppError, active_app
|
||||
from ._easing import DEFAULT_SCROLL_EASING
|
||||
from ._layout import Layout
|
||||
from ._segment_tools import align_lines
|
||||
@@ -2491,9 +2491,9 @@ class Widget(DOMNode):
|
||||
return Style()
|
||||
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()
|
||||
await self.post_message(event)
|
||||
self.post_message(event)
|
||||
|
||||
def _refresh_scroll(self) -> None:
|
||||
"""Refreshes the scroll position."""
|
||||
@@ -2579,7 +2579,7 @@ class Widget(DOMNode):
|
||||
"""
|
||||
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.
|
||||
|
||||
Args:
|
||||
@@ -2588,11 +2588,13 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
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:
|
||||
self.log.warning(self, f"IS NOT RUNNING, {message!r} not sent")
|
||||
return await super().post_message(message)
|
||||
try:
|
||||
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:
|
||||
"""Called when there are no more events on the queue.
|
||||
@@ -2608,13 +2610,13 @@ class Widget(DOMNode):
|
||||
else:
|
||||
if self._scroll_required:
|
||||
self._scroll_required = False
|
||||
screen.post_message_no_wait(messages.UpdateScroll(self))
|
||||
screen.post_message(messages.UpdateScroll())
|
||||
if self._repaint_required:
|
||||
self._repaint_required = False
|
||||
screen.post_message_no_wait(messages.Update(self, self))
|
||||
screen.post_message(messages.Update(self))
|
||||
if self._layout_required:
|
||||
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:
|
||||
"""Give focus to this widget.
|
||||
@@ -2729,12 +2731,12 @@ class Widget(DOMNode):
|
||||
def _on_focus(self, event: events.Focus) -> None:
|
||||
self.has_focus = True
|
||||
self.refresh()
|
||||
self.post_message_no_wait(events.DescendantFocus(self))
|
||||
self.post_message(events.DescendantFocus())
|
||||
|
||||
def _on_blur(self, event: events.Blur) -> None:
|
||||
self.has_focus = False
|
||||
self.refresh()
|
||||
self.post_message_no_wait(events.DescendantBlur(self))
|
||||
self.post_message(events.DescendantBlur())
|
||||
|
||||
def _on_descendant_blur(self, event: events.DescendantBlur) -> None:
|
||||
if self._has_focus_within:
|
||||
|
||||
@@ -165,9 +165,14 @@ class Button(Static, can_focus=True):
|
||||
button: The button that was pressed.
|
||||
"""
|
||||
|
||||
def __init__(self, button: Button) -> None:
|
||||
self.button = button
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def button(self) -> Button:
|
||||
return cast(Button, self.sender)
|
||||
def control(self) -> Button:
|
||||
"""Alias for the button."""
|
||||
return self.button
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -235,7 +240,7 @@ class Button(Static, can_focus=True):
|
||||
# Manage the "active" effect:
|
||||
self._start_active_affect()
|
||||
# ...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:
|
||||
"""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:
|
||||
if event.key == "enter" and not self.disabled:
|
||||
self._start_active_affect()
|
||||
await self.post_message(Button.Pressed(self))
|
||||
self.post_message(Button.Pressed(self))
|
||||
|
||||
@classmethod
|
||||
def success(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Provides a check box widget."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ._toggle_button import ToggleButton
|
||||
|
||||
|
||||
@@ -14,3 +16,14 @@ class Checkbox(ToggleButton):
|
||||
|
||||
# https://github.com/Textualize/textual/issues/1814
|
||||
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
|
||||
|
||||
@@ -319,25 +319,31 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sender: DataTable,
|
||||
data_table: DataTable,
|
||||
value: CellType,
|
||||
coordinate: Coordinate,
|
||||
cell_key: CellKey,
|
||||
) -> None:
|
||||
self.data_table = data_table
|
||||
"""The data table."""
|
||||
self.value: CellType = value
|
||||
"""The value in the highlighted cell."""
|
||||
self.coordinate: Coordinate = coordinate
|
||||
"""The coordinate of the highlighted cell."""
|
||||
self.cell_key: CellKey = cell_key
|
||||
"""The key for the highlighted cell."""
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "value", self.value
|
||||
yield "coordinate", self.coordinate
|
||||
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):
|
||||
"""Posted by the `DataTable` widget when a cell is selected.
|
||||
|
||||
@@ -348,25 +354,31 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sender: DataTable,
|
||||
data_table: DataTable,
|
||||
value: CellType,
|
||||
coordinate: Coordinate,
|
||||
cell_key: CellKey,
|
||||
) -> None:
|
||||
self.data_table = data_table
|
||||
"""The data table."""
|
||||
self.value: CellType = value
|
||||
"""The value in the cell that was selected."""
|
||||
self.coordinate: Coordinate = coordinate
|
||||
"""The coordinate of the cell that was selected."""
|
||||
self.cell_key: CellKey = cell_key
|
||||
"""The key for the selected cell."""
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "value", self.value
|
||||
yield "coordinate", self.coordinate
|
||||
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):
|
||||
"""Posted when a row is highlighted.
|
||||
|
||||
@@ -376,18 +388,26 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
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
|
||||
"""The y-coordinate of the cursor that highlighted the row."""
|
||||
self.row_key: RowKey = row_key
|
||||
"""The key of the row that was highlighted."""
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "cursor_row", self.cursor_row
|
||||
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):
|
||||
"""Posted when a row is selected.
|
||||
|
||||
@@ -397,18 +417,26 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
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
|
||||
"""The y-coordinate of the cursor that made the selection."""
|
||||
self.row_key: RowKey = row_key
|
||||
"""The key of the row that was selected."""
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "cursor_row", self.cursor_row
|
||||
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):
|
||||
"""Posted when a column is highlighted.
|
||||
|
||||
@@ -419,19 +447,25 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, sender: DataTable, cursor_column: int, column_key: ColumnKey
|
||||
self, data_table: DataTable, cursor_column: int, column_key: ColumnKey
|
||||
) -> None:
|
||||
self.data_table = data_table
|
||||
"""The data table."""
|
||||
self.cursor_column: int = cursor_column
|
||||
"""The x-coordinate of the column that was highlighted."""
|
||||
self.column_key = column_key
|
||||
"""The key of the column that was highlighted."""
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "cursor_column", self.cursor_column
|
||||
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):
|
||||
"""Posted when a column is selected.
|
||||
|
||||
@@ -442,67 +476,85 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, sender: DataTable, cursor_column: int, column_key: ColumnKey
|
||||
self, data_table: DataTable, cursor_column: int, column_key: ColumnKey
|
||||
) -> None:
|
||||
self.data_table = data_table
|
||||
"""The data table."""
|
||||
self.cursor_column: int = cursor_column
|
||||
"""The x-coordinate of the column that was selected."""
|
||||
self.column_key = column_key
|
||||
"""The key of the column that was selected."""
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "cursor_column", self.cursor_column
|
||||
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):
|
||||
"""Posted when a column header/label is clicked."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sender: DataTable,
|
||||
data_table: DataTable,
|
||||
column_key: ColumnKey,
|
||||
column_index: int,
|
||||
label: Text,
|
||||
):
|
||||
self.data_table = data_table
|
||||
"""The data table."""
|
||||
self.column_key = column_key
|
||||
"""The key for the column."""
|
||||
self.column_index = column_index
|
||||
"""The index for the column."""
|
||||
self.label = label
|
||||
"""The text of the label."""
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "column_key", self.column_key
|
||||
yield "column_index", self.column_index
|
||||
yield "label", self.label.plain
|
||||
|
||||
@property
|
||||
def control(self) -> DataTable:
|
||||
"""Alias for the data table."""
|
||||
return self.data_table
|
||||
|
||||
class RowLabelSelected(Message, bubble=True):
|
||||
"""Posted when a row label is clicked."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sender: DataTable,
|
||||
data_table: DataTable,
|
||||
row_key: RowKey,
|
||||
row_index: int,
|
||||
label: Text,
|
||||
):
|
||||
self.data_table = data_table
|
||||
"""The data table."""
|
||||
self.row_key = row_key
|
||||
"""The key for the column."""
|
||||
self.row_index = row_index
|
||||
"""The index for the column."""
|
||||
self.label = label
|
||||
"""The text of the label."""
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "row_key", self.row_key
|
||||
yield "row_index", self.row_index
|
||||
yield "label", self.label.plain
|
||||
|
||||
@property
|
||||
def control(self) -> DataTable:
|
||||
"""Alias for the data table."""
|
||||
return self.data_table
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -896,7 +948,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
return
|
||||
else:
|
||||
cell_key = self.coordinate_to_cell_key(coordinate)
|
||||
self.post_message_no_wait(
|
||||
self.post_message(
|
||||
DataTable.CellHighlighted(
|
||||
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)
|
||||
if is_valid_row:
|
||||
row_key = self._row_locations.get_key(row_index)
|
||||
self.post_message_no_wait(
|
||||
DataTable.RowHighlighted(self, row_index, row_key)
|
||||
)
|
||||
self.post_message(DataTable.RowHighlighted(self, row_index, row_key))
|
||||
|
||||
def _highlight_column(self, column_index: int) -> None:
|
||||
"""Apply highlighting to the column at the given index, and post event."""
|
||||
self.refresh_column(column_index)
|
||||
if column_index < len(self.columns):
|
||||
column_key = self._column_locations.get_key(column_index)
|
||||
self.post_message_no_wait(
|
||||
self.post_message(
|
||||
DataTable.ColumnHighlighted(self, column_index, column_key)
|
||||
)
|
||||
|
||||
@@ -1837,13 +1887,13 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
message = DataTable.HeaderSelected(
|
||||
self, column.key, column_index, label=column.label
|
||||
)
|
||||
self.post_message_no_wait(message)
|
||||
self.post_message(message)
|
||||
elif is_row_label_click:
|
||||
row = self.ordered_rows[row_index]
|
||||
message = DataTable.RowLabelSelected(
|
||||
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":
|
||||
# Only post selection events if there is a visible row/col/cell cursor.
|
||||
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
|
||||
cell_key = self.coordinate_to_cell_key(cursor_coordinate)
|
||||
if cursor_type == "cell":
|
||||
self.post_message_no_wait(
|
||||
self.post_message(
|
||||
DataTable.CellSelected(
|
||||
self,
|
||||
self.get_cell_at(cursor_coordinate),
|
||||
@@ -1911,10 +1961,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
elif cursor_type == "row":
|
||||
row_index, _ = cursor_coordinate
|
||||
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":
|
||||
_, column_index = cursor_coordinate
|
||||
_, column_key = cell_key
|
||||
self.post_message_no_wait(
|
||||
DataTable.ColumnSelected(self, column_index, column_key)
|
||||
)
|
||||
self.post_message(DataTable.ColumnSelected(self, column_index, column_key))
|
||||
|
||||
@@ -77,9 +77,9 @@ class DirectoryTree(Tree[DirEntry]):
|
||||
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
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -176,7 +176,7 @@ class DirectoryTree(Tree[DirEntry]):
|
||||
if not dir_entry.loaded:
|
||||
self.load_directory(event.node)
|
||||
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:
|
||||
event.stop()
|
||||
@@ -184,4 +184,4 @@ class DirectoryTree(Tree[DirEntry]):
|
||||
if dir_entry is None:
|
||||
return
|
||||
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))
|
||||
|
||||
@@ -146,10 +146,15 @@ class Input(Widget, can_focus=True):
|
||||
input: The `Input` widget that was changed.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: Input, value: str) -> None:
|
||||
super().__init__(sender)
|
||||
def __init__(self, input: Input, value: str) -> None:
|
||||
super().__init__()
|
||||
self.input: Input = input
|
||||
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):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: Input, value: str) -> None:
|
||||
super().__init__(sender)
|
||||
def __init__(self, input: Input, value: str) -> None:
|
||||
super().__init__()
|
||||
self.input: Input = input
|
||||
self.value: str = value
|
||||
self.input: Input = sender
|
||||
|
||||
@property
|
||||
def control(self) -> Input:
|
||||
"""Alias for self.input."""
|
||||
return self.input
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -243,7 +253,7 @@ class Input(Widget, can_focus=True):
|
||||
async def watch_value(self, value: str) -> None:
|
||||
if self.styles.auto_dimensions:
|
||||
self.refresh(layout=True)
|
||||
await self.post_message(self.Changed(self, value))
|
||||
self.post_message(self.Changed(self, value))
|
||||
|
||||
@property
|
||||
def cursor_width(self) -> int:
|
||||
@@ -479,4 +489,4 @@ class Input(Widget, can_focus=True):
|
||||
|
||||
async def action_submit(self) -> None:
|
||||
"""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))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Provides a list item widget for use with `ListView`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from textual import events
|
||||
from textual.message import Message
|
||||
from textual.reactive import reactive
|
||||
@@ -41,10 +43,12 @@ class ListItem(Widget, can_focus=False):
|
||||
class _ChildClicked(Message):
|
||||
"""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:
|
||||
self.post_message_no_wait(self._ChildClicked(self))
|
||||
self.post_message(self._ChildClicked(self))
|
||||
|
||||
def watch_highlighted(self, value: bool) -> None:
|
||||
self.set_class(value, "--highlight")
|
||||
|
||||
@@ -48,8 +48,9 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
|
||||
item: The highlighted item, if there is one highlighted.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: ListView, item: ListItem | None) -> None:
|
||||
super().__init__(sender)
|
||||
def __init__(self, list_view: ListView, item: ListItem | None) -> None:
|
||||
super().__init__()
|
||||
self.list_view = list_view
|
||||
self.item: ListItem | None = item
|
||||
|
||||
class Selected(Message, bubble=True):
|
||||
@@ -62,8 +63,9 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
|
||||
item: The selected item.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: ListView, item: ListItem) -> None:
|
||||
super().__init__(sender)
|
||||
def __init__(self, list_view: ListView, item: ListItem) -> None:
|
||||
super().__init__()
|
||||
self.list_view = list_view
|
||||
self.item: ListItem = item
|
||||
|
||||
def __init__(
|
||||
@@ -143,7 +145,7 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
|
||||
new_child = None
|
||||
|
||||
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:
|
||||
"""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
|
||||
if selected_child is None:
|
||||
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:
|
||||
"""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:
|
||||
self.focus()
|
||||
self.index = self._nodes.index(event.sender)
|
||||
self.post_message_no_wait(self.Selected(self, event.sender))
|
||||
self.index = self._nodes.index(event.item)
|
||||
self.post_message(self.Selected(self, event.item))
|
||||
|
||||
def _scroll_highlighted_region(self) -> None:
|
||||
"""Used to keep the highlighted index within vision"""
|
||||
|
||||
@@ -100,7 +100,7 @@ class MarkdownBlock(Static):
|
||||
|
||||
async def action_link(self, href: str) -> None:
|
||||
"""Called on link click."""
|
||||
await self.post_message(Markdown.LinkClicked(href, sender=self))
|
||||
self.post_message(Markdown.LinkClicked(href))
|
||||
|
||||
|
||||
class MarkdownHeader(MarkdownBlock):
|
||||
@@ -524,26 +524,24 @@ class Markdown(Widget):
|
||||
class TableOfContentsUpdated(Message, bubble=True):
|
||||
"""The table of contents was updated."""
|
||||
|
||||
def __init__(
|
||||
self, table_of_contents: TableOfContentsType, *, sender: Widget
|
||||
) -> None:
|
||||
super().__init__(sender=sender)
|
||||
def __init__(self, table_of_contents: TableOfContentsType) -> None:
|
||||
super().__init__()
|
||||
self.table_of_contents: TableOfContentsType = table_of_contents
|
||||
"""Table of contents."""
|
||||
|
||||
class TableOfContentsSelected(Message, bubble=True):
|
||||
"""An item in the TOC was selected."""
|
||||
|
||||
def __init__(self, block_id: str, *, sender: Widget) -> None:
|
||||
super().__init__(sender=sender)
|
||||
def __init__(self, block_id: str) -> None:
|
||||
super().__init__()
|
||||
self.block_id = block_id
|
||||
"""ID of the block that was selected."""
|
||||
|
||||
class LinkClicked(Message, bubble=True):
|
||||
"""A link in the document was clicked."""
|
||||
|
||||
def __init__(self, href: str, *, sender: Widget) -> None:
|
||||
super().__init__(sender=sender)
|
||||
def __init__(self, href: str) -> None:
|
||||
super().__init__()
|
||||
self.href: str = href
|
||||
"""The link that was selected."""
|
||||
|
||||
@@ -702,9 +700,7 @@ class Markdown(Widget):
|
||||
)
|
||||
)
|
||||
|
||||
await self.post_message(
|
||||
Markdown.TableOfContentsUpdated(table_of_contents, sender=self)
|
||||
)
|
||||
self.post_message(Markdown.TableOfContentsUpdated(table_of_contents))
|
||||
with self.app.batch_update():
|
||||
await self.query("MarkdownBlock").remove()
|
||||
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:
|
||||
node_data = message.node.data
|
||||
if node_data is not None:
|
||||
await self.post_message(
|
||||
Markdown.TableOfContentsSelected(node_data["block_id"], sender=self)
|
||||
await self._post_message(
|
||||
Markdown.TableOfContentsSelected(node_data["block_id"])
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Provides a radio button widget."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ._toggle_button import ToggleButton
|
||||
|
||||
|
||||
@@ -21,3 +23,14 @@ class RadioButton(ToggleButton):
|
||||
|
||||
# https://github.com/Textualize/textual/issues/1814
|
||||
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
|
||||
|
||||
@@ -37,15 +37,14 @@ class RadioSet(Container):
|
||||
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.
|
||||
|
||||
Args:
|
||||
sender: The radio set sending the message.
|
||||
pressed: The radio button that was pressed.
|
||||
"""
|
||||
super().__init__(sender)
|
||||
self.input = sender
|
||||
super().__init__()
|
||||
self.radio_set = radio_set
|
||||
"""A reference to the `RadioSet` that was changed."""
|
||||
self.pressed = pressed
|
||||
"""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
|
||||
# pressed button via the normal route. So here we go under the
|
||||
# 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."""
|
||||
|
||||
def __init__(
|
||||
@@ -114,16 +113,14 @@ class RadioSet(Container):
|
||||
event: The event.
|
||||
"""
|
||||
# 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
|
||||
# changed.
|
||||
self.post_message_no_wait(
|
||||
self.Changed(self, cast(RadioButton, event.input))
|
||||
)
|
||||
self.post_message(self.Changed(self, event.radio_button))
|
||||
# ...then look for the button that was previously the pressed
|
||||
# one and unpress it.
|
||||
for button in self._buttons.filter(".-on"):
|
||||
if button != event.input:
|
||||
if button != event.radio_button:
|
||||
button.value = False
|
||||
break
|
||||
else:
|
||||
@@ -134,7 +131,7 @@ class RadioSet(Container):
|
||||
event.stop()
|
||||
if not self._buttons.filter(".-on"):
|
||||
with self.prevent(RadioButton.Changed):
|
||||
event.input.value = True
|
||||
event.radio_button.value = True
|
||||
|
||||
@property
|
||||
def pressed_button(self) -> RadioButton | None:
|
||||
|
||||
@@ -87,10 +87,15 @@ class Switch(Widget, can_focus=True):
|
||||
input: The `Switch` widget that was changed.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: Switch, value: bool) -> None:
|
||||
super().__init__(sender)
|
||||
def __init__(self, switch: Switch, value: bool) -> None:
|
||||
super().__init__()
|
||||
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__(
|
||||
self,
|
||||
@@ -124,7 +129,7 @@ class Switch(Widget, can_focus=True):
|
||||
self.animate("slider_pos", target_slider_pos, duration=0.3)
|
||||
else:
|
||||
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:
|
||||
self.set_class(slider_pos == 1, "-on")
|
||||
|
||||
@@ -218,15 +218,15 @@ class ToggleButton(Static, can_focus=True):
|
||||
class Changed(Message, bubble=True):
|
||||
"""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.
|
||||
|
||||
Args:
|
||||
sender: The toggle button sending the message.
|
||||
toggle_button: The toggle button sending the message.
|
||||
value: The value of the toggle button.
|
||||
"""
|
||||
super().__init__(sender)
|
||||
self.input = sender
|
||||
super().__init__()
|
||||
self._toggle_button = toggle_button
|
||||
"""A reference to the toggle button that was changed."""
|
||||
self.value = value
|
||||
"""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.
|
||||
"""
|
||||
self.set_class(self.value, "-on")
|
||||
self.post_message_no_wait(self.Changed(self, self.value))
|
||||
self.post_message(self.Changed(self, self.value))
|
||||
|
||||
@@ -14,7 +14,6 @@ from .._cache import LRUCache
|
||||
from .._immutable_sequence_view import ImmutableSequenceView
|
||||
from .._loop import loop_last
|
||||
from .._segment_tools import line_pad
|
||||
from .._types import MessageTarget
|
||||
from ..binding import Binding, BindingType
|
||||
from ..geometry import Region, Size, clamp
|
||||
from ..message import Message
|
||||
@@ -436,11 +435,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
node: The node that was collapsed.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
|
||||
) -> None:
|
||||
def __init__(self, node: TreeNode[EventTreeDataType]) -> None:
|
||||
self.node: TreeNode[EventTreeDataType] = node
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
class NodeExpanded(Generic[EventTreeDataType], Message, bubble=True):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
|
||||
) -> None:
|
||||
def __init__(self, node: TreeNode[EventTreeDataType]) -> None:
|
||||
self.node: TreeNode[EventTreeDataType] = node
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
class NodeHighlighted(Generic[EventTreeDataType], Message, bubble=True):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
|
||||
) -> None:
|
||||
def __init__(self, node: TreeNode[EventTreeDataType]) -> None:
|
||||
self.node: TreeNode[EventTreeDataType] = node
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
class NodeSelected(Generic[EventTreeDataType], Message, bubble=True):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
|
||||
) -> None:
|
||||
def __init__(self, node: TreeNode[EventTreeDataType]) -> None:
|
||||
self.node: TreeNode[EventTreeDataType] = node
|
||||
super().__init__(sender)
|
||||
super().__init__()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -779,7 +770,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
node._selected = True
|
||||
self._cursor_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:
|
||||
self._invalidate()
|
||||
@@ -1027,10 +1018,10 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
return
|
||||
if node.is_expanded:
|
||||
node.collapse()
|
||||
self.post_message_no_wait(self.NodeCollapsed(self, node))
|
||||
self.post_message(self.NodeCollapsed(node))
|
||||
else:
|
||||
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:
|
||||
meta = event.style.meta
|
||||
@@ -1117,4 +1108,4 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
node = line.path[-1]
|
||||
if self.auto_expand:
|
||||
self._toggle_node(node)
|
||||
self.post_message_no_wait(self.NodeSelected(self, node))
|
||||
self.post_message(self.NodeSelected(node))
|
||||
|
||||
@@ -10,7 +10,6 @@ from textual.app import App
|
||||
from textual.coordinate import Coordinate
|
||||
from textual.events import Click, MouseMove
|
||||
from textual.message import Message
|
||||
from textual.message_pump import MessagePump
|
||||
from textual.widgets import DataTable
|
||||
from textual.widgets.data_table import (
|
||||
CellDoesNotExist,
|
||||
@@ -556,9 +555,8 @@ async def test_coordinate_to_cell_key_invalid_coordinate():
|
||||
table.coordinate_to_cell_key(Coordinate(9999, 9999))
|
||||
|
||||
|
||||
def make_click_event(sender: MessagePump):
|
||||
def make_click_event():
|
||||
return Click(
|
||||
sender=sender,
|
||||
x=1,
|
||||
y=2,
|
||||
delta_x=0,
|
||||
@@ -577,7 +575,7 @@ async def test_datatable_on_click_cell_cursor():
|
||||
app = DataTableApp()
|
||||
async with app.run_test() as pilot:
|
||||
table = app.query_one(DataTable)
|
||||
click = make_click_event(app)
|
||||
click = make_click_event()
|
||||
column_key = table.add_column("ABC")
|
||||
table.add_row("123")
|
||||
row_key = table.add_row("456")
|
||||
@@ -591,13 +589,11 @@ async def test_datatable_on_click_cell_cursor():
|
||||
"CellSelected",
|
||||
]
|
||||
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.cell_key == CellKey(row_key, column_key)
|
||||
assert cell_highlighted_event.coordinate == Coordinate(1, 0)
|
||||
|
||||
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.cell_key == CellKey(row_key, column_key)
|
||||
assert cell_selected_event.coordinate == Coordinate(1, 0)
|
||||
@@ -610,7 +606,7 @@ async def test_on_click_row_cursor():
|
||||
async with app.run_test():
|
||||
table = app.query_one(DataTable)
|
||||
table.cursor_type = "row"
|
||||
click = make_click_event(app)
|
||||
click = make_click_event()
|
||||
table.add_column("ABC")
|
||||
table.add_row("123")
|
||||
row_key = table.add_row("456")
|
||||
@@ -619,12 +615,11 @@ async def test_on_click_row_cursor():
|
||||
assert app.message_names == ["RowHighlighted", "RowHighlighted", "RowSelected"]
|
||||
|
||||
row_highlighted: DataTable.RowHighlighted = app.messages[1]
|
||||
assert row_highlighted.sender is table
|
||||
|
||||
assert row_highlighted.row_key == row_key
|
||||
assert row_highlighted.cursor_row == 1
|
||||
|
||||
row_selected: DataTable.RowSelected = app.messages[2]
|
||||
assert row_selected.sender is table
|
||||
assert row_selected.row_key == row_key
|
||||
assert row_highlighted.cursor_row == 1
|
||||
|
||||
@@ -639,7 +634,7 @@ async def test_on_click_column_cursor():
|
||||
column_key = table.add_column("ABC")
|
||||
table.add_row("123")
|
||||
table.add_row("456")
|
||||
click = make_click_event(app)
|
||||
click = make_click_event()
|
||||
table.on_click(event=click)
|
||||
await wait_for_idle(0)
|
||||
assert app.message_names == [
|
||||
@@ -648,12 +643,10 @@ async def test_on_click_column_cursor():
|
||||
"ColumnSelected",
|
||||
]
|
||||
column_highlighted: DataTable.ColumnHighlighted = app.messages[1]
|
||||
assert column_highlighted.sender is table
|
||||
assert column_highlighted.column_key == column_key
|
||||
assert column_highlighted.cursor_column == 0
|
||||
|
||||
column_selected: DataTable.ColumnSelected = app.messages[2]
|
||||
assert column_selected.sender is table
|
||||
assert column_selected.column_key == column_key
|
||||
assert column_highlighted.cursor_column == 0
|
||||
|
||||
@@ -669,7 +662,6 @@ async def test_hover_coordinate():
|
||||
assert table.hover_coordinate == Coordinate(0, 0)
|
||||
|
||||
mouse_move = MouseMove(
|
||||
sender=app,
|
||||
x=1,
|
||||
y=2,
|
||||
delta_x=0,
|
||||
@@ -694,7 +686,6 @@ async def test_header_selected():
|
||||
column_key = table.add_column("number")
|
||||
table.add_row(3)
|
||||
click_event = Click(
|
||||
sender=table,
|
||||
x=3,
|
||||
y=0,
|
||||
delta_x=0,
|
||||
@@ -708,7 +699,6 @@ async def test_header_selected():
|
||||
table.on_click(click_event)
|
||||
await pilot.pause()
|
||||
message: DataTable.HeaderSelected = app.messages[-1]
|
||||
assert message.sender is table
|
||||
assert message.label == Text("number")
|
||||
assert message.column_index == 0
|
||||
assert message.column_key == column_key
|
||||
@@ -729,7 +719,6 @@ async def test_row_label_selected():
|
||||
table.add_column("number")
|
||||
row_key = table.add_row(3, label="A")
|
||||
click_event = Click(
|
||||
sender=table,
|
||||
x=1,
|
||||
y=1,
|
||||
delta_x=0,
|
||||
@@ -743,7 +732,6 @@ async def test_row_label_selected():
|
||||
table.on_click(click_event)
|
||||
await pilot.pause()
|
||||
message: DataTable.RowLabelSelected = app.messages[-1]
|
||||
assert message.sender is table
|
||||
assert message.label == Text("A")
|
||||
assert message.row_index == 0
|
||||
assert message.row_key == row_key
|
||||
|
||||
@@ -19,7 +19,7 @@ class ValidWidget(Widget):
|
||||
|
||||
async def test_dispatch_key_valid_key():
|
||||
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 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
|
||||
tab and ctrl+i are both considered valid."""
|
||||
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 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."""
|
||||
widget = DuplicateHandlersWidget()
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ async def test_paste_app():
|
||||
|
||||
app = PasteApp()
|
||||
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)
|
||||
|
||||
assert len(paste_events) == 1
|
||||
|
||||
@@ -34,7 +34,7 @@ def chunks(data, size):
|
||||
|
||||
@pytest.fixture
|
||||
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])
|
||||
@@ -65,7 +65,6 @@ def test_bracketed_paste(parser):
|
||||
assert len(events) == 1
|
||||
assert isinstance(events[0], Paste)
|
||||
assert events[0].text == pasted_text
|
||||
assert events[0].sender == mock.sentinel
|
||||
|
||||
|
||||
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))
|
||||
assert len(events) == 1
|
||||
assert isinstance(events[0], TerminalSupportsSynchronizedOutput)
|
||||
assert events[0].sender == mock.sentinel
|
||||
|
||||
|
||||
def test_terminal_mode_reporting_synchronized_output_not_supported(parser):
|
||||
|
||||
@@ -15,7 +15,7 @@ class CheckboxApp(App[None]):
|
||||
yield Checkbox(value=True, id="cb3")
|
||||
|
||||
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:
|
||||
|
||||
@@ -15,7 +15,7 @@ class RadioButtonApp(App[None]):
|
||||
yield RadioButton(value=True, id="rb3")
|
||||
|
||||
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:
|
||||
|
||||
@@ -19,9 +19,9 @@ class RadioSetApp(App[None]):
|
||||
def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
|
||||
self.events_received.append(
|
||||
(
|
||||
event.input.id,
|
||||
event.radio_set.id,
|
||||
event.index,
|
||||
[button.value for button in event.input.query(RadioButton)],
|
||||
[button.value for button in event.radio_set.query(RadioButton)],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user