Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a detail column to the Tree widget #5096

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added

- Added support for a detail column to the Tree widget https://github.com/Textualize/textual/pull/5096

## [0.83.0] - 2024-10-10

### Added
Expand Down
37 changes: 37 additions & 0 deletions docs/examples/widgets/detail_tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from rich.text import Text

from textual.app import App, ComposeResult
from textual.widgets import Tree


class TreeApp(App):
def compose(self) -> ComposeResult:
tree: Tree[dict] = Tree("A bit of everything")
tree.root.expand()
fruit = tree.root.add("Fruit", expand=True)
fruit.add_leaf("Orange", detail="🍊")
fruit.add_leaf("Apple", detail="🍎")
fruit.add_leaf("Banana", detail=":banana:")
fruit.add_leaf("Pear", detail="🍐")

# https://en.wikipedia.org/wiki/Demographics_of_the_United_Kingdom
pop = tree.root.add("Population", expand=True)
uk = pop.add("United Kingdom", expand=True, detail="67,081,234")
uk.add_leaf("England", detail="56,550,138")
uk.add_leaf("Scotland", detail="5,466,000")
uk.add_leaf("Wales", detail="3,169,586")
uk.add_leaf("Northern Ireland", detail="1,895,510")

# https://en.wikipedia.org/wiki/List_of_countries_by_average_yearly_temperature
temps = tree.root.add("Average Temperatures", expand=True)
temps.add_leaf("Burkina Faso", detail=Text("30.40 °C", style="red"))
temps.add_leaf("New Zealand", detail="[red]10.46 °C[/red]")
temps.add_leaf("Canada", detail="[blue]-4.03 °C[/blue]")
temps.add_leaf("Greenland", detail=Text("-18.68 °C", style="blue"))

yield tree


if __name__ == "__main__":
app = TreeApp()
app.run()
20 changes: 20 additions & 0 deletions docs/widgets/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ The example below creates a simple tree.
Tree widgets have a "root" attribute which is an instance of a [TreeNode][textual.widgets.tree.TreeNode]. Call [add()][textual.widgets.tree.TreeNode.add] or [add_leaf()][textual.widgets.tree.TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child which you can use to add additional levels.


All nodes have a `detail` attribute that can be used to provide additional information about the node. This information will be shown right justified in the tree node. The following example demonstrates how to use the `detail` attribute.

=== "Output"


```{.textual path="docs/examples/widgets/detail_tree.py"}

```


=== "detail_tree.py"


```python

--8<-- "docs/examples/widgets/detail_tree.py"

```


## Reactive Attributes

| Name | Type | Default | Description |
Expand Down
73 changes: 70 additions & 3 deletions src/textual/widgets/_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def __init__(
*,
expanded: bool = True,
allow_expand: bool = True,
detail: str | Text | None = None,
) -> None:
"""Initialise the node.

Expand All @@ -112,6 +113,7 @@ def __init__(
data: Optional data to associate with the node.
expanded: Should the node be attached in an expanded state?
allow_expand: Should the node allow being expanded by the user?
detail: Optional detail text to display.
"""
self._tree = tree
self._parent = parent
Expand All @@ -128,6 +130,11 @@ def __init__(
self._updates: int = 0
self._line: int = -1

if detail is None:
self._detail = Text("")
else:
self._detail = tree.process_label(detail)

def __rich_repr__(self) -> rich.repr.Result:
yield self._label.plain
yield self.data
Expand Down Expand Up @@ -356,6 +363,29 @@ def set_label(self, label: TextType) -> None:
self._label = text_label
self._tree.call_later(self._tree._refresh_node, self)

@property
def detail(self) -> Text:
"""Detail text for the node."""
return self._detail

@detail.setter
def detail(self, detail: str | Text | None) -> None:
self.set_detail(detail)

def set_detail(self, detail: str | Text | None) -> None:
"""Set the detail text for the node.

Args:
detail: A string or Text object with the detail text.
"""
if detail is None:
detail = Text("")

self._updates += 1
text_detail = self._tree.process_label(detail)
self._detail = text_detail
self._tree.call_later(self._tree._refresh_node, self)

def add(
self,
label: TextType,
Expand All @@ -365,6 +395,7 @@ def add(
after: int | TreeNode[TreeDataType] | None = None,
expand: bool = False,
allow_expand: bool = True,
detail: str | Text | None = None,
) -> TreeNode[TreeDataType]:
"""Add a node to the sub-tree.

Expand Down Expand Up @@ -424,7 +455,7 @@ def add(
)

text_label = self._tree.process_label(label)
node = self._tree._add_node(self, text_label, data)
node = self._tree._add_node(self, text_label, data, detail=detail)
node._expanded = expand
node._allow_expand = allow_expand
self._updates += 1
Expand All @@ -440,6 +471,7 @@ def add_leaf(
*,
before: int | TreeNode[TreeDataType] | None = None,
after: int | TreeNode[TreeDataType] | None = None,
detail: str | Text | None = None,
) -> TreeNode[TreeDataType]:
"""Add a 'leaf' node (a node that can not expand).

Expand All @@ -448,6 +480,7 @@ def add_leaf(
data: Optional data.
before: Optional index or `TreeNode` to add the node before.
after: Optional index or `TreeNode` to add the node after.
detail: Optional detail text.

Returns:
New node.
Expand All @@ -466,6 +499,7 @@ def add_leaf(
after=after,
expand=False,
allow_expand=False,
detail=detail,
)
return node

Expand Down Expand Up @@ -763,6 +797,7 @@ def __init__(
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
detail: str | Text | None = None,
) -> None:
"""Initialise a Tree.

Expand All @@ -777,10 +812,14 @@ def __init__(

text_label = self.process_label(label)

text_detail = Text("")
if detail is not None:
text_detail = self.process_label(detail)

self._updates = 0
self._tree_nodes: dict[NodeID, TreeNode[TreeDataType]] = {}
self._current_id = 0
self.root = self._add_node(None, text_label, data)
self.root = self._add_node(None, text_label, data, detail=text_detail)
"""The root node of the tree."""
self._line_cache: LRUCache[LineCacheKey, Strip] = LRUCache(1024)
self._tree_lines_cached: list[_TreeLine[TreeDataType]] | None = None
Expand Down Expand Up @@ -822,8 +861,11 @@ def _add_node(
label: Text,
data: TreeDataType | None,
expand: bool = False,
detail: str | Text | None = None,
) -> TreeNode[TreeDataType]:
node = TreeNode(self, parent, self._new_id(), label, data, expanded=expand)
node = TreeNode(
self, parent, self._new_id(), label, data, expanded=expand, detail=detail
)
self._tree_nodes[node._id] = node
self._updates += 1
return node
Expand Down Expand Up @@ -853,6 +895,31 @@ def render_label(
prefix = ("", base_style)

text = Text.assemble(prefix, node_label)

if node._detail.cell_len > 0:
node_detail = node._detail.copy()
node_detail.stylize(style)

total_width = self.size.width
line = self._tree_lines[node.line]

guide_width = line._get_guide_width(self.guide_depth, self.show_root)

right_margin = 1
if self.show_vertical_scrollbar:
right_margin += 2

space_width = (
total_width
- text.cell_len
- node_detail.cell_len
- guide_width
- right_margin
)
space_width = max(1, space_width)
space_text = Text(" " * space_width)
text = Text.assemble(text, space_text, node_detail)

return text

def get_label_width(self, node: TreeNode[TreeDataType]) -> int:
Expand Down
Loading