From 39a764f49fff7ec3363b8ea25fce3fbf1b67ca58 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 9 Nov 2022 17:23:28 +0000 Subject: [PATCH] call later --- CHANGELOG.md | 5 ++++ docs/examples/guide/input/binding01.py | 2 +- src/textual/app.py | 33 +++++++++++++-------- src/textual/cli/previews/colors.py | 2 +- src/textual/message_pump.py | 12 +++++++- src/textual/screen.py | 9 +++--- src/textual/widget.py | 4 +-- src/textual/widgets/_directory_tree.py | 2 +- tests/test_call_later.py | 41 ++++++++++++++++++++++++++ 9 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 tests/test_call_later.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 047026758..c5f0f3504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/examples/guide/input/binding01.py b/docs/examples/guide/input/binding01.py index 12711f637..75458bd19 100644 --- a/docs/examples/guide/input/binding01.py +++ b/docs/examples/guide/input/binding01.py @@ -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__": diff --git a/src/textual/app.py b/src/textual/app.py index dba40a935..f85b6c021 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -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. diff --git a/src/textual/cli/previews/colors.py b/src/textual/cli/previews/colors.py index 9315c47ca..56ac645c6 100644 --- a/src/textual/cli/previews/colors.py +++ b/src/textual/cli/previews/colors.py @@ -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) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 29f2d07ef..615ec0742 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -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) diff --git a/src/textual/screen.py b/src/textual/screen.py index c06c86936..7d9c74609 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -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() diff --git a/src/textual/widget.py b/src/textual/widget.py index b4723487c..1a3537ad3 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -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).""" diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 7332a1d16..4961db81d 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -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 diff --git a/tests/test_call_later.py b/tests/test_call_later.py new file mode 100644 index 000000000..025eb8c12 --- /dev/null +++ b/tests/test_call_later.py @@ -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