Ext.namespace('Ext.ux.form');

/**
 * @class Ext.ux.form.BrowseButton
 * @extends Ext.Button
 * Ext.Button that provides a customizable file browse button.
 * Clicking this button, pops up a file dialog box for a user to select the file to upload.
 * This is accomplished by having a transparent <input type="file"> box above the Ext.Button.
 * When a user thinks he or she is clicking the Ext.Button, they're actually clicking the hidden input "Browse..." box.
 * Note: this class can be instantiated explicitly or with xtypes anywhere a regular Ext.Button can be except in 2 scenarios:
 * - Panel.addButton method both as an instantiated object or as an xtype config object.
 * - Panel.buttons config object as an xtype config object.
 * These scenarios fail because Ext explicitly creates an Ext.Button in these cases.
 * Browser compatibility:
 * Internet Explorer 6:
 * - no issues
 * Internet Explorer 7:
 * - no issues
 * Internet Explorer 8:
 * - no issues
 * Firefox 3 - Windows:
 * - pointer cursor doesn't display when hovering over the button.
 * Safari 3 - Windows:
 * - no issues.
 * @author loeppky - based on the work done by MaximGB in Ext.ux.UploadDialog (http://extjs.com/forum/showthread.php?t=21558)
 * The follow the curosr float div idea also came from MaximGB. With patches for IE8 by 4Him.
 * @see http://extjs.com/forum/showthread.php?t=29032
 * @constructor
 * Create a new BrowseButton.
 * @param {Object} config Configuration options
 */
