diff --git a/CHANGELOG.md b/CHANGELOG.md index 26d7a0dd2..3a00465ea 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 diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 64e40ebe2..415cb9e4e 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. | """ @@ -971,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: @@ -978,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: @@ -985,26 +989,50 @@ 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 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 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) def action_scroll_end(self) -> None: + """Move the cursor to the bottom of the tree. + + Note: + Here bottom means vertically, not branch depth. + """ self.cursor_line = self.last_line 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: + pass + else: + 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: 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: