/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace('GHOTI');

Ext.isBlank = function ( value ) { return value === null || value === undefined; };

/* Test if an Object is empty */
GHOTI.hasKeys = function (obj) {
	for( var k in obj ) {
		if( obj.hasOwnProperty(k) ) { return true; }
	}
	return false;
};

/*
** Behaves exactly as GHOTI.path.join, but looks up the first parameter
** in GHOTI.url.paths
*/
GHOTI.url = function( s ) {
	var args = Array.prototype.slice.call( arguments, 1 );
	args.unshift( GHOTI.url.paths[s] );
	return GHOTI.path.join.apply( this, args );
};
/*
** A map of nemonic names to paths
*/
GHOTI.url.paths = {
	'icon': '/static/icons/'
};

String.prototype.slugify = function() {
	return this.replace( /[^\w\s\-]/g, '' ).replace( /^\s+|\s+$/g, '' ).toLowerCase().replace( /[\-\s]+/g, '-' );
};

GHOTI.browser = {
	open_window: function( url, window_name ) {
		var win
			,w=800
			,h=600
			,options='status=no,menubar=no,scrollbars=yes,resizable=yes,toolbar=no';
		if (window.screen) {
			w = window.screen.availWidth;
			h = window.screen.availHeight;
		}

		options += ',width='+w+',height='+h+',screenX=0,screenY=0,left=0,top=0';
		//open it maximized
		win = window.open( url ,window_name ,options);
		if( win ) {
			win.moveTo(0,0);
			win.resizeTo( screen.availWidth, screen.availHeight );
			win.focus();
		} else {
			Ext.Msg.alert("Popup was not allowed to open"
				,"The print window was not allowed to open. Check if it was blocked and allow it from the bar at the top of the page."
			);
		}
	}
};


/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace('GHOTI');

GHOTI.path = {
	/*
	** Function to behave like Python's os.path.join
	*/
	join: function ( ) {
		var b ,i
			,len = arguments.length
			,a = arguments[0];
		for( i=1; i<len; i++ ) {
			b = arguments[i];
			if( b === undefined ) { continue; }
			b = b.toString();
			if( b.charAt(0) == '/' ) {
				a = b;
			} else if( a === '' || a.charAt( a.length-1 ) == '/' ) {
				a += b;
			} else {
				a += "/" + b;
			}
		}
		return a;
	}
};


/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace('GHOTI');

GHOTI.loader = function () {
	var next_in_queue
		,queue = []			// Scripts pending loading
		,loading = false	// Are we currently in a loading queue?
		,script_map = {'': 1}	// Catch any empty requests
		,head = document.getElementsByTagName("head")[0];

	function add_tag( url, id, opt ) {
		var tag = document.createElement('script');

		tag.type = 'text/javascript';
		tag.src = url;
		tag.id = id || Ext.id();

		loading = true;

		head.appendChild(tag);

		Ext.EventManager.on( tag, 'load', next_in_queue ,this );
		if( opt.notify ) {
			Ext.EventManager.on( tag, 'load', opt.notify ,opt.scope );
		}
	}

	next_in_queue = function ( ) {
		loading = false;
		if( queue.length ) {
			add_tag.apply( this, queue.pop() );
		}
	};

	function id_from_url( url ) {
		return String(url).replace('.', '/');
	}

	return {
		require: function () {
			var i, l, item, url, id, options;

			for( i=0, l=arguments.length; i < l; i++ ) {
				item = arguments[i];

				if( Ext.type(item) === 'string' ) {
					if( !item.match(/\.js$/i) ) {
						// Assume they've passed Foo.bar.baz and expect Foo/bar/baz.js
						url = GHOTI.url('scripts', item.replace('.', '/') + ".js" );
					} else {
						url = item;
					}
					id = item;
					options = {};
				} else if( Ext.type(item) == 'array' ) {
					url = item[0];
					id = item[1] || id_from_url(url);
					options = item[2] || {};
				} else {
					console.error( "Not sure what to do with: ", item );
				}
				// Have we already loded it?
				if( script_map[id] ) {
					if( options.notify ) {
						options.notify.apply( options.scope, [] );
					}
					continue;
				}

				queue.push( [ url, id, options || {} ] );
			}
			// Once we're done, ensure it's processing
			if( !this.loading ) {
				next_in_queue();
			}
		}
	};
}();

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace('GHOTI.utils');

GHOTI.utils.Format = {
	/*
	** Returns a function that will left-pad a value to the given size.
	** Default padchar is '0'
	*/
	leftPadRenderer: function ( size, padchar ) {
		return function ( value ) {
			return String.leftPad(value, size, padchar || '0');
		};
	}
	/*
	*/
	,renderBool: function ( value, meta ) {
		meta.css = ( value ) ? 'x-grid3-bool-true' : 'x-grid3-bool-false';
		return '';
	}
	/*
	** Render the display portion of a GHOTI serialised foreign key
	*/
	,renderFKey: function ( value ) {
		return value.display;
	}
};

/*global Ext GHOTI EOS */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace('GHOTI.layout');

/**
 * @class Ext.layout.CBoxLayout
 * @extends Ext.layout.BoxLayout
 * A layout that arranges items horizontally in equal widths
 */
GHOTI.layout.CBoxLayout = Ext.extend(Ext.layout.BoxLayout, {
    // default inter-column gap
    gap: 5
    // private
    ,onLayout : function(ct, target){
        GHOTI.layout.CBoxLayout.superclass.onLayout.call(this, ct, target);

        var c ,i ,cm ,cw ,innerCtHeight
            ,cs = this.getItems(ct)
            ,len = cs.length
            ,restore = []
            ,maxHeight = 0
            ,size = this.getTargetSize(target)
            ,w = size.width - target.getPadding('lr') - this.scrollOffset
            ,h = size.height - target.getPadding('tb')
            ,l = this.padding.left
            ,t = this.padding.top;

        // Determine max height
        for( i =0; i < len ; i++ ) {
            c = cs[i];
            cm = c.margins; // These are set by BoxLayout
            maxHeight = Math.max( maxHeight, c.preferredHeight || (c.getHeight() + cm.top + cm.bottom) );
        }

        // Each child is total width, less width of gaps (children+1), divided by children
        cw = Math.floor( (w - ((len+1)*this.gap) ) / len );
        for(i = 0; i < len; i++){
            c = cs[i];
            cm = c.margins;

            c.setPosition(l, t + cm.top);
            c.setSize( (cw-(cm.left+cm.right)) , maxHeight );
            l += (cw + this.gap);
        }

        innerCtHeight = maxHeight + this.padding.top + this.padding.bottom;
        this.innerCt.setSize(w, innerCtHeight);
    }
});

Ext.Container.LAYOUTS.cbox = GHOTI.layout.CBoxLayout;

/*global Ext GHOTI EOS */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace('GHOTI.layout');

/**
 * @class Ext.layout.FieldsetLayout
 * @extends Ext.layout.FormLayout
 * A layout which calculates the height of contained fields
 */

GHOTI.layout.FieldsetLayout = Ext.extend( Ext.layout.FormLayout, {
    onLayout: function (ct, target) {
        GHOTI.layout.FieldsetLayout.superclass.onLayout.apply( this, arguments );

        ct.deferHeight = false;

        var innerHeight = target.getPadding('tb');

        ct.items.each(function (c) {
            var oe, om ,oh;
            if( c.isFormField || c.fieldLabel ) {
                // Fields in a form layout are wrapped in two layers of div
                oe = Ext.get(c.container.dom.parentNode);
            } else {
                oe = c.el;
            }
            om = oe.getMargins('tb');
            oh = oe.getHeight();
            innerHeight += ( oh + om );
        });

        /* Set the _body_ to the right height */
        target.setHeight( innerHeight );

        ct.preferredHeight = innerHeight
            + target.getMargins('tb')
            + ct.el.getPadding('tb')
            + ct.el.getBorderWidth('tb')
            + ct.el.getMargins('tb')
        ;
    }
});
Ext.Container.LAYOUTS.fieldset = GHOTI.layout.FieldsetLayout;

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.form");

GHOTI.form.ErrorHilight = function ( form, action ) {
	if(
		action.failureType !== Ext.form.Action.SERVER_INVALID &&
		action.faulureType !== Ext.form.Action.CLIENT_INVALID
	) { return; }

	var errors = action.result.errors
		,field
		,f;
	for ( f in action.result.errors ) {
		if( errors.hasOwnProperty(f) ) {
			field = form.findField(f);
			Ext.fly(field.el.dom.parentNode).highlight("ff8080").highlight("ff8080");
		}
	}
};


/*global Ext GHOTI window */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.form");

/* Overrides setValues to work backward: pick fields from record that are in the form */
GHOTI.form.BasicForm = Ext.extend( Ext.form.BasicForm, {
	setValues: function (values) {
		var bf = this;
		this.items.each( function(f) {
			var val, hval;
			if( f.isFormField ) {
				if( f.isXType('combo') ) {
					val = bf.lookupValue( values, f.name );
					if( Ext.isBlank(val) && f.hiddenName ) {
						val = bf.lookupValue( values, f.hiddenName );
					}
					if( !Ext.isBlank(val) ) {
						if( typeof val === 'object' ) {
							if( val.value === null ) {
								val = { value: '', display: '' };
							}
							if( f.rendered ) {
								f.setValue( val.display );
								f.hiddenField.value = val.value;
							} else {
								f.value = val.display;
								f.hiddenValue = val.value;
							}
						} else {
							hval = f.hiddenName ?
								bf.lookupValue( values, f.hiddenName )
								: null;
							if( !Ext.isBlank(hval) ) {
								if( f.rendered ) {
									f.setValue(val);
									f.hiddenField.value = hval;
								} else {
									f.value = val;
									f.hiddenValue = hval;
								}
							} else {
								f.setValue( val );
							}
						}
					}

				} else if( f.isXType('checkbox') || f.isXType('radio') ) {
					val = bf.lookupValue( values, f.name );
					if( !Ext.isBlank(val) ) {
						if( typeof val === 'object' ) {
							val = val.value;
						}
						if( f.rendered ) {
							f.setValue( val == f.inputValue );
						} else {
							f.checked = ( val == f.inputValue );
						}
					}
				} else if( f.isXType('radiogroup') ) {
					val = bf.lookupValue( values, f.name );
					if( !Ext.isBlank(val) ) {
						if( typeof val === 'object' ) {
							val = val.value;
						}
						if( f.rendered ) {
							f.setValue(val.toString());
						} else {
							f.value = val.toString();
						}
					}
				} else if( f.isXType('checkboxgroup') ) {
					val = bf.lookupValue( values, f.name );
					if( !Ext.isBlank(val) ) {
						Ext.each( val, function (v,i) { val[i] = v.id.toString(); } );
						f.items.each( function (subField) {
							if( subField.rendered ) {
								subField.setValue( val.indexOf(subField.inputValue) != -1 );
							} else {
								subField.checked = ( val.indexOf(subField.inputValue) != -1 );
							}
						});
					}
				} else if( f.isXType('multiselect') ) {
					val = bf.lookupValue( values, f.name );
					if( !Ext.isBlank(val) ) {
						val = Ext.pluck( val, 'value' );
						f.setValue(val);
					}
				} else {
					// it's a security violation to set value on a File input
					if( f.inputType != 'file' ) {
						val = bf.lookupValue( values, f.name );
						if( !Ext.isBlank(val) ) { f.setValue(val); }
					}
				}
			}
		});
		return this;
	}
	,lookupValue: function ( obj, field ) {
		if( !field ) { return undefined; }
		var i ,parts = field.split('.')
			,len = parts.length;
		for( i=0; i<len; i++ ){
			if( !obj) { return false; }
			obj = obj[parts[i]];
		}
		return obj;
	}
});
Ext.reg("ghoti-form-basicform" ,GHOTI.form.BasicForm );

GHOTI.form.FormPanel = Ext.extend( Ext.form.FormPanel, {
	createForm: function(){
		var config = Ext.applyIf({listeners: {}}, this.initialConfig);
		return new GHOTI.form.BasicForm(null, config);
	}
});
Ext.reg("ghoti-form-formpanel" ,GHOTI.form.FormPanel );

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.form");

/* Download a file in the background
	'o' contains:
	url: where to send it
	method: how to send it (default: POST)
	id: ID used last time -- will be updated or supplied if missing.
		Used to keep track of iframe and form created.  Since there's no
		"completion" event, we don't know when to clean up.  Instead, we ensure
		no more than one pair exist.
	baseParams: what to send
*/
GHOTI.form.HiddenDownload = function ( o ) {
	var i, j, l, f, g, t, frame, id, form;

	id = o.id;
	// Clean up from last time
	if( id ) {
		Ext.removeNode( document.getElementById(id) );
		Ext.removeNode( document.getElementById(id+"-form") );
	} else {
		id = Ext.id();
		o.id = id;
	}

	// Build iframe to target
	frame = document.createElement('iframe');
	frame.id = id;
	frame.name = id;
	frame.className = 'x-hidden';
	if( Ext.isIE ) {
		frame.src= Ext.SSL_SECURE_URL;
	}
	document.body.appendChild(frame);

	if( Ext.isIE ) {
		document.frames[id].name = id;
	}

	// Build form to submit
	form = document.createElement('form');
	form.id = id + '-form';
	form.className = 'x-hidden';
	form.target = id;
	form.method = o.method || 'POST';
	form.action = o.url;

	for( i in o.baseParams || {} ) {
		if( o.baseParams.hasOwnProperty(i) ) {
			f = document.createElement('input');
			f.type = 'hidden';
			f.name = i;

			t = Ext.type(o.baseParams[i]);
			if( t == 'array' ) {
				for ( j=0, l=o.baseParams[i].length ; j < l ; j++ ) {
					g = f.cloneNode(false);
					g.value = o.baseParams[i][j];
					form.appendChild(g);
				}
			} else {
				f.value = o.baseParams[i].toString();
				form.appendChild(f);
			}
		}
	}

	document.body.appendChild(form);
	form.submit();
	return o;
};

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.form");

GHOTI.form.FieldTooltipPlugin = function () {
	return {
		init: function (cmp) {
			cmp.on('render', this.addTooltip, cmp, { single: true } );
		}
		,addTooltip: function ( field ) {
			if( field.tooltip ) {
				var c = field.el.parent().first();
				while( c ) {
					Ext.QuickTips.register({
						target: c.id
						,text: field.tooltip
					});
					c = c.next();
				}
			}
		}
	};
}();

GHOTI.form.FieldHelpPlugin = function () {
	return {
		init: function (cmp) {
			cmp.on('render', this.addHelpText, cmp, { single: true } );
		}
		,addHelpText: function ( field ) {
			if( field.helptext ) {
				if( field.xtype === 'radio' || field.xtype === 'checkbox' ) {
					field.helpTextEl = field.wrap.parent().createChild({
						tag: 'div'
						,html: this.helptext
						,cls: 'x-form-'+this.el.dom.type+'-help'
					});
				} else {
					// XXX This is TOTALLY untested
					field.helpTextEl = field.itemEl.parent().createChild({
						tag: 'div'
						,html: this.helptext
						,cls: this.baseCls+'-help'
					});
				}
			}
		}
	};
}();

/* Plugin to make a ComboBox auto-select the item if there's only one */
GHOTI.form.ComboAutoSelect = function () {
    return {
        init: function (cmp) {
            cmp.on('render', GHOTI.form.ComboAutoSelect.onRender, cmp );
        }
        ,initialLoad: function (store, records) {
            if( records.length == 1 ) {
                this.setValue(records[0].get(this.valueField));
            }
        }
        ,onRender: function (cmp) {
            cmp.store.on('load', GHOTI.form.ComboAutoSelect.initialLoad, cmp, { single: true } );
            cmp.store.load();
        }
    };
}();

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.form");

