From 215330d99ccd07e31bab5286b6c962af7d1a41e5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 6 Aug 2022 08:47:22 +0100 Subject: [PATCH] remove functionality --- src/textual/_node_list.py | 6 ++++++ src/textual/app.py | 13 ++++++++++--- src/textual/css/query.py | 36 +++++++++++++++++++++++++++++++----- src/textual/dom.py | 2 +- src/textual/events.py | 4 ++++ src/textual/message_pump.py | 11 +++++++---- src/textual/widget.py | 29 +++++++++++++++++++++++++++++ 7 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/textual/_node_list.py b/src/textual/_node_list.py index cd61cb217..b8d16f2f4 100644 --- a/src/textual/_node_list.py +++ b/src/textual/_node_list.py @@ -45,6 +45,12 @@ class NodeList: self._nodes_set.add(widget) self._updates += 1 + def _remove(self, widget: Widget) -> None: + if widget in self._nodes_set: + del self._nodes[self._nodes.index(widget)] + self._nodes_set.remove(widget) + self._updates += 1 + def _clear(self) -> None: if self._nodes: self._nodes.clear() diff --git a/src/textual/app.py b/src/textual/app.py index 18f229e9d..bb32b3555 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -884,6 +884,16 @@ class App(Generic[ReturnType], DOMNode): for _widget_id, widget in name_widgets: widget.post_message_no_wait(events.Mount(sender=parent)) + def unregister(self, widget: Widget) -> None: + """Unregister a widget. + + Args: + widget (Widget): _description_ + """ + if isinstance(widget._parent, Widget): + widget._parent.children._remove(widget) + self.registry.discard(widget) + async def _disconnect_devtools(self): await self.devtools.disconnect() @@ -906,9 +916,6 @@ class App(Generic[ReturnType], DOMNode): child = self.registry.pop() await child.close_messages() - async def remove(self, child: MessagePump) -> None: - self.registry.remove(child) - async def shutdown(self): await self._disconnect_devtools() driver = self._driver diff --git a/src/textual/css/query.py b/src/textual/css/query.py index 43fdef576..1d7a92256 100644 --- a/src/textual/css/query.py +++ b/src/textual/css/query.py @@ -24,6 +24,7 @@ from .parse import parse_selectors if TYPE_CHECKING: from ..dom import DOMNode + from ..widget import Widget class NoMatchingNodesError(Exception): @@ -36,14 +37,16 @@ class DOMQuery: self, node: DOMNode | None = None, selector: str | None = None, - nodes: list[DOMNode] | None = None, + nodes: list[Widget] | None = None, ) -> None: + from ..widget import Widget + self._selector = selector - self._nodes: list[DOMNode] = [] + self._nodes: list[Widget] = [] if nodes is not None: self._nodes = nodes elif node is not None: - self._nodes = list(node.walk_children()) + self._nodes = [node for node in node.walk_children()] else: self._nodes = [] @@ -58,9 +61,12 @@ class DOMQuery: """True if non-empty, otherwise False.""" return bool(self._nodes) - def __iter__(self) -> Iterator[DOMNode]: + def __iter__(self) -> Iterator[Widget]: return iter(self._nodes) + def __getitem__(self, index: int) -> DOMNode: + return self._nodes[index] + def __rich_repr__(self) -> rich.repr.Result: yield self._nodes @@ -73,6 +79,7 @@ class DOMQuery: Returns: DOMQuery: New DOM Query. """ + selector_set = parse_selectors(selector) query = DOMQuery( nodes=[_node for _node in self._nodes if match(selector_set, _node)] @@ -94,7 +101,7 @@ class DOMQuery: ) return query - def first(self) -> DOMNode: + def first(self) -> Widget: """Get the first matched node. Returns: @@ -107,6 +114,19 @@ class DOMQuery: f"No nodes match the selector {self._selector!r}" ) + def last(self) -> Widget: + """Get the last matched node. + + Returns: + DOMNode: A DOM Node. + """ + if self._nodes: + return self._nodes[-1] + else: + raise NoMatchingNodesError( + f"No nodes match the selector {self._selector!r}" + ) + def add_class(self, *class_names: str) -> DOMQuery: """Add the given class name(s) to nodes.""" for node in self._nodes: @@ -125,6 +145,12 @@ class DOMQuery: node.toggle_class(*class_names) return self + def remove(self) -> DOMQuery: + """Remove matched nodes from the DOM""" + for node in self._nodes: + node.remove() + return self + def set_styles(self, css: str | None = None, **styles: str) -> DOMQuery: """Set styles on matched nodes. diff --git a/src/textual/dom.py b/src/textual/dom.py index 78f90e306..3fad14739 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -254,7 +254,7 @@ class DOMNode(MessagePump): """ Returns: ``True`` if this DOMNode is displayed (``display != "none"``), ``False`` otherwise. """ - return self.styles.display != "none" + return self.styles.display != "none" and not (self._closing or self._closed) @display.setter def display(self, new_val: bool | str) -> None: diff --git a/src/textual/events.py b/src/textual/events.py index ee9105ad1..b4ff68c54 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -130,6 +130,10 @@ class Unmount(Event, bubble=False): """Sent when a widget is unmounted, and may no longer receive messages.""" +class Remove(Event, bubble=False): + """Sent to a widget to ask it to remove itself from the DOM.""" + + 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 61f3465f8..36b0d6693 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -214,16 +214,19 @@ class MessagePump: async def close_messages(self) -> None: """Close message queue, and optionally wait for queue to finish processing.""" - if self._closed: + if self._closed or self._closing: return - + print(self, "close_messages") self._closing = True await self._message_queue.put(MessagePriority(None)) - for task in self._child_tasks: + self.app.unregister(self) + 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: + if self._task is not None and asyncio.current_task() != self._task: # Ensure everything is closed before returning await self._task diff --git a/src/textual/widget.py b/src/textual/widget.py index d1f218bc2..09fedacbf 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -181,6 +181,16 @@ class Widget(DOMNode): self.scroll_to(0, 0, animate=False) def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None: + """Mount child widgets (making this widget a container). + + Widgets may be passed as positional arguments or keyword arguments. If keyword arguments, + the keys will be set as the Widget's id. + + Example: + self.mount(Static("hello"), header=Header()) + + + """ self.app.register(self, *anon_widgets, **widgets) self.screen.refresh() @@ -815,6 +825,17 @@ 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. + """ + parent = self.parent + if isinstance(parent, Widget): + return parent.scroll_to_widget(self) + return False + def __init_subclass__( cls, can_focus: bool = False, @@ -1051,6 +1072,10 @@ class Widget(DOMNode): self.check_idle() + def remove(self) -> None: + """Remove the Widget from the DOM (effectively deleting it)""" + self.post_message_no_wait(events.Remove(self)) + def render(self) -> RenderableType: """Get renderable for widget. @@ -1122,6 +1147,10 @@ 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() + self.parent.refresh(layout=True) + def on_mount(self, event: events.Mount) -> None: widgets = list(self.compose()) if widgets: