call later

This commit is contained in:
Will McGugan
2022-11-09 17:23:28 +00:00
parent 51d5e7db0c
commit 39a764f49f
9 changed files with 88 additions and 22 deletions

View File

@@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed ### Changed
- Watchers are now called immediately when setting the attribute if they are synchronous. https://github.com/Textualize/textual/pull/1145 - 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 ## [0.4.0] - 2022-11-08

View File

@@ -23,7 +23,7 @@ class BindingApp(App):
bar = Bar(color) bar = Bar(color)
bar.styles.background = Color.parse(color).with_alpha(0.5) bar.styles.background = Color.parse(color).with_alpha(0.5)
self.mount(bar) 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__": if __name__ == "__main__":

View File

@@ -1606,19 +1606,28 @@ class App(Generic[ReturnType], DOMNode):
screen (Screen): Screen instance screen (Screen): Screen instance
renderable (RenderableType): A Rich renderable. renderable (RenderableType): A Rich renderable.
""" """
if screen is not self.screen or renderable is None:
return try:
if self._running and not self._closed and not self.is_headless: if screen is not self.screen or renderable is None:
console = self.console return
self._begin_update()
try: if self._running and not self._closed and not self.is_headless:
console = self.console
self._begin_update()
try: try:
console.print(renderable) try:
except Exception as error: print(renderable)
self._handle_exception(error) console.print(renderable)
finally: except Exception as error:
self._end_update() self._handle_exception(error)
console.file.flush() 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]: def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
"""Get the widget under the given coordinates. """Get the widget under the given coordinates.

View File

@@ -71,7 +71,7 @@ class ColorsApp(App):
yield Footer() yield Footer()
def on_mount(self) -> None: def on_mount(self) -> None:
self.call_later(self.update_view) self.call_after_refresh(self.update_view)
def update_view(self) -> None: def update_view(self) -> None:
content = self.query_one("Content", Content) content = self.query_one("Content", Content)

View File

@@ -251,7 +251,7 @@ class MessagePump(metaclass=MessagePumpMeta):
self._timers.add(timer) self._timers.add(timer)
return 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 """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. 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)) message = messages.InvokeLater(self, partial(callback, *args, **kwargs))
self.post_message_no_wait(message) 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: def _on_invoke_later(self, message: messages.InvokeLater) -> None:
# Forward InvokeLater message to the Screen # Forward InvokeLater message to the Screen
self.app.screen._invoke_later(message.callback) self.app.screen._invoke_later(message.callback)

View File

@@ -333,11 +333,11 @@ class Screen(Widget):
self._compositor.update_widgets(self._dirty_widgets) self._compositor.update_widgets(self._dirty_widgets)
self.app._display(self, self._compositor.render()) self.app._display(self, self._compositor.render())
self._dirty_widgets.clear() self._dirty_widgets.clear()
self.update_timer.pause()
if self._callbacks: if self._callbacks:
self.post_message_no_wait(events.InvokeCallbacks(self)) self.post_message_no_wait(events.InvokeCallbacks(self))
self.update_timer.pause()
async def _on_invoke_callbacks(self, event: events.InvokeCallbacks) -> None: async def _on_invoke_callbacks(self, event: events.InvokeCallbacks) -> None:
"""Handle PostScreenUpdate events, which are sent after the screen is updated""" """Handle PostScreenUpdate events, which are sent after the screen is updated"""
await self._invoke_and_clear_callbacks() await self._invoke_and_clear_callbacks()
@@ -346,6 +346,8 @@ class Screen(Widget):
"""If there are scheduled callbacks to run, call them and clear """If there are scheduled callbacks to run, call them and clear
the callback queue.""" the callback queue."""
if self._callbacks: if self._callbacks:
display_update = self._compositor.render(full=True)
self.app._display(self, display_update)
callbacks = self._callbacks[:] callbacks = self._callbacks[:]
self._callbacks.clear() self._callbacks.clear()
for callback in callbacks: for callback in callbacks:
@@ -402,8 +404,7 @@ class Screen(Widget):
self.app._handle_exception(error) self.app._handle_exception(error)
return return
display_update = self._compositor.render(full=full) 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: async def _on_update(self, message: messages.Update) -> None:
message.stop() message.stop()

View File

@@ -1576,7 +1576,7 @@ class Widget(DOMNode):
""" """
parent = self.parent parent = self.parent
if isinstance(parent, Widget): if isinstance(parent, Widget):
self.call_later( self.call_after_refresh(
parent.scroll_to_widget, parent.scroll_to_widget,
self, self,
animate=animate, animate=animate,
@@ -1989,7 +1989,7 @@ class Widget(DOMNode):
except NoScreen: except NoScreen:
pass pass
self.app.call_later(set_focus, self) self.app.call_after_refresh(set_focus, self)
def reset_focus(self) -> None: def reset_focus(self) -> None:
"""Reset the focus (move it to the next available widget).""" """Reset the focus (move it to the next available widget)."""

View File

@@ -92,7 +92,7 @@ class DirectoryTree(TreeControl[DirEntry]):
self.render_tree_label.cache_clear() self.render_tree_label.cache_clear()
def on_mount(self) -> None: 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]): async def load_directory(self, node: TreeNode[DirEntry]):
path = node.data.path path = node.data.path

41
tests/test_call_later.py Normal file
View 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