/*
** Subclass of a Display field which uses a 'renderer' function.
*/
GHOTI.form.RendererField = Ext.extend( Ext.form.DisplayField, {
    setRawValue: function (v) {
        v = this.renderer(v);
        return GHOTI.form.RendererField.superclass.setRawValue.call( this, v );
    }
    ,renderer: function (v) { return v; }
});
Ext.reg('ghoti-form-renderfield', GHOTI.form.RendererField );

GHOTI.form.CheckboxList = Ext.extend( Ext.form.Field, {
    displayField: 1,
    valueField: 0,
    allowBlank: true,
    defaultAutoCreate : {tag: "div"},
    initComponent: function () {
        GHOTI.form.CheckboxList.superclass.initComponent.call(this);
        if(Ext.isArray(this.store)){
            if (Ext.isArray(this.store[0])){
                this.store = new Ext.data.ArrayStore({
                    fields: ['value','text'],
                    data: this.store
                });
                this.valueField = 'value';
            }else{
                this.store = new Ext.data.ArrayStore({
                    fields: ['text'],
                    data: this.store,
                    expandData: true
                });
                this.valueField = 'text';
            }
            this.displayField = 'text';
        } else {
            this.store = Ext.StoreMgr.lookup(this.store);
        }
    },
    onRender: function(ct, position){
        GHOTI.form.CheckboxList.superclass.onRender.apply(this, arguments);
        this.view = new Ext.DataView({
            renderTo: this.el,
            height: this.height,
            width: this.width,
            store: this.store,
            // We need this to stop IE8 from exploding
            style: 'overflow-x: hidden; overflow-y: auto;',
            tpl: new Ext.XTemplate(
                '<tpl for=".">',
                    '<div>',
                        '<label>',
                            '<input type="checkbox" name="'+this.name+'" value="{'+this.valueField+'}">',
                            '{'+this.displayField+'}',
                        '</label>',
                    '</div>',
                '</tpl>'
            ).compile()
        });
    },
    getValue: function( valueField ) {
        var val = [];
        Ext.each( this.el.query('input'), function( inp ) {
            if( inp.checked ) { val.push(inp.value); }
        });
        return val;
    },
    setValue: function(values) {
        if( Ext.isObject(values[0]) ) {
            for( var i=0, l=values.length; i < l ; i++ ) {
                values[i] = values[i].value.toString();
            }
        }
        Ext.each( this.el.query('input'), function( inp ) {
            inp.checked = ( values.indexOf(inp.value) == -1 ) ? false : true;
        }, this);
    }
});
Ext.reg('ghoti-form-checkboxlist', GHOTI.form.CheckboxList );

/*
** A field to show existing file download link, or file Input to change
*/
GHOTI.form.FileField = Ext.extend( Ext.form.Field, {
    initComponent: function () {
        this.autoCreate = { tag: 'div', children: [
            { tag: 'input', type: 'file', name: this.name },
            { tag: 'div', children: [
                { tag: 'a', href: '/download/' +this.value, html: this.value }
            ]}
        ]};
        GHOTI.form.FileField.superclass.initComponent.call(this);
    },
    setValue: function ( value ) {
        this.value = value;
        if( this.rendered ) {
            this.el.child('input').value = value;
            var link = this.el.child('a');
            link.set({ href: '/download/' + value });
            link.update(value);
        }
    }
});
Ext.reg('ghoti-form-filefield', GHOTI.form.FileField );

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.widget");

GHOTI.widget.ColumnPanel = Ext.extend( Ext.Container, {
    layout: 'cbox'
    ,autoEl: 'div'
});
Ext.reg('columns', GHOTI.widget.ColumnPanel );

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace('GHOTI.widget');

GHOTI.widget.TableView = Ext.extend( Ext.DataView, {
	tpl: '<tpl for="rows">{.}</tpl>'
	,tmpl: {
		hdrRow: '<tr class="g-tv-hdr-row"><tpl for="headers">{.}</tpl></tr>'
		,hdrCell: '<th><div>{header}</div></th>'
		,bodyRow: '<tr id="{id}" class="g-tv-row"><tpl for="cells">{.}</tpl></tr>'
		,bodyCell: '<td>{value}</td>'
		,colGroup: '<tpl for="cols"><col class="g-tv-col{#}"></col></tpl><col class="g-tv-scroll"></col>'
	}
	,autoEl : { tag: 'table' ,cls: "g-tv" }
	,itemSelector: 'tr.g-tv-row'

	,initComponent: function () {
		for( var k in this.tmpl ) {
			if( this.tmpl.hasOwnProperty(k) ) {
				this.tmpl[k] = new Ext.XTemplate(this.tmpl[k]).compile();
			}
		}
		GHOTI.widget.TableView.superclass.initComponent.apply( this, arguments );
	}
	,onRender: function () {
		GHOTI.widget.TableView.superclass.onRender.apply( this, arguments );

		this.colGroup = this.el.createChild({ tag: 'colgroup' });
		this.tHead = this.el.createChild({ tag: 'thead' ,cls: 'g-tv-head' });
		this.tBody = this.el.createChild({ tag: 'tbody' ,cls: 'g-tv-body' });

		this.initColumns();
		this.initHeaders();
	}
	,getTemplateTarget: function () {
		return this.tBody;
	}
	,initHeaders: function () {
		var i
			,headers = [];

		for( i=0; i < this.columns.length; i++ ) {
			headers.push( this.tmpl.hdrCell.apply( this.columns[i] ) );
		}
		// Add dummy for scroll space
		headers.push( this.tmpl.hdrCell.apply({}) );

		this.tmpl.hdrRow.overwrite( this.tHead, {
			'headers': headers
		});
	}
	,initColumns: function () {
		this.tmpl.colGroup.overwrite( this.colGroup, {
			cols: this.columns
		});
	}
	,collectData: function ( records, startIdx ) {
		var i ,j ,cells
			,l = this.columns.length
			,t = this.tmpl
			,headers = []
			,rows = [];

		// Generate body rows
		for( i=0; i < records.length ; i ++ ) {
			r = records[i];
			cells = [];
			// render cell for each column
			for ( j=0; j < l ; j++ ) {
				cells.push( t.bodyCell.apply({
					value: r.get(this.columns[j].dataIndex)
				}) );
			}
			rows.push( t.bodyRow.apply({ cells: cells ,id: [ this.id, r.get('id')].join('-') }) );
		}

		return {
			'rows': rows
		};
	}
	,onResize: function ( adjWidth, adjHeight, rawWidth, rawHeight ) {
		GHOTI.widget.TableView.superclass.onResize.apply( this, arguments );

		if( adjHeight ) {
			this.tBody.setHeight( rawHeight - 18 );
		}
	}
});
Ext.reg('tableview', GHOTI.widget.TableView );
/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.widget");

