sourceafReflux::ResourceTree.fan

using gfx
using fwt

** (Widget) - A tree widget that displays 'Resource' hierarchies. 'ResourceTree' is a wrapper around the FWT 
** [Tree]`fwt::Tree` widget with the following enhancements:
** 
**  - A 'Resource' specific tree model. 
**  - Hassle free 'refreshResource()' and 'showResource()' methods that just work.
**  - Event data return the 'Resource' that's been actioned.
**  
** Because 'ResourceTree' does not extend 'fwt:Widget' it can not be added directly. 
** Instead, add the 'tree' field which returns the wrapped FWT Tree instance.
** 
**   syntax: fantom
** 
**   tree := ResourceTree(reflux)
**   ContentPane() {
**       it.content = tree.tree
**   }
class ResourceTree {
    private Reflux reflux
    
    ** The underlying FWT Tree widget.
    Tree tree

    ** The model that customises the look of the tree. Leave as is for default behaviour.
    ** 
    ** You should call 'refreshAll' after setting a new model.
    ResourceTreeModel model := ResourceTreeModelImpl() {
        set {
            tree.model = TreeModelAdapter(reflux, roots, it)
            &model = it         
        }
    }
    
    ** The root resources of the tree.
    ** 
    ** You should call 'refreshAll' after setting new roots.
    Resource[] roots := Resource#.emptyList {
        set {
            tree.model = TreeModelAdapter(reflux, it, model)
            &roots = it
        }
    }

    ** Creates a 'ResourceTree'. Use the ctor to pass in a tree:
    **   syntax: fantom
    **   ResourceTree(reflux) {
    **       it.tree = Tree {
    **           it.border = false
    **       }
    **       it.roots = myRoots
    **       it.model = MyModel()
    **   }
    ** 
    ** Note that, as shown above, the 'tree' must be set before the model and / or roots. 
    new make(Reflux reflux, |This|? in := null) {
        this.reflux = reflux
        tree = Tree()
        in(this)
        tree.onAction.add |Event e| {
            node := (TreeNode?) e.data
            e.data = node?.resource
            onAction.fire(e)
            e.data = node
        }

        tree.onSelect.add |Event e| {
            node := (TreeNode?) e.data
            e.data = node?.resource
            onSelect.fire(e)
            e.data = node
        }

        tree.onPopup.add |Event e| {
            node := (TreeNode?) e.data
            e.data = node?.resource
            onPopup.fire(e)
            e.data = node
        }       
    }

    ** Callback when a node is double clicked or Return/Enter key is pressed.
    **
    ** Event id fired:
    **   - 'EventId.modified'
    **
    ** Event fields:
    **   - 'Event.data': the 'Resource' actioned
    once EventListeners onAction() { EventListeners() }

    ** Callback when selected nodes change.
    **
    ** Event id fired:
    **   - 'EventId.select'
    **
    ** Event fields:
    **   - 'Event.data': the 'Resource' selected
    once EventListeners onSelect() { EventListeners() }

    ** Callback when user invokes a right click popup action. 
    ** If the callback wishes to display a popup, then set the 'Event.popup' field with menu to open.
    ** If multiple callbacks are installed, the first one to return a nonnull popup consumes the event.
    **
    ** To show a menu created from the 'Resource', add the following:
    ** 
    **   tree.onPopup.add |Event event| {
    **       event.popup = (event.data as Resource)?.populatePopup(Menu())
    **   }
    ** 
    ** Event id fired:
    **   - 'EventId.popup'
    **
    ** Event fields:
    **   - 'Event.data': the 'Resource' selected, or 'null' if this is a background popup.
    **   - 'Event.pos': the mouse position of the popup.
    once EventListeners onPopup() { EventListeners() }

    ** Update the entire tree's contents from the model.
    Void refreshAll() {
        tree.refreshAll     
    }
    
    ** Updates the specified resource in the model before showing it.
    Void refreshResource(Resource resource) {
        path := findNodePath(resource)

        if (path.getSafe(-2) != null) { // null for root nodes
            path.getSafe(-2).refresh
            tree.refreshNode(path.getSafe(-2))
        } else {
            tree.model = TreeModelAdapter(reflux, roots, model)
            tree.refreshAll                 
        }
        
        Desktop.callLater(50ms) |->| {
            showResource(resource)
        }       
    }

    ** Updates the specified resource in the model before showing it.
    Void refreshResourceUri(Uri resourceUri) {
        refreshResource(reflux.resolve(resourceUri.toStr))
    }
    
    ** Scrolls and expands the tree until the 'Resource' is visible.
    ** This also selects the resource in the tree.
    Void showResource(Resource resource) {
        path := findNodePath(resource)
        path.eachRange(0..-2) { tree.setExpanded(it, true) }
        tree.show(path.last)
        tree.select(path.last)
    }
    
    ** Scrolls and expands the tree until the 'Resource' is visible.
    ** This also selects the resource in the tree.
    Void showResourceUri(Uri resourceUri) {
        showResource(reflux.resolve(resourceUri.toStr))
    }

    ** Get and set the selected nodes.
    ** 
    ** Convenience for 'tree.selected()'
    Resource[] selected {
        get {
            ((TreeNode[]) tree.selected).map { it.resource }
        }
        set {
            tree.selected = it.map { findNode(it) }
        }
    }

    ** Return the 'Resource' at the specified coordinate relative to this widget. 
    ** Return 'null' if there is no 'Resource' at given coordinate.
    Resource? resourceAt(Point pos) {
        ((TreeNode?) tree.nodeAt(pos))?.resource
    }
    
    ** Return the expanded state for this 'Resource'.
    Bool isExpanded(Resource resource) {
        tree.isExpanded(findNode(resource))
    }

    ** Set the expanded state for this 'Resource'.
    Void setExpanded(Resource resource, Bool expanded) {
        tree.setExpanded(findNode(resource), expanded)
    }
    
    private TreeNode findNode(Resource resource) {
        findNodePath(resource).last
    }

    private TreeNode[] findNodePath(Resource resource) {
        nodePath    := TreeNode[,]
        nodes       := (TreeNode[]) tree.model.roots
        resPath     := path(resource)
        resPath.each |Uri path| {
            node := nodes.find { it.resource.uri == path }
            if (node == null)
                throw ArgErr("Could not find node in tree: $path")
            nodePath.add(node)
            nodes = node.children
        }
        return nodePath
    }

    private Uri[] path(Resource? resource) {
        path    := Uri[,]
        while (resource != null) {
            path.add(resource.uri)
            next := resource.resolveParent
            if (next == null) {
                parent := resource.parent
                next = (parent == null) ? null : reflux.resolve(parent.toStr)
            }
            resource = next
        }
        return path.reverse
    }
    
    private TreeModelAdapter treeModel() {
        tree.model
    }
}

