rivo / tview

Terminal UI library with rich, interactive widgets — written in Golang
MIT License
10.36k stars 547 forks source link

TreeView.ExpandAll()/SetExpanded not working as expected #412

Closed karlredman closed 4 years ago

karlredman commented 4 years ago

Hi Great work! Thanks you. I'm having an issue whereby I think I'm missing something or that I would like clarification about if possible please.

Description:

What is hapening:

What is expected:

RootLevel
├──Administration
├──Projects
│  ├──EditFrontmatter
│  ├──Elegorium
│  ├──Heorot
│  ├──JobSearch
│  ├──My-Articles
│  ├──Parasynthesis
│  ├──Parasynthetic
│  │  ├──Stuff
│  │  ├──and
│  │  └──things
│  ├──Timetrap_GoTUI
│  ├──Timetrap_TUI
│  ├──githubio
│  └──parasynthetic_dev
└──default

Code:

Sorry this is kind of a long example. Note that once the state changes on the tree, subsequent behavior of the tree changes -so restarting the application is probably necessary in order to see if/that things are working as expected/unexpectedly. I added kb input so you can fiddle with the behaviors a little easier.

package main

// Demo code for the TreeView primitive.
// Original: https://github.com/rivo/tview/blob/master/demos/treeview/main.go
//
// Modifications are commented with this pattern: `// **<comment>

import (
    "path/filepath"

    "github.com/gdamore/tcell"
    "github.com/rivo/tview"
    "github.com/twpayne/go-vfs"
    "github.com/twpayne/go-vfs/vfst"
)

type FileTreeNodeRef struct {
    // **TreeNode reference data
    IsRoot bool
    IsDir  bool
    Path   string
}

// Show a navigable tree view of the current directory.
func main() {

    // **use vfs to mock filesystem (for consistent testing)
    fs, _, _ := vfst.NewTestFS(map[string]interface{}{
        "/RootLevel/Administration":                "",
        "/RootLevel/default":                       "",
        "/RootLevel/Projects/EditFrontmatter":      "",
        "/RootLevel/Projects/Elegorium":            "",
        "/RootLevel/Projects/githubio":             "",
        "/RootLevel/Projects/Heorot":               "",
        "/RootLevel/Projects/JobSearch":            "",
        "/RootLevel/Projects/My-Articles":          "",
        "/RootLevel/Projects/Parasynthesis":        "",
        "/RootLevel/Projects/Parasynthetic/things": "",
        "/RootLevel/Projects/Parasynthetic/and":    "",
        "/RootLevel/Projects/Parasynthetic/Stuff":  "",
        "/RootLevel/Projects/parasynthetic_dev":    "",
        "/RootLevel/Projects/Timetrap_GoTUI":       "",
        "/RootLevel/Projects/Timetrap_TUI":         "",
    })

    // **create the file system
    pathfs := vfs.NewPathFS(fs, "/")

    rootDir := "/RootLevel"
    root := tview.NewTreeNode("RootLevel").
        SetReference(FileTreeNodeRef{true, true, "/RootLevel"}). // **has a reference
        SetColor(tcell.ColorRed)
    tree := tview.NewTreeView().
        SetRoot(root).
        SetCurrentNode(root)

    // A helper function which adds the files and directories of the given path
    // to the given target node.
    add := func(target *tview.TreeNode, path string) {
        files, err := pathfs.ReadDir(path) // **using vfs here
        if err != nil {
            panic(err)
        }
        for _, file := range files {
            ref := FileTreeNodeRef{ // **setup reference
                false,
                file.IsDir(),
                filepath.Join(path, file.Name()),
            }
            node := tview.NewTreeNode(file.Name()).
                // SetReference(filepath.Join(path, file.Name())).
                SetReference(ref).  // **replaces original SetReference()
                SetSelectable(true) // **always selectable
            if file.IsDir() {
                node.SetExpanded(true) // **directories SHOULD expand by default
                node.SetColor(tcell.ColorGreen)
            }
            target.AddChild(node)
        }
    }

    // Add the current directory to the root node.
    add(root, rootDir)

    // If a directory was selected, open it.
    tree.SetSelectedFunc(func(node *tview.TreeNode) {
        reference := node.GetReference().(FileTreeNodeRef)
        if reference.IsRoot { // **uses reference data
            return // Selecting the root node does nothing.
        } else if !reference.IsDir {
            return // **Stub for actions on Files
        }
        children := node.GetChildren()
        if len(children) == 0 {
            // Load and show files in this directory.
            path := reference.Path
            add(node, path)
        } else {
            // Collapse if visible, expand if collapsed.
            node.SetExpanded(!node.IsExpanded())
        }
    })

    // ** help/info textbox
    info := tview.NewTextView().
        SetText(`
Keyboard inputCapture:
'1':
    FAILS
    tree.GetCurrentNode().Expand()
'2':
    ONLY works AFTER a previous manual expand and collapse
    root.ExpandAll()
'3':
    Works same as root.ExpandAll()
    tree.GetCurrentNode().ExpandAll()
'4':
    WORKS
    tree.GetCurrentNode().Collapse()
'5':
    WORKS
    root.CollapseAll()
'6':
    FAILS
    walk and expand 2 levels from root
`)

    // **layout for multiple widgets
    flex := tview.NewFlex().
        AddItem(tree, 0, 1, true).
        AddItem(info, 0, 1, false)

    // **add app and input handler
    app := tview.NewApplication().
        SetRoot(flex, true)

    app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
        // **showcase functions
        switch event.Key() {
        case tcell.KeyRune:
            switch event.Rune() {
            case '1':
                tree.GetCurrentNode().Expand()
            case '2':
                tree.GetCurrentNode().ExpandAll()
            case '3':
                root.ExpandAll()
            case '4':
                tree.GetCurrentNode().Collapse()
            case '5':
                root.CollapseAll()
            case '6':
                // **attempt to walk the top level tree and expand subdirectories
                // **for simplicity we directly call 2 levels only
                root.SetExpanded(true)
                root.Walk(func(node, parent *tview.TreeNode) bool {
                    if node.GetReference().(FileTreeNodeRef).IsDir {
                        node.SetExpanded(true)

                        node.Walk(func(node, parent *tview.TreeNode) bool {
                            if node.GetReference().(FileTreeNodeRef).IsDir {
                                node.SetExpanded(true)
                            }
                            return true // **second walk
                        })
                    }
                    return true // **first walk
                })
            }
        }
        return event
    })

    appErr := app.Run()
    if appErr != nil {
        panic(appErr)
    }
}
karlredman commented 4 years ago