Ext.ux.form.BrowseButton = Ext.extend(Ext.Button, {
    /*
     * Config options:
     */
    /**
     * @cfg {String} inputFileName
     * Name to use for the hidden input file DOM element.  Deaults to "file".
     */
    inputFileName: 'file',
    /**
     * @cfg {Boolean} debug
     * Toggle for turning on debug mode.
     * Debug mode doesn't make clipEl transparent so that one can see how effectively it covers the Ext.Button.
     * In addition, clipEl is given a green background and floatEl a red background to see how well they are positioned.
     */
    debug: false,


    /*
     * Private constants:
     */
    /**
     * @property FLOAT_EL_WIDTH
     * @type Number
     * The width (in pixels) of floatEl.
     * It should be less than the width of the IE "Browse" button's width (65 pixels), since IE doesn't let you resize it.
     * We define this width so we can quickly center floatEl at the mouse cursor without having to make any function calls.
     * @private
     */
    FLOAT_EL_WIDTH: 60,

    /**
     * @property FLOAT_EL_HEIGHT
     * @type Number
     * The heigh (in pixels) of floatEl.
     * It should be less than the height of the "Browse" button's height.
     * We define this height so we can quickly center floatEl at the mouse cursor without having to make any function calls.
     * @private
     */
    FLOAT_EL_HEIGHT: 18,


    /*
     * Private properties:
     */
    /**
     * @property buttonCt
     * @type Ext.Element
     * Element that contains the actual Button DOM element.
     * We store a reference to it, so we can easily grab its size for sizing the clipEl.
     * @private
     */
    buttonCt: null,
    /**
     * @property clipEl
     * @type Ext.Element
     * Element that contains the floatEl.
     * This element is positioned to fill the area of Ext.Button and has overflow turned off.
     * This keeps floadEl tight to the Ext.Button, and prevents it from masking surrounding elements.
     * @private
     */
    clipEl: null,
    /**
     * @property floatEl
     * @type Ext.Element
     * Element that contains the inputFileEl.
     * This element is size to be less than or equal to the size of the input file "Browse" button.
     * It is then positioned wherever the user moves the cursor, so that their click always clicks the input file "Browse" button.
     * Overflow is turned off to preven inputFileEl from masking surrounding elements.
     * @private
     */
    floatEl: null,
    /**
     * @property inputFileEl
     * @type Ext.Element
     * Element for the hiden file input.
     * @private
     */
    inputFileEl: null,
    /**
     * @property originalHandler
     * @type Function
     * The handler originally defined for the Ext.Button during construction using the "handler" config option.
     * We need to null out the "handler" property so that it is only called when a file is selected.
     * @private
     */
    originalHandler: null,
    /**
     * @property originalScope
     * @type Object
     * The scope originally defined for the Ext.Button during construction using the "scope" config option.
     * While the "scope" property doesn't need to be nulled, to be consistent with originalHandler, we do.
     * @private
     */
    originalScope: null,
    /**
     * @property BROWSERS_OFFSET
     * @type Object
     * The browsers specific offsets used to position the clipping element for better overlay tightness. For
     * Ext 3, Ext 2 offsets are used unless there is an Ext 3 entry.
     * @private
     * @author 4Him
     */
    BROWSERS_OFFSETS: {
        Ext2: {
            IE8:   {left: -8, top: -16, width: 16,  height: 22},
            IE:    {left: -8, top: -3,  width: 16,  height: 6},
            Opera: {left: -8, top: -3,  width: -18, height: -1},
            Gecko: {left: -8, top: -6,  width: 16,  height: 10},
            Safari:{left: -4, top: -2,  width: 6,   height: 6}
        },
        Ext3: {
            IE8:   {left: -7,           width: 10},
            IE:    {left: -3,           width: 6},
            Gecko: {                    width: 11}
        }
    },
    /**
     * @property isExt2x
     * @type boolean
     * Whether we are currently using Ext 2.x
     * @private
     * @author 4Him
     */
    isExt2x: Ext.version.match(/^2\./),


    /*
     * Protected Ext.Button overrides
     */
    /**
     * @see Ext.Button.initComponent
     */
    initComponent: function(){
        Ext.ux.form.BrowseButton.superclass.initComponent.call(this);
        // Store references to the original handler and scope before nulling them.
        // This is done so that this class can control when the handler is called.
        // There are some cases where the hidden file input browse button doesn't completely cover the Ext.Button.
        // The handler shouldn't be called in these cases.  It should only be called if a new file is selected on the file system.
        this.originalHandler = this.handler;
        this.originalScope = this.scope;
        this.handler = null;
        this.scope = null;
    },

    /**
     * @see Ext.Button.onRender
     */
    onRender: function(ct, position){
        Ext.ux.form.BrowseButton.superclass.onRender.call(this, ct, position); // render the Ext.Button
        // Patch for compatibility with 3.x (@author 4Him, based on dario's 05-10-2009 post)
        if(this.isExt2x) {
            this.buttonCt = this.el.child('.x-btn-center em');
        } else {
            this.buttonCt = this.el.child('.x-btn-mc em');
        }
        this.buttonCt.position('relative'); // this is important!
        var styleCfg = {
            position: 'absolute',
            overflow: 'hidden',
            top: '0px', // default
            left: '0px' // default
        };
        // browser specifics for better overlay tightness - modified by 4Him
        for(var browser in this.BROWSERS_OFFSETS.Ext2) {
            if(Ext['is'+browser]) {
                Ext.apply(styleCfg, {
                    left: this.getBrowserOffset(browser, 'left')+'px',
                    top: this.getBrowserOffset(browser, 'top')+'px'
                });
                break;
            }
        }
        this.clipEl = this.buttonCt.createChild({
            tag: 'div',
            style: styleCfg
        });
        this.setClipSize();
        this.clipEl.on({
            'mousemove': this.onButtonMouseMove,
            'mouseover': this.onButtonMouseMove,
            scope: this
        });

        this.floatEl = this.clipEl.createChild({
            tag: 'div',
            style: {
                position: 'absolute',
                width: this.FLOAT_EL_WIDTH + 'px',
                height: this.FLOAT_EL_HEIGHT + 'px',
                overflow: 'hidden'
            }
        });


        if (this.debug) {
            this.clipEl.applyStyles({
                'background-color': 'green'
            });
            this.floatEl.applyStyles({
                'background-color': 'red'
            });
        } else {
            // We don't set the clipEl to be transparent, because IE 6/7 occassionaly looses mouse events for transparent elements.
            // We have listeners on the clipEl that can't be lost as they're needed for realligning the input file element.
            this.floatEl.setOpacity(0.0);
        }

        // Cover cases where someone tabs to the button:
        // Listen to focus of the button so we can translate the focus to the input file el.
        var buttonEl = this.el.child(this.buttonSelector);
        buttonEl.on('focus', this.onButtonFocus, this);
        // In IE, it's possible to tab to the text portion of the input file el.
        // We want to listen to keyevents so that if a space is pressed, we "click" the input file el.
        if (Ext.isIE) {
            this.el.on('keydown', this.onButtonKeyDown, this);
        }

        this.createInputFile();
    },


    /*
     * Private helper methods:
     */
    /**
     * Returns an offset based on this.BROWSERS_OFFSET
     * If currently using Ext 3.x, tries to find a value for 3.x and if there is none for 3.x, it
     * returns a value for 2.x
     * @param {string} the desired offset. Can be one of the following: 'left', 'top', 'width', 'height'
     * @param {string} browser the browser for which to return the offset
     * @return {int} the desired offset
     * @author 4Him
     */
    getBrowserOffset: function(browser, which) {
        if(!this.isExt2x && this.BROWSERS_OFFSETS.Ext3[browser] && this.BROWSERS_OFFSETS.Ext3[browser][which]) {
            return this.BROWSERS_OFFSETS.Ext3[browser][which];
        } else {
            return this.BROWSERS_OFFSETS.Ext2[browser][which];
        }
    },

    /**
     * Sets the size of clipEl so that is covering as much of the button as possible.
     * @private
     */
    setClipSize: function(){
        if (this.clipEl) {
            var width = this.buttonCt.getWidth();
            var height = this.buttonCt.getHeight();
            // The button container can have a width and height of zero when it's rendered in a hidden panel.
            // This is most noticable when using a card layout, as the items are all rendered but hidden,
            // (unless deferredRender is set to true).
            // In this case, the clip size can't be determined, so we attempt to set it later.
            // This check repeats until the button container has a size.
            if (width === 0 || (height === 0 && !Ext.isIE8)) {  // ugly hack (Ext.isIE8) by 4Him
                this.setClipSize.defer(100, this);
            } else {
                // Loop by 4Him
                for(var browser in this.BROWSERS_OFFSETS.Ext2) {
                    if(Ext['is'+browser]) {
                        width = width + this.getBrowserOffset(browser, 'width');
                        height = height + this.getBrowserOffset(browser, 'height');
                        break;
                    }
                }
                this.clipEl.setSize(width, height);
            }
        }
    },

    /**
     * Creates the input file element and adds it to inputFileCt.
     * The created input file elementis sized, positioned, and styled appropriately.
     * Event handlers for the element are set up, and a tooltip is applied if defined in the original config.
     * @private
     */
    createInputFile: function(){
        // When an input file gets detached and set as the child of a different DOM element,
        // straggling <em> elements get left behind.
        // I don't know why this happens but we delete any <em> elements we can find under the floatEl to prevent a memory leak.
        this.floatEl.select('em').each(function(el){
            el.remove();
        });
        this.inputFileEl = this.floatEl.createChild({
            tag: 'input',
            type: 'file',
            size: 1, // must be > 0. It's value doesn't really matter due to our masking div (inputFileCt).
            name: this.inputFileName || Ext.id(this.el),
            tabindex: this.tabIndex,
            // Use the same pointer as an Ext.Button would use.  This doesn't work in Firefox.
            // This positioning right-aligns the input file to ensure that the "Browse" button is visible.
            style: {
                position: 'absolute',
                cursor: 'pointer',
                right: '0px',
                top: '0px'
            }
        });
        this.inputFileEl = this.inputFileEl.child('input') || this.inputFileEl;
        // IE8 needs opacity on the 'file input' element - @author 4Him
        if(Ext.isIE8) {
            this.inputFileEl.setOpacity(0.0);
        }

        // setup events
        this.inputFileEl.on({
            'click': this.onInputFileClick,
            'change': this.onInputFileChange,
            'focus': this.onInputFileFocus,
            'select': this.onInputFileFocus,
            'blur': this.onInputFileBlur,
            scope: this
        });

        // add a tooltip
        if (this.tooltip) {
            if (typeof this.tooltip == 'object') {
                Ext.QuickTips.register(Ext.apply({
                    target: this.inputFileEl
                }, this.tooltip));
            } else {
                this.inputFileEl.dom[this.tooltipType] = this.tooltip;
            }
        }
    },

    /**
     * Redirecting focus to the input file element so the user can press space and select files.
     * @param {Event} e focus event.
     * @private
     */
    onButtonFocus: function(e){
        if (this.inputFileEl) {
            this.inputFileEl.focus();
            e.stopEvent();
        }
    },

    /**
     * Handler for the IE case where once can tab to the text box of an input file el.
     * If the key is a space, we simply "click" the inputFileEl.
     * @param {Event} e key event.
     * @private
     */
    onButtonKeyDown: function(e){
        if (this.inputFileEl && e.getKey() == Ext.EventObject.SPACE) {
            this.inputFileEl.dom.click();
            e.stopEvent();
        }
    },

    /**
     * Handler when the cursor moves over the clipEl.
     * The floatEl gets centered to the cursor location.
     * @param {Event} e mouse event.
     * @private
     */
    onButtonMouseMove: function(e){
        var xy = e.getXY();
        xy[0] -= this.FLOAT_EL_WIDTH / 2;
        xy[1] -= this.FLOAT_EL_HEIGHT / 2;
        this.floatEl.setXY(xy);
    },

    /**
     * Add the visual enhancement to the button when the input file recieves focus.
     * This is the tip for the user that now he/she can press space to select the file.
     * @private
     */
    onInputFileFocus: function(e){
        if (!this.isDisabled) {
            this.el.addClass("x-btn-over");
        }
    },

    /**
     * Removes the visual enhancement from the button.
     * @private
     */
    onInputFileBlur: function(e){
        this.el.removeClass("x-btn-over");
    },

    /**
     * Handler when inputFileEl's "Browse..." button is clicked.
     * @param {Event} e click event.
     * @private
     */
    onInputFileClick: function(e){
        e.stopPropagation();
    },

    /**
     * Handler when inputFileEl changes value (i.e. a new file is selected).
     * @private
     */
    onInputFileChange: function(){
        if (this.originalHandler) {
            this.originalHandler.call(this.originalScope, this);
        }
    },


    /*
     * Public methods:
     */
    /**
     * Detaches the input file associated with this BrowseButton so that it can be used for other purposed (e.g. uplaoding).
     * The returned input file has all listeners and tooltips applied to it by this class removed.
     * @param {Boolean} whether to create a new input file element for this BrowseButton after detaching.
     * True will prevent creation.  Defaults to false.
     * @return {Ext.Element} the detached input file element.
     */
    detachInputFile: function(noCreate){
        var result = this.inputFileEl;

        if (typeof this.tooltip == 'object') {
            Ext.QuickTips.unregister(this.inputFileEl);
        } else {
            this.inputFileEl.dom[this.tooltipType] = null;
        }
        this.inputFileEl.removeAllListeners();
        this.inputFileEl = null;

        if (!noCreate) {
            this.createInputFile();
        }
        return result;
    },

    /**
     * @return {Ext.Element} the input file element attached to this BrowseButton.
     */
    getInputFile: function(){
        return this.inputFileEl;
    },

    /**
     * @see Ext.Button.disable
     */
    disable: function(){
        Ext.ux.form.BrowseButton.superclass.disable.call(this);
        this.inputFileEl.dom.disabled = true;
    },

    /**
     * @see Ext.Button.enable
     */
    enable: function(){
        Ext.ux.form.BrowseButton.superclass.enable.call(this);
        this.inputFileEl.dom.disabled = false;
    }
});

