Index: lib/GeoExt/widgets/tree/ComplexLayerNode.js
===================================================================
--- lib/GeoExt/widgets/tree/ComplexLayerNode.js	(revision 0)
+++ lib/GeoExt/widgets/tree/ComplexLayerNode.js	(revision 0)
@@ -0,0 +1,397 @@
+/**
+ * Copyright (c) 2008-2009 The Open Source Geospatial Foundation
+ * 
+ * Published under the BSD license.
+ * See http://svn.geoext.org/core/trunk/geoext/license.txt for the full text
+ * of the license.
+ */
+
+Ext.namespace("GeoExt.tree");
+
+/** private: constructor
+ *  .. class:: LayerNodeUI
+ *
+ *      Place in a separate file if this should be documented.
+ */
+GeoExt.tree.ComplexLayerNodeUI = Ext.extend(Ext.tree.TreeNodeUI, {
+    
+    /** private: method[constructor]
+     */
+    constructor: function(config) {
+        GeoExt.tree.ComplexLayerNodeUI.superclass.constructor.apply(this, arguments);
+    },
+    
+    /** private: method[render]
+     *  :param bulkRender: ``Boolean``
+     */
+    render: function(bulkRender) {
+        var layer, a = this.node.attributes;
+        if(this.node.layer){
+			layer = this.node.layer
+		}
+		else if(a.layer && a.layer instanceof OpenLayers.Layer){
+			layer = a.layer;
+		}
+		else{
+		var lyrNdx = this.node.layerStore.find('title',a.layer);
+		if(lyrNdx>-1)layer=this.node.layerStore.getAt(lyrNdx).get('layer');	
+		}
+		if(layer){
+			//then we have a regular layer node and should pass it to the LayerNode render
+			GeoExt.tree.LayerNodeUI.prototype.render.apply(this,arguments);
+			return;
+		}
+		else{
+			//we have some other kind of node
+			if (a.checked === undefined) {
+			/*if all child nodes or unrendered child nodes contain either 
+	 		* a visible layer or a checked node attribute, then this
+	 		* node should be checked
+	 		*/
+				var checked;
+				this.node.eachChild(function(n){
+					checked = (n.attributes.checked||(n.layer&&n.layer.getVisibility())||(n.attributes.layer&&n.layer.getVisibility&&n.layer.getVisibility()));
+					return checked;
+				})
+				a.checked=checked;
+			}
+		}
+        GeoExt.tree.ComplexLayerNodeUI.superclass.render.apply(this, arguments);
+        var cb = this.checkbox;
+        if (a.checkedGroup) {
+			// replace the checkbox with a radio button
+			var radio = Ext.DomHelper.insertAfter(cb, ['<input type="radio" name="', a.checkedGroup, '_checkbox" class="', cb.className, cb.checked ? '" checked="checked"' : '', '"></input>'].join(""));
+			radio.defaultChecked = cb.defaultChecked;
+			Ext.get(cb).remove();
+			this.checkbox = radio;
+			// at least one layer group must be visible with radio buttoned layer containers
+			this.enforceOneVisible();
+		}
+    },
+    
+    /** private: method[onClick]
+     *  :param e: ``Object``
+     */
+    onClick: function(e) {
+        if(e.getTarget('.x-tree-node-cb', 1)) {
+            this.toggleCheck(this.isChecked());
+        } else {
+            GeoExt.tree.ComplexLayerNodeUI.superclass.onClick.apply(this, arguments);
+        }
+    },
+    
+    /** private: method[toggleCheck]
+     * :param value: ``Boolean``
+     */
+    toggleCheck: function(value) {
+        value = (value === undefined ? !this.isChecked() : value);
+        GeoExt.tree.ComplexLayerNodeUI.superclass.toggleCheck.call(this, value);
+        this.enforceOneVisible();
+    },
+    
+    /** private: method[enforceOneVisible]
+     * 
+     *  Makes sure that only one layer is visible if checkedGroup is set.
+     */
+    enforceOneVisible: function(){
+		var attributes = this.node.attributes;
+		var group = attributes.checkedGroup;
+		if (group) {
+			var checkedNodes = [], checkedCount = 0;
+			this.node.parentNode.eachChild(function(n){
+				if (n.checked || n.attributes.checked || n.ui.isChecked()) {
+					checkedNodes.push(n)
+				}
+			})
+			checkedCount = checkedNodes.length;
+		}
+		// enforce "not more than one visible"
+		if (checkedCount > 1) {
+			//keep the first checked node checked and uncheck the rest
+			checkedNodes.shift();
+			Ext.each(checkedNodes, function(n){
+				n.cascade(function(nd){
+					if (nd.ui.isChecked()) {
+						nd.ui.toggleCheck(false);
+					}
+					if (nd.layer && nd.layer.visibility) {
+						nd.layer.setVisibility(false);
+					}
+				})
+			});
+		}
+		// enforce "at least one visible"
+		if (checkedCount === 0 && attributes.checked == false) {
+			this.toggleCheck(true);
+			this.node.cascade(function(nd){
+				if ((nd.ui.isChecked() || nd.attributes.checked) && (nd.layer && !nd.layer.visibility)) {
+					nd.layer.setVisibility(true);
+				}
+			})
+		}
+	},
+    
+    /** private: method[appendDDGhost]
+     *  :param ghostNode ``DOMElement``
+     *  
+     *  For radio buttons, makes sure that we do not use the option group of
+     *  the original, otherwise only the original or the clone can be checked 
+     */
+    appendDDGhost : function(ghostNode){
+        var n = this.elNode.cloneNode(true);
+        var radio = Ext.DomQuery.select("input[type='radio']", n);
+        Ext.each(radio, function(r) {
+            r.name = r.name + "_clone";
+        });
+        ghostNode.appendChild(n);
+    }
+});
+
+
+/** api: (define)
+ *  module = GeoExt.tree
+ *  class = LayerNode
+ *  base_link = `Ext.tree.TreeNode <http://extjs.com/deploy/dev/docs/?class=Ext.tree.TreeNode>`_
+ */
+
+/** api: constructor
+ *  .. class:: LayerNode(config)
+ * 
+ *      A subclass of ``Ext.tree.TreeNode`` that is connected to an
+ *      ``OpenLayers.Layer`` by setting the node's layer property. Checking or
+ *      unchecking the checkbox of this node will directly affect the layer and
+ *      vice versa. The default iconCls for this node's icon is
+ *      "gx-tree-layer-icon", unless it has children.
+ * 
+ *      Setting the node's layer property to a layer name instead of an object
+ *      will also work. As soon as a layer is found, it will be stored as layer
+ *      property in the attributes hash.
+ * 
+ *      The node's text property defaults to the layer name.
+ *      
+ *      If the node has a checkedGroup attribute configured, it will be
+ *      rendered with a radio button instead of the checkbox. The value of
+ *      the checkedGroup attribute is a string, identifying the options group
+ *      for the node.
+ * 
+ *      To use this node type in a ``TreePanel`` config, set ``nodeType`` to
+ *      "gx_layer".
+ */
+GeoExt.tree.ComplexLayerNode = Ext.extend(Ext.tree.AsyncTreeNode, {
+    
+    /** api: config[layerStore]
+     *  :class:`GeoExt.data.LayerStore` ``or "auto"``
+     *  The layer store containing the layer that this node represents.  If set
+     *  to "auto", the node will query the ComponentManager for a
+     *  :class:`GeoExt.MapPanel`, take the first one it finds and take its layer
+     *  store. This property is only required if ``layer`` is provided as a
+     *  string.
+     */
+    layerStore: null,
+    
+    /** api: config[loader]
+     *  ``Ext.tree.TreeLoader|Object`` If provided, subnodes will be added to
+     *  this LayerNode. Obviously, only loaders that process an
+     *  ``OpenLayers.Layer`` or :class:`GeoExt.data.LayerRecord` (like
+     *  :class:`GeoExt.tree.LayerParamsLoader`) will actually generate child
+     *  nodes here. If provided as ``Object``, a
+     *  :class:`GeoExt.tree.LayerParamLoader` instance will be created, with
+     *  the provided object as configuration.
+     */
+    
+    /** private: method[constructor]
+     *  Private constructor override.
+     */
+    constructor: function(config) {
+        if(config.layer){
+			//we have a regular layer node, pass it to that
+			GeoExt.tree.LayerNode.prototype.constructor.apply(this,arguments);
+			return;
+		}
+		
+		config.leaf = config.leaf || !(config.children || config.loader);
+        
+        if(!config.iconCls && !config.children) {
+            config.iconCls = "gx-tree-layer-icon";
+        }
+        if(config.loader && !(config.loader instanceof Ext.tree.TreeLoader || config.loader instanceof GeoExt.tree.LayerLoader)) {
+            config.loader = new GeoExt.tree.LayerLoader(config.loader);
+        }
+        
+        this.defaultUI = this.defaultUI || GeoExt.tree.ComplexLayerNodeUI;
+        if(config.layerStore && !(config.layerStore instanceof GeoExt.data.LayerStore)){
+			config.layerStore = new GeoExt.data.LayerStore(config.layerStore);
+		}
+        Ext.apply(this, {
+            layerStore: config.layerStore
+        });
+        if (config.text) {
+            this.fixedText = true;
+        }
+        GeoExt.tree.ComplexLayerNode.superclass.constructor.apply(this, arguments);
+    },
+
+    /** private: method[render]
+     *  :param bulkRender: ``Boolean``
+     */
+    render: function(bulkRender) {
+            // guess the store if not provided
+            if(!this.layerStore || this.layerStore == "auto") {
+                this.layerStore = GeoExt.MapPanel.guess().layers;
+            }
+        if (!this.rendered) {
+            var ui = this.getUI();
+                            if(!this.text) {
+                    this.text = 'Complex Layer Group'
+                }
+               if(!this.attributes.hidden) ui.show();
+                this.addVisibilityEventHandlers();
+            }
+            if(this.layerStore instanceof GeoExt.data.LayerStore) {
+                this.addStoreEventHandlers();
+            }            
+        GeoExt.tree.ComplexLayerNode.superclass.render.apply(this, arguments);
+    },
+    
+    /** private: method[addVisibilityHandlers]
+     *  Adds handlers that sync the checkbox state with the layer's visibility
+     *  state
+     */
+    addVisibilityEventHandlers: function() {
+/*
+        this.layer.events.on({
+            "visibilitychanged": this.onLayerVisibilityChanged,
+            scope: this
+        }); 
+
+*/        this.on({
+            "checkchange": this.onCheckChange,
+            scope: this
+        });
+    },
+    
+    /** private: method[onLayerVisiilityChanged
+     *  handler for visibilitychanged events on the layer
+     */
+    /*
+onLayerVisibilityChanged: function() {
+        if(!this._visibilityChanging) {
+            this.getUI().toggleCheck(this.layer.getVisibility());
+        }
+    },
+    
+*/
+    /** private: method[onCheckChange]
+     *  :param node: ``GeoExt.tree.LayerNode``
+     *  :param checked: ``Boolean``
+     *
+     *  handler for checkchange events 
+     */
+    onCheckChange: function(node, checked){
+		this._visibilityChanging = true;
+		node.eachChild(function(n){
+			var layer = n.layer;
+			if (n.attributes.checkedGroup != undefined) {
+				if(n instanceof GeoExt.tree.ComplexLayerNode || n instanceof GeoExt.tree.LayerNode){
+					n.ui.enforceOneVisible();
+				}
+			}
+			else if (checked && layer && layer.isBaseLayer && layer.map) {
+				layer.map.setBaseLayer(layer);
+			}
+			else if (layer) {
+				layer.setVisibility(checked);
+			}
+			else {
+				n.ui.toggleChecked(checked)
+			}
+		});
+		delete this._visibilityChanging;
+	},
+    
+    /** private: method[addStoreEventHandlers]
+     *  Adds handlers that make sure the node disappeares when the layer is
+     *  removed from the store, and appears when it is re-added.
+     */
+    addStoreEventHandlers: function() {
+        this.layerStore.on({
+            "add": this.onStoreAdd,
+            "remove": this.onStoreRemove,
+            //TODO:figure out a good handler for the update event
+		    //"update": this.onStoreUpdate,
+            scope: this
+        });
+    },
+    
+    /** private: method[onStoreAdd]
+     *  :param store: ``Ext.data.Store``
+     *  :param records: ``Array(Ext.data.Record)``
+     *  :param index: ``Number``
+     *
+     *  handler for add events on the store 
+     */
+
+onStoreAdd: function(store, records, index) {
+        for(var i=0; i<records.length; ++i) {
+            var t = records[i].get("title");
+            if(this.text == t && this.rendered) {
+                this.getUI().show();
+                break;
+            } else if (this.text == t) {
+                this.render();
+                break;
+            }
+        }
+    },
+    
+    /** private: method[onStoreRemove]
+     *  :param store: ``Ext.data.Store``
+     *  :param record: ``Ext.data.Record``
+     *  :param index: ``Number``
+     *
+     *  handler for remove events on the store 
+     */
+    onStoreRemove: function(store, record, index) {
+        if(this.text == record.get("title")) {
+            this.getUI().hide();
+        }
+    },
+
+    /** private: method[onStoreUpdate]
+     *  :param store: ``Ext.data.Store``
+     *  :param record: ``Ext.data.Record``
+     *  :param operation: ``String``
+     *  
+     *  Listener for the store's update event.
+     */
+    onStoreUpdate: function(store, record, operation) {
+        var t = record.get("title");
+    },
+
+    /** private: method[destroy]
+     */
+    destroy: function() {
+		this.cascade(function(n){
+			if(n.layer){
+				// call the regular layer node destroy function
+				n.destroy();
+			}
+		});
+		//remove the layerStore from this node
+        var layerStore = this.layerStore;
+        if(layerStore) {
+            layerStore.un("add", this.onStoreAdd, this);
+            layerStore.un("remove", this.onStoreRemove, this);
+            //TODO: determine what/how to handle update event
+			//layerStore.un("update", this.onStoreUpdate, this);
+        }
+        delete this.layerStore;
+        this.un("checkchange", this.onCheckChange, this);
+        GeoExt.tree.ComplexLayerNode.superclass.destroy.apply(this, arguments);
+    }
+});
+
+/**
+ * NodeType: gx_complexlayer
+ */
+Ext.tree.TreePanel.nodeTypes.gx_complexlayer = GeoExt.tree.ComplexLayerNode;
