From 841c01cd5d8487985d2694fedd3674a925da0f29 Mon Sep 17 00:00:00 2001 From: Jean-Paul Balabanian Date: Sun, 6 Oct 2024 19:15:31 +0200 Subject: [PATCH 1/2] Add a detail column to the Tree widget --- CHANGELOG.md | 6 + docs/examples/widgets/detail_tree.py | 37 +++++ docs/widgets/tree.md | 20 +++ src/textual/widgets/_tree.py | 64 +++++++- .../test_tree_with_detail_example.svg | 154 ++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 4 + 6 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 docs/examples/widgets/detail_tree.py create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_with_detail_example.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 2471d5ed43..4d941360c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/examples/widgets/detail_tree.py b/docs/examples/widgets/detail_tree.py new file mode 100644 index 0000000000..ac94e525fb --- /dev/null +++ b/docs/examples/widgets/detail_tree.py @@ -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() diff --git a/docs/widgets/tree.md b/docs/widgets/tree.md index e1c4f33d1e..84ae827666 100644 --- a/docs/widgets/tree.md +++ b/docs/widgets/tree.md @@ -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 | diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index f73dd8515c..740d714137 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -101,6 +101,7 @@ def __init__( *, expanded: bool = True, allow_expand: bool = True, + detail: str | Text | None = None, ) -> None: """Initialise the node. @@ -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 @@ -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 @@ -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, @@ -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. @@ -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 @@ -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). @@ -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. @@ -466,6 +499,7 @@ def add_leaf( after=after, expand=False, allow_expand=False, + detail=detail, ) return node @@ -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. @@ -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 @@ -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 @@ -853,6 +895,22 @@ 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) + space_width = ( + total_width - text.cell_len - node_detail.cell_len - guide_width - 3 + ) + 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: diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_with_detail_example.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_with_detail_example.svg new file mode 100644 index 0000000000..71df1822b4 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_with_detail_example.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TreeApp + + + + + + + + + + ▼ A bit of everything +┣━━ ▼ Fruit +┃   ┣━━ Orange                                                             🍊 +┃   ┣━━ Apple                                                              🍎 +┃   ┣━━ Banana                                                             🍌 +┃   ┗━━ Pear                                                               🍐 +┣━━ ▼ Population +┃   ┗━━ ▼ United Kingdom                                           67,081,234 +┃       ┣━━ England                                                56,550,138 +┃       ┣━━ Scotland                                                5,466,000 +┃       ┣━━ Wales                                                   3,169,586 +┃       ┗━━ Northern Ireland                                        1,895,510 +┗━━ ▼ Average Temperatures +    ┣━━ Burkina Faso                                                 30.40 °C +    ┣━━ New Zealand                                                  10.46 °C +    ┣━━ Canada                                                       -4.03 °C +    ┗━━ Greenland                                                   -18.68 °C + + + + + + + + + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index e35b3e9393..7b26f11453 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -295,6 +295,10 @@ def test_tree_example(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "tree.py") +def test_tree_with_detail_example(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "detail_tree.py") + + def test_markdown_example(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "markdown.py") From 215f8c26924ecc733597e676f5ed2033dabe01b2 Mon Sep 17 00:00:00 2001 From: Jean-Paul Balabanian Date: Thu, 10 Oct 2024 10:06:02 +0200 Subject: [PATCH 2/2] Set right margin based on vertical scrollbar visibility --- src/textual/widgets/_tree.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 740d714137..f86371c8bf 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -904,8 +904,17 @@ def render_label( 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 - 3 + total_width + - text.cell_len + - node_detail.cell_len + - guide_width + - right_margin ) space_width = max(1, space_width) space_text = Text(" " * space_width)