Ext.reg('browsebutton', Ext.ux.form.BrowseButton);

Ext.namespace('Ext.ux.UploadPanel');
/**
 * Example Usage:
 *
 * new Ext.ux.UploadPanel({
 *  title: "Upload Files",
 *  upload: {
 *    url: '/uploads',
 *    method: 'POST'
 *  }
 * });
 */
Ext.ux.UploadPanel = Ext.extend(Ext.grid.GridPanel, {
  initComponent: function() {
    this.fileupload = Ext.data.Record.create([
      { name: 'filename' },
      { name: 'status' },
      { name: 'progress', type: 'integer' },
      { name: 'file' }
    ]);
    
    this.progress_column = new Ext.ux.grid.ProgressColumn({
      id: 'progress',
      header: 'Progress',
      sortable: false,
      dataIndex: 'progress',
      textPst: '%',
      colored: true
    });

    this.datareader = new Ext.data.ArrayReader({ id: 'filename' }, this.fileupload);
    Ext.apply(this, {
      store: new Ext.data.Store({
        reader: this.datareader
      }),
      columns: [
        { header: 'ID', hidden: true, sortable: true, dataIndex: 'id' },
        { header: 'Filename', sortable: true, dataIndex: 'filename', width: 160 },
        this.progress_column,
        { header: 'Status', sortable: true, dataIndex: 'status', width: 50 }
      ],
      viewConfig: { forceFit: true },
      stripeRows: true,
      autoExpandColumn: 'filename',
      tbar: [{
        xtype: 'browsebutton',
        handler: this.add_file,
        scope: this,
        inputFileName: 'file',
        text: 'Add File'
      }, {
        id: 'clear_queue_button',
        handler: this.clear_queue,
        scope: this,
        text: "Clear Queue"            
      }, {
        id: 'abort_transfer_button',
        text: 'Abort Transfer',
        handler: this.abort_transfer,
        scope: this,
        disabled: true
      }, '-', {
        text: 'Remove Selected',
        scope: this,
        disabled: true
      }]
    });
    
    Ext.ux.UploadPanel.superclass.initComponent.call(this);
    this.on('beforedestroy', this.abort_transfer, this);
  },
  
  render_progress: function(percent)
  {
    if(!percent) { percent = 0; }
    return percent+'%';       
  },  
  
  add_file: function(browsebutton)
  {
    var input_file_el = browsebutton.detachInputFile();
    var name = input_file_el.dom.value;

    if(!this.upload_form) {
      var form_el = this.body.createChild({
        tag: 'form',
        style: 'display:none'
      });

      this.upload_form = new Ext.form.BasicForm(form_el, {
        method: 'POST',
        fileUpload: true
      });
    }

    input_file_el.appendTo(this.upload_form.getEl());
    
    row = new this.fileupload({
      filename: name,
      status: 'Queued',
      progress: 0,
      file: input_file_el
    });
    this.store.add([row]);
  },
  
  clear_queue: function()
  {
    this.store.removeAll();
  },
  
  abort_transfer: function()
  {
    if (this.current_record) {
      this.current_record.set('status', 'Aborted');
      this.current_record.commit();
      this.remove_upload_frame();
    }

    if(this.task) {
      Ext.TaskMgr.stop(this.task);
      this.task = null;
    }
    this.stop();    
  },
  
  complete_transfer: function()
  {
    if (this.current_record) {
      this.current_record.set('progress', 100);
      this.current_record.set('status', 'Done');
      this.current_record.commit();
      this.upload_files();
    } else {
      this.stop();
    }
  },
  
  upload_files: function()
  {
    if(!this.upload_form) { return false; }
    this.current_record = this.prep_next_file();
    if (this.current_record) {
      this.upload_form.submit({
        url: this.upload_url + "?X-Progress-ID=" + this.uuid
      });
      this.monitor();
      this.start();
    } else {
      this.stop();
    }
  },
  
  prep_next_file: function()
  {
    var record = null;

    this.uuid = "";
    for(i=0; i<32; i++) { this.uuid += Math.floor(Math.random() * 16).toString(16); }

    this.store.each(function(r) {
      if (!record && r.get('status') == 'Queued') {
        record = r;
      } else {
        r.get('file').dom.disabled = true;
      }
    });
    if (record) {
      record.get('file').dom.disabled = false;
      record.commit();
    }
    
    return record;
  },  
  
  monitor: function() 
  {
    var handler = function() {
      ExtApp.xrequest({
        url: this.progress_url + "?X-Progress-ID=" + this.uuid,
        scope: this,
        listeners: {
          success: function(xhr, options, obj) {
            if(!this.current_record) { return false; }

            if(obj.received && obj.size) {
              this.current_record.set('progress', parseInt(obj.received/obj.size*100));
            }
            if(obj.state) {
              this.current_record.set('status', Ext.util.Format.capitalize(obj.state));
              if(obj.state == 'done') {
                return this.complete_transfer();
              }
            }
            this.current_record.commit();
          }
        }
      });
    };

    this.task = Ext.TaskMgr.start({
      run: handler,
      scope: this,
      interval: 3000
    });
  },
  
  start: function()
  {
    this.clear_button(false);
    this.abort_button(true);
  },

  stop: function()
  {
    this.curent_record = null;
    this.clear_button(true);
    this.abort_button(false);
  },
  
  clear_button: function(enabled)
  {
    button = Ext.getCmp('clear_queue_button');
    button.setDisabled(!enabled);
    return button;
  },
  
  abort_button: function(enabled)
  {
    button = Ext.getCmp('abort_transfer_button');
    button.setDisabled(!enabled);
    return button;
  },  
  
  remove_upload_frame: function()
  {
    var upload_frame = Ext.getBody().child('iframe.x-hidden:last');
    if (upload_frame) {
      upload_frame.removeAllListeners();
      upload_frame.dom.src = 'about:blank';
      upload_frame.remove();
      upload_frame = null;
    }
  }
});
Ext.reg('uploadpanel', Ext.ux.UploadPanel);

Ext.namespace('Ext.ux.StandaloneGrid');

Ext.ux.StandaloneGrid = Ext.extend(Ext.grid.GridPanel, {
  initComponent: function() {
    if(!this.default_data) { this.default_data = []; }
    this.record = Ext.data.Record.create(this.fields);
    this.reader = new Ext.data.JsonReader({ id: this.field_id }, this.record );    
    this.store = new Ext.data.Store({ reader: this.reader, data: this.default_data });

    this.selected = {
      first: function()
      {
        return this.getSelections()[0];
      }.createDelegate(this),
      
      all: function()
      {
        return this.getSelections();
      }.createDelegate(this)
    };
    
    Ext.ux.StandaloneGrid.superclass.initComponent.call(this);
  },

  add_record: function(params) {
    var r = new this.record(params, params[this.field_id]);
    this.store.add([r]);
  }
});

Ext.reg('standalonegrid', Ext.ux.StandaloneGrid);

/**
 * Not Completed
 * Working on getting BasicGrid into a Ext User Extention.
 */
Ext.namespace('Ext.ux.BasicGrid');
Ext.ux.BasicGrid = Ext.extend(Ext.grid.GridPanel, {
  fields: null,
  dataRoot: 'results',
  dataCount: 'count',
  dataId: 'id',
  proxyUrl: '/',
  proxyMethod: 'GET',
  proxyParams: null,
  pageSize: 0,
  title: false,
  autoExpandColumn: 0,
  frame: false,
  border: true,
  stripeRows: true,
  viewConfig: { autoFill: true },
  
  sm: new Ext.grid.RowSelectionModel({ singleSelect: true }),
  default_data: { count: 0, results: [] },
  
  initComponent: function() {
    this.record = Ext.data.Record.create(this.fields);
    
    this.reader = new Ext.data.JsonReader({ 
      totalProperty: this.dataCount, 
      root: this.dataRoot,
      id: this.dataId      
    }, this.record );
    
    this.http_proxy = new Ext.data.HttpProxy({
      url: this.proxyUrl,
      method: this.proxyMethod,
      params: this.proxyParams,
      headers: { accept: 'application/json, text/javascript, */*' }
    });
    
    this.http_proxy.on('loadexception', function(proxy, o, response, event) {
      if (response.getResponseHeader['Content-Type'].match('text/javascript')) {
        eval(response.responseText);
      } else {
        Ext.Msg.alert('Error Encountered', "An error was encountered while trying to retrieve data from the server. Server response was: " + response.responseText);
      }
    });
    
    this.store = new Ext.data.Store({ proxy: this.http_proxy, reader: this.reader, data: this.default_data });
    
    if(!this.bbar && this.pageSize) {
      this.bbar = new Ext.PagingToolbar({
        pageSize: this.pageSize,
        store: this.store,
        displayInfo: true,
        displayMsg: 'Displaying items {0} - {1} of {2}',
        emptyMsg: "No items to display"
      });
    }    
    
    /* Call parent constructor */
    Ext.ux.BasicGrid.superclass.initComponent.call(this);
  },
  
  selected: {
    first: function()
    {
      return this.getSelections()[0];
    },
    
    all: function()
    {
      return this.getSelections();
    }
  }  
});
Ext.reg('basicgrid', Ext.ux.BasicGrid);


/**
 * Scrolling Panel.  This will create a panel that will automatically scroll the contents.
 * When the user mouses over the panel the scrolling will stop and resume as soon as the
 * mouse has left the body area of the panel.
 * 
 * TODO: stop animation when panel is collapsed or hidden
 * TODO: start animation when panel is expanded or displayed
 * 
 * All standard panel options are available along with the following additions:
 *    speed:  Controls the scroll rate of the panel, higher numbers == faster, 
 *              lower numbers == slower. (Default: 10)
 *    
 */
Ext.ns('Ext.ux.ScrollingPanel');
Ext.ux.ScrollingPanel = Ext.extend(Ext.Panel, {
  active: false,
  direction: 'down',
  animation_opts: null,
  speed: 10,
  
  initComponent: function() {
    Ext.ux.ScrollingPanel.superclass.initComponent.call(this);
  },
  
  start: function() {
    this.active = true;
    if(this.is_scrolling()) { return; }
    this.animate();
  },
  
  stop: function() {
    this.active = false;
    if (!this.is_scrolling()) { return; }
    this.animation_opts.anim.stop();    
  },
  
  animate: function(change_direction) {
    if (!this.is_active() || this.is_scrolling()) { return; }
    if(change_direction) { this.change_direction(); }
    
    var duration = this.calc_duration();
    this.animation_opts = this.prep_animation(duration);
    
    this.body.scroll(this.direction, this.body.dom.scrollHeight, this.animation_opts);    
  },
  
  is_active: function() {
    return this.active;
  },
  
  is_scrolling: function() {
    if (this.animation_opts && this.animation_opts.anim && this.animation_opts.anim.isAnimated()) {
      return true;
    }
    return false;    
  },
  
  prep_animation: function(duration) {
    if(!duration) { duration = 20; }
    
    return { 
      duration: duration, 
      callback: this.animate.createDelegate(this, [true]), 
      easing: 'easeNone' 
    };    
  },
  
  change_direction: function() {
    this.direction = (this.direction == 'down' ? 'up' : 'down');
  },
  
  calc_duration: function() {
    if(this.direction == 'down') {
      return this.calc_duration_down();    
    } else {
      return this.calc_duration_up();
    }    
  },
  
  calc_duration_down: function() {
    var ch = this.body.dom.clientHeight;
    var st = this.body.dom.scrollTop;
    var sh = this.body.dom.scrollHeight;
    
    return parseFloat((sh - st - ch)/this.speed).toFixed(2);    
  },
  
  calc_duration_up: function() {
    var ch = this.body.dom.clientHeight;
    var st = this.body.dom.scrollTop;
    var sh = this.body.dom.scrollHeight;
    
    return parseFloat((st)/this.speed).toFixed(2);    
  },
  
  onRender: function(ct, position) {
    Ext.ux.ScrollingPanel.superclass.onRender.call(this, ct, position);
    
    this.body.on('mouseover', function(e) {
      if (e.within(this.body, true)) {
        this.stop();
      }
    }, this);
          
    this.body.on('mouseout', function(e) {
      if (!e.within(this.body, true)) {
        this.start();
      }
    }, this);
  },
  
  afterRender: function() {
    Ext.ux.ScrollingPanel.superclass.afterRender.call(this);
    
    if(this.defer) {
      this.start.defer(this.defer, this);
    } else {
      this.start();
    }
  }
});

Ext.reg('scrollingpanel', Ext.ux.ScrollingPanel);

/**
 * LargeMenuButton extension
 * 
 * To properly have an image show up for the button you must create a css class that sets a
 *  background image (Example: .button1 { background: url('images/icons/small/info.png') })
 *  
 * The following css is required to make it look correct:
    div.menu-button {
      padding: 10px 0 10px 0;
      text-align:center;
    }
    
    div.menu-button:hover {
      background-color: yellow;
    }
    
    button.menu-button  {
      padding-top: 64px;
      min-width: 64px;
      border: none;
      font-weight: bold;
      white-space: normal;
      background-position: top center !important;
    }
 */

Ext.namespace('Ext.ux.LargeMenuButton');
Ext.ux.LargeMenuButton = Ext.extend(Ext.Button, {
  template: new Ext.Template('<div ext:qtip="{1:htmlEncode}"><button class="menu-button"></button><div>{0}</div></div>'),
  cls: 'menu-button',
  setSize: function(w,h) { this.getEl().setSize(w, h); }
});
Ext.reg('largemenubutton', Ext.ux.LargeMenuButton);


/**
 * FancyWizardy Action for buttons and stuff.
 * TODO: Need to make it more customizable, currently forces largemenubuttons and showing buttons to the left of content.
 */
Ext.namespace('Ext.ux.FancyWizard');
Ext.ux.FancyWizard = Ext.extend(Ext.Panel, {
  layout: 'border',
  border: true,
  buttonAlign: 'center',
  valid: false,
  
  isValid: function() { return this.valid; },
  
  initComponent: function() {
    Ext.ux.FancyWizard.superclass.initComponent.call(this);
    this.setup_panels();
    
    this.previousButton = this.addButton({
      text: '&laquo; Previous',
      disabled: true,
      scope: this,
      handler: this.back
    });
    
    this.cancelButton = this.addButton({
      text: 'Cancel',
      scope: this,
      handler: this.cancel
    });
    
    this.nextButton = this.addButton({
      text: 'Next &raquo;',
      disabled: true,
      scope: this,
      handler: this.forward
    });
    
    this.finishButton = this.addButton({
      xtype: 'button',
      text: 'Finish',
      disabled: true,
      scope: this,
      handler: this.finish
    });
    
    this.on('afterlayout', function() {
      this.update_buttons();
    });
  }, 
  
  finish: function() {
    if(this.submit) { this.submit(); }
  },
  
  setup_panels: function() {
    this.setup_menu();
    this.setup_content();
  },
  
  setup_menu: function() {
    this.wizard_menu = new Ext.Panel({
      region: 'west',
      border: false,
      cls: 'right-border-only',
      width: 94,
      bodyStyle: 'padding: 3px;'
    });
    this.add(this.wizard_menu);
  },
  
  setup_content: function() {
    this.wizard_cards = new Ext.Panel({
      layout: 'card',
      activeItem: 0,
      region: 'center',
      border: false,
      defaults: { border: false },
      listeners: {
        afterlayout: {
          fn: this.update_buttons,
          scope: this
        }
      }
    });
    this.add(this.wizard_cards); 
  },
  
  forward: function(skipCallbacks) {
    var current = this.wizard_cards.layout.activeItem;
    var item = this.findNext();
    if (item === undefined) { return; }
    
    if(!skipCallbacks && current.beforeForwardAction) {
      var status = current.beforeForwardAction.call(current, this, item);
      if(status === false) { return false; }
    }
    
    this.activate_card(item);
    
    if(!skipCallbacks && current.afterForwardAction) {
      current.afterForwardAction.call(current, this, item);
    }
  },
  
  cancel: function() {
    Ext.Msg.alert('Error', 'Please define a cancel method');
  },
  
  back: function() {
    item = this.findPrev();
    if (item === undefined) { return; }
    this.activate_card(item);
  },
  
  update_buttons: function() {
    if(this.findPrev() === undefined || this.findPrev() === false) {
      this.previousButton.disable();  
    } else {
      this.previousButton.enable();
    }
    
    if(this.findNext() === undefined || this.findNext() === false) {
      this.nextButton.disable();
    } else {
      this.nextButton.enable();
    }
    
    if(this.isValid()) {
      this.finishButton.enable();
    } else {
      this.finishButton.disable();
    }
  },
  
  findPrev: function() {
    var index = this.wizard_cards.items.indexOf(this.wizard_cards.layout.activeItem);
    return this.wizard_cards.items.get(index - 1);
  },
  
  findNext: function() {
    var index = this.wizard_cards.items.indexOf(this.wizard_cards.layout.activeItem);
    var card = this.wizard_cards.items.get(index + 1);
    if(!card || !card.wizardButton || card.wizardButton.disabled) { return false; }
    return card;
  },
  
  update_activate_card_css: function(card, current) {
    if (current) { current.removeClass('selected'); }
    card.wizardButton.addClass('selected');
  },
    
  activate_card: function(card, skip_callbacks) {
    if(!skip_callbacks && card.beforeActivate) {
      var status = card.beforeActivate(this);
      if(status === false) { return false; }
    }
    
    this.update_activate_card_css(card, this.wizard_cards.layout.activeItem.wizardButton);
    this.wizard_cards.layout.setActiveItem(card);
    
    if(!skip_callbacks && card.onActivate) {
      card.onActivate(this);
    }
  },
  
  add_card: function(card_config, options) {
    Ext.apply(card_config, options);
    var card = this.wizard_cards.add(card_config);
    
    Ext.applyIf(card.wizardButton, {
      scale: 'large',
      anchor: '100%',
      iconAlign: 'top'      
    });
    
    card.wizardButton = new Ext.Button(card.wizardButton);
    card.wizardButton.on('click', function() {
      this.activate_card(card);
    }, this);
    
    this.wizard_menu.add(card.wizardButton);
    this.wizard_menu.add({ border: false, height: 3 });
    
    if(this.wizard_menu.items.length == 1) {
      this.update_activate_card_css(card);
    }
  }
});


