haxeui/haxeui-core

View on GitHub
haxe/ui/containers/TreeViewNode.hx

Summary

Maintainability
Test Coverage
package haxe.ui.containers;

import haxe.ui.behaviours.Behaviour;
import haxe.ui.behaviours.DataBehaviour;
import haxe.ui.components.Image;
import haxe.ui.containers.Box;
import haxe.ui.containers.HBox;
import haxe.ui.containers.VBox;
import haxe.ui.core.Component;
import haxe.ui.core.CompositeBuilder;
import haxe.ui.core.InteractiveComponent;
import haxe.ui.core.ItemRenderer;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.TreeViewEvent;
import haxe.ui.util.Variant;

#if (haxe_ver >= 4.2)
import Std.isOfType;
#else
import Std.is as isOfType;
#end

@:composite(TreeViewNodeEvents, TreeViewNodeBuilder)
class TreeViewNode extends VBox {
    @:behaviour(Expanded, false)        public var expanded:Bool;
    @:behaviour(Expandable)             public var expandable:Null<Bool>;
    
    @:call(AddNode)                     public function addNode(data:Dynamic):TreeViewNode;
    @:call(RemoveNode)                  public function removeNode(node:TreeViewNode):TreeViewNode;
    @:call(ClearNodes)                  public function clearNodes():Void;
    @:call(GetNodesInternal)            private function getNodesInternal():Array<Component>;
    
    public var parentNode:TreeViewNode = null;
    
    public function nodePath(field:String = null):String {
        if (field == null) { // lets try to guess a field to use in the path
            if (Reflect.hasField(this.data, "id")) {
                field = "id";
            } else if (Reflect.hasField(this.data, "nodeId")) {
                field = "nodeId";
            } else {
                field = "text";
            }
        }
        var parts = [];
        var p = this;
        while (p != null) {
            parts.push(Reflect.field(p.data, field));
            p = p.parentNode;
        }
        parts.reverse();
        return parts.join("/");
    }
    
    public function findNodes(fieldValue:Any, fieldName:String = null):Array<TreeViewNode> {
        if (fieldName == null) { // lets try to guess a field to use
            fieldName = "text";
        }

        var foundNodes = [];
        if (Reflect.hasField(this.data, fieldName)) {
            var nodeFieldValue = Reflect.field(this.data, fieldName);
            if (nodeFieldValue == fieldValue) {
                foundNodes.push(this);
            }
        }

        for (child in getNodes()) {
            var childNodesFound = child.findNodes(fieldValue, fieldName);
            if (childNodesFound != null && childNodesFound.length > 0) {
                foundNodes = foundNodes.concat(childNodesFound);
            }
        }

        return foundNodes;
    }

    public function findNode(fieldValue:Any, fieldName:String = null):TreeViewNode {
        if (fieldName == null) { // lets try to guess a field to use
            fieldName = "text";
        }

        if (Reflect.hasField(this.data, fieldName)) {
            var nodeFieldValue = Reflect.field(this.data, fieldName);
            if (nodeFieldValue == fieldValue) {
                return this;
            }
        }

        var foundNode = null;
        for (child in getNodes()) {
            foundNode = child.findNode(fieldValue, fieldName);
            if (foundNode != null) {
                break;
            }
        }

        return foundNode;
    }

    public function findNodeByPath(path:String, field:String = null):TreeViewNode {
        var foundNode = null;
        
        var parts = path.split("/");
        var part = parts.shift();
        
        var nodes = getNodes();
        for (node in nodes) {
            if (node.matchesPathPart(part, field)) {
                if (parts.length == 0) {
                    foundNode = node;
                } else {
                    foundNode = node.findNodeByPath(parts.join("/"), field);
                }
                break;
            }
        }
        
        return foundNode;
    }
    
    private function matchesPathPart(part:String, field:String = null):Bool {
        if (field == null) { // lets try to guess a field to use in the path
            if (Reflect.hasField(this.data, "id")) {
                field = "id";
            } else if (Reflect.hasField(this.data, "nodeId")) {
                field = "nodeId";
            } else {
                field = "text";
            }
        }
        
        if (Reflect.hasField(this.data, field) == false) {
            return false;
        }
        
        return Std.string(Reflect.field(this.data, field)) == part;
    }
    
    public function getNodes():Array<TreeViewNode> {
        var nodes:Array<TreeViewNode> = [];
        var internalNodes = getNodesInternal();
        for (node in internalNodes) {
            nodes.push(cast node);
        }
        return nodes;
    }
    
    public var selected(get, set):Bool;
    private function get_selected():Bool {
        var treeview = findAncestor(TreeView);
        return treeview.selectedNode == this;
    }
    private function set_selected(value:Bool):Bool {
        var treeview = findAncestor(TreeView);
        treeview.selectedNode = this;
        return value;
    }
    
    private var _data:Dynamic = null;
    public var data(get, set):Dynamic;
    private function get_data():Dynamic {
        return _data;
    }
    private function set_data(value:Dynamic):Dynamic {
        if (value == _data) {
            return value;
        }

        _data = value;
        invalidateComponentData();
        return value;
    }
    