GHOTI.widget.MenuApp = Ext.extend( Ext.Viewport, {
	layout: 'border'
	,defaultMenuInsert: null
	,initComponent: function () {
		GHOTI.widget.MenuApp.superclass.initComponent.apply( this, arguments );
		var config = this.mainpanel;
		if( config ) {
			if( typeof(config) === "string" ) {
				config = { xtype: config };
			}
		} else {
			config = { xtype: 'tabpanel' };
		}
		Ext.applyIf( config, {
			id: 'app-tab-panel'
			,region: 'center'
			,tabPosition: 'bottom'
			,enableTabScroll: true
			,tbar: [ ]
		});
		this.mainPanel = this.add( config );
		this.mainPanel.on('add', this.syncSize, this );
	}
	,registerPanel: function ( menu ) {
		var i, b, bconfig, tb = this.mainPanel.getTopToolbar();
		b = tb.items.find( function ( o ) { return o.text == menu; } );
		if( !b ) {
			bconfig = {
				text: menu
				,menu: {
					defaults: { handler: this.handleMenuClick ,scope: this }
					,items: []
				}
			};
			if( this.defaultMenuInsert !== null ) {
				b = tb.insertButton( this.defaultMenuInsert, bconfig );
			} else {
				b = tb.addButton( bconfig );
			}
			if( this.sortMenus ) {
				tb.items.sort('ASC', function(a, b) {
					if( !b.menu ) {
						if( !a.menu ) {
							return ( a.id < b.id ? -1 : 1 );
						} else {
							// Menu button alway tests as lower than non-menu
							return -1;
						}
                    }
					if( !a.menu ) { return 1; }
					return ( a.text < b.text ? -1 : 1 );
				});
			}
		}
		for( i=1; i < arguments.length ; i++ ) {
			b.menu.add( arguments[i] );
		}
		if( this.sortMenus ) {
			b.menu.items.sort('ASC', function( a, b ) {
				return ( a.text < b.text ? -1 : 1 );
			});
		}
	}
	,handleMenuClick: function ( m, ev ) {
		var tid = [ this.id, m.id ].join('-'),
			t= this.mainPanel.getItem( tid );
		if( !t ) {
			t = this.mainPanel.add({
				title: m.title || m.text
				,id: tid
				,xtype: m.type
				,closable: m.closable || true
			});
		}
		this.mainPanel.setActiveTab(t);
	}
});
Ext.reg('ghoti-widget-menuapp', GHOTI.widget.MenuApp );

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.widget");

/*
** This is the tapPosition sensitive parts of TabPanel
** rewritten to put the tabStrip in tbar
*/
GHOTI.widget.MenuTabPanel = Ext.extend( Ext.TabPanel, {
	initComponent : function(){
		this.frame = false;
		// Deliberate -- we copied all the relevant code from TabPanel
		Ext.TabPanel.superclass.initComponent.call(this);
		this.addEvents(
			'beforetabchange',
			'tabchange',
			'contextmenu'
		);

		this.setLayout(new Ext.layout.CardLayout(Ext.apply({
			layoutOnCardChange: this.layoutOnTabChange,
			deferredRender: this.deferredRender
		}, this.layoutConfig)));

		this.stripTarget = 'tbar';
		this.elements += ',tbar';

		if(!this.stack){
			this.stack = Ext.TabPanel.AccessStack();
		}
		this.initItems();
	}
	,onRender : function(ct, position){
		// Deliberate -- we copied all the relevant code from TabPanel
		Ext.TabPanel.superclass.onRender.call(this, ct, position);

		if(this.plain){
			this.tbar.addClass('x-tab-panel-top-plain');
		}

		var tt, st = this.tbar,
			beforeEl = null;

		this.stripWrap = st.createChild({cls:'x-tab-strip-wrap', cn:{
			tag:'ul', cls:'x-tab-strip x-tab-strip-top' }});

		st.createChild({cls:'x-tab-strip-spacer'}, beforeEl);
		this.strip = new Ext.Element(this.stripWrap.dom.firstChild);

		this.edge = this.strip.createChild({tag:'li', cls:'x-tab-edge', cn: [{tag: 'span', cls: 'x-tab-strip-text', cn: '&#160;'}]});
		this.strip.createChild({cls:'x-clear'});

		this.body.addClass('x-tab-panel-body-top');

		if(!this.itemTpl){
			tt = new Ext.Template(
				'<li class="{cls}" id="{id}"><a class="x-tab-strip-close"></a>',
				'<a class="x-tab-right" href="#"><em class="x-tab-left">',
				'<span class="x-tab-strip-inner"><span class="x-tab-strip-text {iconCls}">{text}</span></span>',
				'</em></a></li>'
			);
			tt.disableFormats = true;
			tt.compile();
			Ext.TabPanel.prototype.itemTpl = tt;
		}

		this.items.each(this.initTab, this);
	}

	,autoSizeTabs : function(){
		var count = this.items.length,
			//ce = this.tabPosition != 'bottom' ? 'header' : 'footer',
			//ow = this.tbar.dom.offsetWidth,
			aw = this.tbar.dom.clientWidth;

		if(!this.resizeTabs || count < 1 || !aw){ // !aw for display:none
			return;
		}

		var each = Math.max(Math.min(Math.floor((aw-4) / count) - this.tabMargin, this.tabWidth), this.minTabWidth); // -4 for float errors in IE
		this.lastTabWidth = each;
		var lis = this.strip.query("li:not(.x-tab-edge)");
		for(var i = 0, len = lis.length; i < len; i++) {
			var li = lis[i],
				inner = Ext.fly(li).child('.x-tab-strip-inner', true),
				tw = li.offsetWidth,
				iw = inner.offsetWidth;
			inner.style.width = (each - (tw-iw)) + 'px';
		}
	}

	,autoScrollTabs : function(){
		this.pos = this.tbar;
		var count = this.items.length,
			//ow = this.pos.dom.offsetWidth,
			tw = this.pos.dom.clientWidth,
			wrap = this.stripWrap,
			wd = wrap.dom,
			cw = wd.offsetWidth,
			pos = this.getScrollPos(),
			l = this.edge.getOffsetsTo(this.stripWrap)[0] + pos;

		if(!this.enableTabScroll || count < 1 || cw < 20){ // 20 to prevent display:none issues
			return;
		}
		if(l <= tw){
			wd.scrollLeft = 0;
			wrap.setWidth(tw);
			if(this.scrolling){
				this.scrolling = false;
				this.pos.removeClass('x-tab-scrolling');
				this.scrollLeft.hide();
				this.scrollRight.hide();
				// See here: http://extjs.com/forum/showthread.php?t=49308&highlight=isSafari
				if(Ext.isAir || Ext.isWebKit){
					wd.style.marginLeft = '';
					wd.style.marginRight = '';
				}
			}
		}else{
			if(!this.scrolling){
				this.pos.addClass('x-tab-scrolling');
				// See here: http://extjs.com/forum/showthread.php?t=49308&highlight=isSafari
				if(Ext.isAir || Ext.isWebKit){
					wd.style.marginLeft = '18px';
					wd.style.marginRight = '18px';
				}
			}
			tw -= wrap.getMargins('lr');
			wrap.setWidth(tw > 20 ? tw : 20);
			if(!this.scrolling){
				if(!this.scrollLeft){
					this.createScrollers();
				}else{
					this.scrollLeft.show();
					this.scrollRight.show();
				}
			}
			this.scrolling = true;
			if(pos > (l-tw)){ // ensure it stays within bounds
				wd.scrollLeft = l-tw;
			}else{ // otherwise, make sure the active tab is still visible
				this.scrollToTab(this.activeTab, false);
			}
			this.updateScrollButtons();
		}
	}

});
Ext.reg('ghoti-widget-menutabpanel', GHOTI.widget.MenuTabPanel );

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.widget");

GHOTI.widget.FilterField = Ext.extend( Ext.form.TwinTriggerField, {
	blankText: '{Search}',
	validationEvent: false,
	validatOnBlur: false,
	trigger1Class: 'x-form-clear-trigger',
	trigger2Class: 'x-form-search-trigger',
	hideTrigger1: true,
	catchKeys: false,
	initComponent: function () {
		this.addEvents('trigger1', 'trigger2');
		if( this.grid ) {
			this.store = this.grid.getStore();
			this.cols = this.grid.getColumnModel().config;
		}
		GHOTI.widget.FilterField.superclass.initComponent.call(this);
		// Catch enter
		if( this.catchKeys ) {
			this.on('specialkey', function (f, e) {
				if( e.getKey() == e.ENTER) {
					this.onTrigger2Click();
				} else if( e.getKey() == e.ESC ) {
					this.onTrigger1Click();
				}
			}, this );
		}
	},
	// Clear search
	onTrigger1Click: function () {
		this.el.dom.value = '';
		this.triggers[0].hide();
		this.fireEvent('trigger1');
	},
	// Search
	onTrigger2Click: function () {
		var value = this.getRawValue();
		this.triggers[0].show();
		this.fireEvent('trigger2', value );
	}
});
Ext.reg('ghoti-form-filterfield', GHOTI.widget.FilterField );


// Following the pattern of Ext.PagingToolbar
GHOTI.widget.SearchToolbar = Ext.extend( Ext.Toolbar, {
	initComponent: function () {
		var i, l, col, searchItems, userItems,
			cm = this.colModel,
			items = [];
		if( !Ext.isArray(cm) ) { cm = cm.config; }	// Assume it's a constructed colModel
		for( i=0, l=cm.length; i<l ;i++ ) {
			col = cm[i];
			if( col.dataIndex && col.filterable !== false ) {
				items.push({ text: col.header ,checked: items.length == 0 ,value: col.dataIndex });
			}
		}

		searchItems = [
			this.searchField = new Ext.CycleButton({
				items: items,
				showText: true
			}),
			this.field = new GHOTI.widget.FilterField({
				catchKeys: true,
				listeners: {
					'trigger1': { scope: this, fn: this.clearSearch },
					'trigger2': { scope: this, fn: this.onSearch }
				}
			})
		];
		userItems = this.items;
		if( this.prependButtons ) {
			this.items = userItems.concat(searchItems);
		} else {
			this.items = searchItems.concat(userItems);
		}
		GHOTI.widget.SearchToolbar.superclass.initComponent.apply( this, arguments );
	}
	// XXX How to do this using filter__ without purging pre-set filters?
	,onSearch: function (value) {
		this.clearFilter();
		this.store.baseParams['filter__'+this.searchField.getActiveItem().value] = value;
		this.store.load();
	}
	,clearFilter: function () {
		if( !this.store.baseParams ) { return; }
		Ext.iterate( this.store.baseParams, function (k,v) {
			if( k.slice(0,8) == 'filter__' ) {
				delete this.store.baseParams[k];
			}
		}, this);
	}
	,clearSearch: function () {
		if( !this.rendered ) { return; }
		this.clearFilter();
		if(this.field) {
			this.field.setValue('');
		}
		this.store.load();
	}
    ,getFilter: function() {
        filter = {}
        Ext.iterate( this.store.baseParams, function (k,v) {
            if( k.slice(0,8) == 'filter__' ) {
                filter[k] = v;
            }
        }, this);
        return filter;
    }
    ,downloadState: {
        id: this.dlId
        ,method: 'POST'
    }
    ,onDownload: function () {
        this.downloadState.baseParams = this.getFilter();
        this.downloadState.url = GHOTI.path.join( this.url, 'download/' );
        GHOTI.form.HiddenDownload( this.downloadState );
    }

});
Ext.reg("ghoti-widget-searchtoolbar" ,GHOTI.widget.SearchToolbar );

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.widget");

/*
** If you set "loadOnRender" the URL supplied will be used to save AND load the data.
** This fits well the RESTful "edit" approach.
*/

GHOTI.widget.FormWindow = Ext.extend( Ext.Window, {
	width: 500	// Windows without width screw up in IE
	,autoHeight: true
	,formReset: true
	,loadOnRender: false
	,loadFormItems: false	// URL to load from
	,initComponent: function () {
		if( !this.form ) {
			this.form = new GHOTI.form.FormPanel({
				url: this.dataUrl || this.url
				,defaults: { autoHeight: true, xtype: 'textfield' }
				,items: this.items
				,baseParams: this.baseParams
                ,autoScroll: true
			});
			delete this.items;
			delete this.baseParams;
		}
		Ext.applyIf( this, {
			buttons: [
				{ text: "Cancel" ,handler: this.closeWindow ,scope: this }
				,{ text: "Save" ,handler: this.saveWindow ,scope: this }
			]
			,items: [ this.form ]
		});
		GHOTI.widget.FormWindow.superclass.initComponent.apply( this, arguments );
		if( this.loadFormItems ) {
			Ext.Ajax.request({
				url: this.loadFormItems || GHOTI.path.join( this.url, "add/" )
				,success: this.doLoadFormItems
				,scope: this
			});
		} else if( this.loadOnRender ) {
			this.on("render", this.loadForm, this );
		}
	}
	,doLoadFormItems: function( response, options ) {
		this.add.apply( this, Ext.decode(response.responseText));
		this.doLayout();
		if( this.loadOnRender ) {
			this.loadForm();
		}
	}
	,onRender: function () {
		if( this.formReset ) {
			//this.on("show", this.form.form.reset, this.form.form );
			this.form.form.reset();
		}
		GHOTI.widget.FormWindow.superclass.onRender.apply( this, arguments );
	}
	,loadForm: function () {
		this.form.load({
			url: this.url
			,method: "GET"
		});
	}
	,closeWindow: function () {
		this.close();
	}
	,saveWindow: function () {
		this.enableButtons( false );
		this.form.form.submit({
			url: this.url
			,success: this.closeWindow
			,failure: this.enableButtons.createDelegate( this, [ true ] )
			,scope: this
		});
	}
	,enableButtons: function ( yes ) {
		Ext.invoke( this.buttons, yes ? 'enable' : 'disable' );
	}
});
Ext.reg('ghoti-widget-formwindow', GHOTI.widget.FormWindow);

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.widget");


/*
** Ref: http://extjs.com/forum/showthread.php?t=43615
*/
GHOTI.widget.TemplateRenderer = function(tmpl) {
	if( Ext.type(tmpl) === 'string' || Ext.type(tmpl) === 'array' ) {
		tmpl = new Ext.XTemplate(tmpl);
		tmpl.compile();
	}
	this.tmpl = tmpl;
};
GHOTI.widget.TemplateRenderer.prototype = {
	render: function( el, response, updateManager, callback ) {
		var json = Ext.decode( response.responseText );
		response.json = json;
		el.update( this.tmpl.apply(json), false, callback );
	}
};

GHOTI.widget.TemplatePanel = Ext.extend( Ext.Panel, {
	initComponent: function () {
		GHOTI.widget.TemplatePanel.superclass.initComponent.apply( this ,arguments );
		if( Ext.type(this.tmpl) === 'string' || Ext.type(this.tmpl) === 'array' ) {
			this.tmpl = new Ext.XTemplate(this.tmpl);
			this.tmpl.compile();
		}
		this.on('render', function() {
			var um = this.body.getUpdater();
			um.renderer = new GHOTI.widget.TemplateRenderer( this.tmpl );
		} ,this ,{ single: true } );
	}
	,onRender: function (ct, position ) {
		GHOTI.widget.TemplatePanel.superclass.onRender.apply( this, arguments );
		if( this.data ) {
			this.update( this.data );
		}
	}
	,update: function (data) {
		this.tmpl.overwrite( this.body, data );
	}
});
Ext.reg('ghoti-widget-templatepanel' ,GHOTI.widget.TemplatePanel );

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.widget");

