Merge pull request #1700 from davep/tree-select-expand-divorce

Enhance `Tree` so that there is a expand/collapse action that is separate from select
This commit is contained in:
Dave Pearson
2023-01-31 14:33:43 +00:00
committed by GitHub
3 changed files with 41 additions and 9 deletions

View File

@@ -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 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 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 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 <kbd>Space</kbd> https://github.com/Textualize/textual/issues/1433
### Changed ### Changed

View File

@@ -284,6 +284,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
BINDINGS: ClassVar[list[BindingType]] = [ BINDINGS: ClassVar[list[BindingType]] = [
Binding("enter", "select_cursor", "Select", show=False), Binding("enter", "select_cursor", "Select", show=False),
Binding("space", "toggle_node", "Toggle", show=False),
Binding("up", "cursor_up", "Cursor Up", show=False), Binding("up", "cursor_up", "Cursor Up", show=False),
Binding("down", "cursor_down", "Cursor Down", 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 | | Key(s) | Description |
| :- | :- | | :- | :- |
| enter | Select the current item. | | enter | Select the current item. |
| space | Toggle the expand/collapsed space of the current item. |
| up | Move the cursor up. | | up | Move the cursor up. |
| down | Move the cursor down. | | down | Move the cursor down. |
""" """
@@ -971,6 +973,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
self._invalidate() self._invalidate()
def action_cursor_up(self) -> None: def action_cursor_up(self) -> None:
"""Move the cursor up one node."""
if self.cursor_line == -1: if self.cursor_line == -1:
self.cursor_line = self.last_line self.cursor_line = self.last_line
else: else:
@@ -978,6 +981,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
self.scroll_to_line(self.cursor_line) self.scroll_to_line(self.cursor_line)
def action_cursor_down(self) -> None: def action_cursor_down(self) -> None:
"""Move the cursor down one node."""
if self.cursor_line == -1: if self.cursor_line == -1:
self.cursor_line = 0 self.cursor_line = 0
else: else:
@@ -985,26 +989,50 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
self.scroll_to_line(self.cursor_line) self.scroll_to_line(self.cursor_line)
def action_page_down(self) -> None: def action_page_down(self) -> None:
"""Move the cursor down a page's-worth of nodes."""
if self.cursor_line == -1: if self.cursor_line == -1:
self.cursor_line = 0 self.cursor_line = 0
self.cursor_line += self.scrollable_content_region.height - 1 self.cursor_line += self.scrollable_content_region.height - 1
self.scroll_to_line(self.cursor_line) self.scroll_to_line(self.cursor_line)
def action_page_up(self) -> None: def action_page_up(self) -> None:
"""Move the cursor up a page's-worth of nodes."""
if self.cursor_line == -1: if self.cursor_line == -1:
self.cursor_line = self.last_line self.cursor_line = self.last_line
self.cursor_line -= self.scrollable_content_region.height - 1 self.cursor_line -= self.scrollable_content_region.height - 1
self.scroll_to_line(self.cursor_line) self.scroll_to_line(self.cursor_line)
def action_scroll_home(self) -> None: def action_scroll_home(self) -> None:
"""Move the cursor to the top of the tree."""
self.cursor_line = 0 self.cursor_line = 0
self.scroll_to_line(self.cursor_line) self.scroll_to_line(self.cursor_line)
def action_scroll_end(self) -> None: 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.cursor_line = self.last_line
self.scroll_to_line(self.cursor_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: 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: try:
line = self._tree_lines[self.cursor_line] line = self._tree_lines[self.cursor_line]
except IndexError: except IndexError:

View File

@@ -48,23 +48,26 @@ async def test_tree_node_selected_message() -> None:
assert pilot.app.messages == ["NodeExpanded", "NodeSelected"] 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: async def test_tree_node_expanded_message() -> None:
"""Expanding a node should result in an expanded message being emitted.""" """Expanding a node should result in an expanded message being emitted."""
async with TreeApp().run_test() as pilot: async with TreeApp().run_test() as pilot:
await pilot.press("enter") await pilot.press("space")
assert pilot.app.messages == ["NodeExpanded", "NodeSelected"] assert pilot.app.messages == ["NodeExpanded"]
async def test_tree_node_collapsed_message() -> None: async def test_tree_node_collapsed_message() -> None:
"""Collapsing a node should result in a collapsed message being emitted.""" """Collapsing a node should result in a collapsed message being emitted."""
async with TreeApp().run_test() as pilot: async with TreeApp().run_test() as pilot:
await pilot.press("enter", "enter") await pilot.press("space", "space")
assert pilot.app.messages == [ assert pilot.app.messages == ["NodeExpanded", "NodeCollapsed"]
"NodeExpanded",
"NodeSelected",
"NodeCollapsed",
"NodeSelected",
]
async def test_tree_node_highlighted_message() -> None: async def test_tree_node_highlighted_message() -> None: