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