/*
** Help view supporting multiple books
*/
GHOTI.widget.HelpLibrary = Ext.extend( Ext.Panel, {
	id: 'help-library'
	,icons: {
		book: GHOTI.url('icon', 'book.png')
		,open_book: GHOTI.url('icon', 'book_open.png')
		,leaf: GHOTI.url('icon', 'page_white_text.png')
	}
	,initComponent: function () {
		Ext.applyIf( this, {
			title: "Help"
			,layout: "border"
			,url: "/help/"
		});
		this.loader = new Ext.tree.TreeLoader({
			dataUrl: this.url
			,requestMethod: 'GET'
		});
		this.loader.on('beforeload', function ( treeLoader, node ) {
			treeLoader.baseParams.slug = node.attributes.slug || '';
			treeLoader.dataUrl = this.url + node.attributes.book + "/";
		} ,this );
		this.loader.on('load' ,function ( self, node, resp ) {
			node.eachChild( function (n) {
				n.getUI().getIconEl().src = this.icons[( n.leaf ? 'leaf' : 'book' )];
				if( !n.leaf ) {
					n.on('beforecollapse' ,function (n) { n.getUI().getIconEl().src= this.icons.book; } ,this );
					n.on('beforeexpand' ,function (n) { n.getUI().getIconEl().src= this.icons.open_book; } ,this );
				}
				n.on('click' ,this.loadContent.createDelegate( this, [ n ] ) );
			} ,this );
		}, this );
		GHOTI.widget.HelpLibrary.superclass.initComponent.apply( this, arguments );
	}
	,onRender: function () {
		this.contentPanel = this.add({ region: 'center' });
		this.indexTree = this.add({
			xtype: 'treepanel'
			,region: 'north'
			,split: 'true'
			,height: 100
			,rootVisible: false
			,root: new Ext.tree.TreeNode({ expanded: true ,isTarget: false })
		});
		for( var i=0; i < this.books.length ; i++ ) {
			this.addBook( this.books[i] );
		}
		GHOTI.widget.HelpLibrary.superclass.onRender.apply( this, arguments );
	}
	,addBook: function ( config ) {
		var newnode,
			root = this.indexTree.getRootNode()
			,o = {
				text: "Help"
				,isTarget: false
				,loader: this.loader
				,icon: this.icons.book
			};
		Ext.apply( o, config );
		newnode = root.appendChild( new Ext.tree.AsyncTreeNode(o) );
		newnode.on( 'beforecollapse', function (n) { n.getUI().getIconEl().src= this.icons.book; } ,this );
		newnode.on( 'beforeexpand', function (n) { n.getUI().getIconEl().src= this.icons.open_book; } ,this );
	}
	,loadContent: function ( node ) {
		this.contentPanel.load({ method: "GET" ,url: this.url + node.attributes.book + "/" + node.attributes.slug + "/" });
	}
});
Ext.reg("ghoti-widget-library", GHOTI.widget.HelpLibrary );

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.widget");

GHOTI.widget.ListTabPanel = Ext.extend( Ext.Container, {
    /*
     * Which field to use as the title of each tab
    */
    titleField: 'id'
    ,layout: 'border'
    ,autoEl: 'div'
    ,initComponent: function () {
        if( !this.listConfig ) { this.listConfig = {}; }
        Ext.applyIf( this.listConfig, {
            xtype: this.listXtype
            ,region: 'west'
            ,split: true
            ,width: 300
        });

        if( !this.tabConfig ) { this.tabConfig = {}; }
        Ext.applyIf( this.tabConfig, {
            xtype: 'tabpanel'
            ,region: 'center'
            ,autoScroll: true
        });

        GHOTI.widget.ListTabPanel.superclass.initComponent.apply( this, arguments );
        this.listGrid = this.add( this.listConfig );
        this.listGrid.on('rowdblclick', this.showRecordTab, this );

        this.tabPanel = this.add( this.tabConfig );
        this.tabPanel.on('add', this.syncSize, this, { buffer: 200 } );
    }
    ,syncSize: function () {
        if( this.rendered ) {
            GHOTI.widget.ListTabPanel.superclass.syncSize.apply( this, arguments );
        }
    }
    ,showRecordTab: function ( grid, rowIndex, ev ) {
        var store = grid.store
            ,rec = store.getAt(rowIndex)
            ,new_id = grid.getId() + "-" + rec.get( store.reader.meta.id ).toString()
            ,t = this.tabPanel.items.get(new_id);
        if( !t ) {
            t = this.tabPanel.add({
                xtype: this.tabXtype
                ,rec: rec
                ,id: new_id
                ,title: this.getTabTitle(rec)
                ,closable: true
            });
        }
        this.tabPanel.setActiveTab(t);
    }
    /*
    ** Generate the tab title for a given record
    */
    ,getTabTitle: function ( rec ) {
        return rec.get( this.titleField );
    }
});

Ext.reg('ghoti-widget-listtab', GHOTI.widget.ListTabPanel );

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace('GHOTI.data');

/*
** Preens a store, removing dirty flags when fields are changed back to
** their original values.
**
** Use:
**   Store.on('update', GHOTI.data.preenStore );
*/
GHOTI.data.preenStore = function ( store, rec, op ) {
	if( rec.modified ) {
		for( var k in rec.modified ) {
			if( rec.get(k) == rec.modified[k] ) {
				delete rec.modified[k];
			}
		}
		if( !GHOTI.hasKeys( rec.modified ) ) {
			delete rec.modified;
		}
	}
	if( !rec.modified && rec.dirty ) {
		rec.dirty = false;
		if( store.modified.indexOf(rec) != -1 ) {
			store.modified.remove(rec);
		}
	}
};

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.grid");

/*
 *	Base Grid
 *
 * A grid with many helper add-ons
 */
