textual icon indicating copy to clipboard operation
textual copied to clipboard

TreeControl doesn't refresh when adding new nodes

Open erezsh opened this issue 1 year ago • 3 comments

I'm using textual 0.3.0.

As the title says, when I add nodes, the tree control doesn't refresh.

Interestingly enough, after toggling its css, it starts to behave correctly.

Attached below is the code for a minimally reproducing program. Run it, wait a few seconds, and then press either F or D and see it suddenly updates.

Whatever "mode" gets activated, it should be active before the user inputs anything.

test_tree_refresh.py


from textual import events
from textual.app import App, ComposeResult
from textual.containers import Container, Vertical
from textual.reactive import var
from textual.widgets import Footer, Header, Static

from functools import lru_cache
import os.path

from rich.console import RenderableType

from rich.text import Text

from textual.widgets._tree_control import TreeControl, TreeNode


class DirectoryTree(TreeControl[str]):

    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 = "$"
        super().__init__(label, data, name=name, id=id, classes=classes)
        self.root.tree.guide_style = "black"

    def render_node(self, node: TreeNode[str]) -> RenderableType:
        return self.render_tree_label(
            node,
            node.data,
            node.expanded,
            node.is_cursor,
            node.id == self.hover_node,
            self.has_focus,
        )

    @lru_cache(maxsize=1024 * 32)
    def render_tree_label(
        self,
        node: TreeNode[str],
        data,
        expanded: bool,
        is_cursor: bool,
        is_hover: bool,
        has_focus: bool,
    ) -> RenderableType:
        meta = {
            "@click": f"click_label({node.id})",
            "tree_node": node.id,
            "cursor": node.is_cursor,
        }
        label = Text(data)
        if is_hover:
            label.stylize("underline")

        label.stylize("white")

        label.highlight_regex(r"\d*", "cyan")

        if label.plain.startswith("."):
            label.stylize("dim")

        if is_cursor and has_focus:
            label.stylize("reverse")

        icon = "📂" if expanded else "📁"
        icon_label = Text(f"{icon} ", no_wrap=True, overflow="ellipsis") + label
        icon_label.apply_meta(meta)
        return icon_label



    def on_styles_updated(self) -> None:
        self.render_tree_label.cache_clear()



class TestApp(App):
    """Textual code browser app."""

    CSS_PATH = "test_tree_refresh.css"
    BINDINGS = [
        ("f", "toggle_files", "Toggle Files"),
        ("q", "quit", "Quit"),
        ("d", "toggle_dark", "Toggle dark mode")
    ]

    show_tree = var(True)

    def watch_show_tree(self, show_tree: bool) -> None:
        """Called when show_tree is modified."""
        self.set_class(show_tree, "-show-tree")

    def compose(self) -> ComposeResult:
        """Compose our UI."""
        dt = DirectoryTree(".", id="tree")
        dt.root.expand()
        yield Header()
        yield Container(
            Vertical(dt, id="tree-view"),
            Vertical(Static(id="code", expand=True), id="code-view"),
        )
        yield Footer()

    def on_mount(self, event: events.Mount) -> None:
        tree_view = self.query_one("#tree-view")
        tree_view.focus()

        self.set_interval(0.1, self.add_node)

    def add_node(self):
        tree = self.query_one("#tree")
        tree.root.add('foo', 'bar')

    def action_toggle_files(self) -> None:
        """Called in response to key binding."""
        self.show_tree = not self.show_tree


TestApp().run()

test_tree_refresh.css:

Screen {
    background: $surface-darken-1;
}

#tree-view {
    display: none;
    scrollbar-gutter: stable;
    width: auto;
}

TestApp.-show-tree #tree-view {
    display: block;
    dock: left;
    height: 100%;
    max-width: 50%;
    background: #151C25;
}

DirectoryTree {
    padding-right: 1;
}

#code-view {
    overflow: auto scroll;
    min-width: 100%;
}
#code {
    width: auto;
}

erezsh avatar Nov 01 '22 00:11 erezsh

Sorry for the delay. The current TreeControl implementation has a few deficiencies. We're working on a new version.

I suspect your issue could be solved with a call to _clear_caches call in the meantime.

willmcgugan avatar Nov 13 '22 14:11 willmcgugan

Hi @willmcgugan , thanks for looking into it.

Can you please explain your solution? The only reference to _clear_caches I can find is in _data_table: https://github.com/search?q=repo%3ATextualize%2Ftextual%20_clear_caches%20&type=code

We're working on a new version.

Any estimation to when it will be ready? And is there any issue for it, so I can subscribe to notifications?

erezsh avatar Nov 14 '22 12:11 erezsh

@erezsh #741 would be the closest to an issue regarding reworking the tree control, but likely the best thing to watch is the draft PR Will has going for his work on this: https://github.com/Textualize/textual/pull/1170

And of course it will be mentioned in the release notes for whatever release this is rolled into.

davep avatar Nov 14 '22 13:11 davep

@erezsh Just to update, the Tree update got released around the middle of last month.

davep avatar Jan 04 '23 15:01 davep

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

github-actions[bot] avatar Jan 04 '23 15:01 github-actions[bot]

Hi @davep , I see that you removed _tree_control.py.. Did you move TreeControl elsewhere, or just removed it?

erezsh avatar Jan 04 '23 16:01 erezsh

Renamed to _tree.py

willmcgugan avatar Jan 04 '23 17:01 willmcgugan