mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
comments, set_focus, and screen resize
This commit is contained in:
@@ -30,7 +30,7 @@ class CodeBrowser(App):
|
||||
|
||||
def watch_show_tree(self, show_tree: bool) -> None:
|
||||
"""Called when show_tree is modified."""
|
||||
self.set_class(show_tree, "-show-tree")
|
||||
self.set_class(show_tree, "-show-tree")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose our UI."""
|
||||
|
||||
@@ -15,7 +15,7 @@ Input {
|
||||
|
||||
#results-container {
|
||||
background: $background 50%;
|
||||
margin: 0;
|
||||
margin: 0 0 1 0;
|
||||
height: 100%;
|
||||
overflow: hidden auto;
|
||||
border: tall $background;
|
||||
|
||||
@@ -37,7 +37,7 @@ from .css.stylesheet import Stylesheet
|
||||
from .design import ColorSystem
|
||||
from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog
|
||||
from .devtools.redirect_output import StdoutRedirector
|
||||
from .dom import DOMNode, NoScreen
|
||||
from .dom import DOMNode
|
||||
from .driver import Driver
|
||||
from .drivers.headless_driver import HeadlessDriver
|
||||
from .features import FeatureFlag, parse_features
|
||||
@@ -1014,15 +1014,18 @@ class App(Generic[ReturnType], DOMNode):
|
||||
process_messages = super()._process_messages
|
||||
|
||||
async def run_process_messages():
|
||||
Reactive.initialize_object(self)
|
||||
|
||||
compose_event = events.Compose(sender=self)
|
||||
await self._dispatch_message(compose_event)
|
||||
mount_event = events.Mount(sender=self)
|
||||
await self._dispatch_message(mount_event)
|
||||
|
||||
Reactive._initialize_object(self)
|
||||
|
||||
self.title = self._title
|
||||
self.stylesheet.update(self)
|
||||
self.refresh()
|
||||
|
||||
await self.animator.start()
|
||||
await self._ready()
|
||||
if ready_callback is not None:
|
||||
@@ -1166,6 +1169,14 @@ class App(Generic[ReturnType], DOMNode):
|
||||
widget._start_messages()
|
||||
|
||||
def is_mounted(self, widget: Widget) -> bool:
|
||||
"""Check if a widget is mounted.
|
||||
|
||||
Args:
|
||||
widget (Widget): A widget.
|
||||
|
||||
Returns:
|
||||
bool: True of the widget is mounted.
|
||||
"""
|
||||
return widget in self._registry
|
||||
|
||||
async def _close_all(self) -> None:
|
||||
@@ -1195,7 +1206,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
stylesheet.set_variables(self.get_css_variables())
|
||||
stylesheet.reparse()
|
||||
stylesheet.update(self.app, animate=animate)
|
||||
self.screen._refresh_layout(self.size, full=True)
|
||||
# self.screen._refresh_layout(self.size, full=True)
|
||||
|
||||
def _display(self, screen: Screen, renderable: RenderableType | None) -> None:
|
||||
"""Display a renderable within a sync.
|
||||
@@ -1387,7 +1398,6 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
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:
|
||||
|
||||
@@ -72,6 +72,9 @@ class ColorsApp(App):
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.call_later(self.update_view)
|
||||
|
||||
def update_view(self) -> None:
|
||||
content = self.query_one("Content", Content)
|
||||
content.mount(ColorsView())
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ class Stylesheet:
|
||||
css = css_file.read()
|
||||
path = os.path.abspath(filename)
|
||||
except Exception as error:
|
||||
raise StylesheetError(f"unable to read {filename!r}; {error}")
|
||||
raise StylesheetError(f"unable to read CSS file {filename!r}") from None
|
||||
self.source[str(path)] = CssSource(css, False, 0)
|
||||
self._require_parse = True
|
||||
|
||||
|
||||
@@ -326,6 +326,9 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
except MessagePumpClosed:
|
||||
break
|
||||
|
||||
is_mount = isinstance(message, events.Mount)
|
||||
if is_mount:
|
||||
Reactive._initialize_object(self)
|
||||
try:
|
||||
await self._dispatch_message(message)
|
||||
except CancelledError:
|
||||
@@ -335,10 +338,13 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
self.app._handle_exception(error)
|
||||
break
|
||||
finally:
|
||||
if isinstance(message, events.Mount):
|
||||
if is_mount:
|
||||
self._mounted_event.set()
|
||||
|
||||
self._message_queue.task_done()
|
||||
current_time = time()
|
||||
|
||||
# Insert idle events
|
||||
if self._message_queue.empty() or (
|
||||
self._max_idle is not None
|
||||
and current_time - self._last_idle > self._max_idle
|
||||
|
||||
@@ -20,6 +20,12 @@ if TYPE_CHECKING:
|
||||
ReactiveType = TypeVar("ReactiveType")
|
||||
|
||||
|
||||
class _NotSet:
|
||||
pass
|
||||
|
||||
|
||||
_NOT_SET = _NotSet()
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@@ -83,26 +89,31 @@ class Reactive(Generic[ReactiveType]):
|
||||
return cls(default, layout=False, repaint=False, init=True)
|
||||
|
||||
@classmethod
|
||||
def initialize_object(cls, obj: object) -> None:
|
||||
"""Call any watchers / computes for the first time.
|
||||
def _initialize_object(cls, obj: object) -> None:
|
||||
"""Set defaults and call any watchers / computes for the first time.
|
||||
|
||||
Args:
|
||||
obj (Reactable): An object with Reactive descriptors
|
||||
"""
|
||||
if not hasattr(obj, "__reactive_initialized"):
|
||||
startswith = str.startswith
|
||||
for key in obj.__class__.__dict__.keys():
|
||||
if startswith(key, "_init_"):
|
||||
name = key[6:]
|
||||
for key in obj.__class__.__dict__:
|
||||
if startswith(key, "_default_"):
|
||||
name = key[9:]
|
||||
# Check defaults
|
||||
if not hasattr(obj, name):
|
||||
# Attribute has no value yet
|
||||
default = getattr(obj, key)
|
||||
default_value = default() if callable(default) else default
|
||||
# Set the default vale (calls `__set__`)
|
||||
setattr(obj, name, default_value)
|
||||
setattr(obj, "__reactive_initialized", True)
|
||||
|
||||
def __set_name__(self, owner: Type[MessageTarget], name: str) -> None:
|
||||
|
||||
# Check for compute method
|
||||
if hasattr(owner, f"compute_{name}"):
|
||||
# Compute methods are stored in a list called `__computes`
|
||||
try:
|
||||
computes = getattr(owner, "__computes")
|
||||
except AttributeError:
|
||||
@@ -110,31 +121,46 @@ class Reactive(Generic[ReactiveType]):
|
||||
setattr(owner, "__computes", computes)
|
||||
computes.append(name)
|
||||
|
||||
# The name of the attribute
|
||||
self.name = name
|
||||
# The internal name where the attribute's value is stored
|
||||
self.internal_name = f"_reactive_{name}"
|
||||
default = self._default
|
||||
setattr(owner, f"_init_{name}", default)
|
||||
setattr(owner, f"_default_{name}", default)
|
||||
|
||||
def __get__(self, obj: Reactable, obj_type: type[object]) -> ReactiveType:
|
||||
if not hasattr(obj, self.internal_name):
|
||||
init_name = f"_init_{self.name}"
|
||||
value: _NotSet | ReactiveType = getattr(obj, self.internal_name, _NOT_SET)
|
||||
if isinstance(value, _NotSet):
|
||||
# No value present, we need to set the default
|
||||
init_name = f"_default_{self.name}"
|
||||
default = getattr(obj, init_name)
|
||||
default_value = default() if callable(default) else default
|
||||
# Set and return the value
|
||||
setattr(obj, self.internal_name, default_value)
|
||||
if self._init:
|
||||
self._check_watchers(obj, self.name, default_value, first_set=True)
|
||||
return default_value
|
||||
return getattr(obj, self.internal_name)
|
||||
return value
|
||||
|
||||
def __set__(self, obj: Reactable, value: ReactiveType) -> None:
|
||||
name = self.name
|
||||
current_value = getattr(obj, self.name)
|
||||
current_value = getattr(obj, name)
|
||||
# Check for validate function
|
||||
validate_function = getattr(obj, f"validate_{name}", None)
|
||||
# Check if this is the first time setting the value
|
||||
first_set = getattr(obj, f"__first_set_{self.internal_name}", True)
|
||||
# Call validate, but not on first set.
|
||||
if callable(validate_function) and not first_set:
|
||||
value = validate_function(value)
|
||||
# If the value has changed, or this is the first time setting the value
|
||||
if current_value != value or first_set:
|
||||
# Set the first set flag to False
|
||||
setattr(obj, f"__first_set_{self.internal_name}", False)
|
||||
# Store the internal value
|
||||
setattr(obj, self.internal_name, value)
|
||||
# Check all watchers
|
||||
self._check_watchers(obj, name, current_value, first_set=first_set)
|
||||
# Refresh according to descriptor flags
|
||||
if self._layout or self._repaint:
|
||||
obj.refresh(repaint=self._repaint, layout=self._layout)
|
||||
|
||||
@@ -142,50 +168,77 @@ class Reactive(Generic[ReactiveType]):
|
||||
def _check_watchers(
|
||||
cls, obj: Reactable, name: str, old_value: Any, first_set: bool = False
|
||||
) -> None:
|
||||
"""Check watchers, and call watch methods / computes
|
||||
|
||||
Args:
|
||||
obj (Reactable): The reactable object.
|
||||
name (str): Attribute name.
|
||||
old_value (Any): The old (previous) value of the attribute.
|
||||
first_set (bool, optional): True if this is the first time setting the value. Defaults to False.
|
||||
"""
|
||||
# Get the current value.
|
||||
internal_name = f"_reactive_{name}"
|
||||
value = getattr(obj, internal_name)
|
||||
|
||||
async def update_watcher(
|
||||
obj: Reactable, watch_function: Callable, old_value: Any, value: Any
|
||||
) -> None:
|
||||
"""Call watch function, and run compute.
|
||||
|
||||
Args:
|
||||
obj (Reactable): Reactable object.
|
||||
watch_function (Callable): Watch method.
|
||||
old_value (Any): Old value.
|
||||
value (Any): new value.
|
||||
"""
|
||||
_rich_traceback_guard = True
|
||||
# Call watch with one or two parameters
|
||||
if count_parameters(watch_function) == 2:
|
||||
watch_result = watch_function(old_value, value)
|
||||
else:
|
||||
watch_result = watch_function(value)
|
||||
# Optionally await result
|
||||
if isawaitable(watch_result):
|
||||
await watch_result
|
||||
# Run computes
|
||||
await Reactive._compute(obj)
|
||||
|
||||
# Check for watch method
|
||||
watch_function = getattr(obj, f"watch_{name}", None)
|
||||
if callable(watch_function):
|
||||
# Post a callback message, so we can call the watch method in an orderly async manner
|
||||
obj.post_message_no_wait(
|
||||
events.Callback(
|
||||
obj,
|
||||
sender=obj,
|
||||
callback=partial(
|
||||
update_watcher, obj, watch_function, old_value, value
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Check for watchers set via `watch`
|
||||
watcher_name = f"__{name}_watchers"
|
||||
watchers = getattr(obj, watcher_name, ())
|
||||
for watcher in watchers:
|
||||
obj.post_message_no_wait(
|
||||
events.Callback(
|
||||
obj,
|
||||
sender=obj,
|
||||
callback=partial(update_watcher, obj, watcher, old_value, value),
|
||||
)
|
||||
)
|
||||
|
||||
if not first_set:
|
||||
obj.post_message_no_wait(
|
||||
events.Callback(obj, callback=partial(Reactive._compute, obj))
|
||||
)
|
||||
# Run computes
|
||||
obj.post_message_no_wait(
|
||||
events.Callback(sender=obj, callback=partial(Reactive._compute, obj))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def _compute(cls, obj: Reactable) -> None:
|
||||
"""Invoke all computes.
|
||||
|
||||
Args:
|
||||
obj (Reactable): Reactable object.
|
||||
"""
|
||||
_rich_traceback_guard = True
|
||||
computes = getattr(obj, "__computes", [])
|
||||
for compute in computes:
|
||||
|
||||
@@ -387,12 +387,12 @@ class Screen(Widget):
|
||||
|
||||
def _on_screen_resume(self) -> None:
|
||||
"""Called by the App"""
|
||||
|
||||
size = self.app.size
|
||||
self._refresh_layout(size, full=True)
|
||||
|
||||
async def _on_resize(self, event: events.Resize) -> None:
|
||||
event.stop()
|
||||
self._screen_resized(event.size)
|
||||
|
||||
async def _handle_mouse_move(self, event: events.MouseMove) -> None:
|
||||
try:
|
||||
|
||||
@@ -336,7 +336,8 @@ class Widget(DOMNode):
|
||||
|
||||
"""
|
||||
self.app._register(self, *anon_widgets, **widgets)
|
||||
self.app.screen.refresh(layout=True)
|
||||
# self.app.screen.refresh(layout=True)
|
||||
# self.refresh(layout=True)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Called by Textual to create child widgets.
|
||||
@@ -1820,7 +1821,10 @@ class Widget(DOMNode):
|
||||
visible. Defaults to True.
|
||||
"""
|
||||
|
||||
self.screen.set_focus(self, scroll_visible=scroll_visible)
|
||||
def set_focus(widget: Widget):
|
||||
widget.screen.set_focus(self, scroll_visible=scroll_visible)
|
||||
|
||||
self.app.call_later(set_focus, self)
|
||||
|
||||
def reset_focus(self) -> None:
|
||||
"""Reset the focus (move it to the next available widget)."""
|
||||
|
||||
Reference in New Issue
Block a user