GHOTI.grid.BaseGrid = Ext.extend( Ext.grid.GridPanel, {
	paginated: false
	,loadOnRender: false
	,searchbar: false
	,downloadbutton: false
    ,pageSize: 20	// Set this to 0 for no limit.
	,mustSelectTitle: "No selection."
	,mustSelectText: "Please select an item first."
	,confirmDelTitle: "Delete Record"
	,confirmDelMessage: "Are you sure you want to delete this record?"
	,currentWindow: null
	,initComponent: function () {
		// Create a store if none supplied
		if( !this.store && this.url ) {
			this.store = new Ext.data.JsonStore({
				url: this.url
				,baseParams: Ext.apply({ limit: this.pageSize }, this.baseParams || {} )
				,sortInfo: this.defaultSortInfo || {}
				,remoteSort: true
				,autoDestroy: true
			});
		}
		if( this.paginated ) {
			var cfg = {
				store: this.store
				,xtype: 'paging'
				,pageSize: this.pageSize
				,items: this.bbar || []
			};
			if( typeof this.paginated === 'object' ) {
				Ext.apply( cfg, this.paginated );
			}
			this.bbar = cfg;
		}

		if( this.searchbar ) {
			var cfg = {
				store: this.store
				,xtype: 'ghoti-widget-searchtoolbar'
				,colModel: this.searchColModel || this.colModel || this.cm || this.columns 
				,items: (this.tbar || []).concat(this.downloadbutton ? {
                    text: 'Download',
                    group: 'Bulk',
                    tooltip: 'Download record details',
                    iconCls: 'silk-disk',
                    handler: this.onDownload, scope: this
                } : [])
			};
			if( typeof this.searchbar === 'object' ) {
				Ext.apply( cfg, this.searchbar );
			}
			this.tbar = cfg;
		}
		GHOTI.grid.BaseGrid.superclass.initComponent.apply( this, arguments );
		this.getSelectionModel().on('selectionchange', this.updateButtons, this );

		if( this.loadOnRender === true ) {
			this.on( "viewready" ,this.refreshView ,this );
		}
		this.updateButtons(this.getSelectionModel());
	}
	,updateButtons: function ( sm, sel ) {
		if( sel ) {
			// Cell Selection Model
		} else {
			var i, l, c = sm.getCount(),
				b = this.getTopToolbar(),
				buttons = [];
			if( b ) {
				buttons.push.apply( buttons, b.findByType('button') );
			}
			b = this.getBottomToolbar();
			if( b ) {
				buttons.push.apply( buttons, b.findByType('button') );
			}
			for( i=0, l=buttons.length; i < l ; i++ ) {
				b = buttons[i];
				if( b.requireSelect ) {
					if( b.requireSelect == 'multi' ) {
						b[ c ? 'enable' : 'disable' ]();
					} else {
						b[ c == 1 ? 'enable' : 'disable' ]();
					}
				}
			}
		}
	}
	,refreshView: function( page ) {
		if(this.paginated) {
			var bbar = this.getBottomToolbar();
			if( page ) {
				bbar.cursor = 0;
			}
			bbar.doLoad(bbar.cursor);
		} else {
			this.store.reload();
		}
	}
	// Default action handlers
	,showWindow: function ( config ) {
		if( this.currentWindow === null ) {
			this.currentWindow = Ext.create( config, 'window' );
			this.currentWindow.on("close", this.onWindowClose, this );
			this.currentWindow.show(this).center();
		}
	}
	,displayFormWindow: function ( config ) {
		config.xtype = 'ghoti-widget-formwindow';
		return this.showWindow(config);
	}
	,onWindowClose: function () {
		if( this.currentWindow !== null ) {
			this.currentWindow = null;
		}
		this.refreshView();
	}
	,onAddItem: function () {
		this.displayFormWindow( this.getAddForm() );
	}
	,getAddForm: function () {
		if( this.addForm ) {
			return this.addForm;
		}
		return Ext.apply({
			id: this.id + "-add-window"
			,title: this.getAddTitle()
			,url: this.getAddUrl()
			,items: this.addFields
		}, this.addFormConfig || {} );
	}
	,getAddUrl: function () {
		return this.url + "add/";
	}
	,getAddTitle: function () {
		return this.addFormTitle || "";
	}
	,onEditItem: function () {
		var rec = this.getSelectionModel().getSelected();
		this.displayFormWindow( this.getEditForm( rec ) );
	}
	,getEditForm: function ( rec ) {
		var f = this.editForm || this.addForm;
		if ( !f ) {
			f = Ext.apply({
				id: this.id + "-edit-window"
				,title: this.getEditTitle()
				,loadOnRender: true
				,url: this.getEditUrl( rec )
				,items: this.editFields || this.addFields
			}, this.editFormConfig || this.addFormConfig || {} );
		}
		return f;
	}
	,getEditUrl: function ( rec ) {
		return this.url + rec.get('id').toString() + "/";
	}
	,getEditTitle: function ( rec ) {
		return this.editFormTitle || "";
	}
	,onDelItem: function () {
		var rec;
		if( this.multiDelete ) {
			rec = Ext.invoke( this.getSelectionModel().getSelections(), 'get', 'id' );
		} else {
			rec = this.getSelectionModel().getSelected().get('id');
		}
		// Confirm they really want to do this.
		Ext.MessageBox.confirm(
			this.confirmDelTitle
			,this.confirmDelMessage
			,this.onDelConfirm.createDelegate( this, [ rec ], 0 )
			,this
		);
	}
	,onDelConfirm: function ( rec, btn ) {
		if( btn == "yes" ) {
			Ext.Ajax.request({
				url: this.getDelUrl( rec )
				,params: { id: rec, confirm: true }
				,success: this.onDelSuccess || this.refreshView
				,failure: this.onDelFailure || this.refreshView
				,scope: this
			});
		}
	}
	,getDelUrl: function ( rec ) {
		if( Ext.isArray(rec) ) {
			return GHOTI.path.join( this.url, 'delete/' );
		} else {
			return GHOTI.path.join( this.url, rec, "delete/" );
		}
	}
    ,getFilter: function() {
        filter = {}
        // Ignores Pagination
        Ext.iterate( this.store.baseParams, function (k,v) {
            if( k.slice(0,8) == 'filter__' ) {
                filter[k] = v;
            }
        }, this);
        return filter;
    }
    ,downloadState: {
        id: this.dlId
        ,method: 'POST'
    }
    ,onDownload: function () {
        this.downloadState.baseParams = this.getFilter();
        this.downloadState.url = GHOTI.path.join( this.url, 'download/' );
        GHOTI.form.HiddenDownload( this.downloadState );
    }

});
Ext.reg('ghoti-grid-basegrid', GHOTI.grid.BaseGrid );

/*global Ext GHOTI */
/*jslint laxbreak: true, browser: true, forin: true, evil: false, onevar: true, undef: true, white: false */

Ext.namespace("GHOTI.grid");

/*
** Resize columns in a Grid to be only as wide as needed to fit their content
**
** Any column marked as "fixed" will not be adjusted.
*/
GHOTI.grid.ResizePlugin = function () {
	return {
		init: function (cmp) {
			Ext.apply( cmp, {
				autoSizeColumns: function() {
					var i, j, k, cw, cells,
						view =  this.getView(),
						cm = this.colModel,
						l = cm.getColumnCount();

					if( !this.rendered ) { return; }

					if( !view ) { return; }
					if( !view.hasRows() ) {
						// There's no rows, so we can't do much
						return;
					}
					// first time through, get the padding
					if( !this.autoSizePadding ) {
						if( Ext.isWebKit ) {
							this.autoSizePadding = -1;
						} else {
							this.autoSizePadding = Ext.fly(view.getCell(0,0).firstChild).getPadding('lr');
						}
					}

					cm.suspendEvents();

					// If we don't do this, cols never shrink
					for( i = 0 ; i < l ; i ++ ) {
						// Don't resize fixed cols
						if( cm.isFixed(i) ) { continue; }
						cm.setColumnWidth(i, 1 );
					}

					view.updateAllColumnWidths();

					for (i = 0; i < l; i++) {
						// Don't resize fixed cols
						if( cm.isFixed(i) ) { continue; }
						cw = [
							this.view.getHeaderCell(i).firstChild.scrollWidth + ( ( this.enableHdMenu === true ) ? 15 : 3 )
						];
						// Find the widest cell
						cells = Ext.query(
							'.x-grid3-td-'+this.colModel.getColumnId(i)+' div',
							this.getView().mainBody.dom
						);
						for( j=0, k=cells.length; j < k ; j++ ) {
							cw.push(cells[j].scrollWidth);
						}
						cm.setColumnWidth( i, Math.max.apply( Math, cw ) + this.autoSizePadding );
					}

					cm.resumeEvents();
					view.updateAllColumnWidths();

				}
			});
			cmp.store.on("load" ,cmp.autoSizeColumns ,cmp );
			cmp.store.on("update", cmp.autoSizeColumns, cmp );
			cmp.on('beforedestroy', cmp.store.un.createDelegate( cmp.store, [ 'update', cmp.autoSizeColumns, cmp ] ) );
			cmp.on('beforedestroy', cmp.store.un.createDelegate( cmp.store, [ 'load', cmp.autoSizeColumns, cmp ] ) );
		}
	};
}();

/*
** Same as above, but for Buffered grid views
**
** Any column marked as "fixed" will not be adjusted.
*/
GHOTI.grid.BufferResizePlugin = function () {
	return {
		init: function (cmp) {
			Ext.apply( cmp, {
				autoSizeColumns: function() {
					var i,
						view =  this.getView(),
						cm = this.colModel,
						l = cm.getColumnCount();

					if( !this.rendered ) { return; }

					if( !view ) { return; }
					if( !view.hasRows() ) {
						// There's no rows, so we can't do much
						return;
					}
					// first time through, get the padding and create a textMetric
					if( !this.autoSizePadding ) {
						i = view.getCell(0,0).firstChild;
						if( Ext.isWebKit ) {
							this.autoSizePadding = -1;
						} else {
							this.autoSizePadding = Ext.fly(i).getPadding('lr');
						}
						this.textMetric = Ext.util.TextMetrics.createInstance(i);
					}

					cm.suspendEvents();

					// If we don't do this, cols never shrink
					for( i = 0 ; i < l ; i ++ ) {
						// Don't resize fixed cols
						if( cm.isFixed(i) ) { continue; }
						cm.setColumnWidth(i, 1 );
					}

					view.updateAllColumnWidths();

					for (i = 0; i < l; i++) {
						// Don't resize fixed cols
						if( cm.isFixed(i) ) { continue; }
						cm.setColumnWidth( i, this.calcColumnSize(i) );
					}

					cm.resumeEvents();
					view.updateAllColumnWidths();
				}
				,calcColumnSize: function(c) {
					var i ,l ,recs = this.store.data.items
						// Get the width of the header cell
						,cw = [
							this.view.getHeaderCell(c).firstChild.scrollWidth + ( ( this.enableHdMenu === true ) ? 15 : 3 )
						]
						,tm = this.textMetric
						,di = this.colModel.getDataIndex(c);

					for( i=0, l = recs.length; i < l ; i ++ ) {
						cw.push( tm.getWidth( recs[i].get(di) ) );
					}
					return Math.max.apply( Math, cw ) + this.autoSizePadding;
				}
			});
			cmp.store.on("load" ,cmp.autoSizeColumns ,cmp );
			cmp.on("render" ,cmp.autoSizeColumns ,cmp );
		}
	};
}();


