mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
call later
This commit is contained in:
@@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
### Changed
|
||||
|
||||
- Watchers are now called immediately when setting the attribute if they are synchronous. https://github.com/Textualize/textual/pull/1145
|
||||
- Widget.call_later has been renamed to Widget.call_after_refresh.
|
||||
|
||||
### Added
|
||||
|
||||
- Added Widget.call_later which invokes a callback on idle.
|
||||
|
||||
## [0.4.0] - 2022-11-08
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class BindingApp(App):
|
||||
bar = Bar(color)
|
||||
bar.styles.background = Color.parse(color).with_alpha(0.5)
|
||||
self.mount(bar)
|
||||
self.call_later(self.screen.scroll_end, animate=False)
|
||||
self.call_after_refresh(self.screen.scroll_end, animate=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1606,19 +1606,28 @@ class App(Generic[ReturnType], DOMNode):
|
||||
screen (Screen): Screen instance
|
||||
renderable (RenderableType): A Rich renderable.
|
||||
"""
|
||||
if screen is not self.screen or renderable is None:
|
||||
return
|
||||
if self._running and not self._closed and not self.is_headless:
|
||||
console = self.console
|
||||
self._begin_update()
|
||||
try:
|
||||
|
||||
try:
|
||||
if screen is not self.screen or renderable is None:
|
||||
return
|
||||
|
||||
if self._running and not self._closed and not self.is_headless:
|
||||
console = self.console
|
||||
self._begin_update()
|
||||
try:
|
||||
console.print(renderable)
|
||||
except Exception as error:
|
||||
self._handle_exception(error)
|
||||
finally:
|
||||
self._end_update()
|
||||
console.file.flush()
|
||||
try:
|
||||
print(renderable)
|
||||
console.print(renderable)
|
||||
except Exception as error:
|
||||
self._handle_exception(error)
|
||||
finally:
|
||||
self._end_update()
|
||||
console.file.flush()
|
||||
finally:
|
||||
self.post_display_hook()
|
||||
|
||||
def post_display_hook(self) -> None:
|
||||
"""Called immediately after a display is done. Used in tests."""
|
||||
|
||||
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
||||
"""Get the widget under the given coordinates.
|
||||
|
||||
@@ -71,7 +71,7 @@ class ColorsApp(App):
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.call_later(self.update_view)
|
||||
self.call_after_refresh(self.update_view)
|
||||
|
||||
def update_view(self) -> None:
|
||||
content = self.query_one("Content", Content)
|
||||
|
||||
@@ -251,7 +251,7 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
self._timers.add(timer)
|
||||
return timer
|
||||
|
||||
def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
def call_after_refresh(self, callback: Callable, *args, **kwargs) -> None:
|
||||
"""Schedule a callback to run after all messages are processed and the screen
|
||||
has been refreshed. Positional and keyword arguments are passed to the callable.
|
||||
|
||||
@@ -263,6 +263,16 @@ class MessagePump(metaclass=MessagePumpMeta):
|
||||
message = messages.InvokeLater(self, partial(callback, *args, **kwargs))
|
||||
self.post_message_no_wait(message)
|
||||
|
||||
def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
"""Schedule a callback to run after all messages are processed in this object.
|
||||
Positional and keywords arguments are passed to the callable.
|
||||
|
||||
Args:
|
||||
callback (Callable): Callable to call next.
|
||||
"""
|
||||
message = events.Callback(self, callback=partial(callback, *args, **kwargs))
|
||||
self.post_message_no_wait(message)
|
||||
|
||||
def _on_invoke_later(self, message: messages.InvokeLater) -> None:
|
||||
# Forward InvokeLater message to the Screen
|
||||
self.app.screen._invoke_later(message.callback)
|
||||
|
||||
@@ -333,11 +333,11 @@ class Screen(Widget):
|
||||
self._compositor.update_widgets(self._dirty_widgets)
|
||||
self.app._display(self, self._compositor.render())
|
||||
self._dirty_widgets.clear()
|
||||
|
||||
self.update_timer.pause()
|
||||
if self._callbacks:
|
||||
self.post_message_no_wait(events.InvokeCallbacks(self))
|
||||
|
||||
self.update_timer.pause()
|
||||
|
||||
async def _on_invoke_callbacks(self, event: events.InvokeCallbacks) -> None:
|
||||
"""Handle PostScreenUpdate events, which are sent after the screen is updated"""
|
||||
await self._invoke_and_clear_callbacks()
|
||||
@@ -346,6 +346,8 @@ class Screen(Widget):
|
||||
"""If there are scheduled callbacks to run, call them and clear
|
||||
the callback queue."""
|
||||
if self._callbacks:
|
||||
display_update = self._compositor.render(full=True)
|
||||
self.app._display(self, display_update)
|
||||
callbacks = self._callbacks[:]
|
||||
self._callbacks.clear()
|
||||
for callback in callbacks:
|
||||
@@ -402,8 +404,7 @@ class Screen(Widget):
|
||||
self.app._handle_exception(error)
|
||||
return
|
||||
display_update = self._compositor.render(full=full)
|
||||
if display_update is not None:
|
||||
self.app._display(self, display_update)
|
||||
self.app._display(self, display_update)
|
||||
|
||||
async def _on_update(self, message: messages.Update) -> None:
|
||||
message.stop()
|
||||
|
||||
@@ -1576,7 +1576,7 @@ class Widget(DOMNode):
|
||||
"""
|
||||
parent = self.parent
|
||||
if isinstance(parent, Widget):
|
||||
self.call_later(
|
||||
self.call_after_refresh(
|
||||
parent.scroll_to_widget,
|
||||
self,
|
||||
animate=animate,
|
||||
@@ -1989,7 +1989,7 @@ class Widget(DOMNode):
|
||||
except NoScreen:
|
||||
pass
|
||||
|
||||
self.app.call_later(set_focus, self)
|
||||
self.app.call_after_refresh(set_focus, self)
|
||||
|
||||
def reset_focus(self) -> None:
|
||||
"""Reset the focus (move it to the next available widget)."""
|
||||
|
||||
@@ -92,7 +92,7 @@ class DirectoryTree(TreeControl[DirEntry]):
|
||||
self.render_tree_label.cache_clear()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.call_later(self.load_directory, self.root)
|
||||
self.call_after_refresh(self.load_directory, self.root)
|
||||
|
||||
async def load_directory(self, node: TreeNode[DirEntry]):
|
||||
path = node.data.path
|
||||
|
||||
41
tests/test_call_later.py
Normal file
41
tests/test_call_later.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import asyncio
|
||||
from textual.app import App
|
||||
|
||||
|
||||
class CallLaterApp(App[None]):
|
||||
def __init__(self) -> None:
|
||||
self.display_count = 0
|
||||
super().__init__()
|
||||
|
||||
def post_display_hook(self) -> None:
|
||||
self.display_count += 1
|
||||
|
||||
|
||||
async def test_call_later() -> None:
|
||||
"""Check that call later makes a call."""
|
||||
app = CallLaterApp()
|
||||
called_event = asyncio.Event()
|
||||
|
||||
async with app.run_test():
|
||||
app.call_later(called_event.set)
|
||||
await asyncio.wait_for(called_event.wait(), 1)
|
||||
|
||||
|
||||
async def test_call_after_refresh() -> None:
|
||||
"""Check that call later makes a call after a refresh."""
|
||||
app = CallLaterApp()
|
||||
|
||||
display_count = -1
|
||||
|
||||
called_event = asyncio.Event()
|
||||
|
||||
def callback() -> None:
|
||||
nonlocal display_count
|
||||
called_event.set()
|
||||
display_count = app.display_count
|
||||
|
||||
async with app.run_test():
|
||||
app.call_after_refresh(callback)
|
||||
await asyncio.wait_for(called_event.wait(), 1)
|
||||
app_display_count = app.display_count
|
||||
assert app_display_count > display_count
|
||||
Reference in New Issue
Block a user