mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #2463 from davep/directory-tree-redux
Allow changing the "root" of a `DirectoryTree`
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user