mirror of https://github.com/Bios-Marcel/cordless
920 lines
24 KiB
Go
920 lines
24 KiB
Go
package tview
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
tcell "github.com/gdamore/tcell/v2"
|
|
)
|
|
|
|
// Tree navigation events.
|
|
const (
|
|
treeNone int = iota
|
|
treeHome
|
|
treeEnd
|
|
treeUp
|
|
treeDown
|
|
treePageUp
|
|
treePageDown
|
|
)
|
|
|
|
// TreeNode represents one node in a tree view.
|
|
type TreeNode struct {
|
|
// The reference object.
|
|
reference interface{}
|
|
|
|
// This node's child nodes.
|
|
children []*TreeNode
|
|
|
|
// The item's text.
|
|
text string
|
|
|
|
// This text is a prefixes in front of the normal text.
|
|
prefixes []string
|
|
|
|
// The text color.
|
|
color tcell.Color
|
|
|
|
blinking bool
|
|
|
|
underline bool
|
|
|
|
// Whether or not this node can be selected.
|
|
selectable bool
|
|
|
|
// Whether or not this node's children should be displayed.
|
|
expanded bool
|
|
|
|
// The additional horizontal indent of this node's text.
|
|
indent int
|
|
|
|
// An optional function which is called when the user selects this node.
|
|
selected func()
|
|
|
|
// Temporary member variables.
|
|
parent *TreeNode // The parent node (nil for the root).
|
|
level int // The hierarchy level (0 for the root, 1 for its children, and so on).
|
|
graphicsX int // The x-coordinate of the left-most graphics rune.
|
|
textX int // The x-coordinate of the first rune of the text.
|
|
}
|
|
|
|
// NewTreeNode returns a new tree node.
|
|
func NewTreeNode(text string) *TreeNode {
|
|
return &TreeNode{
|
|
text: text,
|
|
color: Styles.PrimaryTextColor,
|
|
indent: 2,
|
|
expanded: true,
|
|
selectable: true,
|
|
}
|
|
}
|
|
|
|
// Walk traverses this node's subtree in depth-first, pre-order (NLR) order and
|
|
// calls the provided callback function on each traversed node (which includes
|
|
// this node) with the traversed node and its parent node (nil for this node).
|
|
// The callback returns whether traversal should continue with the traversed
|
|
// node's child nodes (true) or not recurse any deeper (false).
|
|
func (n *TreeNode) Walk(callback func(node, parent *TreeNode) bool) *TreeNode {
|
|
n.parent = nil
|
|
nodes := []*TreeNode{n}
|
|
for len(nodes) > 0 {
|
|
// Pop the top node and process it.
|
|
node := nodes[len(nodes)-1]
|
|
nodes = nodes[:len(nodes)-1]
|
|
if !callback(node, node.parent) {
|
|
// Don't add any children.
|
|
continue
|
|
}
|
|
|
|
// Add children in reverse order.
|
|
for index := len(node.children) - 1; index >= 0; index-- {
|
|
node.children[index].parent = node
|
|
nodes = append(nodes, node.children[index])
|
|
}
|
|
}
|
|
|
|
return n
|
|
}
|
|
|
|
// SetReference allows you to store a reference of any type in this node. This
|
|
// will allow you to establish a mapping between the TreeView hierarchy and your
|
|
// internal tree structure.
|
|
func (n *TreeNode) SetReference(reference interface{}) *TreeNode {
|
|
n.reference = reference
|
|
return n
|
|
}
|
|
|
|
// GetReference returns this node's reference object.
|
|
func (n *TreeNode) GetReference() interface{} {
|
|
return n.reference
|
|
}
|
|
|
|
// SetChildren sets this node's child nodes.
|
|
func (n *TreeNode) SetChildren(childNodes []*TreeNode) *TreeNode {
|
|
n.children = childNodes
|
|
return n
|
|
}
|
|
|
|
// GetText returns this node's text.
|
|
func (n *TreeNode) GetText() string {
|
|
return n.text
|
|
}
|
|
|
|
// GetPrefixes returns this node's prefix text.
|
|
func (n *TreeNode) GetPrefixes() []string {
|
|
return n.prefixes
|
|
}
|
|
|
|
// GetParent returns a refrence to this nodes parent node or nil if this is a
|
|
// root node.
|
|
func (n *TreeNode) GetParent() *TreeNode {
|
|
return n.parent
|
|
}
|
|
|
|
// GetChildren returns this node's children.
|
|
func (n *TreeNode) GetChildren() []*TreeNode {
|
|
return n.children
|
|
}
|
|
|
|
// ClearChildren removes all child nodes from this node.
|
|
func (n *TreeNode) ClearChildren() *TreeNode {
|
|
n.children = nil
|
|
return n
|
|
}
|
|
|
|
// AddChild adds a new child node to this node.
|
|
func (n *TreeNode) AddChild(node *TreeNode) *TreeNode {
|
|
n.children = append(n.children, node)
|
|
return n
|
|
}
|
|
|
|
// SetSelectable sets a flag indicating whether this node can be selected by
|
|
// the user.
|
|
func (n *TreeNode) SetSelectable(selectable bool) *TreeNode {
|
|
n.selectable = selectable
|
|
return n
|
|
}
|
|
|
|
// IsSelectable returns wether the node is selectable or not.
|
|
func (n *TreeNode) IsSelectable() bool {
|
|
return n.selectable
|
|
}
|
|
|
|
// SetSelectedFunc sets a function which is called when the user selects this
|
|
// node by hitting Enter when it is selected.
|
|
func (n *TreeNode) SetSelectedFunc(handler func()) *TreeNode {
|
|
n.selected = handler
|
|
return n
|
|
}
|
|
|
|
// SetExpanded sets whether or not this node's child nodes should be displayed.
|
|
func (n *TreeNode) SetExpanded(expanded bool) *TreeNode {
|
|
n.expanded = expanded
|
|
return n
|
|
}
|
|
|
|
// Expand makes the child nodes of this node appear.
|
|
func (n *TreeNode) Expand() *TreeNode {
|
|
n.expanded = true
|
|
return n
|
|
}
|
|
|
|
// Collapse makes the child nodes of this node disappear.
|
|
func (n *TreeNode) Collapse() *TreeNode {
|
|
n.expanded = false
|
|
return n
|
|
}
|
|
|
|
// ExpandAll expands this node and all descendent nodes.
|
|
func (n *TreeNode) ExpandAll() *TreeNode {
|
|
n.Walk(func(node, parent *TreeNode) bool {
|
|
node.expanded = true
|
|
return true
|
|
})
|
|
return n
|
|
}
|
|
|
|
// CollapseAll collapses this node and all descendent nodes.
|
|
func (n *TreeNode) CollapseAll() *TreeNode {
|
|
n.Walk(func(node, parent *TreeNode) bool {
|
|
n.expanded = false
|
|
return true
|
|
})
|
|
return n
|
|
}
|
|
|
|
// IsExpanded returns whether the child nodes of this node are visible.
|
|
func (n *TreeNode) IsExpanded() bool {
|
|
return n.expanded
|
|
}
|
|
|
|
// SetText sets the node's text which is displayed.
|
|
func (n *TreeNode) SetText(text string) *TreeNode {
|
|
n.text = text
|
|
return n
|
|
}
|
|
|
|
// AddPrefix sets the node's prefix text which is displayed. Duplicates are
|
|
// ignored.
|
|
func (n *TreeNode) AddPrefix(newPrefix string) *TreeNode {
|
|
for _, prefix := range n.prefixes {
|
|
if prefix == newPrefix {
|
|
return n
|
|
}
|
|
}
|
|
|
|
n.prefixes = append(n.prefixes, newPrefix)
|
|
return n
|
|
}
|
|
|
|
// RemovePrefix removes the given prefix, maintaining the order of the items.
|
|
func (n *TreeNode) RemovePrefix(prefix string) {
|
|
for removeIndex, oldPrefix := range n.prefixes {
|
|
if oldPrefix == prefix {
|
|
n.prefixes = append(n.prefixes[:removeIndex], n.prefixes[removeIndex+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// ClearPrefixes removes all prefixes by nulling the underlying array.
|
|
func (n *TreeNode) ClearPrefixes() {
|
|
n.prefixes = nil
|
|
}
|
|
|
|
// SortPrefixes sorts all currently set prefixes. This function sin't executed
|
|
// once new prefixes are added.
|
|
func (n *TreeNode) SortPrefixes(lessFunction func(a, b string) bool) {
|
|
sort.Slice(n.prefixes, func(a, b int) bool {
|
|
aItem := n.prefixes[a]
|
|
bItem := n.prefixes[b]
|
|
return lessFunction(aItem, bItem)
|
|
})
|
|
}
|
|
|
|
// GetColor returns the node's color.
|
|
func (n *TreeNode) GetColor() tcell.Color {
|
|
return n.color
|
|
}
|
|
|
|
// SetColor sets the node's text color.
|
|
func (n *TreeNode) SetColor(color tcell.Color) *TreeNode {
|
|
n.color = color
|
|
return n
|
|
}
|
|
|
|
func (n *TreeNode) SetUnderline(underline bool) {
|
|
n.underline = underline
|
|
}
|
|
|
|
func (n *TreeNode) SetBlinking(blinking bool) {
|
|
n.blinking = blinking
|
|
}
|
|
|
|
// SetIndent sets an additional indentation for this node's text. A value of 0
|
|
// keeps the text as far left as possible with a minimum of line graphics. Any
|
|
// value greater than that moves the text to the right.
|
|
func (n *TreeNode) SetIndent(indent int) *TreeNode {
|
|
n.indent = indent
|
|
return n
|
|
}
|
|
|
|
// TreeView displays tree structures. A tree consists of nodes (TreeNode
|
|
// objects) where each node has zero or more child nodes and exactly one parent
|
|
// node (except for the root node which has no parent node).
|
|
//
|
|
// The SetRoot() function is used to specify the root of the tree. Other nodes
|
|
// are added locally to the root node or any of its descendents. See the
|
|
// TreeNode documentation for details on node attributes. (You can use
|
|
// SetReference() to store a reference to nodes of your own tree structure.)
|
|
//
|
|
// Nodes can be selected by calling SetCurrentNode(). The user can navigate the
|
|
// selection or the tree by using the following keys:
|
|
//
|
|
// - j, down arrow, right arrow: Move (the selection) down by one node.
|
|
// - k, up arrow, left arrow: Move (the selection) up by one node.
|
|
// - g, home: Move (the selection) to the top.
|
|
// - G, end: Move (the selection) to the bottom.
|
|
// - Ctrl-F, page down: Move (the selection) down by one page.
|
|
// - Ctrl-B, page up: Move (the selection) up by one page.
|
|
//
|
|
// Selected nodes can trigger the "selected" callback when the user hits Enter.
|
|
//
|
|
// The root node corresponds to level 0, its children correspond to level 1,
|
|
// their children to level 2, and so on. Per default, the first level that is
|
|
// displayed is 0, i.e. the root node. You can call SetTopLevel() to hide
|
|
// levels.
|
|
//
|
|
// If graphics are turned on (see SetGraphics()), lines indicate the tree's
|
|
// hierarchy. Alternative (or additionally), you can set different prefixes
|
|
// using SetPrefixes() for different levels, for example to display hierarchical
|
|
// bullet point lists.
|
|
//
|
|
// See https://github.com/Bios-Marcel/cordless/tview/wiki/TreeView for an example.
|
|
type TreeView struct {
|
|
*Box
|
|
|
|
// The root node.
|
|
root *TreeNode
|
|
|
|
// The currently selected node or nil if no node is selected.
|
|
currentNode *TreeNode
|
|
|
|
// The movement to be performed during the call to Draw(), one of the
|
|
// constants defined above.
|
|
movement int
|
|
|
|
// The top hierarchical level shown. (0 corresponds to the root level.)
|
|
topLevel int
|
|
|
|
// Strings drawn before the nodes, based on their level.
|
|
bulletCharacters []string
|
|
|
|
// This decides whether the selection will cycle when reaching the end
|
|
// or the beginning of the tree.
|
|
cycleSelection bool
|
|
|
|
// Vertical scroll offset.
|
|
offsetY int
|
|
|
|
// If set to true, all node texts will be aligned horizontally.
|
|
align bool
|
|
|
|
// If set to true, the tree structure is drawn using lines.
|
|
graphics bool
|
|
|
|
// The color of the lines.
|
|
graphicsColor tcell.Color
|
|
|
|
// An optional function which is called when the user has navigated to a new
|
|
// tree node.
|
|
changed func(node *TreeNode)
|
|
|
|
// An optional function which is called when a tree item was selected.
|
|
selected func(node *TreeNode)
|
|
|
|
// The visible nodes, top-down, as set by process().
|
|
nodes []*TreeNode
|
|
|
|
// Decides wether it allows usage of vim bindings for navigation.
|
|
vimBindings bool
|
|
|
|
// Decides wether a search and selection will be triggered on rune input.
|
|
searchOnType bool
|
|
|
|
jumpTime time.Time
|
|
jumpBuffer string
|
|
}
|
|
|
|
// NewTreeView returns a new tree view.
|
|
func NewTreeView() *TreeView {
|
|
return &TreeView{
|
|
Box: NewBox(),
|
|
graphics: true,
|
|
graphicsColor: Styles.GraphicsColor,
|
|
searchOnType: true,
|
|
vimBindings: false,
|
|
}
|
|
}
|
|
|
|
// SetRoot sets the root node of the tree.
|
|
func (t *TreeView) SetRoot(root *TreeNode) *TreeView {
|
|
t.root = root
|
|
return t
|
|
}
|
|
|
|
// GetRoot returns the root node of the tree. If no such node was previously
|
|
// set, nil is returned.
|
|
func (t *TreeView) GetRoot() *TreeNode {
|
|
return t.root
|
|
}
|
|
|
|
// SetCurrentNode sets the currently selected node. Provide nil to clear all
|
|
// selections. Selected nodes must be visible and selectable, or else the
|
|
// selection will be changed to the top-most selectable and visible node.
|
|
//
|
|
// This function does NOT trigger the "changed" callback.
|
|
func (t *TreeView) SetCurrentNode(node *TreeNode) *TreeView {
|
|
t.currentNode = node
|
|
return t
|
|
}
|
|
|
|
// GetCurrentNode returns the currently selected node or nil of no node is
|
|
// currently selected.
|
|
func (t *TreeView) GetCurrentNode() *TreeNode {
|
|
return t.currentNode
|
|
}
|
|
|
|
// SetTopLevel sets the first tree level that is visible with 0 referring to the
|
|
// root, 1 to the root's child nodes, and so on. Nodes above the top level are
|
|
// not displayed.
|
|
func (t *TreeView) SetTopLevel(topLevel int) *TreeView {
|
|
t.topLevel = topLevel
|
|
return t
|
|
}
|
|
|
|
// SetBulletCharacters defines the strings drawn before the nodes' texts.
|
|
// This is a slice of strings where each element corresponds to a node's
|
|
// hierarchy level, i.e. 0 for the root, 1 for the root's children, and
|
|
// so on (levels will cycle).
|
|
//
|
|
// For example, to display a hierarchical list with bullet points:
|
|
//
|
|
// treeView.SetGraphics(false).
|
|
// SetBulletCharacters([]string{"* ", "- ", "x "})
|
|
func (t *TreeView) SetBulletCharacters(prefixes []string) *TreeView {
|
|
t.bulletCharacters = prefixes
|
|
return t
|
|
}
|
|
|
|
// SetAlign controls the horizontal alignment of the node texts. If set to true,
|
|
// all texts except that of top-level nodes will be placed in the same column.
|
|
// If set to false, they will indent with the hierarchy.
|
|
func (t *TreeView) SetAlign(align bool) *TreeView {
|
|
t.align = align
|
|
return t
|
|
}
|
|
|
|
// SetCycleSelection controls the selection behaviour when there is no more
|
|
// node that is eligible for selection. For example if you are index 1 and
|
|
// index 0 isn't selectable anymore, it will try selecting the last index.
|
|
func (t *TreeView) SetCycleSelection(cycleSelection bool) *TreeView {
|
|
t.cycleSelection = cycleSelection
|
|
return t
|
|
}
|
|
|
|
// SetGraphics sets a flag which determines whether or not line graphics are
|
|
// drawn to illustrate the tree's hierarchy.
|
|
func (t *TreeView) SetGraphics(showGraphics bool) *TreeView {
|
|
t.graphics = showGraphics
|
|
return t
|
|
}
|
|
|
|
// SetGraphicsColor sets the colors of the lines used to draw the tree structure.
|
|
func (t *TreeView) SetGraphicsColor(color tcell.Color) *TreeView {
|
|
t.graphicsColor = color
|
|
return t
|
|
}
|
|
|
|
// SetChangedFunc sets the function which is called when the user navigates to
|
|
// a new tree node.
|
|
func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) *TreeView {
|
|
t.changed = handler
|
|
return t
|
|
}
|
|
|
|
// SetVimBindingsEnabled decides wether the usage of vim bindings for
|
|
// navigation is possible or not. This setting disables the search on
|
|
// rune input.
|
|
func (t *TreeView) SetVimBindingsEnabled(enabled bool) *TreeView {
|
|
t.vimBindings = enabled
|
|
|
|
if enabled {
|
|
t.searchOnType = false
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
// SetSearchOnTypeEnabled enables / disables search through the tree on rune
|
|
// input.
|
|
func (t *TreeView) SetSearchOnTypeEnabled(enabled bool) *TreeView {
|
|
t.searchOnType = enabled
|
|
|
|
if enabled {
|
|
t.vimBindings = false
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
// SetSelectedFunc sets the function which is called when the user selects a
|
|
// node by pressing Enter on the current selection.
|
|
func (t *TreeView) SetSelectedFunc(handler func(node *TreeNode)) *TreeView {
|
|
t.selected = handler
|
|
return t
|
|
}
|
|
|
|
// GetScrollOffset returns the number of node rows that were skipped at the top
|
|
// of the tree view. Note that when the user navigates the tree view, this value
|
|
// is only updated after the tree view has been redrawn.
|
|
func (t *TreeView) GetScrollOffset() int {
|
|
return t.offsetY
|
|
}
|
|
|
|
// GetRowCount returns the number of "visible" nodes. This includes nodes which
|
|
// fall outside the tree view's box but notably does not include the children
|
|
// of collapsed nodes. Note that this value is only up to date after the tree
|
|
// view has been drawn.
|
|
func (t *TreeView) GetRowCount() int {
|
|
return len(t.nodes)
|
|
}
|
|
|
|
// process builds the visible tree, populates the "nodes" slice, and processes
|
|
// pending selection actions.
|
|
func (t *TreeView) process() {
|
|
_, _, _, height := t.GetInnerRect()
|
|
|
|
// Determine visible nodes and their placement.
|
|
var graphicsOffset, maxTextX int
|
|
t.nodes = nil
|
|
selectedIndex := -1
|
|
topLevelGraphicsX := -1
|
|
if t.graphics {
|
|
graphicsOffset = 1
|
|
}
|
|
t.root.Walk(func(node, parent *TreeNode) bool {
|
|
// Set node attributes.
|
|
node.parent = parent
|
|
if parent == nil {
|
|
node.level = 0
|
|
node.graphicsX = 0
|
|
node.textX = 0
|
|
} else {
|
|
node.level = parent.level + 1
|
|
node.graphicsX = parent.textX
|
|
node.textX = node.graphicsX + graphicsOffset + node.indent
|
|
}
|
|
if !t.graphics && t.align {
|
|
// Without graphics, we align nodes on the first column.
|
|
node.textX = 0
|
|
}
|
|
if node.level == t.topLevel {
|
|
// No graphics for top level nodes.
|
|
node.graphicsX = 0
|
|
node.textX = 0
|
|
}
|
|
|
|
// Add the node to the list.
|
|
if node.level >= t.topLevel {
|
|
// This node will be visible.
|
|
if node.textX > maxTextX {
|
|
maxTextX = node.textX
|
|
}
|
|
if node == t.currentNode && node.selectable {
|
|
selectedIndex = len(t.nodes)
|
|
}
|
|
|
|
// Maybe we want to skip this level.
|
|
if t.topLevel == node.level && (topLevelGraphicsX < 0 || node.graphicsX < topLevelGraphicsX) {
|
|
topLevelGraphicsX = node.graphicsX
|
|
}
|
|
|
|
t.nodes = append(t.nodes, node)
|
|
}
|
|
|
|
// Recurse if desired.
|
|
return node.expanded
|
|
})
|
|
|
|
// Post-process positions.
|
|
for _, node := range t.nodes {
|
|
// If text must align, we correct the positions.
|
|
if t.align && node.level > t.topLevel {
|
|
node.textX = maxTextX
|
|
}
|
|
|
|
// If we skipped levels, shift to the left.
|
|
if topLevelGraphicsX > 0 {
|
|
node.graphicsX -= topLevelGraphicsX
|
|
node.textX -= topLevelGraphicsX
|
|
}
|
|
}
|
|
|
|
// Process selection. (Also trigger events if necessary.)
|
|
if selectedIndex >= 0 {
|
|
// Move the selection.
|
|
newSelectedIndex := selectedIndex
|
|
MovementSwitch:
|
|
switch t.movement {
|
|
case treeUp:
|
|
for newSelectedIndex > 0 {
|
|
newSelectedIndex--
|
|
if t.nodes[newSelectedIndex].selectable {
|
|
break MovementSwitch
|
|
}
|
|
}
|
|
|
|
//Cycle to the bottom if we are at the top.
|
|
if t.cycleSelection {
|
|
newSelectedIndex = len(t.nodes)
|
|
for newSelectedIndex > selectedIndex {
|
|
newSelectedIndex--
|
|
if t.nodes[newSelectedIndex].selectable {
|
|
break MovementSwitch
|
|
}
|
|
}
|
|
}
|
|
|
|
newSelectedIndex = selectedIndex
|
|
|
|
case treeDown:
|
|
for newSelectedIndex < len(t.nodes)-1 {
|
|
newSelectedIndex++
|
|
if t.nodes[newSelectedIndex].selectable {
|
|
break MovementSwitch
|
|
}
|
|
}
|
|
|
|
//Cycle to the top if we are at the bottom.
|
|
if t.cycleSelection {
|
|
newSelectedIndex = -1
|
|
for newSelectedIndex < selectedIndex {
|
|
newSelectedIndex++
|
|
if t.nodes[newSelectedIndex].selectable {
|
|
break MovementSwitch
|
|
}
|
|
}
|
|
}
|
|
|
|
newSelectedIndex = selectedIndex
|
|
case treeHome:
|
|
for newSelectedIndex = 0; newSelectedIndex < len(t.nodes); newSelectedIndex++ {
|
|
if t.nodes[newSelectedIndex].selectable {
|
|
break MovementSwitch
|
|
}
|
|
}
|
|
newSelectedIndex = selectedIndex
|
|
case treeEnd:
|
|
for newSelectedIndex = len(t.nodes) - 1; newSelectedIndex >= 0; newSelectedIndex-- {
|
|
if t.nodes[newSelectedIndex].selectable {
|
|
break MovementSwitch
|
|
}
|
|
}
|
|
newSelectedIndex = selectedIndex
|
|
case treePageDown:
|
|
if newSelectedIndex+height < len(t.nodes) {
|
|
newSelectedIndex += height
|
|
} else {
|
|
newSelectedIndex = len(t.nodes) - 1
|
|
}
|
|
for ; newSelectedIndex < len(t.nodes); newSelectedIndex++ {
|
|
if t.nodes[newSelectedIndex].selectable {
|
|
break MovementSwitch
|
|
}
|
|
}
|
|
newSelectedIndex = selectedIndex
|
|
case treePageUp:
|
|
if newSelectedIndex >= height {
|
|
newSelectedIndex -= height
|
|
} else {
|
|
newSelectedIndex = 0
|
|
}
|
|
for ; newSelectedIndex >= 0; newSelectedIndex-- {
|
|
if t.nodes[newSelectedIndex].selectable {
|
|
break MovementSwitch
|
|
}
|
|
}
|
|
newSelectedIndex = selectedIndex
|
|
}
|
|
t.currentNode = t.nodes[newSelectedIndex]
|
|
if newSelectedIndex != selectedIndex {
|
|
t.movement = treeNone
|
|
if t.changed != nil {
|
|
t.changed(t.currentNode)
|
|
}
|
|
}
|
|
selectedIndex = newSelectedIndex
|
|
|
|
// Move selection into viewport.
|
|
if selectedIndex-t.offsetY >= height {
|
|
t.offsetY = selectedIndex - height + 1
|
|
}
|
|
if selectedIndex < t.offsetY {
|
|
t.offsetY = selectedIndex
|
|
}
|
|
} else {
|
|
// If selection is not visible or selectable, select the first candidate.
|
|
if t.currentNode != nil {
|
|
for index, node := range t.nodes {
|
|
if node.selectable {
|
|
selectedIndex = index
|
|
t.currentNode = node
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if selectedIndex < 0 {
|
|
t.currentNode = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw draws this primitive onto the screen.
|
|
func (t *TreeView) Draw(screen tcell.Screen) bool {
|
|
res := t.Box.Draw(screen)
|
|
if !res {
|
|
return false
|
|
}
|
|
|
|
if t.root == nil {
|
|
return false
|
|
}
|
|
|
|
t.process()
|
|
|
|
// Scroll the tree.
|
|
x, y, width, height := t.GetInnerRect()
|
|
switch t.movement {
|
|
case treeUp:
|
|
t.offsetY--
|
|
case treeDown:
|
|
t.offsetY++
|
|
case treeHome:
|
|
t.offsetY = 0
|
|
case treeEnd:
|
|
t.offsetY = len(t.nodes)
|
|
case treePageUp:
|
|
t.offsetY -= height
|
|
case treePageDown:
|
|
t.offsetY += height
|
|
}
|
|
t.movement = treeNone
|
|
|
|
// Fix invalid offsets.
|
|
if t.offsetY >= len(t.nodes)-height {
|
|
t.offsetY = len(t.nodes) - height
|
|
}
|
|
if t.offsetY < 0 {
|
|
t.offsetY = 0
|
|
}
|
|
|
|
// Draw the tree.
|
|
posY := y
|
|
lineStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.graphicsColor)
|
|
for index, node := range t.nodes {
|
|
// Skip invisible parts.
|
|
if posY >= y+height+1 {
|
|
break
|
|
}
|
|
if index < t.offsetY {
|
|
continue
|
|
}
|
|
|
|
// Draw the graphics.
|
|
if t.graphics {
|
|
// Draw ancestor branches.
|
|
ancestor := node.parent
|
|
for ancestor != nil && ancestor.parent != nil && ancestor.parent.level >= t.topLevel {
|
|
if ancestor.graphicsX >= width {
|
|
continue
|
|
}
|
|
|
|
// Draw a branch if this ancestor is not a last child.
|
|
if ancestor.parent.children[len(ancestor.parent.children)-1] != ancestor {
|
|
if posY-1 >= y && ancestor.textX > ancestor.graphicsX {
|
|
PrintJoinedSemigraphics(screen, x+ancestor.graphicsX, posY-1, Borders.Vertical, t.graphicsColor)
|
|
}
|
|
if posY < y+height {
|
|
screen.SetContent(x+ancestor.graphicsX, posY, Borders.Vertical, nil, lineStyle)
|
|
}
|
|
}
|
|
ancestor = ancestor.parent
|
|
}
|
|
|
|
if node.textX > node.graphicsX && node.graphicsX < width {
|
|
// Connect to the node above.
|
|
if posY-1 >= y && t.nodes[index-1].graphicsX <= node.graphicsX && t.nodes[index-1].textX > node.graphicsX {
|
|
PrintJoinedSemigraphics(screen, x+node.graphicsX, posY-1, Borders.TopLeft, t.graphicsColor)
|
|
}
|
|
|
|
// Join this node.
|
|
if posY < y+height {
|
|
screen.SetContent(x+node.graphicsX, posY, Borders.BottomLeft, nil, lineStyle)
|
|
for pos := node.graphicsX + 1; pos < node.textX && pos < width; pos++ {
|
|
screen.SetContent(x+pos, posY, Borders.Horizontal, nil, lineStyle)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw the prefix and the text.
|
|
if node.textX < width && posY < y+height {
|
|
// Prefix.
|
|
var bulletCharacterWidth int
|
|
if len(t.bulletCharacters) > 0 {
|
|
_, bulletCharacterWidth = Print(screen, t.bulletCharacters[(node.level-t.topLevel)%len(t.bulletCharacters)], x+node.textX, posY, width-node.textX, AlignLeft, node.color)
|
|
}
|
|
|
|
// Text.
|
|
if node.textX+bulletCharacterWidth < width {
|
|
style := tcell.StyleDefault
|
|
if !IsVtxxx {
|
|
style = style.Foreground(node.color)
|
|
}
|
|
if node == t.currentNode {
|
|
style = style.Reverse(true)
|
|
}
|
|
if node.blinking {
|
|
style = style.Blink(true)
|
|
}
|
|
if node.underline {
|
|
style = style.Underline(true)
|
|
}
|
|
var fullPrefix string
|
|
for _, prefix := range node.prefixes {
|
|
fullPrefix += prefix
|
|
}
|
|
printWithStyle(screen, fullPrefix+node.text, x+node.textX+bulletCharacterWidth, posY, width-node.textX-bulletCharacterWidth, AlignLeft, style)
|
|
}
|
|
}
|
|
|
|
// Advance.
|
|
posY++
|
|
}
|
|
|
|
t.drawOverflow(screen, t.offsetY != 0, (t.offsetY != len(t.nodes)-t.innerHeight) && len(t.nodes) > t.innerHeight)
|
|
|
|
return true
|
|
}
|
|
|
|
// InputHandler returns the handler for this primitive.
|
|
func (t *TreeView) InputHandler() InputHandlerFunc {
|
|
return t.WrapInputHandler(t.DefaultInputHandler)
|
|
}
|
|
|
|
// DefaultInputHandler handles basic navigation and interaction with the
|
|
// TreeView.
|
|
func (t *TreeView) DefaultInputHandler(event *tcell.EventKey, setFocus func(p Primitive)) *tcell.EventKey {
|
|
// Because the tree is flattened into a list only at drawing time, we also
|
|
// postpone the (selection) movement to drawing time.
|
|
switch key := event.Key(); key {
|
|
case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight:
|
|
t.movement = treeDown
|
|
case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft:
|
|
t.movement = treeUp
|
|
case tcell.KeyHome:
|
|
t.movement = treeHome
|
|
case tcell.KeyEnd:
|
|
t.movement = treeEnd
|
|
case tcell.KeyPgDn, tcell.KeyCtrlF:
|
|
t.movement = treePageDown
|
|
case tcell.KeyPgUp, tcell.KeyCtrlB:
|
|
t.movement = treePageUp
|
|
case tcell.KeyRune:
|
|
if t.vimBindings {
|
|
switch event.Rune() {
|
|
case 'g':
|
|
t.movement = treeHome
|
|
case 'G':
|
|
t.movement = treeEnd
|
|
case 'j':
|
|
t.movement = treeDown
|
|
case 'k':
|
|
t.movement = treeUp
|
|
default:
|
|
return event
|
|
}
|
|
} else if t.searchOnType {
|
|
if time.Since(t.jumpTime) > (500 * time.Millisecond) {
|
|
t.jumpBuffer = ""
|
|
}
|
|
|
|
if event.Key() == tcell.KeyRune {
|
|
t.jumpTime = time.Now()
|
|
t.jumpBuffer += strings.ToLower(string(event.Rune()))
|
|
|
|
node := t.FindFirstSelectableNode(t.GetRoot(), t.jumpBuffer)
|
|
if node != nil {
|
|
t.SetCurrentNode(node)
|
|
}
|
|
}
|
|
}
|
|
case tcell.KeyEnter:
|
|
if t.currentNode != nil {
|
|
if t.selected != nil {
|
|
t.selected(t.currentNode)
|
|
}
|
|
if t.currentNode.selected != nil {
|
|
t.currentNode.selected()
|
|
}
|
|
}
|
|
default:
|
|
return event
|
|
}
|
|
|
|
t.process()
|
|
return nil
|
|
}
|
|
|
|
// FindFirstSelectableNode iterates through the tree from top to bottom, trying
|
|
// to find a node that is selectable and has the given text as its prefix. The
|
|
// search is case-insensitive.
|
|
func (t *TreeView) FindFirstSelectableNode(node *TreeNode, text string) *TreeNode {
|
|
for _, child := range node.GetChildren() {
|
|
if len(child.GetChildren()) == 0 {
|
|
if child.IsSelectable() && strings.HasPrefix(strings.ToLower(child.GetText()), text) {
|
|
return child
|
|
}
|
|
} else {
|
|
subResult := t.FindFirstSelectableNode(child, text)
|
|
if subResult != nil {
|
|
return subResult
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|