After digging around for a while I think I've narrowed it down to 1 of 3 options:

  1. Find some way to send a KeyEnter event to the tree node after a SetCurrentNode() call
  2. Ask for a version of SetCurretNode() that will trigger a "changed" callback
  3. copy tview's treeview.go and customize as needed (obviously least desirable)

I'm not sure what the best option is... Option 2 seems like the best one relative to the 'most control' vs 'least intrusive' trade off.

I can submit a PR if you'd like me to demonstrate a public method that triggers the "changed" callback. I'm reluctant to code it if this kind of thing wouldn't be considered good for the library.

karlredman commented 4 years ago

I am closing this ticket with an explanation.

As it turns out I was being silly about the expansion problem. I must have been tired. So, if anyone else thinks they are having this issue keep in mind that the original code doesn't walk the entire tree to add the nodes outright. The fix would be to move the add variable to a plain old recursive function that would resemble something like the following. ...

func add(target *tview.TreeNode, path string, pathfs *vfs.PathFS) {
    files, err := pathfs.ReadDir(path) // **using vfs here
    if err != nil {
        panic(err)
    }
    for _, file := range files {
        ref := FileTreeNodeRef{ // **setup reference
            false,
            file.IsDir(),
            filepath.Join(path, file.Name()),
        }
        node := tview.NewTreeNode(file.Name()).
            // SetReference(filepath.Join(path, file.Name())).
            SetReference(ref).  // **replaces original SetReference()
            SetSelectable(true) // **always selectable
        if file.IsDir() {
            node.SetExpanded(true) // **directories SHOULD expand by default
            node.SetColor(tcell.ColorGreen)
            add(node, ref.Path, pathfs) // ** RECURSIVELY ADD DIRECTORIES
        }
        target.AddChild(node)
    }
}
mih-kopylov commented 1 year ago

@karlredman As for

2. Ask for a version of SetCurretNode() that will trigger a "changed" callback

When changing current selected node manually, sometimes it's essential to trigger a "changed" and a "selected" callback so that the relevant state updates according to the new current node.

Is there a way to do that?