mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #667 from Textualize/tree-fix
partial tree control 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 {
|
||||
/*border:heavy red;*/
|
||||
/* tint: 10% green; */
|
||||
|
||||
@@ -6,7 +6,8 @@ from rich.text import Text
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.reactive import Reactive
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static, DataTable
|
||||
from textual.widgets import Static, DataTable, DirectoryTree
|
||||
from textual.layout import Vertical
|
||||
|
||||
CODE = '''
|
||||
from __future__ import annotations
|
||||
@@ -130,6 +131,7 @@ class BasicApp(App, css_path="basic.css"):
|
||||
classes="scrollable",
|
||||
),
|
||||
table,
|
||||
Widget(DirectoryTree("~/projects/textual"), id="tree-container"),
|
||||
Error(),
|
||||
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
||||
Warning(),
|
||||
|
||||
22
sandbox/will/tree.py
Normal file
22
sandbox/will/tree.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from textual.app import App
|
||||
|
||||
from textual.layout import Container
|
||||
from textual.widgets import DirectoryTree
|
||||
|
||||
|
||||
class TreeApp(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
overflow: auto;
|
||||
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self):
|
||||
tree = DirectoryTree("~/projects")
|
||||
yield Container(tree)
|
||||
|
||||
|
||||
app = TreeApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -13,7 +13,7 @@ from .. import events
|
||||
from ..message import Message
|
||||
from ..reactive import Reactive
|
||||
from .._types import MessageTarget
|
||||
from . import TreeControl, TreeClick, TreeNode, NodeID
|
||||
from ._tree_control import TreeControl, TreeClick, TreeNode, NodeID
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -30,11 +30,18 @@ class FileClick(Message, bubble=True):
|
||||
|
||||
|
||||
class DirectoryTree(TreeControl[DirEntry]):
|
||||
def __init__(self, path: str, name: str = None) -> None:
|
||||
self.path = path.rstrip("/")
|
||||
def __init__(
|
||||
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)
|
||||
data = DirEntry(path, True)
|
||||
super().__init__(label, name=name, data=data)
|
||||
data = DirEntry(self.path, True)
|
||||
super().__init__(label, data, name=name, id=id, classes=classes)
|
||||
self.root.tree.guide_style = "black"
|
||||
|
||||
has_focus: Reactive[bool] = Reactive(False)
|
||||
@@ -50,7 +57,7 @@ class DirectoryTree(TreeControl[DirEntry]):
|
||||
node.tree.guide_style = (
|
||||
"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:
|
||||
return self.render_tree_label(
|
||||
@@ -81,12 +88,12 @@ class DirectoryTree(TreeControl[DirEntry]):
|
||||
if is_hover:
|
||||
label.stylize("underline")
|
||||
if is_dir:
|
||||
label.stylize("bold magenta")
|
||||
label.stylize("bold")
|
||||
icon = "📂" if expanded else "📁"
|
||||
else:
|
||||
label.stylize("bright_green")
|
||||
|
||||
icon = "📄"
|
||||
label.highlight_regex(r"\..*$", "green")
|
||||
label.highlight_regex(r"\..*$", "italic")
|
||||
|
||||
if label.plain.startswith("."):
|
||||
label.stylize("dim")
|
||||
@@ -112,7 +119,7 @@ class DirectoryTree(TreeControl[DirEntry]):
|
||||
await node.expand()
|
||||
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
|
||||
if not dir_entry.is_dir:
|
||||
await self.emit(FileClick(self, dir_entry.path))
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from typing import Generic, Iterator, NewType, TypeVar
|
||||
from typing import ClassVar, Generic, Iterator, NewType, TypeVar
|
||||
|
||||
import rich.repr
|
||||
from rich.console import RenderableType
|
||||
from rich.style import Style
|
||||
from rich.text import Text, TextType
|
||||
from rich.tree import Tree
|
||||
from rich.padding import PaddingDimensions
|
||||
|
||||
from .. import events
|
||||
from ..reactive import Reactive
|
||||
from .._types import MessageTarget
|
||||
from ..widget import Widget
|
||||
@@ -169,33 +168,52 @@ class TreeClick(Generic[NodeDataType], Message, bubble=True):
|
||||
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__(
|
||||
self,
|
||||
label: TextType,
|
||||
data: NodeDataType,
|
||||
*,
|
||||
name: str | None = None,
|
||||
padding: PaddingDimensions = (1, 1),
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
self.data = data
|
||||
|
||||
self.id = NodeID(0)
|
||||
self.node_id = NodeID(0)
|
||||
self.nodes: dict[NodeID, TreeNode[NodeDataType]] = {}
|
||||
self._tree = Tree(label)
|
||||
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.nodes[NodeID(self.id)] = self.root
|
||||
super().__init__(name=name)
|
||||
self.padding = padding
|
||||
self.nodes[NodeID(self.node_id)] = self.root
|
||||
super().__init__(name=name, id=id, classes=classes)
|
||||
|
||||
hover_node: Reactive[NodeID | None] = Reactive(None)
|
||||
cursor: Reactive[NodeID] = Reactive(NodeID(0), layout=True)
|
||||
cursor_line: Reactive[int] = Reactive(0, repaint=False)
|
||||
show_cursor: Reactive[bool] = Reactive(False, layout=True)
|
||||
cursor: Reactive[NodeID] = Reactive(NodeID(0))
|
||||
cursor_line: Reactive[int] = Reactive(0)
|
||||
show_cursor: Reactive[bool] = Reactive(False)
|
||||
|
||||
def watch_show_cursor(self, value: bool) -> None:
|
||||
self.emit_no_wait(CursorMove(self, self.cursor_line))
|
||||
@@ -211,14 +229,14 @@ class TreeControl(Generic[NodeDataType], Widget):
|
||||
data: NodeDataType,
|
||||
) -> None:
|
||||
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_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)
|
||||
child_tree.label = child_node
|
||||
self.nodes[self.id] = child_node
|
||||
self.nodes[self.node_id] = child_node
|
||||
|
||||
self.refresh(layout=True)
|
||||
|
||||
@@ -249,6 +267,7 @@ class TreeControl(Generic[NodeDataType], Widget):
|
||||
return None
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
self._tree.guide_style = self.component_styles["tree--guides"].node.rich_style
|
||||
return self._tree
|
||||
|
||||
def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType:
|
||||
@@ -307,24 +326,3 @@ class TreeControl(Generic[NodeDataType], Widget):
|
||||
if previous_node is not None:
|
||||
self.cursor_line -= 1
|
||||
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