mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
tree fix
This commit is contained in:
@@ -24,6 +24,23 @@ App > Screen {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tree-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 20;
|
||||||
|
margin: 1 2;
|
||||||
|
background: $panel;
|
||||||
|
padding: 1 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectoryTree {
|
||||||
|
padding: 0 1;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DataTable {
|
DataTable {
|
||||||
/*border:heavy red;*/
|
/*border:heavy red;*/
|
||||||
/* tint: 10% green; */
|
/* tint: 10% green; */
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ from rich.text import Text
|
|||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.reactive import Reactive
|
from textual.reactive import Reactive
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.widgets import Static, DataTable
|
from textual.widgets import Static, DataTable, DirectoryTree
|
||||||
|
from textual.layout import Vertical
|
||||||
|
|
||||||
CODE = '''
|
CODE = '''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -130,6 +131,7 @@ class BasicApp(App, css_path="basic.css"):
|
|||||||
classes="scrollable",
|
classes="scrollable",
|
||||||
),
|
),
|
||||||
table,
|
table,
|
||||||
|
Widget(DirectoryTree("~/projects/textual"), id="tree-container"),
|
||||||
Error(),
|
Error(),
|
||||||
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
||||||
Warning(),
|
Warning(),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from .. import events
|
|||||||
from ..message import Message
|
from ..message import Message
|
||||||
from ..reactive import Reactive
|
from ..reactive import Reactive
|
||||||
from .._types import MessageTarget
|
from .._types import MessageTarget
|
||||||
from . import TreeControl, TreeClick, TreeNode, NodeID
|
from ._tree_control import TreeControl, TreeClick, TreeNode, NodeID
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -30,11 +30,18 @@ class FileClick(Message, bubble=True):
|
|||||||
|
|
||||||
|
|
||||||
class DirectoryTree(TreeControl[DirEntry]):
|
class DirectoryTree(TreeControl[DirEntry]):
|
||||||
def __init__(self, path: str, name: str = None) -> None:
|
def __init__(
|
||||||
self.path = path.rstrip("/")
|
self,
|
||||||
|
path: str,
|
||||||
|
*,
|
||||||
|
name: str | None = None,
|
||||||
|
id: str | None = None,
|
||||||
|
classes: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.path = os.path.expanduser(path.rstrip("/"))
|
||||||
label = os.path.basename(self.path)
|
label = os.path.basename(self.path)
|
||||||
data = DirEntry(path, True)
|
data = DirEntry(self.path, True)
|
||||||
super().__init__(label, name=name, data=data)
|
super().__init__(label, data, name=name, id=id, classes=classes)
|
||||||
self.root.tree.guide_style = "black"
|
self.root.tree.guide_style = "black"
|
||||||
|
|
||||||
has_focus: Reactive[bool] = Reactive(False)
|
has_focus: Reactive[bool] = Reactive(False)
|
||||||
@@ -50,7 +57,7 @@ class DirectoryTree(TreeControl[DirEntry]):
|
|||||||
node.tree.guide_style = (
|
node.tree.guide_style = (
|
||||||
"bold not dim red" if node.id == hover_node else "black"
|
"bold not dim red" if node.id == hover_node else "black"
|
||||||
)
|
)
|
||||||
self.refresh(layout=True)
|
self.refresh()
|
||||||
|
|
||||||
def render_node(self, node: TreeNode[DirEntry]) -> RenderableType:
|
def render_node(self, node: TreeNode[DirEntry]) -> RenderableType:
|
||||||
return self.render_tree_label(
|
return self.render_tree_label(
|
||||||
@@ -81,12 +88,12 @@ class DirectoryTree(TreeControl[DirEntry]):
|
|||||||
if is_hover:
|
if is_hover:
|
||||||
label.stylize("underline")
|
label.stylize("underline")
|
||||||
if is_dir:
|
if is_dir:
|
||||||
label.stylize("bold magenta")
|
label.stylize("bold")
|
||||||
icon = "📂" if expanded else "📁"
|
icon = "📂" if expanded else "📁"
|
||||||
else:
|
else:
|
||||||
label.stylize("bright_green")
|
|
||||||
icon = "📄"
|
icon = "📄"
|
||||||
label.highlight_regex(r"\..*$", "green")
|
label.highlight_regex(r"\..*$", "italic")
|
||||||
|
|
||||||
if label.plain.startswith("."):
|
if label.plain.startswith("."):
|
||||||
label.stylize("dim")
|
label.stylize("dim")
|
||||||
@@ -112,7 +119,7 @@ class DirectoryTree(TreeControl[DirEntry]):
|
|||||||
await node.expand()
|
await node.expand()
|
||||||
self.refresh(layout=True)
|
self.refresh(layout=True)
|
||||||
|
|
||||||
async def handle_tree_click(self, message: TreeClick[DirEntry]) -> None:
|
async def on_tree_click(self, message: TreeClick[DirEntry]) -> None:
|
||||||
dir_entry = message.node.data
|
dir_entry = message.node.data
|
||||||
if not dir_entry.is_dir:
|
if not dir_entry.is_dir:
|
||||||
await self.emit(FileClick(self, dir_entry.path))
|
await self.emit(FileClick(self, dir_entry.path))
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
from typing import Generic, Iterator, NewType, TypeVar
|
from typing import ClassVar, Generic, Iterator, NewType, TypeVar
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
from rich.style import Style
|
|
||||||
from rich.text import Text, TextType
|
from rich.text import Text, TextType
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
from rich.padding import PaddingDimensions
|
|
||||||
|
|
||||||
|
from .. import events
|
||||||
from ..reactive import Reactive
|
from ..reactive import Reactive
|
||||||
from .._types import MessageTarget
|
from .._types import MessageTarget
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
@@ -169,33 +168,52 @@ class TreeClick(Generic[NodeDataType], Message, bubble=True):
|
|||||||
yield "node", self.node
|
yield "node", self.node
|
||||||
|
|
||||||
|
|
||||||
class TreeControl(Generic[NodeDataType], Widget):
|
class TreeControl(Generic[NodeDataType], Widget, can_focus=True):
|
||||||
|
CSS = """
|
||||||
|
TreeControl {
|
||||||
|
background: $panel;
|
||||||
|
color: $text-panel;
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeControl > .tree--guides {
|
||||||
|
color: $secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMPONENT_CLASSES: ClassVar[set[str]] = {
|
||||||
|
"tree--guides",
|
||||||
|
"tree--labels",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
label: TextType,
|
label: TextType,
|
||||||
data: NodeDataType,
|
data: NodeDataType,
|
||||||
*,
|
*,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
padding: PaddingDimensions = (1, 1),
|
id: str | None = None,
|
||||||
|
classes: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
self.id = NodeID(0)
|
self.node_id = NodeID(0)
|
||||||
self.nodes: dict[NodeID, TreeNode[NodeDataType]] = {}
|
self.nodes: dict[NodeID, TreeNode[NodeDataType]] = {}
|
||||||
self._tree = Tree(label)
|
self._tree = Tree(label)
|
||||||
self.root: TreeNode[NodeDataType] = TreeNode(
|
self.root: TreeNode[NodeDataType] = TreeNode(
|
||||||
None, self.id, self, self._tree, label, data
|
None, self.node_id, self, self._tree, label, data
|
||||||
)
|
)
|
||||||
|
|
||||||
self._tree.label = self.root
|
self._tree.label = self.root
|
||||||
self.nodes[NodeID(self.id)] = self.root
|
self.nodes[NodeID(self.node_id)] = self.root
|
||||||
super().__init__(name=name)
|
super().__init__(name=name, id=id, classes=classes)
|
||||||
self.padding = padding
|
|
||||||
|
|
||||||
hover_node: Reactive[NodeID | None] = Reactive(None)
|
hover_node: Reactive[NodeID | None] = Reactive(None)
|
||||||
cursor: Reactive[NodeID] = Reactive(NodeID(0), layout=True)
|
cursor: Reactive[NodeID] = Reactive(NodeID(0))
|
||||||
cursor_line: Reactive[int] = Reactive(0, repaint=False)
|
cursor_line: Reactive[int] = Reactive(0)
|
||||||
show_cursor: Reactive[bool] = Reactive(False, layout=True)
|
show_cursor: Reactive[bool] = Reactive(False)
|
||||||
|
|
||||||
def watch_show_cursor(self, value: bool) -> None:
|
def watch_show_cursor(self, value: bool) -> None:
|
||||||
self.emit_no_wait(CursorMove(self, self.cursor_line))
|
self.emit_no_wait(CursorMove(self, self.cursor_line))
|
||||||
@@ -211,14 +229,14 @@ class TreeControl(Generic[NodeDataType], Widget):
|
|||||||
data: NodeDataType,
|
data: NodeDataType,
|
||||||
) -> None:
|
) -> None:
|
||||||
parent = self.nodes[node_id]
|
parent = self.nodes[node_id]
|
||||||
self.id = NodeID(self.id + 1)
|
self.node_id = NodeID(self.node_id + 1)
|
||||||
child_tree = parent._tree.add(label)
|
child_tree = parent._tree.add(label)
|
||||||
child_node: TreeNode[NodeDataType] = TreeNode(
|
child_node: TreeNode[NodeDataType] = TreeNode(
|
||||||
parent, self.id, self, child_tree, label, data
|
parent, self.node_id, self, child_tree, label, data
|
||||||
)
|
)
|
||||||
parent.children.append(child_node)
|
parent.children.append(child_node)
|
||||||
child_tree.label = child_node
|
child_tree.label = child_node
|
||||||
self.nodes[self.id] = child_node
|
self.nodes[self.node_id] = child_node
|
||||||
|
|
||||||
self.refresh(layout=True)
|
self.refresh(layout=True)
|
||||||
|
|
||||||
@@ -249,6 +267,7 @@ class TreeControl(Generic[NodeDataType], Widget):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
|
self._tree.guide_style = self.component_styles["tree--guides"].node.rich_style
|
||||||
return self._tree
|
return self._tree
|
||||||
|
|
||||||
def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType:
|
def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType:
|
||||||
@@ -307,24 +326,3 @@ class TreeControl(Generic[NodeDataType], Widget):
|
|||||||
if previous_node is not None:
|
if previous_node is not None:
|
||||||
self.cursor_line -= 1
|
self.cursor_line -= 1
|
||||||
self.cursor = previous_node.id
|
self.cursor = previous_node.id
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
from textual import events
|
|
||||||
from textual.app import App
|
|
||||||
|
|
||||||
class TreeApp(App):
|
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
|
||||||
await self.screen.dock(TreeControl("Tree Root", data="foo"))
|
|
||||||
|
|
||||||
async def handle_tree_click(self, message: TreeClick) -> None:
|
|
||||||
if message.node.empty:
|
|
||||||
await message.node.add("foo")
|
|
||||||
await message.node.add("bar")
|
|
||||||
await message.node.add("baz")
|
|
||||||
await message.node.expand()
|
|
||||||
else:
|
|
||||||
await message.node.toggle()
|
|
||||||
|
|
||||||
TreeApp(log_path="textual.log").run()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user