From fd349aa658d9e6af513932e6b0076fb1e8187fa8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 19 Aug 2022 09:33:36 +0100 Subject: [PATCH] fix for removing --- docs/examples/introduction/timers.css | 6 ++--- docs/examples/introduction/timers.py | 23 ++++++++++-------- src/textual/app.py | 34 ++++++++++++++++++++------- src/textual/events.py | 5 ++++ src/textual/message_pump.py | 26 ++++++++++++-------- src/textual/widget.py | 34 +++++++++------------------ src/textual/widgets/_static.py | 2 +- 7 files changed, 73 insertions(+), 57 deletions(-) diff --git a/docs/examples/introduction/timers.css b/docs/examples/introduction/timers.css index e4188de60..a5f2295fb 100644 --- a/docs/examples/introduction/timers.css +++ b/docs/examples/introduction/timers.css @@ -5,7 +5,6 @@ TimerWidget { border: tall $panel-darken-2; margin: 1; padding: 0 1; - transition: background 200ms linear; } @@ -23,6 +22,7 @@ Button { dock: left; } + TimerWidget.started { opacity: 100%; text-style: bold; @@ -43,13 +43,11 @@ TimerWidget.started #reset { visibility: hidden } - #stop { dock: left; display: none; } - -Button#reset { +#reset { dock: right; } diff --git a/docs/examples/introduction/timers.py b/docs/examples/introduction/timers.py index 9ae350b68..8ff76895e 100644 --- a/docs/examples/introduction/timers.py +++ b/docs/examples/introduction/timers.py @@ -1,4 +1,4 @@ -from time import time +from time import monotonic from textual.app import App, ComposeResult from textual.layout import Container @@ -14,7 +14,7 @@ class TimeDisplay(Static): def watch_time_delta(self, time_delta: float) -> None: minutes, seconds = divmod(time_delta, 60) hours, minutes = divmod(minutes, 60) - self.update(f"{hours:02.0f}:{minutes:02.0f}:{seconds:02.2f}") + self.update(f"{hours:02.0f}:{minutes:02.0f}:{seconds:05.2f}") class TimerWidget(Static): @@ -28,10 +28,9 @@ class TimerWidget(Static): self.update_timer = self.set_interval(1 / 30, self.update_elapsed, pause=True) def update_elapsed(self) -> None: - time_delta = ( - self.total + time() - self.start_time if self.started else self.total + self.query_one(TimeDisplay).time_delta = ( + self.total + monotonic() - self.start_time if self.started else self.total ) - self.query_one(TimeDisplay).time_delta = time_delta def compose(self) -> ComposeResult: yield Button("Start", id="start", variant="success") @@ -41,13 +40,15 @@ class TimerWidget(Static): def watch_started(self, started: bool) -> None: if started: - self.start_time = time() + self.start_time = monotonic() self.update_timer.resume() self.add_class("started") + self.query_one("#stop").focus() else: self.update_timer.pause() - self.total += time() - self.start_time + self.total += monotonic() - self.start_time self.remove_class("started") + self.query_one("#start").focus() def on_button_pressed(self, event: Button.Pressed) -> None: button_id = event.button.id @@ -59,6 +60,8 @@ class TimerWidget(Static): class TimerApp(App): + """Manage the timers.""" + def on_load(self) -> None: self.bind("a", "add_timer", description="Add") self.bind("r", "remove_timer", description="Remove") @@ -70,8 +73,8 @@ class TimerApp(App): def action_add_timer(self) -> None: new_timer = TimerWidget() - self.query_one("Container").mount(new_timer) - self.call_later(new_timer.scroll_visible) + self.query_one(Container).mount(new_timer) + new_timer.scroll_visible() def action_remove_timer(self) -> None: timers = self.query("Container TimerWidget") @@ -79,6 +82,6 @@ class TimerApp(App): timers.last().remove() -app = TimerApp(css_path="timers.css") +app = TimerApp(title="TimerApp", css_path="timers.css") if __name__ == "__main__": app.run() diff --git a/src/textual/app.py b/src/textual/app.py index 5e5decfb9..70020ceae 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -7,7 +7,7 @@ import os import platform import sys import warnings -from contextlib import redirect_stdout +from contextlib import redirect_stdout, redirect_stderr from datetime import datetime from pathlib import PurePath from time import perf_counter @@ -974,8 +974,10 @@ class App(Generic[ReturnType], DOMNode): if self.is_headless: await run_process_messages() else: - with redirect_stdout(StdoutRedirector(self.devtools, self._log_file)): # type: ignore - await run_process_messages() + redirector = StdoutRedirector(self.devtools, self._log_file) + with redirect_stderr(redirector): + with redirect_stdout(redirector): # type: ignore + await run_process_messages() finally: driver.stop_application_mode() except Exception as error: @@ -1070,9 +1072,12 @@ class App(Generic[ReturnType], DOMNode): Args: widget (Widget): A Widget to unregister """ + if self.focused is widget: + self.focused = None + if isinstance(widget._parent, Widget): widget._parent.children._remove(widget) - widget._attach(None) + widget._detach() self._registry.discard(widget) async def _disconnect_devtools(self): @@ -1291,13 +1296,13 @@ class App(Generic[ReturnType], DOMNode): return False return True - async def on_update(self, message: messages.Update) -> None: + async def _on_update(self, message: messages.Update) -> None: message.stop() - async def on_layout(self, message: messages.Layout) -> None: + async def _on_layout(self, message: messages.Layout) -> None: message.stop() - async def on_key(self, event: events.Key) -> None: + async def _on_key(self, event: events.Key) -> None: if event.key == "tab": self.focus_next() elif event.key == "shift+tab": @@ -1305,15 +1310,26 @@ class App(Generic[ReturnType], DOMNode): else: await self.press(event.key) - async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: + async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None: log("shutdown request") await self.close_messages() - async def on_resize(self, event: events.Resize) -> None: + async def _on_resize(self, event: events.Resize) -> None: event.stop() self.screen._screen_resized(event.size) await self.screen.post_message(event) + async def _on_remove(self, event: events.Remove) -> None: + widget = event.widget + if widget.has_parent: + widget.parent.refresh(layout=True) + + remove_widgets = list(widget.walk_children(Widget, with_self=True)) + for child in remove_widgets: + self._unregister(child) + for child in remove_widgets: + await child.close_messages() + async def action_press(self, key: str) -> None: await self.press(key) diff --git a/src/textual/events.py b/src/textual/events.py index 7dc006b12..fceadd8bd 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -16,6 +16,7 @@ MouseEventT = TypeVar("MouseEventT", bound="MouseEvent") if TYPE_CHECKING: from ._timer import Timer as TimerClass from ._timer import TimerCallback + from .widget import WIdget @rich.repr.auto @@ -128,6 +129,10 @@ class Unmount(Event, bubble=False): class Remove(Event, bubble=False): """Sent to a widget to ask it to remove itself from the DOM.""" + def __init__(self, sender: MessageTarget, widget: Widget) -> None: + self.widget = widget + super().__init__(sender) + class Show(Event, bubble=False): """Sent when a widget has become visible.""" diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 02583de9b..38e013dd9 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -87,7 +87,7 @@ class MessagePump(metaclass=MessagePumpMeta): self._disabled_messages: set[type[Message]] = set() self._pending_message: Message | None = None self._task: Task | None = None - self._child_tasks: WeakSet[Task] = WeakSet() + self._timers: WeakSet[Timer] = WeakSet() @property def task(self) -> Task: @@ -130,6 +130,10 @@ class MessagePump(metaclass=MessagePumpMeta): """ self._parent = parent + def _detach(self) -> None: + """Set the parent to None to remove the node from the tree.""" + self._parent = None + def check_message_enabled(self, message: Message) -> bool: return type(message) not in self._disabled_messages @@ -199,7 +203,8 @@ class MessagePump(metaclass=MessagePumpMeta): repeat=0, pause=pause, ) - self._child_tasks.add(timer.start()) + timer.start() + self._timers.add(timer) return timer def set_interval( @@ -220,7 +225,8 @@ class MessagePump(metaclass=MessagePumpMeta): repeat=repeat or None, pause=pause, ) - self._child_tasks.add(timer.start()) + timer.start() + self._timers.add(timer) return timer def call_later(self, callback: Callable, *args, **kwargs) -> None: @@ -248,13 +254,11 @@ class MessagePump(metaclass=MessagePumpMeta): if self._closed or self._closing: return self._closing = True + for timer in self._timers: + await timer.stop() + self._timers.clear() await self._message_queue.put(MessagePriority(None)) - cancel_tasks = list(self._child_tasks) - for task in cancel_tasks: - task.cancel() - for task in cancel_tasks: - await task - self._child_tasks.clear() + if self._task is not None and asyncio.current_task() != self._task: # Ensure everything is closed before returning await self._task @@ -265,11 +269,13 @@ class MessagePump(metaclass=MessagePumpMeta): async def process_messages(self) -> None: self._running = True try: - return await self._process_messages() + await self._process_messages() except CancelledError: pass finally: self._running = False + for timer in self._timers: + await timer.stop() async def _process_messages(self) -> None: """Process messages until the queue is closed.""" diff --git a/src/textual/widget.py b/src/textual/widget.py index ccaa4d2b1..3feb3a827 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -859,16 +859,11 @@ class Widget(DOMNode): ) return delta - def scroll_visible(self) -> bool: - """Scroll the container to make this widget visible. - - Returns: - bool: True if the parent was scrolled. - """ + def scroll_visible(self) -> None: + """Scroll the container to make this widget visible.""" parent = self.parent if isinstance(parent, Widget): - return parent.scroll_to_widget(self) - return False + self.call_later(parent.scroll_to_widget, self) def __init_subclass__( cls, @@ -1126,9 +1121,7 @@ class Widget(DOMNode): def remove(self) -> None: """Remove the Widget from the DOM (effectively deleting it)""" - for child in self.children: - child.remove() - self.post_message_no_wait(events.Remove(self)) + self.app.post_message_no_wait(events.Remove(self, widget=self)) def render(self) -> RenderableType: """Get renderable for widget. @@ -1158,12 +1151,13 @@ class Widget(DOMNode): Args: event (events.Idle): Idle event. """ - if self._repaint_required: - self._repaint_required = False - self.screen.post_message_no_wait(messages.Update(self, self)) - if self._layout_required: - self._layout_required = False - self.screen.post_message_no_wait(messages.Layout(self)) + if self._parent is not None: + if self._repaint_required: + self._repaint_required = False + self.screen.post_message_no_wait(messages.Update(self, self)) + if self._layout_required: + self._layout_required = False + self.screen.post_message_no_wait(messages.Layout(self)) def focus(self) -> None: """Give input focus to this widget.""" @@ -1201,12 +1195,6 @@ class Widget(DOMNode): async def on_key(self, event: events.Key) -> None: await self.dispatch_key(event) - async def on_remove(self, event: events.Remove) -> None: - await self.close_messages() - assert self.parent - self.parent.refresh(layout=True) - self.app._unregister(self) - def _on_mount(self, event: events.Mount) -> None: widgets = list(self.compose()) if widgets: diff --git a/src/textual/widgets/_static.py b/src/textual/widgets/_static.py index eb3359dbb..22cd90b72 100644 --- a/src/textual/widgets/_static.py +++ b/src/textual/widgets/_static.py @@ -29,4 +29,4 @@ class Static(Widget): def update(self, renderable: RenderableType) -> None: self.renderable = renderable - self.refresh(layout=True) + self.refresh()