text: define(["dojo/_base/declare", "dojo/_base/sniff", "dojo/_base/lang", "dojo/aspect", "dojo/dom", "dojo/dom-attr", "dojo/dom-class", "dojo/dom-geometry", "dojo/dom-style", "dojo/dom-construct", "dojo/on", "dojo/_base/window", "dojo/mouse", "dijit/_WidgetBase", "dijit/_TemplatedMixin", "dijit/_Container", "./FisheyeListItem"],
  	function(declare, has, lang, aspect, dom, attr, domClass, geometry, style, construct, on, winUtil, mouse, _WidgetBase, _TemplatedMixin, _Container, FisheyeListItem){
  
  		return declare("dojox.widget.FisheyeList",
  			[_WidgetBase, _TemplatedMixin, _Container], {
  				// summary:
  				//		Menu similar to the fish eye menu on the Mac OS
  				// example:
  				// |	<div dojoType="dojo.widget.FisheyeList"
  				// |		itemWidth="40" itemHeight="40"
  				// |		itemMaxWidth="150" itemMaxHeight="150"
  				// |		orientation="horizontal"
  				// |		effectUnits="2"
  				// |		itemPadding="10"
  				// |		attachEdge="center"
  				// |		labelEdge="bottom">
  				// |
  				// |		<div dojoType="dojox.widget.FisheyeListItem"
  				// |			id="item1"
  				// |			onclick="alert('click on' + this.label + '(from widget id ' + this.widgetId + ')!');"
  				// |			label="Item 1"
  				// |			iconSrc="images/fisheye_1.png">
  				// |		</div>
  				// |		...
  				// |	</div>
  
  				constructor: function(){
  					//
  					// TODO
  					// fix really long labels in vertical mode
  					//
  
  					this.pos = {'x': -1, 'y': -1};	// current cursor position, relative to the grid
  
  					// for conservative trigger mode, when triggered, timerScale is gradually increased from 0 to 1
  					this.timerScale = 1.0;
  				},
  
  				EDGE: {
  					CENTER: 0,
  					LEFT: 1,
  					RIGHT: 2,
  					TOP: 3,
  					BOTTOM: 4
  				},
  
  				templateString: '<div class="dojoxFisheyeListBar" data-dojo-attach-point="containerNode"></div>',
  
  				snarfChildDomOutput: true,
  
  				// itemWidth: Integer
  				//		width of menu item (in pixels) in it's dormant state (when the mouse is far away)
  				itemWidth: 40,
  
  				// itemHeight: Integer
  				//		height of menu item (in pixels) in it's dormant state (when the mouse is far away)
  				itemHeight: 40,
  
  				// itemMaxWidth: Integer
  				//		width of menu item (in pixels) in it's fully enlarged state (when the mouse is directly over it)
  				itemMaxWidth: 150,
  
  				// itemMaxHeight: Integer
  				//		height of menu item (in pixels) in it's fully enlarged state (when the mouse is directly over it)
  				itemMaxHeight: 150,
  
  				imgNode: null,
  
  				// orientation: String
  				//		orientation of the menu, either "horizontal" or "vertical"
  				orientation: 'horizontal',
  
  				// isFixed: Boolean
  				//		toggle to enable additional listener (window scroll) if FisheyeList is in a fixed postion
  				isFixed: false,
  
  				// conservativeTrigger: Boolean
  				//		if true, don't start enlarging menu items until mouse is over an image;
  				//		if false, start enlarging menu items as the mouse moves near them.
  				conservativeTrigger: false,
  
  				// effectUnits: Number
  				//		controls how much reaction the menu makes, relative to the distance of the mouse from the menu
  				effectUnits: 2,
  
  				// itemPadding: Integer
  				//		padding (in pixels) between each menu item
  				itemPadding: 10,
  
  				// attachEdge: String
  				//		Controls the border that the menu items don't expand past;
  				//		for example, if set to "top", then the menu items will drop downwards as they expand.
  				//		Values: "center", "left", "right", "top", "bottom".
  				attachEdge: 'center',
  
  				// labelEdge: String
  				//		Controls were the labels show up in relation to the menu item icons.
  				//		Values: "center", "left", "right", "top", "bottom".
  				labelEdge: 'bottom',
  
  				postCreate: function(){
  					var e = this.EDGE,
  						isHorizontal = this.isHorizontal = (this.orientation == 'horizontal');
  
  					dom.setSelectable(this.domNode, false);
  
  					this.selectedNode = -1;
  
  					this.isOver = false;
  					this.hitX1 = -1;
  					this.hitY1 = -1;
  					this.hitX2 = -1;
  					this.hitY2 = -1;
  
  					//
  					// only some edges make sense...
  					//
  					this.anchorEdge = this._toEdge(this.attachEdge, e.CENTER);
  					this.labelEdge  = this._toEdge(this.labelEdge,  e.TOP);
  
  					if(this.labelEdge == e.CENTER){ this.labelEdge = e.TOP; }
  
  					if(isHorizontal){
  						if(this.anchorEdge == e.LEFT){ this.anchorEdge = e.CENTER; }
  						if(this.anchorEdge == e.RIGHT){ this.anchorEdge = e.CENTER; }
  						if(this.labelEdge == e.LEFT){ this.labelEdge = e.TOP; }
  						if(this.labelEdge == e.RIGHT){ this.labelEdge = e.TOP; }
  					}else{
  						if(this.anchorEdge == e.TOP){ this.anchorEdge = e.CENTER; }
  						if(this.anchorEdge == e.BOTTOM){ this.anchorEdge = e.CENTER; }
  						if(this.labelEdge == e.TOP){ this.labelEdge = e.LEFT; }
  						if(this.labelEdge == e.BOTTOM){ this.labelEdge = e.LEFT; }
  					}
  
  					//
  					// figure out the proximity size
  					//
  					var effectUnits = this.effectUnits;
  					this.proximityLeft   = this.itemWidth  * (effectUnits - 0.5);
  					this.proximityRight  = this.itemWidth  * (effectUnits - 0.5);
  					this.proximityTop	= this.itemHeight * (effectUnits - 0.5);
  					this.proximityBottom = this.itemHeight * (effectUnits - 0.5);
  
  					if(this.anchorEdge == e.LEFT){
  						this.proximityLeft = 0;
  					}
  					if(this.anchorEdge == e.RIGHT){
  						this.proximityRight = 0;
  					}
  					if(this.anchorEdge == e.TOP){
  						this.proximityTop = 0;
  					}
  					if(this.anchorEdge == e.BOTTOM){
  						this.proximityBottom = 0;
  					}
  					if(this.anchorEdge == e.CENTER){
  						this.proximityLeft   /= 2;
  						this.proximityRight  /= 2;
  						this.proximityTop	/= 2;
  						this.proximityBottom /= 2;
  					}
  				},
  
  				startup: function(){
  					// summary:
  					//		create our connections and setup our FisheyeList
  					this.children = this.getChildren();
  					//original postCreate() --tk
  					this._initializePositioning();
  
  					//
  					// in liberal trigger mode, activate menu whenever mouse is close, in conservative mode, pause until needed
  					//
  					this._onMouseMoveHandle = on.pausable(winUtil.doc.documentElement, "mousemove", lang.hitch(this, "_onMouseMove"));
  					if(this.conservativeTrigger){
  						this._onMouseMoveHandle.pause();
  					}
  					if(this.isFixed){
  						this.own(on(winUtil.doc,"scroll", lang.hitch(this, this._onScroll)));
  					}
  
  					// Deactivate the menu if mouse is moved off screen (doesn't work for FF?)
  					this.own(
  						on(winUtil.doc.documentElement, mouse.leave, lang.hitch(this, "_onBodyOut")),
  						aspect.after(this, "addChild", lang.hitch(this, "_initializePositioning"), true),
  						aspect.after(winUtil.global, "onresize", lang.hitch(this, "_initializePositioning"), true)
  					);
  				},
  
  				_initializePositioning: function(){
  					this.itemCount = this.children.length;
  
  					this.barWidth  = (this.isHorizontal ? this.itemCount : 1) * this.itemWidth;
  					this.barHeight = (this.isHorizontal ? 1 : this.itemCount) * this.itemHeight;
  
  					this.totalWidth  = this.proximityLeft + this.proximityRight  + this.barWidth;
  					this.totalHeight = this.proximityTop  + this.proximityBottom + this.barHeight;
  
  					//
  					// calculate effect ranges for each item
  					//
  
  					for(var i=0; i<this.children.length; i++){
  
  						this.children[i].posX = this.itemWidth  * (this.isHorizontal ? i : 0);
  						this.children[i].posY = this.itemHeight * (this.isHorizontal ? 0 : i);
  
  						this.children[i].cenX = this.children[i].posX + (this.itemWidth  / 2);
  						this.children[i].cenY = this.children[i].posY + (this.itemHeight / 2);
  
  						var isz = this.isHorizontal ? this.itemWidth : this.itemHeight,
  							r = this.effectUnits * isz,
  							c = this.isHorizontal ? this.children[i].cenX : this.children[i].cenY,
  							lhs = this.isHorizontal ? this.proximityLeft : this.proximityTop,
  							rhs = this.isHorizontal ? this.proximityRight : this.proximityBottom,
  							siz = this.isHorizontal ? this.barWidth : this.barHeight,
  
  							range_lhs = r,
  							range_rhs = r;
  
  						if(range_lhs > c+lhs){ range_lhs = c+lhs; }
  						if(range_rhs > (siz-c+rhs)){ range_rhs = siz-c+rhs; }
  
  						this.children[i].effectRangeLeft = range_lhs / isz;
  						this.children[i].effectRangeRght = range_rhs / isz;
  
  						//dojo.debug('effect range for '+i+' is '+range_lhs+'/'+range_rhs);
  					}
  
  					//
  					// create the bar
  					//
  					style.set(this.domNode, {width: this.barWidth + 'px', height: this.barHeight + 'px'});
  
  					//
  					// position the items
  					//
  					for(i=0; i<this.children.length; i++){
  						var itm = this.children[i];
  						var elm = itm.domNode;
  						style.set(elm, {left: itm.posX + 'px', top: itm.posY + 'px', width: this.itemWidth + 'px', height: this.itemHeight + 'px'});
  
  						style.set(itm.imgNode, {left: this.itemPadding+'%', top: this.itemPadding+'%', width: (100 - 2 * this.itemPadding) + '%', height: (100 - 2 * this.itemPadding) + '%'});
  					}
  					//
  					// calc the grid
  					//
  					this._calcHitGrid();
  				},
  
  				_overElement: function(/* DomNode|String */ node, /* Event */ e){
  					// summary:
  					//		Returns whether the mouse is over the passed element.
  					// node:
  					//		Must must be display:block (ie, not a `<span>`)
  					node = dom.byId(node);
  					var mouse = {x: e.pageX, y: e.pageY},
  						absolute = geometry.position(node, true),
  						top = absolute.y,
  						bottom = top + absolute.h,
  						left = absolute.x,
  						right = left + absolute.w;
  
  					return (mouse.x >= left
  						&& mouse.x <= right
  						&& mouse.y >= top
  						&& mouse.y <= bottom
  						);	// Boolean
  				},
  
  				_onBodyOut: function(/*Event*/ e){
  					// clicking over an object inside of body causes this event to fire; ignore that case
  					if( this._overElement(winUtil.body(), e) ){
  						return;
  					}
  					this._setDormant(e);
  				},
  
  				_setDormant: function(/*Event*/ e){
  					// summary:
  					//		called when mouse moves out of menu's range
  
  					if(!this.isOver){ return; }	// already dormant?
  					this.isOver = false;
  
  					if(this.conservativeTrigger){
  						// user can't re-trigger the menu expansion
  						// until he mouses over a icon again
  						this._onMouseMoveHandle.pause();
  					}
  					this._onGridMouseMove(-1, -1);
  				},
  
  				_setActive: function(/*Event*/ e){
  					// summary:
  					//		called when mouse is moved into menu's range
  
  					if(this.isOver){ return; }	// already activated?
  					this.isOver = true;
  
  					if(this.conservativeTrigger){
  						// switch event handlers so that we handle mouse events from anywhere near
  						// the menu
  						this._onMouseMoveHandle.resume();
  
  						this.timerScale=0.0;
  
  						// call mouse handler to do some initial necessary calculations/positioning
  						this._onMouseMove(e);
  
  						// slowly expand the icon size so it isn't jumpy
  						this._expandSlowly();
  					}
  				},
  
  				_onMouseMove: function(/*Event*/ e){
  					// summary:
  					//		called when mouse is moved
  					if(	(e.pageX >= this.hitX1) && (e.pageX <= this.hitX2) &&
  						(e.pageY >= this.hitY1) && (e.pageY <= this.hitY2)	){
  						if(!this.isOver){
  							this._setActive(e);
  						}
  						this._onGridMouseMove(e.pageX-this.hitX1, e.pageY-this.hitY1);
  					}else{
  						if(this.isOver){
  							this._setDormant(e);
  						}
  					}
  				},
  
  				_onScroll: function(){
  					this._calcHitGrid();
  				},
  
  				onResized: function(){
  					this._calcHitGrid();
  				},
  
  				_onGridMouseMove: function(x, y){
  					// summary:
  					//		called when mouse is moved in the vicinity of the menu
  					this.pos = {x:x, y:y};
  					this._paint();
  				},
  
  				_paint: function(){
  					var x=this.pos.x;
  					var y=this.pos.y;
  
  					if(this.itemCount <= 0){ return; }
  
  					//
  					// figure out our main index
  					//
  					var pos = this.isHorizontal ? x : y,
  						prx = this.isHorizontal ? this.proximityLeft : this.proximityTop,
  						siz = this.isHorizontal ? this.itemWidth : this.itemHeight,
  						sim = this.isHorizontal ?
  							(1.0-this.timerScale)*this.itemWidth + this.timerScale*this.itemMaxWidth :
  							(1.0-this.timerScale)*this.itemHeight + this.timerScale*this.itemMaxHeight,
  
  						cen = ((pos - prx) / siz) - 0.5,
  						max_off_cen = (sim / siz) - 0.5;
  
  					if(max_off_cen > this.effectUnits){ max_off_cen = this.effectUnits; }
  
  					//
  					// figure out our off-axis weighting
  					//
  					var off_weight = 0, cen2;
  
  					if(this.anchorEdge == this.EDGE.BOTTOM){
  						cen2 = (y - this.proximityTop) / this.itemHeight;
  						off_weight = (cen2 > 0.5) ? 1 : y / (this.proximityTop + (this.itemHeight / 2));
  					}
  					if(this.anchorEdge == this.EDGE.TOP){
  						cen2 = (y - this.proximityTop) / this.itemHeight;
  						off_weight = (cen2 < 0.5) ? 1 : (this.totalHeight - y) / (this.proximityBottom + (this.itemHeight / 2));
  					}
  					if(this.anchorEdge == this.EDGE.RIGHT){
  						cen2 = (x - this.proximityLeft) / this.itemWidth;
  						off_weight = (cen2 > 0.5) ? 1 : x / (this.proximityLeft + (this.itemWidth / 2));
  					}
  					if(this.anchorEdge == this.EDGE.LEFT){
  						cen2 = (x - this.proximityLeft) / this.itemWidth;
  						off_weight = (cen2 < 0.5) ? 1 : (this.totalWidth - x) / (this.proximityRight + (this.itemWidth / 2));
  					}
  					if(this.anchorEdge == this.EDGE.CENTER){
  						if(this.isHorizontal){
  							off_weight = y / (this.totalHeight);
  						}else{
  							off_weight = x / (this.totalWidth);
  						}
  
  						if(off_weight > 0.5){
  							off_weight = 1 - off_weight;
  						}
  
  						off_weight *= 2;
  					}
  
  					//
  					// set the sizes
  					//
  					for(var i=0; i<this.itemCount; i++){
  						var weight = this._weighAt(cen, i);
  						if(weight < 0){weight = 0;}
  						this._setItemSize(i, weight * off_weight);
  					}
  
  					//
  					// set the positions
  					//
  
  					var main_p = Math.round(cen),
  						offset = 0;
  
  					if(cen < 0){
  
  						main_p = 0;
  
  					}else if(cen > this.itemCount - 1){
  
  						main_p = this.itemCount -1;
  
  					}else{
  
  						offset = (cen - main_p) * ((this.isHorizontal ? this.itemWidth : this.itemHeight) - this.children[main_p].sizeMain);
  					}
  
  					this._positionElementsFrom(main_p, offset);
  				},
  
  				_weighAt: function(/*Integer*/ cen, /*Integer*/ i){
  					var dist = Math.abs(cen - i),
  						limit = ((cen - i) > 0) ? this.children[i].effectRangeRght : this.children[i].effectRangeLeft;
  
  					return (dist > limit) ? 0 : (1 - dist / limit); // Integer
  				},
  
  				_setItemSize: function(p, scale){
  					if(this.children[p].scale == scale){ return; }
  					this.children[p].scale = scale;
  
  					scale *= this.timerScale;
  					var w = Math.round(this.itemWidth  + ((this.itemMaxWidth  - this.itemWidth ) * scale)),
  						h = Math.round(this.itemHeight + ((this.itemMaxHeight - this.itemHeight) * scale));
  
  					if(this.isHorizontal){
  
  						this.children[p].sizeW = w;
  						this.children[p].sizeH = h;
  
  						this.children[p].sizeMain = w;
  						this.children[p].sizeOff  = h;
  
  						var y = 0;
  						if(this.anchorEdge == this.EDGE.TOP){
  							y = (this.children[p].cenY - (this.itemHeight / 2));
  						}else if(this.anchorEdge == this.EDGE.BOTTOM){
  							y = (this.children[p].cenY - (h - (this.itemHeight / 2)));
  						}else{
  							y = (this.children[p].cenY - (h / 2));
  						}
  
  						this.children[p].usualX = Math.round(this.children[p].cenX - (w / 2));
  						style.set(this.children[p].domNode, {top: y + 'px', left: this.children[p].usualX + 'px'});
  
  					}else{
  
  						this.children[p].sizeW = w;
  						this.children[p].sizeH = h;
  
  						this.children[p].sizeOff  = w;
  						this.children[p].sizeMain = h;
  
  						var x = 0;
  						if(this.anchorEdge == this.EDGE.LEFT){
  							x = this.children[p].cenX - (this.itemWidth / 2);
  						}else if(this.anchorEdge == this.EDGE.RIGHT){
  							x = this.children[p].cenX - (w - (this.itemWidth / 2));
  						}else{
  							x = this.children[p].cenX - (w / 2);
  						}
  
  						this.children[p].usualY = Math.round(this.children[p].cenY - (h / 2));
  						style.set(this.children[p].domNode, {left: x + 'px', top: this.children[p].usualY + 'px'});
  					}
  
  					style.set(this.children[p].domNode, {width: w + 'px', height: h + 'px'});
  
  					if(this.children[p].svgNode){
  						this.children[p].svgNode.setSize(w, h);
  					}
  				},
  
  				_positionElementsFrom: function(p, offset){
  					var pos = 0;
  
  					var usual, start;
  					if(this.isHorizontal){
  						usual = "usualX";
  						start = "left";
  					}else{
  						usual = "usualY";
  						start = "top";
  					}
  					pos = Math.round(this.children[p][usual] + offset);
  					if(style.get(this.children[p].domNode, start) != (pos + 'px')){
  						style.set(this.children[p].domNode, start, pos + 'px');
  						this._positionLabel(this.children[p]);
  					}
  
  					// position before
  					var bpos = pos;
  					for(var i=p-1; i>=0; i--){
  						bpos -= this.children[i].sizeMain;
  
  						if(style.get(this.children[p].domNode, start) != (bpos + 'px')){
  							style.set(this.children[i].domNode, start, bpos + 'px');
  							this._positionLabel(this.children[i]);
  						}
  					}
  
  					// position after
  					var apos = pos;
  					for(i=p+1; i<this.itemCount; i++){
  						apos += this.children[i-1].sizeMain;
  						if(style.get(this.children[p].domNode, start) != (apos + 'px')){
  							style.set(this.children[i].domNode, start, apos + 'px');
  							this._positionLabel(this.children[i]);
  						}
  					}
  
  				},
  
  				_positionLabel: function(itm){
  					var x = 0;
  					var y = 0;
  
  					var mb = geometry.getMarginBox(itm.lblNode);
  
  					if(this.labelEdge == this.EDGE.TOP){
  						x = Math.round((itm.sizeW / 2) - (mb.w / 2));
  						y = -mb.h;
  					}
  
  					if(this.labelEdge == this.EDGE.BOTTOM){
  						x = Math.round((itm.sizeW / 2) - (mb.w / 2));
  						y = itm.sizeH;
  					}
  
  					if(this.labelEdge == this.EDGE.LEFT){
  						x = -mb.w;
  						y = Math.round((itm.sizeH / 2) - (mb.h / 2));
  					}
  
  					if(this.labelEdge == this.EDGE.RIGHT){
  						x = itm.sizeW;
  						y = Math.round((itm.sizeH / 2) - (mb.h / 2));
  					}
  
  					style.set(itm.lblNode, {left: x + 'px', top: y + 'px'});
  				},
  
  				_calcHitGrid: function(){
  
  					var pos = geometry.position(this.domNode, true);
  
  					this.hitX1 = pos.x - this.proximityLeft;
  					this.hitY1 = pos.y - this.proximityTop;
  					this.hitX2 = this.hitX1 + this.totalWidth;
  					this.hitY2 = this.hitY1 + this.totalHeight;
  
  				},
  
  				_toEdge: function(inp, def){
  					return this.EDGE[inp.toUpperCase()] || def;
  				},
  
  				_expandSlowly: function(){
  					// summary:
  					//		slowly expand the image to user specified max size
  					if(!this.isOver){ return; }
  					this.timerScale += 0.2;
  					this._paint();
  					if(this.timerScale<1.0){
  						setTimeout(lang.hitch(this, "_expandSlowly"), 10);
  					}
  				}
  			});
  
  	});
  