diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c8024f0ab..32e39835a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Add - Added `x_axis` and `y_axis` parameters to `Widget.scroll_to_region` https://github.com/Textualize/textual/pull/5047 +- Added `Tree.move_cursor_to_line` https://github.com/Textualize/textual/pull/5052 ### Changed - Tree will no longer scroll the X axis when moving the cursor https://github.com/Textualize/textual/pull/5047 +- DirectoryTree will no longer select the first node https://github.com/Textualize/textual/pull/5052 ### Fixed - Fixed widgets occasionally not getting Resize events https://github.com/Textualize/textual/pull/5048 +- Fixed tree regression https://github.com/Textualize/textual/pull/5052 ## [0.80.1] - 2024-09-24 diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 28bd0501e6..efc56d8613 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -362,9 +362,12 @@ async def watch_path(self) -> None: If the path is changed the directory tree will be repopulated using the new value as the root. """ + has_cursor = self.cursor_node is not None self.reset_node(self.root, str(self.path), DirEntry(self.PATH(self.path))) await self.reload() - self.move_cursor(self.root, animate=False) + if has_cursor: + self.cursor_line = 0 + self.scroll_to(0, 0, animate=False) def process_label(self, label: TextType) -> Text: """Process a str or Text into a label. May be overridden in a subclass to modify how labels are rendered. diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 4992327a3d..f73dd8515c 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -654,7 +654,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): """Show the root of the tree.""" hover_line = var(-1) """The line number under the mouse pointer, or -1 if not under the mouse pointer.""" - cursor_line = var(-1) + cursor_line = var(-1, always_update=True) """The line with the cursor, or -1 if no cursor.""" show_guides = reactive(True) """Enable display of tree guide lines.""" @@ -846,7 +846,7 @@ def render_label( if node._allow_expand: prefix = ( - self.ICON_NODE if node.is_expanded else self.ICON_NODE_EXPANDED, + self.ICON_NODE_EXPANDED if node.is_expanded else self.ICON_NODE, base_style + TOGGLE_STYLE, ) else: @@ -930,6 +930,24 @@ def move_cursor( animate=animate and abs(self.cursor_line - previous_cursor_line) > 1, ) + def move_cursor_to_line(self, line: int, animate=False) -> None: + """Move the cursor to the given line. + + Args: + line: The line number (negative indexes are offsets from the last line). + animate: Enable scrolling animation. + + Raises: + IndexError: If the line doesn't exist. + """ + if self.cursor_line == line: + return + try: + node = self._tree_lines[line].node + except IndexError: + raise IndexError(f"No line no. {line} in the tree") + self.move_cursor(node, animate=animate) + def select_node(self, node: TreeNode[TreeDataType] | None) -> None: """Move the cursor to the given node and select it, or reset cursor. @@ -1079,22 +1097,33 @@ def watch_hover_line(self, previous_hover_line: int, hover_line: int) -> None: def watch_cursor_line(self, previous_line: int, line: int) -> None: previous_node = self._get_node(previous_line) - # Refresh previous cursor node + node = self._get_node(line) + + if self.cursor_node is not None: + self.cursor_node._selected = False + if previous_node is not None: - self._refresh_node(previous_node) previous_node._selected = False + + if node is not None: + node._selected = True + self._cursor_node = node + else: self._cursor_node = None - node = self._get_node(line) + if previous_line == line: + # No change, so no need for refresh + return + + # Refresh previous cursor node + if previous_node is not None: + self._refresh_node(previous_node) + # Refresh new node if node is not None: self._refresh_node(node) - node._selected = True - self._cursor_node = node if previous_node != node: self.post_message(self.NodeHighlighted(node)) - else: - self._cursor_node = None def watch_guide_depth(self, guide_depth: int) -> None: self._invalidate() diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_json_tree.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_json_tree.svg index 60c4f9652c..eda460ce4e 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_json_tree.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_json_tree.svg @@ -19,144 +19,144 @@ font-weight: 700; } - .terminal-2731236263-matrix { + .terminal-4238498685-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2731236263-title { + .terminal-4238498685-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2731236263-r1 { fill: #c5c8c6 } -.terminal-2731236263-r2 { fill: #e3e3e3 } -.terminal-2731236263-r3 { fill: #e2e3e3 } -.terminal-2731236263-r4 { fill: #24292f } -.terminal-2731236263-r5 { fill: #008139 } -.terminal-2731236263-r6 { fill: #14191f } -.terminal-2731236263-r7 { fill: #e2e3e3;font-weight: bold } -.terminal-2731236263-r8 { fill: #98e024 } -.terminal-2731236263-r9 { fill: #211505;font-weight: bold } -.terminal-2731236263-r10 { fill: #fea62b;font-weight: bold } -.terminal-2731236263-r11 { fill: #58d1eb;font-weight: bold } -.terminal-2731236263-r12 { fill: #f4005f;font-style: italic; } -.terminal-2731236263-r13 { fill: #a7a9ab } -.terminal-2731236263-r14 { fill: #4c5055 } + .terminal-4238498685-r1 { fill: #c5c8c6 } +.terminal-4238498685-r2 { fill: #e3e3e3 } +.terminal-4238498685-r3 { fill: #e2e3e3 } +.terminal-4238498685-r4 { fill: #24292f } +.terminal-4238498685-r5 { fill: #008139 } +.terminal-4238498685-r6 { fill: #14191f } +.terminal-4238498685-r7 { fill: #e2e3e3;font-weight: bold } +.terminal-4238498685-r8 { fill: #98e024 } +.terminal-4238498685-r9 { fill: #211505;font-weight: bold } +.terminal-4238498685-r10 { fill: #fea62b;font-weight: bold } +.terminal-4238498685-r11 { fill: #58d1eb;font-weight: bold } +.terminal-4238498685-r12 { fill: #f4005f;font-style: italic; } +.terminal-4238498685-r13 { fill: #a7a9ab } +.terminal-4238498685-r14 { fill: #4c5055 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TreeApp + TreeApp - + - - TreeApp -▶ Root -└── ▶ {} JSON▁▁ -    ├── code='5060292302201' -    ├── ▶ {} product -    │   ┣━━ _id='5060292302201' -    │   ┣━━ ▼ [] _keywords -    │   ┣━━ ▼ [] added_countries_tags -    │   ┣━━ ▼ [] additives_debug_tags -    │   ┣━━ additives_n=2 -    │   ┣━━ additives_old_n=2 -    │   ┣━━ ▼ [] additives_old_tags -    │   ┣━━ ▼ [] additives_original_tags -    │   ┣━━ ▼ [] additives_prev_original_tags -    │   ┣━━ ▼ [] additives_tags -    │   ┣━━ additives_tags_n=None -    │   ┣━━ allergens='en:milk' -    │   ┣━━ ▼ [] allergens_debug_tags -    │   ┣━━ allergens_from_ingredients='en:milk, milk' -    │   ┣━━ allergens_from_user='(en) en:milk' -    │   ┣━━ ▼ [] allergens_hierarchy -    │   ┣━━ ▼ [] allergens_tags - - a Add node  c Clear  t Toggle root ^p palette + + TreeApp +▼ Root +└── ▼ {} JSON▁▁ +    ├── code='5060292302201' +    ├── ▼ {} product +    │   ┣━━ _id='5060292302201' +    │   ┣━━ ▶ [] _keywords +    │   ┣━━ ▶ [] added_countries_tags +    │   ┣━━ ▶ [] additives_debug_tags +    │   ┣━━ additives_n=2 +    │   ┣━━ additives_old_n=2 +    │   ┣━━ ▶ [] additives_old_tags +    │   ┣━━ ▶ [] additives_original_tags +    │   ┣━━ ▶ [] additives_prev_original_tags +    │   ┣━━ ▶ [] additives_tags +    │   ┣━━ additives_tags_n=None +    │   ┣━━ allergens='en:milk' +    │   ┣━━ ▶ [] allergens_debug_tags +    │   ┣━━ allergens_from_ingredients='en:milk, milk' +    │   ┣━━ allergens_from_user='(en) en:milk' +    │   ┣━━ ▶ [] allergens_hierarchy +    │   ┣━━ ▶ [] allergens_tags + + a Add node  c Clear  t Toggle root ^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_markdown.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_markdown.svg index 099faf14c1..172048c8b5 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_markdown.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_example_markdown.svg @@ -19,145 +19,145 @@ font-weight: 700; } - .terminal-952527153-matrix { + .terminal-3777259831-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-952527153-title { + .terminal-3777259831-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-952527153-r1 { fill: #c5c8c6 } -.terminal-952527153-r2 { fill: #24292f } -.terminal-952527153-r3 { fill: #e1e1e1 } -.terminal-952527153-r4 { fill: #e2e3e3 } -.terminal-952527153-r5 { fill: #96989b } -.terminal-952527153-r6 { fill: #008139 } -.terminal-952527153-r7 { fill: #4ebf71;font-weight: bold } -.terminal-952527153-r8 { fill: #939393;font-weight: bold } -.terminal-952527153-r9 { fill: #4ebf71;text-decoration: underline; } -.terminal-952527153-r10 { fill: #e1e1e1;text-decoration: underline; } -.terminal-952527153-r11 { fill: #fea62b;font-weight: bold } -.terminal-952527153-r12 { fill: #a7a9ab } -.terminal-952527153-r13 { fill: #a6742c;font-weight: bold } -.terminal-952527153-r14 { fill: #727579 } -.terminal-952527153-r15 { fill: #4c5055 } + .terminal-3777259831-r1 { fill: #c5c8c6 } +.terminal-3777259831-r2 { fill: #24292f } +.terminal-3777259831-r3 { fill: #e1e1e1 } +.terminal-3777259831-r4 { fill: #e2e3e3 } +.terminal-3777259831-r5 { fill: #96989b } +.terminal-3777259831-r6 { fill: #008139 } +.terminal-3777259831-r7 { fill: #4ebf71;font-weight: bold } +.terminal-3777259831-r8 { fill: #939393;font-weight: bold } +.terminal-3777259831-r9 { fill: #4ebf71;text-decoration: underline; } +.terminal-3777259831-r10 { fill: #e1e1e1;text-decoration: underline; } +.terminal-3777259831-r11 { fill: #fea62b;font-weight: bold } +.terminal-3777259831-r12 { fill: #a7a9ab } +.terminal-3777259831-r13 { fill: #a6742c;font-weight: bold } +.terminal-3777259831-r14 { fill: #727579 } +.terminal-3777259831-r15 { fill: #4c5055 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MarkdownApp + MarkdownApp - + - - -▶ Ⅰ Textual Markdown Browser -└── Ⅱ Do You Want to Know More?Textual Markdown Browser - -  Welcome fellow adventurer! If you ran  -markdown.py from the terminal you are  -  viewing demo.md with Textual's built in      -  Markdown widget. - -  The widget supports much of the Markdown     -  spec. There is also an optional Table of     -  Contents sidebar which you will see to  -  your left. - - -Do You Want to Know More? - -  See example.md for more examples of what     -  this can do. - - - - - t TOC  b Back  f Forward ^p palette + + +▼ Ⅰ Textual Markdown Browser +└── Ⅱ Do You Want to Know More?Textual Markdown Browser + +  Welcome fellow adventurer! If you ran  +markdown.py from the terminal you are  +  viewing demo.md with Textual's built in      +  Markdown widget. + +  The widget supports much of the Markdown     +  spec. There is also an optional Table of     +  Contents sidebar which you will see to  +  your left. + + +Do You Want to Know More? + +  See example.md for more examples of what     +  this can do. + + + + + t TOC  b Back  f Forward ^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_markdown_viewer_example.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_markdown_viewer_example.svg index 8f7a13d321..e5ee416b5b 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_markdown_viewer_example.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_markdown_viewer_example.svg @@ -19,143 +19,143 @@ font-weight: 700; } - .terminal-1438494665-matrix { + .terminal-4022513615-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1438494665-title { + .terminal-4022513615-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1438494665-r1 { fill: #c5c8c6 } -.terminal-1438494665-r2 { fill: #24292f } -.terminal-1438494665-r3 { fill: #e1e1e1 } -.terminal-1438494665-r4 { fill: #1e1e1e } -.terminal-1438494665-r5 { fill: #e2e3e3 } -.terminal-1438494665-r6 { fill: #96989b } -.terminal-1438494665-r7 { fill: #008139 } -.terminal-1438494665-r8 { fill: #4ebf71;font-weight: bold } -.terminal-1438494665-r9 { fill: #939393;font-weight: bold } -.terminal-1438494665-r10 { fill: #4ebf71;text-decoration: underline; } -.terminal-1438494665-r11 { fill: #14191f } -.terminal-1438494665-r12 { fill: #e1e1e1;font-style: italic; } -.terminal-1438494665-r13 { fill: #e1e1e1;font-weight: bold } + .terminal-4022513615-r1 { fill: #c5c8c6 } +.terminal-4022513615-r2 { fill: #24292f } +.terminal-4022513615-r3 { fill: #e1e1e1 } +.terminal-4022513615-r4 { fill: #1e1e1e } +.terminal-4022513615-r5 { fill: #e2e3e3 } +.terminal-4022513615-r6 { fill: #96989b } +.terminal-4022513615-r7 { fill: #008139 } +.terminal-4022513615-r8 { fill: #4ebf71;font-weight: bold } +.terminal-4022513615-r9 { fill: #939393;font-weight: bold } +.terminal-4022513615-r10 { fill: #4ebf71;text-decoration: underline; } +.terminal-4022513615-r11 { fill: #14191f } +.terminal-4022513615-r12 { fill: #e1e1e1;font-style: italic; } +.terminal-4022513615-r13 { fill: #e1e1e1;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MarkdownExampleApp + MarkdownExampleApp - + - - -▶ Ⅰ Markdown Viewer -├── Ⅱ FeaturesMarkdown Viewer -├── Ⅱ Tables -└── Ⅱ Code Blocks  This is an example of Textual's MarkdownViewer -  widget. - - -Features - -  Markdown syntax and extensions are supported. -▇▇ -● Typography emphasisstronginline code etc. -● Headers -● Lists (bullet and ordered) -● Syntax highlighted code blocks -● Tables! - - -Tables - -  Tables are displayed in a DataTable widget. - - + + +▼ Ⅰ Markdown Viewer +├── Ⅱ FeaturesMarkdown Viewer +├── Ⅱ Tables +└── Ⅱ Code Blocks  This is an example of Textual's MarkdownViewer +  widget. + + +Features + +  Markdown syntax and extensions are supported. +▇▇ +● Typography emphasisstronginline code etc. +● Headers +● Lists (bullet and ordered) +● Syntax highlighted code blocks +● Tables! + + +Tables + +  Tables are displayed in a DataTable widget. + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_clearing_and_expansion.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_clearing_and_expansion.svg index 504a39ca29..8617454347 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_clearing_and_expansion.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_clearing_and_expansion.svg @@ -19,133 +19,133 @@ font-weight: 700; } - .terminal-1587429777-matrix { + .terminal-1892565393-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1587429777-title { + .terminal-1892565393-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1587429777-r1 { fill: #e2e3e3 } -.terminal-1587429777-r2 { fill: #211505;font-weight: bold } -.terminal-1587429777-r3 { fill: #1a1000;font-weight: bold } -.terminal-1587429777-r4 { fill: #c5c8c6 } + .terminal-1892565393-r1 { fill: #e2e3e3 } +.terminal-1892565393-r2 { fill: #211505;font-weight: bold } +.terminal-1892565393-r3 { fill: #1a1000;font-weight: bold } +.terminal-1892565393-r4 { fill: #c5c8c6 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TreeClearingSnapshotApp + TreeClearingSnapshotApp - + - - ▶ Left▼ Right - - - - - - - - - - - - - - - - - - - - - - + + ▼ Left▶ Right + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_example.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_example.svg index b182a8be11..7f65a3cc86 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_example.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_example.svg @@ -19,133 +19,133 @@ font-weight: 700; } - .terminal-3674088692-matrix { + .terminal-1078671588-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3674088692-title { + .terminal-1078671588-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3674088692-r1 { fill: #e2e3e3 } -.terminal-3674088692-r2 { fill: #211505;font-weight: bold } -.terminal-3674088692-r3 { fill: #c5c8c6 } -.terminal-3674088692-r4 { fill: #008139 } + .terminal-1078671588-r1 { fill: #e2e3e3 } +.terminal-1078671588-r2 { fill: #211505;font-weight: bold } +.terminal-1078671588-r3 { fill: #c5c8c6 } +.terminal-1078671588-r4 { fill: #fea62b;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TreeApp + TreeApp - + - - ▶ Dune -└── ▶ Characters -    ├── Paul -    ├── Jessica -    └── Chani - - - - - - - - - - - - - - - - - - + + ▼ Dune +┗━━ ▼ Characters +    ┣━━ Paul +    ┣━━ Jessica +    ┗━━ Chani + + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/snapshot_apps/directory_tree_reload.py b/tests/snapshot_tests/snapshot_apps/directory_tree_reload.py index f1dcc07941..f8c0dee21f 100644 --- a/tests/snapshot_tests/snapshot_apps/directory_tree_reload.py +++ b/tests/snapshot_tests/snapshot_apps/directory_tree_reload.py @@ -56,7 +56,3 @@ def rmdir(self, path: Path) -> None: elif file.is_dir(): self.rmdir(file) path.rmdir() - - -if __name__ == "__main__": - DirectoryTreeReloadApp(Path("playground")).run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index b0b848e381..427e55a82a 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -501,7 +501,7 @@ def test_directory_tree_reloading(snap_compare, tmp_path): async def run_before(pilot): await pilot.app.setup(tmp_path) await pilot.press( - "e", "e", "down", "down", "down", "down", "e", "down", "d", "r" + "down", "e", "e", "down", "down", "down", "down", "e", "down", "d", "r" ) assert snap_compare(