mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
fix for removing
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,7 +974,9 @@ 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
|
||||
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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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,6 +1151,7 @@ class Widget(DOMNode):
|
||||
Args:
|
||||
event (events.Idle): Idle event.
|
||||
"""
|
||||
if self._parent is not None:
|
||||
if self._repaint_required:
|
||||
self._repaint_required = False
|
||||
self.screen.post_message_no_wait(messages.Update(self, self))
|
||||
@@ -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:
|
||||
|
||||
@@ -29,4 +29,4 @@ class Static(Widget):
|
||||
|
||||
def update(self, renderable: RenderableType) -> None:
|
||||
self.renderable = renderable
|
||||
self.refresh(layout=True)
|
||||
self.refresh()
|
||||
|
||||
Reference in New Issue
Block a user