/*
 * Ext JS Library 2.2
 * Copyright(c) 2006-2008, Ext JS, LLC.
 * licensing@extjs.com
 * 
 * http://extjs.com/license
 */
Ext.ux.form.FileUploadField = Ext.extend(Ext.form.TextField,  {
    /**
     * @cfg {String} buttonText The button text to display on the upload button (defaults to
     * 'Browse...').  Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text
     * value will be used instead if available.
     */
    buttonText: 'Browse...',
    /**
     * @cfg {Boolean} buttonOnly True to display the file upload field as a button with no visible
     * text field (defaults to false).  If true, all inherited TextField members will still be available.
     */
    buttonOnly: false,
    /**
     * @cfg {Number} buttonOffset The number of pixels of space reserved between the button and the text field
     * (defaults to 3).  Note that this only applies if {@link #buttonOnly} = false.
     */
    buttonOffset: 3,
    /**
     * @cfg {Object} buttonCfg A standard {@link Ext.Button} config object.
     */

    // private
    readOnly: true,
    
    /**
     * @hide 
     * @method autoSize
     */
    autoSize: Ext.emptyFn,
    
    // private
    initComponent: function(){
        Ext.ux.form.FileUploadField.superclass.initComponent.call(this);
        
        this.addEvents(
            /**
             * @event fileselected
             * Fires when the underlying file input field's value has changed from the user
             * selecting a new file from the system file selection dialog.
             * @param {Ext.form.FileUploadField} this
             * @param {String} value The file value returned by the underlying file input field
             */
            'fileselected'
        );
    },
    
    // private
    onRender : function(ct, position){
        Ext.ux.form.FileUploadField.superclass.onRender.call(this, ct, position);
        
        this.wrap = this.el.wrap({cls:'x-form-field-wrap x-form-file-wrap'});
        this.el.addClass('x-form-file-text');
        this.el.dom.removeAttribute('name');
        
        this.fileInput = this.wrap.createChild({
            id: this.getFileInputId(),
            name: this.name||this.getId(),
            cls: 'x-form-file',
            tag: 'input', 
            type: 'file',
            size: 1
        });
        
        var btnCfg = Ext.applyIf(this.buttonCfg || {}, {
            text: this.buttonText
        });
        this.button = new Ext.Button(Ext.apply(btnCfg, {
            renderTo: this.wrap,
            cls: 'x-form-file-btn' + (btnCfg.iconCls ? ' x-btn-icon' : '')
        }));
        
        if(this.buttonOnly){
            this.el.hide();
            this.wrap.setWidth(this.button.getEl().getWidth());
        }
        
        this.fileInput.on('change', function(){
            var v = this.fileInput.dom.value;
            this.setValue(v);
            this.fireEvent('fileselected', this, v);
        }, this);
    },
    
    // private
    getFileInputId: function(){
        return this.id+'-file';
    },
    
    // private
    onResize : function(w, h){
        Ext.ux.form.FileUploadField.superclass.onResize.call(this, w, h);
        
        this.wrap.setWidth(w);
        
        if(!this.buttonOnly){
            var width = this.wrap.getWidth() - this.button.getEl().getWidth() - this.buttonOffset;
            this.el.setWidth(width);
        }
    },
    
    // private
    preFocus : Ext.emptyFn,
    
    // private
    getResizeEl : function(){
        return this.wrap;
    },

    // private
    getPositionEl : function(){
        return this.wrap;
    },

    // private
    alignErrorIcon : function(){
        this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]);
    }
    
});
Ext.reg('fileuploadfield', Ext.ux.form.FileUploadField);

