1058 lines
25 KiB
Go
1058 lines
25 KiB
Go
package widget
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/data/binding"
|
|
"fyne.io/fyne/v2/driver/desktop"
|
|
"fyne.io/fyne/v2/internal/cache"
|
|
"fyne.io/fyne/v2/internal/widget"
|
|
"fyne.io/fyne/v2/theme"
|
|
)
|
|
|
|
// allTreeNodesID represents all tree nodes when refreshing requested nodes
|
|
const allTreeNodesID = "_ALLNODES"
|
|
|
|
// TreeNodeID represents the unique id of a tree node.
|
|
type TreeNodeID = string
|
|
|
|
// Declare conformity with interfaces
|
|
var _ fyne.Focusable = (*Tree)(nil)
|
|
var _ fyne.Widget = (*Tree)(nil)
|
|
|
|
// Tree widget displays hierarchical data.
|
|
// Each node of the tree must be identified by a Unique TreeNodeID.
|
|
//
|
|
// Since: 1.4
|
|
type Tree struct {
|
|
BaseWidget
|
|
Root TreeNodeID
|
|
|
|
ChildUIDs func(uid TreeNodeID) (c []TreeNodeID) `json:"-"` // Return a sorted slice of Children TreeNodeIDs for the given Node TreeNodeID
|
|
CreateNode func(branch bool) (o fyne.CanvasObject) `json:"-"` // Return a CanvasObject that can represent a Branch (if branch is true), or a Leaf (if branch is false)
|
|
IsBranch func(uid TreeNodeID) (ok bool) `json:"-"` // Return true if the given TreeNodeID represents a Branch
|
|
OnBranchClosed func(uid TreeNodeID) `json:"-"` // Called when a Branch is closed
|
|
OnBranchOpened func(uid TreeNodeID) `json:"-"` // Called when a Branch is opened
|
|
OnSelected func(uid TreeNodeID) `json:"-"` // Called when the Node with the given TreeNodeID is selected.
|
|
OnUnselected func(uid TreeNodeID) `json:"-"` // Called when the Node with the given TreeNodeID is unselected.
|
|
UpdateNode func(uid TreeNodeID, branch bool, node fyne.CanvasObject) `json:"-"` // Called to update the given CanvasObject to represent the data at the given TreeNodeID
|
|
|
|
branchMinSize fyne.Size
|
|
currentFocus TreeNodeID
|
|
focused bool
|
|
leafMinSize fyne.Size
|
|
offset fyne.Position
|
|
open map[TreeNodeID]bool
|
|
scroller *widget.Scroll
|
|
selected []TreeNodeID
|
|
}
|
|
|
|
// NewTree returns a new performant tree widget defined by the passed functions.
|
|
// childUIDs returns the child TreeNodeIDs of the given node.
|
|
// isBranch returns true if the given node is a branch, false if it is a leaf.
|
|
// create returns a new template object that can be cached.
|
|
// update is used to apply data at specified data location to the passed template CanvasObject.
|
|
//
|
|
// Since: 1.4
|
|
func NewTree(childUIDs func(TreeNodeID) []TreeNodeID, isBranch func(TreeNodeID) bool, create func(bool) fyne.CanvasObject, update func(TreeNodeID, bool, fyne.CanvasObject)) *Tree {
|
|
t := &Tree{ChildUIDs: childUIDs, IsBranch: isBranch, CreateNode: create, UpdateNode: update}
|
|
t.ExtendBaseWidget(t)
|
|
return t
|
|
}
|
|
|
|
// NewTreeWithData creates a new tree widget that will display the contents of the provided data.
|
|
//
|
|
// Since: 2.4
|
|
func NewTreeWithData(data binding.DataTree, createItem func(bool) fyne.CanvasObject, updateItem func(binding.DataItem, bool, fyne.CanvasObject)) *Tree {
|
|
t := NewTree(
|
|
data.ChildIDs,
|
|
func(id TreeNodeID) bool {
|
|
children := data.ChildIDs(id)
|
|
return len(children) > 0
|
|
},
|
|
createItem,
|
|
func(i TreeNodeID, branch bool, o fyne.CanvasObject) {
|
|
item, err := data.GetItem(i)
|
|
if err != nil {
|
|
fyne.LogError(fmt.Sprintf("Error getting data item %s", i), err)
|
|
return
|
|
}
|
|
updateItem(item, branch, o)
|
|
})
|
|
|
|
data.AddListener(binding.NewDataListener(t.Refresh))
|
|
return t
|
|
}
|
|
|
|
// NewTreeWithStrings creates a new tree with the given string map.
|
|
// Data must contain a mapping for the root, which defaults to empty string ("").
|
|
//
|
|
// Since: 1.4
|
|
func NewTreeWithStrings(data map[string][]string) (t *Tree) {
|
|
t = &Tree{
|
|
ChildUIDs: func(uid string) (c []string) {
|
|
c = data[uid]
|
|
return
|
|
},
|
|
IsBranch: func(uid string) (b bool) {
|
|
_, b = data[uid]
|
|
return
|
|
},
|
|
CreateNode: func(branch bool) fyne.CanvasObject {
|
|
return NewLabel("Template Object")
|
|
},
|
|
UpdateNode: func(uid string, branch bool, node fyne.CanvasObject) {
|
|
node.(*Label).SetText(uid)
|
|
},
|
|
}
|
|
t.ExtendBaseWidget(t)
|
|
return
|
|
}
|
|
|
|
// CloseAllBranches closes all branches in the tree.
|
|
func (t *Tree) CloseAllBranches() {
|
|
t.propertyLock.Lock()
|
|
t.open = make(map[TreeNodeID]bool)
|
|
t.propertyLock.Unlock()
|
|
t.Refresh()
|
|
}
|
|
|
|
// CloseBranch closes the branch with the given TreeNodeID.
|
|
func (t *Tree) CloseBranch(uid TreeNodeID) {
|
|
t.ensureOpenMap()
|
|
t.propertyLock.Lock()
|
|
t.open[uid] = false
|
|
t.propertyLock.Unlock()
|
|
if f := t.OnBranchClosed; f != nil {
|
|
f(uid)
|
|
}
|
|
t.Refresh()
|
|
}
|
|
|
|
// CreateRenderer is a private method to Fyne which links this widget to its renderer.
|
|
func (t *Tree) CreateRenderer() fyne.WidgetRenderer {
|
|
t.ExtendBaseWidget(t)
|
|
c := newTreeContent(t)
|
|
s := widget.NewScroll(c)
|
|
t.scroller = s
|
|
r := &treeRenderer{
|
|
BaseRenderer: widget.NewBaseRenderer([]fyne.CanvasObject{s}),
|
|
tree: t,
|
|
content: c,
|
|
scroller: s,
|
|
}
|
|
s.OnScrolled = t.offsetUpdated
|
|
r.updateMinSizes()
|
|
r.content.viewport = r.MinSize()
|
|
return r
|
|
}
|
|
|
|
// IsBranchOpen returns true if the branch with the given TreeNodeID is expanded.
|
|
func (t *Tree) IsBranchOpen(uid TreeNodeID) bool {
|
|
if uid == t.Root {
|
|
return true // Root is always open
|
|
}
|
|
t.ensureOpenMap()
|
|
t.propertyLock.RLock()
|
|
defer t.propertyLock.RUnlock()
|
|
return t.open[uid]
|
|
}
|
|
|
|
// FocusGained is called after this Tree has gained focus.
|
|
//
|
|
// Implements: fyne.Focusable
|
|
func (t *Tree) FocusGained() {
|
|
if t.currentFocus == "" {
|
|
if childUIDs := t.ChildUIDs; childUIDs != nil {
|
|
if ids := childUIDs(""); len(ids) > 0 {
|
|
t.currentFocus = ids[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
t.focused = true
|
|
t.ScrollTo(t.currentFocus)
|
|
t.RefreshItem(t.currentFocus)
|
|
}
|
|
|
|
// FocusLost is called after this Tree has lost focus.
|
|
//
|
|
// Implements: fyne.Focusable
|
|
func (t *Tree) FocusLost() {
|
|
t.focused = false
|
|
t.Refresh() //Item(t.currentFocus)
|
|
}
|
|
|
|
// MinSize returns the size that this widget should not shrink below.
|
|
func (t *Tree) MinSize() fyne.Size {
|
|
t.ExtendBaseWidget(t)
|
|
return t.BaseWidget.MinSize()
|
|
}
|
|
|
|
// RefreshItem refreshes a single item, specified by the item ID passed in.
|
|
//
|
|
// Since: 2.4
|
|
func (t *Tree) RefreshItem(id TreeNodeID) {
|
|
if t.scroller == nil {
|
|
return
|
|
}
|
|
r := cache.Renderer(t.scroller.Content.(*treeContent))
|
|
if r == nil {
|
|
return
|
|
}
|
|
|
|
r.(*treeContentRenderer).refreshForID(id)
|
|
}
|
|
|
|
// OpenAllBranches opens all branches in the tree.
|
|
func (t *Tree) OpenAllBranches() {
|
|
t.ensureOpenMap()
|
|
t.walkAll(func(uid, parent TreeNodeID, branch bool, depth int) {
|
|
if branch {
|
|
t.propertyLock.Lock()
|
|
t.open[uid] = true
|
|
t.propertyLock.Unlock()
|
|
}
|
|
})
|
|
t.Refresh()
|
|
}
|
|
|
|
// OpenBranch opens the branch with the given TreeNodeID.
|
|
func (t *Tree) OpenBranch(uid TreeNodeID) {
|
|
t.ensureOpenMap()
|
|
t.propertyLock.Lock()
|
|
t.open[uid] = true
|
|
t.propertyLock.Unlock()
|
|
if f := t.OnBranchOpened; f != nil {
|
|
f(uid)
|
|
}
|
|
t.Refresh()
|
|
}
|
|
|
|
// Resize sets a new size for a widget.
|
|
func (t *Tree) Resize(size fyne.Size) {
|
|
t.propertyLock.RLock()
|
|
s := t.size
|
|
t.propertyLock.RUnlock()
|
|
|
|
if s == size {
|
|
return
|
|
}
|
|
|
|
t.propertyLock.Lock()
|
|
t.size = size
|
|
t.propertyLock.Unlock()
|
|
|
|
t.Refresh() // trigger a redraw
|
|
}
|
|
|
|
// ScrollToBottom scrolls to the bottom of the tree.
|
|
//
|
|
// Since 2.1
|
|
func (t *Tree) ScrollToBottom() {
|
|
if t.scroller == nil {
|
|
return
|
|
}
|
|
|
|
y, size := t.findBottom()
|
|
t.scroller.Offset.Y = y + size.Height - t.scroller.Size().Height
|
|
|
|
t.offsetUpdated(t.scroller.Offset)
|
|
t.Refresh()
|
|
}
|
|
|
|
// ScrollTo scrolls to the node with the given id.
|
|
//
|
|
// Since 2.1
|
|
func (t *Tree) ScrollTo(uid TreeNodeID) {
|
|
if t.scroller == nil {
|
|
return
|
|
}
|
|
|
|
y, size, ok := t.offsetAndSize(uid)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// TODO scrolling to a node should open all parents if they aren't already
|
|
if y < t.scroller.Offset.Y {
|
|
t.scroller.Offset.Y = y
|
|
} else if y+size.Height > t.scroller.Offset.Y+t.scroller.Size().Height {
|
|
t.scroller.Offset.Y = y + size.Height - t.scroller.Size().Height
|
|
}
|
|
|
|
t.offsetUpdated(t.scroller.Offset)
|
|
t.Refresh()
|
|
}
|
|
|
|
// ScrollToTop scrolls to the top of the tree.
|
|
//
|
|
// Since 2.1
|
|
func (t *Tree) ScrollToTop() {
|
|
if t.scroller == nil {
|
|
return
|
|
}
|
|
|
|
t.scroller.Offset.Y = 0
|
|
t.offsetUpdated(t.scroller.Offset)
|
|
t.Refresh()
|
|
}
|
|
|
|
// Select marks the specified node to be selected.
|
|
func (t *Tree) Select(uid TreeNodeID) {
|
|
if len(t.selected) > 0 {
|
|
if uid == t.selected[0] {
|
|
return // no change
|
|
}
|
|
if f := t.OnUnselected; f != nil {
|
|
f(t.selected[0])
|
|
}
|
|
}
|
|
t.selected = []TreeNodeID{uid}
|
|
t.ScrollTo(uid)
|
|
if f := t.OnSelected; f != nil {
|
|
f(uid)
|
|
}
|
|
}
|
|
|
|
// ToggleBranch flips the state of the branch with the given TreeNodeID.
|
|
func (t *Tree) ToggleBranch(uid string) {
|
|
if t.IsBranchOpen(uid) {
|
|
t.CloseBranch(uid)
|
|
} else {
|
|
t.OpenBranch(uid)
|
|
}
|
|
}
|
|
|
|
// TypedKey is called if a key event happens while this Tree is focused.
|
|
//
|
|
// Implements: fyne.Focusable
|
|
func (t *Tree) TypedKey(event *fyne.KeyEvent) {
|
|
switch event.Name {
|
|
case fyne.KeySpace:
|
|
t.Select(t.currentFocus)
|
|
case fyne.KeyDown:
|
|
t.RefreshItem(t.currentFocus)
|
|
next := false
|
|
t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) {
|
|
if next {
|
|
t.currentFocus = id
|
|
next = false
|
|
} else if id == t.currentFocus {
|
|
next = true
|
|
}
|
|
})
|
|
|
|
t.ScrollTo(t.currentFocus)
|
|
t.RefreshItem(t.currentFocus)
|
|
case fyne.KeyLeft:
|
|
// If the current focus is on a branch which is open, just close it
|
|
if t.IsBranch(t.currentFocus) && t.IsBranchOpen(t.currentFocus) {
|
|
t.CloseBranch(t.currentFocus)
|
|
} else {
|
|
// Every other case should move the focus to the current parent node
|
|
t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) {
|
|
if id == t.currentFocus && p != "" {
|
|
t.currentFocus = p
|
|
}
|
|
})
|
|
}
|
|
|
|
t.RefreshItem(t.currentFocus)
|
|
t.ScrollTo(t.currentFocus)
|
|
t.RefreshItem(t.currentFocus)
|
|
case fyne.KeyRight:
|
|
if t.IsBranch(t.currentFocus) {
|
|
t.OpenBranch(t.currentFocus)
|
|
}
|
|
children := []TreeNodeID{}
|
|
if childUIDs := t.ChildUIDs; childUIDs != nil {
|
|
children = childUIDs(t.currentFocus)
|
|
}
|
|
|
|
if len(children) > 0 {
|
|
t.currentFocus = children[0]
|
|
}
|
|
|
|
t.RefreshItem(t.currentFocus)
|
|
t.ScrollTo(t.currentFocus)
|
|
t.RefreshItem(t.currentFocus)
|
|
case fyne.KeyUp:
|
|
t.RefreshItem(t.currentFocus)
|
|
previous := ""
|
|
t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) {
|
|
if id == t.currentFocus && previous != "" {
|
|
t.currentFocus = previous
|
|
}
|
|
previous = id
|
|
})
|
|
|
|
t.ScrollTo(t.currentFocus)
|
|
t.RefreshItem(t.currentFocus)
|
|
}
|
|
}
|
|
|
|
// TypedRune is called if a text event happens while this Tree is focused.
|
|
//
|
|
// Implements: fyne.Focusable
|
|
func (t *Tree) TypedRune(_ rune) {
|
|
// intentionally left blank
|
|
}
|
|
|
|
// Unselect marks the specified node to be not selected.
|
|
func (t *Tree) Unselect(uid TreeNodeID) {
|
|
if len(t.selected) == 0 || t.selected[0] != uid {
|
|
return
|
|
}
|
|
|
|
t.selected = nil
|
|
t.Refresh()
|
|
if f := t.OnUnselected; f != nil {
|
|
f(uid)
|
|
}
|
|
}
|
|
|
|
// UnselectAll sets all nodes to be not selected.
|
|
//
|
|
// Since: 2.1
|
|
func (t *Tree) UnselectAll() {
|
|
if len(t.selected) == 0 {
|
|
return
|
|
}
|
|
|
|
selected := t.selected
|
|
t.selected = nil
|
|
t.Refresh()
|
|
if f := t.OnUnselected; f != nil {
|
|
for _, uid := range selected {
|
|
f(uid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Tree) ensureOpenMap() {
|
|
t.propertyLock.Lock()
|
|
defer t.propertyLock.Unlock()
|
|
if t.open == nil {
|
|
t.open = make(map[string]bool)
|
|
}
|
|
}
|
|
|
|
func (t *Tree) findBottom() (y float32, size fyne.Size) {
|
|
sep := theme.Padding()
|
|
t.walkAll(func(id, _ TreeNodeID, branch bool, _ int) {
|
|
size = t.leafMinSize
|
|
if branch {
|
|
size = t.branchMinSize
|
|
}
|
|
|
|
// Root node is not rendered unless it has been customized
|
|
if t.Root == "" && id == "" {
|
|
// This is root node, skip
|
|
return
|
|
}
|
|
|
|
// If this is not the first item, add a separator
|
|
if y > 0 {
|
|
y += sep
|
|
}
|
|
|
|
y += size.Height
|
|
})
|
|
if y > 0 {
|
|
y -= sep
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *Tree) offsetAndSize(uid TreeNodeID) (y float32, size fyne.Size, found bool) {
|
|
t.walkAll(func(id, _ TreeNodeID, branch bool, _ int) {
|
|
m := t.leafMinSize
|
|
if branch {
|
|
m = t.branchMinSize
|
|
}
|
|
if id == uid {
|
|
found = true
|
|
size = m
|
|
} else if !found {
|
|
// Root node is not rendered unless it has been customized
|
|
if t.Root == "" && id == "" {
|
|
// This is root node, skip
|
|
return
|
|
}
|
|
// If this is not the first item, add a separator
|
|
if y > 0 {
|
|
y += theme.Padding()
|
|
}
|
|
|
|
y += m.Height
|
|
}
|
|
})
|
|
return
|
|
}
|
|
|
|
func (t *Tree) offsetUpdated(pos fyne.Position) {
|
|
if t.offset == pos {
|
|
return
|
|
}
|
|
t.offset = pos
|
|
t.scroller.Content.Refresh()
|
|
}
|
|
|
|
func (t *Tree) walk(uid, parent TreeNodeID, depth int, onNode func(TreeNodeID, TreeNodeID, bool, int)) {
|
|
if isBranch := t.IsBranch; isBranch != nil {
|
|
if isBranch(uid) {
|
|
onNode(uid, parent, true, depth)
|
|
if t.IsBranchOpen(uid) {
|
|
if childUIDs := t.ChildUIDs; childUIDs != nil {
|
|
for _, c := range childUIDs(uid) {
|
|
t.walk(c, uid, depth+1, onNode)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
onNode(uid, parent, false, depth)
|
|
}
|
|
}
|
|
}
|
|
|
|
// walkAll visits every open node of the tree and calls the given callback with TreeNodeID, whether node is branch, and the depth of node.
|
|
func (t *Tree) walkAll(onNode func(TreeNodeID, TreeNodeID, bool, int)) {
|
|
t.walk(t.Root, "", 0, onNode)
|
|
}
|
|
|
|
var _ fyne.WidgetRenderer = (*treeRenderer)(nil)
|
|
|
|
type treeRenderer struct {
|
|
widget.BaseRenderer
|
|
tree *Tree
|
|
content *treeContent
|
|
scroller *widget.Scroll
|
|
}
|
|
|
|
func (r *treeRenderer) MinSize() (min fyne.Size) {
|
|
min = r.scroller.MinSize()
|
|
min = min.Max(r.tree.branchMinSize)
|
|
min = min.Max(r.tree.leafMinSize)
|
|
return
|
|
}
|
|
|
|
func (r *treeRenderer) Layout(size fyne.Size) {
|
|
r.content.viewport = size
|
|
r.scroller.Resize(size)
|
|
}
|
|
|
|
func (r *treeRenderer) Refresh() {
|
|
r.updateMinSizes()
|
|
s := r.tree.Size()
|
|
if s.IsZero() {
|
|
r.tree.Resize(r.tree.MinSize())
|
|
} else {
|
|
r.Layout(s)
|
|
}
|
|
r.scroller.Refresh()
|
|
r.content.Refresh()
|
|
canvas.Refresh(r.tree.super())
|
|
}
|
|
|
|
func (r *treeRenderer) updateMinSizes() {
|
|
if f := r.tree.CreateNode; f != nil {
|
|
r.tree.branchMinSize = newBranch(r.tree, f(true)).MinSize()
|
|
r.tree.leafMinSize = newLeaf(r.tree, f(false)).MinSize()
|
|
}
|
|
}
|
|
|
|
var _ fyne.Widget = (*treeContent)(nil)
|
|
|
|
type treeContent struct {
|
|
BaseWidget
|
|
tree *Tree
|
|
viewport fyne.Size
|
|
}
|
|
|
|
func newTreeContent(tree *Tree) (c *treeContent) {
|
|
c = &treeContent{
|
|
tree: tree,
|
|
}
|
|
c.ExtendBaseWidget(c)
|
|
return
|
|
}
|
|
|
|
func (c *treeContent) CreateRenderer() fyne.WidgetRenderer {
|
|
return &treeContentRenderer{
|
|
BaseRenderer: widget.BaseRenderer{},
|
|
treeContent: c,
|
|
branches: make(map[string]*branch),
|
|
leaves: make(map[string]*leaf),
|
|
branchPool: &syncPool{},
|
|
leafPool: &syncPool{},
|
|
}
|
|
}
|
|
|
|
func (c *treeContent) Resize(size fyne.Size) {
|
|
c.propertyLock.RLock()
|
|
s := c.size
|
|
c.propertyLock.RUnlock()
|
|
|
|
if s == size {
|
|
return
|
|
}
|
|
|
|
c.propertyLock.Lock()
|
|
c.size = size
|
|
c.propertyLock.Unlock()
|
|
|
|
c.Refresh() // trigger a redraw
|
|
}
|
|
|
|
var _ fyne.WidgetRenderer = (*treeContentRenderer)(nil)
|
|
|
|
type treeContentRenderer struct {
|
|
widget.BaseRenderer
|
|
treeContent *treeContent
|
|
separators []fyne.CanvasObject
|
|
objects []fyne.CanvasObject
|
|
branches map[string]*branch
|
|
leaves map[string]*leaf
|
|
branchPool pool
|
|
leafPool pool
|
|
}
|
|
|
|
func (r *treeContentRenderer) Layout(size fyne.Size) {
|
|
r.treeContent.propertyLock.Lock()
|
|
defer r.treeContent.propertyLock.Unlock()
|
|
|
|
r.objects = nil
|
|
branches := make(map[string]*branch)
|
|
leaves := make(map[string]*leaf)
|
|
|
|
pad := theme.Padding()
|
|
offsetY := r.treeContent.tree.offset.Y
|
|
viewport := r.treeContent.viewport
|
|
width := fyne.Max(size.Width, viewport.Width)
|
|
separatorCount := 0
|
|
separatorThickness := theme.SeparatorThicknessSize()
|
|
separatorSize := fyne.NewSize(width, separatorThickness)
|
|
separatorOff := (pad + separatorThickness) / 2
|
|
y := float32(0)
|
|
// walkAll open branches and obtain nodes to render in scroller's viewport
|
|
r.treeContent.tree.walkAll(func(uid, _ string, isBranch bool, depth int) {
|
|
// Root node is not rendered unless it has been customized
|
|
if r.treeContent.tree.Root == "" {
|
|
depth = depth - 1
|
|
if uid == "" {
|
|
// This is root node, skip
|
|
return
|
|
}
|
|
}
|
|
|
|
// If this is not the first item, add a separator
|
|
addSeparator := y > 0
|
|
if addSeparator {
|
|
y += pad
|
|
separatorCount++
|
|
}
|
|
|
|
m := r.treeContent.tree.leafMinSize
|
|
if isBranch {
|
|
m = r.treeContent.tree.branchMinSize
|
|
}
|
|
if y+m.Height < offsetY {
|
|
// Node is above viewport and not visible
|
|
} else if y > offsetY+viewport.Height {
|
|
// Node is below viewport and not visible
|
|
} else {
|
|
// Node is in viewport
|
|
|
|
if addSeparator {
|
|
var separator fyne.CanvasObject
|
|
if separatorCount < len(r.separators) {
|
|
separator = r.separators[separatorCount]
|
|
} else {
|
|
separator = NewSeparator()
|
|
r.separators = append(r.separators, separator)
|
|
}
|
|
separator.Move(fyne.NewPos(0, y-separatorOff))
|
|
separator.Resize(separatorSize)
|
|
r.objects = append(r.objects, separator)
|
|
separatorCount++
|
|
}
|
|
|
|
var n fyne.CanvasObject
|
|
if isBranch {
|
|
b, ok := r.branches[uid]
|
|
if !ok {
|
|
b = r.getBranch()
|
|
if f := r.treeContent.tree.UpdateNode; f != nil {
|
|
f(uid, true, b.Content())
|
|
}
|
|
b.update(uid, depth)
|
|
}
|
|
branches[uid] = b
|
|
n = b
|
|
r.objects = append(r.objects, b)
|
|
} else {
|
|
l, ok := r.leaves[uid]
|
|
if !ok {
|
|
l = r.getLeaf()
|
|
if f := r.treeContent.tree.UpdateNode; f != nil {
|
|
f(uid, false, l.Content())
|
|
}
|
|
l.update(uid, depth)
|
|
}
|
|
leaves[uid] = l
|
|
n = l
|
|
r.objects = append(r.objects, l)
|
|
}
|
|
if n != nil {
|
|
n.Move(fyne.NewPos(0, y))
|
|
n.Resize(fyne.NewSize(width, m.Height))
|
|
}
|
|
}
|
|
y += m.Height
|
|
})
|
|
|
|
// Hide any separators that haven't been reused
|
|
for ; separatorCount < len(r.separators); separatorCount++ {
|
|
r.separators[separatorCount].Hide()
|
|
}
|
|
|
|
// Release any nodes that haven't been reused
|
|
for uid, b := range r.branches {
|
|
if _, ok := branches[uid]; !ok {
|
|
r.branchPool.Release(b)
|
|
}
|
|
}
|
|
for uid, l := range r.leaves {
|
|
if _, ok := leaves[uid]; !ok {
|
|
r.leafPool.Release(l)
|
|
}
|
|
}
|
|
|
|
r.branches = branches
|
|
r.leaves = leaves
|
|
}
|
|
|
|
func (r *treeContentRenderer) MinSize() (min fyne.Size) {
|
|
r.treeContent.propertyLock.Lock()
|
|
defer r.treeContent.propertyLock.Unlock()
|
|
|
|
r.treeContent.tree.walkAll(func(uid, _ string, isBranch bool, depth int) {
|
|
// Root node is not rendered unless it has been customized
|
|
if r.treeContent.tree.Root == "" {
|
|
depth = depth - 1
|
|
if uid == "" {
|
|
// This is root node, skip
|
|
return
|
|
}
|
|
}
|
|
|
|
// If this is not the first item, add a separator
|
|
if min.Height > 0 {
|
|
min.Height += theme.Padding()
|
|
}
|
|
|
|
m := r.treeContent.tree.leafMinSize
|
|
if isBranch {
|
|
m = r.treeContent.tree.branchMinSize
|
|
}
|
|
m.Width += float32(depth) * (theme.IconInlineSize() + theme.Padding())
|
|
min.Width = fyne.Max(min.Width, m.Width)
|
|
min.Height += m.Height
|
|
})
|
|
return
|
|
}
|
|
|
|
func (r *treeContentRenderer) Objects() []fyne.CanvasObject {
|
|
return r.objects
|
|
}
|
|
|
|
func (r *treeContentRenderer) Refresh() {
|
|
r.refreshForID(allTreeNodesID)
|
|
}
|
|
|
|
func (r *treeContentRenderer) refreshForID(toDraw TreeNodeID) {
|
|
s := r.treeContent.Size()
|
|
if s.IsZero() {
|
|
r.treeContent.Resize(r.treeContent.MinSize().Max(r.treeContent.tree.Size()))
|
|
} else {
|
|
r.Layout(s)
|
|
}
|
|
r.treeContent.propertyLock.RLock()
|
|
for id, b := range r.branches {
|
|
if toDraw != allTreeNodesID && id != toDraw {
|
|
continue
|
|
}
|
|
|
|
b.Refresh()
|
|
}
|
|
for id, l := range r.leaves {
|
|
if toDraw != allTreeNodesID && id != toDraw {
|
|
continue
|
|
}
|
|
|
|
l.Refresh()
|
|
}
|
|
r.treeContent.propertyLock.RUnlock()
|
|
canvas.Refresh(r.treeContent.super())
|
|
}
|
|
|
|
func (r *treeContentRenderer) getBranch() (b *branch) {
|
|
o := r.branchPool.Obtain()
|
|
if o != nil {
|
|
b = o.(*branch)
|
|
} else {
|
|
var content fyne.CanvasObject
|
|
if f := r.treeContent.tree.CreateNode; f != nil {
|
|
content = f(true)
|
|
}
|
|
b = newBranch(r.treeContent.tree, content)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (r *treeContentRenderer) getLeaf() (l *leaf) {
|
|
o := r.leafPool.Obtain()
|
|
if o != nil {
|
|
l = o.(*leaf)
|
|
} else {
|
|
var content fyne.CanvasObject
|
|
if f := r.treeContent.tree.CreateNode; f != nil {
|
|
content = f(false)
|
|
}
|
|
l = newLeaf(r.treeContent.tree, content)
|
|
}
|
|
return
|
|
}
|
|
|
|
var _ desktop.Hoverable = (*treeNode)(nil)
|
|
var _ fyne.CanvasObject = (*treeNode)(nil)
|
|
var _ fyne.Tappable = (*treeNode)(nil)
|
|
|
|
type treeNode struct {
|
|
BaseWidget
|
|
tree *Tree
|
|
uid string
|
|
depth int
|
|
hovered bool
|
|
icon fyne.CanvasObject
|
|
isBranch bool
|
|
content fyne.CanvasObject
|
|
}
|
|
|
|
func (n *treeNode) Content() fyne.CanvasObject {
|
|
return n.content
|
|
}
|
|
|
|
func (n *treeNode) CreateRenderer() fyne.WidgetRenderer {
|
|
background := canvas.NewRectangle(theme.HoverColor())
|
|
background.CornerRadius = theme.SelectionRadiusSize()
|
|
background.Hide()
|
|
return &treeNodeRenderer{
|
|
BaseRenderer: widget.BaseRenderer{},
|
|
treeNode: n,
|
|
background: background,
|
|
}
|
|
}
|
|
|
|
func (n *treeNode) Indent() float32 {
|
|
return float32(n.depth) * (theme.IconInlineSize() + theme.Padding())
|
|
}
|
|
|
|
// MouseIn is called when a desktop pointer enters the widget
|
|
func (n *treeNode) MouseIn(*desktop.MouseEvent) {
|
|
n.hovered = true
|
|
n.partialRefresh()
|
|
}
|
|
|
|
// MouseMoved is called when a desktop pointer hovers over the widget
|
|
func (n *treeNode) MouseMoved(*desktop.MouseEvent) {
|
|
}
|
|
|
|
// MouseOut is called when a desktop pointer exits the widget
|
|
func (n *treeNode) MouseOut() {
|
|
n.hovered = false
|
|
n.partialRefresh()
|
|
}
|
|
|
|
func (n *treeNode) Tapped(*fyne.PointEvent) {
|
|
if n.tree.currentFocus != "" {
|
|
n.tree.RefreshItem(n.tree.currentFocus)
|
|
}
|
|
|
|
n.tree.Select(n.uid)
|
|
if !fyne.CurrentDevice().IsMobile() {
|
|
canvas := fyne.CurrentApp().Driver().CanvasForObject(n.tree)
|
|
if canvas != nil {
|
|
canvas.Focus(n.tree)
|
|
}
|
|
n.tree.currentFocus = n.uid
|
|
n.Refresh()
|
|
}
|
|
}
|
|
|
|
func (n *treeNode) partialRefresh() {
|
|
if r := cache.Renderer(n.super()); r != nil {
|
|
r.(*treeNodeRenderer).partialRefresh()
|
|
}
|
|
}
|
|
|
|
func (n *treeNode) update(uid string, depth int) {
|
|
n.uid = uid
|
|
n.depth = depth
|
|
n.propertyLock.Lock()
|
|
n.Hidden = false
|
|
n.propertyLock.Unlock()
|
|
n.partialRefresh()
|
|
}
|
|
|
|
var _ fyne.WidgetRenderer = (*treeNodeRenderer)(nil)
|
|
|
|
type treeNodeRenderer struct {
|
|
widget.BaseRenderer
|
|
treeNode *treeNode
|
|
background *canvas.Rectangle
|
|
}
|
|
|
|
func (r *treeNodeRenderer) Layout(size fyne.Size) {
|
|
x := theme.Padding() + r.treeNode.Indent()
|
|
y := float32(0)
|
|
r.background.Resize(size)
|
|
if r.treeNode.icon != nil {
|
|
r.treeNode.icon.Move(fyne.NewPos(x, y))
|
|
r.treeNode.icon.Resize(fyne.NewSize(theme.IconInlineSize(), size.Height))
|
|
}
|
|
x += theme.IconInlineSize()
|
|
x += theme.Padding()
|
|
if r.treeNode.content != nil {
|
|
r.treeNode.content.Move(fyne.NewPos(x, y))
|
|
r.treeNode.content.Resize(fyne.NewSize(size.Width-x, size.Height))
|
|
}
|
|
}
|
|
|
|
func (r *treeNodeRenderer) MinSize() (min fyne.Size) {
|
|
if r.treeNode.content != nil {
|
|
min = r.treeNode.content.MinSize()
|
|
}
|
|
min.Width += theme.InnerPadding() + r.treeNode.Indent() + theme.IconInlineSize()
|
|
min.Height = fyne.Max(min.Height, theme.IconInlineSize())
|
|
return
|
|
}
|
|
|
|
func (r *treeNodeRenderer) Objects() (objects []fyne.CanvasObject) {
|
|
objects = append(objects, r.background)
|
|
if r.treeNode.content != nil {
|
|
objects = append(objects, r.treeNode.content)
|
|
}
|
|
if r.treeNode.icon != nil {
|
|
objects = append(objects, r.treeNode.icon)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (r *treeNodeRenderer) Refresh() {
|
|
if c := r.treeNode.content; c != nil {
|
|
if f := r.treeNode.tree.UpdateNode; f != nil {
|
|
f(r.treeNode.uid, r.treeNode.isBranch, c)
|
|
}
|
|
}
|
|
r.partialRefresh()
|
|
}
|
|
|
|
func (r *treeNodeRenderer) partialRefresh() {
|
|
if r.treeNode.icon != nil {
|
|
r.treeNode.icon.Refresh()
|
|
}
|
|
r.background.CornerRadius = theme.SelectionRadiusSize()
|
|
if len(r.treeNode.tree.selected) > 0 && r.treeNode.uid == r.treeNode.tree.selected[0] {
|
|
r.background.FillColor = theme.SelectionColor()
|
|
r.background.Show()
|
|
} else if r.treeNode.hovered || (r.treeNode.tree.focused && r.treeNode.tree.currentFocus == r.treeNode.uid) {
|
|
r.background.FillColor = theme.HoverColor()
|
|
r.background.Show()
|
|
} else {
|
|
r.background.Hide()
|
|
}
|
|
r.background.Refresh()
|
|
r.Layout(r.treeNode.size)
|
|
canvas.Refresh(r.treeNode.super())
|
|
}
|
|
|
|
var _ fyne.Widget = (*branch)(nil)
|
|
|
|
type branch struct {
|
|
*treeNode
|
|
}
|
|
|
|
func newBranch(tree *Tree, content fyne.CanvasObject) (b *branch) {
|
|
b = &branch{
|
|
treeNode: &treeNode{
|
|
tree: tree,
|
|
icon: newBranchIcon(tree),
|
|
isBranch: true,
|
|
content: content,
|
|
},
|
|
}
|
|
b.ExtendBaseWidget(b)
|
|
return
|
|
}
|
|
|
|
func (b *branch) update(uid string, depth int) {
|
|
b.treeNode.update(uid, depth)
|
|
b.icon.(*branchIcon).update(uid, depth)
|
|
}
|
|
|
|
var _ fyne.Tappable = (*branchIcon)(nil)
|
|
|
|
type branchIcon struct {
|
|
Icon
|
|
tree *Tree
|
|
uid string
|
|
}
|
|
|
|
func newBranchIcon(tree *Tree) (i *branchIcon) {
|
|
i = &branchIcon{
|
|
tree: tree,
|
|
}
|
|
i.ExtendBaseWidget(i)
|
|
return
|
|
}
|
|
|
|
func (i *branchIcon) Refresh() {
|
|
if i.tree.IsBranchOpen(i.uid) {
|
|
i.Resource = theme.MoveDownIcon()
|
|
} else {
|
|
i.Resource = theme.NavigateNextIcon()
|
|
}
|
|
i.Icon.Refresh()
|
|
}
|
|
|
|
func (i *branchIcon) Tapped(*fyne.PointEvent) {
|
|
i.tree.ToggleBranch(i.uid)
|
|
}
|
|
|
|
func (i *branchIcon) update(uid string, depth int) {
|
|
i.uid = uid
|
|
i.Refresh()
|
|
}
|
|
|
|
var _ fyne.Widget = (*leaf)(nil)
|
|
|
|
type leaf struct {
|
|
*treeNode
|
|
}
|
|
|
|
func newLeaf(tree *Tree, content fyne.CanvasObject) (l *leaf) {
|
|
l = &leaf{
|
|
&treeNode{
|
|
tree: tree,
|
|
content: content,
|
|
isBranch: false,
|
|
},
|
|
}
|
|
l.ExtendBaseWidget(l)
|
|
return
|
|
}
|