fix for removing

This commit is contained in:
Will McGugan
2022-08-19 09:33:36 +01:00
parent e761e7ae8f
commit fd349aa658
7 changed files with 73 additions and 57 deletions

View File

@@ -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;
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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."""

View File

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

View File

@@ -29,4 +29,4 @@ class Static(Widget):
def update(self, renderable: RenderableType) -> None:
self.renderable = renderable
self.refresh(layout=True)
self.refresh()