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▊
-├── Ⅱ Features▊Markdown Viewer
-├── Ⅱ Tables▊
-└── Ⅱ Code Blocks▊ This is an example of Textual's MarkdownViewer
-▊ widget.
-▊
-▊
-▊Features
-▊
-▊ Markdown syntax and extensions are supported.
-▊▇▇
-▊● Typography emphasis, strong, inline code etc.
-▊● Headers
-▊● Lists (bullet and ordered)
-▊● Syntax highlighted code blocks
-▊● Tables!
-▊
-▊
-▊Tables
-▊
-▊ Tables are displayed in a DataTable widget.
-▊
-▊
+
+ ▊
+▼ Ⅰ Markdown Viewer▊
+├── Ⅱ Features▊Markdown Viewer
+├── Ⅱ Tables▊
+└── Ⅱ Code Blocks▊ This is an example of Textual's MarkdownViewer
+▊ widget.
+▊
+▊
+▊Features
+▊
+▊ Markdown syntax and extensions are supported.
+▊▇▇
+▊● Typography emphasis, strong, inline 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(