[GH-ISSUE #412] TreeView.ExpandAll()/SetExpanded not working as expected #303

Closed
opened 2026-03-04 01:03:46 +03:00 by kerem · 4 comments
Owner

Originally created by @karlredman on GitHub (Feb 26, 2020).
Original GitHub issue: https://github.com/rivo/tview/issues/412

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:

  • I'm trying to use code similar to the TreeView demo/example
  • I can't seem to get the TreeView to display with all nodes expanded/open upon instantiation
  • I'm unable to find a way to even have the 1st level of nodes bellow the root node to expand without manually selecting them

What is hapening:

  • The TreeView nodes will expand when selected but I can't get them to be all initially open
  • The ExpandAll() function only works on nodes that have been manually opened Previously.
  • Expand() doesn't seem to work at all (except for the root node)
  • SetExpanded() only seems to work on root node
  • The ColapseAll() and Collapse() work as expected.
  • Using Walk() with node.SetExpanded(true) doesn't do anything

What is expected:

  • Some way to expand at least the 1st 2 levels of a TreeView under it's root node
  • ExpandAll should expand at least the 1st level of directories under the root node
  • I would like to have the tree's initial state to be something like this:
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)
	}
}

Originally created by @karlredman on GitHub (Feb 26, 2020). Original GitHub issue: https://github.com/rivo/tview/issues/412 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: * I'm trying to use code similar to the TreeView [demo/example](https://github.com/rivo/tview/blob/master/demos/treeview/main.go) * I can't seem to get the TreeView to display with all nodes expanded/open upon instantiation * I'm unable to find a way to even have the 1st level of nodes bellow the root node to expand without manually selecting them ## What is hapening: * The TreeView nodes will expand when selected but I can't get them to be all initially open * The `ExpandAll()` function only works on nodes that have been manually opened Previously. * `Expand()` doesn't seem to work at all (except for the root node) * `SetExpanded()` only seems to work on root node * The `ColapseAll()` and `Collapse()` work as expected. * Using `Walk()` with `node.SetExpanded(true`) doesn't do anything ## What is expected: * Some way to expand *at least* the 1st 2 levels of a TreeView under it's *root* node * `ExpandAll` should expand *at least* the 1st level of directories under the *root* node * I would like to have the tree's initial state to be something like this: ```sh 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. ```go 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) } } ```
kerem closed this issue 2026-03-04 01:03:46 +03:00
Author
Owner

@karlredman commented on GitHub (Feb 26, 2020):

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.

<!-- gh-comment-id:591318746 --> @karlredman commented on GitHub (Feb 26, 2020): 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.
Author
Owner

@karlredman commented on GitHub (Mar 2, 2020):

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)
	}
}

<!-- gh-comment-id:593553977 --> @karlredman commented on GitHub (Mar 2, 2020): 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. ... ```go 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) } } ```
Author
Owner

@mih-kopylov commented on GitHub (Jan 6, 2023):

@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?

<!-- gh-comment-id:1373955407 --> @mih-kopylov commented on GitHub (Jan 6, 2023): @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?
Author
Owner

@quantonganh commented on GitHub (Dec 20, 2024):

@mih-kopylov Could you please try something like this?

tree.SetCurrentNode(node)
if selectedFunc := tree.GetSelectedFunc(); selectedFunc != nil {
    selectedFunc(node)
}
<!-- gh-comment-id:2556283121 --> @quantonganh commented on GitHub (Dec 20, 2024): @mih-kopylov Could you please try something like this? ```go tree.SetCurrentNode(node) if selectedFunc := tree.GetSelectedFunc(); selectedFunc != nil { selectedFunc(node) } ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/tview#303
No description provided.