** A model to customise the look of a 'ResourceTree'.
mixin ResourceTreeModel {

    ** Get the text to display.
    ** Defaults to 'resource.name'.
    virtual Str text(Resource resource) { resource.name }

    ** Get the image to display.
    ** Defaults to 'resource.icon'.
    virtual Image? image(Resource resource) { resource.icon }

    ** Get the font for specified resource or 'null' for default.
    virtual Font? font(Resource resource) { null }

    ** Get the foreground color for specified node or 'null' for default.
    virtual Color? fg(Resource resource) { null }

    ** Get the background color for specified node or 'null' for default.
    virtual Color? bg(Resource resource) { null }

    ** Return if this has or might have children.   This
    ** is an optimisation to display an expansion control
    ** without actually loading all the children.
    ** 
    ** Defaults to 'resource.hasChildren'.
    virtual Bool hasChildren(Resource resource) { resource.hasChildren }

    ** Returns the children (resource URIs) of the specified node.
    ** If no children return an empty list.
    ** 
    ** Defaults to 'resource.children'.
    virtual Uri[] children(Resource resource) { resource.children }
}

internal class ResourceTreeModelImpl : ResourceTreeModel { }

internal class TreeModelAdapter : TreeModel {
    override TreeNode[] roots
    ResourceTreeModel   model
    Reflux              reflux
    
    new make(Reflux reflux, Resource[] roots, ResourceTreeModel model) {
        this.reflux = reflux
        this.roots  = TreeNode.map(reflux, null, roots)
        this.model  = model
    }

    override Str    text        (Obj n) { model.text(res(n))    }
    override Image? image       (Obj n) { model.image(res(n))   }
    override Font?  font        (Obj n) { model.font(res(n))    }
    override Color? fg          (Obj n) { model.fg(res(n))      }
    override Color? bg          (Obj n) { model.bg(res(n))      }
    override Bool   hasChildren (Obj n) { node(n).hasChildren   }
    override Obj[]  children    (Obj n) { node(n).children      }
    
    Resource res(TreeNode n) { n.resource }
    TreeNode node(TreeNode n) { n }
}

internal class TreeNode {
    Reflux      reflux
    TreeNode?   parent
    Resource    resource
    
    new make(Reflux reflux, TreeNode? parent, Resource resource) {
        this.reflux = reflux
        this.parent = parent
        this.resource = resource
    }
    
    Bool hasChildren() { !children.isEmpty }

    TreeNode[]? children {
        get {
            if (&children == null)
                &children = map(reflux, this, resource.children.map { resource.resolveChild(it) ?: reflux.resolve(it.toStr, false) }.exclude { it == null })
            return &children
        }
    }

    Void refresh() {
        children = null
    }
    
//  override Str toStr() { return resource.toStr }

    static TreeNode[] map(Reflux reflux, TreeNode? parent, Resource[] resources) {
        resources.map { TreeNode(reflux, parent, it) }
    }
}