From 6bc7e98889583f874aa5fd6ad4bee7da8ec58088 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 16 Dec 2022 09:09:29 +0000 Subject: [PATCH 1/6] fix scroll to top --- src/textual/geometry.py | 20 ++++++++++---------- src/textual/widget.py | 2 +- src/textual/widgets/_tree.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/textual/geometry.py b/src/textual/geometry.py index 26520c6b95..e29790e304 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -320,7 +320,7 @@ def get_scroll_to_visible( Offset: An offset required to add to region to move it inside window_region. """ - if region in window_region: + if region in window_region and not top: # Region is already inside the window, so no need to move it. return NULL_OFFSET @@ -341,19 +341,19 @@ def get_scroll_to_visible( key=abs, ) - if not ( + if top: + delta_y = top_ - window_top + + elif not ( (window_bottom > top_ >= window_top) and (window_bottom > bottom >= window_top) ): # The window needs to scroll on the Y axis to bring region in to view - if top: - delta_y = top_ - window_top - else: - delta_y = min( - top_ - window_top, - top_ - (window_bottom - region.height), - key=abs, - ) + delta_y = min( + top_ - window_top, + top_ - (window_bottom - region.height), + key=abs, + ) return Offset(delta_x, delta_y) def __bool__(self) -> bool: diff --git a/src/textual/widget.py b/src/textual/widget.py index 31344f58cc..7177a17c05 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1805,7 +1805,7 @@ def scroll_to_region( if spacing is not None: window = window.shrink(spacing) - if window in region: + if window in region and not top: return Offset() delta_x, delta_y = Region.get_scroll_to_visible(window, region, top=top) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 7b19dadce3..17c980ddf7 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -805,7 +805,7 @@ async def _on_click(self, event: events.Click) -> None: cursor_line = meta["line"] if meta.get("toggle", False): node = self.get_node_at_line(cursor_line) - if node is not None and self.auto_expand: + if node is not None: self._toggle_node(node) else: From 70cc5472217470a38784f249f00cc516c2da922b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 16 Dec 2022 15:50:47 +0000 Subject: [PATCH 2/6] tree auto width --- src/textual/app.py | 7 +++---- src/textual/css/constants.py | 17 +++++++++-------- src/textual/dom.py | 7 ++++--- src/textual/widget.py | 12 +++++++----- src/textual/widgets/_tree.py | 7 ++++++- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 758afea0ec..3a99ee9fa0 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1080,9 +1080,9 @@ def _get_screen(self, screen: Screen | str) -> tuple[Screen, AwaitMount]: _screen = self.get_screen(screen) if not _screen.is_running: widgets = self._register(self, _screen) - return (_screen, AwaitMount(widgets)) + return (_screen, AwaitMount(_screen, widgets)) else: - return (_screen, AwaitMount([])) + return (_screen, AwaitMount(_screen, [])) def _replace_screen(self, screen: Screen) -> Screen: """Handle the replaced screen. @@ -1128,7 +1128,7 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount: self.screen.post_message_no_wait(events.ScreenResume(self)) self.log.system(f"{self.screen} is current (SWITCHED)") return await_mount - return AwaitMount([]) + return AwaitMount(self.screen, []) def install_screen(self, screen: Screen, name: str | None = None) -> AwaitMount: """Install a screen. @@ -1563,7 +1563,6 @@ def _register( if widget.children: self._register(widget, *widget.children) apply_stylesheet(widget) - return list(widgets) def _unregister(self, widget: Widget) -> None: diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index e181af84a8..48aa851473 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -36,20 +36,21 @@ VALID_TEXT_ALIGN: Final = {"start", "end", "left", "right", "center", "justify"} VALID_SCROLLBAR_GUTTER: Final = {"auto", "stable"} VALID_STYLE_FLAGS: Final = { - "none", - "not", - "bold", + "b", "blink", + "bold", + "dim", + "i", "italic", - "underline", + "none", + "not", + "o", "overline", + "reverse", "strike", - "b", - "i", "u", + "underline", "uu", - "o", - "reverse", } NULL_SPACING: Final = Spacing.all(0) diff --git a/src/textual/dom.py b/src/textual/dom.py index 49057b67dc..9e83d45945 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -507,8 +507,8 @@ def text_style(self) -> Style: @property def rich_style(self) -> Style: """Get a Rich Style object for this DOMNode.""" - background = WHITE - color = BLACK + background = Color(0, 0, 0, 0) + color = Color(255, 255, 255, 0) style = Style() for node in reversed(self.ancestors_with_self): styles = node.styles @@ -520,7 +520,8 @@ def rich_style(self) -> Style: if styles.has_rule("auto_color") and styles.auto_color: color = background.get_contrast_text(color.a) style += Style.from_color( - (background + color).rich_color, background.rich_color + (background + color).rich_color if (background.a or color.a) else None, + background.rich_color if background.a else None, ) return style diff --git a/src/textual/widget.py b/src/textual/widget.py index 7177a17c05..1d5f4365bc 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -85,7 +85,8 @@ class AwaitMount: """ - def __init__(self, widgets: Sequence[Widget]) -> None: + def __init__(self, parent: Widget, widgets: Sequence[Widget]) -> None: + self._parent = parent self._widgets = widgets def __await__(self) -> Generator[None, None, None]: @@ -97,6 +98,7 @@ async def await_mount() -> None: ] if aws: await wait(aws) + self._parent.refresh(layout=True) return await_mount().__await__() @@ -595,12 +597,12 @@ def mount( else: parent = self - return AwaitMount( - self.app._register( - parent, *widgets, before=insert_before, after=insert_after - ) + mounted = self.app._register( + parent, *widgets, before=insert_before, after=insert_after ) + return AwaitMount(self, mounted) + def move_child( self, child: int | Widget, diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 17c980ddf7..7619112d86 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -614,6 +614,11 @@ def _tree_lines(self) -> list[_TreeLine]: assert self._tree_lines_cached is not None return self._tree_lines_cached + def _on_idle(self) -> None: + """Check tree needs a rebuild on idle.""" + # Property calls build if required + self._tree_lines + def _build(self) -> None: """Builds the tree by traversing nodes, and creating tree lines.""" @@ -805,7 +810,7 @@ async def _on_click(self, event: events.Click) -> None: cursor_line = meta["line"] if meta.get("toggle", False): node = self.get_node_at_line(cursor_line) - if node is not None: + if node is not None and self.auto_expand: self._toggle_node(node) else: From 70d4ebc316d4df4371d0a2b0817cce3579626af3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 16 Dec 2022 20:19:01 +0000 Subject: [PATCH 3/6] Added message to exit method --- src/textual/_compositor.py | 2 +- src/textual/app.py | 7 ++++++- src/textual/widgets/_tree.py | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 75c7b973f1..ec350ebfe2 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -449,7 +449,7 @@ def add_widget( map[widget] = MapGeometry( region + layout_offset, order, - clip, + clip if widget.is_container else sub_clip, total_region.size, container_size, virtual_region, diff --git a/src/textual/app.py b/src/textual/app.py index 3a99ee9fa0..4e0c244573 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -414,14 +414,19 @@ def screen_stack(self) -> list[Screen]: """list[Screen]: A *copy* of the screen stack.""" return self._screen_stack.copy() - def exit(self, result: ReturnType | None = None) -> None: + def exit( + self, result: ReturnType | None = None, message: RenderableType | None = None + ) -> None: """Exit the app, and return the supplied result. Args: result (ReturnType | None, optional): Return value. Defaults to None. + message (RenderableType | None): Optional message to display on exit. """ self._return_value = result self.post_message_no_wait(messages.ExitApp(sender=self)) + if message: + self._exit_renderables.append(message) @property def focused(self) -> Widget | None: diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 7619112d86..de61976be3 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -451,6 +451,7 @@ def get_label_width(self, node: TreeNode[TreeDataType]) -> int: def clear(self) -> None: """Clear all nodes under root.""" + self._line_cache.clear() self._tree_lines_cached = None self._current_id = 0 root_label = self.root._label @@ -810,7 +811,7 @@ async def _on_click(self, event: events.Click) -> None: cursor_line = meta["line"] if meta.get("toggle", False): node = self.get_node_at_line(cursor_line) - if node is not None and self.auto_expand: + if node is not None: self._toggle_node(node) else: From 124df593614f9c9689051efd54cf4196cf320cc5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 16 Dec 2022 20:53:11 +0000 Subject: [PATCH 4/6] handle exit from load --- CHANGELOG.md | 1 + src/textual/app.py | 43 +++++++++++++++++++++++-------------------- src/textual/screen.py | 2 ++ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf376002b..6d7aa32510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - When looking for bindings that have priority, they are now looked from `App` downwards. https://github.com/Textualize/textual/issues/1343 - `BINDINGS` on an `App`-derived class have priority by default. https://github.com/Textualize/textual/issues/1343 - `BINDINGS` on a `Screen`-derived class have priority by default. https://github.com/Textualize/textual/issues/1343 +- Added a message parameter to Widget.exit ### Fixed diff --git a/src/textual/app.py b/src/textual/app.py index c034a23284..1b95b84001 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -350,6 +350,7 @@ def __init__( self.devtools = DevtoolsClient() self._return_value: ReturnType | None = None + self._exit = False self.css_monitor = ( FileMonitor(self.css_path, self._on_css_change) @@ -425,6 +426,7 @@ def exit( result (ReturnType | None, optional): Return value. Defaults to None. message (RenderableType | None): Optional message to display on exit. """ + self._exit = True self._return_value = result self.post_message_no_wait(messages.ExitApp(sender=self)) if message: @@ -1413,28 +1415,29 @@ async def invoke_ready_callback() -> None: ) driver = self._driver = driver_class(self.console, self, size=terminal_size) - driver.start_application_mode() - try: - if headless: - await run_process_messages() - else: - if self.devtools is not None: - devtools = self.devtools - assert devtools is not None - from .devtools.redirect_output import StdoutRedirector - - redirector = StdoutRedirector(devtools) - with redirect_stderr(redirector): - with redirect_stdout(redirector): # type: ignore - await run_process_messages() + if not self._exit: + driver.start_application_mode() + try: + if headless: + await run_process_messages() else: - null_file = _NullFile() - with redirect_stderr(null_file): - with redirect_stdout(null_file): - await run_process_messages() + if self.devtools is not None: + devtools = self.devtools + assert devtools is not None + from .devtools.redirect_output import StdoutRedirector + + redirector = StdoutRedirector(devtools) + with redirect_stderr(redirector): + with redirect_stdout(redirector): # type: ignore + await run_process_messages() + else: + null_file = _NullFile() + with redirect_stderr(null_file): + with redirect_stdout(null_file): + await run_process_messages() - finally: - driver.stop_application_mode() + finally: + driver.stop_application_mode() except Exception as error: self._handle_exception(error) diff --git a/src/textual/screen.py b/src/textual/screen.py index c08bd20b4e..0de76479e4 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -297,6 +297,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: self.focused.post_message_no_wait(events.Blur(self)) self.focused.emit_no_wait(events.DescendantBlur(self)) self.focused = None + self.log.debug("focus was removed") elif widget.can_focus: if self.focused != widget: if self.focused is not None: @@ -310,6 +311,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: self.screen.scroll_to_widget(widget) widget.post_message_no_wait(events.Focus(self)) widget.emit_no_wait(events.DescendantFocus(self)) + self.log.debug(widget, "was focused") async def _on_idle(self, event: events.Idle) -> None: # Check for any widgets marked as 'dirty' (needs a repaint) From df1bcd5331246950709504eaa3b6877e4dc0f151 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 17 Dec 2022 09:20:30 +0000 Subject: [PATCH 5/6] Fix bindings action --- src/textual/_compositor.py | 2 +- src/textual/app.py | 6 +++--- src/textual/binding.py | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index ec350ebfe2..75c7b973f1 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -449,7 +449,7 @@ def add_widget( map[widget] = MapGeometry( region + layout_offset, order, - clip if widget.is_container else sub_clip, + clip, total_region.size, container_size, virtual_region, diff --git a/src/textual/app.py b/src/textual/app.py index 1b95b84001..a6b7ad27aa 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1742,13 +1742,12 @@ async def check_bindings(self, key: str, priority: bool = False) -> bool: """Handle a key press. Args: - key (str): A key + key (str): A key. priority (bool): If `True` check from `App` down, otherwise from focused up. Returns: bool: True if the key was handled by a binding, otherwise False """ - for namespace, bindings in ( reversed(self._binding_chain) if priority else self._binding_chain ): @@ -2053,7 +2052,8 @@ async def _prune_node(self, root: Widget) -> None: self._unregister(root) async def action_check_bindings(self, key: str) -> None: - await self.check_bindings(key) + if not await self.check_bindings(key, priority=True): + await self.check_bindings(key, priority=False) async def action_quit(self) -> None: """Quit the app as soon as possible.""" diff --git a/src/textual/binding.py b/src/textual/binding.py index 7b34f12f07..7170d3dfa5 100644 --- a/src/textual/binding.py +++ b/src/textual/binding.py @@ -77,9 +77,11 @@ def make_bindings(bindings: Iterable[BindingType]) -> Iterable[Binding]: description=binding.description, show=binding.show, key_display=binding.key_display, - priority=default_priority - if binding.priority is None - else binding.priority, + priority=( + default_priority + if binding.priority is None + else binding.priority + ), ) self.keys: MutableMapping[str, Binding] = ( From d264ebb4c8fb395dafed070ea855b76e4212e09f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 17 Dec 2022 09:25:38 +0000 Subject: [PATCH 6/6] version bump --- CHANGELOG.md | 5 +++-- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7aa32510..e5d964375d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## Unreleased +## [0.7.0] - 2022-12-17 ### Added @@ -266,7 +266,8 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling -[0.6.0]: https://github.com/Textualize/textual/compare/v0.3.0...v0.6.0 +[0.7.0]: https://github.com/Textualize/textual/compare/v0.6.0...v0.7.0 +[0.6.0]: https://github.com/Textualize/textual/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/Textualize/textual/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/Textualize/textual/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/Textualize/textual/compare/v0.2.1...v0.3.0 diff --git a/pyproject.toml b/pyproject.toml index 63ae6f2ee8..91404e3b42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.6.0" +version = "0.7.0" homepage = "https://github.com/Textualize/textual" description = "Modern Text User Interface framework" authors = ["Will McGugan "]