GHOTI.grid.RefreshPlugin = function () {
	return {
		init: function (cmp) {
			cmp.refreshTask = Ext.TaskMgr.start({
				run: cmp.refreshView
				,args: []
				,scope: cmp
				,interval: cmp.refreshInterval || 10000
			});
			cmp.on("beforedestroy", Ext.TaskMgr.stop.createCallback( cmp.refreshTask ) );
		}
	};
}();

GHOTI.grid.PageSizePlugin = function () {
	return {
		init: function (cmp) {
			cmp.on('afterlayout', this.adjustPageSize );
			cmp.on('bodyresize' ,this.adjustPageSize );
		}
		,adjustPageSize: function ( grid ) {

			var tb,
				rows = grid.view.mainWrap.dom.clientHeight / grid.view.innerHd.clientHeight;

			if( !isNaN(rows) ) {

				rows = Math.max( grid.minRows || 10 , Math.floor(rows) );
				grid.store.baseParams.limit = rows;

				tb =  grid.getBottomToolbar();
				if( tb && tb.isXType('paging') && tb.store === grid.store ) {
					tb.pageSize = rows;
				}
				tb = grid.getTopToolbar();
				if( tb && tb.isXType('paging') && tb.store === grid.store ) {
					tb.pageSize = rows;
				}
			}
		}
	};
}();

/*
Scan the top and bottom toolbars of a grid, and set the buttons enabled/disabled
according to their requireSelected value.
*/
GHOTI.grid.ActiveButtonPlugin = function () {
	return {
		init: function ( cmp ) {
			cmp.getSelectionModel().on('selectionchange', this.updateButtons, cmp);
			GHOTI.grid.ActiveButtonPlugin.updateButtons.call( cmp, cmp.getSelectionModel() );
		},
		updateButtons: function ( sm, sel ) {
			/*
			This doesn't make sense on a Cell selection model.
			CellSM passes second param, Row doesn't.
			*/
			if( sel ) { return; }
			var c = sm.getCount(),
				tb = this.getBottomToolbar();
			if( tb ) { GHOTI.grid.ActiveButtonPlugin.updateToolbar(tb, c); }
			tb = this.getTopToolbar();
			if( tb ) { GHOTI.grid.ActiveButtonPlugin.updateToolbar(tb, c); }
		},
		updateToolbar: function (tb, count) {
			tb.items.each( function (b) {
				if( b.requireSelected == 'multi' ) {
					b[count ? 'enable' : 'disable']();
				} else {
					b[count == 1 ? 'enable' : 'disable']();
				}
			}, this );
		}
	};
}();

// http://www.sencha.com/forum/showthread.php?93901-DataTip-show-a-complex-tooltip-based-upon-node-s-attributes-or-Record-s-data.&highlight=datatip
/**
 * @class Ext.ux.DataTip
 * @extends Ext.ToolTip.
 * <p>This plugin implements automatic tooltip generation for an arbitrary number of child nodes <i>within</i> a Component.</p>
 * <p>This plugin is applied to a high level Component, which contains repeating elements, and depending on the host Component type,
 * it automatically selects a {@link Ext.ToolTip#delegate delegate} so that it appears when the mouse enters a sub-element.</p>
 * <p>When applied to a GridPanel, this ToolTip appears when over a row, and the Record's data is applied
 * using this object's {@link Ext.Component#tpl tpl} template.</p>
 * <p>When applied to a DataView, this ToolTip appears when over a view node, and the Record's data is applied
 * using this object's {@link Ext.Component#tpl tpl} template.</p>
 * <p>When applied to a TreePanel, this ToolTip appears when over a tree node, and the Node's {@link Ext.tree.TreeNode#attributes attributes} are applied
 * using this object's {@link Ext.Component#tpl tpl} template.</p>
 * <p>When applied to a FormPanel, this ToolTip appears when over a Field, and the Field's <code>tooltip</code> property is used is applied
 * using this object's {@link Ext.Component#tpl tpl} template, or if it is a string, used as HTML content.</p>
 * <p>If more complex logic is needed to determine content, then the {@link Ext.Component#beforeshow beforeshow} event may be used.<p>
 * <p>This class also publishes a <b><code>beforeshowtip</code></b> event through its host Component. The <i>host Component</i> fires the
 * <b><code>beforeshowtip</code></b> event.
 */
Ext.ux.DataTip = Ext.extend(Ext.ToolTip, (function() {

//  Target the body (if the host is a Panel), or, if there is no body, the main Element.
    function onHostRender() {
        var e = this.body || this.el;
        if (this.dataTip.renderToTarget) {
            this.dataTip.render(e);
        }
        this.dataTip.initTarget(e);
    }

    function updateTip(tip, data) {
        if (tip.rendered) {
            tip.update(data);
        } else {
            if (Ext.isString(data)) {
                tip.html = data;
            } else {
                tip.data = data;
            }
        }
    }
    
    function beforeTreeTipShow(tip) {
        var e = Ext.fly(tip.triggerElement).findParent('div.x-tree-node-el', null, true),
            node = e ? tip.host.getNodeById(e.getAttribute('tree-node-id', 'ext')) : null;
        if(node){
            updateTip(tip, node.attributes);
        } else {
            return false;
        }
    }

    function beforeGridTipShow(tip) {
        var rec = this.host.getStore().getAt(this.host.getView().findRowIndex(tip.triggerElement));
        if (rec){
            updateTip(tip, rec);
        } else {
            return false;
        }
    }

    function beforeViewTipShow(tip) {
        var rec = this.host.getRecord(tip.triggerElement);
        if (rec){
            updateTip(tip, rec);
        } else {
            return false;
        }
    }

    function beforeFormTipShow(tip) {
        var el = Ext.fly(tip.triggerElement).child('input,textarea'),
            field = el ? this.host.getForm().findField(el.id) : null;
        if (field && (field.tooltip || tip.tpl)){
            updateTip(tip, field.tooltip || field);
        } else {
            return false;
        }
    }

    function beforeComboTipShow(tip) {
        var rec = this.host.store.getAt(this.host.selectedIndex);
        if (rec){
            updateTip(tip, rec);
        } else {
            return false;
        }
    }

    return {
        init: function(host) {
            host.dataTip = this;
            this.host = host;
            if (host instanceof Ext.tree.TreePanel) {
                this.delegate = this.delegate || 'div.x-tree-node-el';
                this.on('beforeshow', beforeTreeTipShow);
            } else if (host instanceof Ext.grid.GridPanel) {
                this.delegate = this.delegate || host.getView().rowSelector;
                this.on('beforeshow', beforeGridTipShow);
            } else if (host instanceof Ext.DataView) {
                this.delegate = this.delegate || host.itemSelector;
                this.on('beforeshow', beforeViewTipShow);
            } else if (host instanceof Ext.FormPanel) {
                this.delegate = 'div.x-form-item';
                this.on('beforeshow', beforeFormTipShow);
            } else if (host instanceof Ext.form.ComboBox) {
                this.delegate = this.delegate || host.itemSelector;
                this.on('beforeshow', beforeComboTipShow);
            }
            if (host.rendered) {
                onHostRender.call(host);
            } else {
                host.onRender = host.onRender.createSequence(onHostRender);
            }
        }
    };
})());

