UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

940 lines (825 loc) 30.6 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2009 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Martin Wittemann (martinwittemann) ************************************************************************ */ /** * <h2>Tree Controller</h2> * * *General idea* * * The tree controller is the controller made for the {@link qx.ui.tree.Tree} * widget in qooxdoo. Therefore, it is responsible for creating and adding the * tree folders to the tree given as target. * * *Features* * * * Synchronize the model and the target * * Label and icon are bindable * * Takes care of the selection * * Passes on the options used by {@link qx.data.SingleValueBinding#bind} * * *Usage* * * As model, you can use every qooxdoo widget structure having one property, * which is a data array holding the children of the current node. There can * be as many additional as you like. * You need to specify a model, a target, a child path and a label path to * make the controller work. * * *Cross reference* * * * If you want to bind single values, use {@link qx.data.controller.Object} * * If you want to bind a list like widget, use {@link qx.data.controller.List} * * If you want to bin a form widget, use {@link qx.data.controller.Form} */ qx.Class.define("qx.data.controller.Tree", { extend : qx.core.Object, include: qx.data.controller.MSelection, implement : [ qx.data.controller.ISelection ], /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ /** * @param model {qx.core.Object?null} The root element of the model, which holds * the data. * * @param target {qx.ui.tree.Tree?null} The target widgets which should be a tree. * * @param childPath {String?null} The name of the property in the model, which * holds the data array containing the children. * * @param labelPath {String?null} The name of the property in the model, * which holds the value to be displayed as the label of the tree items. */ construct : function(model, target, childPath, labelPath) { this.base(arguments); // internal bindings reference this.__bindings = {}; this.__boundProperties = []; // reference to the child this.__childrenRef = { a:1 }; if (childPath != null) { this.setChildPath(childPath); } if (labelPath != null) { this.setLabelPath(labelPath); } if (model != null) { this.setModel(model); } if (target != null) { this.setTarget(target); } }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties : { /** The root element of the data. */ model : { check: "qx.core.Object", apply: "_applyModel", event: "changeModel", nullable: true, dereference: true }, /** The tree to bind the data to. */ target : { apply: "_applyTarget", event: "changeTarget", init: null, nullable: true, dereference: true }, /** The name of the property, where the children are stored in the model. */ childPath : { check: "String", apply: "_applyChildPath", nullable: true }, /** * The name of the property, where the value for the tree folders label * is stored in the model classes. */ labelPath : { check: "String", apply: "_applyLabelPath", nullable: true }, /** * The name of the property, where the source for the tree folders icon * is stored in the model classes. */ iconPath : { check: "String", apply: "_applyIconPath", nullable: true }, /** * A map containing the options for the label binding. The possible keys * can be found in the {@link qx.data.SingleValueBinding} documentation. */ labelOptions : { apply: "_applyLabelOptions", nullable: true }, /** * A map containing the options for the icon binding. The possible keys * can be found in the {@link qx.data.SingleValueBinding} documentation. */ iconOptions : { apply: "_applyIconOptions", nullable: true }, /** * Delegation object, which can have one ore more function defined by the * {@link IControllerDelegate} interface. */ delegate : { apply: "_applyDelegate", init: null, nullable: true } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { // private members __childrenRef : null, __bindings : null, __boundProperties : null, __oldChildrenPath : null, /* --------------------------------------------------------------------------- APPLY METHODS --------------------------------------------------------------------------- */ /** * If a new delegate is set, it applies the stored configuration for the * tree folder to the already created folders once. * * @param value {qx.core.Object|null} The new delegate. * @param old {qx.core.Object|null} The old delegate. */ _applyDelegate: function(value, old) { this._setConfigureItem(value, old); this._setCreateItem(value, old); this._setBindItem(value, old); }, /** * Apply-method which will be called after the icon options had been * changed. This method will invoke a renewing of all bindings. * * @param value {Map|null} The new options map. * @param old {Map|null} The old options map. */ _applyIconOptions: function(value, old) { this.__renewBindings(); }, /** * Apply-method which will be called after the label options had been * changed. This method will invoke a renewing of all bindings. * * @param value {Map|null} The new options map. * @param old {Map|null} The old options map. */ _applyLabelOptions: function(value, old) { this.__renewBindings(); }, /** * Apply-method which will be called after the target had been * changed. This method will clean up the old tree and will initially * build up the new tree containing the data from the model. * * @param value {qx.ui.tree.Tree|null} The new tree. * @param old {qx.ui.tree.Tree|null} The old tree. */ _applyTarget: function(value, old) { // if there was an old target if (old != undefined) { this.__emptyTarget(old); } // if a model is set if (this.getModel() != null) { // build up the tree this.__buildTree(); } // add a listener for the target change this._addChangeTargetListener(value, old); }, /** * Apply-method which will be called after the model had been * changed. This method invoke a new building of the tree. * * @param value {qx.core.Object|null} The new tree. * @param old {qx.core.Object|null} The old tree. */ _applyModel: function(value, old) { this.__buildTree(); }, /** * Apply-method which will be called after the child path had been * changed. This method invoke a new building of the tree. * * @param value {String|null} The new path to the children property. * @param old {String|null} The old path to the children property. */ _applyChildPath: function(value, old) { // save the old name because it is needed to remove the old bindings this.__oldChildrenPath = old; this.__buildTree(); // reset the old name this.__oldChildrenPath = null; }, /** * Apply-method which will be called after the icon path had been * changed. This method invoke a new building of the tree. * * @param value {String|null} The new path to the icon property. * @param old {String|null} The old path or the icon property. */ _applyIconPath: function(value, old) { this.__renewBindings(); }, /** * Apply-method which will be called after the label path had been * changed. This method invoke a new building of the tree. * * @param value {String|null} The new path to the label property. * @param old {String|null} The old path of the label property. */ _applyLabelPath: function(value, old) { this.__buildTree(); }, /* --------------------------------------------------------------------------- EVENT HANDLER --------------------------------------------------------------------------- */ /** * Handler function handling the change of a length of a children array. * This method invokes a rebuild of the corresponding subtree. * * @param ev {qx.event.type.Event} The changeLength event of a data array. */ __changeModelChildren: function(ev) { // get the stored data var children = ev.getTarget(); qx.core.ObjectRegistry.register(children); var treeNode = this.__childrenRef[children.toHashCode()].treeNode; var modelNode = this.__childrenRef[children.toHashCode()].modelNode; // update the subtree this.__updateTreeChildren(treeNode, modelNode); // update the selection in case a selected element has been removed this._updateSelection(); }, /** * Handler function taking care of the changes of the children array itself. * * @param e {qx.event.type.Data} Change event for the children property. */ __changeChildrenArray: function(e) { var children = e.getData(); var oldChildren = e.getOldData(); // get the old ref and delete it var oldRef = this.__childrenRef[oldChildren.toHashCode()]; oldChildren.removeListenerById(oldRef.changeListenerId); this.debug("1: removing children="+ oldChildren.toHashCode() + " from this=" + this.toHashCode()); delete this.__childrenRef[oldChildren.toHashCode()]; // remove the old change listener for the children oldRef.modelNode.removeListenerById(oldRef.changeChildernListenerId); // add a new change listener var modelNode = oldRef.modelNode; var property = qx.Class.getPropertyDefinition( oldRef.modelNode.constructor, this.getChildPath() ); var eventName = property.event; var changeChildernListenerId = modelNode.addListener( eventName, this.__changeChildrenArray, this ); // add the new ref var treeNode = oldRef.treeNode; this.debug("1: adding children="+ children.toHashCode() + " to this=" + this.toHashCode()); this.__childrenRef[children.toHashCode()] = { modelNode: modelNode, treeNode: treeNode, changeListenerId: oldRef.changeListenerId, changeChildernListenerId : changeChildernListenerId }; // update the subtree this.__updateTreeChildren(treeNode, modelNode); // update the selection in case a selected element has been removed this._updateSelection(); }, /* --------------------------------------------------------------------------- ITEM HANDLING --------------------------------------------------------------------------- */ /** * Creates a TreeFolder and delegates the configure method if a delegate is * set and the needed function (configureItem) is available. * * @return {qx.ui.tree.core.AbstractTreeItem} The created and configured TreeFolder. */ _createItem: function() { var delegate = this.getDelegate(); // check if a delegate and a create method is set if (delegate != null && delegate.createItem != null) { var item = delegate.createItem(); } else { var item = new qx.ui.tree.TreeFolder(); } // check if a delegate is set and if the configure function is available if (delegate != null && delegate.configureItem != null) { delegate.configureItem(item); } return item; }, /** * Internal helper function to build up the tree corresponding to the data * stored in the model. This function creates the root node and hands the * recursive creation of all subtrees to the {#__updateTreeChildren} * function. */ __buildTree: function() { // only fill the target if there is a target, its known how to // access the children and what needs to be displayed as label if (this.getTarget() == null || this.getChildPath() == null) { return; } // check for the binding knowledge if ( (this.getLabelPath() == null && this.getDelegate() == null) || (this.getLabelPath() == null && this.getDelegate() != null && this.getDelegate().bindItem == null) ) { return; } // Clean the target completely this.__emptyTarget(); // only build up a new tree if a model is given if (this.getModel() != null) { // create a new root node var rootNode = this._createItem(); rootNode.setModel(this.getModel()); // bind the root node this.__addBinding(this.getModel(), rootNode); this.__updateTreeChildren(rootNode, this.getModel()); // assign the new root once the tree has been built this.getTarget().setRoot(rootNode); } }, /** * Main method building up the tree folders corresponding to the given * model node. The new created subtree will be added to the given tree node. * * @param rootNode {qx.ui.tree.TreeFolder} The tree folder to add the new * created subtree. * * @param modelNode {qx.core.Object} The model nodes which represent the * data in the current subtree. */ __updateTreeChildren: function(rootNode, modelNode) { // ignore items which don't have children if (modelNode["get" + qx.lang.String.firstUp(this.getChildPath())] == undefined) { return; } // get all children of the current model node var children = modelNode["get" + qx.lang.String.firstUp(this.getChildPath())](); // store the children reference if (this.__childrenRef[children.toHashCode()] == undefined) { // add the listener for the change var changeListenerId = children.addListener( "change", this.__changeModelChildren, this ); // add a listener for the change of the children array itself var property = qx.Class.getPropertyDefinition( modelNode.constructor, this.getChildPath() ); var eventName = property.event; var changeChildernListenerId = modelNode.addListener( eventName, this.__changeChildrenArray, this ); this.debug("2: adding children="+ children.toHashCode() + " to this=" + this.toHashCode()); this.__childrenRef[children.toHashCode()] = { modelNode: modelNode, treeNode: rootNode, changeListenerId: changeListenerId, changeChildernListenerId : changeChildernListenerId }; } // go threw all children in the model for (var i = 0; i < children.length; i++) { // if there is no node in the tree or the current node does not fit if (rootNode.getChildren()[i] == null || children.getItem(i) != rootNode.getChildren()[i].getModel()) { //check if the node was just moved for (var j = i; j < rootNode.getChildren().length; j++) { if (rootNode.getChildren()[j].getModel() === children.getItem(i)) { var oldIndex = j; break; } } // if it is in the tree if (oldIndex != undefined) { // get the corresponding node var currentNode = rootNode.getChildren()[oldIndex]; // check if it is selected if (this.getTarget().isSelected(currentNode)) { var wasSelected = true; } // remove the item at its old place (will remove the selection) rootNode.removeAt(oldIndex); // add the node at the current position rootNode.addAt(currentNode, i); // select it again if it was selected if (wasSelected) { this.getTarget().addToSelection(currentNode); } // if the node is new } else { // add the child node var treeNode = this._createItem(); treeNode.setModel(children.getItem(i)); rootNode.addAt(treeNode, i); this.__addBinding(children.getItem(i), treeNode); // add all children recursive this.__updateTreeChildren(treeNode, children.getItem(i)); } } } // remove the rest of the tree items if they exist for (var i = rootNode.getChildren().length -1; i >= children.length; i--) { var treeFolder = rootNode.getChildren()[i]; this.__removeFolder(treeFolder, rootNode); } }, /** * Removes all folders and bindings for the current set target. * @param tree {qx.ui.tree.Tree} The tree to empty. */ __emptyTarget: function(tree) { if (tree == null) { tree = this.getTarget(); } // only do something if a tree is set if (tree == null) { return; } // remove the root node var root = tree.getRoot(); if (root != null) { tree.setRoot(null); this.__removeAllFolders(root); var model = root.getModel(); if (model) { this.__removeBinding(model); } root.destroy(); this.debug("erasing all children from this=" + this.toHashCode()); this.__childrenRef = { b: 2}; } }, /** * Removes all child folders of the given tree node. Also removes all * bindings for the removed folders. * * @param node {qx.ui.tree.core.AbstractTreeItem} The used tree folder. */ __removeAllFolders: function(node) { var children = node.getChildren() || []; // remove all subchildren for (var i = children.length - 1; i >= 0; i--) { if (children[i].getChildren().length > 0) { this.__removeAllFolders(children[i]); } this.__removeFolder(children[i], node); } }, /** * Internal helper method removing the given folder form the given root * node. All set bindings will be removed and the old tree folder will be * destroyed. * * @param treeFolder {qx.ui.tree.core.AbstractTreeItem} The folder to remove. * @param rootNode {qx.ui.tree.core.AbstractTreeItem} The folder holding the * treeFolder. */ __removeFolder: function(treeFolder, rootNode) { // get the model var model = treeFolder.getModel(); var childPath = this.__oldChildrenPath || this.getChildPath(); var childrenGetterName = "get" + qx.lang.String.firstUp(childPath); // if the model does have a child path if (model[childrenGetterName] != undefined) { // remove the old children listener var children = model[childrenGetterName](); this.debug("2: removing children="+ children.toHashCode() + " from this=" + this.toHashCode()); var oldRef = this.__childrenRef[children.toHashCode()]; children.removeListenerById(oldRef.changeListenerId); model.removeListenerById(oldRef.changeChildernListenerId); // also remove all its children [BUG #4296] this.__removeAllFolders(treeFolder); // delete the model reference delete this.__childrenRef[children.toHashCode()]; } // get the binding and remove it this.__removeBinding(model); // remove the folder from the tree rootNode.remove(treeFolder); // get rid of the old tree folder treeFolder.destroy(); }, /* --------------------------------------------------------------------------- BINDING STUFF --------------------------------------------------------------------------- */ /** * Helper method for binding a given property from the model to the target * widget. * This method should only be called in the * {@link qx.data.controller.IControllerDelegate#bindItem} function * implemented by the {@link #delegate} property. * * @param sourcePath {String | null} The path to the property in the model. * If you use an empty string, the whole model item will be bound. * @param targetPath {String} The name of the property in the target * widget. * @param options {Map | null} The options to use by * {@link qx.data.SingleValueBinding#bind} for the binding. * @param targetWidget {qx.ui.tree.core.AbstractTreeItem} The target widget. * @param modelNode {var} The model node which should be bound to the target. */ bindProperty: function(sourcePath, targetPath, options, targetWidget, modelNode) { // set up the binding var id = modelNode.bind(sourcePath, targetWidget, targetPath, options); // check for the storage for the references if (this.__bindings[targetPath] == null) { this.__bindings[targetPath] = {}; } // store the binding reference var storage = this.__bindings[targetPath]; qx.core.ObjectRegistry.register(modelNode); if (storage[modelNode.toHashCode()]) { if (storage[modelNode.toHashCode()].id) { throw new Error( "Can not bind the same target property '" + targetPath + "' twice." ); } storage[modelNode.toHashCode()].id = id; } else { storage[modelNode.toHashCode()] = { id: id, reverseId: null, treeNode: targetWidget }; } // save the bound property if (!this.__boundProperties.includes(targetPath)) { this.__boundProperties.push(targetPath); } }, /** * Helper method for binding a given property from the target widget to * the model. * This method should only be called in the * {@link qx.data.controller.IControllerDelegate#bindItem} function * implemented by the {@link #delegate} property. * * @param targetPath {String | null} The path to the property in the model. * @param sourcePath {String} The name of the property in the target * widget. * @param options {Map | null} The options to use by * {@link qx.data.SingleValueBinding#bind} for the binding. * @param sourceWidget {qx.ui.tree.core.AbstractTreeItem} The source widget. * @param modelNode {var} The model node which should be bound to the target. */ bindPropertyReverse : function( targetPath, sourcePath, options, sourceWidget, modelNode ) { // set up the binding var id = sourceWidget.bind(sourcePath, modelNode, targetPath, options); // check for the storage for the references if (this.__bindings[sourcePath] == null) { this.__bindings[sourcePath] = {}; } // check if there is already a stored item var storage = this.__bindings[sourcePath]; qx.core.ObjectRegistry.register(modelNode); if (storage[modelNode.toHashCode()]) { if (storage[modelNode.toHashCode()].reverseId) { throw new Error( "Can not reverse bind the same target property '" + targetPath + "' twice." ); } storage[modelNode.toHashCode()].reverseId = id; } else { storage[modelNode.toHashCode()] = { id: null, reverseId: id, treeNode: sourceWidget }; } // save the bound property if (!this.__boundProperties.includes(sourcePath)) { this.__boundProperties.push(sourcePath); } }, /** * Helper method for binding the default properties (label and icon) from * the model to the target widget. * * This method should only be called in the * {@link qx.data.controller.IControllerDelegate#bindItem} function * implemented by the {@link #delegate} property. * * @param treeNode {qx.ui.tree.core.AbstractTreeItem} The tree node * corresponding to the model node. * @param modelNode {qx.core.Object} The model node holding the data. */ bindDefaultProperties : function(treeNode, modelNode) { // label binding this.bindProperty(this.getLabelPath(), "label", this.getLabelOptions(), treeNode, modelNode); // icon binding if (this.getIconPath() != null) { this.bindProperty(this.getIconPath(), "icon", this.getIconOptions(), treeNode, modelNode); } }, /** * Helper method renewing all bindings with the currently saved options and * paths. */ __renewBindings: function() { // get the first bound property var firstProp; for (var key in this.__bindings) { firstProp = key; break; } // go through all stored bindings for that property // (should have all the same amount of entries and tree nodes) for (var hash in this.__bindings[firstProp]) { // get the data var treeNode = this.__bindings[firstProp][hash].treeNode; var modelNode = qx.core.ObjectRegistry.fromHashCode(hash); // remove the old bindings this.__removeBinding(modelNode); // add the new bindings this.__addBinding(modelNode, treeNode); } }, /** * Internal helper method adding the right bindings from the given * modelNode to the given treeNode. * * @param modelNode {qx.core.Object} The model node holding the data. * @param treeNode {qx.ui.tree.TreeFolder} The corresponding tree folder * to the model node. */ __addBinding: function(modelNode, treeNode) { var delegate = this.getDelegate(); // if a delegate for creating the binding is given, use it if (delegate != null && delegate.bindItem != null) { delegate.bindItem(this, treeNode, modelNode); // otherwise, try to bind the listItem by default } else { this.bindDefaultProperties(treeNode, modelNode); } }, /** * Internal helper method for removing bindings for a given model node. * * @param modelNode {qx.core.Object} the model node for which the bindings * should be removed. */ __removeBinding: function(modelNode) { for (var i = 0; i < this.__boundProperties.length; i++) { var property = this.__boundProperties[i]; var bindingsMap = this.__bindings[property][modelNode.toHashCode()]; if (bindingsMap != null) { if (bindingsMap.id) { modelNode.removeBinding(bindingsMap.id); bindingsMap.id = null; } if (bindingsMap.reverseId) { bindingsMap.treeNode.removeBinding(bindingsMap.reverseId); bindingsMap.reverseId = null; } delete this.__bindings[property][modelNode.toHashCode()]; } } }, /* --------------------------------------------------------------------------- DELEGATE HELPER --------------------------------------------------------------------------- */ /** * Helper method for applying the delegate It checks if a configureItem * is set end invokes the initial process to apply the given function. * * @param value {Object} The new delegate. * @param old {Object} The old delegate. */ _setConfigureItem: function(value, old) { if ( value != null && value.configureItem != null && this.getTarget() != null && this.getModel() != null ) { var children = this.getTarget().getRoot().getItems(true, true, false); for (var i = 0; i < children.length; i++) { value.configureItem(children[i]); } } }, /** * Helper method for applying the delegate. It checks if a createItem * is set and invokes the initial process to apply the given function. * * @param value {Object} The new delegate. * @param old {Object} The old delegate. */ _setCreateItem: function(value, old) { // do nothing if no tree can be build if (this.getTarget() == null || this.getModel() == null) { return; } // do nothing if no delegate function is set if (value == null || value.createItem == null) { return; } // do nothing it the delegate function has not changed if (old && old.createItem && value && value.createItem && old.createItem == value.createItem) { return; } this._startSelectionModification(); this.__emptyTarget(); this.__buildTree(); this._endSelectionModification(); this._updateSelection(); }, /** * Helper method for applying the delegate It checks if a bindItem * is set end invokes the initial process to apply the given function. * * @param value {Object} The new delegate. * @param old {Object} The old delegate. */ _setBindItem: function(value, old) { // if a new bindItem function is set if (value != null && value.bindItem != null) { // do nothing if the bindItem function did not change if (old != null && old.bindItem != null && value.bindItem == old.bindItem) { return; } this.__buildTree(); } } }, /* ***************************************************************************** DESTRUCTOR ***************************************************************************** */ destruct : function() { if (this.getTarget() && !this.getTarget().isDisposed()) { this.setTarget(null); } if (this.getModel() != null && !this.getModel().isDisposed()) { this.setModel(null); } this.__bindings = this.__childrenRef = this.__boundProperties = null; } });