From 67eb5e753adb50fafdb375fb2c0d104a0215bbeb Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 23 Jan 2023 17:25:06 +0000 Subject: [PATCH 01/15] Add support for deeply expanding/collapsing/toggling nodes This commit moves the bulk of the work of each action into an internal method that does everything *apart* from invalidating the tree. The idea being that all of the expanded states get updated, all of the update counts get updated, and then finally one single tree invalidation takes place (the latter taking place in the public method, which calls the related internal method). See #1430. --- src/textual/widgets/_tree.py | 59 +++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index fc930f945..05162b3e2 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import ClassVar, Generic, NewType, TypeVar +from typing import ClassVar, Generic, Iterator, NewType, TypeVar import rich.repr from rich.style import NULL_STYLE, Style @@ -159,22 +159,67 @@ class TreeNode(Generic[TreeDataType]): self._allow_expand = allow_expand self._updates += 1 - def expand(self) -> None: - """Expand a node (show its children).""" + def _expand(self, expand_all) -> None: + """Mark a node as expanded (its children are shown). + + Args: + expand_all: If `True` expand the all offspring at all depths. + """ self._expanded = True self._updates += 1 + if expand_all: + for child in self.children: + child._expand(expand_all=True) + + def expand(self, expand_all: bool = False) -> None: + """Expand a node (show its children). + + Args: + expand_all: If `True` expand the all offspring at all depths. + """ + self._expand(expand_all) self._tree._invalidate() - def collapse(self) -> None: - """Collapse the node (hide children).""" + def _collapse(self, collapse_all: bool) -> None: + """Mark a node as collapsed (its children are hidden). + + Args: + collapse_all: If `True` collapse the all offspring at all depths. + """ self._expanded = False + if collapse_all: + for child in self.children: + child._collapse(collapse_all=True) self._updates += 1 + + def collapse(self, collapse_all: bool = True) -> None: + """Collapse the node (hide children). + + Args: + collapse_all: If `True` collapse the all offspring at all depths. + """ + self._collapse(collapse_all) self._tree._invalidate() - def toggle(self) -> None: - """Toggle the expanded state.""" + def _toggle(self, toggle_all: bool) -> None: + """Toggle the expanded state of the node. + + Args: + toggle_all: If `True` toggle the all offspring at all depths. + """ self._expanded = not self._expanded + if toggle_all: + for child in self.children: + child._toggle(toggle_all=True) self._updates += 1 + + def toggle(self, toggle_all: bool = True) -> None: + """Toggle the expanded state. + + Args: + toggle_all: If `True` toggle the all offspring at all depths. + """ + self._toggle(toggle_all) self._tree._invalidate() @property From 2ac3a03471fbefbee05e817850b6a5c797e1042e Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 23 Jan 2023 20:56:09 +0000 Subject: [PATCH 02/15] Add a missing type hint to TreeNode._expand --- src/textual/widgets/_tree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 05162b3e2..c40dddf24 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -159,7 +159,7 @@ class TreeNode(Generic[TreeDataType]): self._allow_expand = allow_expand self._updates += 1 - def _expand(self, expand_all) -> None: + def _expand(self, expand_all: bool) -> None: """Mark a node as expanded (its children are shown). Args: From c57ca884ca32874057ad2e3037d035720a4b3f70 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 23 Jan 2023 21:09:21 +0000 Subject: [PATCH 03/15] Correct some terrible English --- src/textual/widgets/_tree.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index c40dddf24..0a076f0b7 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -163,7 +163,7 @@ class TreeNode(Generic[TreeDataType]): """Mark a node as expanded (its children are shown). Args: - expand_all: If `True` expand the all offspring at all depths. + expand_all: If `True` expand all offspring at all depths. """ self._expanded = True self._updates += 1 @@ -175,7 +175,7 @@ class TreeNode(Generic[TreeDataType]): """Expand a node (show its children). Args: - expand_all: If `True` expand the all offspring at all depths. + expand_all: If `True` expand all offspring at all depths. """ self._expand(expand_all) self._tree._invalidate() @@ -184,7 +184,7 @@ class TreeNode(Generic[TreeDataType]): """Mark a node as collapsed (its children are hidden). Args: - collapse_all: If `True` collapse the all offspring at all depths. + collapse_all: If `True` collapse all offspring at all depths. """ self._expanded = False if collapse_all: @@ -196,7 +196,7 @@ class TreeNode(Generic[TreeDataType]): """Collapse the node (hide children). Args: - collapse_all: If `True` collapse the all offspring at all depths. + collapse_all: If `True` collapse all offspring at all depths. """ self._collapse(collapse_all) self._tree._invalidate() @@ -205,7 +205,7 @@ class TreeNode(Generic[TreeDataType]): """Toggle the expanded state of the node. Args: - toggle_all: If `True` toggle the all offspring at all depths. + toggle_all: If `True` toggle all offspring at all depths. """ self._expanded = not self._expanded if toggle_all: @@ -217,7 +217,7 @@ class TreeNode(Generic[TreeDataType]): """Toggle the expanded state. Args: - toggle_all: If `True` toggle the all offspring at all depths. + toggle_all: If `True` toggle all offspring at all depths. """ self._toggle(toggle_all) self._tree._invalidate() From 91d6f2b9731f545027a9cfee5fa17f95291e296c Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 23 Jan 2023 21:12:16 +0000 Subject: [PATCH 04/15] Force keyword argument use for expand/collapse/toggle_all It might seem excessive for just a single argument, but I feel it's worthwhile doing it here. It's a single boolean parameter on each of the methods that, left bare, will always end up reading badly. Consider: tree.toggle( True ) vs: tree.toggle( toggle_all=True ) the former looks awkward at best and ugly at worst; toggle True? What does that even mean? The latter, while a touch more verbose, makes it really clear what's going on. Trying this on for size. --- src/textual/widgets/_tree.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 0a076f0b7..e13ffef0a 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -171,7 +171,7 @@ class TreeNode(Generic[TreeDataType]): for child in self.children: child._expand(expand_all=True) - def expand(self, expand_all: bool = False) -> None: + def expand(self, *, expand_all: bool = False) -> None: """Expand a node (show its children). Args: @@ -192,7 +192,7 @@ class TreeNode(Generic[TreeDataType]): child._collapse(collapse_all=True) self._updates += 1 - def collapse(self, collapse_all: bool = True) -> None: + def collapse(self, *, collapse_all: bool = True) -> None: """Collapse the node (hide children). Args: @@ -213,7 +213,7 @@ class TreeNode(Generic[TreeDataType]): child._toggle(toggle_all=True) self._updates += 1 - def toggle(self, toggle_all: bool = True) -> None: + def toggle(self, *, toggle_all: bool = True) -> None: """Toggle the expanded state. Args: From 20636a55344afd9b8e8a287ca1f1839b234b36fc Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 23 Jan 2023 21:21:56 +0000 Subject: [PATCH 05/15] Remove some duplication of effort No need to set the keyword to True when I can just pass the parameter's value in anyway. This reads a bit nicer. --- src/textual/widgets/_tree.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index e13ffef0a..6e7ec422a 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -169,7 +169,7 @@ class TreeNode(Generic[TreeDataType]): self._updates += 1 if expand_all: for child in self.children: - child._expand(expand_all=True) + child._expand(expand_all) def expand(self, *, expand_all: bool = False) -> None: """Expand a node (show its children). @@ -189,7 +189,7 @@ class TreeNode(Generic[TreeDataType]): self._expanded = False if collapse_all: for child in self.children: - child._collapse(collapse_all=True) + child._collapse(collapse_all) self._updates += 1 def collapse(self, *, collapse_all: bool = True) -> None: @@ -210,7 +210,7 @@ class TreeNode(Generic[TreeDataType]): self._expanded = not self._expanded if toggle_all: for child in self.children: - child._toggle(toggle_all=True) + child._toggle(toggle_all) self._updates += 1 def toggle(self, *, toggle_all: bool = True) -> None: From bf8a2745bcd65d02568fe559002f0723e18d0bf0 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 23 Jan 2023 21:25:48 +0000 Subject: [PATCH 06/15] Update the CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b014c4fbb..dc513c2a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [0.11.0] - Unreleased +### Added + +- Added an optional `expand_all` parameter to `TreeNode.expand` https://github.com/Textualize/textual/issues/1430 +- Added an optional `collapse_all` parameter to `TreeNode.collapse` https://github.com/Textualize/textual/issues/1430 +- Added an optional `toggle_all` parameter to `TreeNode.toggle` https://github.com/Textualize/textual/issues/1430 + ### Fixed - Fixed stuck screen https://github.com/Textualize/textual/issues/1632 From 8815d82ba4865238eca875575246d1e9c1a33d02 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 24 Jan 2023 10:36:42 +0000 Subject: [PATCH 07/15] Remove unnecessary import This got added while I was experimenting with something earlier, and I forgot to remove it and didn't notice it slip in with a commit. --- src/textual/widgets/_tree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 6e7ec422a..f39076c22 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import ClassVar, Generic, Iterator, NewType, TypeVar +from typing import ClassVar, Generic, NewType, TypeVar import rich.repr from rich.style import NULL_STYLE, Style From 6743ec6a3070756cb1428b0f901c6bf3cf000963 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 24 Jan 2023 10:47:44 +0000 Subject: [PATCH 08/15] Fix the defaults for collapse_all and toggle_all Somehow I'd managed to typo them as True when they obviously should be False to maintain backward-compatibility (and generally this is the only sensible default). --- src/textual/widgets/_tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index f39076c22..aa03fcf3e 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -192,7 +192,7 @@ class TreeNode(Generic[TreeDataType]): child._collapse(collapse_all) self._updates += 1 - def collapse(self, *, collapse_all: bool = True) -> None: + def collapse(self, *, collapse_all: bool = False) -> None: """Collapse the node (hide children). Args: @@ -213,7 +213,7 @@ class TreeNode(Generic[TreeDataType]): child._toggle(toggle_all) self._updates += 1 - def toggle(self, *, toggle_all: bool = True) -> None: + def toggle(self, *, toggle_all: bool = False) -> None: """Toggle the expanded state. Args: From a36583612c78d21fe8bbd2cce2bb6da2caf70fe8 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 24 Jan 2023 11:07:40 +0000 Subject: [PATCH 09/15] Change TreeNode.toggle to act only from the source node See https://github.com/Textualize/textual/pull/1644#issuecomment-1401720808 where Darren raises the excellent point that while the "technically correct" approach that I had was... well, technically correct I guess (it toggled all the nodes from the target node down), it didn't have what was likely the desired effect. So this commit does away with the previous logic for doing the toggle and instead simply calls on expand or collapse depending on the state of the source node. --- src/textual/widgets/_tree.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index aa03fcf3e..bb94c8c01 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -201,26 +201,16 @@ class TreeNode(Generic[TreeDataType]): self._collapse(collapse_all) self._tree._invalidate() - def _toggle(self, toggle_all: bool) -> None: - """Toggle the expanded state of the node. - - Args: - toggle_all: If `True` toggle all offspring at all depths. - """ - self._expanded = not self._expanded - if toggle_all: - for child in self.children: - child._toggle(toggle_all) - self._updates += 1 - def toggle(self, *, toggle_all: bool = False) -> None: """Toggle the expanded state. Args: toggle_all: If `True` toggle all offspring at all depths. """ - self._toggle(toggle_all) - self._tree._invalidate() + if self._expanded: + self.collapse(collapse_all=toggle_all) + else: + self.expand(expand_all=toggle_all) @property def label(self) -> TextType: From 2841936e15423557ad9a7a236c82d69cd5a0a3af Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 24 Jan 2023 11:36:13 +0000 Subject: [PATCH 10/15] Clarify how TreeNode.toggle works at depth --- src/textual/widgets/_tree.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index bb94c8c01..93425625a 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -205,7 +205,8 @@ class TreeNode(Generic[TreeDataType]): """Toggle the expanded state. Args: - toggle_all: If `True` toggle all offspring at all depths. + toggle_all: If `True` set the expanded state of all offspring + nodes at all depths to match this node's toggled state. """ if self._expanded: self.collapse(collapse_all=toggle_all) From c678e3e1e373753b6c8316516b53b6d9e8c0f34c Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 25 Jan 2023 16:48:04 +0000 Subject: [PATCH 11/15] Reduce the expand/collapse/toggle kwarg to just 'all' See https://github.com/Textualize/textual/pull/1644#discussion_r1086493525 -- not exactly my preference but it's been decided it makes for a nicer interface. --- src/textual/widgets/_tree.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 93425625a..ef9e1f345 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -171,13 +171,13 @@ class TreeNode(Generic[TreeDataType]): for child in self.children: child._expand(expand_all) - def expand(self, *, expand_all: bool = False) -> None: + def expand(self, *, all: bool = False) -> None: """Expand a node (show its children). Args: - expand_all: If `True` expand all offspring at all depths. + all: If `True` expand all offspring at all depths. """ - self._expand(expand_all) + self._expand(all) self._tree._invalidate() def _collapse(self, collapse_all: bool) -> None: @@ -192,26 +192,26 @@ class TreeNode(Generic[TreeDataType]): child._collapse(collapse_all) self._updates += 1 - def collapse(self, *, collapse_all: bool = False) -> None: + def collapse(self, *, all: bool = False) -> None: """Collapse the node (hide children). Args: - collapse_all: If `True` collapse all offspring at all depths. + all: If `True` collapse all offspring at all depths. """ - self._collapse(collapse_all) + self._collapse(all) self._tree._invalidate() - def toggle(self, *, toggle_all: bool = False) -> None: + def toggle(self, *, all: bool = False) -> None: """Toggle the expanded state. Args: - toggle_all: If `True` set the expanded state of all offspring + all: If `True` set the expanded state of all offspring nodes at all depths to match this node's toggled state. """ if self._expanded: - self.collapse(collapse_all=toggle_all) + self.collapse(all=all) else: - self.expand(expand_all=toggle_all) + self.expand(all=all) @property def label(self) -> TextType: From e59064e2043dbea58d7985c3d627ea3f3f49cbef Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 25 Jan 2023 20:39:49 +0000 Subject: [PATCH 12/15] Switch away from a keyword approach to a dedicated method approach While descriptive keywords tend to be a preference within the Textual codebase for many things, this was one of those times where a developer's code using the library was likely going to read better if there's a switch to using dedicated methods; this approach means we can just go with "all" rather than "{action}_all" without needing to shadow a Python builtin. This also does mirror mount/mount_all too. --- src/textual/widgets/_tree.py | 52 +++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index ef9e1f345..b26ccd1c7 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -160,7 +160,7 @@ class TreeNode(Generic[TreeDataType]): self._updates += 1 def _expand(self, expand_all: bool) -> None: - """Mark a node as expanded (its children are shown). + """Mark the node as expanded (its children are shown). Args: expand_all: If `True` expand all offspring at all depths. @@ -171,47 +171,51 @@ class TreeNode(Generic[TreeDataType]): for child in self.children: child._expand(expand_all) - def expand(self, *, all: bool = False) -> None: - """Expand a node (show its children). + def expand(self) -> None: + """Expand the node (show its children).""" + self._expand(False) + self._tree._invalidate() - Args: - all: If `True` expand all offspring at all depths. - """ - self._expand(all) + def expand_all(self) -> None: + """Expand the node (show its children) and all those below it.""" + self._expand(True) self._tree._invalidate() def _collapse(self, collapse_all: bool) -> None: - """Mark a node as collapsed (its children are hidden). + """Mark the node as collapsed (its children are hidden). Args: collapse_all: If `True` collapse all offspring at all depths. """ self._expanded = False + self._updates += 1 if collapse_all: for child in self.children: child._collapse(collapse_all) - self._updates += 1 - def collapse(self, *, all: bool = False) -> None: - """Collapse the node (hide children). - - Args: - all: If `True` collapse all offspring at all depths. - """ - self._collapse(all) + def collapse(self) -> None: + """Collapse the node (hide its children).""" + self._collapse(False) self._tree._invalidate() - def toggle(self, *, all: bool = False) -> None: - """Toggle the expanded state. + def collapse_all(self) -> None: + """Collapse the node (hide its children) and all those below it.""" + self._collapse(True) + self._tree._invalidate() - Args: - all: If `True` set the expanded state of all offspring - nodes at all depths to match this node's toggled state. - """ + def toggle(self) -> None: + """Toggle the node's expanded state.""" if self._expanded: - self.collapse(all=all) + self.collapse() else: - self.expand(all=all) + self.expand() + + def toggle_all(self) -> None: + """Toggle the node's expanded state and make all those below it match.""" + if self._expanded: + self.collapse_all() + else: + self.expand_all() @property def label(self) -> TextType: From 64ac4345a8a42b7667ed5ca1f603984b085e42cb Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 25 Jan 2023 20:47:05 +0000 Subject: [PATCH 13/15] Update the CHANGELOG to reflect the method-only approach --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 855d48d88..8efab77b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,15 +9,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added -- Added an optional `expand_all` parameter to `TreeNode.expand` https://github.com/Textualize/textual/issues/1430 -- Added an optional `collapse_all` parameter to `TreeNode.collapse` https://github.com/Textualize/textual/issues/1430 -- Added an optional `toggle_all` parameter to `TreeNode.toggle` https://github.com/Textualize/textual/issues/1430 +- Added optional `TreeNode.expand_all` https://github.com/Textualize/textual/issues/1430 +- Added optional `TreeNode.collapse_all` https://github.com/Textualize/textual/issues/1430 +- Added optional `TreeNode.toggle_all` https://github.com/Textualize/textual/issues/1430 ### Changed - Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637 - ### Fixed - Fixed stuck screen https://github.com/Textualize/textual/issues/1632 From 90736129cf060a957bcf67efd78b3080e1918335 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 25 Jan 2023 20:48:07 +0000 Subject: [PATCH 14/15] Prune a word hangover in CHANGELOG Nowt optional about the new methods! --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8efab77b4..667d96fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added -- Added optional `TreeNode.expand_all` https://github.com/Textualize/textual/issues/1430 -- Added optional `TreeNode.collapse_all` https://github.com/Textualize/textual/issues/1430 -- Added optional `TreeNode.toggle_all` https://github.com/Textualize/textual/issues/1430 +- Added `TreeNode.expand_all` https://github.com/Textualize/textual/issues/1430 +- Added `TreeNode.collapse_all` https://github.com/Textualize/textual/issues/1430 +- Added `TreeNode.toggle_all` https://github.com/Textualize/textual/issues/1430 ### Changed From cbe62fadc3c75c3b45560cf2e6cbf9d750445f6d Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Fri, 27 Jan 2023 13:28:25 +0000 Subject: [PATCH 15/15] Add unit tests for all the expand/collapse/toggle Tree methods --- tests/tree/test_tree_expand_etc.py | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/tree/test_tree_expand_etc.py diff --git a/tests/tree/test_tree_expand_etc.py b/tests/tree/test_tree_expand_etc.py new file mode 100644 index 000000000..a55a44e98 --- /dev/null +++ b/tests/tree/test_tree_expand_etc.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from textual.app import App, ComposeResult +from textual.widgets import Tree + + +class TreeApp(App[None]): + """Test tree app.""" + + def compose(self) -> ComposeResult: + yield Tree("Test") + + def on_mount(self) -> None: + tree = self.query_one(Tree) + for n in range(10): + tree.root.add(f"Trunk {n}") + node = tree.root.children[0] + for n in range(10): + node = node.add(str(n)) + + +async def test_tree_node_expand() -> None: + """Expanding one node should not expand all nodes.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(Tree).root.expand() + assert pilot.app.query_one(Tree).root.is_expanded is True + check_node = pilot.app.query_one(Tree).root.children[0] + while check_node.children: + assert any(child.is_expanded for child in check_node.children) is False + check_node = check_node.children[0] + + +async def test_tree_node_expand_all() -> None: + """Expanding all on a node should expand all child nodes too.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(Tree).root.expand_all() + assert pilot.app.query_one(Tree).root.is_expanded is True + check_node = pilot.app.query_one(Tree).root.children[0] + while check_node.children: + assert check_node.children[0].is_expanded is True + assert any(child.is_expanded for child in check_node.children[1:]) is False + check_node = check_node.children[0] + + +async def test_tree_node_collapse() -> None: + """Collapsing one node should not collapse all nodes.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(Tree).root.expand_all() + pilot.app.query_one(Tree).root.children[0].collapse() + assert pilot.app.query_one(Tree).root.children[0].is_expanded is False + check_node = pilot.app.query_one(Tree).root.children[0].children[0] + while check_node.children: + assert all(child.is_expanded for child in check_node.children) is True + check_node = check_node.children[0] + + +async def test_tree_node_collapse_all() -> None: + """Collapsing all on a node should collapse all child noes too.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(Tree).root.expand_all() + pilot.app.query_one(Tree).root.children[0].collapse_all() + assert pilot.app.query_one(Tree).root.children[0].is_expanded is False + check_node = pilot.app.query_one(Tree).root.children[0].children[0] + while check_node.children: + assert check_node.children[0].is_expanded is False + assert all(child.is_expanded for child in check_node.children[1:]) is True + check_node = check_node.children[0] + + +async def test_tree_node_toggle() -> None: + """Toggling one node should not toggle all nodes.""" + async with TreeApp().run_test() as pilot: + assert pilot.app.query_one(Tree).root.is_expanded is False + check_node = pilot.app.query_one(Tree).root.children[0] + while check_node.children: + assert any(child.is_expanded for child in check_node.children) is False + check_node = check_node.children[0] + pilot.app.query_one(Tree).root.toggle() + assert pilot.app.query_one(Tree).root.is_expanded is True + check_node = pilot.app.query_one(Tree).root.children[0] + while check_node.children: + assert any(child.is_expanded for child in check_node.children) is False + check_node = check_node.children[0] + + +async def test_tree_node_toggle_all() -> None: + """Toggling all on a node should toggle all child nodes too.""" + async with TreeApp().run_test() as pilot: + assert pilot.app.query_one(Tree).root.is_expanded is False + check_node = pilot.app.query_one(Tree).root.children[0] + while check_node.children: + assert any(child.is_expanded for child in check_node.children) is False + check_node = check_node.children[0] + pilot.app.query_one(Tree).root.toggle_all() + assert pilot.app.query_one(Tree).root.is_expanded is True + check_node = pilot.app.query_one(Tree).root.children[0] + while check_node.children: + assert check_node.children[0].is_expanded is True + assert any(child.is_expanded for child in check_node.children[1:]) is False + check_node = check_node.children[0] + pilot.app.query_one(Tree).root.toggle_all() + assert pilot.app.query_one(Tree).root.is_expanded is False + check_node = pilot.app.query_one(Tree).root.children[0] + while check_node.children: + assert any(child.is_expanded for child in check_node.children) is False + check_node = check_node.children[0]