Merge pull request #2463 from davep/directory-tree-redux

Allow changing the "root" of a `DirectoryTree`
This commit is contained in:
Dave Pearson
2023-05-03 12:15:31 +01:00
committed by GitHub
2 changed files with 101 additions and 40 deletions

View File

@@ -21,11 +21,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed ### Changed
- Setting attributes with a `compute_` method will now raise an `AttributeError` https://github.com/Textualize/textual/issues/2383 - Setting attributes with a `compute_` method will now raise an `AttributeError` https://github.com/Textualize/textual/issues/2383
- Unknown psuedo-selectors will now raise a tokenizer error (previously they were silently ignored) https://github.com/Textualize/textual/pull/2445
- Breaking change: `DirectoryTree.FileSelected.path` is now always a `Path` https://github.com/Textualize/textual/issues/2448
- Breaking change: `Directorytree.load_directory` renamed to `Directorytree._load_directory` https://github.com/Textualize/textual/issues/2448
- Unknown pseudo-selectors will now raise a tokenizer error (previously they were silently ignored) https://github.com/Textualize/textual/pull/2445 - Unknown pseudo-selectors will now raise a tokenizer error (previously they were silently ignored) https://github.com/Textualize/textual/pull/2445
### Added ### Added
- Watch methods can now optionally be private https://github.com/Textualize/textual/issues/2382 - Watch methods can now optionally be private https://github.com/Textualize/textual/issues/2382
- Added `DirectoryTree.path` reactive attribute https://github.com/Textualize/textual/issues/2448
- Added `DirectoryTree.FileSelected.node` https://github.com/Textualize/textual/pull/2463
- Added `DirectoryTree.reload` https://github.com/Textualize/textual/issues/2448
- Added textual.on decorator https://github.com/Textualize/textual/issues/2398 - Added textual.on decorator https://github.com/Textualize/textual/issues/2398
## [0.22.3] - 2023-04-29 ## [0.22.3] - 2023-04-29

View File

