diff --git a/CHANGELOG.md b/CHANGELOG.md index 87edabf69..96a1b1f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally - Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to Space https://github.com/Textualize/textual/issues/1433 - Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437 +- Added DOMNode.watch method https://github.com/Textualize/textual/pull/1750 ### Changed diff --git a/src/textual/demo.py b/src/textual/demo.py index 907d93307..58eb6aea0 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -15,7 +15,7 @@ from rich.text import Text from textual.app import App, ComposeResult from textual.binding import Binding from textual.containers import Container, Horizontal -from textual.reactive import reactive, watch +from textual.reactive import reactive from textual.widgets import ( Button, Checkbox, @@ -203,7 +203,7 @@ class DarkSwitch(Horizontal): yield Static("Dark mode toggle", classes="label") def on_mount(self) -> None: - watch(self.app, "dark", self.on_dark_change, init=False) + self.watch(self.app, "dark", self.on_dark_change, init=False) def on_dark_change(self, dark: bool) -> None: self.query_one(Checkbox).value = self.app.dark diff --git a/src/textual/dom.py b/src/textual/dom.py index c56c45b34..97034a56f 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -4,6 +4,7 @@ import re from inspect import getfile from typing import ( TYPE_CHECKING, + Callable, ClassVar, Iterable, Iterator, @@ -31,7 +32,7 @@ from .css.parse import parse_declarations from .css.styles import RenderStyles, Styles from .css.tokenize import IDENTIFIER from .message_pump import MessagePump -from .reactive import Reactive +from .reactive import Reactive, _watch from .timer import Timer from .walk import walk_breadth_first, walk_depth_first @@ -647,6 +648,19 @@ class DOMNode(MessagePump): """ return [child for child in self.children if child.display] + def watch( + self, obj: DOMNode, attribute_name: str, callback: Callable, init: bool = True + ) -> None: + """Watches for modifications to reactive attributes on another object. + + Args: + obj: Object containing attribute to watch. + attribute_name: Attribute to watch. + callback: A callback to run when attribute changes. + init: Check watchers on first call. + """ + _watch(self, obj, attribute_name, callback, init=init) + def get_pseudo_classes(self) -> Iterable[str]: """Get any pseudo classes applicable to this Node, e.g. hover, focus. diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index ba7cc3485..cdcc4fdcf 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -126,7 +126,6 @@ class MessagePump(metaclass=MessagePumpMeta): @property def is_attached(self) -> bool: """Check the node is attached to the app via the DOM.""" - from .app import App node = self diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 38d631a65..cd891ffec 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -325,10 +325,12 @@ class var(Reactive[ReactiveType]): ) -def watch( +def _watch( + node: DOMNode, obj: Reactable, attribute_name: str, callback: Callable[[Any], object], + *, init: bool = True, ) -> None: """Watch a reactive variable on an object. @@ -346,7 +348,7 @@ def watch( watcher_list = watchers.setdefault(attribute_name, []) if callback in watcher_list: return - watcher_list.append((obj, callback)) + watcher_list.append((node, callback)) if init: current_value = getattr(obj, attribute_name, None) Reactive._check_watchers(obj, attribute_name, current_value) diff --git a/src/textual/widgets/_footer.py b/src/textual/widgets/_footer.py index 0b1c9a808..00ccf9e1f 100644 --- a/src/textual/widgets/_footer.py +++ b/src/textual/widgets/_footer.py @@ -8,7 +8,7 @@ from rich.console import RenderableType from rich.text import Text from .. import events -from ..reactive import Reactive, watch +from ..reactive import Reactive from ..widget import Widget @@ -66,7 +66,7 @@ class Footer(Widget): self.refresh() def on_mount(self) -> None: - watch(self.screen, "focused", self._focus_changed) + self.watch(self.screen, "focused", self._focus_changed) def _focus_changed(self, focused: Widget | None) -> None: self._key_text = None diff --git a/src/textual/widgets/_header.py b/src/textual/widgets/_header.py index b2df426cc..d09c4dad8 100644 --- a/src/textual/widgets/_header.py +++ b/src/textual/widgets/_header.py @@ -5,7 +5,7 @@ from datetime import datetime from rich.text import Text from ..widget import Widget -from ..reactive import Reactive, watch +from ..reactive import Reactive class HeaderIcon(Widget): @@ -133,5 +133,5 @@ class Header(Widget): def set_sub_title(sub_title: str) -> None: self.query_one(HeaderTitle).sub_text = sub_title - watch(self.app, "title", set_title) - watch(self.app, "sub_title", set_sub_title) + self.watch(self.app, "title", set_title) + self.watch(self.app, "sub_title", set_sub_title)