Ext.ux.NavigationHandler = function(config) {
  config = config || {};
  this.initialConfig = config;
  Ext.apply(this, config);  
  
  this.addEvents(
    /**
     * @event load
     * Fires after the page is loaded
     */
    'load',
    
    /**
     * @event start
     * Fires after the location.hash monitor has been started
     */
    'start',
    
    /**
     * @event stop
     * Fires after the location.hash monitor has been stopped
     */
    'stop');
    
  Ext.ux.NavigationHandler.superclass.constructor.call(this);

  if(Ext.isIE7) {
    var ihistory = Ext.DomHelper.append(Ext.getBody(), '<iframe id="iHistory" style="display:none"></iframe>');
    var iframe = ihistory.contentDocument || ihistory.contentWindow.document;
    iframe.open();
    iframe.close();
    iframe.location.hash = top.location.hash.replace(/^#/,'');
  }
  
  this.initComponent();
  
  if (this.autoStart) {
    if (!Ext.isReady) { 
      Ext.onReady(this.start, this);
    } else {
      this.start();
    } 
  }
};
Ext.extend(Ext.ux.NavigationHandler, Ext.util.Observable, {
  /**
   * @cfg {Hash} pages
   * A hash of page configurations to load custom navigation links
   */
  pages: {},
  
  /**
   * @cfg {Boolean} autoStart
   * Autostart the monitor the changes to the location hash
   */
  autoStart: true,
  
  /* //protected
   * Function to be implemented by NavigationHandler subclasses, it is empty by default.
   */
  initComponent: Ext.emptyFn,
  
  /* //protected
   * Monitor for the location hash changes
   */
  monitor: null,
  
  /*
   * @cfg {Integer}
   * The delay in milliseconds between checks for changes in location.hash
   */
  monitorDelay: 50,
  
  /* //protected
   * Monitor handler function that watches for changes to the location.hash
   */
  monitorHandler: function() {
    var loc = this.getHash();
    if (!loc && this.defaultLocation) { 
      loc = this.defaultLocation; 
    }
    if (this.current_page != loc || this.forceLoadUrl === true || this.forceLoadHandler === true) {
      this.open(loc);
    }
  },
  
  /**
   * Start monitoring the location.hash for changes
   */
  start: function() {
    this.monitor = setInterval(this.monitorHandler.createDelegate(this), this.monitorDelay); 
    this.fireEvent('start', this);
  },
  
  /**
   * Stop monitoring the location.hash for changes
   */
  stop: function() {
    clearInterval(this.monitor);
    this.fireEvent('stop', this);
  },
  
  /**
   * Get the current location.hash, '#' hash been stripped from the hash
   * @return {String}
   */
  getHash: function() {
    if (Ext.isIE7) {
      var ihistory = Ext.getDom('iHistory');
      var iframe = ihistory.contentDocument || ihistory.contentWindow.document;
      return iframe.location.hash.replace(/^#/, '');
    } else {
      return top.location.hash.replace(/^#/, '');
    }
  },
  
  getPage: function(page) {
    return this.pages[page];
  },
  
  getRequest: function(hash) {
    var request = {};
    request.hash = hash || this.getHash() ;
    request.params = request.hash.split('/');
    request.controller = request.params.shift();
    request.action = request.params.shift();
    
    if (request.params.size() > 0) {
      var params = {};
      for (var ii = 0; ii < request.params.size(); ii++) {
        var item = request.params[ii].split(':');
        params[item.shift()] = item.join(':');
      }
      request.params = params;
    }
    
    return request;    
  },
  
  /**
   * Set the location.hash
   * @param {String} hash
   */
  setHash: function(hash) {
    if (Ext.isIE7) {
      var ihistory = Ext.getDom('iHistory');
      var iframe = ihistory.contentDocument || ihistory.contentWindow.document;
      iframe.open();
      iframe.close();
      iframe.location.hash = hash;
    } else {
      if (hash[0] != '#') { hash = '#' + hash; }
    }
    top.location.hash = hash;
  },
  
  load: function(hash, forceUrl, forceHandler) {
    this.setHash(hash);
    if (forceUrl === true) { this.forceLoadUrl = true; }
    if (forceHandler === true) { this.forceLoadHandler = true; }
  },
  
  runHandler: function(from_url) {
    var request = this.getRequest(); 
    var page = this.getPage(request.controller);
    if (page && page.handler && Ext.isReady) {
      page.handler(request, from_url); 
    }
  },
  
  open: function(hash) {
    if (!hash) { return; }
    
    var request = this.getRequest(hash);
    var page = this.getPage(request.controller);
      
    if (page) {
      if (page.url && (this.current_url != page.url || this.forceLoadUrl === true)) {
        switch(page.mode) {
          case 'window':
            window.open(page.url, page.target);
            break;
          case 'html':
            ExtApp.load(page.url);
            break;
          default:
            ExtApp.request({
              url: page.url,
              listeners: {
                scope: this,
                success: function(){
                  this.runHandler(true);
                },
                failure: function(){
                  ExtApp.Notice.show({ msg: "An error occured while trying to load the requested page" })
                }
              }
            });
        }
      } else {
        if (this.current_page != hash || this.forceLoadHandler === true) {
          this.runHandler(false);
        }
      }
      this.current_url = page.url;
      this.current_page = hash;
      this.forceLoadUrl = false;
      this.forceLoadHandler = false;
      this.fireEvent('load', hash, page);
    }
  }
});

Ext.ux.SearchField = Ext.extend(Ext.form.TwinTriggerField, {
  initComponent : function(){
    Ext.ux.SearchField.superclass.initComponent.call(this);
    this.on('specialkey', function(f, e){
        if(e.getKey() == e.ENTER){
            this.onTrigger2Click();
        }
    }, this);
  },

  validationEvent:false,
  validateOnBlur:false,
  trigger1Class:'x-form-clear-trigger',
  trigger2Class:'x-form-search-trigger',
  hideTrigger1:true,
  width:300,
  hasSearch : false,
  paramName : 'query',

  onTrigger1Click : function(){
    if(this.hasSearch){
      this.el.dom.value = '';
      var o = {start: 0};
      
      this.store.clearFilter();
      this.triggers[0].hide();
      this.hasSearch = false;
    }
  },

  onTrigger2Click : function(){
    var search_string = this.getRawValue();
    if(search_string.length < 1){
      this.onTrigger1Click();
      return;
    }
    var v;
    var o = {start: 0};
    var field;
    var visible = true;
    var found = null;
    var search = new RegExp();
    
    //Remove multiple spaces
    var search_items = search_string.replace(/\s+/,' ').split(' ');
    
    //Always clear the last filter before doing a new filter
    this.store.filterBy(function(record, id){
      
      for (var ii=0; ii < search_items.length; ii++) {
        found = false;
        search.compile(search_items[ii], 'i');
        for (field in record.data) {
          found = found || search.test(record.get(field));
        }
        if(found === false) { return false; }
      }
      return true;
    });
    
    this.hasSearch = true;
    this.triggers[0].show();
  }
});


Ext.ux.FormTab = Ext.extend(Ext.FormPanel, {
  initComponent: function() {
    this.addEvents(
      /**
       * @event save
       * Fires after the form has been saved
       */
      'saved',
      
      /**
       * @event error
       * Fires after the form has received an error from the server.
       */
      'error'
    );
    Ext.ux.FormTab.superclass.initComponent.call(this);
  },
  
  submit: function(config) {
    this.getForm().submit({
      url: config.url,
      method: config.method || 'POST',
      waitMsg: config.waitMsg || 'Saving Form',
      waitTitle: config.waitTitle || 'Please Wait...',
      scope: this,
      success: this.successHandler,
      failure: this.failureHandler
    });
  },
  
  successHandler: function(form, action) {
    if (action.result.success) {
      this.fireEvent('saved', action.result)
    } else {
      this.fireEvent('error', action.result)
    }
    if (action.result.flash) { 
      ExtApp.Notice.show({ msg: action.result.flash }); 
    }    
  },
  
  failureHandler: function(form, action) {
    this.fireEvent('error', action.result)
    if (action.result && action.result.flash) { 
      ExtApp.Notice.show({ msg: action.result.flash }); 
    }
  }
});
Ext.reg('formtab', Ext.ux.FormTab);

Ext.ux.DescriptionGrid = Ext.extend(Ext.grid.GridPanel, {
  loadMask: { msg:'Fetching records from the server...' },

  initComponent: function() {
    this.addEvents('ctxchange');

    Ext.applyIf(this.actions, {
      "refresh": {
        icon: 'images/icons/small/table_refresh.png',
        text:'Refresh',
        scope:this,
        handler: function(){
          this.store.reload();
        }
      }
    });

    Ext.applyIf(this, {
      /**
       * @cfg {String} Set the default sort field and direction for the grid
       */
      sortInfo: {
        field: 0,
        direction: 'ASC'
      },

      actions: {},

      top_menu_items: [],
      row_menu_items: [],

      data: { },

      updateRowClass: null,
      sm: new Ext.grid.RowSelectionModel({ singleSelect: true }),
      context_menu: [this.actions.refresh],
      viewConfig: {},
      cls: 'project-grid'
    });

    Ext.applyIf(this.viewConfig, {
      enableRowBody: true,
      showDescription: true,
      scrollDelay: 50,
      rowHeight: 60,
      forceFit: true,
      getRowClass : this.applyRowClass.createDelegate(this)
    });

    this.store = new Ext.data.Store({
      "proxy": new Ext.data.HttpProxy({
        "url": this.data.url,
        "method": this.data.method || 'GET'
      }),
      "reader": new Ext.data.JsonReader({
        "fields": this.data.fields
      }),
      "sortInfo": this.sortInfo,
      "autoLoad": this.data.autoLoad || false
    });

    this.setupActions();
    this.setupStatusBar();
    if (this.tbar !== false) {
      this.tbar = new Ext.Toolbar();
      this.addTopMenuItems();
    }

    Ext.ux.DescriptionGrid.superclass.initComponent.call(this);

    this.on('rowcontextmenu', this.onContextClick, this);
    this.getStore().on('beforeload', function() {
      this.getSelectionModel().clearSelections();
    }, this);
    this.getSelectionModel().on('selectionchange', function(sm) {
      this.ctxRecord = sm.getSelected();
      this.fireEvent('ctxchange', this, this.ctxRecord);
    }, this);
  },

  getView: function() {
    if(!this.view){
      this.view = new Ext.ux.grid.BufferView(this.viewConfig);
    }
    return this.view;
  },

  load: function(options) {
    this.store.load(options);
  },

  setupStatusBar: function() {
    if (!this.summaryMsg) { this.summaryMsg = 'Found {totalCount} {title}'; }

    this.countTpl = new Ext.Template(this.summaryMsg);

    this.countItem = new Ext.Toolbar.TextItem(this.countTpl.apply({
      "title": this.title,
      "totalCount": 0
    }));

    if (this.bbar !== false) {
      this.bbar = new Ext.Toolbar();

      Ext.each(this.bottom_menu_items, function(item) {
        this.bbar.add(this.actions[item]);
      });
      this.bbar.add([this.actions.refresh, '->', this.countItem]);

      this.store.on('load', function() {
        var totalCount = this.store.getTotalCount();
        var el = Ext.get(this.countItem.getEl());
        var text = this.countTpl.apply({
          "title": this.title,
          "totalCount": totalCount
        });
        if (el) { el.dom.innerHTML = text; }
      }, this);
    }
  },

  setupActions: function() {
    for(var index in this.actions) {
      if (!this.actions[index].execute) {
        var action = this.actions[index];
        Ext.applyIf(action, { scope: this });
        this.actions[index] = new Ext.Action(action);
      }
    }
  },

  createContextMenu: function() {
    // create context menu on first right click
    var menu = new Ext.menu.Menu({
      items: this.context_menu
    });
    menu.on('hide', this.onContextHide, this);

    if(this.row_menu_items) {
      menu.add('-');
      for(var ii=0; ii < this.row_menu_items.length; ii++) {
        var action = this.actions[this.row_menu_items[ii]];
        menu.add(action);
      }
    }

    return menu;
  },

  addTopMenuItems: function() {
    var name;
    var end = this.top_menu_items.length;
    if (end > 0) {
      for (var ii = 0; ii < end; ii++) {
        name = this.top_menu_items[ii]
        if(name == '-' || name == '->') {
          this.tbar.add(name);
        } else {
          if(ii>0) { this.tbar.add('-'); }
          this.tbar.add(this.actions[name]);
        }
      }
    }
  },

  onContextClick : function(grid, index, e){
    if(!this.menu){
      this.menu = this.createContextMenu();
    }

    e.stopEvent();
    var sm = grid.getSelectionModel();
    if (sm.getSelected() != grid.getStore().getAt(index)) {
      grid.getSelectionModel().selectRow(index);
    }
    this.menu.showAt(e.getXY());
  },

  onContextHide : function(){
  },

  /*
   * Strictly a callback function so that instances of this class can add
   * additional classes to the row
   */
  updateRowClass: false,

  // within this function "this" is actually the GridView
  applyRowClass: function(record, rowIndex, p, ds) {
    var row_class = 'x-grid3-row-collapsed';
    if (this.view.showDescription && this.data.description) {
      var desc = '';
      var data = record.get(this.data.description);

      if (data) {
        if (this.data.renderer) {
          desc = this.data.renderer(data);
        } else {
          desc = Ext.util.Format.ellipsis(Ext.util.Format.stripTags(data), 400);
        }
      }
      if(this.data.adjust_description_cols) { p.cols += this.data.adjust_description_cols; }
      p.body = String.format('<p>{0}</p>', desc);
      row_class = 'x-grid3-row-expanded';
    }

    if(this.updateRowClass) {
      row_class += ' ' + this.updateRowClass(record);
    }
    return row_class;
  },

  toggleDescription : function(show){
    this.view.showDescription = show;
    this.view.refresh();
  },

  reload: function() {
    this.store.reload();
  }
});
Ext.reg('descriptiongrid', Ext.ux.DescriptionGrid);

Ext.ux.SearchField = Ext.extend(Ext.form.TwinTriggerField, {
    initComponent : function(){
      this.addEvents('search', 'clear');
      Ext.ux.SearchField.superclass.initComponent.call(this);
      this.on('specialkey', function(f, e){
          if(e.getKey() == e.ENTER){
              this.onTrigger2Click();
          }
      }, this);
    },

    validationEvent:false,
    validateOnBlur:false,
    trigger1Class:'x-form-clear-trigger',
    trigger2Class:'x-form-search-trigger',
    hideTrigger1:true,
    width:180,
    hasSearch : false,
    paramName : 'query',

    onTrigger1Click : function(){
      if(this.hasSearch){
        var o = {};
        o[this.paramName] = '';
        this.el.dom.value = '';
        this.triggers[0].hide();
        this.hasSearch = false;
        this.store.reload({params:o});
        this.fireEvent('clear', this);
      }
    },

    onTrigger2Click : function(){
      var v = this.getRawValue();
      if(v.length < 1){
          this.onTrigger1Click();
          return;
      }
      var o = {};
      o[this.paramName] = v;
      this.store.reload({params:o});
      this.hasSearch = true;
      this.triggers[0].show();
      this.fireEvent('search', this, v);
    }
});