From 5c17bc34828af23ce6c38c71f09de815a09a67a2 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 12:36:22 +0000 Subject: [PATCH 01/12] Add a toggle action to Tree, along with a new binding See #1433. The idea here is that the user has an option of expanding/collapsing a non-leaf node without causing a selected event, or (with auto_expand turned off) cause a selected event without an expand/collapse event. As this will need a new binding, I've chosen the space bar as the key to toggle the expanded state. --- src/textual/widgets/_tree.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 64e40ebe2..b3d85cf6a 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -284,6 +284,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): BINDINGS: ClassVar[list[BindingType]] = [ Binding("enter", "select_cursor", "Select", show=False), + Binding("space", "toggle_node", "Toggle", show=False), Binding("up", "cursor_up", "Cursor Up", show=False), Binding("down", "cursor_down", "Cursor Down", show=False), ] @@ -291,6 +292,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): | Key(s) | Description | | :- | :- | | enter | Select the current item. | + | space | Toggle the expand/collapsed space of the current item. | | up | Move the cursor up. | | down | Move the cursor down. | """ @@ -1004,6 +1006,14 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self.cursor_line = self.last_line self.scroll_to_line(self.cursor_line) + def action_toggle_node(self) -> None: + try: + line = self._tree_lines[self.cursor_line] + except IndexError: + pass + else: + self._toggle_node(line.path[-1]) + def action_select_cursor(self) -> None: try: line = self._tree_lines[self.cursor_line] From 775165ec1224c68a1716411c129cd97e71d76f5f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 12:48:19 +0000 Subject: [PATCH 02/12] Add a docstring to action_toggle_node I want to add docstrings to the actions I'm adding as part of #1700, so with this in mind I'm going to add docstrings to all the actions. --- src/textual/widgets/_tree.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index b3d85cf6a..5a3e0939b 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -1015,6 +1015,13 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self._toggle_node(line.path[-1]) def action_select_cursor(self) -> None: + """Cause a select event for the target node. + + Note: + If `auto_expand` is `True` use of this action on a non-leaf node + will cause both an expand/collapse event to occour, as well as a + selected event. + """ try: line = self._tree_lines[self.cursor_line] except IndexError: From ca224b76aba5788929ad754ed809d89af5d2c626 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 12:50:12 +0000 Subject: [PATCH 03/12] Add a docstring to action_toggle_node --- src/textual/widgets/_tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 5a3e0939b..6c4e4aa0a 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -1007,6 +1007,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self.scroll_to_line(self.cursor_line) def action_toggle_node(self) -> None: + """Toggle the expanded state of the target node.""" try: line = self._tree_lines[self.cursor_line] except IndexError: From 93d4863e0b891394bd1c9a741485c6063266c880 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 12:50:40 +0000 Subject: [PATCH 04/12] Add a docstring to action_cursor_up --- src/textual/widgets/_tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 6c4e4aa0a..ff60da1f6 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -973,6 +973,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self._invalidate() def action_cursor_up(self) -> None: + """Move the cursor up one node.""" if self.cursor_line == -1: self.cursor_line = self.last_line else: From 84fa94978e2efa081dbe3020323acdf4bb90c591 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 12:51:04 +0000 Subject: [PATCH 05/12] Add a docstring to action_cursor_down --- src/textual/widgets/_tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index ff60da1f6..1c0465c6d 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -981,6 +981,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self.scroll_to_line(self.cursor_line) def action_cursor_down(self) -> None: + """Move the cursor down one node.""" if self.cursor_line == -1: self.cursor_line = 0 else: From 17d84bc75be66c752cfad219df5781e59f58a140 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 12:51:41 +0000 Subject: [PATCH 06/12] Add a docstring to action_page_down --- src/textual/widgets/_tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 1c0465c6d..5fef9e912 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -989,6 +989,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self.scroll_to_line(self.cursor_line) def action_page_down(self) -> None: + """Move the cursor down a page's-worth of nodes.""" if self.cursor_line == -1: self.cursor_line = 0 self.cursor_line += self.scrollable_content_region.height - 1 From b2aecfa847aea146a938cc904f9b9a727a9af7e0 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 13:00:06 +0000 Subject: [PATCH 07/12] Add a docstring to action_page_up --- src/textual/widgets/_tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 5fef9e912..4f3c731a0 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -996,6 +996,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self.scroll_to_line(self.cursor_line) def action_page_up(self) -> None: + """Move the cursor up a page's-worth of nodes.""" if self.cursor_line == -1: self.cursor_line = self.last_line self.cursor_line -= self.scrollable_content_region.height - 1 From ca773f4350f0a3134ee56bfa9c5e84e25a74fa7e Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 13:02:34 +0000 Subject: [PATCH 08/12] Add a docstring to action_scroll_home --- src/textual/widgets/_tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 4f3c731a0..18464a018 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -1003,6 +1003,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self.scroll_to_line(self.cursor_line) def action_scroll_home(self) -> None: + """Move the cursor to the top of the tree.""" self.cursor_line = 0 self.scroll_to_line(self.cursor_line) From 9e406f525e04c9b3f85f88a6ca33aeb3e205d74f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 13:03:23 +0000 Subject: [PATCH 09/12] Add a docstring to action_scroll_end --- src/textual/widgets/_tree.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 18464a018..0d6ac4baf 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -1008,6 +1008,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self.scroll_to_line(self.cursor_line) def action_scroll_end(self) -> None: + """Move the cursor to the bottom of the tree. + + Note: + Here button means vertically, not branch depth. + """ self.cursor_line = self.last_line self.scroll_to_line(self.cursor_line) From 606af8d3a28da1596d19f5ef188417ba7b0265a5 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 13:12:03 +0000 Subject: [PATCH 10/12] Correct a typo in the docstring of action_scroll_end --- src/textual/widgets/_tree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 0d6ac4baf..415cb9e4e 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -1011,7 +1011,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): """Move the cursor to the bottom of the tree. Note: - Here button means vertically, not branch depth. + Here bottom means vertically, not branch depth. """ self.cursor_line = self.last_line self.scroll_to_line(self.cursor_line) From 2086b713dbc5416f133955ff63e9b890109c6b0b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 13:17:58 +0000 Subject: [PATCH 11/12] Update the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e3694a5..1715cad83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added more keyboard actions and related bindings to `Input` https://github.com/Textualize/textual/pull/1676 - Added App.scroll_sensitivity_x and App.scroll_sensitivity_y to adjust how many lines the scroll wheel moves the scroll position https://github.com/Textualize/textual/issues/928 - Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally +- Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to Space https://github.com/Textualize/textual/issues/1433 ### Changed From 6db5217f3e6fa9cb4780ee582daa6d4f18eb3a45 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 31 Jan 2023 13:32:46 +0000 Subject: [PATCH 12/12] Modify Tree message unit tests to take new action in to account See #1433. --- tests/tree/test_tree_messages.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/tree/test_tree_messages.py b/tests/tree/test_tree_messages.py index 67620d70e..df7442d31 100644 --- a/tests/tree/test_tree_messages.py +++ b/tests/tree/test_tree_messages.py @@ -48,23 +48,26 @@ async def test_tree_node_selected_message() -> None: assert pilot.app.messages == ["NodeExpanded", "NodeSelected"] +async def test_tree_node_selected_message_no_auto() -> None: + """Selecting a node should result in only a selected message being emitted.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(MyTree).auto_expand = False + await pilot.press("enter") + assert pilot.app.messages == ["NodeSelected"] + + async def test_tree_node_expanded_message() -> None: """Expanding a node should result in an expanded message being emitted.""" async with TreeApp().run_test() as pilot: - await pilot.press("enter") - assert pilot.app.messages == ["NodeExpanded", "NodeSelected"] + await pilot.press("space") + assert pilot.app.messages == ["NodeExpanded"] async def test_tree_node_collapsed_message() -> None: """Collapsing a node should result in a collapsed message being emitted.""" async with TreeApp().run_test() as pilot: - await pilot.press("enter", "enter") - assert pilot.app.messages == [ - "NodeExpanded", - "NodeSelected", - "NodeCollapsed", - "NodeSelected", - ] + await pilot.press("space", "space") + assert pilot.app.messages == ["NodeExpanded", "NodeCollapsed"] async def test_tree_node_highlighted_message() -> None: