sourceafQuickFlux::EntityTree.fan

using gfx
using fwt
using flux
using afIoc

** Autobuild me!
class EntityTree {

    @Inject private QuickFluxIcons  icons
    @Inject private EventHub    eventHub
    @Inject private Registry    registry
    
    private EntityTreeModel?    entityTreeModel
    private Text                findWidget  := Text()
    
    Tree                        tree        := Tree()
    Pane                        content
    |EntityMeta|[]              filterFuncs := |EntityMeta|[,]
    |->Entity[]|                rootsFunc   := |->Entity[]| { Entity#.emptyList } 
    Menu?                       emptyPopup
    Str                         findText {
        get { findWidget.text }
        private set { }
    }
    
    Entity[]                    roots {
        get { entityTreeModel.roots }
        private set { }
    }
    
    new make(Bool showFilter, |This| inject) {
        inject(this)
        
        content = EdgePane {
            if (showFilter)
                top = EdgePane {
                    center = findWidget
                    right = EdgePane { 
                        left = Button() { 
                            text = "Go" 
                            onAction.add { findWidget.onAction.fire(Event()) }
                        }
                        right = Button() { 
                            text = "Clear" 
                            onAction.add { findWidget.text = ""; findWidget.onAction.fire(Event()) }
                        }
                    }
                }
            center = tree
        }
        
        findWidget.onAction.add |->| {
            entityTreeModel.collapseAll(tree)
            tree.selected = [,]
            if (findText.isEmpty)
                return
            entityTreeModel.walk |EntityMeta node| {
                if (entityContains(node.entity, findText.lower)) {
                    node.parentNode?.show(tree)
                    tree.selected = [node.entity]
                }
            }
        }
        
        tree.onAction.add |event| {
            entityTreeModel.toMeta(event.data).action
        }

        tree.onPopup.add |event| {
            if (event.data == null) {
                event.popup = emptyPopup
                return
            }
            node := entityTreeModel.toMeta(event.data)
            event.popup = node.populateMenu(Menu())
        }

        eventHub.addListener(Events.newEntity) |Entity entity| {
            node := entityTreeModel.toMeta(entity)
            if (node.parent != null) {
                node.refresh(tree)
                node.expand(tree)
            } else {
                refreshAll
            }
        }

        eventHub.addListener(Events.entitySaved) |Entity entity| {
            node := entityTreeModel.toMeta(entity)
            if (node.parent != null) {
                node.refresh(tree)
                node.expand(tree)
            } else {
                refreshAll
            }
        }
        
        eventHub.addListener(Events.entityDeleted) |Entity entity| {
            node := entityTreeModel.toMeta(entity)
            if (node.parent != null) {
                node.parentNode.refresh(tree)
                node.parentNode.expand(tree)
            } else {
                refreshAll
            }
        }
        
        eventHub.addListener(Events.refreshEverthing) |->| {
            refreshAll
        }       
    }

    Bool entityContains(Entity entity, Str text) {
        entity.typeof.fields.findAll { it.type.fits(Str#) }.any |field->Bool| {
            Str str := field.get(entity) ?: ""
            return str.lower.contains(text) 
        }
    }
    
    Void refreshAll() {
        inCtor := (tree.model.typeof == TreeModel#) 
        entityTreeModel = registry.autobuild(EntityTreeModel#, [rootsFunc, filterFuncs])
        tree.model = entityTreeModel
        if (!inCtor)    // don't refresh during ctor
            tree.refreshAll
    }
    
    Void expand(Entity entity) {
        entityTreeModel.toMeta(entity).expand(tree)
        tree.select(entity)
    }
    
    Command collapseAllCommand() {
        Command("Collapse All", icons.icoCollapseAll) |->| {
            entityTreeModel.collapseAll(tree)
        }
    }
    
    EntityMeta[] toMetas(Entity[] entities) {
        entityTreeModel.toMetas(entities)
    }
    
    EntityMeta toMeta(Entity entity) {
        entityTreeModel.toMeta(entity)
    }   
}

internal class EntityTreeModel : TreeModel {
    
    @Inject 
    private EntityMetaSrc       entityMetaSrc
    private Entity:EntityMeta   nodeMap := [:]
    private |EntityMeta|[]      filterFuncs
    private |->Entity[]|        rootsFunc
    
    new make(|->Entity[]| rootsFunc, |EntityMeta|[] filterFuncs, |This|in) { 
        in(this)
        this.rootsFunc = rootsFunc
        this.filterFuncs = filterFuncs
    }
    
    override Obj[] roots() {
        reFilter
        return toMetas(rootsFunc.call).findAll { (it as EntityMeta).visible }.map { (it as EntityMeta).entity }
    }
    
    override Str text(Obj entity) {
        toMeta(entity).text
    }

    override Image? image(Obj entity) {
        toMeta(entity).icon
    }
    
    override Obj[] children(Obj entity) {
        toMetas(toMeta(entity).children).findAll { (it as EntityMeta).visible }.map { (it as EntityMeta).entity }
    }

    Void reFilter() {
        nodeMap.vals.each { it.visible = true }
        filterFuncs.each |func| {
            toMetas(rootsFunc.call).each { filter(func, it) }
        }
    }

    Void walk(|EntityMeta| func, Entity[] treeFodder := rootsFunc.call) {
        treeFodder.each |entity| {
            node := toMeta(entity)
            func.call(node)
            walk(func, node.children)
        }
    }

    Void filter(|EntityMeta| func, EntityMeta node) {
        func.call(node)
        if (node.visible)
            node.childrenAsNodes.each { filter(func, it) }
    }

    EntityMeta[] toMetas(Entity[] entities) {
        entities.map { toMeta(it) }
    }
    
    EntityMeta toMeta(Entity entity) {
        nodeMap.getOrAdd(entity) {
            entityMetaSrc.toMeta(entity, nodeMap)
        }
    }
    
    Void collapseAll(Tree tree) {
        nodeMap.keys.each { tree.setExpanded(it, false) }
    }
}