This commit is contained in:
Will McGugan
2022-08-11 13:33:26 +01:00
parent 806582fcb4
commit 1a1076bbea
4 changed files with 72 additions and 48 deletions

View File

@@ -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; */

View File

@@ -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(),

View File

@@ -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))

View File

@@ -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()