    private override function get_text():String {
        return _data.text;
    }
    private override function set_text(value:String):String {
        if (_data == null) {
            _data = {};
        }
        _data.text = value;
        this.data = Reflect.copy(_data); // TEMP
        return value;
    }
    
    private override function get_icon():Variant {
        return Variant.fromDynamic(_data.icon);
    }
    private override function set_icon(value:Variant):Variant {
        if (_data == null) {
            _data = {};
        }
        _data.icon = value;
        this.data = Reflect.copy(_data); // TEMP
        return value;
    }
}

//***********************************************************************************************************
// Behaviours
//***********************************************************************************************************
@:dox(hide) @:noCompletion
private class AddNode extends Behaviour {
    public override function call(param:Any = null):Variant {
        var node = new TreeViewNode();
        node.data = param;
        _component.addComponent(node);
        return node;
    }
}

@:dox(hide) @:noCompletion
private class RemoveNode extends Behaviour {
    public override function call(param:Any = null):Variant {
        var node:TreeViewNode = param;
        _component.removeComponent(node);
        return node;
    }
}

@:dox(hide) @:noCompletion
private class ClearNodes extends Behaviour {
    public override function call(param:Any = null):Variant {
        var node:TreeViewNode = cast(_component, TreeViewNode);
        var nodes = node.findComponents(TreeViewNode, 3);
        for (n in nodes) {
            node.removeComponent(n);
        }
        return null;
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class Expanded extends DataBehaviour {
    private override function validateData() {
        var childContainer = _component.findComponent("treenode-child-container", Box);
        if (childContainer != null) {
            if (_value == true) {
                childContainer.show();
            } else {
                childContainer.hide();
            }

            var builder:TreeViewNodeBuilder = cast(_component._compositeBuilder, TreeViewNodeBuilder);
            builder.updateIconClass();
        }
        
        var eventType = TreeViewEvent.NODE_EXPANDED;
        if (_value == false) {
            eventType = TreeViewEvent.NODE_COLLAPSED;
        }
        var event = new TreeViewEvent(eventType);
        event.node = cast(_component, TreeViewNode);
        var treeview = _component.findAncestor(TreeView);
        treeview.dispatch(event);

        
    }
}

@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
private class Expandable extends DataBehaviour {
    private override function validateData() {
        var builder:TreeViewNodeBuilder = cast(_component._compositeBuilder, TreeViewNodeBuilder);
        if (_value == true) {
            @:privateAccess builder.changeToExpandableRenderer();
        } else {
            @:privateAccess builder.changeToNonExpandableRenderer();
        }
    }
}

@:dox(hide) @:noCompletion
private class GetNodesInternal extends Behaviour {
    public override function call(param:Any = null):Variant {
        var nodes = _component.findComponents(TreeViewNode, 3); // TODO: is this brittle? Will it always be 3?
        return nodes;
    }
}

//***********************************************************************************************************
// Events
//***********************************************************************************************************
@:dox(hide) @:noCompletion
private class TreeViewNodeEvents extends haxe.ui.events.Events {
    @:keep private var _node:TreeViewNode;
    
    public function new(node:TreeViewNode) {
        super(node);
        _node = node;
    }
}

//***********************************************************************************************************
// Composite Builder
//***********************************************************************************************************
@:dox(hide) @:noCompletion
private class TreeViewNodeBuilder extends CompositeBuilder {
    private var _node:TreeViewNode;
    private var _nodeContainer:HBox = null;
    private var _expandCollapseIcon:Image = null;
    private var _renderer:ItemRenderer = null;
    private var _childContainer:VBox = null;
    
    private var _isExpandable:Bool = false;
    
    public function new(node:TreeViewNode) {
        super(node);
        _node = node;
    }
    
    public override function onInitialize() {
        var treeview = _node.findAncestor(TreeView);
        
        _nodeContainer = new HBox();
        _nodeContainer.addClass("treenode-container");
        _expandCollapseIcon = new Image();
        _expandCollapseIcon.scriptAccess = false;
        _expandCollapseIcon.addClass("treenode-expand-collapse-icon");
        _expandCollapseIcon.id = "treenode-expand-collapse-icon";
        _expandCollapseIcon.registerEvent(MouseEvent.CLICK, onExpandCollapseClicked);
        _nodeContainer.registerEvent(MouseEvent.CLICK, onContainerClick);
        _nodeContainer.registerEvent(MouseEvent.RIGHT_CLICK, onContainerClick);
        _nodeContainer.registerEvent(MouseEvent.DBL_CLICK, onContainerDblClick);
        _nodeContainer.addComponent(_expandCollapseIcon);

        _renderer = treeview.itemRenderer.cloneComponent();
        _renderer.itemIndex = _node.parentComponent.getComponentIndex(_node);
        _renderer.data = _node.data;
        _nodeContainer.addComponent(_renderer);
        
        
        if (_isExpandable == true) {
            makeExpandableRendererChanges();
        }
        _node.addComponentAt(_nodeContainer, 0);
    }

    private function onContainerClick(event:MouseEvent) {
        if (_expandCollapseIcon.hitTest(event.screenX, event.screenY)) {
            return;
        }

        var interactives = _node.findComponentsUnderPoint(event.screenX, event.screenY, InteractiveComponent);
        if (interactives.length > 0) {
            return;
        }
        
        var treeview = _node.findAncestor(TreeView);
        treeview.selectedNode = _node;
    }
    
    private function onContainerDblClick(event:MouseEvent) {
        var interactives = _node.findComponentsUnderPoint(event.screenX, event.screenY, InteractiveComponent);
        if (interactives.length > 0) {
            return;
        }
        
        onExpandCollapseClicked(event);
    }
    
    private function onExpandCollapseClicked(event:MouseEvent) {
        if (!_isExpandable) {
            return;
        }
        event.cancel();
        _node.expanded = !_node.expanded;
        updateIconClass();
    }
    
    public function updateIconClass() {
        if (_expandCollapseIcon != null) {
            if (_node.expanded == true) {
                _expandCollapseIcon.swapClass("node-expanded", "node-collapsed");
            } else {
                _expandCollapseIcon.swapClass("node-collapsed", "node-expanded");
            }
        }
    }
    
    public override function validateComponentData() {
        _renderer.data = _node.data;
    }
    
    private function changeToExpandableRenderer() {
        if (_isExpandable == true) {
            return;
        }
        
        _isExpandable = true;
        makeExpandableRendererChanges();
    }
    
    private function changeToNonExpandableRenderer() {
        if (_isExpandable == false) {
            return;
        }
        
        _isExpandable = false;
        makeNonExpandableRendererChanges();
    }
    
    private function makeNonExpandableRendererChanges() {
        var treeview = _node.findAncestor(TreeView);
        
        if (_expandCollapseIcon != null) {
            _expandCollapseIcon.removeClasses(["node-collapsed", "node-expanded"]);
        }
        
        if (_renderer != null) {
            var wasSelected = (treeview.selectedNode == _node);
            var data = _renderer.data;
            var newRenderer = treeview.itemRenderer.cloneComponent();
            newRenderer.data = data;
            _nodeContainer.removeComponent(_renderer);
            _renderer = newRenderer;
            _nodeContainer.addComponent(newRenderer);
            if (wasSelected == true) {
                //treeview.clearSelection();
                treeview.selectedNode = _node;
            }
        }
    }
    
    private function makeExpandableRendererChanges() {
        var treeview = _node.findAncestor(TreeView);
        
        updateIconClass();
        if (_renderer != null) {
            var wasSelected = (treeview.selectedNode == _node);
            var itemIndex = _renderer.itemIndex;
            var data = _renderer.data;
            var newRenderer = treeview.expandableItemRenderer.cloneComponent();
            newRenderer.data = data;
            newRenderer.itemIndex = itemIndex;
            _nodeContainer.removeComponent(_renderer);
            _renderer = newRenderer;
            _nodeContainer.addComponent(newRenderer);
            if (wasSelected == true) {
                //treeview.clearSelection();
                treeview.selectedNode = _node;
            }
        }
    }
    
    public override function addComponent(child:Component) {
        if (child == _renderer || child == _childContainer) {
            return null;
        }
        
        if ((child is TreeViewNode)) {
            cast(child, TreeViewNode).parentNode = _node;
            if (_childContainer == null) {
                _childContainer = new VBox();
                if (_node.expanded == true) {
                    _childContainer.show();
                } else {
                    _childContainer.hide();
                }
                _childContainer.addClass("treenode-child-container");
                _childContainer.id = "treenode-child-container";
                _node.addComponent(_childContainer);
            }
            changeToExpandableRenderer();
            return _childContainer.addComponent(child);
        }
        
        return null;
    }
    
    public override function addComponentAt(child:Component, index:Int) {
        if (child == _renderer || child == _childContainer) {
            return null;
        }
        
        if ((child is TreeViewNode)) {
            cast(child, TreeViewNode).parentNode = _node;
            if (_childContainer == null) {
                _childContainer = new VBox();
                if (_node.expanded == true) {
                    _childContainer.show();
                } else {
                    _childContainer.hide();
                }
                _childContainer.addClass("treenode-child-container");
                _childContainer.id = "treenode-child-container";
                _node.addComponent(_childContainer);
            }
            changeToExpandableRenderer();
            return _childContainer.addComponentAt(child,index);
        }
        
        return null;
    }
    
    public override function removeComponent(child:Component, dispose:Bool = true, invalidate:Bool = true) {
        if ((child is TreeViewNode)) {
            cast(child, TreeViewNode).parentNode = null;
            var c = _childContainer.removeComponent(child, dispose, invalidate);
            if (_childContainer.numComponents == 0 && _node.expandable != true) {
                changeToNonExpandableRenderer();
            }
            return c;
        }
        return null;
    }
}