mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #3203 from TomJGooding/feat-directory-tree-add-directory-selected-message
feat(directory tree): add directory selected message
This commit is contained in:
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- `Screen.title`
|
||||
- `Screen.sub_title`
|
||||
- Properties `Header.screen_title` and `Header.screen_sub_title` https://github.com/Textualize/textual/pull/3199
|
||||
- Added `DirectoryTree.DirectorySelected` message https://github.com/Textualize/textual/issues/3200
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -29,7 +30,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Breaking change: Widget.notify and App.notify now return None https://github.com/Textualize/textual/pull/3275
|
||||
- App.unnotify is now private (renamed to App._unnotify) https://github.com/Textualize/textual/pull/3275
|
||||
|
||||
|
||||
## [0.36.0] - 2023-09-05
|
||||
|
||||
### Added
|
||||
|
||||
@@ -90,6 +90,31 @@ class DirectoryTree(Tree[DirEntry]):
|
||||
"""The `Tree` that had a file selected."""
|
||||
return self.node.tree
|
||||
|
||||
class DirectorySelected(Message):
|
||||
"""Posted when a directory is selected.
|
||||
|
||||
Can be handled using `on_directory_tree_directory_selected` in a
|
||||
subclass of `DirectoryTree` or in a parent widget in the DOM.
|
||||
"""
|
||||
|
||||
def __init__(self, node: TreeNode[DirEntry], path: Path) -> None:
|
||||
"""Initialise the DirectorySelected object.
|
||||
|
||||
Args:
|
||||
node: The tree node for the directory that was selected.
|
||||
path: The path of the directory that was selected.
|
||||
"""
|
||||
super().__init__()
|
||||
self.node: TreeNode[DirEntry] = node
|
||||
"""The tree node of the directory that was selected."""
|
||||
self.path: Path = path
|
||||
"""The path of the directory that was selected."""
|
||||
|
||||
@property
|
||||
def control(self) -> Tree[DirEntry]:
|
||||
"""The `Tree` that had a directory selected."""
|
||||
return self.node.tree
|
||||
|
||||
path: var[str | Path] = var["str | Path"](PATH("."), init=False, always_update=True)
|
||||
"""The path that is the root of the directory tree.
|
||||
|
||||
@@ -414,5 +439,7 @@ class DirectoryTree(Tree[DirEntry]):
|
||||
dir_entry = event.node.data
|
||||
if dir_entry is None:
|
||||
return
|
||||
if not self._safe_is_dir(dir_entry.path):
|
||||
if self._safe_is_dir(dir_entry.path):
|
||||
self.post_message(self.DirectorySelected(event.node, dir_entry.path))
|
||||
else:
|
||||
self.post_message(self.FileSelected(event.node, dir_entry.path))
|
||||
|
||||
174
tests/tree/test_directory_tree.py
Normal file
174
tests/tree/test_directory_tree.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.text import Text
|
||||
from textual import on
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import DirectoryTree
|
||||
|
||||
|
||||
class DirectoryTreeApp(App[None]):
|
||||
"""DirectoryTree test app."""
|
||||
|
||||
def __init__(self, path):
|
||||
super().__init__()
|
||||
self._tmp_path = path
|
||||
self.messages = []
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DirectoryTree(self._tmp_path)
|
||||
|
||||
@on(DirectoryTree.FileSelected)
|
||||
@on(DirectoryTree.DirectorySelected)
|
||||
def record(
|
||||
self, event: DirectoryTree.FileSelected | DirectoryTree.DirectorySelected
|
||||
) -> None:
|
||||
self.messages.append(event.__class__.__name__)
|
||||
|
||||
|
||||
async def test_directory_tree_file_selected_message(tmp_path) -> None:
|
||||
"""Selecting a file should result in a file selected message being emitted."""
|
||||
|
||||
FILE_NAME = "hello.txt"
|
||||
|
||||
# Creating one file under root
|
||||
file = tmp_path / FILE_NAME
|
||||
file.touch()
|
||||
async with DirectoryTreeApp(tmp_path).run_test() as pilot:
|
||||
tree = pilot.app.query_one(DirectoryTree)
|
||||
await pilot.pause()
|
||||
|
||||
# Sanity check - file is the only child of root
|
||||
assert len(tree.root.children) == 1
|
||||
node = tree.root.children[0]
|
||||
assert node.label == Text(FILE_NAME)
|
||||
|
||||
# Navigate to the file and select it
|
||||
await pilot.press("down", "enter")
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == ["FileSelected"]
|
||||
|
||||
|
||||
async def test_directory_tree_directory_selected_message(tmp_path) -> None:
|
||||
"""Selecting a directory should result in a directory selected message being emitted."""
|
||||
|
||||
SUBDIR = "subdir"
|
||||
FILE_NAME = "hello.txt"
|
||||
|
||||
# Creating node with one file as its child
|
||||
subdir = tmp_path / SUBDIR
|
||||
subdir.mkdir()
|
||||
file = subdir / FILE_NAME
|
||||
file.touch()
|
||||
async with DirectoryTreeApp(tmp_path).run_test() as pilot:
|
||||
tree = pilot.app.query_one(DirectoryTree)
|
||||
await pilot.pause()
|
||||
|
||||
# Sanity check - subdirectory is the only child of root
|
||||
assert len(tree.root.children) == 1
|
||||
node = tree.root.children[0]
|
||||
assert node.label == Text(SUBDIR)
|
||||
|
||||
# Navigate to the subdirectory and select it
|
||||
await pilot.press("down", "enter")
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == ["DirectorySelected"]
|
||||
|
||||
# Select the subdirectory again
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
assert pilot.app.messages == ["DirectorySelected", "DirectorySelected"]
|
||||
|
||||
|
||||
async def test_directory_tree_reload_node(tmp_path) -> None:
|
||||
"""Reloading a node of a directory tree should display newly created file inside the directory."""
|
||||
|
||||
RELOADED_DIRECTORY = "parentdir"
|
||||
FILE1_NAME = "log.txt"
|
||||
FILE2_NAME = "hello.txt"
|
||||
|
||||
# Creating node with one file as its child
|
||||
reloaded_dir = tmp_path / RELOADED_DIRECTORY
|
||||
reloaded_dir.mkdir()
|
||||
file1 = reloaded_dir / FILE1_NAME
|
||||
file1.touch()
|
||||
|
||||
async with DirectoryTreeApp(tmp_path).run_test() as pilot:
|
||||
tree = pilot.app.query_one(DirectoryTree)
|
||||
await pilot.pause()
|
||||
|
||||
# Sanity check - node is the only child of root
|
||||
assert len(tree.root.children) == 1
|
||||
node = tree.root.children[0]
|
||||
assert node.label == Text(RELOADED_DIRECTORY)
|
||||
node.expand()
|
||||
await pilot.pause()
|
||||
|
||||
# Creating new file under the node
|
||||
file2 = reloaded_dir / FILE2_NAME
|
||||
file2.touch()
|
||||
|
||||
# Without reloading the node, the newly created file does not show up as its child
|
||||
assert len(node.children) == 1
|
||||
assert node.children[0].label == Text(FILE1_NAME)
|
||||
|
||||
tree.reload_node(node)
|
||||
node.collapse()
|
||||
node.expand()
|
||||
await pilot.pause()
|
||||
|
||||
# After reloading the node, both files show up as children
|
||||
assert len(node.children) == 2
|
||||
assert [child.label for child in node.children] == [
|
||||
Text(filename) for filename in sorted({FILE1_NAME, FILE2_NAME})
|
||||
]
|
||||
|
||||
|
||||
async def test_directory_tree_reload_other_node(tmp_path) -> None:
|
||||
"""Reloading a node of a directory tree should not reload content of other directory."""
|
||||
|
||||
RELOADED_DIRECTORY = "parentdir"
|
||||
NOT_RELOADED_DIRECTORY = "otherdir"
|
||||
FILE1_NAME = "log.txt"
|
||||
NOT_RELOADED_FILE3_NAME = "demo.txt"
|
||||
NOT_RELOADED_FILE4_NAME = "unseen.txt"
|
||||
|
||||
# Creating two nodes, each having one file as child
|
||||
reloaded_dir = tmp_path / RELOADED_DIRECTORY
|
||||
reloaded_dir.mkdir()
|
||||
file1 = reloaded_dir / FILE1_NAME
|
||||
file1.touch()
|
||||
non_reloaded_dir = tmp_path / NOT_RELOADED_DIRECTORY
|
||||
non_reloaded_dir.mkdir()
|
||||
file3 = non_reloaded_dir / NOT_RELOADED_FILE3_NAME
|
||||
file3.touch()
|
||||
|
||||
async with DirectoryTreeApp(tmp_path).run_test() as pilot:
|
||||
tree = pilot.app.query_one(DirectoryTree)
|
||||
await pilot.pause()
|
||||
|
||||
# Sanity check - the root has the two nodes as its children (in alphabetical order)
|
||||
assert len(tree.root.children) == 2
|
||||
unaffected_node = tree.root.children[0]
|
||||
node = tree.root.children[1]
|
||||
assert unaffected_node.label == Text(NOT_RELOADED_DIRECTORY)
|
||||
assert node.label == Text(RELOADED_DIRECTORY)
|
||||
unaffected_node.expand()
|
||||
node.expand()
|
||||
await pilot.pause()
|
||||
assert len(unaffected_node.children) == 1
|
||||
assert unaffected_node.children[0].label == Text(NOT_RELOADED_FILE3_NAME)
|
||||
|
||||
# Creating new file under the node that won't be reloaded
|
||||
file4 = non_reloaded_dir / NOT_RELOADED_FILE4_NAME
|
||||
file4.touch()
|
||||
|
||||
tree.reload_node(node)
|
||||
node.collapse()
|
||||
node.expand()
|
||||
unaffected_node.collapse()
|
||||
unaffected_node.expand()
|
||||
await pilot.pause()
|
||||
|
||||
# After reloading one node, the new file under the other one does not show up
|
||||
assert len(unaffected_node.children) == 1
|
||||
assert unaffected_node.children[0].label == Text(NOT_RELOADED_FILE3_NAME)
|
||||
Reference in New Issue
Block a user