@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import ClassVar, Iterable from typing import ClassVar, Iterable
@@ -10,6 +9,7 @@ from rich.text import Text, TextType
from ..events import Mount from ..events import Mount
from ..message import Message from ..message import Message
from ..reactive import var
from ._tree import TOGGLE_STYLE, Tree, TreeNode from ._tree import TOGGLE_STYLE, Tree, TreeNode
@@ -17,26 +17,19 @@ from ._tree import TOGGLE_STYLE, Tree, TreeNode
class DirEntry: class DirEntry:
"""Attaches directory information to a node.""" """Attaches directory information to a node."""
path: str path: Path
is_dir: bool """The path of the directory entry."""
loaded: bool = False loaded: bool = False
"""Has this been loaded?"""
class DirectoryTree(Tree[DirEntry]): class DirectoryTree(Tree[DirEntry]):
"""A Tree widget that presents files and directories. """A Tree widget that presents files and directories."""
Args:
path: Path to directory.
name: The name of the widget, or None for no name.
id: The ID of the widget in the DOM, or None for no ID.
classes: A space-separated list of classes, or None for no classes.
disabled: Whether the directory tree is disabled or not.
"""
COMPONENT_CLASSES: ClassVar[set[str]] = { COMPONENT_CLASSES: ClassVar[set[str]] = {
"directory-tree--folder",
"directory-tree--file",
"directory-tree--extension", "directory-tree--extension",
"directory-tree--file",
"directory-tree--folder",
"directory-tree--hidden", "directory-tree--hidden",
} }
""" """
@@ -55,10 +48,6 @@ class DirectoryTree(Tree[DirEntry]):
text-style: bold; text-style: bold;
} }
DirectoryTree > .directory-tree--file {
}
DirectoryTree > .directory-tree--extension { DirectoryTree > .directory-tree--extension {
text-style: italic; text-style: italic;
} }
@@ -73,14 +62,28 @@ class DirectoryTree(Tree[DirEntry]):
Can be handled using `on_directory_tree_file_selected` in a subclass of Can be handled using `on_directory_tree_file_selected` in a subclass of
`DirectoryTree` or in a parent widget in the DOM. `DirectoryTree` or in a parent widget in the DOM.
Attributes:
path: The path of the file that was selected.
""" """
def __init__(self, path: str) -> None: def __init__(self, node: TreeNode[DirEntry], path: Path) -> None:
self.path: str = path """Initialise the FileSelected object.
Args:
node: The tree node for the file that was selected.
path: The path of the file that was selected.
"""
super().__init__() super().__init__()
self.node: TreeNode[DirEntry] = node
"""The tree node of the file that was selected."""
self.path: Path = path
"""The path of the file that was selected."""
path: var[str | Path] = var["str | Path"](Path("."), init=False)
"""The path that is the root of the directory tree.
Note:
This can be set to either a `str` or a `pathlib.Path` object, but
the value will always be a `pathlib.Path` object.
"""
def __init__( def __init__(
self, self,
@@ -91,18 +94,54 @@ class DirectoryTree(Tree[DirEntry]):
classes: str | None = None, classes: str | None = None,
disabled: bool = False, disabled: bool = False,
) -> None: ) -> None:
str_path = os.fspath(path) """Initialise the directory tree.
self.path = str_path
Args:
path: Path to directory.
name: The name of the widget, or None for no name.
id: The ID of the widget in the DOM, or None for no ID.
classes: A space-separated list of classes, or None for no classes.
disabled: Whether the directory tree is disabled or not.
"""
self.path = path
super().__init__( super().__init__(
str_path, str(path),
data=DirEntry(str_path, True), data=DirEntry(Path(path)),
name=name, name=name,
id=id, id=id,
classes=classes, classes=classes,
disabled=disabled, disabled=disabled,
) )
def process_label(self, label: TextType): def reload(self) -> None:
"""Reload the `DirectoryTree` contents."""
self.reset(str(self.path), DirEntry(Path(self.path)))
self._load_directory(self.root)
def validate_path(self, path: str | Path) -> Path:
"""Ensure that the path is of the `Path` type.
Args:
path: The path to validate.
Returns:
The validated Path value.
Note:
The result will always be a Python `Path` object, regardless of
the value given.
"""
return Path(path)
def watch_path(self) -> None:
"""Watch for changes to the `path` of the directory tree.
If the path is changed the directory tree will be repopulated using
the new value as the root.
"""
self.reload()
def process_label(self, label: TextType) -> Text:
"""Process a str or Text into a label. Maybe overridden in a subclass to modify how labels are rendered. """Process a str or Text into a label. Maybe overridden in a subclass to modify how labels are rendered.
Args: Args:
@@ -118,7 +157,19 @@ class DirectoryTree(Tree[DirEntry]):
first_line = text_label.split()[0] first_line = text_label.split()[0]
return first_line return first_line
def render_label(self, node: TreeNode[DirEntry], base_style: Style, style: Style): def render_label(
self, node: TreeNode[DirEntry], base_style: Style, style: Style
) -> Text:
"""Render a label for the given node.
Args:
node: A tree node.
base_style: The base style of the widget.
style: The additional style for the label.
Returns:
A Rich Text object containing the label.
"""
node_label = node._label.copy() node_label = node._label.copy()
node_label.stylize(style) node_label.stylize(style)
@@ -165,40 +216,44 @@ class DirectoryTree(Tree[DirEntry]):
""" """
return paths return paths
def load_directory(self, node: TreeNode[DirEntry]) -> None: def _load_directory(self, node: TreeNode[DirEntry]) -> None:
"""Load the directory contents for a given node.
Args:
node: The node to load the directory contents for.
"""
assert node.data is not None assert node.data is not None
dir_path = Path(node.data.path)
node.data.loaded = True node.data.loaded = True
directory = sorted( directory = sorted(
self.filter_paths(dir_path.iterdir()), self.filter_paths(node.data.path.iterdir()),
key=lambda path: (not path.is_dir(), path.name.lower()), key=lambda path: (not path.is_dir(), path.name.lower()),
) )
for path in directory: for path in directory:
node.add( node.add(
path.name, path.name,
data=DirEntry(str(path), path.is_dir()), data=DirEntry(path),
allow_expand=path.is_dir(), allow_expand=path.is_dir(),
) )
node.expand() node.expand()
def _on_mount(self, _: Mount) -> None: def _on_mount(self, _: Mount) -> None:
self.load_directory(self.root) self._load_directory(self.root)
def _on_tree_node_expanded(self, event: Tree.NodeSelected) -> None: def _on_tree_node_expanded(self, event: Tree.NodeExpanded) -> None:
event.stop() event.stop()
dir_entry = event.node.data dir_entry = event.node.data
if dir_entry is None: if dir_entry is None:
return return
if dir_entry.is_dir: if dir_entry.path.is_dir():
if not dir_entry.loaded: if not dir_entry.loaded:
self.load_directory(event.node) self._load_directory(event.node)
else: else:
self.post_message(self.FileSelected(dir_entry.path)) self.post_message(self.FileSelected(event.node, dir_entry.path))
def _on_tree_node_selected(self, event: Tree.NodeSelected) -> None: def _on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
event.stop() event.stop()
dir_entry = event.node.data dir_entry = event.node.data
if dir_entry is None: if dir_entry is None:
return return
if not dir_entry.is_dir: if not dir_entry.path.is_dir():
self.post_message(self.FileSelected(dir_entry.path)) self.post_message(self.FileSelected(event.node, dir_entry.path))