
/*

  SmartClient Ajax RIA system
  Version v9.1p_2021-05-18/LGPL Deployment (2021-05-18)

  Copyright 2000 and beyond Isomorphic Software, Inc. All rights reserved.
  "SmartClient" is a trademark of Isomorphic Software, Inc.

  LICENSE NOTICE
     INSTALLATION OR USE OF THIS SOFTWARE INDICATES YOUR ACCEPTANCE OF
     ISOMORPHIC SOFTWARE LICENSE TERMS. If you have received this file
     without an accompanying Isomorphic Software license file, please
     contact licensing@isomorphic.com for details. Unauthorized copying and
     use of this software is a violation of international copyright law.

  DEVELOPMENT ONLY - DO NOT DEPLOY
     This software is provided for evaluation, training, and development
     purposes only. It may include supplementary components that are not
     licensed for deployment. The separate DEPLOY package for this release
     contains SmartClient components that are licensed for deployment.

  PROPRIETARY & PROTECTED MATERIAL
     This software contains proprietary materials that are protected by
     contract and intellectual property law. You are expressly prohibited
     from attempting to reverse engineer this software or modify this
     software for human readability.

  CONTACT ISOMORPHIC
     For more information regarding license rights and restrictions, or to
     report possible license violations, please contact Isomorphic Software
     by email (licensing@isomorphic.com) or web (www.isomorphic.com).

*/

if(window.isc&&window.isc.module_Core&&!window.isc.module_Grids){isc.module_Grids=1;isc._moduleStart=isc._Grids_start=(isc.timestamp?isc.timestamp():new Date().getTime());if(isc._moduleEnd&&(!isc.Log||(isc.Log && isc.Log.logIsDebugEnabled('loadTime')))){isc._pTM={ message:'Grids load/parse time: ' + (isc._moduleStart-isc._moduleEnd) + 'ms', category:'loadTime'};
if(isc.Log && isc.Log.logDebug)isc.Log.logDebug(isc._pTM.message,'loadTime');
else if(isc._preLog)isc._preLog[isc._preLog.length]=isc._pTM;
else isc._preLog=[isc._pTM]}isc.definingFramework=true;

if (window.isc && isc.version != "v9.1p_2021-05-18/LGPL Deployment") {
    isc.logWarn("SmartClient module version mismatch detected: This application is loading the core module from "
        + "SmartClient version '" + isc.version + "' and additional modules from 'v9.1p_2021-05-18/LGPL Deployment'. Mixing resources from different "
        + "SmartClient packages is not supported and may lead to unpredictable behavior. If you are deploying resources "
        + "from a single package you may need to clear your browser cache, or restart your browser."
        + (isc.Browser.isSGWT ? " SmartGWT developers may also need to clear the gwt-unitCache and run a GWT Compile." : ""));
}





//> @interface List
// An interface for an ordered collection of items.
// <P>
// This is the interface that is expected by list-oriented display components such as the
// ListGrid.  The JavaScript native Array object is retrofitted to support the List interface.
// Also, a valid List can be created by mixing the List interface into any class that supports:
// <ul>
// <li> for read-only support: get(position), getLength()
// <li> for modifiable support: set(position), addAt(object, position), removeAt(position)
// </ul>
// <P>
// NOTE: this interface is compatible with the java.util.List interface, except that:
// <ul>
// <li> for removal by index, removeAt() must be called instead of remove().  In Java, remove()
// is an overloaded method that takes either an int or Object, whereas in JavaScript, a
// Number is an Object.
// <li> Iterators don't exist.
// </ul>
// Some methods from the Java List interface have been omitted from the documentation to avoid
// redundancy.
//
// @treeLocation Client Reference/System
// @visibility external
//<
isc.ClassFactory.defineInterface("List");

// Read-only
// --------------------------------------------------------------------------------------------
// - basics that must be implemented
//   "get", "getLength",
//
// - routines that can be implemented in terms of basics
//   - trivial routines
//     "isEmpty", "itemIsPresent", "rangeIsPresent", "first", "last",
//   - routines to consider for a custom implementation for performance
//     "indexOf", "lastIndexOf", "contains", "containsAll", "intersect",
//     "getItems", "getRange", "duplicate",

// Modification
// --------------------------------------------------------------------------------------------
// - basics that must be implemented
//   "set", "addAt", "removeAt",
//   - NOTE: technically, set() and setLength() are sufficient as a minimal interface, however,
//     this means that addAt() and removeAt() would need to be implemented by
//     lengthening/shortening the List and then setting every slot beyond the modified point,
//     which seems like a very silly default implementation.

// - routines that can be implemented in terms of basics
//   - trivial routines
//     "add", "addList",
//   - routines to consider for a custom implementation for performance
//      "setLength", "addListAt", "remove", "removeList", "sort", "sortByProperty"

//   - notifications
//     "dataChanged", "_startChangingData", "_doneChangingData",




isc.List.addInterfaceMethods({

init : function () {
    if (!this.data) this.data = [];
},

// --------------------------------------------------------------------------------------------
// Read-only Interface
// --------------------------------------------------------------------------------------------

// Basics that must be implemented
// --------------------------------------------------------------------------------------------

//>    @method        list.get()
// Return the item at a particular position
// @group access
//
//        @param    pos    (Number)    position of the element to get
//
//        @return    (object)        whatever's at that position, undef if not found
// @visibility external
//<
// expected to be implemented by target

//>    @method        list.getLength()
// Return the number of items in this list
//
// @group access
//
//        @return    (Number)    number of items in the list
// @visibility external
//<
// expected to be implemented by target

// Implementable in terms of basics
// --------------------------------------------------------------------------------------------

//>    @method        list.isEmpty()
// Return whether or not this array is empty
//
// @group access
//        @return    (boolean)    true == this array is empty, false == some items in the array
// @visibility external
//<
// [stolen from Array]

//>    @method        list.first()
// Return the first item in this list
//
// @group access
//        @return    (any)    first item in the list
// @visibility external
//<
first : function () {
    return this.get(0);
},

//>    @method        list.last()
// Return the last item in this list
//
// @group access
//        @return    (any)    last item in the list
// @visibility external
//<
last : function () {
    return this.get(this.getLength()-1);
},

// Below might need custom implementations for performance
// --------------------------------------------------------------------------------------------

//>    @method        list.indexOf()
// Return the position in the list of the first instance of the specified object.
// <p>
// If pos is specified, starts looking after that position.
// <p>
// Returns -1 if not found.
//
// @group access
//        @param    obj         (any)        object to look for
//        @param    [pos]     (number)    earliest index to consider
//        @param    [endPos] (number)    last index to consider
//
//        @return    (number)            position of the item, if found, -1 if not found
// @visibility external
//<
indexOf : function (obj, pos, endPos) {
    // normalize position to the start of the list
    if (pos == null) pos = 0;
    if (endPos == null) endPos = this.getLength() - 1;

    for (var i = pos; i <= endPos; i++) {
        if (this.get(i) == obj) return i;
    }

    // not found -- return the not found flag
    return -1;
},

//>    @method        list.lastIndexOf()
// Return the position in the list of the last instance of the specified object.
// <p>
// If pos is specified, starts looking before that position.
// <p>
// Returns -1 if not found.
//
// @param    obj        (any)        object to look for
// @param    [pos]     (number)    last index to consider
// @param    [endPos] (number)    earliest index to consider
//
// @return    (number)            position of the item, if found, -1 if not found
//
// @group access
// @visibility external
//<
lastIndexOf : function (obj, pos, endPos) {
    // normalize position to the end of the list
    if (pos == null) pos = this.getLength() - 1;
    if (endPos == null) endPos = 0;

    for (var i = pos; i >= endPos; i--)
        if (this.get(i) == obj) return i;

    // not found -- return the not found flag
    return -1;
},

//>    @method        list.findIndex()
// Find the index of the first Object where property == value in the object.
// <P>
// Pass an Object instead to match multiple properties.
// <P>
// Note: for string values, matches are case sensitive.
//
// @param propertyName (String or Object or AdvancedCriteria) property to match, or if an Object is passed, set of
//                                        properties and values to match
// @param [value] (any) value to compare against (if propertyName is a string)
// @return (int) index of the first matching Object or -1 if not found
//
// @group access, find
// @visibility external
//<
// [stolen from Array]

//>    @method        list.findNextIndex()
// Like +link{findIndex()}, but inspects a range from startIndex to endIndex.
//
// @param startIndex (int) first index to consider
// @param propertyName (String or Object or AdvancedCriteria) property to match, or if an Object is passed, set of
//                                        properties and values to match
// @param [value] (any) value to compare against (if propertyName is a string)
// @param [endIndex] (int) last index to consider
// @return (int) index of the first matching Object or -1 if not found
//
// @group access, find
// @visibility external
//<

findNextIndex : function (start, property, value, endPos, getContext) {
    var length = this.getLength();
    if (start == null) start = 0;
    else if (start >= length) return -1;
    if (endPos == null) endPos = length - 1;
    if (property == null) return -1;

    if (isc.isA.String(property)) {
        // single property to match
        for (var i = start; i <= endPos; i++) {
            var item = this.get(i, getContext);
            if (item && item[property] == value) return i;
        }
        // return -1 if we didn't find the object
        return -1;
    } else {
        // "property" is an object specifying a set of properties to match
        return this.findNextMatch(property, start, endPos);
    }
},

//>    @method list.find()
// Like +link{findIndex()}, but returns the object itself instead of its index.
//
// @param propertyName (String or Object or AdvancedCriteria) property to match, or if an Object is passed, set of
//                                        properties and values to match
// @param [value] (any) value to compare against (if propertyName is a string)
// @return (Object) first matching object or null if not found
//
// @group access, find
// @visibility external
//<
// [stolen from Array]

//>    @method list.findAll()
// Find all objects where property == value in the object.
// <P>
// Pass an Object as the <code>propertyName</code> argument to match multiple properties.
//
// @param propertyName (String or Object or AdvancedCriteria) property to match, or if an Object is passed, set of
//                                        properties and values to match
// @param [value] (any) value to compare against (if propertyName is a string)
// @return (Array) all matching Objects or null if none found
//
// @group access, find
// @visibility external
//<
findAll : function (property, value) {

    if (property == null) return null;

    if (isc.isA.String(property)) {
        var matches = null,
            l = this.getLength()
        ;
        // single property to match
        for (var i = 0; i < l; i++) {
            var item = this.get(i);
            if (item && item[property] == value) {
                if (matches == null) matches = [];
                matches.add(item);
            }
        }
        return matches;
    } else {
        // "property" is an object specifying a set of properties to match
        return this.findAllMatches(property);
    }
},

//>    @method        list.contains()
// Return if this list contains the specified object.
// <P>
// If pos is specified, starts looking after that position.
//
// @group access
//        @param    obj        (any)        item to look for
//        @param    [pos]    (number)    optional position in the list to look after
//
//        @return    (boolean)    true == item was found, false == not found
// @visibility external
//<
// [stolen from Array]

//> @method     list.containsAll()
// Return whether this list contains all the item in the specified list.
//
// @group access
//      @param list     (List)      items to look for
//      @return (boolean)   whether all items were found
// @visibility external
//<
// [stolen from Array]

//>    @method        list.intersect()
// Return the list of items that are in both this list and the passed-in list(s).
//
//        @group    arrayMath
//
//        @param    lists    (all List arguments)        lists to intersect with
//        @return    (List)    intersection
// @visibility external
//<
// [stolen from Array]

//> @method     list.equals()
// Return whether this list is equal to another list.
// <P>
// Two lists are equal only if they have the same length and all contained items are in the same
// order and are also equal.
//
// @group access
//      @param list     (List)      list to check for equality
//      @return (boolean)   whether the specified list is equal to this list
// @visibility external
//<
// [stolen from Array]

//>    @method        list.getItems()
// Return the items at a list of specified positions.
//
// @group access
//        @param    itemList    (List of Number)        array of positions
//
//        @return    (array)        subset of the array, in the same order as itemList
// @visibility external
//<
// [stolen from Array]

//>    @method        list.getRange()
// Return the items between position start and end, non-inclusive at the end.
//
// @group access
//        @param    start    (number)    start position
//        @param    end        (number)    end position
//
//        @return    (Array)        subset of the array from start -&gt; end-1
// @visibility external
//<
getRange : function (start, end) {
    if (end == null) end = this.getLength() - 1;
    var output = [];
    for (var i = start; i < end; i++) {
        output[output.length] = this.get(i);
    }
    return output;
},

// see ResultSet.getCachedRow()
getCachedRow : function (rowNum) { return this.get(rowNum); },

//>    @method        list.duplicate()    (A)
// Return an Array that is a shallow copy of the list, that is, containing the same items.
//
// @group access
//        @return    (Array)        new array, pointing to the same items
// @visibility external
//<
duplicate : function () {
    return this.getClass().create().addList(this);
},

// --------------------------------------------------------------------------------------------
// Modification Interface
// --------------------------------------------------------------------------------------------

// Basics that must be implemented
// --------------------------------------------------------------------------------------------

//>    @method        list.set()
// Change the array element at a particular position.
// <P>
// set() can be used to expand the length of the list.
//
// @group modification
//        @param    pos    (number)    position in the list to change
//        @param    obj    (object)    new value for that position
//
//        @return    (object)        whatever's at that position, null if not found
// @visibility external
//<
// expected to be implemented by target

//>    @method        list.addAt()
// Add a single item to this array at a specific position in the list, sliding other items over
// to fit.
//
// @group modification
//        @param        obj    (object)    object to add
//        @param        pos    (number)    position in the list to add at
//
//        @return            (object)    object that was added
// @visibility external
//<
// expected to be implemented by target

//>    @method        list.removeAt()
// Remove the item at the specified position, rearranging all subsequent items to fill the gap
//
// @group modification
//        @param    pos    (number)    position to remove
//
//        @return    (any)    item that was removed
// @visibility external
//<
// expected to be implemented by target

// Implementable in terms of basics
// --------------------------------------------------------------------------------------------

//>    @method        list.add()
// Add an object to this list, at the end
//
// @group modification
//        @param    object    (any)    object to add
//
//        @return    (any)            pointer to the object passed in
// @visibility external
//<
// As implemented below, if no position is specified, the object will be added to the end of
// the list.
// Note that array.add will attempt to preserve sort order if it is currently sorted when
// add() is called with no explicit position param.

add : function (object, secondArg) {
    var undefined;
    if (secondArg !== undefined) {
        // support calling as add(object, index)
        return this.addAt(object, secondArg);
    }
    this.addAt(object, this.getLength());

    // return the object that was added
    return object;
},

//>    @method        list.addList()
// Add a list of items to this array.
// <P>
// Note: you can specify that a subset range be added by passing start and end indices
//
// @group modification
//        @param    list    (array)        list of items to add
//        @param    [listStartRow]    (number)    optional start index in list
//        @param    [listEndRow]    (number)    optional end index in list (non-inclusive)
//
//        @return    (list)                list of items that were added
// @visibility external
//<
// [stolen from Array]

// Below might need custom implementations for performance
// --------------------------------------------------------------------------------------------

//>    @method        list.setLength()
// Set the length of this list.
// <P>
// If the length of the list is shortened, any elements past the new length of the list are removed.
// If the length is increased, all positions past the old length have the value
// <code>undefined</code>.
//
// @group modification
//        @param    length    (number)    new length
// @visibility external
//<
setLength : function (length) {
    this._startChangingData();
    if (length > this.getLength()) {
        // pad the list with empty slots
        var undefined;
        while (length > this.getLength()) this.add(undefined);
    } else {
        // remove everything beyond the specified length
        while (length < this.getLength()) this.removeAt(this.getLength()-1);
    }
    this._doneChangingData();
},

// Below methods need to shift indices of all existing items
// --------------------------------------------------------------------------------------------

//>    @method        list.addListAt()
// Add list of items list to this array at item pos.  All items after array[pos] will slide down to
// fit new items.
//
// @group modification
//        @param    list    (array)        new array of items
//        @param    pos        (number)    position in this list to put the new items
//
//        @return    (array)        the list of items that was added
// @visibility external
//<
addListAt : function (list, pos) {
    this._startChangingData();

    var length = list.getLength();
    for (var i = 0; i < length; i++) {
        this.addAt(list.get(i), pos+i);
    }

    this._doneChangingData();

    // return the list that was added
    return list;
},

//>    @method        list.remove()
// Remove first instance of the passed object from this array, sliding other items around to
// fill gaps.
//
// @group modification
// @param obj (any) item to remove
//
// @return (boolean) true if a matching object was found and removed, false if no matching
// object was found and the list remains unchanged.
// @visibility external
//<
remove : function (obj) {

    // return removed item, per java.util.List
    // if (isc.isA.Number(obj)) return this.removeAt(obj);

    var index = this.indexOf(obj);
    if (index == -1) return false;

    this._startChangingData();

    var length = this.getLength();
    for (var i = index; i < length; i++) this.set(i, this.get(i+1));
    this.setLength(length-1);

    this._doneChangingData();

    return true; // indicating object was removed, per java.util.Collection
},

//>    @method        list.removeList()
// Remove all instances of objects in the specified list from this list, sliding the remaining
// objects around to fill gaps.
//
// @group modification
//        @param    list    (array)        list of items to remove
//
//        @return    (list)    list of items passed in
// @visibility external
//<
removeList : function (removeList) {
    if (removeList == null) return null;

    // get ready to change data...
    this._startChangingData();

    var changed = false;
    for (var i = 0; i < this.getLength(); i++) {
        var item = this.get(i);

        // remove the current item and stay at the same position in the list
        if (removeList.contains(item)) {
            changed = true;
            this.removeAt(i);
            i--;
        }
    }
    this._doneChangingData();

    // return whether the list was changed
    return removeList;
},



//> @method    list.sort()
// Sorts the elements of the List in place.
// <P>
// The optional comparator function should take two parameters "a" and "b" which are the two list
// items to compare, and should return:
// <ul>
// <li> a value less than zero, if "a" is less than "b" such that "a" should appear earlier in the
//      list
// <li> zero, if "a" and "b" are equal
// <li> a value greater than zero, if "a" is greater than "b" such that "b" should appear earlier in
//      the list
// </ul>
//
//     @param    [comparator]  (function) comparator function to use
//     @return   (List)                   the list itself
// @visibility external
//<
sort : function (comparator) {
    // dump all the items to a native Array and sort them
    var items = this.getRange(0, this.getLength());
    items.sort(comparator);

    // then set every slot in the current List
    for (var i = 0; i < items.length; i++) this.set(i, items[i]);

    return this;
},

//>    @method list.getProperty()
// Return a new Array where the value of item i is the value of "property" of item i in this
// array.  If an item doesn't have that property or is null, return item will be null.
//
// @param property (string)    name of the property to look for
//
// @return (Array) array of the values of property in each item of this list
// @group iteration
// @visibility external
//<
getProperty : function (property) {
    var values = [];
    // then set every slot in the current List
    for (var i = 0; i < this.getLength(); i++) {
        var item = this.get(i);
        values[i] = item != null ? item[property] : null;
    }
    return values;
},


//>    @method        list.sortByProperty()
// Sort a list of objects by a given property of each item.
// <P>
// The optional normalizer, if passed as a function, is called for each item in the List, and
// should return whatever value should be used for sorting, which does not have to agree with
// the property value. By passing a normalizer function you can achieve any kind of sorting
// you'd like, including sorting by multiple properties.
// <P>
// NOTE: string sort is case INsensitive by default
//
//        @group    sorting
//
//        @param    property      (string)    name of the property to sort by
//        @param    up              (boolean)    true == sort ascending, false == sort descending
//        @param    [normalizer] (function or ValueMap)
//              May be specified as a function, with signature
//              <code>normalize(item, propertyName, context)</code>, where <code>item</code> is
//              a pointer to the item in the array, <code>propertyName</code> is the
//              property by which the array is being sorted, and <code>context</code> is the
//              arbitrary context passed into this method. Normalizer function should return
//              the value normalized for sorting.<br>
//              May also be specified as a ValueMap which maps property values to sortable values.
//      @param [context] (any) Callers may pass an arbitrary context into the sort method, which
//                          will then be made available to the normalizer function
//      @return (List) the list itself
//
// @visibility external
//<
sortByProperty : function (property, direction, normalizer, context) {
    // dump all the items to a native Array and sort them
    var items = this.getRange(0, this.getLength());
    items.sortByProperty(property, direction, normalizer, context);

    // then set every slot in the current List
    for (var i = 0; i < items.length; i++) this.set(i, items[i]);

    return this;
},


//> @method list.getValueMap()
// Get a map of the form <code>{ item[idField] -&gt; item[displayField] }</code>, for all
// items in the list.  Note that if more than one item has the same <code>idProperty</code>,
// the value for the later item in the list will clobber the value for the earlier item.
//
// @param idField (string)  Property to use as ID (data value) in the valueMap
// @param displayField (string) Property to use a display value in the valueMap
// @return (object) valueMap object
// @visibility external
//<
// imported as part of isc._stealArrayMethods


// DataChanged notification
// --------------------------------------------------------------------------------------------

//>    @method        list.dataChanged()    (A)
// Method called when this array changes in some way.  Observe the method to react to changes in
// this list.
// <P>
// Note: dataChanged() will only fire when items are added, removed or rearranged.  If a list
// contains objects, dataChanged() will not fire if changes are made to objects within the list
// without changing their position within the list.  If an observer of dataChanged() needs to react
// to such a change, you can manually fire dataChanged() by simply calling it.
// <P>
// Note: may be called multiple times as the result of a multi-item add or remove, etc.
//
// @group modification
// @visibility external
//<
dataChanged : function () {

    if (this.onDataChanged) this.onDataChanged()
}

//>    @method        list._startChangingData()    (A)
//            Internal method to indicate that data will be changed within the context of a function.
//            Each occurance of a call to this method should be matched with an occurance of
//            _doneChangingData() -- when they balance, the public dataChanged() method will be called
//            exactly once.  This lets observers of the dataChanged() method only get called once for
//            a set of changes.
//
//            For example, clearRange() calls clearItem() repeatedly; we don't want each of
//             these "nested" calls to clearItem to generate a dataChanged message,
//             (we only want one when clearRange is done).  However, clearItem() when called by
//             itself (outside of any higher-level operation) *should* call dataChanged when it
//             is done.
//
//<
// [stolen from Array]

//>    @method        list._doneChangingData()    (A)
//            Internal method to indicate that we're done changing data in the current scope.
//            See list._startChangingData()
//<
// [stolen from Array]

});

// steal methods from Array
isc._stealArrayMethods = function () {
    var methodList = [
                      // these are internal helpers only
                      "containsSubstring", "containsAllSubstring", "intersectDates", "intersectSubstring",
                      // methods that are implemented using only the List API on Array (because
                      // performance difference doesn't matter)
                      "isEmpty", "contains", "containsAll", "intersect", "equals",
                      "getItems", "addList", "getValueMap", "removeEvery",
                      "_startChangingData", "_doneChangingData", "_isChangingData",
                      // old ISC backcompat
                      "getItem", "setItem", "removeItem", "clearAll",
                      // find
                      "find", "findIndex", "findAllIndices", "findNextMatch", "findAllMatches", "findByKeys",
                      // Java.util.List compat
                      "size", "subList", "addAll", "removeAll", "clear"];
    // NOTE: applyMask won't work here, since the input is an Array instance, which is
    // ambiguous with passing an Array full of objects to mask.
    var methods = {};
    for (var i = 0; i < methodList.length; i++) {
        var methodName = methodList[i];
        methods[methodName] = Array.prototype[methodName];
    }

    isc.List.addInterfaceMethods(methods);
}
isc._stealArrayMethods();


// Override isA.List to return true for arrays as well as lists
// we have to do this here after the List interface has been defined...
isc.addMethods(isc.isA, {
//> @classMethod isA.List()
// Does <code>object</code> implement the  <code>List</code> interface?
// @param   object  (object)    object to test
// @return (boolean) <code>true</code> if the object is an Array or belongs to another class that
//                   implements the <code>List</code> API.
// @visibility external
//<
_$List:"List",
List : function (object) {
    if (object == null) return false;
    if (isc.isA.Array(object)) return true;
    // standard implementation for objects inheriting from interfaces/classes
    return object.isA && object.isA(this._$List);
}

});










//>    @class    Tree
//
// A Tree is a data model representing a set of objects linked into a hierarchy.
// <P>
// A Tree has no visual presentation, it is displayed by a +link{TreeGrid} or +link{ColumnTree}
// when supplied as +link{treeGrid.data} or +link{columnTree.data}.
// <P>
// A Tree can be constructed out of a List of objects interlinked by IDs or via explicitly
// specified Arrays of child objects.  See +link{attr:Tree.modelType} for an explanation of how
// to pass data to a Tree.
// <P>
// Typical usage is to call +link{treeGrid.fetchData()} to cause automatic creation of a
// +link{ResultTree}, which is a type of Tree that automatically handles loading data on
// demand.  For information on DataBinding Trees, see +link{group:treeDataBinding}.
//
// @implements List
// @treeLocation Client Reference/System
// @visibility external
//<
isc.ClassFactory.defineClass("Tree", null, "List");

// List.getProperty() needs to be explicitly installed because there is a Class.getProperty()
isc.Tree.addProperties({
    getProperty : isc.List.getInstanceProperty("getProperty")
})

//> @groupDef ancestry
// Parent/child relationships
//<

//> @groupDef openList
// Managing the list of currently visible nodes based on the open state of parents
// <P>
// This state may move to the TreeGrid
// @visibility internal
//<

isc.Tree.addClassProperties({

//>    @type    DisplayNodeType
//
// Flag passed to functions as displayNodeType, telling the function whether it should work on
// folders, leaves or both at once.
//        @group    ancestry
// @visibility external
//
//    @value    null/unset                      operate on both folders and leaves
FOLDERS_AND_LEAVES:null,
//     @value    "folders"                       operate on folders only, ignoring leaves
FOLDERS_ONLY: "folders",
//    @value    "leaves"                        operate on leaves only, ignoring folders
LEAVES_ONLY: "leaves",
//<

//>    @type    LoadState
// Trees that dynamically load nodes keep track of whether each node has loaded its children.
//
//    @value    isc.Tree.UNLOADED                    children have not been loaded and are not loading
UNLOADED: "unloaded",
//    @value    isc.Tree.LOADING                    currently in the process of loading
LOADING: "loading",
//    @value    isc.Tree.FOLDERS_LOADED                folders only are already loaded
FOLDERS_LOADED: "foldersLoaded",
//    @value    isc.Tree.LOADED                        already fully loaded
LOADED: "loaded",
//    @value    isc.Tree.LOADED_PARTIAL_CHILDREN    children form a ResultSet having only a partial
//                                                cache (applies only to the "paged"
//                                                +link{resultTree.fetchMode,fetchMode})
LOADED_PARTIAL_CHILDREN: "loadedPartialChildren",
// @group loadState
// @visibility external
//<

//> @type TreeModelType
//
// @value "parent" In this model, each node has an ID unique across the whole tree and a
// parent ID that points to its parent.  The name of the unique ID property can be specified
// via +link{attr:Tree.idField} and the name of the parent ID property can be specified via
// +link{attr:Tree.parentIdField}.  The initial set of nodes can be passed in as a list to
// +link{attr:Tree.data} and also added as a list later via +link{method:Tree.linkNodes}.
// Whether or not a given node is a folder is determined by the value of the property specified
// by +link{attr:Tree.isFolderProperty}.
// <br><br>
// The "parent" modelType is best for integrating with relational storage (because nodes can
// map easily to rows in a table) and collections of Beans and is the model used for DataBound
// trees.
PARENT:"parent",
//
// @value "children" In this model, nodes specify their children as a list of nodes.  The
// property that holds the children nodes is determined by +link{attr:Tree.childrenProperty}.
// Nodes are not required to have an ID that is unique across the whole tree (in fact, no ID is
// required at all).  Node names (specified by the +link{attr:Tree.nameProperty}, unique within
// their siblings, are optional but not required.  Whether or not a given node is a folder is
// determined by the presence of the children list (+link{attr:Tree.childrenProperty}).
CHILDREN:"children",
//
// @visibility external
//<

//> @type TreeFilterMode
// Mode for applying criteria to a tree.
// @value "strict" only nodes that actually match criteria are shown.  If a parent does not
//                 match the criteria, it will not be shown, even if it has children that do
//                 match the criteria
STRICT:"strict",
// @value "keepParents" parent nodes are kept if they have children which match the criteria,
//                      or, in a tree with
//                      +link{resultTree.loadDataOnDemand,loadDataOnDemand:true}, if they have
//                      not loaded children yet.
KEEP_PARENTS:"keepParents",
// @group treeFilter
// @visibility external
//<

autoID: 0

});


//
//    add instance defaults to the tree
//
isc.Tree.addProperties({

//> @attr tree.modelType        (TreeModelType: "children" : IRWA)
//
// Selects the model used to construct the tree representation.  See +link{TreeModelType} for
// the available options and their implications.
// <P>
// If the "parent" modelType is used, you can provide the initial parent-linked data set to the
// tree via the +link{attr:Tree.data} attribute.  If the "children" modelType is used, you can
// provide the initial tree structure to the Tree via the +link{attr:Tree.root} attribute.
//
// @see attr:Tree.data
// @see attr:Tree.root
//
// @visibility external
// @example nodeTitles
//<
modelType: "children",

//> @attr tree.isFolderProperty     (String: "isFolder": IRW)
//
// Name of property that defines whether a node is a folder.  By default this is set to
// +link{TreeNode.isFolder}.
//
// @see TreeNode.isFolder
// @visibility external
//<
isFolderProperty: "isFolder",

//> @attr tree.defaultIsFolder (boolean : null : IR)
// Controls whether nodes are assumed to be folders or leaves by default.
// <P>
// Nodes that have children or have the +link{isFolderProperty} set to true will be considered
// folders by default.  Other nodes will be considered folders or leaves by default according
// to this setting.
//
// @see treeGrid.loadDataOnDemand
// @visibility external
//<

//> @attr tree.reportCollisions (Boolean : true : IR)
// If new nodes are added to a tree with modelType:"parent" which have the same
// +link{tree.idField,id field value} as existing nodes, the existing nodes are removed when
// the new nodes are added.
// <P>
// If reportCollisions is true, the Tree will log a warning in the developer console about this.
// <P>
// Note that if an id collision occurs between a new node and its ancestor, the ancestor will be
// removed and the new node will not be added to the tree.
// @visibility external
//<
reportCollisions:true,



//>    @attr    tree.autoSetupParentLinks        (boolean : true : IRWA)
//            true == we should automatically set up links
//             from nodes to their parents at init() time
//
//        @see    tree.init()
//<
autoSetupParentLinks:true,

//>    @attr    tree.pathDelim        (string : "/" : IRWA)
//
// Specifies the delimiter between node names.  The pathDelim is used to construct a unique
// path to each node. A path can be obtained for any node by calling
// +link{method:Tree.getPath} and can be used to find any node in the tree by calling
// +link{method:Tree.find}.  Note that you can also hand-construct a path - in other words
// you are not required to call +link{method:Tree.getPath} in order to later use
// +link{method:Tree.find} to retrieve it.
// <br><br>
// The pathDelim can be any character or sequence of characters, but must be a unique string
// with respect to the text that can appear in the +link{attr:Tree.nameProperty} that's used
// for naming the nodes.  So for example, if you have the following tree:
// <pre>
// one
//   two
//     three/four
// </pre>
// Then you will be unable to find the <code>three/four</code> node using
// +link{method:Tree.find} if your tree is using the default pathDelim of /.
// In such a case, you can use a different pathDelim for the tree.  For example if you used |
// for the path delim, then you can find the <code>three/four</code> node in the tree above by
// calling <code>tree.find("one|two|three/four")</code>.
// <br><br>
// The pathDelim is used only by +link{method:Tree.getPath} and +link{method:Tree.find} and
// does not affect any aspect of the tree structure or other forms of tree navigation (such as
// via +link{method:Tree.getChildren}).
//
// @see attr:Tree.nameProperty
// @see method:Tree.find
// @visibility external
//<
pathDelim:"/",

// not documented:
// parentProperty : always generated, // direct pointer to parent node

treeProperty : "_isc_tree", // internal property pointing back to the origin tree

//>    @attr tree.nameProperty     (string : "name" : IRW)
//
// Name of the property on a +link{TreeNode} that holds a name for the node that is unique
// among its immediate siblings, thus allowing a unique path to be used to identify the node,
// similar to a file system.  Default value is "name".  See +link{TreeNode.name} for usage.
//
// @see TreeNode.name
// @visibility external
// @example nodeTitles
//<
nameProperty:"name",

//>    @attr tree.titleProperty    (string : "title" : IRW)
//
// Name of the property on a +link{TreeNode} that holds the title of the node as it should be
// shown to the user.  Default value is "title".  See +link{TreeNode.title} for usage.
//
// @visibility external
//<
titleProperty:"title",

//> @attr tree.idField    (string : "id" : IRA)
//
// Name of the property on a +link{TreeNode} that holds an id for the node which is unique
// across the entire Tree.  Required for all nodes for trees with modelType "parent".
// Default value is "id".  See +link{TreeNode.id} for usage.
//
// @see TreeNode.id
// @visibility external
// @example nodeTitles
//<

//> @attr tree.parentIdField (string : "parentId" : IRA)
//
// For trees with modelType "parent", this property specifies the name of the property
// that contains the unique parent ID of a node.  Default value is "parentId".  See
// +link{TreeNode.parentId} for usage.
//
// @see TreeNode.parentId
// @visibility external
// @example nodeTitles
//<

//>    @attr    tree.childrenProperty    (string : "children" : IRW)
//
// For trees with the modelType "children", this property specifies the name of the property
// that contains the list of children for a node.
//
// @see attr:Tree.modelType
// @visibility external
// @example childrenArrays
//<
childrenProperty:"children",

//>    @attr    tree.openProperty    (string : null : IRWA)
//
// The property consulted by the default implementation of +link{Tree.isOpen()} to determine if the
// node is open or not.  By default, this property is auto-generated for you, but you can set
// it to a custom value if you want to declaratively specify this state, but be careful - if
// you display this Tree in multiple TreeGrids at the same time, the open state will not be
// tracked independently - see +link{group:sharingNodes} for more info on this.
//
// @group    openList
// @see group:sharingNodes
// @visibility external
// @example initialData
//<

//>    @attr    tree.cacheOpenList    (boolean : true : IRWA)
//        @group    openList
//            If true, we cache the open list and only recalculate it
//            if the tree has been marked as dirty.  If false, we get the openList
//            every time.
//<
cacheOpenList:true,

//>    @attr    tree.openListCriteria    (string|function : null : IRWA)
//        @group    openList
//            Criteria for whether or not nodes are included in the openList
//<


//> @attr tree.data             (List of TreeNode : null : IR)
//
// Optional initial data for the tree. How this data is interpreted depends on this tree's
// +link{tree.modelType}.
// <P>
// If <code>modelType</code> is <code>"parent"</code>, the list that you provide will be passed
// to +link{method:Tree.linkNodes}, integrating the nodes into the tree.
// <p>
// In this case the root node may be supplied explicitly via +link{Tree.root}, or auto generated,
// picking up its <code>id</code> via +link{Tree.rootValue}. Any nodes in the data with no
// explicitly specified +link{treeNode.parentId} will be added as children to this root element.
// <P>
// To create this tree:
// <pre>
// foo
//   bar
// zoo
// </pre>
// with modelType:"parent", you can do this:
// <pre>
// Tree.create({
//   data: [
//     {name: "foo", id: "foo"},
//     {name: "bar", id: "bar", parentId: "foo"},
//     {name: "zoo", id: "zoo"}
// });
// </pre>
// Or this (explicitly specified root):
// <pre>
// Tree.create({
//   root: {id: "root"},
//   data: [
//     {name: "foo", id: "foo", parentId: "root"},
//     {name: "bar", id: "bar", parentId: "foo"},
//     {name: "zoo", id: "zoo", parentId: "root"}
// });
// </pre>
// Or this (explicitly specified rootValue):
// <pre>
// Tree.create({
//   rootValue: "root",
//   data: [
//     {name: "foo", id: "foo", parentId: "root"},
//     {name: "bar", id: "bar", parentId: "foo"},
//     {name: "zoo", id: "zoo", parentId: "root"}
// });
// </pre>
// Specifying the root node explicitly allows you to give it a name, changing the way path
// derivation works (see +link{Tree.root} for more on naming the root node).
// <P>
// For <code>modelType:"children"</code> trees, the data passed in will be assumed to be an
// array of children of the tree's root node.
//
// @see attr:Tree.modelType
// @see TreeNode
// @visibility external
// @example nodeTitles
//<

//> @attr tree.rootValue             (string|number : null : IR)
//
// If you are using the "parent" modelType and did not specify a root node via +link{Tree.root}
// with an id (+link{Tree.idField}), then you can provide the root node's id via this property.
// See the example in +link{Tree.data} for more info.
//
// @see Tree.data
// @visibility external
// @example nodeTitles
//<

//>    @attr    tree.root        (TreeNode : null : IRW)
//
// If you're using the "parent" modelType, you can provide the root node configuration via this
// property.  If you don't provide it, one will be auto-created for you with an empty name.
// Read on for a description of what omitting the name property on the root node means for path
// derivation.
// <p>
// If you're using the "children" modelType, you can provide the initial tree data via this
// property.  So, for example, to construct the following tree:
// <pre>
// foo
//   bar
// zoo
// </pre>
// You would initialize the tree as follows:
// <pre>
// Tree.create({
//     root: { name:"root", children: [
//         { name:"foo", children: [
//             { name: "bar" }
//         ]},
//         { name: "zoo" }
//     ]}
// });
// </pre>
// Note that if you provide a <code>name</code> property for the root node, then the path to
// any node underneath it will start with that name.  So in the example above, the path to the
// <code>bar</code> node would be <code>root/foo/bar</code> (assuming you're using the default
// +link{attr:Tree.pathDelim}.  If you omit the name attribute on the root node, then its name
// is automatically set to the +link{attr:Tree.pathDelim} value.  So in the example above, if
// you omitted <code>name:"root"</code>, then the path to the <code>bar</code> node would be
// <code>/foo/bar</code>.
// <br><br>
// Note: if you initialize a Tree with no <code>root</code> value, a root node will be
// auto-created for you.  You can then call +link{method:Tree.add} to construct the tree.
//
// @see Tree.modelType
// @see Tree.setRoot()
//
// @visibility external
// @example childrenArrays
//<

//discardParentlessNodes

//> @attr tree.discardParentlessNodes (Boolean : false : IRA)
// If this tree has +link{Tree.modelType,modelType:"parent"}, should nodes in the data array for the
// tree be dropped if they have an explicitly specified value for the +link{attr:Tree.parentIdField}
// which doesn't match any other nodes in the tree. If set to false these nodes will be added as
// children of the root node.
// @visibility external
//<
discardParentlessNodes:false,

//> @attr Tree.indexByLevel (boolean : false : IR)
// If enabled, the tree keeps an index of nodes by level, so that +link{tree.getLevelNodes()}
// can operate more efficiently
//<
indexByLevel: false,

//> @object TreeNode
//
// Every node in the tree is represented by a TreeNode object which is an object literal with a
// set of properties that configure the node.
// <p>
// When a Tree is supplied as +link{TreeGrid.data} to +link{TreeGrid}, you can also set
// properties from +link{ListGridRecord} on the TreeNode (e.g. setting
// +link{ListGridRecord.enabled}:<code>false</code> on the node).
//
// @treeLocation Client Reference/Grids/TreeGrid
// @treeLocation Client Reference/System/Tree
// @visibility external
//<


//> @attr treeNode.enabled  (boolean : null : IR)
// @include ListGridRecord.enabled
// @visibility external
//<

//> @attr treeNode.canDrag  (boolean : null : IRA)
// Governs whether this node can be dragged. Only has an effect if this node is displayed in
// a +link{TreeGrid} where +link{TreeGrid.canDragRecordsOut}, +link{TreeGrid.canReorderRecords}
// or +link{TreeGrid.canReparentNodes} is <code>true</code>.
// @visibility external
//<

//> @attr treeNode.canAcceptDrop (boolean : null : IRA)
//
// Governs whether dragged data (typically other <code>treeNode</code>s) may be dropped over
// this node. Only has an effect if this node is displayed in a +link{TreeGrid} where
// +link{TreeGrid.canAcceptDroppedRecords}, +link{TreeGrid.canReorderRecords} or
// +link{TreeGrid.canReparentNodes} is true.
//
// @visibility external
//<

//> @attr treeNode.isFolder (Boolean or String : null : IR)
//
// Set to <code>true</code> or a string that is not equal to (ignoring case)
// <code>"false"</code> to explicitly mark this node as a folder.  See +link{Tree.isFolder} for
// a full description of how the +link{Tree} determines whether a node is a folder or not.
// <p>
// Note: the name of this property can be changed by setting +link{Tree.isFolderProperty}.
//
// @see Tree.isFolderProperty
// @visibility external
//<

//> @attr treeNode.name (String : null, but see below : IR)
//
// Provides a name for the node that is unique among its immediate siblings, thus allowing a
// unique path to be used to identify the node, similar to a file system.  See
// +link{Tree.getPath()}.
// <p>
// If the nameProperty is not set on a given node, the +link{TreeNode.id} will be used instead.  If
// this is also missing, +link{tree.getName()} and +link{tree.getPath()} will auto-generate a
// unique name for you.  Thus names are not required, but if the dataset you are using already
// has usable names for each node, using them can make APIs such as +link{tree.find()} more
// useful.  Alternatively, if your dataset has unique ids consider providing those as
// +link{TreeNode.id}.
// <P>
// If a value provided for the nameProperty of a node (e.g. node.name) is not a
// string, it will be converted to a string by the Tree via ""+value.
// <p>
// This property is also used as the default title for the node (see +link{Tree.getTitle()})
// if +link{TreeNode.title} is not specified.
// <p>
// Note: the name of this property can be changed by setting +link{Tree.nameProperty}.
//
// @see Tree.nameProperty
// @see Tree.pathDelim
// @see Tree.getPath
// @see Tree.getTitle
// @visibility external
//<

//> @attr treeNode.title (HTML : null : IR)
//
// The title of the node as it should appear next to the node icon in the +link{Tree}.  If left
// unset, the value of +link{TreeNode.name} is used by default.  See the description in
// +link{Tree.getTitle()} for full details.
// <p>
// Note: the name of this property can be changed by setting +link{Tree.titleProperty}.
//
// @see Tree.titleProperty
// @see Tree.getTitle()
// @visibility external
//<

//> @attr treeNode.id (String or Number: null : IR)
//
// Specifies the unique ID of this node.
// <P>
// Required for trees with +link{Tree.modelType} "parent".  With modelType:"parent", the unique
// ID of a node, together with the unique ID of its parent (see +link{TreeNode.parentId}) is
// used by +link{Tree.linkNodes} to link a list of nodes into a tree.
// <p>
// Note: the name of this property can be changed by setting +link{Tree.idField}.
//
// @see TreeNode.parentId
// @see Tree.linkNodes()
// @see Tree.modelType
// @see Tree.idField
// @visibility external
//<

//> @attr treeNode.parentId (String or Number : null : IR)
//
// For trees with modelType:"parent", this property specifies the unique ID of this node's
// parent node.
// The unique ID of a node, together with the unique ID of its parent is used by
// +link{method:Tree.linkNodes} to link a list of nodes into a tree.
// <p>
// Note: the name of this property can be changed by setting +link{Tree.parentIdField}.
//
// @see TreeNode.id
// @see Tree.linkNodes()
// @see Tree.modelType
// @see Tree.parentIdField
// @visibility external
//<

//> @attr treeNode.children (List of TreeNode : null : IRW)
//
// For trees with the modelType "children", this property specifies the children of this
// TreeNode.
// <p>
// Note: the name of this property can be changed by setting +link{Tree.childrenProperty}
//
// @see Tree.modelType
// @see Tree.childrenProperty
// @visibility external
//<

//> @attr   treeNode.icon   (SCImgURL : null : [IRW])
// This Property allows the developer to customize the icon displayed next to a node.
// Set <code>node.icon</code> to the URL of the desired icon to display and
// it will be shown instead of the standard +link{treeGrid.nodeIcon} for this node.<br>
// Note that if +link{TreeNode.showOpenIcon} and/or +link{TreeNode.showDropIcon}
// is true for this node, customized icons for folder nodes will be appended with the
// +link{treeGrid.openIconSuffix} or +link{treeGrid.dropIconSuffix} suffixes on state change
// as with the standard +link{TreeGrid.folderIcon} for this treeGrid.  Also note that for
// custom folder icons, the +link{treeGrid.closedIconSuffix} will never be appended.
// <P>You can change the name of this property by setting
// +link{TreeGrid.customIconProperty}.
// @group treeIcons
// @visibility external
//<

//> @attr   treeNode.showOpenIcon (Boolean : false : [IRWA])
// For folder nodes showing custom icons (set via +link{treeNode.icon}),
// this property allows the developer to specify on a per-node basis whether an
// open state icon should be displayed when the folder is open.
// Set <code>node.showOpenIcon</code> to true to show the open state
// icons, or false to suppress this.<br>
// If not specified, this behavior is determined by +link{TreeGrid.showCustomIconOpen}
// for this node.
// <P>You can change the name of this property by setting
// +link{TreeGrid.customIconOpenProperty}.
// @see treeGrid.customIconProperty
// @see treeGrid.showCustomIconOpen
// @visibility external
// @group treeIcons
//<
showOpenIcon: false,

//> @attr   treeNode.showDropIcon (Boolean : false : [IRWA])
// For folder nodes showing custom icons (set via +link{treeNode.icon}),
// this property allows the developer to specify on a per-node basis whether a
// drop state icon should be displayed when the
// user drop-hovers over this folder.<br>
// Set <code>node.showDropIcon</code> to true to show the drop state
// icon, or false to suppress this.<br>
// If not specified, this behavior is determined by +link{treeGrid.showCustomIconDrop}
// for this node.
// <P>You can change the name of this property by setting
// +link{TreeGrid.customIconDropProperty}.
// @see treeGrid.customIconProperty
// @see treeGrid.showCustomIconDrop
// @visibility external
// @group treeIcons
//<
showDropIcon: false,


//>    @attr    tree.sortProp            (string : null : IRW)
//        @group    openList
//            Name of the property to sort by.
//            Set to null because we don't sort by default.
//<


//>    @attr    tree.sortDirection                (SortDirection : "ascending" : IRW)
//            Sort ascending by default
//<
sortDirection: "ascending",

//>    @attr tree.showRoot (Boolean : false : IRW)
// Controls whether the implicit root node is returned as part of the visible tree,
// specifically, whether it is returned in +link{getOpenList()}, which is the API view
// components typically use to get the list of visible nodes.
// <p>
// Default is to have the root node be implicit and not included in the open list, which means
// that the visible tree begins with the children of root.  This allows multiple nodes to
// appear at the top level of the tree.
// <P>
// You can set <code>showRoot:true</code> to show the single, logical root node as the only
// top-level node.  This property is only meaningful for Trees where you supplied a value for
// +link{Tree.root}, otherwise, you will see an automatically generated root node that is
// meaningless to the user.
//
// @visibility external
//<
showRoot: false,

//>    @attr tree.autoOpenRoot            (Boolean : true : IRW)
//
// If true, the root node is automatically opened when the tree is created or
// +link{Tree.setRoot()} is called.
//
// @visibility external
//<
autoOpenRoot: true,

//>    @attr tree.separateFolders    (Boolean : false : IRW)
// Should folders be sorted separately from leaves or should nodes be ordered according to
// their sort field value regardless of whether the node is a leaf or folder?
// @see tree.sortFoldersBeforeLeaves
// @visibility external
//<
separateFolders:false,

//>    @attr tree.sortFoldersBeforeLeaves (Boolean : true : IRW)
// If +link{tree.separateFolders} is true, should folders be displayed above or below leaves?
// When set to <code>true</code> folders will appear above leaves when the
// <code>sortDirection</code> applied to the tree is +link{type:SortDirection,"ascending"}
// @visibility external
//<
sortFoldersBeforeLeaves:true,

//>    @attr tree.defaultNodeTitle (string : "Untitled" : IRW)
//
// Title assigned to nodes without a +link{attr:Tree.titleProperty} value or a
// +link{attr:Tree.nameProperty} value.
//
// @visibility external
//<
defaultNodeTitle:"Untitled",

//>    @attr tree.defaultLoadState (LoadState : isc.Tree.UNLOADED : IRW)
//        @group    loadState
//            default load state for nodes where is has not been explicitly set
//<
// ResultTree defines a setter for this property.
defaultLoadState: isc.Tree.UNLOADED

});

//
//    add methods to the tree
//
isc.Tree.addMethods({
//>    @method    tree.init()    (A)
// Initialize the tree.<br><br>
//
// Links the initially provided nodes of the tree according to the tree.modelType.
// <br><br>
//
// Gives the tree a global ID and places it in the global scope.
//
//        @group    creation
//
//        @param    [all arguments]    (object)    objects with properties to override from default
//
// @see group:sharingNodes
//<
init : function () {
    this.setupProperties();

    // if a root wasn't specified, create one
    this.setRoot(this.root || this.makeRoot());

    // load breadth-first on init if so configured
    if (this.loadOnInit && this.loadBatchSize >= 0) this.loadSubtree(null, null, true);
},

setupProperties : function () {
    // make sure we have a global ID, but avoid doing this more than once as subclasses may
    // already have set up an ID
    if (this.ID == null || window[this.ID] != this) isc.ClassFactory.addGlobalID(this);

    // use a unique property for the parent link so that nodes moved between trees can't get
    // confused.  Advanced usages may still override.
    if (!this.parentProperty) this.parentProperty = "_parent_"+this.ID;

    // we rely on being able to scribble the isFolderProperty on nodes - if the user set this
    // to null or the empty string, create a unique identifier.
    if (!this.isFolderProperty) this.isFolderProperty = "_isFolder_"+this.ID;

    // initialize here instead of in addProperties() so we can detect if the user provided
    // explicit values - used by ResultTree.
    if (this.idField == null) this.idField = "id";
    if (this.parentIdField == null) this.parentIdField = "parentId";

    // set the openProperty if it wasn't set already
    if (!this.openProperty) this.openProperty = "_isOpen_" + this.ID;

    // Create an empty _levelNodes array if we're indexing by level
    if (this.indexByLevel) this._levelNodes = [];

    // An auto-generated property name to store precomputed lengths of open lists
    this._cachedLengthProperty = "_cachedLength_" + this.ID;

    // An auto-generated property name to store a boolean flag for whether the lengths of the
    // ancestors of a node will be updated to reflect changes to the node or one of its
    // descendants.  The value of the property is actually a number (or undefined) and it
    // is said to have a true value when the number is greater than zero.
    this._recursionCountProperty = "_recursionCount_" + this.ID;
},

//> @method tree.duplicate()
// Create a copy of tree. If includeData is <code>true</code>, the tree nodes are copied.
// Otherwise, just the tree settings and an empty root node are in the new tree.
//
// @param [includeData] (bool)  Should tree nodes be copied?
// @param [includeLoadState] (bool)  Should tree node loadState be retained?
// @return (tree) copy of tree.
// @group creation
// @visibility internal
//<
_knownProperties : ["autoOpenRoot", "childrenProperty", "defaultIsFolder",
                    "defaultNodeTitle", "discardParentlessNodes", "idField",
                    "isFolderProperty", "modelType", "nameProperty",
                    "parentIdField", "pathDelim", "reportCollisions", "rootValue",
                    "showRoot", "titleProperty", "isMultiDSTree", "dataSource", "operation" ],
_$openProperty: "openProperty",
_copyKnownProperties : function (newTree) {
    // Copy known properties
    for (var i = 0; i < this._knownProperties.length; i++) {
        var propertyName = this._knownProperties[i],
            value = this[propertyName];
        if (value !== undefined) {
            newTree[propertyName] = value;
        }
    }

    // Handle some special dynamic properties
    var value = this[this._$openProperty];
    if (value !== undefined && !value.startsWith("_isOpen_")) {
        newTree[this._$openProperty] = value;
    }
},
duplicate : function (includeData, includeLoadState) {

    // Create a new tree object
    var newTree = isc.Tree.create();
    this._copyKnownProperties(newTree);

    // Create a clean root node
    newTree.setRoot(this.getCleanNodeData(this.getRoot(), false, false, includeLoadState));

    // Copy nodes
    if (includeData) {
        var nodes = this.getOpenList(null, isc.Tree.FOLDERS_AND_LEAVES, null, null, null, null, true);
        nodes = this.getCleanNodeData(nodes, false, false, includeLoadState);
        newTree._linkNodes(nodes);
    }

    return newTree;
},


destroy : function () {
    this.destroyed = true;
    if (this._openNormalizer != null) this._openNormalizer.call(window);

    isc.ClassFactory.dereferenceGlobalID(this);
    this.Super("destroy", arguments);
},

//>    @method    tree.makeRoot()
//        @group    creation
//             Make a new, empty root node.
//
//        @return    (object)     new root node.
//<
makeRoot : function () {
    var root = {};
    var undef;
    if (this.idField !== undef) root[this.idField] = this.rootValue;
    root[this.treeProperty] = this.ID;
    return root;
},

// Convert a node to a folder and return any change in the length of the node's parent
// resulting from that conversion.  Callers are expected to add the change in length to all
// parents of the node.
convertToFolder : function (node) {

    var pagedResultTree = (
            isc.ResultTree != null && isc.isA.ResultTree(this) && this.isPaged()),
        prevState = pagedResultTree && this.getLoadState(node),
        wasFolder = this.isFolder(node),
        changesParentLength = !wasFolder && node != this.root,
        origLength, parent;
    if (changesParentLength) {
        parent = this.getParent(node);
        changesParentLength = (parent != null);
        if (changesParentLength) {
            origLength = this._getNodeLengthToParent(node, parent);
        }
    }

    // Mark the node as a folder.
    node[this.isFolderProperty] = true;


    if (pagedResultTree) {
        var newState = this.getLoadState(node),
            prevFlag = (
                prevState === isc.Tree.LOADED ||
                prevState === isc.Tree.LOADED_PARTIAL_CHILDREN),
            newFlag = (
                newState === isc.Tree.LOADED ||
                newState === isc.Tree.LOADED_PARTIAL_CHILDREN);

        if (prevFlag != newFlag) {
            // Only update the _visibleDescendantsCachedProperty if it has been set before on
            // the node.
            var parent = this.getParent(node);
            if (isc.isA.Boolean(node[this._visibleDescendantsCachedProperty])) {
                this._setVisibleDescendantsCached(node, null, parent, false);
            }
        }
    }

    // Update the length of the node.
    var cachedLength = node[this._cachedLengthProperty];
    if (cachedLength == null) cachedLength = 0;
    node[this._cachedLengthProperty] = cachedLength + this._getDeltaLength(node, wasFolder, true);


    // Return any change in the length of the parent caused by converting the node to
    // a folder.
    if (changesParentLength) {
        return this._getNodeLengthToParent(node, parent) - origLength;
    } else {
        return 0;
    }
},

//>    @method    tree.makeNode()
//             Make a new, empty node from just a path
//            NOTE: creates any parents along the chain, as necessary
//        @group    creation
//        @return    (TreeNode)     new node
//<
// autoConvertParents forces the conversion of nodes in the parent chain to leaf or folder status as
// necessary to avoid dups.  For example, makeNode('foo') followed by makeNode('foo/') would
// normally create a leaf foo and a folder foo.  If autoConvertParents is set to true, there would
// only be the folder foo regardless of the makeNode() call order.
//
makeNode : function (path, autoConvertParents) {

    // first try to find the node -- if we can find it, just return it
    var node = this.find(path);
    if (node) {
        if (autoConvertParents) {
            var deltaLength = this.convertToFolder(node);
            if (deltaLength != 0 && node != this.root) {
                this._updateParentLengths(this.getParent(node), deltaLength);
            }
        }
        return node;
    }

    // The path will be in the format:
    // "root/p1/p2/p3/newLeaf" or
    // "/p1/p2/p3/newFolder/"
    //      where p1 etc are existing parents

    // get the parent path for this node
    var pathComponents = path.split(this.pathDelim);    // array:['','p1','p2','p3','newNode']

    // The path must start at the root - if it doesn't, assume it was intended to
    var rootName = this.getRoot()[this.nameProperty];
    if (rootName.endsWith(this.pathDelim)) {
        rootName = rootName.substring(0, rootName.length - this.pathDelim.length);
    }

    if (pathComponents[0] != rootName) pathComponents.addAt(rootName, 0);

    // If we're making a folder rather than a leaf, the path passed in will finish with the path
    // delimiter, so we'll have a blank at the end of the array
    var newNodeName = pathComponents[pathComponents.length - 1],
        makingLeaf = (newNodeName != isc.emptyString);

    if (!makingLeaf) {
        // chop off the empty slot at the end
        pathComponents.length = pathComponents.length -1;
        newNodeName = pathComponents[pathComponents.length - 1]
    }
//    this.logWarn("makingLeaf: " + makingLeaf + ", pathComponents:" + pathComponents);

    var parentPath = pathComponents.slice(0, (pathComponents.length -1)).join(this.pathDelim)
                     + this.pathDelim;


    // get a pointer to the parent
    var parent = this.find(parentPath);


    if (parent == null) {
        parent = this.find(parentPath.substring(0, parentPath.length - this.pathDelim.length));
    }

    // We need to create the parent if it doesn't exist, or is a leaf, and we're not converting
    // parents.  Call ourselves recursively to get the parent.
    // NOTE: this should bottom out at the root, which should always be defined
    if (!parent) {
        parent = this.makeNode(parentPath, autoConvertParents);
    } else if (!this.isFolder(parent)) {
        // If necessary convert the leaf parent to a folder
        var deltaLength = this.convertToFolder(parent);
        if (deltaLength != 0 && parent != this.root) {
            this._updateParentLengths(this.getParent(parent), deltaLength);
        }
    }

    // make the actual node
    var node = {};

    // set the name and path of the node
    node[this.nameProperty] = newNodeName;

    // making a folder - convert the node to a folder
    if (!makingLeaf) {
        var deltaLength = this.convertToFolder(node);
        if (deltaLength != 0 && node != this.root) {
            this._updateParentLengths(this.getParent(node), deltaLength);
        }
    }

    // and add it to the tree
    return this._add(node, parent);
},


//>    @method    tree.isRoot()
//
// Return true if the passed node is the root node.
//
// @param    node    (TreeNode)     node to test
// @return            (Boolean)    true if the node is the root node
//
// @visibility external
//<
isRoot : function (node) {
    return this.root == node;
},

//>    @method    tree.setupParentLinks()    (A)
//            Make sure the parent links are set up in all children of the root.
//            This lets you create a simple structure without back-links, while
//             having the back-links set up automatically
//        @group    ancestry
//
//        @param    [node]    (TreeNode)    parent node to set up child links to
//                                     (default is this.root)
//<
setupParentLinks : function (node) {
    // if the node wasn't passed in, use the root
    if (!node) node = this.root;
    return this._traverse(node, true, false, false, false);
},

// Recursively traverse the tree to implement setupParentLinks() and to assign the correct
// node lengths to the node and its descendants.  The setupParentLinks and assignCachedLengths
// are boolean arguments.  The node is the tree node and is expected to exist.  The last
// argument, recurse, is a boolean flag used internally to determine whether the current
// execution is at the top level of recursive calls to this method.
_traverse : function (node, setupParentLinks, assignCachedLengths, canonicalizeChildren, recurse) {


    if (setupParentLinks && node[this.idField] != null) {
        this.nodeIndex[node[this.idField]] = node;
    }

    // get the children array of the node
    var children = node[this.childrenProperty];


    if (children) {
        if (setupParentLinks) {
            // current assumption whenever loading subtrees is that if any children are returned
            // for a node, it's the complete set, and the node is marked "loaded"
            this.setLoadState(node, isc.Tree.LOADED);
        }

        // handle the children property containing a single child object.
        if (!(isc.isAn.Array(children) || isc.isA.ResultSet(children))) {
            children = node[this.childrenProperty] = [children];
        }
    }

    if (children) {
        // for each child
        var isArray = isc.isAn.Array(children),
            isResultSet = !isArray && isc.isA.ResultSet(children);

        var length = (isResultSet ? children._getCachedLength() : children.getLength());
        for (var i = 0; i < length; ++i) {
            var child = (isArray ? children[i] : children.getCachedRow(i));

            // if the child is null, skip it
            if (!child) continue;

            if (setupParentLinks) {
                // set the parentId on the child if it isn't set already
                if (child[this.parentIdField] == null && node[this.idField] != null)
                    child[this.parentIdField] = node[this.idField];

                // set the child's parent to the parent
                child[this.parentProperty] = node;

                this._addToLevelCache(child, node);
            }

            // If the child is a folder, call this method recursively on the child.
            if (this.isFolder(child)) {
                this._traverse(
                    child, setupParentLinks, assignCachedLengths, canonicalizeChildren, true);
            } else if (setupParentLinks && child[this.idField] != null) {
                this.nodeIndex[child[this.idField]] = child; // link into the nodeIndex
            }

            // Assign the _cachedLengthProperty on the child.  This is done after the recursive
            // call as the child's length can depend on the node lengths of its children.
            if (assignCachedLengths) {
                child[this._cachedLengthProperty] = this._getNodeLength(child);
            }
        }
    }

    if (canonicalizeChildren && children) {

        children = node[this.childrenProperty] = this._canonicalizeChildren(node, children, true);
        if (isc.isA.ResultSet(children)) {
            if (!(children.lengthIsKnown() && children.allMatchingRowsCached())) {
                this._setVisibleDescendantsCached(node, false, null, false);
            }
        }
    }

    // If this is the top level of the recursion, then the _cachedLengthProperty has been set
    // on all nodes except for the original node.  Set node[this._cachedLengthProperty] here.
    if (assignCachedLengths && !recurse) {
        node[this._cachedLengthProperty] = this._getNodeLength(node);
    }
},

//> @method tree.linkNodes()
// Adds an array of tree nodes into a Tree of +link{modelType} "parent".
// <P>
// The provided TreeNodes must contain, at a minimum, a field containing a unique ID for the
// node (specified by +link{attr:Tree.idField}) and a field containing the ID of the node's
// parent node (specified by +link{attr:Tree.parentIdField}).
// <P>
// This method handles receiving a mixture of leaf nodes and parent nodes, even out of order and
// with any tree depth.
// <P>
// Nodes may be passed with the +link{childrenProperty} already populated with an Array of
// children that should also be added to the Tree, and this is automatically handled.
//
// @param nodes (Array of TreeNode) list of nodes to link into the tree.
//
// @see attr:Tree.data
// @see attr:Tree.modelType
// @visibility external
//<
connectByParentID : function (records, idProperty, parentIdProperty, rootValue, isFolderProperty) {
    this._linkNodes(records, idProperty, parentIdProperty, rootValue, isFolderProperty);
},
connectByParentId : function (records, idProperty, parentIdProperty, rootValue, isFolderProperty) {
    this._linkNodes(records, idProperty, parentIdProperty, rootValue, isFolderProperty);
},


// NOTE: this does not handle multi-column (multi-property) primary keys
linkNodes : function (records, idProperty, parentIdProperty, rootValue, isFolderProperty, contextNode, suppressDataChanged) {
    return this._linkNodes(records, idProperty, parentIdProperty, rootValue, isFolderProperty, contextNode, suppressDataChanged);
},
_linkNodes : function (records, idProperty, parentIdProperty, rootValue, isFolderProperty, contextNode, suppressDataChanged) {

    if (this.modelType == "fields") {
        this.connectByFields(records);
        return;
    }

    records = records || this.data;
    idProperty = (idProperty != null) ? idProperty : this.idField;
    parentIdProperty = (parentIdProperty != null) ? parentIdProperty : this.parentIdField;
    rootValue = (rootValue != null) ? rootValue : this.rootValue;

    var newNodes = [];
    newNodes.addList(records);

    // build a local index of the nodes passed in. this will allow us to find parents within the
    // tree without having to do multiple array.finds (so it'll be linear time lookup)
    var localNodeIndex = {};
    for (var i = 0; i < newNodes.length; i++) {
        var id = newNodes[i][idProperty];
        if (id != null) localNodeIndex[id] = newNodes[i];
    }

    for (var i = 0; i < newNodes.length; i++) {
        var node = newNodes[i];

        // We look up parent chains and add interlinked nodes in parent order
        // so if we already have this node in the tree, skip it
        if (this.nodeIndex[node[idProperty]] == node) continue;
        if (node == null) continue;

        // Our parentId property may point to another node passed in (potentially in a chain)
        // In this case, ensure we link these parents into the tree first.
        var newParentId = node[parentIdProperty],
            newParent = newParentId != null ? localNodeIndex[newParentId] : null,
            newParents = []
        ;

        while (newParent != null) {
            if (newParent) newParents.add(newParent);
            newParentId = newParent[parentIdProperty];
            // Note: don't infinite loop if parentId==id - that's bad data, really, but such
            // datasets exist in the wild..
            newParent = newParentId != null && newParentId != node[parentIdProperty] ? localNodeIndex[newParentId] : null;
        }

        for (var ii = newParents.length; ii--; ) {
            if (this.logIsDebugEnabled(this._$treeLinking)) {
                this.logDebug("linkNodes running - adding interlinked parents to the tree in "+
                    " reverse hierarchical order -- currently adding node with id:"+
                    newParents[ii][idProperty], this._$treeLinking);
            }
            this._linkNode(newParents[ii], idProperty, parentIdProperty,
                           contextNode, rootValue);
            // at this point the parent is linked into the real tree --
            // blank out the entry in the local index so other nodes linked to it do
            // the right thing
            delete localNodeIndex[newParents[ii][idProperty]];
        }
        // Actually link in this node
        this._linkNode(node, idProperty, parentIdProperty, contextNode, rootValue);
        // blank out this slot - this will avoid us picking up this node in the newParents
        // array of other nodes when it has already been added to the tree if appropriate
        delete localNodeIndex[node[idProperty]];
    }

    this._clearNodeCache(true);
    if (!suppressDataChanged) this.dataChanged();
},

// old synonyms for backcompat
connectByParentID : function (records, idProperty, parentIdProperty, rootValue, isFolderProperty) {
    this._linkNodes(records, idProperty, parentIdProperty, rootValue, isFolderProperty);
},
connectByParentId : function (records, idProperty, parentIdProperty, rootValue, isFolderProperty) {
    this._linkNodes(records, idProperty, parentIdProperty, rootValue, isFolderProperty);
},

// _linkNode - helper to actually attach a node to our tree - called from the for-loop in _linkNodes()
// returns true if the node was successfully added to the tree.
_$treeLinking:"treeLinking",
_linkNode : function (node, idProperty, parentIdProperty, contextNode, rootValue) {

    var logDebugEnabled = this.logIsDebugEnabled(this._$treeLinking);

    var id = node[idProperty],
        parentId = node[parentIdProperty],
        undef,
        nullRootValue = (rootValue == null),
        // Note explicit === for emptyString comparison necessary as
        // 0 == "", but zero is a valid identifier
        nullParent = (parentId == null || parentId == -1 || parentId === isc.emptyString),
        parent = this.nodeIndex[parentId];

    if (parent) {
        if (logDebugEnabled) {
            this.logDebug("found parent " + parent[idProperty] +
                         " for child " + node[idProperty], this._$treeLinking);
        }
        this.__add(node, parent);
    } else if (!nullRootValue && parentId == rootValue) {

        if (logDebugEnabled) {
            this.logDebug("root node: " + node[idProperty], this._$treeLinking);
        }
        // this is a root node
        this.__add(node, this.root);

    } else {
        // Drop nodes with an explicit parent we can't find if discardParentlessNodes is true
        if (!nullParent && this.discardParentlessNodes) {
            this.logWarn("Couldn't find parent: " + parentId + " for node with id:" + id,
                         this._$treeLinking);
        } else {

            var defaultParent = contextNode || this.root;
            // if a contextNode was supplied, use that as the default parent node for all
            // nodes that are missing a parentId - this is for loading immediate children
            // only, without specifying a parentId
            if (logDebugEnabled) {
                this.logDebug("child:" + node[idProperty] +
                              (nullParent ? " has no explicit parent " :
                                        (" unable to find specified parent:" + parentId)) +
                              "- linking to default node " +
                              defaultParent[idProperty], this._$treeLinking);
            }
            this.__add(node, defaultParent);
        }
    }
},

connectByFields : function (data) {
    if (!data) data = this.data;
    // for each record
    for (var i = 0; i < data.length; i++) {
        this.addNodeByFields(data[i]);
    }
},

addNodeByFields : function (node) {
    // go through each field in this.fields in turn, descending through the hierarchy, creating
    // hierarchy as necessary


    var parent = this.root;
    for (var i = 0; i < this.fieldOrder.length; i++) {
        var fieldName = this.fieldOrder[i],
            fieldValue = node[fieldName];

        var folderName = isc.isA.String(fieldValue) ? fieldValue :
                                                      fieldValue + isc.emptyString,
            childNum = this.findChildNum(parent, folderName),
            child;
        if (childNum != -1) {
            //this.logWarn("found child for '" + fieldName + "':'" + fieldValue + "'");
            child = this.getChildren(parent).get(childNum);
        } else {
            // if there's no child with this field value, create one
            //this.logWarn("creating child for '" + fieldName + "':'" + fieldValue + "'");
            child = {};
            child[this.nameProperty] = folderName;
            this._add(child, parent);
            var deltaLength = this.convertToFolder(child);
            if (deltaLength != 0) {
                this._updateParentLengths(parent, deltaLength);
            }
        }
        parent = child;
    }
    // add the new node to the Tree
    //this.logWarn("adding node at: " + this.getPath(parent));
    this._add(node, parent);
},

//>    @method    tree.getRoot()
//
// Returns the root node of the tree.
//
// @return  (TreeNode)    the root node
//
// @visibility external
//<
getRoot : function () {
    return this.root;
},

//>    @method    tree.setRoot()
//
// Set the root node of the tree.
//
// @param   newRoot (TreeNode)    new root node
// @param   autoOpen (boolean)  set to true to automatically open the new root node.
//
// @visibility external
//<
setRoot : function (newRoot, autoOpen) {

    // assign the new root
    this.root = newRoot;

    // avoid issues if setRoot() is used to re-root a Tree on one of its own nodes
    var newRootFromSameTree = (newRoot && isc.endsWith(this.parentProperty, this.ID));
    if (newRootFromSameTree) newRoot[this.parentProperty] = null;


    var calcLength = !(newRootFromSameTree && this.root[this._cachedLengthProperty] != null);

    // make sure root points to us as its tree
    this.root[this.treeProperty] = this.ID;

    if (this.rootValue == null) this.rootValue = this.root[this.idField];

    // If the root node has no name, assign the path property to it.  This is for backcompat
    // and also a reasonable default.
    var rootName = this.root[this.nameProperty];
    if (rootName == null || rootName == isc.emptyString) {
        var wasFolder = !calcLength && this.isFolder(this.root);

        this.root[this.nameProperty] = this.pathDelim;
        // Setting the name can change the folderness of the node so update the root node's
        // length.
        if (!calcLength) {
            this.root[this._cachedLengthProperty] += this._getDeltaLength(
                this.root, wasFolder, this.isFolder(this.root));
        }
    }

    // Set the initial cached length of the new root node.
    if (calcLength) {
        var isFolder = this.isFolder(this.root);
        this.root[this._cachedLengthProperty] = (
            (isFolder && this.openDisplayNodeType != isc.Tree.LEAVES_ONLY) ||
            (!isFolder && this.openDisplayNodeType != isc.Tree.FOLDERS_ONLY) ? 1 : 0);
    }

    var pagedResultTree = (
            isc.ResultTree != null && isc.isA.ResultTree(this) && this.isPaged());
    if (pagedResultTree) {
        if (this.root[this._visibleDescendantsCachedProperty] == null) {
            this.root[this._visibleDescendantsCachedProperty] = true;
            this._setVisibleDescendantsCached(this.root, null, null, true);
        }
    }

    // the root node is always a folder
    if (!this.isFolder(this.root)) {
        this.convertToFolder(this.root);
    }

    // NOTE: this index is permanent, staying with this Tree instance so that additional sets of
    // nodes can be incrementally linked into the existing structure.
    this.nodeIndex = {};

    // (re)create the structure of the Tree according to the model type
    if ("parent" == this.modelType) {
        // nodes provided as flat list (this.data); each record is expected to have a property
        // which is a globally unique ID (this.idField) and a property which has the globally
        // unique ID of its parent (this.parentIdField).

        // assemble the tree from this.data if present
        // Pass in the param to suppress dataChanged since we'll fire that below
        if (this.data) {
            this._linkingNodes = true;
            this._linkNodes(null, null, null, null, null, null, true);
            delete this._linkingNodes;
        }
    } else if ("fields" == this.modelType) {

        // nodes provided as flat list; a list of fields, in order, defines the Tree
        if (this.data) this.connectByFields();

    } else if ("children" == this.modelType) {

        // Each parent has an array of children.  Traverse the tree, starting at the root,
        // to setup the parent links (assuming this.autoSetupParentLinks is true) and to
        // assign the initial node lengths (i.e. _cachedLengthProperty) on all of the nodes.
        this._traverse(this.root, this.autoSetupParentLinks, true, false, false);

        if (this.data) {
            var data = this.data;
            this.data = null;
            this._addList(data, this.root);
        }
    } else {
        this.logWarn("Unsupported modelType: " + this.modelType);
    }

    // Slot the root node into nodeIndex
    this.setupParentLinks();

    // open the new root if autoOpen: true passed in or this.autoOpenRoot is true.  Suppress
    // autoOpen if autoOpen:false passed in
    if (autoOpen !== false && (this.autoOpenRoot || autoOpen)) {
        this.openFolder(newRoot);
    }

    // Slot the root node into nodeIndex, and, for paged ResultTrees, change the container
    // of the children of any node with a valid childCountProperty value from an array to
    // a ResultSet.
    this._traverse(this.root, true, false, pagedResultTree, false);

    // mark the tree as dirty and note that the data has changed
    this._clearNodeCache();
    this.dataChanged();
},

// get a copy of these nodes without all the properties the Tree scribbles on them.
// Note the intent here is that children should in fact be serialized unless the caller has
// explicitly trimmed them.
getCleanNodeData : function (nodeList, includeChildren, cleanChildren, includeLoadState) {

    return isc.Tree.getCleanNodeData(nodeList, includeChildren, true, includeLoadState, this);
},

//
// identity methods -- override these for your custom trees
//

//>    @method    tree.getName()
//
// Get the 'name' of a node.  This is node[+link{Tree.nameProperty}].  If that value has not
// been set on the node, a unique value (within this parent) will be auto-generated and
// returned.
//
// @param    node    (TreeNode)    node in question
// @return            (string)    name of the node
//
// @visibility external
//<
_autoName : 0,
getName : function (node) {
    var ns = isc._emptyString;

    if (!node) return ns;

    var name = node[this.nameProperty];
    if (name == null) name = node[this.idField];
    if (name == null) {
        // unnamed node: give it a unique name.


        // never assign an autoName to a node not from our tree
        if (!this.isDescendantOf(node, this.root) && node != this.root) return null;

        // assign unique autoNames per tree so we don't get cross-tree name collisions on D&D
        if (!this._autoNameBase) this._autoNameBase = isc.Tree.autoID++ + "_";
        name = this._autoNameBase+this._autoName++;
        // set a flag noting that we auto-assigned this name.
        // This is useful in databinding for us to determine whether path-based state
        // information can be reliably mapped to new data, for example on cache
        // invalidation.
        node._autoAssignedName = true;
    }

    // convert to string because we call string methods on this value elsewhere
    if (!isc.isA.String(name)) name = ns+name;

    // cache
    node[this.nameProperty] = name;
    return name;
},

//>    @method    tree.getTitle()
//
// Return the title of a node -- the name as it should be presented to the user.  This method
// works as follows:
// <ul>
// <li> If a +link{attr:Tree.titleProperty} is set on the node, the value of that property is
// returned.
// <li> Otherwise, if the +link{attr:Tree.nameProperty} is set on the node, that value is
// returned, minus any trailing +link{attr:Tree.pathDelim}.
// <li> Finally, if none of the above yielded a title, the value of
// +link{attr:Tree.defaultNodeTitle} is returned.
// </ul>
// You can override this method to return the title of your choice for a given node.
// <br><br>
// To override the title for an auto-constructed tree (for example, in a databound TreeGrid),
// override +link{method:TreeGrid.getNodeTitle} instead.
//
// @param node  (TreeNode) node for which the title is being requested
// @return      (string) title to display
//
// @see method:TreeGrid.getNodeTitle
//
// @visibility external
//<
getTitle : function (node) {
    if (!node) return null;
    // if the node has an explicit title, return that
    if (node[this.titleProperty] != null) return node[this.titleProperty];

    // otherwise derive from the name
    var name = node[this.nameProperty];
    if (name == null) name = this.defaultNodeTitle;
    return (isc.endsWith(name, this.pathDelim)
                ? name.substring(0,name.length-this.pathDelim.length)
                : name);
},

//>    @method    tree.getPath()
//
// Returns the path of a node - a path has the following format:
// <code>([name][pathDelim]?)*</code>
// <br><br>
// For example, in this tree:
// <pre>
// root
//   foo
//     bar
// </pre>
// Assuming that +link{attr:Tree.pathDelim} is the default <code>/</code>, the <code>bar</code>
// node would have the path <code>root/foo/bar</code> and the path for the <code>foo</code>
// node would be <code>root/foo</code>.
// <br><br>
// Once you have a path to a node, you can call find(path) to retrieve a reference to the node
// later.
//
// @param    node    (TreeNode)    node in question
// @return            (string)    path to the node
//
// @see method:Tree.getParentPath
// @visibility external
//<
getPath : function (node) {
    var parent = this.getParent(node);
    if (parent == null) return this.getName(node);

    var parentName = this.getName(parent);
    return this.getPath(parent) +
            (parentName == this.pathDelim ? isc.emptyString : this.pathDelim) +
                this.getName(node);
},

//>    @method    tree.getParentPath()
//
// Given a node, return the path to its parent.  This works just like
// +link{method:Tree.getPath} except the node itself is not reported as part of the path.
//
// @param    node    (TreeNode)    node in question
// @return            (string) path to the node's parent
//
// @see method:Tree.getPath
// @visibility external
//<
getParentPath : function (node) {
    // get the node's path
    var name = this.getName(node),
        path = this.getPath(node);

    // return the path minus the name of the node
    return path.substring(0, path.length - name.length - this.pathDelim.length);
},

//>    @method    tree.getParent()
//
// Returns the parent of this node.
//
// @param   node    (TreeNode)    node in question
// @return  (node)              parent of this node
//
// @visibility external
//<
getParent : function (node) {
    if (node == null) return null;
    return node[this.parentProperty];
},

//>    @method    tree.getParents()
//
// Given a node, return an array of the node's parents with the immediate parent first.  The
// node itself is not included in the result.  For example, for the following tree:
// <pre>
// root
//   foo
//     bar
// </pre>
// Calling <code>tree.getParents(bar)</code> would return: <code>[foo, root]</code>.  Note that
// the returned array will contain references to the nodes, not the names.
//
// @param   node    (TreeNode)            node in question
// @return          (Array)             array of node's parents
//
// @visibility external
//<
getParents : function (node) {
    var list = [],
        parent = this.getParent(node);
    // while parents exist
    while (parent) {
        // add them to the list
        list.add(parent);

        // if the parent is the root, jump out!
        //    this lets us handle subTrees of other trees
        if (parent == this.root) break;

        // and get the next parent in the chain
        parent = this.getParent(parent);
    }
    // return the list of parents
    return list;
},

//>    @method    tree.getLevel()    (A)
//
// Return the number of levels deep this node is in the tree.  For example, for this tree:
// <pre>
// root
//   foo
//     bar
// </pre>
// Calling <code>tree.getLevel(bar)</code> will return <code>2</code>.
// <P>
// Note +link{showRoot} defaults to false so that multiple nodes can be shown at top level.  In
// this case, the top-level nodes still have root as a parent, so have level 1, even though
// they have no visible parents.
//
// @param   node    (TreeNode)    node in question
// @return          (number)    number of parents the node has
//
// @visibility external
//<
getLevel : function (node) {
    return this.getParents(node).length;
},

// Given a node, iterate up the parent chain and return an array containing each level for
// which the node or its ancestor has a following sibling
// Required for treeGrid connectors
// We could improve performance here by cacheing this information on each node and having this
// method be called recursively on parents rather than iterating through the parents' array
// for every node this method is called on.
_getFollowingSiblingLevels : function (node) {
    var levels = [],
        parents = this.getParents(node),
        level = parents.length;
    // note that parents come back ordered with the root last so iterate through them forwards
    // to iterate up the tree
    for (var i = 0; i < level; i++) {
        var children = this.getChildren(parents[i]),
            childrenLength = (
                isc.isA.ResultSet(children) ?
                    children._getCachedLength() : children.getLength());
        if (children.indexOf(node) != childrenLength - 1) {
            levels.add(level - i);
        }
        node = parents[i];
    }
    return levels;
},

//>    @method    tree.isFolder()
//
// Determines whether a particular node is a folder.  The logic works as follows:<br><br>
// <ul>
// <li> If the +link{TreeNode} has a value for the +link{attr:Tree.isFolderProperty}
// (+link{TreeNode.isFolder} by default) that value is returned.
// <li> Next, the existence of the +link{attr:Tree.childrenProperty} (by default
// +link{TreeNode.children}) is checked on the +link{TreeNode}.  If the node has the children
// property defined (regardless of whether it actually has any children), then isFolder()
// returns true for that node.
// </ul>
// <P>
// You can override this method to provide your own interpretation of what constitutes a folder.
//
// @param    node    (TreeNode)    node in question
// @return            (Boolean)    true if the node is a folder
//
// @visibility external
//<
isFolder : function (node) {
    if (node == null) return false;

    // explicit isFolder set
    var isFolder = node[this.isFolderProperty];
    if (isFolder != null) return isFolder;

    // has a children array (may have zero actual children currently, but having a children
    // array is sufficient for us to regard this as a folder).  Note that we scribble the
    // children array on the nodes even in modelTypes other than "children", so this check
    // is correct for other modelTypes as well.
    if (node[this.childrenProperty]) return true;

    // infer folderness from the name of the node
    // XXX 10/13/2005 : this is purposefully not documented.  We have it here for backcompat
    // with trees that may have relied on this, but disclosing this will confuse people -
    // they'll start to think about having to tack on the path delimiter on their nodes to
    // signify folderness, which in turn translates into confusion about when you should or
    // should not supply the slash or give back a trailing slash from e.g. getPath()
    var name = this.getName(node);

    // if there's no name, we have no way of knowing
       if (name == null) return false;

    // if the last character is the pathDelim, it's a folder.
    return isc.endsWith(name, this.pathDelim);
},

//>    @method    tree.isLeaf()
//
// Returns true if the passed in node is a leaf.
//
// @param   node    (TreeNode)    node in question
// @return          (Boolean)   true if the node is a leaf
//
// @visibility external
//<
isLeaf : function (node) {
    return ! this.isFolder(node);
},

//> @method tree.isFirst() (A)
// Note: because this needs to take the sort order into account, it can be EXTREMELY expensive!
// @group ancestry
// Return true if this item is the first one in its parent's list.
//
// @param  node (TreeNode)  node in question
// @return (boolean)  true == node is the first child of its parent
//<
isFirst : function (node) {
    var parent = this.getParent(node);
    if (! parent) return true;

    var kids = this.getChildren(parent, this.opendisplayNodeType,
            this._openNormalizer, this.sortDirection, null, this._sortContext,
            false, false);
    if (isc.isA.ResultSet(kids)) {
        return (kids._getCachedLength() > 0 && kids.getCachedRow(0) == node);
    } else {
        return (kids.first() == node);
    }
},

//>    @method    tree.isLast()    (A)
//         Note: because this needs to take the sort order into account, it can be EXTREMELY expensive!
//        @group    ancestry
//            Return true if this item is the last one in its parent's list.
//
//        @param    node    (TreeNode)    node in question
//        @return            (boolean)    true == node is the last child of its parent
//<
isLast : function (node) {
    var parent = this.getParent(node);
    if (! parent) return true;

    var kids = this.getChildren(parent, this.opendisplayNodeType,
            this._openNormalizer, this.sortDirection, null, this._sortContext,
            false, false);
    if (isc.isA.ResultSet(kids)) {
        var length = kids._getCachedLength();
        return (length > 0 && kids.getCachedRow(length - 1) == node);
    } else {
        return (kids.last() == node);
    }
},


//
//    finding a node
//

//>    @method    tree.findById()    (A)
//
// Find the node with the specified ID.  Specifically, it returns the node whose idField
// matches the id passed to this method. If the tree is using the "parent" modelType, this
// lookup will be constant-time.  For all other modelTypes, the tree will be searched
// recursively.
//
// @group   location
// @param   id (string)    ID of the node to return.
// @return  (object)       node with appropriate ID, or null if not found.
//
// @see attr:Tree.idField
// @see method:Tree.find
//
// @visibility external
//<
findById : function (id) {
    return this.find(this.idField, id);
},


//>    @method    tree.find()
//
// Find a node within this tree using a string path or by attribute value(s).  This method can be
// called with 1 or 2 arguments. If a single String
// argument is supplied, the value of the argument is treated as the path to the node.  If a
// single argument of type Object is provided, it is treated as a set of field name/value
// pairs to search for (see +link{List.find}).
// <br>
// If 2 arguments are supplied, this method will treat the first argument as a fieldName, and
// return the first node encountered where <code>node[fieldName]</code> matches the second
// argument.  So for example, given this tree:
// <pre>
// foo
//   zoo
//     bar
//   moo
//     bar
// </pre>
// Assuming your +link{attr:Tree.pathDelim} is the default <code>/</code> and <code>foo</code>
// is the name of the root node, then
// <code>tree.find("foo/moo/bar")</code> would return the <code>bar</code> node under the
// <code>moo</code> node.
// <br><br>
// <code>tree.find("name", "bar")</code> would return the first <code>bar</code> node because
// it is the first one in the list whose <code>name</code> (default value of
// +link{attr:Tree.nameProperty}) property matches the value
// <code>bar</code>.  The two argument usage is generally more interesting when your tree nodes
// have some custom unique property that you wish to search on.  For example if your tree nodes
// had a unique field called "UID", their serialized form would look something like this:
// <pre>
// { name: "foo", children: [...], UID:"someUniqueId"}
// </pre>
// You could then call <code>tree.find("UID", "someUniqueId")</code> to find that node.  Note
// that the value doesn't have to be a string - it can be any valid JS value, but since this
// data generally comes from the server, the typical types are string, number, and boolean.
// <br><br>
// The usage where you pass a single object is interesting when your tree nodes have a number
// of custom properties that you want to search for in combination.  Say your tree nodes had
// properties for "color" and "shape"; <code>tree.find({color: "green", shape: "circle"})</code>
// would return the first node in the tree where both properties matched.
// <br><br>
// When searching by path, trailing path delimiters are ignored.  So for example
// <code>tree.find("foo/zoo/bar")</code> is equivalent to
// <code>tree.find("foo/zoo/bar/")</code>
//
// @group location
// @param fieldNameOrPath   (string)    Either the path to the node to be found, or the name of
//                                      a field which should match the value passed as a second
//                                      parameter
// @param [value]          (any)     If specified, this is the desired value for the
//                                   appropriate field
// @return (object) the node matching the supplied criteria or null if not found
//
// @see attr:Tree.root
// @see attr:Tree.pathDelim
// @see attr:Tree.nameProperty
//
// @visibility external
//<
// NOTE: This should be a good generic implemention, try overriding findChildNum instead.
find : function (fieldName, value) {

    var undef;
    if (value === undef && isc.isA.String(fieldName)) return this._findByPath(fieldName);

    if (value !== undef) {
        // constant time lookup when we have nodeIndex
        if (fieldName == this.idField) return this.nodeIndex[value];
        // special-case root, which may not appear in getDescendants() depending on this.showRoot
        if (this.root[fieldName] == value) return this.root;
        // Use 'getDescendants()' to retrieve both open and closed nodes.
        return this.getDescendants().find(fieldName, value);
    } else {
        // fieldName is an Object, so use the multi-property option of List.find()
        var searchList = this.getDescendants();
        searchList.add(this.root);
        return searchList.find(fieldName);
    }

},

findAll : function (fieldName, value) {
    // Use 'getDescendants()' to retrieve both open and closed nodes.
    return this.getDescendants().findAll(fieldName, value);
},

// Find a node within this tree by path.
_findByPath : function (path) {



    // return early for cases of referring to just root
    if (path == this.pathDelim) return this.root;
    var rootPath = this.getPath(this.root);
    if (path == rootPath) return this.root;

    var node = this.root,
        lastDelimPosition = 0,
        delimLength = this.pathDelim.length;

    // if the path starts with a references to root, start beyond it
    if (isc.startsWith(path, rootPath)) {
        lastDelimPosition = rootPath.length;
    } else if (isc.startsWith(path, this.pathDelim)) {
        lastDelimPosition += delimLength;
    }

    //this.logWarn("path: " + path);

    while (true) {
        var delimPosition = path.indexOf(this.pathDelim, lastDelimPosition);

        //this.logWarn("delimPosition: " + delimPosition);

        // skip over two delims in a row (eg "//") and trailing (single) delimeter
        if (delimPosition == lastDelimPosition) {
            //this.logWarn("extra delimeter at: " + delimPosition);
            lastDelimPosition += delimLength;
            continue;
        }

        var moreDelims = (delimPosition != -1),
            // name of the child to look for at this level
            name = path.substring(lastDelimPosition, moreDelims ? delimPosition : path.length),
            // find the node number of that child
            nodeNum = this.findChildNum(node, name);

        //this.logWarn("name: " + name);

        if (nodeNum == -1) return null;

        node = node[this.childrenProperty].getCachedRow(nodeNum);

        // if there are no more delimeters we're done
        if (!moreDelims) return node;

        // advance the lastDelimiter
        lastDelimPosition = delimPosition + delimLength;

        // if we got all the way to the end of the path, we're done:  return the node
        if (lastDelimPosition == path.length) return node;
    }
},

//>    @method    tree.findChildNum()    (A)
//        @group    location
//            Given a parent and the name of a child, return the number of that child.
//
//         Note: names of folder nodes will have pathDelim stuck to the end
//
//        @param    parent    (TreeNode)    parent node
//        @param    name    (string)    name of the child node to find
//        @return            (number)    index number of the child, -1 if not found
//<
findChildNum : function (parent, name) {
    var children = this.getChildren(parent);

    if (children == null) {
        return -1;
    }
    if (name == null) return -1;

    var length = (
            isc.isA.ResultSet(children) ? children._getCachedLength() : children.getLength()),
        nameHasDelim = isc.endsWith(name, this.pathDelim),
        delimLength = this.pathDelim.length;
    for (var i = 0; i < length; i++) {

        var child = children.getCachedRow(i);
        if (child != null) {
            var childName = this.getName(child),
                lengthDiff = childName.length - name.length;

            if (lengthDiff == 0 && childName == name) return i;

            if (lengthDiff == delimLength) {
                // match if childName has trailing delim and name does not
                if (isc.startsWith(childName, name) &&
                    isc.endsWith(childName, this.pathDelim) && !nameHasDelim)
                {
                    return i;
                }
            } else if (nameHasDelim && lengthDiff == -delimLength) {
                // match if name has trailing delim and childName does not
                if (isc.startsWith(name, childName)) return i;
            }
        }
    }

    // not found, return -1
    return -1;
},

//>    @method    tree.getChildren()
//
// Returns all children of a node.  If the node is a leaf, this method returns null.
// <P>
// For databound trees the return value could be a +link{class:ResultSet} rather than a simple
// array - so it's important to access the return value using the +link{interface:List}
// interface instead of as a native Javascript Array.
// <smartclient>
// The case that a ResultSet may be returned can only happen if the tree is a
// +link{class:ResultTree} and the +link{resultTree.fetchMode} is set to "paged".
// </smartclient>
// <smartgwt>
// If the underlying set of children is incomplete then this method will return only those
// nodes that have already been loaded from the server.
// </smartgwt>
//
// @param node (TreeNode) The node whose children you want to fetch.
// @return (List of TreeNode) List of children for the node, including an empty List if the
//                            node has no children.  For a leaf, returns null.
// @see tree.getChildrenResultSet()
// @visibility external
//<

getChildren : function (parentNode, displayNodeType, normalizer, sortDirection, criteria,
                        context, returnNulls, treatEmptyFoldersAsLeaves, dontUseNormalizer) {


    // If separateFolders is true, we need to have an openNormalizer so we can sort/separate
    // leaves and folders
    // This will not actually mark the tree as sorted by any property since we're not setting up
    // a sortProp.

    if (!dontUseNormalizer &&
        normalizer == null && this._openNormalizer == null && this.separateFolders)
    {

        if (this._sortSpecifiers != null) this.setSort(this._sortSpecifiers);
        else this.sortByProperty();

        normalizer = this._openNormalizer;
    }

    if (parentNode == null) parentNode = this.root;

    // if we're passed a leaf, it has no children, return empty array
    if (this.isLeaf(parentNode)) return null;

    // if the parentNode doesn't have a child array, create one
    if (parentNode[this.childrenProperty] == null) {
        if (returnNulls) return null;
        var children = [];
        parentNode[this.childrenProperty] = children;
        // just return the new empty children array
        return children;
    }

    var pagedResultTree = (
            isc.ResultTree != null && isc.isA.ResultTree(this) && this.isPaged()),
        list = parentNode[this.childrenProperty],
        subset;

    // If a criteria was passed in, remove all items that don't pass the criteria.
    if (criteria) {
        subset = [];

        var listLength = (pagedResultTree && isc.isA.ResultSet(list) ?
                list._getCachedLength() : list.getLength());
        for (var i = 0; i < listLength; ++i) {
            var childNode = list.getCachedRow(i);
            if (childNode != null) {
                // CALLBACK API:  available variables:  "node,parent,tree"
                if (this.fireCallback(
                        criteria, "node,parent,tree", [childNode, parentNode, this]))
                {
                    subset[subset.length] = childNode;
                }
            }
        }

        list = subset;
    }

    // Reduce the list if a displayNodeType was specified.
    if (displayNodeType == isc.Tree.FOLDERS_ONLY) {
        // If only folders were specified, get the subset that are folders.
        subset = [];
        var listLength = (pagedResultTree && isc.isA.ResultSet(list) ?
                list._getCachedLength() : list.getLength());
        for (var i = 0; i < listLength; ++i) {
            var childNode = list.getCachedRow(i);
            if (childNode != null) {
                var isFolder = this.isFolder(childNode);
                if (isFolder && treatEmptyFoldersAsLeaves) {
                    var c = childNode[this.childrenProperty];
                    isFolder = !(c != null && c.isEmpty());
                }
                if (isFolder) {
                    subset[subset.length] = childNode;
                }
            }
        }
    } else if (displayNodeType == isc.Tree.LEAVES_ONLY) {
        // If only leaves were specified, get the subset that are leaves.
        subset = [];
        var listLength = (pagedResultTree && isc.isA.ResultSet(list) ?
                list._getCachedLength() : list.getLength());
        for (var i = 0; i < listLength; ++i) {
            var childNode = list.getCachedRow(i);
            if (childNode != null) {
                var isLeaf = this.isLeaf(childNode);
                if (!isLeaf && treatEmptyFoldersAsLeaves) {
                    var c = childNode[this.childrenProperty];
                    isLeaf = (c != null && c.isEmpty());
                }
                if (isLeaf) {
                    subset[subset.length] = childNode;
                }
            }
        }
    } else {
        // Otherwise return the entire list (folders and leaves).
        subset = list;
    }

    // if a normalizer is specified, sort before returning
    if (normalizer) {


        var sortProps = this._sortSpecifiers ? this._sortSpecifiers.getProperty("property") : [];


        var ascendingComparator = null,
            descendingComparator = null;
        if (isc.Browser.isFirefox && normalizer === this._openNormalizer) {
            ascendingComparator = this._openAscendingComparator;
            descendingComparator = this._openDescendingComparator;
        }

        if (this._sortSpecifiers) {
            // Update the normalizer on each sort-spec if one isn't present.
            for (var spec = this._sortSpecifiers.length; spec--; ) {
                var specObj = this._sortSpecifiers[spec];
                if (!specObj.normalizer) {
                    specObj.normalizer = normalizer;
                    specObj._comparator = (
                        Array.shouldSortAscending(specObj.direction) ?
                            ascendingComparator : descendingComparator);
                }
            }
        }

        // we now support sorting on all of the groupByFields
        var groupByField = !this._groupByField ? null :
              (isc.isAn.Array(this._groupByField) ? this._groupByField : [this._groupByField]);

        if (// we're not in a grouped LG OR
                !groupByField ||
                // the special 'alwaysSortGroupHeaders' flag is set (indicating group headers have
                // multiple meaningful field values, as when we show summaries in headers) OR
                this.alwaysSortGroupHeaders
                ||
                //// we're not grouping on the first sortField and this isn't a group-node OR
                (!groupByField.contains(sortProps[0]) && parentNode != this.getRoot()) ||
                //// we're sorting the group-nodes and the sort-field IS the first groupByField
                (groupByField.contains(sortProps[0]))
        ) {
            if (this._sortSpecifiers) {

                if (pagedResultTree && isc.isA.ResultSet(subset)) {
                    subset = subset.getAllVisibleRows() || [];
                }

                if (parentNode.groupMembers) {
                    // The parentNode is a group-node in a grid.  Process it if it's also
                    // being sorted.
                    var process = (this._sortSpecifiers.find("property",
                            parentNode.groupName) != null);

                    var isRoot = parentNode == this.getRoot();
                    // if it's the rootNode, process it anyway (there'll be no groupName)
                    process = process || isRoot;

                    if (process) {
                        subset.map(function (record) {
                            if (record._isGroup) {
                                // if the record is a group, add values to it for the fields
                                // being sorted, such that we sort them properly according to
                                // the sort specifiers
                                if (parentNode.groupValue && !record[parentNode.groupName] && isRoot) {
                                    record[parentNode.groupName] = parentNode.groupValue;
                                }
                                if (!record[record.groupName]) {
                                    record[record.groupName] = record.groupValue;
                                }
                            }
                        });
                    }
                }

                // remove any summary-rows from the subset before sorting, and then add them
                // back afterwards, so they're always at the end of the subset
                var summaryRows = subset.findAll(this._summaryRecordFlag, true) || [];
                if (summaryRows.length > 0) subset.removeList(summaryRows);
                subset.setSort(this._sortSpecifiers);
                // Summary rows may be implemented as children or siblings of the
                // header nodes (depending on whether we want them to show up
                // when the group is collapsed).
                if (summaryRows.length > 0) {
                    var addAsChildren = [];
                    for (var i = 0; i < summaryRows.length; ) {
                        if (summaryRows[i] == null) break;
                        var groupSummaries = [],
                            currentSummaryRow = summaryRows[i],
                            // summaryTargetNode flag set up in ListGrid grouping
                            // logic
                            target = currentSummaryRow[this._summaryTargetNode];
                        if (target == null) {
                            addAsChildren.add(currentSummaryRow);
                            i++;
                            continue;
                        } else {

                            do {
                                groupSummaries.add(currentSummaryRow);
                                currentSummaryRow = summaryRows[i+1];
                                i++;
                            } while (currentSummaryRow &&
                                     currentSummaryRow[this._summaryTargetNode] == target);
                            // Slot in the summaries as siblings after the
                            // summary header node.
                            subset.addListAt(groupSummaries, subset.indexOf(target)+1);
                        }
                    }
                    if (addAsChildren.length > 0) {
                        subset.addListAt(addAsChildren, subset.length);
                    }
                }
            }
        }
    }


    return subset;
},

//> @method tree.getChildrenResultSet()
// Returns a ResultSet that provides access to any partially-loaded children of a node.  If the
// node is a leaf, this method returns null.
// @param node (TreeNode) The node whose children you want to fetch.
// @return (ResultSet) List of children for the node, including an empty ResultSet if the node
// has no children.  For a leaf, returns null.
// @see tree.getChildren()
// @see tree.allChildrenLoaded()
// @visibility external
//<
getChildrenResultSet : function (node) {
    var children = this.getChildren(node);
    return (isc.isA.ResultSet(children) ? children : null);
},

//>    @method    tree.getFolders()
//
// Returns all the first-level folders of a node.
// <br><br>
// For load on demand trees (those that only have a partial representation client-side), this
// method will return only nodes that have already been loaded from the server.
//
// @param   node    (TreeNode)    node in question
// @return  (List)              List of immediate children that are folders
//
// @visibility external
//<

getFolders : function (node, normalizer, sortDirection, criteria, context) {
    var folders = this.getChildren(
            node, isc.Tree.FOLDERS_ONLY, normalizer, sortDirection, criteria, context, false, false);

    return folders;
},

//>    @method    tree.getLeaves()
//
// Return all the first-level leaves of a node.
// <br><br>
// For load on demand trees (those that only have a partial representation client-side), this
// method will return only nodes that have already been loaded from the server.
//
// @param   node    (TreeNode)    node in question
// @return          (List)      List of immediate children that are leaves.
//
// @visibility external
//<

getLeaves : function (node, normalizer, sortDirection, criteria, context) {
    var leaves = this.getChildren(
            node, isc.Tree.LEAVES_ONLY, normalizer, sortDirection, criteria, context, false, false);

    return leaves;
},

//> @method Tree.getLevelNodes()
// Get all nodes of a certain depth within the tree, optionally starting from
// a specific node.  Level 0 means the immediate children of the passed node,
// so if no node is passed, level 0 is the children of root
// @param depth (integer) level of the tree
// @param [node] (TreeNode) option node to start from
// @return (Array of TreeNode)
//<
getLevelNodes : function (depth, node) {

    if (this.indexByLevel && (node == null || node == this.getRoot())) {
        return this._levelNodes[depth] || [];
    } else {
        if (!node) node = this.getRoot();
        var children = this.getChildren(node);

        if (depth == 0) {

            if (isc.isA.ResultSet(children)) {
                return children.getAllLoadedRows();
            } else {
                return children;
            }
        }
        var result = [];
        if (children) {
            var length = (isc.isA.ResultSet(children) ?
                    children._getCachedLength() : children.getLength());
            for (var i = 0; i < length; ++i) {
                var child = children.getCachedRow(i),
                    nestedChildren = (
                        child != null && this.getLevelNodes(depth - 1, child));
                if (nestedChildren) result.addList(nestedChildren);
            }
        }
        return result;
    }
},

getDepth : function () {
    if (this._levelNodes) return this._levelNodes.length;
    return null;
},

//>    @method    tree.hasChildren()
//
// Returns true if this node has any children.
//
// @param    node            (TreeNode)            node in question
// @return                    (Boolean)            true if the node has children
//
// @visibility external
//<

hasChildren : function (node, displayNodeType) {
    var children = this.getChildren(node, displayNodeType);
    return (children != null && !children.isEmpty());
},

//>    @method    tree.hasFolders()
//
// Return true if this this node has any children that are folders.
//
// @param    node    (TreeNode)    node in question
// @return         (Boolean)   true if the node has children that are folders
//
// @visibility external
//<
hasFolders : function (node) {
    return this.hasChildren(node, isc.Tree.FOLDERS_ONLY);
},

//>    @method    tree.hasLeaves()
//
//  Return whether this node has any children that are leaves.
//
//    @param    node    (TreeNode)    node in question
//    @return            (Boolean)   true if the node has children that are leaves
//
// @visibility external
//<
hasLeaves : function (node) {
    return this.hasChildren(node, isc.Tree.LEAVES_ONLY);
},


//>    @method    tree.isDescendantOf()
//            Is one node a descendant of the other?
//
//        @param    child    (TreeNode)    child node
//        @param    parent    (TreeNode)    parent node
//        @return            (Boolean)    true == parent is an ancestor of child
// @visibility external
//<
isDescendantOf : function (child, parent) {
    if (child == parent) return false;
    var nextParent = child;
    while (nextParent != null) {
        if (nextParent == parent) return true;
        nextParent = nextParent[this.parentProperty];
    }
    return false;
},

//>    @method    tree.getDescendants()
//
// Returns the list of all descendants of a node.  Note: this method can be very slow,
// especially on large trees because it assembles a list of all descendants recursively.
// Generally, +link{method:Tree.find} in combination with +link{method:Tree.getChildren} will
// be much faster.
// <br><br>
// For load on demand trees (those that only have a partial representation client-side), this
// method will return only nodes that have already been loaded from the server.
//
// @param   [node]  (TreeNode)    node in question (the root node is assumed if none is specified)
// @return  (List)              List of descendants of the node.
//
// @visibility external
//<

getDescendants : function (node, displayNodeType, condition) {
    if (!node) node = this.root;

    // create an array to hold the descendants
    var list = [];

    // if condition wasn't passed in, set it to an always true condition
    // XXX convert this to a function if a string, similar to getChildren()
    if (!condition) condition = isc.Class.RET_TRUE;

    // if the node is a leaf, return the empty list
    if (this.isLeaf(node)) return list;

    // iterate through all the children of the node
    // Note that this can't depend on getChildren() to subset the nodes,
    //    because a folder may have children that meet the criteria but not meet the criteria itself.
    var children = this.getChildren(node);
    if (!children) {
        return list;
    }



    // for each child
    var length = (isc.isA.ResultSet(children) ?
            children._getCachedLength() : children.getLength());
    for (var i = 0; i < length; ++i) {
        // get a pointer to the child
        var child = children.getCachedRow(i);

        if (child == null) {
            // Do nothing.
        } else if (this.isFolder(child)) { // if that child is a folder
            // if we're not exluding folders, add the child
            if (displayNodeType != isc.Tree.LEAVES_ONLY && condition(child)) {
                list[list.length] = child;
            }

            // now concatenate the list with the descendants of the child
            list = list.concat(this.getDescendants(child, displayNodeType, condition));

        } else {
            // if we're not excluding leaves, add the leaf to the list
            if (displayNodeType != isc.Tree.FOLDERS_ONLY && condition(child)) {
                list[list.length] = child;
            }
        }
    }
    // finally, return the entire list
    return list;
},

//>    @method    tree.getDescendantFolders()
//
// Returns the list of all descendants of a node that are folders.  This works just like
// +link{method:Tree.getDescendants}, except leaf nodes are not part of the returned list.
// Like +link{method:Tree.getDescendants}, this method can be very slow for large trees.
// Generally, +link{method:Tree.find} in combination with +link{method:Tree.getFolders}
// will be much faster.
// <br><br>
// For load on demand trees (those that only have a partial representation client-side), this
// method will return only nodes that have already been loaded from the server.
//
// @param   [node]      (TreeNode)    node in question (the root node is assumed if none is specified)
// @return  (List)        List of descendants of the node that are folders.
//
// @visibility external
//<

getDescendantFolders : function (node, condition) {
     return this.getDescendants(node, isc.Tree.FOLDERS_ONLY, condition)
},
//>    @method    tree.getDescendantLeaves()
//
// Returns the list of all descendants of a node that are leaves.  This works just like
// +link{method:Tree.getDescendants}, except folders are not part of the returned list.
// Folders are still recursed into, just not returned.  Like +link{method:Tree.getDescendants},
// this method can be very slow for large trees.  Generally, +link{method:Tree.find} in
// combination with +link{method:Tree.getLeaves} will be much faster.
// <br><br>
// For load on demand trees (those that only have a partial representation client-side), this
// method will return only nodes that have already been loaded from the server.
//
// @param   [node]      (TreeNode)    node in question (the root node is assumed if none specified)
// @return  (List)        List of descendants of the node that are leaves.
//
// @visibility external
//<

getDescendantLeaves : function (node, condition) {
    return this.getDescendants(node, isc.Tree.LEAVES_ONLY, condition)
},


//>    @method    tree.dataChanged()    (A)
//
// Called when the structure of this tree is changed in any way.  Intended to be observed.
// <br><br>
// Note that on a big change (many items being added or deleted) this may be called multiple times
//
// @visibility external
//<
dataChanged : function () {},


//
// adding nodes
//

//> @groupDef sharingNodes
//
// For local Trees, that is, Trees that don't use load on demand, SmartClient supports setting
// up the Tree structure by setting properties such as "childrenProperty", directly on data
// nodes.  This allows for simpler, faster structures for many common tree uses, but can create
// confusion if nodes need to be shared across Trees.
// <P>
// <b>using one node in two places in one Tree</b>
// <P>
// To do this, either clone the shared node like so:<pre>
//
//     tree.add(isc.addProperties({}, sharedNode));
//
// </pre> or place the shared data in a shared subobject instead.
// <P>
// <b>sharing nodes or subtrees across Trees</b>
// <P>
// Individual nodes within differing tree structures can be shared by two Trees only if
// +link{Tree.nameProperty}, +link{Tree.childrenProperty}, and +link{Tree.openProperty} have
// different values in each Tree.
// <P>
// As a special case of this, two Trees can maintain different open state across a single
// read-only structure as long as just "openProperty" has a different value in each Tree.
//
// @title Sharing Nodes
// @visibility external
//<


//>    @method    tree.add()
//
// Add a single node under the specified parent
//
// @param    node        (TreeNode)    node to add
// @param    parent        (String or TreeNode)    Parent of the node being added.  You can pass
//                                          in either the +link{TreeNode} itself, or a path to
//                                            the node (as a String), in which case a
//                                            +link{method:Tree.find} is performed to find
//                                            the node.
// @param    [position]    (number)    Position of the new node in the children list. If not
//                                    specified, the node will be added at the end of the list.
// @return (TreeNode or null) The added node. Will return null if the node was not added (typically
//    because the specified <code>parent</code> could not be found in the tree).
//
// @see group:sharingNodes
// @see method:Tree.addList
// @visibility external
//<
// Note: the node passed in is directly integrated into the tree, so you will see properties
// written onto it, etc. We may want to duplicate it before adding, then return a pointer
// to the node as added.
add : function (node, parent, position) {
    return this._add(node, parent, position);
},
_add : function (node, parent, position) {
    if (parent == null && this.modelType == isc.Tree.PARENT) {
        var parentId = node[this.parentIdField];
        if (parentId != null) parent = this.findById(parentId);
    }
    // normalize the parent parameter into a node
    if (isc.isA.String(parent)) {
        parent = this.find(parent);
    } else if (!this.getParent(parent) && parent !== this.getRoot()) {
        // if parent is not in the tree, bail
        isc.logWarn('Tree.add(): specified parent node:' + this.echo(parent) +
                    ' is not in the tree, returning');
        return null;
    }
    // if the parent wasn't found, return null
    // XXX note that we could actually add to the root, but that's probably not what you want
    if (! parent) {
        // get the parentName of the node
        var parentPath = this.getParentPath(node);
        if (parentPath) parent = this.find(parentPath);
        if (! parent) return null;
    }

    this.__add(node, parent, position);

    this._clearNodeCache(true);

    // call the dataChanged method
    this.dataChanged();

    return node;
},

_reportCollision : function (ID) {
    if (this.reportCollisions) {
        this.logWarn("Adding node to tree with id property set to:"+ ID +
            ". A node with this ID is already present in this Tree - that node will be " +
            "replaced. Note that this warning may be disabled by setting the " +
            "reportCollisions attribute to false.");
    }
},

// internal interface, used by _linkNodes(), _addList(), and any other place where we are adding a
// batch of new nodes to the Tree.  This implementation doesn't call _clearNodeCache() or
// dataChanged() and assumes you passed in the parent node as a node object, not a string.

__add : function (node, parent, position) {
    var pagedResultTree = (
            isc.ResultTree != null && isc.isA.ResultTree(this) && this.isPaged());


    var info = {};
    parent[this._recursionCountProperty] = 1 + (parent[this._recursionCountProperty] || 0);
    this._preAdd(node, parent, true, info);
    var deltaParentLength = info.deltaParentLength,
        grandParent = info.grandParent,
        origParentLength = info.origParentLength,
        children = info.children;


    // If position wasn't specified, set it as the last item.
    // NOTE: Specifying position > children.length is technically wrong but happens easily
    // with a remove followed by an add.
    if (position == null || position > children.length) {
        children.add(node);
    } else {
        // add the node to the parent - addAt is slower, so only do this if your position was
        // passed in
        children.addAt(node, position);
    }

    this._postAdd(node, parent, position, info);
    var grandChildren = (pagedResultTree
            ? this._canonicalizeChildren(node, info.grandChildren, false) : info.grandChildren);

    if (pagedResultTree) {
        var fromParent = (parent[this.canReturnOpenSubfoldersProperty] != null),
            openSubfoldersAllowed = (fromParent ?
                parent[this.canReturnOpenSubfoldersProperty] : this.canReturnOpenFolders);

        if (!openSubfoldersAllowed &&
            this.isOpen(node) &&
            grandChildren != null && !grandChildren.isEmpty())
        {
            this.logWarn(
                "Adding the open folder node '" + this.getPath(node) + "' as a child of the " +
                "parent node '" + this.getPath(parent) + "' is contradictory to the setting " +
                "of the " + (fromParent ? "'" + this.canReturnOpenSubfoldersProperty + "' " +
                "property of the parent node." : "'canReturnOpenFolders' property of the tree."));
        }
    }

    var deltaLength = 0;
    if (pagedResultTree && isc.isA.ResultSet(grandChildren)) {
        if (!(grandChildren.lengthIsKnown() && grandChildren.allMatchingRowsCached())) {
            this._setVisibleDescendantsCached(node, false, parent, false);
        }
    } else if (grandChildren != null) {
        // If the node has children, recursively add them to the node.  This ensures that
        // their parent link is set up correctly.

        // Handle children being specified as a single element recursively.
        // _add will slot the element into the new children array.
        if (!isc.isAn.Array(grandChildren)) {
            this.__add(grandChildren, node);
        } else if (grandChildren.length > 0) {
            this.__addList(grandChildren, node);
        }

        // if a children array is present, mark the node as loaded even if the children array
        // is empty - this is a way of indicating an empty folder in XML or JSON results
        this.setLoadState(node, isc.Tree.LOADED);
    }


    if ((--parent[this._recursionCountProperty]) == 0) {
        delete parent[this._recursionCountProperty];

        if (grandParent) {
            // Check if changes in the length of the parent affect the length of the grandParent.
            deltaParentLength += (this._getNodeLengthToParent(parent, grandParent) - origParentLength);

            // Update the lengths of some of the ancestors.
            this._updateParentLengths(grandParent, deltaParentLength);
        }
    }
},

_removeCollision : function (collision) {

    var pagedResultTree = (
            isc.ResultTree != null && isc.isA.ResultTree(this) && this.isPaged());
    if (pagedResultTree) {
        var parent = this.getParent(collision),
            siblings = (parent != null ? this.getChildren(parent) : null);

        if (isc.isA.ResultSet(siblings)) {
            var j = siblings.indexOf(collision);
            if (j != -1) {

                siblings.fillCacheData([null], j);
            }
            return;
        }
    }

    // Otherwise simply remove the collision node.
    this._remove(collision);
},

_findCollision : function (node) {

    var ID = node[this.idField];
    if (ID != null && this.modelType == isc.Tree.PARENT) {
        // note: in modelType:"children", while we do maintain a nodeIndex, an idField is not
        // required and the tree does not depend on globally unique ids
        var collision = this.findById(ID);
        if (collision) {
            return collision;
        }
    }
    return null;
},


_preAdd : function (node, parent, removeCollisions, info) {


    // convert name to a string - we rely on this fact in getTitle() and possibly other
    // places.  Also, ultimately getName() will convert it to a string anyway and at that
    // point, if new values are not strings from the start, sorting won't work as expected (the
    // non-strings will be segregated from the strings).
    this.getName(node);

    // convert the parent node to a folder if necessary
    var deltaParentLength = info.deltaParentLength = this.convertToFolder(parent);

    var grandParent = info.grandParent = (parent != this.root && this.getParent(parent)),
        origParentLength = info.origParentLength = (
            grandParent && this._getNodeLengthToParent(parent, grandParent));

    var collision = this._findCollision(node);
    if (collision) {
        var ID = collision[this.idField];
        this._reportCollision(ID);
        if (removeCollisions) {
            this._removeCollision(collision);
        }
    }

    var children = parent[this.childrenProperty];
    if (!children) children = parent[this.childrenProperty] = [];
    info.children = children;

    // if the children attr contains a single object, assume it to be a single child of
    // the node.

    var childrenResultSet = info.childrenResultSet = isc.isA.ResultSet(children);
    if (children != null && !isc.isAn.Array(children) && !childrenResultSet) {
        parent[this.childrenProperty] = children = [children];
    }

    // parentId-based loading can be used without the parentId
    // appearing in the child nodes, for example, if loading nodes from a large XML structure,
    // we may use the parentId to store the XPath to the parent, and load children via accessing
    // the parentElement.childNodes Array.
    //
    // set the parentId on the node if it isn't set already
    var idField = this.idField
    // just do this unconditionally - it doesn't make sense for the parentId field of the child
    // not to match the idField of the parent.
    node[this.parentIdField] = parent[idField];
    // link to the parent
    node[this.parentProperty] = parent;

    // Set the node to the default LoadState.
    delete node._loadState;
},


_postAdd : function (node, parent, position, info) {
    var idField = this.idField;

    // Link to the Tree (by String ID, not direct pointer).
    node[this.treeProperty] = this.ID;

    // Update nodeIndex.
    // If we don't do a null check there are cases where null values get added into the
    // nodeIndex and children get added to the wrong parent, i.e. when using autoFetch and
    // modeltype 'children' within a treegrid.
    if (node[idField] != null) this.nodeIndex[node[idField]] = node;

    if (!info.childrenResultSet) {
        // Current assumption whenever loading subtrees is that if any
        // children are returned for a node, it's the complete set, and the node is marked "loaded".
        this.setLoadState(parent, isc.Tree.LOADED);
    }

    this._addToLevelCache(node, parent, position);

    // Set the cached length of the node.
    var nodeIsFolder = this.isFolder(node);
    node[this._cachedLengthProperty] = (
        this.openDisplayNodeType != (nodeIsFolder ? isc.Tree.LEAVES_ONLY : isc.Tree.FOLDERS_ONLY) ? 1 : 0);

    var grandChildren = info.grandChildren = node[this.childrenProperty],
        deltaLength;
    if (grandChildren != null) {
        node[this.childrenProperty] = [];
        deltaLength = this._getNodeLengthToParent(node, parent);
    } else {
        deltaLength = this._getNodeLengthToParent(node, parent);

        // canonicalize the isFolder flag on the node
        var wasFolder = this.isFolder(node),
            isFolder = node[this.isFolderProperty];

        // convert to boolean
        if (isFolder != null && !isc.isA.Boolean(isFolder)) {
            isFolder = isc.booleanValue(isFolder, true);
        }

        // ResultTree nodes that don't specify isFolder default to isFolder: true,
        // But Trees work exactly the opposite way
        if (isFolder == null && this.defaultIsFolder) isFolder = true;

        if (isFolder && !wasFolder) {
            deltaLength += this.convertToFolder(node);
        }
        node[this.isFolderProperty] = isFolder;
    }

    // Add deltaLength to the length of the parent.

    parent[this._cachedLengthProperty] += deltaLength;
},

_addToLevelCache : function (nodes, parent, position) {
    if (!this.indexByLevel) return;

    var level = this.getLevel(parent);
    if (!this._levelNodes[level]) this._levelNodes[level] = [];
    var levelNodes = this._levelNodes[level];

    // Special case - array is empty, just add the node to the end
    if (levelNodes.length == 0) {
        if (!isc.isAn.Array(nodes)) {
            levelNodes.push(nodes);
        } else {
            levelNodes.concat(nodes);
        }
    } else {
        // Make sure none of these nodes is already cached
        if (!isc.isAn.Array(nodes)) {
            if (levelNodes.contains(nodes)) return;
        } else {
            var cleanNodes = [];
            for (var j = 0; j < nodes.length; j++) {
                if (!levelNodes.contains(nodes[j])) {
                    cleanNodes.push(nodes[j]);
                }
            }
        }
        // Slot the node(s) into the level cache at the correct position
        var startedThisParent = false,
            siblingCount = 0,
            i = 0;
        for (i; i < levelNodes.length; i++) {
            if (this.getParent(levelNodes[i]) == parent) {
                startedThisParent = true;
            } else if (startedThisParent) {
                break;
            } else {
                continue;
            }
            // Exact equality is important - position 0 means first, position null means last
            if (siblingCount === position) {
                break;
            }
            siblingCount++;
        }

        if (!isc.isAn.Array(nodes)) {
            levelNodes.splice(i, 0, nodes);
        } else {
            // Using concat() because splice, push and unshift all insert the array itself,
            // not the array's contents, and a solution involving a Javascript loop would
            // presumably cause far more churn in the array than passing everything in a
            // single native call and letting the browser deal with it
            if (i == 0) {
                this._levelNodes[level] = cleanNodes.concat(levelNodes);
            } else if (i == levelNodes.length) {
                this._levelNodes[level] = levelNodes.concat(cleanNodes);
            } else {
                this._levelNodes[level] =
                            levelNodes.slice(0, i).concat(cleanNodes, levelNodes.slice(i));
            }
        }
    }
},

//>    @method    tree.addList()
//
// Add a list of nodes to some parent.
//
// @param   nodeList      (List of TreeNode) The list of nodes to add
// @param    parent        (String or TreeNode)    Parent of the nodes being added.  You can pass
//                                          in either the +link{TreeNode} itself, or a path to
//                                            the node (as a String), in which case a
//                                            +link{method:Tree.find} is performed to find
//                                            the node.
// @param    [position]    (number)    Position of the new nodes in the children list. If not
//                                    specified, the nodes will be added at the end of the list.
// @return    (List)    List of added nodes.
//
// @see group:sharingNodes
// @visibility external
//<
addList : function (nodeList, parent, position) {
    return this._addList(nodeList, parent, position);
},
_addList : function (nodeList, parent, position) {
    // normalize the parent property into a node
    if (isc.isA.String(parent)) parent = this.find(parent);

    // if the parent wasn't found, return null
    if (!parent) return null;

    this.__addList(nodeList, parent, position);

    if (!this._deferDataChanged) {
        this._clearNodeCache(true);
        this.dataChanged();
    }

    return nodeList;
},

__addList : function (nodeList, parent, position) {
    // Simply call add repeatedly for each child.
    var length = (
            isc.isA.ResultSet(nodeList) ? nodeList._getCachedLength() : nodeList.getLength());
    for (var i = 0; i < length; ++i) {
        var node = nodeList.getCachedRow(i);
        if (node != null) {
            this.__add(node, parent, position != null ? (position + i) : null);
        }
    }
},


// Structural changes
// --------------------------------------------------------------------------------------------

//>    @method    tree.move()
//
// Moves the specified node to a new parent.
//
// @param    node        (TreeNode)    node to move
// @param    newParent    (TreeNode)    new parent to move the node to
// @param    [position]    (Integer)    Position of the new node in the children list. If not
//                                    specified, the node will be added at the end of the list.
// @visibility external
//<
move : function (node, newParent, position) {
    return this._move(node, newParent, position);
},
_move : function (node, newParent, position) {
    this.moveList([node], newParent, position);
},


//>    @method    tree.moveList()
//            Move a list of nodes under a new parent.
//
//        @group    dataChanges
//
//        @param    nodeList    (List of TreeNode)    list of nodes to move
//        @param    newParent    (TreeNode)    new parent node
//        @param    [position]    (number)    position to place new nodes at.
//                                        If not specified, it'll go at the end
//<
moveList : function (nodeList, newParent, position) {

    for (var i = nodeList.length, duplicated = false; i--; ) {
        var node = nodeList[i];
        if (node == newParent || this.isDescendantOf(newParent, node)) {
            if (!duplicated) {

                duplicated = true;
                nodeList = nodeList.duplicate();
            }

            this.logWarn(
                "Tree.moveList():  Specified node '" + this.getPath(node) + "' is an " +
                "ancestor of the new parent node '" + this.getPath(newParent) + "' and " +
                "therefore cannot be made a child of that parent.  The specified node will " +
                "remain where it is.");
            nodeList.removeAt(i);
        }
    }
    if (nodeList.length == 0) {

        return;
    }

    var node = nodeList[0],
        oldParent = this.getParent(node),
        oldPosition = this.getChildren(oldParent).indexOf(node);

    // internal flag that prevents dataChanged from firing 3 times (from the individual
    // remove/add calls, and then at the end of this method)
    this._deferDataChanged = true;

    // remove the nodes from their old parents
    this._removeList(nodeList);

    // the nodes may be moving within their own parent
    if (newParent == oldParent && nodeList.length == 1) {
        // if there's exactly one node moving, and it moved later in the list, reduce the
        // insertion point by one to compensate for the node's removal
        if (position > oldPosition) position--;
    } else {
        // otherwise just make sure that if the parent's child list has shortened because some
        // nodes from this parent were removed, we don't leave gaps.

        var children = this.getChildren(newParent);
        if (children) {
            var childrenLength = (isc.isA.ResultSet(children) ?
                    children._getCachedLength() : children.getLength());
            if (position > childrenLength) {
                position = childrenLength;
            }
        }
    }

    // add the nodes to the new parent
    this._addList(nodeList, newParent, position);
    // call the dataChanged method to notify anyone who's observing it
    delete this._deferDataChanged;
    this._clearNodeCache(true);
    this.dataChanged();
},

//>    @method    tree.remove()
//
// Removes a node, along with all its children.
//
// @param    node    (TreeNode)    node to remove
// @return            (Boolean)    true if the tree was changed as a result of this call
//
// @visibility external
//<
remove : function (node, noDataChanged) {
    return this._remove(node, noDataChanged);
},
_remove : function (node, noDataChanged) {
    // get the parent of the node
    var parent = this.getParent(node);
    if (! parent) return false;

//    this.logWarn("removing: " + isc.Log.echoAll(node) + " from: " + isc.Log.echoAll(parent));

    // get the children list of the parent and the name of the node
    var children = this.getChildren(parent);
    if (children) {
        // Figure out the child number.
        var position = children.indexOf(node);
        if (position != -1) {
            this.__remove(node, parent, children, position);

            // This can be expensive if we're called iteratively for a large set of nodes  -
            // e.g. via _removeList(), so consult noDataChanged flag.
            if (!noDataChanged) {
                // Mark the entire tree as dirty.
                this._clearNodeCache(true);
                // Call the dataChanged method to notify anyone who's observing it.
                this.dataChanged();
            }
            return true;
        }
    }

    return false;
},


__remove : function (node, parent, children, position) {
    var pagedResultTree = (
            isc.ResultTree != null && isc.isA.ResultTree(this) && this.isPaged());


    parent[this._recursionCountProperty] = 1 + (parent[this._recursionCountProperty] || 0);

    var info = {};
    this._preRemove(node, parent, info);
    var deltaLength = info.deltaLength,
        grandParent = info.grandParent,
        origParentLength = info.origParentLength;

    // Remove the node
    children.remove(node);

    this._postRemove(node, parent, info);

    // Update the length of the ancestors according to the removal of the child node.
    // If the removed node was the last of the parent's children, then the parent will look
    // like a leaf to the grandparent, which may affect the lengths of the grandparent and
    // more distant ancestors.
    var grandParent = info.grandParent;

    if ((--parent[this._recursionCountProperty]) == 0) {
        delete parent[this._recursionCountProperty];
        if (grandParent) {
            var deltaParentLength = (
                    this._getNodeLengthToParent(parent, grandParent) - origParentLength);
            this._updateParentLengths(grandParent, deltaParentLength);
        }
    }
},

_preRemove : function (node, parent, info) {
    // Recursively remove the node and its children from the node index.  We do this rather
    // than call _remove() because we don't want to remove the children from the node
    // itself, just from the tree's cache
    this._removeFromNodeIndex(node);

    info.deltaLength = -this._getNodeLengthToParent(node, parent);
    var grandParent = info.grandParent = (parent != this.root && this.getParent(parent));
    info.origParentLength = grandParent && this._getNodeLengthToParent(parent, grandParent);

    this._removeFromLevelCache(node);

    delete node[this.parentProperty];
    delete node[this.treeProperty];
},

_postRemove : function (node, parent, info) {

    // Update the length of the parent according to the removal of the child node.
    var deltaLength = info.deltaLength;

    parent[this._cachedLengthProperty] += deltaLength;
},

_removeFromNodeIndex : function (node) {
    delete this.nodeIndex[node[this.idField]];
    var children = this.getChildren(node, null, null, null, null, null, true, false);
    if (!children) return;
    var length = (isc.isA.ResultSet(children) ?
            children._getCachedLength() : children.getLength());
    for (var i = 0; i < length; ++i) {
        var child = children.getCachedRow(i);
        if (child != null) {
            this._removeFromNodeIndex(child);
        }
    }
},

//>    @method    tree.removeList()
//
// Remove a list of nodes (not necessarily from the same parent), and all children of those nodes.
//
// @param    nodeList    (List of TreeNode)    list of nodes to remove
// @return                (boolean)    true if the tree was changed as a result of this call
//
// @visibility external
//<
removeList : function (nodeList) {
    return this._removeList(nodeList);
},
_removeList : function (nodeList) {
    // this is our return value
    var changed = false;

    // simply call remove for each node that was removed

    // We can be passed the result of tree.getChildren() - if that happens, then remove() will
    // operate on the same array that we're iterating over, which means nodeList will shrink as
    // we iterate, so count down from nodeList.length instead of counting up.
    // Also note that getChildren() may return a ResultSet so the nodeList argument may be a
    // ResultSet.
    var i = (isc.isA.ResultSet(nodeList) ? nodeList._getCachedLength() : nodeList.getLength());
    while (i--) {
        var node = nodeList.getCachedRow(i);
        if (node != null) {
            if (this._remove(node, true)) {
                changed = true;
            }
        }
    }

    // call the dataChanged method to notify anyone who's observing it
    if (changed && !this._deferDataChanged) {
        this._clearNodeCache(true);
        this.dataChanged();
    }

    return changed;
},

_removeFromLevelCache : function (node, level) {
    if (!this.indexByLevel) return;

    level = level || this.getLevel(node) - 1;

    // Remove index entries for descendants first
    var nodeChildren = this.getChildren(node);
    if (nodeChildren) {
        var length = (isc.isA.ResultSet(nodeChildren) ?
                nodeChildren._getCachedLength() : nodeChildren.getLength());
        for (var i = 0; i < length; ++i) {
            var child = nodeChildren.getCachedRow(i);
            if (child != null) {
                this._removeFromLevelCache(child, level + 1);
            }
        }
    }

    if (this._levelNodes[level]) {
        var levelNodes = this._levelNodes[level];
        for (var i = 0; i < levelNodes.length; i++) {
            if (levelNodes[i] == node) {
                levelNodes.splice(i, 1);
                break;
            }
        }
    }
},


// Loading and unloading of children
// --------------------------------------------------------------------------------------------


//>    @method    tree.getLoadState()
// What is the loadState of a given folder?
//
// @param node (TreeNode) folder in question
// @return (LoadState) state of the node
// @group loadState
// @visibility external
//<
getLoadState : function (node) {
    if (!node) return null;
    if (this.isLeaf(node)) return isc.Tree.LOADED;
    if (!node._loadState) return this.defaultLoadState;
        return node._loadState;
},

//>    @method    tree.isLoaded()
// For a databound tree, has this folder either already loaded its children or is it in the
// process of loading them.
//
// @param node (TreeNode) folder in question
// @return (Boolean) folder is loaded or is currently loading
// @group loadState
// @visibility external
//<
isLoaded : function (node) {
    var loadState = this.getLoadState(node);
    return (
        loadState == isc.Tree.LOADED ||
        loadState == isc.Tree.LOADING ||
        loadState == isc.Tree.LOADED_PARTIAL_CHILDREN);
},

//>    @method    tree.allChildrenLoaded()
// For a databound tree, do the children of this folder form a ResultSet with a full cache.
// <P>
// Note that this method only applies to +link{resultTree.fetchMode} "paged".
// @param node (TreeNode) folder in question
// @return (Boolean) folder's children are a ResultSet with a full cache
// @group loadState
// @see tree.getChildrenResultSet()
// @visibility external
//<
allChildrenLoaded : function (node) {
    var loadState = this.getLoadState(node);
    return (loadState == isc.Tree.LOADED);
},

//>    @method    tree.setLoadState()
// Set the load state of a particular node.
// @group loadState
// @param node (TreeNode) node in question
// @param newState (string) new state to set to
// @return (boolean) folder is loaded or is currently loading
//<
setLoadState : function (node, newState) {
    var prevState = this.getLoadState(node);
    node._loadState = newState;
    newState = this.getLoadState(node);

    // The load state of the node affects the _visibleDescendantsCachedProperty set on the
    // nodes of paged ResultTrees.  Update the value of that property of the node if necessary.
    var prevFlag = (
            prevState === isc.Tree.LOADED ||
            prevState === isc.Tree.LOADED_PARTIAL_CHILDREN),
        newFlag = (
            newState === isc.Tree.LOADED ||
            newState === isc.Tree.LOADED_PARTIAL_CHILDREN),
        pagedResultTree = (
            isc.ResultTree != null && isc.isA.ResultTree(this) && this.isPaged());
    if (pagedResultTree && (prevFlag != newFlag)) {
        // Only update the _visibleDescendantsCachedProperty if it has been set before on the
        // node.
        var parent = this.getParent(node);
        if (isc.isA.Boolean(node[this._visibleDescendantsCachedProperty])) {
            this._setVisibleDescendantsCached(node, null, parent, false);
        }
    }
},

//>    @method    tree.loadRootChildren()
//            Load the root node's children.
//            Broken out into a special function so you can override more cleanly
//                 (default implementation just calls loadChildren)
//      @param  [callback]  (callback) StringMethod to fire when loadChildren() has loaded data.
//        @group    loadState
//<
loadRootChildren : function (callback) {
    this.loadChildren(this.root, callback);
},

//>    @method    tree.loadChildren()
// Load the children of a given node.
// <P>
// For a databound tree this will trigger a fetch against the Tree's DataSource.
//
//
// @param node    (TreeNode)    node in question
// @param [callback] (DSCallback) Optional callback (stringMethod) to fire when loading
//                      completes. Has a single param <code>node</code> - the node whose
//                      children have been loaded, and is fired in the scope of the Tree.
// @group loadState
// @visibility external
//<
loadChildren : function (node, callback) {
    if (!node) {
        node = this.root;
    }
    var pagedResultTree = (
            isc.ResultTree != null && isc.isA.ResultTree(this) && this.isPaged());
    if (pagedResultTree) {
        this._loadChildren(node, 0, this.resultSize, callback);
    } else {
        this._loadChildren(node, null, null, callback);
    }
},


_loadChildren : function (node, start, end, callback) {


    // mark the node as loaded
    this.setLoadState(node, isc.Tree.LOADED);
    if (callback) {
        //Fire the callback in the scope of this tree
        this.fireCallback(callback, "node", [node], this);
    }
},

//>    @method    tree.unloadChildren()
// Unload the children of a folder, returning the folder to the "unloaded" state.
//
// @param node (TreeNode) folder in question
// @group loadState
// @visibility external
//<
// NOTE internal parameter:    [displayNodeType]    (DisplayNodeType)    Type of children to drop
unloadChildren : function (node, displayNodeType) {
    if (node == null || this.isLeaf(node)) {
        return;
    }

    var droppedChildren, newChildren, newLoadState;
    if (displayNodeType == isc.Tree.LEAVES_ONLY) {
        // set the children array to just the folders
        droppedChildren = this.getLeaves(node);
        newChildren = this.getFolders(node);
        // and mark the node as only the folders are loaded
        newLoadState = isc.Tree.FOLDERS_LOADED;
    } else {
        // clear out the children Array
        droppedChildren = node[this.childrenProperty];
        newChildren = [];
        // and mark the node as unloaded
        newLoadState = isc.Tree.UNLOADED;
    }

    var parent, origLength;
    if (droppedChildren) {
        parent = (node != this.root && this.getParent(node));
        origLength = parent && this._getNodeLengthToParent(node, parent);

        for (var i = 0; i < droppedChildren.length; i++) {
            var droppedChild = droppedChildren[i];

            // take the droppedChildren out of the node index
            // NOTE: we shouldn't just call _remove() to do this.  unloadChildren() is essentially
            // discarding cache, whereas calling _remove() in a dataBound tree would actually kick off a
            // DataSource "remove" operation
            this._removeFromNodeIndex(droppedChild);


            node[this._cachedLengthProperty] -= this._getNodeLengthToParent(droppedChild, node);
        }
    }

    node[this.childrenProperty] = newChildren;
    this.setLoadState(node, newLoadState);

    if (droppedChildren && parent) {
        // Update the lengths of the ancestors of the dropped children.  The children's parent,
        // node, already has had its length updated.  Now just update the length of the node's
        // ancestors.
        var deltaLength = (this._getNodeLengthToParent(node, parent) - origLength);
        this._updateParentLengths(parent, deltaLength);
    }

    // mark the tree as dirty and note that the data has changed
    this._clearNodeCache(true);
    this.dataChanged();
},

//>    @method    tree.reloadChildren()
// Reload the children of a folder.
//
// @param node (TreeNode) node in question
// @group loadState
// @visibility external
//<

reloadChildren : function (node, displayNodeType) {
    this.unloadChildren(node, displayNodeType);
    this.loadChildren(node, displayNodeType);
},


//
//    open and close semantics for a set of tree nodes
//


// clears the open node cache (used by getOpenList())
// and optionally the all node cache (used by getNodeList()).
_clearNodeCache : function (allNodes) {
    if (allNodes) this._allListCache = null;
    this._openListCache = null;
},

//>    @method    tree.isOpen()
//
// Whether a particular node is open or closed (works for leaves and folders).
//
// @param    node    (TreeNode)    node in question
// @return  (Boolean)           true if the node is open
//
// @visibility external
//<
isOpen : function (node) {
    return node != null && !!node[this.openProperty];
},


//>    @method    tree.getOpenFolders()
//        Return the list of sub-folders of this tree that have been marked as open.
//        Note: unlike tree.getOpenList(), this only returns *folders* (not files),
//            and this will return nodes that are open even if their parent is not open.
//        @group    openList
//
//        @param    node    (TreeNode)    node to start with.  If not passed, this.root will be used.
//<
getOpenFolders : function (node) {
    if (node == null) node = this.root;
    var openProperty = this.openProperty;
    var openNodes = this.getDescendantFolders(node, function (node) {
        return node[openProperty];
    });
    if (this.isOpen(node)) openNodes.add(node);
    return openNodes;
},

//>    @method    tree.getOpenFolderPaths()
//        Return the list of sub-folders of this tree that have been marked as open.
//        Note: unlike tree.getOpenList(), this only returns *folders* (not files),
//            and this will return nodes that are open even if their parent is not open.
//        @group    openList
//
//        @param    node    (TreeNode)    node to start with.  If not passed, this.root will be used.
//<
getOpenFolderPaths : function (node) {
    var openNodes = this.getOpenFolders(node);
    for (var i = 0; i < openNodes.length; i++) {
        openNodes[i] = this.getPath(openNodes[i]);
    }
    return openNodes;
},

//>    @method    tree.changeDataVisibility()    (A)
// Open or close a node.<br><br>
//
// Note that on a big change (many items being added or deleted) this may be called multiple times.
//
//        @group    openList
//
//        @param    node        (TreeNode)    node in question
//        @param    newState    (boolean)    true == open, false == close
//      @param  [callback] (callback) Optional callback (stringMethod) to fire when loading
//                      completes. Has a single param <code>node</code> - the node whose
//                      children have been loaded, and is fired in the scope of the Tree.
//<
changeDataVisibility : function (node, newState, callback) {
//!DONTOBFUSCATE  (obfuscation breaks the inline function definitions)

    // if they're trying to open a leaf return false
    if (this.isLeaf(node)) {
        if (callback) {
            // Fire the callback in the scope of this tree
            this.fireCallback(callback, "node", [node], this);
        }
        return false;
    }

    // mark the node as open or closed
    var state = node[this.openProperty],
        closedToOpen = !state && newState,
        openToClosed = state && !newState;

    // If the node's openness has changed then its cached length may also have changed.
    if (closedToOpen || openToClosed) {
        var parent = (node != this.root && this.getParent(node)) || null,
            prevLength = parent && this._getNodeLengthToParent(node, parent),
            newLength = (this.openDisplayNodeType != isc.Tree.LEAVES_ONLY ? 1 : 0);

        node[this.openProperty] = newState;

        if (closedToOpen) {
            // node went from closed to open so its length includes the lengths of the children.
            var childrenInOpenList = this.getChildren(
                    node, isc.Tree.FOLDERS_AND_LEAVES, null,
                    this.sortDirection, this.openListCriteria, this._sortContext,
                    true, true, true),
                loadingMarker = (
                    isc.ResultSet != null ? isc.ResultSet.getLoadingMarker() : null);
            // If getChildren returned null, convert to an empty array

            if (childrenInOpenList == null) childrenInOpenList = [];
            var i = (isc.isA.ResultSet(childrenInOpenList) ?
                    childrenInOpenList._getCachedLength() : childrenInOpenList.getLength());


            var pagedResultTree = (
                    isc.ResultTree != null && isc.isA.ResultTree(this) && this.isPaged()),
                knownLengthNulls = pagedResultTree;
            if (pagedResultTree) {
                var openSubfoldersAllowed = (
                        node[this.canReturnOpenSubfoldersProperty] != null ?
                        node[this.canReturnOpenSubfoldersProperty] : this.canReturnOpenFolders),
                    defaultChildLength = (
                        this.openDisplayNodeType == isc.Tree.FOLDERS_AND_LEAVES ? 1 : 0);

                knownLengthNulls = !(openSubfoldersAllowed || defaultChildLength == 0);
            }

            while (i--) {
                var child = childrenInOpenList.getCachedRow(i);
                if (child != null && child != loadingMarker) {
                    newLength += this._getNodeLengthToParent(child, node);
                } else if (knownLengthNulls) {
                    ++newLength;
                }
            }
        }


        var prevCachedLength = node[this._cachedLengthProperty];
        node[this._cachedLengthProperty] = newLength;

        // Add the change in length to all ancestors.
        if (parent) {
            var deltaLength = this._getNodeLengthToParent(node, parent) - prevLength;
            this._updateParentLengths(parent, deltaLength);
        }

        // Incrementally add/remove the node to/from the _openListCache array.
        var affectsOpenListCache = (
                this._openListCache != null &&
                // If the node is the root then this optimization would just be regenerating
                // the open list anyway.  It would be better not to do anything here and let
                // the open list be regenerated lazily by _getOpenList().
                parent != null &&
                // If the `openDisplayNodeType` allows only leaves then only the leaves under
                // the root folder will ever appear in the open list, so the node and its
                // descendants cannot appear in the open list.
                this.openDisplayNodeType != isc.Tree.LEAVES_ONLY &&
                // Skip if there are no nodes to add/remove from the open list.
                (closedToOpen ? newLength > prevCachedLength : newLength < prevCachedLength) &&
                this._includeNodeLengthInParent(node, parent));
        for (var n = node, p = parent; p != null && affectsOpenListCache; ) {
            n = p;
            p = this.getParent(p);
            affectsOpenListCache = (p == null || this._includeNodeLengthInParent(n, p));
        }
        if (parent == null) {
            this._clearNodeCache(false);
        } else if (affectsOpenListCache) {
            // Count the number of nodes preceding `node` in the open list.  Add one to get
            // the starting index of the descendants of `node` in the open list.  This will
            // be passed to splice() to add/remove nodes starting at that index.
            var loadingMarker = (isc.ResultSet != null ? isc.ResultSet.getLoadingMarker() : null),
                foldersInOpenList = (this.openDisplayNodeType != isc.Tree.LEAVES_ONLY),
                // When getting lists of children, the displayNodeType must match all folders
                // (so that we can find a specific folder `n` in each iteration of the
                // following loop) and it must incorporate the current `openDisplayNodeType`
                // (so that all nodes with nonzero length are counted).
                displayNodeType = (
                    this.openDisplayNodeType == isc.Tree.FOLDERS_ONLY
                        ? isc.Tree.FOLDERS_ONLY : isc.Tree.FOLDERS_AND_LEAVES),
                openListIndex = (foldersInOpenList && this.showRoot ? 1 : 0);
            for (var n = node, p = parent; p != null; ) {
                var children = this.getChildren(
                        p, displayNodeType, this._openNormalizer, this.sortDirection,
                        this.openListCriteria, this._sortContext, true, false);
                if (children == null) children = [];

                var length = (isc.ResultSet != null && isc.isA.ResultSet(children) ?
                        children._getCachedLength() : children.getLength());


                for (var i = 0; i < length; ++i) {
                    var child = children.getCachedRow(i);
                    if (child == n) {
                        if (foldersInOpenList) {
                            ++openListIndex;
                        }
                        // Break from the loop.
                        i = length;
                    } else if (!(child == null || child == loadingMarker)) {
                        openListIndex += this._getNodeLengthToParent(child, p);
                    }
                }

                n = p;
                p = this.getParent(p);
            }


            if (closedToOpen) {
                var args = this.getOpenList(
                        node, this.openDisplayNodeType, this._openNormalizer,
                        this.sortDirection, this.openListCriteria, this._sortContext, false);

                // Set the first two arguments so that no nodes are removed from the
                // _openListCache and that nodes are added starting at the `openListIndex`.
                if (foldersInOpenList) {
                    args[0] = 0;
                    args.unshift(openListIndex);
                } else {
                    args.unshift(openListIndex, 0);
                }

                this._openListCache.splice.apply(this._openListCache, args);
            } else { // openToClosed
                this._openListCache.splice(openListIndex, prevCachedLength - newLength);
            }
        }
    } else {
        node[this.openProperty] = newState;
    }

    // if the node is not loaded, load it!
    if (newState && !this.isLoaded(node)) {
        this.loadChildren(node, callback);
    } else if (callback) {
        // Fire the callback in the scope of this tree
        this.fireCallback(callback, "node", [node], this);
    }
},

//>    @method    tree.toggleFolder()
//            Toggle the open state for a particular node
//        @group    openList
//
//        @param    node    (TreeNode)    node in question
//<
toggleFolder : function (node) {
    this.changeDataVisibility(node, !this.isOpen(node));
},


//>    @method    tree.openFolder()
//
// Open a particular node
//
// @param    node    (TreeNode)    node to open
// @param  [callback] (callback) Optional callback (stringMethod) to fire when loading
//                      completes. Has a single param <code>node</code> - the node whose
//                      children have been loaded, and is fired in the scope of the Tree.
// @see ResultTree.dataArrived
// @visibility external
//<
openFolder : function (node, callback) {
    if (node == null) node = this.root;

    // if the node is not already set to the newState
    if (!this.isOpen(node)) {
        // call the dataChanged method to notify anyone who's observing it
        this.changeDataVisibility(node, true, callback);
    } else if (callback) {
        // Fire the callback in the scope of this tree
        this.fireCallback(callback, "node", [node], this);
    }
},


//>    @method    tree.openFolders()
//
// Open a set of folders, specified by path or as pointers to nodes.
//
// @param    nodeList    (List of TreeNode)        List of nodes or node paths.
//
// @see ResultTree.dataArrived
// @visibility external
//<
openFolders : function (nodeList) {
    for (var i = 0; i < nodeList.length; i++) {
        var node = nodeList[i];
        if (node == null) continue;
        if (isc.isA.String(node)) node = this.find(node);
        if (node != null) {
            this.openFolder(node);
        }
    }
},

//>    @method    tree.closeFolder()
//
// Closes a folder
//
// @param    node    (TreeNode)    folder to close
//
// @visibility external
//<
closeFolder : function (node) {
    // if the node is not already set to the newState
    if (this.isOpen(node)) {
        // call the dataChanged method to notify anyone who's observing it
        this.changeDataVisibility(node, false);
    }
},

//>    @method    tree.closeFolders()
//
// Close a set of folders, specified by path or as pointers to nodes.
//
// @param    nodeList    (List of TreeNode)        List of nodes or node paths.
//
// @visibility external
//<
closeFolders : function (nodeList) {
    for (var i = 0; i < nodeList.length; i++) {
        var node = nodeList[i];
        if (node == null) continue;
        if (isc.isA.String(node)) node = this.find(node);
        if (node != null) {
            this.closeFolder(node);
        }
    }
},

//>    @method    tree.openAll()
//
// Open all nodes under a particular node.
//
// @param    [node]    (TreeNode)    node from which to open folders (if not specified, the root
//                              node is used)
// @visibility external
// @example parentLinking
//<
openAll : function (node) {
    if (!node) node = this.root;
    if (node == this.root) {
        // Mark the open node list as dirty.  This avoids an optimization to incrementally
        // maintain the _openListCache array that would not work very well for a bulk operation
        // like this.
        this._clearNodeCache(false);
    }
    var nodeList = this.getDescendants(node, isc.Tree.FOLDERS_ONLY);
    for (var i = 0, length = nodeList.length; i < length; i++) {
        // if the node is not already set to the newState
        if (!this.isOpen(nodeList[i])) {
            // call the dataChanged method to notify anyone who's observing it
            this.changeDataVisibility(nodeList[i], true);
        }
    }
    // make the node itself open
    this.changeDataVisibility(node, true);
},

//>    @method    tree.closeAll()
// Close all nodes under a particular node
//
// @param    [node]    (TreeNode)    node from which to close folders (if not specified, the root
//                              node is used)
//
// @visibility external
//<
closeAll : function (node) {
    if (!node) node = this.root;
    if (node == this.root) {
        // Mark the open node list as dirty.  This avoids an optimization to incrementally
        // maintain the _openListCache array that would not work very well for a bulk operation
        // like this.
        this._clearNodeCache(false);
    }
    var nodeList = this.getDescendants(node, isc.Tree.FOLDERS_ONLY);
    for (var i = 0, length = nodeList.length; i < length; i++) {
        // if the node is not already set to the newState
        if (this.isOpen(nodeList[i])) {
            // call the dataChanged method to notify anyone who's observing it
            this.changeDataVisibility(nodeList[i], false);
        }
    }

    // close the node as well, unless (node==this.root and this.showRoot == false)
    //    this way we make sure you won't close an invisible root,
    //  leaving no way to re-open it.
    if (!(node == this.root && this.showRoot == false)) this.changeDataVisibility(node, false);
},

setOpenDisplayNodeType : function (openDisplayNodeType) {
    var prevOpenDisplayNodeType = this.openDisplayNodeType;
    this.openDisplayNodeType = openDisplayNodeType;
    if (prevOpenDisplayNodeType != openDisplayNodeType) {
        this._clearNodeCache(true);
    }
},

setOpenListCriteria : function (openListCriteria) {
    var prevOpenListCriteria = this.openListCriteria;
    this.openListCriteria = openListCriteria;
    if (prevOpenListCriteria != openListCriteria) {
        this._clearNodeCache(true);
    }
},

setSortProp : function (sortProp) {
    var prevSortProp = this.sortProp;
    this.sortProp = sortProp;
    if (prevSortProp != sortProp) {
        this._clearNodeCache(true);
    }
},

setSortDirection : function (sortDirection) {
    var prevSortDirection = this.sortDirection;
    this.sortDirection = sortDirection;
    if (prevSortDirection != sortDirection) {
        this._clearNodeCache(true);
    }
},

//> @method tree.setShowRoot()
// Setter for +link{Tree.showRoot}.
// @param showRoot (Boolean) new <code>showRoot</code> value
// @visibility external
//<
setShowRoot : function (showRoot) {
    var prevShowRoot = this.showRoot;
    this.showRoot = showRoot;
    if (this.openDisplayNodeType != isc.Tree.LEAVES_ONLY) {
        if (!prevShowRoot && showRoot) {
            // Add the root to the _allListCache and the _openListCache.
            if (this._allListCache != null) {
                this._allListCache.unshift(this.root);
            }
            if (this._openListCache != null) {
                this._openListCache.unshift(this.root);
            }
        } else if (prevShowRoot && !showRoot) {

            // Remove the root from the _allListCache and the _openListCache.
            if (this._allListCache != null) {
                this._allListCache.shift();
            }
            if (this._openListCache != null) {
                this._openListCache.shift();
            }
        }
    }
},

//> @method tree.setSeparateFolders()
// Setter for +link{Tree.separateFolders}.
// @param separateFolders (Boolean) new <code>separateFolders</code> value
// @visibility external
//<
setSeparateFolders : function (separateFolders) {
    var prevSeparateFolders = this.separateFolders;
    this.separateFolders = separateFolders;
    if (prevSeparateFolders ? !separateFolders : separateFolders) {
        this._clearNodeCache(true);
    }
},

//> @method tree.setSortFoldersBeforeLeaves()
// Setter for +link{Tree.sortFoldersBeforeLeaves}.
// @param sortFoldersBeforeLeaves (Boolean) new <code>sortFoldersBeforeLeaves</code> value
// @visibility external
//<
setSortFoldersBeforeLeaves : function (sortFoldersBeforeLeaves) {
    var prevSortFoldersBeforeLeaves = this.sortFoldersBeforeLeaves;
    this.sortFoldersBeforeLeaves = sortFoldersBeforeLeaves;
    if (prevSortFoldersBeforeLeaves ? !sortFoldersBeforeLeaves : sortFoldersBeforeLeaves) {
        this._clearNodeCache(true);
    }
},





//>    @method    tree.getOpenList()
//
// Return a flattened list of nodes that are open under some parent, including the parent
// itself.  If the passed in node is a leaf, this method returns null
//
// @param [node]            (TreeNode)            node in question
// @return                    (List of TreeNode)              flattened list of open nodes
//
// @visibility external
//<

getOpenList : function (node, displayNodeType, normalizer, sortDirection, criteria, context, getAll, dontUseNormalizer) {
    // default to the tree root
    if (! node) node = this.root;

    // default the normalizer to this._openNormalizer and sortDirection to this.sortDirection
    if (dontUseNormalizer) normalizer = null;
    else if (normalizer == null) normalizer = this._openNormalizer;
    if (sortDirection == null)        sortDirection = this.sortDirection;
    if (context == null) context = this._sortContext;
    // if the node is a leaf, return the empty list since it's not going to have any children
    if (this.isLeaf(node)) {
        // prevents mysterious crash if an isFolder() override claims root is a leaf
        if (node == this.root) return [];
        return null;
    }

    // create an array to hold the descendants
    var list = [];

    // add the node if we're not skipping folders (except if the node is the root and showRoot is false)
    if (displayNodeType != isc.Tree.LEAVES_ONLY && (node != this.root || this.showRoot)) {
        list[list.length] = node;
    }

    // if this node is closed, return the list
    if (!getAll && !this.isOpen(node)) return list;

    // iterate through all the children of the node
    var children = this.getChildren(
            node, isc.Tree.FOLDERS_AND_LEAVES, normalizer, sortDirection, criteria, context, false, dontUseNormalizer);
    // for each child
    var loadingMarker = (isc.ResultSet != null ? isc.ResultSet.getLoadingMarker() : null),
        length = (isc.ResultSet != null && isc.isA.ResultSet(children) ?
            children._getCachedLength() : children.getLength());
    for (var i = 0; i < length; ++i) {
        // get a pointer to the child
        var child = children.getCachedRow(i);
        if (child == null || child == loadingMarker) {
            //>DEBUG
            //alert("getOpenList: child # " + i + " of folder " + node.path + " is null!");
            //<DEBUG
            continue;
        }

        // if the child is a folder, recurse, but check that it actually has children -
        // otherwise we eat a function call, array alloc, empty concat, and a bunch of other
        // checks (top of this function) all for nothing.  This is a typical case for loading a
        // large set of folders from the server in loadOnDemand mode
        //
        var grandChildren = child[this.childrenProperty];
        if (grandChildren && !grandChildren.isEmpty()) {
            // now concatenate the list with the descendants of the child
            list = list.concat(this.getOpenList(child, displayNodeType, normalizer,
                                                sortDirection, criteria, context, getAll,
                                                dontUseNormalizer));
        } else {
            // if we're not excluding leaves, add the leaf to the list

            if (displayNodeType != isc.Tree.FOLDERS_ONLY) {
                list[list.length] = child;
            }
        }
    }

    // finally, return the entire list
    return list;
},
// _getOpenListAsync() is an asynchronous method equivalent to getOpenList().
_getOpenListAsync : function (node, displayNodeType, normalizer, sortDirection, criteria, context, getAll,
            thisArg, timerEventProp, batchSize, callback, state) {

    var node0 = node,
        done = false,
        list,
        indexStack, childrenStack,
        i, children;
    if (state == null) {
        // default to the tree root
        if (! node) node0 = node = this.root;

        // default the normalizer to this._openNormalizer and sortDirection to this.sortDirection
        if (normalizer == null)         normalizer = this._openNormalizer;
        if (sortDirection == null)        sortDirection = this.sortDirection;
        if (context == null) context = this._sortContext;

        // create an array to hold the descendants
        list = [];

        // if the node is a leaf, return the empty list since it's not going to have any children
        if (this.isLeaf(node)) {
            // prevents mysterious crash if an isFolder() override claims root is a leaf
            var ret = (node == this.root ? [] : null);
            callback.call(thisArg, ret);
            return;
        }

        indexStack = [];
        childrenStack = [];
        state = {
            list: list,
            node: node,
            indexStack: indexStack,
            childrenStack: childrenStack
        };
    } else {
        list = state.list;
        node = state.node;
        indexStack = state.indexStack;
        childrenStack = state.childrenStack;
        i = indexStack.last();
        children = childrenStack.last();
    }

    for (var count = 0; !done && count < batchSize; ++count) {
        if (node != null) {
            // if the node is a leaf, return the empty list since it's not going to have any children
            if (this.isLeaf(node)) {
                node = null;
                continue;
            }

            // add the node if we're not skipping folders (except if the node is the root and showRoot is false)
            if (displayNodeType != isc.Tree.LEAVES_ONLY && (node != this.root || this.showRoot)) {
                list[list.length] = node;
            }

            // if this node is closed, return the list
            if (!getAll && !this.isOpen(node)) {
                node = null;
                continue;
            }

            // iterate through all the children of the node
            i = 0;
            children = this.getChildren(
                node, displayNodeType, normalizer, sortDirection, criteria, context, false, false);
            indexStack.push(i);
            childrenStack.push(children);
        }

        // Set the node to null to skip the above checks in the next few iterations of the outer,
        // loop.  All that remains is to check this node's children (who have been pushed onto
        // childrenStack).
        node = null;

        // for each child
        var escapeToOuterLoop = false,
            length = (isc.isA.ResultSet(children) ?
                children._getCachedLength() : children.getLength());
        for (; !escapeToOuterLoop && i < length && count < batchSize; ++i) {
            // get a pointer to the child
            var child = children.getCachedRow(i);
            if (child == null) {
                //>DEBUG
                //alert"getOpenList: child # " + i + " of folder " + node.path + " is null!");
                //<DEBUG
                continue;
            }

            // if the child is a folder, recurse, but check that it actually has children -
            // otherwise we eat a function call, array alloc, empty concat, and a bunch of other
            // checks (top of this function) all for nothing.  This is a typical case for loading a
            // large set of folders from the server in loadOnDemand mode
            //
            var grandChildren = child[this.childrenProperty];
            if (grandChildren && !grandChildren.isEmpty()) {
                // now concatenate the list with the descendants of the child
                escapeToOuterLoop = true;
                node = state.node = child;
                continue;
            } else {
                // if we're not excluding leaves, add the leaf to the list

                if (displayNodeType != isc.Tree.FOLDERS_ONLY) {
                    list[list.length] = child;
                }
                ++count;
            }
        }

        // `i` is supposed to be an alias variable for the last index in the indexStack.
        indexStack[indexStack.length - 1] = i;
        if (!escapeToOuterLoop && i >= length) {
            if (indexStack.length > 1) {
                indexStack.pop();
                childrenStack.pop();
                i = indexStack.last();
                children = childrenStack.last();
            } else {
                done = true;
            }
        }
    }

    if (done) {
        // finally, return the entire list
        callback.call(thisArg, list);
    } else {
        state.node = node;
        thisArg[timerEventProp] = this.delayCall(
            "_getOpenListAsync",
            [node0, displayNodeType, normalizer, sortDirection, criteria, context, getAll,
             thisArg, timerEventProp, batchSize, callback, state], 0);
    }
},

//>    @method    tree._getOpenList()    (A)
// Internal routine to set the open list if it needs to be set
//        @group    openList
//<
_getOpenList : function () {
    // if the _openListCache hasn't been calculated,
    //        or we're not supposed to cache the openList
    if (!this._openListCache || !this.cacheOpenList) {
        // recalculate the open list
        this._openListCache = this.getOpenList(this.root, this.openDisplayNodeType,
                                               this._openNormalizer, this.sortDirection,
                                               this.openListCriteria);
    }
    return this._openListCache;
},

//> @method tree.getNodeList()
// Return a flattened list of all nodes in the tree.
//<
getNodeList : function (onlyOpen, dontSort) {
    // Call _getOpenList() if:
    // - we only need opened nodes
    // - either dontSort is not true or we have a (sorted) cache anyway, so the hint
    //   not to use the normalizer is unnecessary information.
    // If we don't have an _openListCache and dontSort is true, then we will want to
    // respect the request to not apply the normalizer, as this is probably faster that recalculating
    // the _openListCache, which will apply the normalizer.
    if (onlyOpen && (dontSort != true || (this._openListCache && this.cacheOpenList))) {
        return this._getOpenList();

    } else if (dontSort) {
        return this.getOpenList(this.root, this.openDisplayNodeType,
                                null, null, this.openListCriteria, null, (onlyOpen != true),
                                dontSort);

    // if the _allListCache hasn't been calculated,
    // or we're not supposed to cache the openList
    } else if (!this._allListCache || !this.cacheAllList) {
        // recalculate the node list
        this._allListCache = this.getAllNodes(this.root);
    }
    return this._allListCache;
},
// _getNodeListAsync() is an asynchronous method equivalent to getNodeList().
_getNodeListAsync : function (thisArg, timerEventProp, batchSize, callback) {
    // if the _allListCache hasn't been calculated,
    // or we're not supposed to cache the openList
    if (! this._allListCache || !this.cacheAllList) {
        // recalculate the node list
        var me = this;
        this._getAllNodesAsync(this.root, thisArg, timerEventProp, batchSize, function (nodes) {
            me._allListCache = nodes;
            callback.call(thisArg, nodes);
        });
    } else {
        return this._allListCache;
    }
},

//> @method tree.getAllNodes()
// Get all the nodes that exist in the tree under a particular node, as a flat list, in
// depth-first traversal order.
//
// @params [node] optional node to start from.  Default is root.
// @return (Array of TreeNode) all the nodes that exist in the tree
// @visibility external
//<
getAllNodes : function (node) {
    return this.getOpenList(node, null, null, null, null, null, true);
},
// _getAllNodesAsync() is an asynchronous method equivalent to getAllNodes().
_getAllNodesAsync : function (node, thisArg, timerEventProp, batchSize, callback) {
    this._getOpenListAsync(node, null, null, null, null, null, true, thisArg, timerEventProp, batchSize, callback);
},

// List API
// --------------------------------------------------------------------------------------------

//>    @method    tree.getLength()
//
// Returns the number of items in the current open list.
//
// @return        (number)    number of items in open list
//
// @see method:Tree.getOpenList
// @visibility external
//<
getLength : function () {

    var length = (this.root[this._cachedLengthProperty] - (
        !this.showRoot && this.openDisplayNodeType != isc.Tree.LEAVES_ONLY ? 1 : 0));

    // assert (length == this._getOpenList().length);

   return length;
},

// _getLengthAsync() is an asynchronous version of getLength().
_getLengthAsync : function (thisArg, timerEventProp, batchSize, callback) {
    callback.call(thisArg, this.getLength());
},

//> @method tree.get()
// Get the item in the openList at a particular position.
// @param pos (Number) position of the node to get
// @return (TreeNode) node at that position
// @group openList, Items
//<
get : function (pos) {
    return this._getOpenList()[pos];
},

// see ResultSet.getCachedRow()
getCachedRow : function (rowNum) {
    return this.get(rowNum);
},


//>    @method    tree.getRange()
//            Get a range of items from the open list
//        @group    openList, Items
//
//      @param  start (number) start position
//      @param  end   (number) end position (NOT inclusive)
//      @return       (Array of TreeNode) list of nodes in the open list
//<
getRange : function (start, end) {
    if (!(0 <= start && start < end)) return [];
    return this._getOpenList().slice(start, end);
},

//>    @method    tree.indexOf()
// @include list.indexOf
//<
indexOf : function (node, pos, endPos) {
    return this._getOpenList().indexOf(node, pos, endPos);
},

//>    @method    tree.lastIndexOf()
// @include list.lastIndexOf
//<
lastIndexOf : function (node, pos, endPos) {
    return this._getOpenList().lastIndexOf(node, pos, endPos);
},

//>    @method    tree.getAllItems()
//            Get the entire list (needed by Selection)
//        @group    openList, Items
//
//        @return        (TreeNode)    all nodes in the open list
//<
getAllItems : function () {
    return this._getOpenList();
},



//>    @method    tree.sortByProperty()
// Handle a 'sortByProperty' call to change the default sort order of the tree
//        @group    sorting
//
//        @param    [property]    (string)    name of the property to sort by
//        @param    [direction]        (boolean)    true == sort ascending
//        @param    [normalizer](function)    sort normalizer (will be derived if not specified)
//<
sortByProperty : function (property, direction, normalizer, context) {
    if (!property && this.separateFolders == false) {
        // if we were called without a sort-property and this.sortProp is set, use it...
        if (this.sortProp) property = this.sortProp;
        else property = this.titleProperty;
    }
    if (!direction) direction = this.sortDirection;
    this.setSort([{
        property: property,
        direction: (isc.isA.String(direction) ? direction :
            (direction == true) ? "ascending" : "descending"),
        normalizer: normalizer,
        context: context
    }]);

},

getSort : function () {
    return this._sortSpecifiers;
},
setSort : function (sortSpecifiers) {
    if (!sortSpecifiers || !sortSpecifiers.length) {
        if (this.sortProp) {
            var direction = this.sortDirection;
            sortSpecifiers = [
                {
                    property: this.sortProp,
                    direction: (isc.isA.String(direction) ? direction :
                        (direction == true) ? "ascending" : "descending")
                }
            ];
        }
    } else {
        // duplicate the sortSpecifiers. We manipulate them directly
        // (adding sort-normalizer, for example) and we don't want upstream code
        // to be affected

        var dup = [];
        for (var i = 0; i < sortSpecifiers.length; i++) {
            var item = sortSpecifiers[i];
            if (item) {
                if (item.context  && !isc.isA.String(item.context)) {
                    var undef,
                        dsField = this.dataSOurce && this.dataSource.getField(item.property),
                        field = item.context.getField(item.property) || dsField;

                    var displayField = field && field.displayField;
                    if (displayField === undef) displayField = dsField && dsField.displayField;
                    var sortByDisplayField = field && field.sortByDisplayField;
                    if (sortByDisplayField === undef) sortByDisplayField = dsField && dsField.sortByDisplayField;

                    if (displayField && (sortByDisplayField != false)) {
                        var opDs = (field && field.optionDataSource) ||
                                    (dsField && dsField.optionDataSource);
                        if (opDs) {
                            opDs = isc.DataSource.getDataSource(field.optionDataSource);
                        }

                        if (!opDs || opDs == isc.DataSource.getDataSource(this.dataSource)) {
                        //if (!field.optionDataSource || opDs == isc.DataSource.getDataSource(this.dataSource)) {
                            this.logInfo("Field:" + field.name + " has displayField:" + displayField +
                                " (with optionDataSource:" + opDs + "). " +
                                "Sorting by displayField. Set field.sortByDisplayField to false to disable this.",
                                "sorting");
                            // store the original fieldName as the owningProperty - used when editing
                            // this sortSpecifier later, in a MultiSort[Panel/Dialog]
                            item.owningField = item.property;
                            if (!item.sortByField) {
                                item.property = field.displayField;
                            } else {
                                item.sortByProperty = field.displayField;
                            }
                        }
                    }
                }
                dup.add(isc.addProperties({}, item));
            }
        }
        sortSpecifiers = dup;
    }

    this._sortSpecifiers = sortSpecifiers;

    // mark as dirty so any list who points to us will be redrawn
    this._clearNodeCache(true);

    this._makeOpenNormalizer();

    // always hang onto the context
    this._sortContext = sortSpecifiers && sortSpecifiers.length > 0 ? sortSpecifiers[0].context : null;

    // call the dataChanged method to notify anyone who's observing it
    this.dataChanged();
},

//>    @method    tree._makeOpenNormalizer()    (A)
// Create a normalizer function according to the sortProp and sortDirection variables
//        @group    sorting
//<
_makeOpenNormalizer : function () {
    var tree = this,
        separateFolders = this.separateFolders != false,
        sortProps = this._sortSpecifiers,
        titleProperty = this.titleProperty;

    var folderPrefix,
        leafPrefix;
    if (this.sortFoldersBeforeLeaves) {
        folderPrefix = "0:";
        leafPrefix = "1:";
    } else {
        folderPrefix = "1:";
        leafPrefix = "0:";
    }

    sortProps.removeEmpty();
    if (sortProps.isEmpty()) {
        this._openNormalizer = isc.Class.NO_OP;
        return;
    }

    var propNames = sortProps.getProperty("property");


    if ((!separateFolders || propNames.length > 1 || !!propNames[0]) &&
        !propNames.contains(titleProperty))
    {
        propNames.add(titleProperty);
    }

    this._openNormalizer = function (obj, property) {
        if (tree == null || tree.destroyed) {
            tree = null;
            return;
        }

        var value = "";

        if (separateFolders) {
            value += (tree.isFolder(obj) ? folderPrefix : leafPrefix);
        }

        for (var i = 0; i < propNames.length; ++i) {
            var innerProp = propNames[i];
            if (!innerProp) continue;

            var isTitle = innerProp == titleProperty;

            var prop = isTitle ? tree.getTitle(obj) : obj[innerProp];
            if (prop == null) continue;


            if (isc.isA.Number(prop)) {
                if (prop > 0) {
                    prop = "1" + prop.stringify(12, true);
                } else {
                    prop = 999999999999 + prop;
                    prop = "0" + prop.stringify(12, true);
                }
            } else if (isc.isA.Date(prop)) {
                prop = prop.getTime();
            }

            if (isTitle) {
                value += String(prop).toLowerCase() + ":";
            } else {
                value += prop + ":";
            }
        }

        return value;
    };


    if (isc.Browser.isFirefox) {
        this._openAscendingComparator = function (first, second) {
            if (first == null || second == null) {
                return Array.compareAscending(first, second);
            }
            var m = first.length,
                n = second.length;
            if (m == 0 || n == 0) {
                return Array.compareAscending(first, second);
            }

            var i = 0,
                j = first.indexOf(":", i),
                k = 0,
                l = second.indexOf(":", k);
            if (j == -1) j = m;
            if (l == -1) l = n;

            for (;;) {
                var cmp = Array.compareAscending(
                    first.substring(i, j), second.substring(k, l));
                if (cmp != 0) {
                    return cmp;
                } else {
                    i = j + 1;
                    k = l + 1;
                    if (i >= m) {
                        if (k >= n) {
                            return 0;
                        } else {
                            return -1;
                        }
                    } else if (k >= n) {
                        return 1;
                    } else {
                        j = first.indexOf(":", i);
                        l = second.indexOf(":", k);
                        if (j == -1) j = m;
                        if (l == -1) l = n;
                    }
                }
            }
        };

        // Same as above, except with `compareDescending` instead of `compareAscending`,
        // `return 1` instead of `return -1`, and `return -1` instead of `return 1`.
        this._openDescendingComparator = function (first, second) {
            if (first == null || second == null) {
                return Array.compareDescending(first, second);
            }
            var m = first.length,
                n = second.length;
            if (m == 0 || n == 0) {
                return Array.compareDescending(first, second);
            }

            var i = 0,
                j = first.indexOf(":", i),
                k = 0,
                l = second.indexOf(":", k);
            if (j == -1) j = m;
            if (l == -1) l = n;

            for (;;) {
                var cmp = Array.compareDescending(
                    first.substring(i, j), second.substring(k, l));
                if (cmp != 0) {
                    return cmp;
                } else {
                    i = j + 1;
                    k = l + 1;
                    if (i >= m) {
                        if (k >= n) {
                            return 0;
                        } else {
                            return 1;
                        }
                    } else if (k >= n) {
                        return -1;
                    } else {
                        j = first.indexOf(":", i);
                        l = second.indexOf(":", k);
                        if (j == -1) j = m;
                        if (l == -1) l = n;
                    }
                }
            }
        };
    }
    return;
},

// Loading batches of children: breadth-first loading up to a maximum
// ---------------------------------------------------------------------------------------

loadBatchSize:50,
loadSubtree : function (node, max, initTime) {
    if (!node) node = this.getRoot();
    if (max == null) max = this.loadBatchSize;

    //this.logWarn("loading subtree of node: " + this.echoLeaf(node) +
    //             "up to max: " + max);

    this._loadingBatch = initTime ? 2 : 1;

    var count = 0,
        stopDepth = 1;
    // load at increasing depth until we hit max or run out of children
    while (count < max) {
        var numLoaded = this._loadToDepth(max, node, count, stopDepth++);
        if (numLoaded == 0) break; // nothing left to load
        count += numLoaded;
    }

    this._loadingBatch = null;

    if (count > 0) this._clearNodeCache(true);
},

// allows loadChildren() to detect we're loading a batch of children and defer loading a folder
// that doesn't have interesting children
loadingBatch : function (initOnly) {
    if (initOnly) return this._loadingBatch == 2;
    else return this._loadingBatch;
},

_loadToDepth : function (max, node, count, stopDepth) {

    var numLoaded = 0;
    if (!this.isOpen(node)) {
        if (!this.isLoaded(node)) this.loadChildren(node);

        // NOTE: we assume that during batch loading, folders can decline to actually load or
        // open, and these should remain closed
        if (this.isLoaded(node)) {
            if (this.openFolder(node) === false) return numLoaded;
        }

        var nodeChildren = node[this.childrenProperty];
        if (nodeChildren) {
            var nodeChildrenLength = (isc.isA.ResultSet(nodeChildren) ?
                    nodeChildren._getCachedLength() : nodeChildren.getLength());
            numLoaded += nodeChildrenLength;
            count += nodeChildrenLength;
        }
    }

    var childNodes = node[this.childrenProperty];

    if (count >= max || stopDepth == 0 || childNodes == null) return numLoaded;

    var length = (isc.isA.ResultSet(childNodes) ?
            childNodes._getCachedLength() : childNodes.getLength());
    for (var i = 0; i < length; ++i) {
        var child = childNodes.getCachedRow(i);
        if (child != null) {
            var loaded = this._loadToDepth(max, child, count, stopDepth - 1);

            numLoaded += loaded;
            count += loaded;

            //this.logWarn("recursed into: " + this.getTitle(child) +
            //             " and loaded: " + loaded +
            //             ", total count: " + count);

            if (count >= max) return numLoaded;
        }
    }
    return numLoaded;
},

// Tree Filtering
// ---------------------------------------------------------------------------------------

//> @method tree.getFilteredTree()
// Filters this tree by the provided criteria, returning a new Tree containing just the nodes
// that match the criteria.
// <P>
// If <code>filterMode</code> is "keepParents", parents are retained if
// any of their children match the criteria even if those parents do not match the criteria.
//
// @param criteria (Criteria or AdvancedCriteria) criteria to filter by
// @param [filterMode] (TreeFilterMode) mode to use for filtering, defaults to "strict"
// @param [dataSource] (DataSource) dataSource to use for filtering, if this Tree does not
//                                  already have one
// @param [requestProperties] (DSRequest) Request properties block. This allows developers to specify
//  properties that would impact the filter such as +link{DSRequest.textMatchStyle}
// @return (Tree) filtered tree
// @group treeFilter
// @visibility external
//<
getFilteredTree : function (criteria, filterMode, dataSource, context) {
    filterMode = filterMode || isc.Tree.STRICT;

    var dataSource = this.dataSource || dataSource;
    if (!dataSource) {
        isc.logWarn("Cannot apply filter to Tree without dataSource");
        return null;
    }

    // Filter the tree in-place to avoid moving nodes around as we add
    // missing parent nodes back in place. We also retain loadState.
    var tree = this.duplicate(true, true);
    if (isc.ResultTree && isc.isA.ResultTree(tree) && tree.isPaged()) {
        tree.setCriteria(isc.DataSource.combineCriteria(criteria, tree.criteria));
    }
    tree._filterChildren(criteria, filterMode, dataSource, tree.getRoot(), context);
    return tree;
},

// Returns true if any children match criteria
_filterChildren : function (criteria, filterMode, dataSource, parent, context) {

    var strict = (filterMode == isc.Tree.STRICT),
        keepParents = !strict;

    var children = parent[this.childrenProperty];
    if (children == null || children.isEmpty()) return false;

    var haveMatchingNodes = false;

    if (isc.isA.String(dataSource)) dataSource = isc.DS.get(dataSource);


    var isResultSet = isc.isA.ResultSet(children);
    strict = strict || isResultSet;

    var i = (isResultSet ? children._getCachedLength() : children.getLength());
    while (i--) {
        var node = children.getCachedRow(i);
        if (node != null) {
            var hasImmediateMatches = false,
                nodeChildren = node[this.childrenProperty];

            if (keepParents) {
                if (nodeChildren != null && !nodeChildren.isEmpty()) {
                    hasImmediateMatches = this._filterChildren(criteria, filterMode, dataSource, node, context);
                }
                haveMatchingNodes = haveMatchingNodes || hasImmediateMatches;
            }

            // Don't have to filter parent node (this child) if keeping parent nodes
            // and there are matching children.
            if (!hasImmediateMatches || strict) {

                var matches = dataSource.applyFilter([node], criteria, context);
                if (matches != null && matches.length > 0) {
                    haveMatchingNodes = true;

                    if (strict && nodeChildren != null && !nodeChildren.isEmpty()) {
                        this._filterChildren(criteria, filterMode, dataSource, node, context);
                    }
                } else {
                    this._remove(node, parent, children, i);
                }
            }
        }
    }
    return haveMatchingNodes;
},


_includeNodeLengthInParent : function (node, parent) {

    // The parent must be open.
    return this.isOpen(parent);
},


_isNodeVisibleToParent : function (node, parent) {
    if (!this._includeNodeLengthInParent(node, parent)) {
        return false;
    } else {
        // Even if the node is technically a folder, if it has no children then, at this point,
        // it is treated as a leaf.
        var grandChildren = node[this.childrenProperty],
            isFolder = this.isFolder(node) && (grandChildren && grandChildren.length);
        return (this.openDisplayNodeType != (isFolder ? isc.Tree.LEAVES_ONLY : isc.Tree.FOLDERS_ONLY));
    }
},


_getNodeLengthToParent : function (node, parent) {


    if (this._includeNodeLengthInParent(node, parent)) {

        var length = node[this._cachedLengthProperty],
            grandChildren = node[this.childrenProperty],
            isFolder = this.isFolder(node),
            treatAsFolder = (grandChildren && grandChildren.length);

        // Even if the node is technically a folder, if it has no children then, at this point,
        // it is treated as a leaf.  This can result in a difference of +/-1 in the length of
        // the node according to the parent.
        if (isFolder != treatAsFolder) {
            length += (
                -(this.openDisplayNodeType != (isFolder ? isc.Tree.LEAVES_ONLY : isc.Tree.FOLDERS_ONLY) ? 1 : 0) +
                (this.openDisplayNodeType != (treatAsFolder ? isc.Tree.LEAVES_ONLY : isc.Tree.FOLDERS_ONLY) ? 1 : 0));
        }

        return length;
    } else {
        return 0;
    }
},

_getNodeLength : function (node) {
    var isFolder = this.isFolder(node),
        isOpen = isFolder && this.isOpen(node),
        length = (this.openDisplayNodeType != (isFolder ? isc.Tree.LEAVES_ONLY : isc.Tree.FOLDERS_ONLY) ? 1 : 0);

    if (isOpen) {
        var childrenInOpenList = this.getChildren(
                node, this.openDisplayNodeType, null,
                this.sortDirection, this.openListCriteria, this._sortContext, false, false, true),
            loadingMarker = (
                isc.ResultSet != null ? isc.ResultSet.getLoadingMarker() : null),
            i = (isc.ResultSet != null && isc.isA.ResultSet(childrenInOpenList) ?
                childrenInOpenList._getCachedLength() : childrenInOpenList.getLength());


        var pagedResultTree = (
                isc.ResultTree != null && isc.isA.ResultTree(this) && this.isPaged()),
            knownLengthNulls = pagedResultTree;
        if (pagedResultTree) {
            var openSubfoldersAllowed = (
                    node[this.canReturnOpenSubfoldersProperty] != null ?
                    node[this.canReturnOpenSubfoldersProperty] : this.canReturnOpenFolders),
                defaultChildLength = (
                    this.openDisplayNodeType == isc.Tree.FOLDERS_AND_LEAVES ? 1 : 0);

            knownLengthNulls = !(openSubfoldersAllowed || defaultChildLength == 0);
        }

        while (i--) {
            var child = childrenInOpenList.getCachedRow(i);
            if (child != null && child != loadingMarker) {
                length += this._getNodeLengthToParent(child, node);
            } else if (knownLengthNulls) {
                ++length;
            }
        }
    }

    return length;
},


_getDeltaLength : function (node, wasFolder, isFolder) {
    if (wasFolder != isFolder) {
        var wasLeaf = !wasFolder,
            isLeaf = !isFolder,
            deltaLength = (
                // The node was a folder (leaf) and now is no longer a folder (leaf)
                // so subtract 1 from the length if the node was displayed in the
                // open list.
                -((wasLeaf && this.openDisplayNodeType != isc.Tree.FOLDERS_ONLY) ||
                (wasFolder && this.openDisplayNodeType != isc.Tree.LEAVES_ONLY) ? 1 : 0) +

                // The node is a new folder (leaf) so add 1 if it is now going to be
                // displayed in the open list.
                ((isLeaf && this.openDisplayNodeType != isc.Tree.FOLDERS_ONLY) ||
                (isFolder && this.openDisplayNodeType != isc.Tree.LEAVES_ONLY) ? 1 : 0));


        return deltaLength;
    } else {
        return 0;
    }
},


_updateParentLengths : function (parent, deltaLength) {

    if (deltaLength != 0) {
        for (;;) {

            parent[this._cachedLengthProperty] += deltaLength;
            var recursionFlag = ((parent[this._recursionCountProperty] || 0) > 0);
            if (!(parent == this.root || recursionFlag)) {
                var grandParent = this.getParent(parent);
                if (this._includeNodeLengthInParent(parent, grandParent)) {
                    parent = grandParent;
                    continue;
                }
            }
            break;
        }
    }
}
});    // END isc.Tree.addMethods()

isc.Tree.addClassMethods({
    // Tree Discovery
    // ---------------------------------------------------------------------------------------
    // utilities for discovering the tree structure of a block of data heuristically

    // heuristically find a property that appears to contain child objects.
    // Searches through an object and find a property that is either Array or Object valued.
    // Returns the property name they were found under.
    // mode:
    // "any" assume the first object or array value we find is the children property
    // "array" assume the first array we find is the children property, no matter the contents
    // "object" assume the first object or array of objects we find is the children property
    //          (don't allow arrays that don't have objects)
    // "objectArray" accept only an array of objects as the children property
    findChildrenProperty : function (node, mode) {
        if (!isc.isAn.Object(node)) return;

        if (!mode) mode = "any";

        var any = (mode == "any"),
            requireObject = (mode == "object"),
            requireArray = (mode == "array"),
            requireObjectArray = (mode == "objectArray");

        for (var propName in node) {
            var propValue = node[propName];
            // note: isAn.Object() matches both Array and Object
            if (isc.isAn.Object(propValue)) {
                if (any) return propName;
                if (isc.isAn.Array(propValue)) {
                    // array of objects always works
                    if (isc.isAn.Object(propValue[0])) return propName;
                    // simple array satisfies all but "object" and "objectArray"
                    if (!requireObject && !requireObjectArray) return propName;
                } else {
                    // object works only for "object" and "any" ("any" covered above)
                    if (requireObject) return propName;
                }
            }
        }
    },

    // given a hierarchy of objects with children under mixed names, heuristically discover the
    // property that holds children and copy it to a single, uniform childrenProperty.  Label each
    // discovered child with a configurable "typeProperty" set to the value of the property
    // that held the children.
    discoverTree : function (nodes, settings, parentChildrenField) {
        if (!settings) settings = {}; // less null checks

        var childrenMode = settings.childrenMode || "any";

        // scanMode: how to scan for the childrenProperty
        // "node": take each node individually
        // "branch": scan direct siblings as a group, looking for best fit
        // "level": scan entire tree levels as a group, looking for best fit
        var scanMode = settings.scanMode || "branch";

        // tieMode: what to do if there is more than one possible childrenProperty when using
        // scanMode "branch" or "level"
        // "node": continue, but pick childrenProperty on a per-node basis (will detect
        //             mixed)
        // "highest": continue, picking the childrenProperty that occurred most as the single
        //            choice
        // "stop": if there's a tie, stop at this level (assume no further children)
        // NOT SUPPORTED YET: "branch": if using scanMode:"level", continue but with scanMode
        //                              "branch"
        var tieMode = settings.tieMode || "node";

            // what to rename the array of children once discovered
        var newChildrenProperty = settings.newChildrenProperty ||
                                  isc.Tree.getInstanceProperty("childrenProperty"),
            typeProperty = settings.typeProperty || "nodeType",
            // for string leaf nodes (if allowed), what property to store the string under in
            // the auto-created object
            nameProperty = settings.nameProperty || "name";

        if (!isc.isAn.Array(nodes)) nodes = [nodes];

        // go through all the nodes on this level and figure out what property occurs most
        // often as a children property.  This allows us to handle edge cases where the
        // property occurs sometimes as an Array and sometimes singular
        var globalBestCandidate;
        if (scanMode == "level" || scanMode == "branch") {
            var candidateCount = {};
            for (var i = 0; i < nodes.length; i++) {
                var node = nodes[i],
                    childrenProperty = null;

                // optimization: this node was up-converted from a String, it can't have
                // children
                if (node._fromString) continue;

                childrenProperty = this.findChildrenProperty(node, childrenMode);

                if (childrenProperty == null) continue;

                candidateCount[childrenProperty] = (candidateCount[childrenProperty] || 0);
                candidateCount[childrenProperty]++;
            }
            var counts = isc.getValues(candidateCount),
                candidates = isc.getKeys(candidateCount);

            if (candidates.length == 0) {
                // no children property could be found
                return;
            } else if (candidates.length == 1) {
                // use the only candidate
                globalBestCandidate = candidates[0];
            } else if (tieMode == "node") {
                // multiple candidates found, don't set globalBestCandidate and we will
                // automatically go per-node
            } else if (tieMode == "stop") {
                return;
            } else { // tieMode == "highest"
                // pick highest and proceed
                var max = counts.max(),
                    maxIndex = counts.indexOf(max);
                globalBestCandidate = candidates[maxIndex];
            }

            //this.logWarn("counts are: " + this.echo(candidateCount) +
            //             ", globalBestCandidate: " + globalBestCandidate);
        }

        var allChildren = [];
        for (var i = 0; i < nodes.length; i++) {
            var node = nodes[i];

            // default to the globalBestCandidate if there is one
            var bestCandidate = globalBestCandidate;

            if (node._fromString) continue; // can't have children

            // determine the best children property individually per node if we haven't already
            // determined it by scanning all nodes
            if (!bestCandidate) {
                bestCandidate = this.findChildrenProperty(node, childrenMode);
                //this.logWarn("individual bestCandidate: " + bestCandidate +
                //             " found for node: " + this.echo(node));
            }

            // no children found
            if (bestCandidate == null) continue;

            // normalize children to an Array (even if absent, if a single bestCandidate
            // property was determined for the level)
            var children = node[bestCandidate];
            if (children != null && !isc.isAn.Array(children)) children = [children];
            else if (children == null) children = [];

            // copy discovered children to the normalized childrenProperty
            node[newChildrenProperty] = children;

            // mark all children with a "type" property indicating the property they were found
            // under.  Needed because this information is missing once we normalize all children
            // arrays to appear under the same property name
            for (var j = 0; j < children.length; j++) {
                var child = children[j];
                // if we end up with Strings in the children (valid only with childrenMode
                // "array") auto-convert them to Objects
                if (isc.isA.String(child)) {
                    children[j] = child = {
                        name:child,
                        _fromString:true
                    }
                }
                child[typeProperty] = bestCandidate;
            }

            // proceed with this node's children
            if (scanMode == "level") {
                allChildren.addAll(children);
            } else {
                this.discoverTree(children, settings, bestCandidate);
            }
        }
        if (scanMode == "level" && allChildren.length > 0) this.discoverTree(allChildren, settings);
    },

    getCleanNodeData : function (nodeList, includeChildren, cleanChildren, includeLoadState,
                                tree)
    {
        if (nodeList == null) return null;

        var nodes = [], wasSingular = false;
        if (!isc.isAn.Array(nodeList)) {
            nodeList = [nodeList];
            wasSingular = true;
        }

        // known imperfections:
        // - by default, isFolderProperty is "isFolder", we write this into nodes and sent it when
        //   saving
        // - we create empty children arrays for childless nodes, and save them

        for (var i = 0; i < nodeList.length; i++) {
            var treeNode = nodeList[i],
                node = {};
            if (tree == null) {
                var treeID = treeNode._isc_tree;
                if (treeID) tree = window[treeID];
            }

            // copy the properties of the tree node, dropping some Tree/TreeGrid artifacts
            for (var propName in treeNode) {

                if ((tree != null && propName == tree.parentProperty) ||
                    // currently hardcoded
                    (!includeLoadState && propName == "_loadState") ||
                    propName == "_isc_tree" ||

                    propName == "__ref" ||
                    propName == "__module" ||
                    // the openProperty and isFolderProperty are documented and settable, and if
                    // they've been set should be saved, so only remove these properties if they
                    // use the prefix that indicates they've been auto-generated (NOTE: this prefix
                    // is obfuscated)
                    propName.startsWith("_isOpen_") ||
                    propName.startsWith("_isFolder_") ||

                    // default nameProperty from ResultTree, which by default does not have
                    // meaningful node names
                    propName.startsWith("__nodePath") ||
                    // class of child nodes, set up by ResultTree
                    propName == "_derivedChildNodeType" ||

                    propName == "_autoAssignedName" ||




                    // from selection model
                    propName.startsWith("_selection_") ||

                    // Explicit false passed as 'includeChildren' param.
                    (includeChildren == false &&
                     tree &&
                     propName == tree.childrenProperty) ||

                    // Do not copy the precalculated length of the tree node.
                    propName.startsWith("_cachedLength_") ||

                    // Do not copy a recursion flag on the tree node.
                    propName.startsWith("_recursionCount_") ||

                    // Do not copy a flag used for paged ResultTrees.
                    propName.startsWith("_visibleDescendantsCached_") ||

                    // Do not copy temporary state used during initial loads of children.
                    propName.startsWith("_initialLoadingState_") ||
                    propName.startsWith("_initialLoadingFetchCount_"))
                {
                    continue;
                }
                node[propName] = treeNode[propName];

                // Clean up the children as well (if there are any)
                if (cleanChildren &&
                    tree &&
                    propName == tree.childrenProperty &&
                    isc.isAn.Array(node[propName]))
                {
                    node[propName] = isc.Tree.getCleanNodeData(node[propName],
                                                               includeChildren, cleanChildren,
                                                               includeLoadState, tree);
                }
            }
            nodes.add(node);
        }
        if (wasSingular) return nodes[0];
        return nodes;
    }
});







//>    @class    Selection
//
// Maintains a 'selected' subset of a List or Array of objects, such as records in a record
// set, or widgets in a selectable header.<br><br>
//
// Includes methods for selecting objects and checking which objects are selected, and also for
// selecting objects as a result of mouse events, including drag selection support.<br>
// The selection object is used automatically to handle selection APIs on +link{class:ListGrid}
// and +link{class:TreeGrid} instances.<br><br>
//
//
// @visibility external
// @treeLocation    Client Reference/System
//<


//
//    create the Selection class
//
isc.ClassFactory.defineClass("Selection");

// add default properties to the class
isc.Selection.addProperties({
    //> @attr selection.selectionProperty (String : null : [IRA])
    // Property to use to mark records as selected.
    // <P>
    // Defaults to an auto-generated property name that starts with an underscore.
    // @visibility serverSelection
    //<
    //selectionProperty:null,

    //>@attr selection.data (Array | List : null : [IRWA])
    //  The set of data for which selection is being managed.  If not specified at init time
    //  this can be set up via the <code>selection.setData()</code> method.
    // @visibility serverSelection
    //<

    //> @attr selection.enabledProperty (String : "enabled" : [IRA])
    // Property used to indicated records as being disabled (therefore unselectable).
    //<

    enabledProperty:"enabled",

    //> @attr selection.canSelectProperty (string : "canSelect" : [IRA])
    // If set to false on a item, selection of that item is not allowed.
    //<

    canSelectProperty:"canSelect",

    //> @attr selection.cascadeSelection (boolean : false : [IRA])
    // Should children be selected when parent is selected? And should parent be
    // selected when any child is selected?
    // <p>
    // Note: Unloaded children are not affected and no load-on-demand is triggered.
    //<
    cascadeSelection:false,

    // _dirty - manages whether we need to update the cache of selected records.
    _dirty:true


});

isc.Selection.addClassProperties({
    //>    @type    SelectionStyle
    //    Different styles of selection that a list, etc. might support
    //        @visibility external
    //        @group    selection
    //
    //    @value    isc.Selection.NONE        don't select at all
    NONE :         "none",
    //    @value    isc.Selection.SINGLE    select only one item at a time
    SINGLE:        "single",
    //    @value    isc.Selection.MULTIPLE    select one or more items
    MULTIPLE:    "multiple",
    //    @value    isc.Selection.SIMPLE    select one or more items as a toggle
    //                                   so you don't need to hold down control keys to select
    //                                  more than one object
    SIMPLE:        "simple",
    //<

    // for generating unique IDs for each Selection
    _selectionID : 0
});


isc.Selection.addMethods({

//>    @method    selection.init()    (A)
//  Initialize this selection instance.<br>
//  Note: if the <code>data</code> property is not set at init time, it should be passed to
//  the selection using the <code>selection.setData</code> method
//        @group    selection
//        @param    [all arguments]    (object)    objects with properties to override from default
// @visibility serverSelection
//<
init : function () {

    // get unique ID and selection properties
    if (!this.selectionProperty) this.selectionProperty = "_selection_"+isc.Selection._selectionID++;

        this.partialSelectionProperty = "_partial" + this.selectionProperty;

    // set the data object so we get notification for add and delete, etc.
    //    NOTE: if the data object wasn't set, use a new arrays
    this.setData((this.data ? this.data : []));
},

// override destroy to clean up pointers to this.data
destroy : function () {
    if (this.data) this.ignoreData(this.data);
    delete this.data;

    // selections aren't stored in global scope so no need to clear window[this.ID]
    this.Super("destroy", arguments);
},

//>    @method    selection.setData()
//            Initialize selection data.<br><br>
//            Call this method to associate the selection with a different data object.<br>
//          <i>Note: No need to call this if the contents of the selection's data is modified</i>
//        @group    selection
//        @param        newData    (array)        new data to maintain selection in
// @visibility serverSelection
//<
setData : function (newData) {

    // if we are currently pointing to data, stop observing it
    if (this.data != null) this.ignoreData(this.data);

    // remember the new data
    this.data = newData;

    // observe the new data so we will update automatically when it changes
    if (this.data != null) this.observeData(this.data);

    // Note that any cache we have is out of date.
    this.markForRedraw();

},


//>    @method    selection.observeData()    (A)
//            Observe methods on the data so we change our state.
//            Called automatically by selection.setData().
//            Observes the data.dataChanged() method to invalidate the selection cache
//        @group    selection
//
//
//    @param    data    (array)        new data to be observed
// @visibility internal
//<
observeData : function (data) {
    var isRS = isc.ResultSet && isc.isA.ResultSet(data);
    if (isRS) {
        this.observe(data, "dataChanged", function (operationType, originalRecord, rowNum, updateData, filterChanged, dataFromCache) {
            this.dataChanged(operationType, originalRecord, rowNum, updateData, filterChanged, dataFromCache);
        });
    } else {
        this.observe(data, "dataChanged", function () {
            this.dataChanged();
        });
    }

    if (data.dataArrived) {
        if (isRS) {
            this.observe(data, "dataArrived", function (startRow, endRow, dataFromCache) {
                this.dataArrived(startRow, endRow, dataFromCache);
            });
        } else {
            this.observe(data, "dataArrived", function () {
                this.dataChanged();
            });
        }
    }
},

//>    @method    selection.ignoreData()    (A)
//            Stop observing methods on data when it goes out of scope.
//            Called automatically by setData
//        @group    selection
//
//        @param    data    (array)        old data to be ignored
// @visibility internal
//<
ignoreData : function (data) {
    if (!data) return;
    if (this.isObserving(data, "dataChanged")) this.ignore(data, "dataChanged");
    if (this.isObserving(data, "dataArrived")) this.ignore(data, "dataArrived");
},

// dataChanged implementation performs a couple of tasks:
// - reselectOnUpdate attribute set by DBC. If set, reapply selection when
//   an updated record is folded back into the ResultSet cache
// - if the dataChange is due to a local filter revealing new records that were
//   in the ResultSet's local cache but hidden due to non-matching criteria,
//   wipe any "selected" property from them so we don't have odd stale-seeming
//   selections popping up on refilter.
// - wipe our complete selection cache to account for records that may have been
//   removed from this.data
dataArrived : function (startRow,endRow,dataFromCache) {
    if (!this.data) return;


    var updateOp,
        originalRecord,
        updateData,
        rowNum,
        filterChanged;
    if (this.data._isChangingData() && isc.isA.ResultSet(this.data)) {
        updateOp = this.data._lastUpdateOperation;
        updateData = this.data._lastUpdateData;
        originalRecord = this.data._lastOrigRecord;
        rowNum = this.data._lastUpdateRow;

    }

    this.dataChanged(updateOp, originalRecord, rowNum, updateData, false, dataFromCache);

    // This method can fire from ResultSet dataChanged doing a filterLocalData()
    // In this case, suppress the next dataChanged() which fires at the end of the
    // resultSet 'dataChanged' method

    if (this.data._isChangingData()) {
        this._ignoreNextDataChanged = true;
    }
},

dataChanged : function (operationType,originalRecord,rowNum,updateData,filterChanged,dataFromCache) {
    if (this._ignoreNextDataChanged) {
        delete this._ignoreNextDataChanged;
        return;
    }
    if (this.reselectOnUpdate && operationType == "update" && originalRecord != null &&
        originalRecord[this.selectionProperty])
    {

        var modifiedRecord = this.data.findByKey(originalRecord);
        if (modifiedRecord) {
            this.performReselectOnUpdate(modifiedRecord);
        }

    // 'dataFromCache' param - only applies to resultSets:
    // Implies the change of data was satisfied from a client side cache of records
    // rather than fresh data coming from the server, and as such we (the selection)
    // may have already seen these records and marked as selected.
    // If this is the case, the selected property should be considered stale and
    // cleared.
    // Use case: A record was selected in a resultSet, the criteria were changed
    // such that it was removed from the filtered data (but retained in
    // the client-side "allRows" cache), then the criteria changed again such that
    // the record is re-introduced into the filtered data set
    // We have to treat as stale - otherwise it's easy to run into bugs such as
    // selectionType:"single" widgets ending up with multiple selected rows.
    } else if (dataFromCache) {
        if (this._dirty) this.cacheSelection();
        // return the cached selection list if possible
        var selection = this._cache,
            newCache = [],
            data = this.getItemList(),
            length = data.getLength();

        if (selection == null ||
            isc.isA.ResultSet != null && isc.isA.ResultSet(data) && !data.lengthIsKnown()) {
        } else {
            for (var i = 0; i < length; i++) {
                // getCachedRow won't trigger fetches if working with a remote dataset
                var item = data.getCachedRow(i),
                    selected = item == null ? false :


                        !!item[this.selectionProperty];

                var inCache;
                if (selected) {
                    inCache = selection.contains(item);
                    if (inCache) {
                        newCache.add(item);
                    } else {

                        item[this.selectionProperty] = false;
                    }
                }
                // If the record is not marked as selected, nothing to do
            }


        }
    }
    this.markForRedraw();
},


performReselectOnUpdate : function (modifiedRecord) {
    this.select(modifiedRecord);
},

//>    @method    selection.markForRedraw()
//            Mark the selection as dirty, so it will be recalculated automatically
//        @group    selection
// @visibility internal
//<
markForRedraw : function () {
    this._dirty = true;
    this._openCache = null;
},

//>    @method    selection.isSelected()
// Return true if a particular item is selected
//
// @param item (object) object to check
// @return (boolean) true == object is selected, false == object is not selected
// @group selection
// @visibility external
//<
// @param [onlyOpen] (Boolean) if we only require searching among open nodes when the data
// is a Tree. For example, in drawing the body of a TreeGrid, we need to know whether an open
// node is selected, so we call isSelected() with onlyOpen = true. This is an optimization
// for large sorted trees.
isSelected : function (item, onlyOpen) {
    // If the data is not a tree or cascade selection is enabled, then set onlyOpen to false.
    // onlyOpen makes sense for trees only, where it is an optimization hint to consider only
    // the open nodes.
    // Cascade selection is fundamentally incompatible with onlyOpen because if a partially
    // selected node is closed, we still want to know whether the closed children of the node
    // are selected so that they may be selected or deselected.
    if (onlyOpen && (this.cascadeSelection || !isc.isA.Tree(this.data))) {
        onlyOpen = false;
    }

    if (this._dirty && !(onlyOpen == true && this._openCache != null) && !this._cachingSelection) {
        this.cacheSelection(onlyOpen);
    }



    if (item == null) return false;

    return !!item[this.selectionProperty];
},

//> @method selection.isPartiallySelected()
// When using tree-oriented selection modes like +link{treeGrid.cascadeSelection}, returns true
// if the record is considered partially selected because only some of it's children are
// selected.
//
// @param  item    (object)  object to check
// @return (boolean)      true == object is partially selected
//                        false == object is not partially selected
// @group  selection
// @visibility external
//<
isPartiallySelected : function (item) {


    if (this._dirty && !this._cachingSelection) this.cacheSelection();
    if (item == null) return false;

    return !!item[this.partialSelectionProperty];
},


//>    @method    selection.anySelected()
// Whether at least one item is selected
//        @group    selection
//
//        @return        (boolean)    true == at least one item is selected
//                                false == nothing at all is selected
// @visibility external
//<
anySelected : function () {
    // return if the selection is non-empty
    return this._getSelection().length > 0;
},

//>    @method    selection.getLength()
// Returns the number of selected records.
//
// @return (int) number of selected records
// @group selection
// @visibility external
//<
getLength : function () {
    return this._getSelection().length;
},


//>    @method    selection.multipleSelected()
//    Whether multiple items are selected
//        @group    selection
//
//        @return        (boolean)    true == more than one item is selected
//                                false == no items are selected, or only one item is selected
// @visibility external
//<
multipleSelected : function () {
    return this._getSelection().length > 1;
},


//> @method selection.getSelection()
// Return an ordered array of all of the selected items
// @param [excludePartialSelections] (Boolean) When true, partially selected records will not be returned.
//                                   Otherwise, all fully and partially selected records are
//                                   returned.
// @return (array) list of selected items
// @group selection
// @visibility external
//<
getSelection : function (excludePartialSelections) {
    var selection = this._getSelection(excludePartialSelections);
    // duplicate the selection so subsequent manipulation of our cache doesn't confuse
    // callers.
    return selection == null ? null : selection.duplicate();
},
_getSelection : function (excludePartialSelections, dontSort) {
    // if the selection is dirty, cache it again
    if (this._dirty) {
        if (this._cachingSelection) this._cachingSelection = false;
        this.cacheSelection(false, dontSort);
    }

    // return the cached selection list if possible
    var selection = this._cache;
    // If partial selections are excluded, built a new list of full selections only.
    if (excludePartialSelections && selection != null && selection.length > 0) {
        var cache = this._cache;
        selection = [];

        // Cache includes both fully and partially selected nodes.
        for (var i = 0; i < cache.length; i++) {
            var item = cache[i];
            if (!this.isPartiallySelected(item)) {
                selection[selection.length] = item;
            }
        }
    }

    return selection;
},


//>    @method    selection.getSelectedRecord()
//            Return the first item in the list that is selected.<br><br>
//
//            Note that this should only be used if you know that one only one item
//             may be selected, or you really don't care about items after the first one.<br><br>
//
//            To get all selected objects, use <code>+link{method:selection.getSelection()}</code>
//        @group    selection
//
//        @return        (object)    first selected record, or null if nothing selected
// @visibility external
//<
getSelectedRecord : function () {
    var selection = this._getSelection();
    if (selection && selection.length > 0) return selection[0];
},


//>    @method    selection.cacheSelection()    (A)
// Cache the selected records since we operate on it so often.
// Sets an internal variable _cache to hold the selection.
// @group selection
// @visibility internal
//<

accessResultSetCache:true,
cacheSelection : function (onlyOpen, dontSort) {


    // Don't allow this method to fire recursively, or in response to 'setSelection'
    // if we're marked as dirty
    if (this._settingSelected || this._suppressCaching) return;
    else if (this._cachingSelection) {

        return;
    }
    // create a new array to hold the cached selection
    // When onlyOpen is true, we create an "openCache", which is a list of all selected open
    // nodes. As long as we're only interested in the open nodes, we can use the openCache
    // if available.
    var cache;
    if (onlyOpen) {
        cache = this._openCache = [];

    } else {

        cache = this._cache = [];
        this._openCache = null;
    }

    var data = this.getItemList(onlyOpen, dontSort);

    // This is critical path code - for maximum efficiency, if we're working with a
    // ResultSet, look directly at its localData array rather than going through the
    // getCachedRow() API
    // undocumented accessResultSetCache attribute allows us to turn this optimization
    // off for cases where it won't work (EG custom class which implements the List interface)
    var isRSCache = false;
    if (this.accessResultSetCache && isc.isA.ResultSet(data)) {
        isRSCache = true;
        data = data.localData || [];
    }
    var useGetCachedRow = !isc.isAn.Array(data),
        length = data.getLength();


    if (isc.isA.ResultSet != null && isc.isA.ResultSet(data) && !data.lengthIsKnown()) {
        this._dirty = false;
        return;
    }
    this._cachingSelection = true;
    this._cachingOnlyOpen = onlyOpen;

    // iterate over the records of the list, selecting those that have the selection property set
    // Critical path - avoid the "cascadeSelection" logic if it doesn't apply
    if (this.cascadeSelection) {
        var delayCache = false;
        for (var i = 0; i < length; i++) {

            // getCachedRow won't trigger fetches if working with a remote dataset
            var item = useGetCachedRow ? data.getCachedRow(i) : data[i];
            if (item != null && item !== Array.LOADING && this.isSelected(item, onlyOpen)) {
                // If cascadeSelection is true and new data has arrived, it may be
                // selected. In this case we need to update the 'partial' selected state
                // of parents, and the selected state of descendents.
                // To handle this - call 'setSelected' with the flag to force a recalculation of
                // cascading selection, and then loop through all records a second time, updating
                // cache.

                if (!this.isPartiallySelected(item)) {
                    this.setSelected(item, true, null, true);
                    delayCache = true;
                }
                if (!delayCache) {
                    cache[cache.length] = item
                }
            }
        }
        // cascading selection - we may have actually manipulated our selection to mark
        // parents as partially selected / children as entirely selected - loop through all
        // nodes again!
        if (delayCache) {
            if (onlyOpen) {
                cache = this._openCache = [];

            } else {
                cache = this._cache = [];
                this._openCache = null;
            }
            for (var i = 0; i < length; i++) {

                // getCachedRow won't trigger fetches if working with a remote dataset
                var item = useGetCachedRow ? data.getCachedRow(i) : data[i];
                if (item != null && item !== Array.LOADING && this.isSelected(item, onlyOpen)) {
                    cache[cache.length] = item
                }
            }
        }
    // no cascade selection
    } else {

        if (isRSCache) {
            for (var i = 0; i < length; i++) {
                var item = data[i];
                if (item != null && item !== Array.LOADING && this.isSelected(item, onlyOpen)) {
                    cache[cache.length] = item
                }
            }
        } else {
            for (var i = 0; i < length; i++) {
                var item = useGetCachedRow ? data.getCachedRow(i) : data[i];
                if (item != null && this.isSelected(item, onlyOpen)) {
                    cache[cache.length] = item
                }
            }
        }
    }

    this._cachingSelection = false;
    //this.logWarn("***** - selection re-cached - *******");

    // note that the selection is no longer dirty if we cached the entire selection
    // The _dirty flag is set if and only if the _cache (the list of all selected nodes whether
    // or not they are open) is current. Thus, if we just prepared an openCache, we shouldn't
    // unset the _dirty flag.
    if (!onlyOpen) this._dirty = false;
},


_cacheSelectionAsync : function (thisArg, timerEventProp, batchSize, callback, state) {

    if (state == null || state.afterGetItemList) {
        if (state == null) {
            // Don't allow this method to fire recursively, or in response to 'setSelection'
            // if we're marked as dirty
            if (this._cachingSelection || this._settingSelected || this._suppressCaching) {
                callback.call(thisArg);
                return;
            }

            var me = this;
            this._getItemListAsync(thisArg, timerEventProp, batchSize, function (data) {
                var state = { data: data, afterGetItemList: true };
                thisArg[timerEventProp] = me.delayCall(
                    "_cacheSelectionAsync", [thisArg, timerEventProp, batchSize, callback, state], 0);
            });
            return;
        }

        state.afterGetItemList = false;

        // create a new array to hold the cached selection
        state.cache = [];
        state.length = state.data.getLength();


        if (isc.isA.ResultSet != null && isc.isA.ResultSet(state.data) && !state.data.lengthIsKnown()) {
            this._dirty = false;
            this._cache = cache;
            this._openCache = null;
            callback.call(thisArg);
            return;
        }
        this._cachingSelection = true;
        this._cachingOnlyOpen = false;
        state.inFirstLoop = true;
        state.delayCache = false;
        state.i = 0;
    }
    var cache = state.cache,
        data = state.data,
        length = state.length,
        inFirstLoop = state.inFirstLoop,
        delayCache = state.delayCache,
        i = state.i,
        dirty = this._dirty;

    // Prevent isSelected() from calling cacheSelection().
    this._dirty = false;

    if (inFirstLoop) {
        // iterate over the records of the list, selecting those that have the selection property set
        var maxI = Math.min(length, i + batchSize);
        for (; i < maxI; i = state.i = i + 1) {

            // getCachedRow won't trigger fetches if working with a remote dataset
            var item = data.getCachedRow(i);
            if (item != null && this.isSelected(item)) {
                // If cascadeSelection is true and new data has arrived, it may be
                // selected. In this case we need to update the 'partial' selected state
                // of parents, and the selected state of descendents.
                // To handle this - call 'setSelected' with the flag to force a recalculation of
                // cascading selection, and then loop through all records a second time, updating
                // cache.

                if (this.cascadeSelection && !this.isPartiallySelected(item)) {
                    this.setSelected(item, true, null, true);
                    delayCache = state.delayCache = true;
                }
                if (!delayCache) {
                    cache[cache.length] = item;
                }
            }
        }
        if (i != length) {
            this._dirty = dirty;
            if (dirty) this._openCache = null;
            this._cachingSelection = false;
            thisArg[timerEventProp] = this.delayCall(
                "_cacheSelectionAsync", [thisArg, timerEventProp, batchSize, callback, state], 0);
            return;
        }
        inFirstLoop = state.inFirstLoop = false;
    }
    if (!inFirstLoop && delayCache) {
        // cascading selection - we may have actually manipulated our selection to mark
        // parents as partially selected / children as entirely selected - loop through all
        // nodes again!

        cache = state.cache = [];
        i = state.i = 0;
        var maxI = Math.min(length, i + batchSize);
        for (; i < length; i = state.i = i + 1) {

            // getCachedRow won't trigger fetches if working with a remote dataset
            var item = data.getCachedRow(i);
            if (item != null && this.isSelected(item)) {
                cache[cache.length] = item;
            }
        }
        if (i != length) {
            this._dirty = dirty;
            if (dirty) this._openCache = null;
            this._cachingSelection = false;
            thisArg[timerEventProp] = this.delayCall(
                "_cacheSelectionAsync", [thisArg, timerEventProp, batchSize, callback, state], 0);
            return;
        }
    }

    this._cachingSelection = false;
    //this.logWarn("***** - selection re-cached - *******");

    // note that the selection is no longer dirty
    this._dirty = false;

    this._cache = cache;
    this._openCache = null;
    callback.call(thisArg);
    return;
},


//>    @method    selection.setSelected()    (A)
// Select or deselect a particular item.<br><br>
// All other selection routines go through this one, so by observing this routine you can
// monitor all selection changes.
//        @group    selection
//
//        @param    item        (object)    object to select
//        @param    newState    (boolean)    turn selection on or off
//
//        @return            (boolean)    true == selection actually changed, false == no change
// @visibility external
//<
// We need the cascadingDirection to avoid changing direction while recursing through tree.
_$up:"up",
_$down:"down",
setSelected : function (item, newState, cascadingDirection, recalculate) {
    // bail if we don't have valid data
    if (this.data == null || this.data.destroyed) {
        return false;
    }

    if (!this._canSelectItem(item)) return false;

     var settingSelected = this._settingSelected;
     this._settingSelected = true;

    var property = this.selectionProperty,
        partialProperty = this.partialSelectionProperty,
        childProp = this.data.childrenProperty || "children",
        isNode = false;

    var oldPartialValue = (isNode ? item.getAttribute(partialProperty) : item[partialProperty]);

    // default to selecting the item
    if (newState == null) newState = true;

    // Set partial property as needed.
    if (this.cascadeSelection && !this.useRemoteSelection) {
        // If this is a parent node and we are selecting/deselecting up the tree,
        // need to determine if the selection is full or partial.
        if (cascadingDirection == this._$up) {
            var partialValue = false,
                length = item[childProp] ? item[childProp].length : 0;

            for (var i = 0; i < length; i++) {
                var child = item[childProp].get(i),
                    isChildNode = false;

                var partialChild = (isChildNode ? child.getAttribute(partialProperty)
                                                : child[partialProperty])
                ;
                if (partialChild ||
                    (newState && !this.isSelected(child)) ||
                    (!newState && this.isSelected(child)))
                {
                    partialValue = true;
                    break;
                }
            }

            if (isNode) {
                item.setAttribute(partialProperty, partialValue + "");
            } else {
                item[partialProperty] = partialValue;
            }

            // If deselecting but there is a partial selection, the node must still be selected.
            if (newState != partialValue) newState = true;
        } else if (item[childProp] && item[childProp].length > 0) {
            // Make sure a left over partial selection is cleared
            if (isNode) {
                item.removeAttribute(partialProperty);
            } else {
                delete item[partialProperty];
            }
        }
    }

    // get the oldState of the item, for detecting changes
    var oldState = isNode ? item.getAttribute(property) : item[property];
    if (oldState == null) oldState = false;
    // set the state of the item
    if (isNode) {

        item.setAttribute(property, (newState == true) + "");
        //this.logWarn("set attribute on: " + this.echoLeaf(item) + " to: " + newState +
        //             ", now reads: " + item.getAttribute(property));
    } else {
        item[property] = newState;
    }



    // remember that this was the last item to be selected
    this.lastSelectionItem = item;
    this.lastSelectionState = newState;
    this.lastSelectionPreviousState = oldState;
    this.lastSelectionPartialValue = partialValue;
    this.lastSelectionPreviousPartialValue = oldPartialValue;

    // if no change to state of item, simply return false
    var newPartialValue = (isNode ? item.getAttribute(partialProperty) : item[partialProperty]);
    var changed = true;
    if (newState == oldState && newPartialValue == oldPartialValue) {
        changed = false;
    }
    if (!recalculate && changed == false) {
        if (!settingSelected) this._settingSelected = false;
        return false;
    }



    // note that the selection is dirty so it can be recalculated
    this.markForRedraw();

    // Select/deselect parent and child records
    if (this.cascadeSelection &&
        !this.useRemoteSelection)
    {
        var lastItem = item,
            lastState = newState,
            lastPrevState = oldState,
            lastPartialState = partialValue,
            lastPrevPartialState = oldPartialValue;


        var cascadeSource = false;
        if (this.cascadeSyncOnly == null) {
            cascadeSource = true;
            this.cascadeSyncOnly = !changed;
        }


        // Select/deselect child records
        if (cascadingDirection != this._$up && !isNode &&
            item[childProp] && item[childProp].length > 0)
        {
            this.selectList(item[childProp], newState);
        }

        if (changed || cascadeSource) {
        // Select/deselect parent records
            if (cascadingDirection != this._$down && isc.isA.Tree(this.data)) {
                var parent = this.data.getParent(item);
                // note: we do this even if isSelected == newState -- we may need
                // to set a partial selected state to fully selected or vice-versa.
                if (parent) {
                    this.setSelected (parent, newState, this._$up);
                }
            }
        }

        this.lastSelectionItem = lastItem;
        this.lastSelectionState = lastState;
        this.lastSelectionPreviousState = lastPrevState;
        this.lastSelectionPartialValue = lastPartialState;
        this.lastSelectionPreviousPartialValue = lastPrevPartialState;
        if (cascadeSource) {
            this.cascadeSyncOnly = null;
        }
    }
    if (!settingSelected) this._settingSelected = false;

    // Fire selectionChange on this.target if present

    if (changed && this.target && this.target.selectionChange) {
        this.target.selectionChange(item, newState);
    }
    // return true to indicate that there was a change in the selection state
    return true;
},

_canSelectItem : function (item) {
    // if the item is null, just return
    if (item == null) {
        return false;
    }

    // if the item is not enabled, just return
    if (item[this.enabledProperty] == false) return false;
    // if the item cannot be selected, just return
    if (item[this.canSelectProperty] == false) return false;

    return true;
},


//>    @method    selection.select()
//            Select a particular item
//        @group    selection
//
//        @param        item    (object)    object to select
//        @return                (boolean)    true == selection actually changed, false == no change
// @visibility external
//<
select : function (item) {
    return this.setSelected(item, true);
},

//>    @method    selection.deselect()
//            Deselect a particular item
//        @group    selection
//
//        @param        item    (object)    object to select
//        @return                (boolean)    true == selection actually changed, false == no change
// @visibility external
//<
deselect : function (item) {
    return this.setSelected(item, false);
},


//>    @method    selection.selectSingle()
// Select a single item and deselect everything else
//        @group    selection
//
//        @param        item    (object)    object to select
//        @return                (boolean)    true == selection actually changed, false == no change
// @visibility external
//<
  selectSingle : function (item) {
    var itemWasSelected, othersWereSelected;


    // deselect the item if selected (and remember whether it was)
    itemWasSelected = this.deselect(item);
    // deselect everything else
    othersWereSelected = this.deselectAll();
    // Reselect the single item
    this.select(item);

    // return true if the item became selected or others were cleared
    return !itemWasSelected || othersWereSelected;
},

//>    @method    selection.selectList()
// Select an array of items (subset of the entire list)
//        @group    selection
//
//        @param        list    (object[])    array of objects to select
//        @return                (boolean)    true == selection actually changed, false == no change
// @visibility external
//<
selectList : function (list, newState, selectionChanged) {

    if (newState == null) newState = true;
    if (!list) return false;
    // Ensure the current cache is up to date
    if (this._dirty) this.cacheSelection();
    else if (!this._cache) this._cache = [];

    var cache = this._cache;
    var length = list.getLength();

    // The deselectAll() API uses the selectionChanged parameter to avoid a potentially costly
    // sort. Because deselectAll() gets the full list of selected records (in some order, but
    // the order doesn't matter for deselectAll()), we know that the selectionChanged list is
    // identical to the list of selected records, which are to be deselected.
    if (selectionChanged == null) {
        selectionChanged = [];
        var data = this.getItemList();

        // This is critical path code - for maximum efficiency, if we're working with a
        // ResultSet, look directly at its localData array rather than going through the
        // getCachedRow() API
        // undocumented accessResultSetCache attribute allows us to turn this optimization
        // off for cases where it won't work (EG custom class which implements the List interface)
        var isRSCache = false;
        if (this.accessResultSetCache && isc.isA.ResultSet(data)) {
            isRSCache = true;
            data = data.localData || [];
        }

        for (var i = 0; i < length; i++) {
            var item = list.get(i),
                selected = this.isSelected(item);

            if (selected == newState) continue;

            // Skip anything which isn't actually in our data
            if (data.indexOf(item) == -1) {

                continue;
            }

            // put together a list of all records that are changing to a new selection state
            if (this._canSelectItem(item)) selectionChanged[selectionChanged.length] = item;
        }
    } else {


        if (cache === selectionChanged) selectionChanged = selectionChanged.duplicate();
    }


    var orig_suppressCaching = this._suppressCaching;
    this._suppressCaching = true;

    // Set a flag indicating we're selecting a range of rows

    var orig_selectingList = this._selectingList;
    this._selectingList = length > 1 ? true : false;

    var anyChanged = false,
        length = selectionChanged.length;

    if (length > 0) this._openCache = null;

    for (var i = 0; i < length; i++) {
        var item = selectionChanged[i];
        // incrementally update the cache before calling 'setSelected'

        if (newState) {
            // selecting new records: add newly selected records at the end of the cache

            cache[cache.length] = item;
        } else {
            // deselecting records: remove the item from the cache
            cache.remove(item);
        }

        anyChanged = this.setSelected(item, newState) || anyChanged;
    }

    this._suppressCaching = orig_suppressCaching;

    // We've been updating the cache as we go. Calling "cacheSelection" here will
    // re-cache.
    // If cascadeSelection is false, this is unnecessary - simply mark as
    // not dirty (note that setSelected calls above will have marked as dirty)
    // Otherwise explicitly call cacheSelection to ensure items not explicitly
    // passed in, but selected by cascading selection, get picked up too


    if (this.cascadeSelection) {
        if (this._dirty) this.cacheSelection();
    } else {
        this._dirty = false;
    }

    this._selectingList = orig_selectingList;

    return anyChanged;
},

//>    @method    selection.deselectList()
//            Deselect an array of items (subset of the entire list)
//        @group    selection
//
//        @param        list    (object[])    array of objects to select
//        @return                (boolean)    true == selection actually changed, false == no change
// @visibility external
//<
deselectList : function (list) {
    return this.selectList(list, false);
},


//>    @method    selection.selectAll()
// Select all records of the list.
// @param [visibleNodesOnly] (boolean) If this selection's data object is a tree,
//   if <code>true</code> is passed for this parameter,
//   only visible nodes will be selected. Nodes embedded in a closed parent folder
//   (and thus hidden from the user) will not be selected.
// @return (boolean) Returns <code>true</code> if the selection actually changed,
//   <code>false</code> if not.
// @visibility external
// @group    selection
//<

selectAll : function (visibleNodesOnly) {
    // data.getLength() will only include the visible items (open) for Tree data
    if (visibleNodesOnly) return this.selectRange(0, this.data.getLength());
    else return this.selectList(this.getItemList());
},

//>    @method    selection.deselectAll()
//            Deselect ALL records of the list
//        @group    selection
//
//        @return                (boolean)    true == selection actually changed, false == no change
// @visibility external
//<
deselectAll : function () {




    // When getting the selection, it does not matter the order in which the items are deselected
    // so pass true for the dontSort optimization hint.
    var selectionChanged = this._getSelection(false, true);

    var returnVal = this.selectList(selectionChanged, false, selectionChanged);

    // Because we now know that everything has been deselected, mark the selection as not dirty
    // and set the selection _cache to an empty array.
    this._dirty = false;
    this._cache = [];
    this._openCache = null;

    return returnVal;
},


//>    @method    selection.selectItem()
// Select a particular item by its position in the list
//
//        @param    position    (number)    index of the item to be selected
//        @return                (boolean)    true == selection actually changed, false == no change
// @group selection
// @visibility external
//<
selectItem : function (position) {
    return this.selectRange(position, position+1);
},


//>    @method    selection.deselectItem()
// Deselect a particular item by its position in the list
//
//        @param    position    (number)    index of the item to be selected
//        @return                (boolean)    true == selection actually changed, false == no change
// @group selection
// @visibility external
//<
deselectItem : function (position) {
    return this.deselectRange(position, position+1);
},





//>    @method    selection.selectRange()
//            Select range of records from <code>start</code> to <code>end</code>, non-inclusive.
//        @group    selection
//
//        @param    start        (number)    start index to select
//        @param    end            (number)    end index (non-inclusive)
//      @param  [newState]  (boolean)   optional new selection state to set.  True means
//                                      selected, false means unselected.  Defaults to true.
//
//        @return                (boolean)    true == selection actually changed, false == no change
// @visibility external
//<
selectRange : function (start, end, newState) {
    if (newState == null) newState = true;

    // Use visible records for range selection
    var data = this.data;



    if (isc.isA.ResultSet != null && isc.isA.ResultSet(data) &&
        !data.rangeIsLoaded(start, end))
    {
        this.warnSelectionRangeNotLoaded();
        return false; // no change
    }

    return this.selectList(data.getRange(start, end), newState);
},

warnSelectionRangeNotLoaded : function () {
    this.logWarn("selectRange called - selection range not loaded. Showing " +
                "selectionRangeNotLoadedMessage to the user.");
    isc.warn(this.selectionRangeNotLoadedMessage);
},

//> @attr selection.selectionRangeNotLoadedMessage (String : Can't select that many records at once.&lt;br&gt;&lt;br&gt;Please try working in smaller batches. : IRWA)
//
// Message to display to the user in a <code>warn</code> dialog if +link{selection.selectRange()} is
// called for a selection on a ResultSet, where the range of records to be selected has not been
// loaded.
// @group i18nMessages
// @visibility external
//<
selectionRangeNotLoadedMessage:"Can't select that many records at once.<br><br>" +
                                "Please try working in smaller batches.",


//>    @method    selection.deselectRange()
//            Deselect range of records from <code>start</code> to <code>end</code>, non-inclusive
//
//        @group    selection
//
//        @param    start    (number)    start index to select
//        @param    end        (number)    end index (non-inclusive)
//
//        @return                (boolean)    true == selection actually changed, false == no change
// @visibility external
//<
deselectRange : function (start, end) {
    return this.selectRange(start, end, false);
},

// DOCFIX: this methods shouldn't require the "target", a Canvas.  Need to fix that before we make
// them public.

//>    @method    selection.selectOnMouseDown()    (A)
// Update the selection as the result of a mouseDown event.
// Handles shift, control, etc. key selection as well.
// Call this from a mouseDown handler.
//
// @group selection, mouseEvents
//
// @param target (Canvas) target object
// @param position (number) position where mouse went down on
//
// @return (boolean) true == selection was changed, false == no change
//<
selectOnMouseDown : function (target, recordNum) {

    // modify selection according to the specified style (defaulting to multiple selection)
    var selectionType = target.selectionType || isc.Selection.MULTIPLE;

    // if the target's selectionType is NONE, just bail
    if (selectionType == isc.Selection.NONE)    return false;

    // remember mouseDown location in case we start drag selecting
    this.startRow = this.lastRow = recordNum;

    //>DEBUG
    this.logDebug("selectOnMouseDown: recordNum: " + recordNum);
    //<DEBUG

    // Pull record based on the visible records
    var record = this.data.get(recordNum),
        recordSelected = this.isSelected(record, true),
        selection // only compute this when used because it can be expensive
    ;

    // prevent mouse-based selection of the LOADING record.  This doesn't make sense and create
    // client-side JS errors very easily.
    if (Array.isLoading(record)) return false;

    // clear flags for deselecting records on mouseUp
    // these are set in the simple and normal cases below (3 and 5)
    // see selectOnMouseUp() for details
    this.deselectRecordOnMouseUp = false;
    this.deselectOthersOnMouseUp = false;


    // In Windows ctrl-click works allows multiple independent row selection/deselection
    // In Mac the equivalent functionality occurs with the Apple key (meta key), since
    //  on that platform ctrl+click == right click

    var metaKeyDown = (isc.Browser.isMac ? isc.EventHandler.metaKeyDown()
                                         : isc.EventHandler.ctrlKeyDown()),
        shiftKeyDown = isc.EH.shiftKeyDown();

    // Case 1: SINGLE selection
    if (selectionType == isc.Selection.SINGLE) {
        // On ctrl+click allow deselection

        if (metaKeyDown && recordSelected) this.deselect(record);
        else if (!recordSelected) this.selectSingle(record);
        else return false;

        return true;

    // Case 2: Shift-selection (select contiguous range of records)

    } else if (shiftKeyDown) {
        selection = this._getSelection();
        // if nothing was selected,
        if (selection.length == 0) {
            // simply select that record
            this.select(record);
            return true;

        // otherwise since something was selected
        } else {
            // select a range of records (visible)
            var data = this.data,
                firstRecord = data.indexOf(selection[0]),
                lastRecord   = data.indexOf(selection.last())
            ;
            // if the clicked record is the last record or beyond
            if (recordNum >= lastRecord) {
                // select from the firstRecord to the clicked record
                this.selectRange(firstRecord, recordNum + 1);

            // else if the clicked record is the first record or before
            } else if (recordNum <= firstRecord) {
                // select from the clicked record to the lastRecord
                this.selectRange(recordNum, lastRecord + 1);

            // otherwise it's in between the start and end record
            } else {
                // select from the firstRecord to the clicked record
                this.selectRange(firstRecord, recordNum + 1);
                // and deselect form the clickedRecord + 1 to the lastRecord
                this.deselectRange(recordNum + 1, lastRecord +1);
            }
            return true;
        }


    // Case 3: SIMPLE selection (toggle selection of this record, but defer deselection until
    // mouseUp)
    } else if (selectionType == isc.Selection.SIMPLE) {

        if (!recordSelected) {
            this.select(record);
            return true;
        } else {
            this.deselectRecordOnMouseUp = true;
            return false;
        }


    // Case 4: meta-key selection in a multiple selection range
    // (simply toggle selection of this record)
    } else if (metaKeyDown) {

        this.setSelected(record, !recordSelected);
        return true;

    // Case 5: normal selection (no modifier keys) in a multiple selection range
    } else {

        if (!recordSelected) {
            // if you click outside of the selection, select the new record and deselect
            // everything else
            this.selectSingle(record);
            return true;
        } else if (isc.EventHandler.rightButtonDown()) {
            // never deselect if you right click on the selection, unless you start drag
            // selecting
            this.deselectOnDragMove = true;
            return false;
        } else {
            // simpleDeselect mode: this mode is designed to make it easy to entirely get rid
            // of your selection, so you don't have to know about ctrl-clicking.  In a
            // nutshell, if you click on the existing selection, it will be entirely
            // deselected.

            if (this.dragSelection) {
                if (this.simpleDeselect) {
                    // if you click on the selection, deselect the entire selection including
                    // the clicked-on cell.  Later, if a drag begins, select the clicked-on
                    // cell.
                    this.deselectAll();
                    this.selectOriginOnDragMove = true;
                    return true;
                }
                // for a drag selection, deselect others immediately; otherwise we'll be
                // dragging out a new selection within/overlapping with an existing selection,
                // which we only want to do on a ctrl-click.  This matches Excel.
                this.selectSingle(record);
                return true;
            } else {
                if (this.simpleDeselect) {
                    // deselect everything on mouseUp, including the cell clicked on
                    this.deselectAllOnMouseUp = true;
                } else {
                    // if we click in a multiple selection, deselect everything but the
                    // clicked-on item, but don't do it until mouseUp in order to allow
                    // dragging the current selection.  This matches Windows Explorer.

                    selection = this._getSelection();
                    this.deselectOthersOnMouseUp = (selection.length > 1);
                }
                return false;
            }
        }
    }

},

//>    @method    selection.selectOnDragMove()    (A)
//            During drag selection, update the selection as a result of a dragMove event
//
//        @group    selection, mouseEvents
//
//        @param    target    (Canvas)    target object
//        @param    position (number)    position where mouse went down on
//
//        @return            (boolean)    true == selection was changed, false == no change
//
//<
selectOnDragMove : function (target, currRow) {
    var startRow = this.startRow,
        lastRow = this.lastRow;

    // If the mouse has moved further away from the start position since the last dragMove, select
    // more cells.  If it's moved closer to the start position, deselect cells.
    if (currRow < 0) {
        //>DEBUG
        this.logWarn("selectOnDragMove: got negative coordinate: " + currRow);
        //<DEBUG
        return;
    }

    if (currRow == lastRow) return; // no change



    if (this.selectOriginOnDragMove) {
        this.select(this.data.getItem(startRow));
        this.selectOriginOnDragMove = false;
    } else if (this.deselectOnDragMove || this.deselectAllOnMouseUp || this.deselectOthersOnMouseUp) {
        // deselect on dragMove is for right-dragging.  The others flags are failsafes in case you
        // use drag selection without setting the flag.
        this.selectSingle(this.data.getItem(startRow));
        this.deselectAllOnMouseUp = this.deselectOthersOnMouseUp = this.deselectOnDragMove = false;
    }

    if ((currRow > startRow && startRow > lastRow) ||
        (lastRow > startRow && startRow > currRow))
    {
        //this.logWarn("dragged from one side of start to the other");
        // dragged from one side of start to the other
        this.deselectAll();
        // select from start to current inclusive
        if (startRow > currRow) {
            this.selectRange(currRow, startRow+1);
        } else {
            this.selectRange(startRow, currRow+1);
        }
    } else if (startRow >= lastRow && lastRow > currRow) {
        //this.logWarn("increasing selection on the left of start");
        // increasing selection on the left of start
        this.selectRange(currRow, lastRow);
    } else if (startRow >= currRow && currRow > lastRow) {
        //this.logWarn("decreasing selection on the left of start");
        // decreasing selection on the left of start
        this.deselectRange(lastRow, currRow);
    } else if (startRow <= currRow && currRow < lastRow) {
        //this.logWarn("decreasing selection on the right of start");
        // decreasing selection on the right of start
        this.deselectRange(currRow+1, lastRow+1);
    } else if (startRow <= lastRow && lastRow < currRow) {
        //this.logWarn("increasing selection on the right of start");
        // increasing selection on the right of start
        this.selectRange(lastRow, currRow+1);
    //>DEBUG
    } else {
        this.logWarn("dragMove case not handled: lastRow: " + lastRow +
                     ", currRow: " + currRow + ", startRow " + startRow);
    //<DEBUG
    }

    this.lastRow = currRow;
},

//>    @method    selection.selectOnMouseUp()    (A)s
// Update the selection as the result of a mouseUp event.
// We currently use this to defer deselection for drag-and-drop of multiple records.
// Call this from a mouseUp handler.
//
// @param target (Canvas) target object
// @param recordNum (number) record number mouse went down on
//
// @return (boolean) true == selection was changed, false == no change
// @see ListGrid.mouseUp()
// @group selection, mouseEvents
//<
selectOnMouseUp : function (target, recordNum) {
    // if the target's selectionType is NONE, just bail
    if (target.selectionType == isc.Selection.NONE)    return false;

    //>DEBUG
    this.logDebug("selectOnMouseUp: recordNum: " + recordNum);
    //<DEBUG

    // JMD 020828:
    //        If multiselection is on and no modifier keys are down, we need to
    // deselect any rows other than the one that is clicked. BUT, we can't do this in
    // selectOnMouseDown() because the user might be clicking on a row in a multiple selection
    // to initiate a drag operation with all of the selected rows. So in selectOnMouseDown()
    // we set a deselectOthersOnMouseUp flag that we can check here and do the deselection
    // if necessary.
    //        Similarly, if SIMPLE selection is enabled we don't want to deselect the current
    // record if the user is initiating a drag. We set a deselectRecordOnMouseUp flag for in this case.
    //
    // We never deselect anything on rightMouseUp since you would right click to show a context menu
    // to operate on the current selection.
    var returnVal = false;
    if (this.deselectOthersOnMouseUp) {
        returnVal = this.selectSingle(this.data.getItem(recordNum));
        this.deselectOthersOnMouseUp = false;
    } else if (this.deselectRecordOnMouseUp) {
        returnVal = this.deselect(this.data.getItem(recordNum));
        this.deselectRecordOnMouseUp = false;
    } else if (this.deselectAllOnMouseUp) {
        returnVal = this.deselectAll();
        this.deselectAllOnMouseUp = false;
    }
    return returnVal;
},

// @param [onlyOpen] (Boolean) are only opened nodes required?
// @param [dontSort] (Boolean) optimization hint for whether the normalizer needs to
// be applied.
getItemList : function (onlyOpen, dontSort) {
    var data = this.data;
    if (isc.isA.Tree(data)) return data.getNodeList(onlyOpen, dontSort);
    return (data != null ? data : []);
},


_getItemListAsync : function (thisArg, timerEventProp, batchSize, callback) {
    if (this.data && isc.isA.Tree(this.data) && isc.isA.Function(this.data._getNodeListAsync)) {
        this.data._getNodeListAsync(thisArg, timerEventProp, batchSize, callback);
    } else {
        callback.call(thisArg, this.getItemList());
    }
}
});    // END isc.Selection.addMethods()









//>    @class    DetailViewer
//
//  Displays one or more records "horizontally" with one property per line.
//
//  @implements DataBoundComponent
//  @treeLocation Client Reference/Grids
//  @visibility external
//<

isc.ClassFactory.defineClass("DetailViewer", "Canvas", "DataBoundComponent");

// add default properties
isc.DetailViewer.addProperties({

    // Data
    // --------------------------------------------------------------------------------------------

    //>    @attr    detailViewer.data        (Array[] of DetailViewerRecord | Array[] of Record | RecordList : null : IRW)
    // A single record object or an array of them, specifying data. Note that DetailViewers do
    // not observe changes to the data array (in other words they will not automatically
    // re-draw when the data provided via this property is altered).
    //
    // @group basics
    // @visibility external
    //<

    //> @attr detailViewer.dataFetchMode (FetchMode : "basic" : IRW)
    // DetailViewers do not yet support paging, and will fetch and render all available
    // records.
    //
    // @group databinding
    // @visibility external
    //<
    dataFetchMode:"basic",

    // dataArity:"either" - DetailViewers support viewing single, or multiple records
    dataArity:"either",

    //> @object DetailViewerRecord
    //
    // A DetailViewerRecord is an object literal with properties that define the values for the
    // various fields of a +link{DetailViewer}.
    // <p>
    // For example a DetailViewer that defines the following fields:
    // <pre>
    // fields : [
    //     {name: "field1"},
    //     {name: "field2"}
    // ],
    // </pre>
    // Might have the following data:
    // <pre>
    // data : [
    //     {field1: "foo", field2: "bar"},
    //     {field1: "field1 value", field2: "field2 value"}
    // ]
    // </pre>
    // Each element in the data array above is an instance of DetailViewerRecord - notice that
    // these are specified simply as object literals with properties.
    //
    // @treeLocation Client Reference/Grids/DetailViewer
    // @visibility external
    //<

    //> @attr detailViewerRecord.linkText (string : null : IRW)
    // The HTML to display in this record for fields with type set to "link". This overrides
    // +link{attr:detailViewerField.linkText}.
    //
    // @see attr:detailViewerField.type
    // @see attr:detailViewerField.linkText
    // @see attr:detailViewer.linkTextProperty
    // @visibility external
    //<

    // Fields
    // ---------------------------------------------------------------------------------------

    //>    @attr    detailViewer.fields        (Array of DetailViewerField : null : IRW)
    //
    // An array of field objects, specifying the order and type of fields to display in this
    // DetailViewer.  In DetailViewers, the fields specify rows.
    //
    // @visibility external
    //<

    //> @object DetailViewerField
    //
    // An object literal with a particular set of properties used to configure the display of
    // and interaction with the rows of a +link{DetailViewer}.
    //
    // @treeLocation Client Reference/Grids/DetailViewer
    // @visibility external
    //<

    //> @attr DetailViewerField.name (String : null : IR)
    // Name property used to identify the field, and determines which attribute from
    // records will be displayed in this field.
    // <P>
    // Must be unique within the DetailViewer as well as a valid JavaScript identifier,
    // as specified by ECMA-262 Section 7.6 (the <smartclient>+link{String.isValidID()}</smartclient>
    // <smartgwt>StringUtil.isValidID()</smartgwt> function can be used to test whether
    // a name is a valid JavaScript identifier).
    // <P>
    // The attribute of the records to display in this field may also be set by
    // +link{displayField}.
    // @visibility external
    //<

    //> @attr DetailViewerField.displayField (String : null : IR)
    // If <code>displayField</code> is defined for the field then the DetailViewer will
    // display the <code>displayField</code> attribute of records instead of the attribute
    // given by the +link{name} of the field.
    // @visibility external
    //<

    //> @attr DetailViewerField.dataPath (String : null : IRA)
    // dataPath property allows this field to display detail from nested data structures
    // @visibility external
    //<

    //> @method DetailViewerField.formatCellValue()
    // Optional method to format the value to display for this field's cells. Takes precedence
    // over +link{DetailViewer.formatCellValue()} for cells in this field.
    // @param value (string) the raw value of the cell
    // @param record (detailViewerRecord) the record being displayed
    // @param field (detailViewerField) the field being displayed
    // @param viewer (detailViewer) the detailViewer containing this field
    // @visibility external
    //<

    //> @attr detailViewerField.dateFormatter (DateDisplayFormat : null : [IRW])
    // Display format to use for date type values within this field.
    // <P>
    // The +link{detailViewerField.timeFormatter} may also be used to format underlying Date values as
    // times (ommitting the date part entirely). If both <code>dateFormatter</code> and
    // <code>timeFormatter</code> are specified on a field, for
    // fields specified as +link{detailViewerField.type,type "time"} the
    // <code>timeFormatter</code> will be used, otherwise the <code>dateFormatter</code>
    // <P>
    // If <code>field.dateFormatter</code> and <code>field.timeFormatter</code> is unspecified,
    // date display format may be defined at the component level via
    // +link{detailViewer.dateFormatter}, or for fields of type <code>"datetime"</code>
    // +link{detailViewer.datetimeFormatter}. Otherwise the
    // default is to use the system-wide default normal date format, configured via
    // +link{Date.setNormalDisplayFormat()}.  Specify any valid +link{type:DateDisplayFormat} to
    // change the format used by this item.
    //
    // @see listGrid.dateFormatter
    // @see listGrid.datetimeFormatter
    // @see listGridField.timeFormatter
    // @visibility external
    //<

    //>    @attr detailViewerField.timeFormatter (TimeDisplayFormat : null : [IRWA])
    // Time-format to apply to date type values within this field.  If specified, any
    // dates displayed in this field will be formatted as times using the appropriate format.
    // This is most commonly only applied to fields specified as type <code>"time"</code> though
    // if no explicit +link{detailViewerField.dateFormatter} is specified it will be respected for other
    // fields as well.
    // <P>
    // If unspecified, a timeFormatter may be defined
    // +link{detailViewer.timeFormatter,at the component level} and will be respected by fields
    // of type <code>"time"</code>.
    //
    // @group appearance
    // @visibility external
    //<
    //timeFormatter:null

    //> @attr detailViewerField.decimalPrecision (number : null : [IRW])
    // @include dataSourceField.decimalPrecision
    //
    // @group appearance
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr detailViewerField.decimalPad (number : null : [IRW])
    // @include dataSourceField.decimalPad
    //
    // @group appearance
    // @serverDS allowed
    // @visibility external
    //<

    //>    @attr    detailViewerField.imageSize (Integer | String : null : IRW)
    // Size of images shown for fieldTypes image in this field.
    // <P>
    // If set to a String, assumed to be a property on each record that specifies the image
    // height.  For example, if <code>field.imageSize</code> is "logoSize",
    // <code>record.logoSize</code> will control the size of the image.
    //
    // @see attr:detailViewerField.imageWidth
    // @see attr:detailViewerField.imageHeight
    //
    // @group imageColumns
    // @visibility external
    //<

      //>    @attr    detailViewerField.imageWidth (Integer | String : null : IRW)
    // Width of images shown for fieldTypes image in this field.
    // <P>
    // If set to a String, assumed to be a property on each record that specifies the image
    // width.  For example, if <code>field.imageWidth</code> is "logoWidth",
    // <code>record.logoWidth</code> will control the width of the image.
    //
    // @see attr:detailViewerField.imageSize
    // @see attr:detailViewerField.imageHeight
    //
    // @group imageColumns
    // @visibility external
    //<

      //>    @attr    detailViewerField.imageHeight (Integer | String : null : IRW)
    // Height of image shown for fieldTypes image in this field.
    // <P>
    // If set to a String, assumed to be a property on each record that specifies the image
    // height.  For example, if <code>field.imageHeight</code> is "logoHeight",
    // <code>record.logoHeight</code> will control the height of the image.
    //
    // @see attr:detailViewerField.imageSize
    // @see attr:detailViewerField.imageWidth
    //
    // @group imageColumns
    // @visibility external
    //<

    //> @attr   detailViewerField.imageURLPrefix (string : null : IRWA)
    // If this field has type set to <code>"image"</code>
    // and the URL for the image displayed is not absolute, the path of the URL will be relative
    // to this string<br>
    //
    // @group imageColumns
    // @visibility external
    //<

    //> @attr detailViewerField.linkText (String : null : IRW)
    // The HTML to display for values of this field if the field type is set to "link".
    // <P>
    // This property sets linkText that will be the same for all records.  You can set linkText
    // on a per-record basis via +link{attr:detailViewerRecord.linkText}.
    //
    // @see attr:detailViewerField.type
    // @see attr:detailViewerRecord.linkText
    // @see attr:detailViewer.linkTextProperty
    // @see attr:detailViewerField.linkTextProperty
    // @visibility external
    //<

    //> @attr detailViewerField.linkTextProperty (string : null : IRW)
    // Name of the property in a DetailViewerRecord that holds the HTML to display for values
    // of this field if the field type is set to "link".
    //
    // @see attr:detailViewerField.type
    // @see attr:detailViewerRecord.linkText
    // @see attr:detailViewerField.linkText
    // @see attr:detailViewer.linkTextProperty
    // @visibility external
    //<

    //> @attr detailViewerField.linkURLPrefix (string : null : IRWA)
    // If this field has type set to <code>"link"</code>, setting this property will apply a
    // standard prefix to the link URL when displaying values of this field.
    // @see attr:detailViewerField.type
    // @visibility external
    //<

    //> @attr detailViewerField.linkURLSuffix (string : null : IRWA)
    // If this field has type set to <code>"link"</code>, setting this property will apply a
    // standard suffix to the link URL when displaying values of this field.
    // @see attr:detailViewerField.type
    // @visibility external
    //<

    //> @attr detailViewerField.target (string : "_blank" : IRW)
    // By default, clicking a link rendered by this item opens it in a new browser window.  You
    // can alter this behavior by setting this property.  The value of this property will be
    // passed as the value to the <code>target</code> attribute of the anchor tag used to render
    // the link.  <code>target</code> is applicable only if the field type is set to "link".
    // @see detailViewerField.type
    // @visibility external
    //<

    //> @attr detailViewerField.format (FormatString : null : IR)
    // +link{FormatString} for numeric or date formatting.  See +link{dataSourceField.format}.
    // @group exportFormatting
    // @visibility external
    //<

    //> @attr detailViewerField.exportFormat (FormatString : null : IR)
    // +link{FormatString} used during exports for numeric or date formatting.  See
    // +link{dataSourceField.exportFormat}.
    // @group exportFormatting
    // @visibility external
    //<

    //> @attr detailViewerField.exportRawValues (Boolean : null : IR)
    //  Dictates whether the data in this field should be exported raw by
    // +link{detailViewer.exportClientData, exportClientData()}.  If set to true for a
    // field, the values in the field-formatters will not be executed for data in this field.
    // @visibility external
    //<

    // ------------
    // Hilite Icons
    // ------------

    //> @attr detailViewer.hiliteIcons (Array of String : ["[SKINIMG]/Dialog/notify.png", "[SKINIMG]/Dialog/warn.png", "[SKINIMG]/actions/approve.png"] : IR)
    // @include dataBoundComponent.hiliteIcons
    // @group hiliting
    // @visibility external
    //<

    //> @attr detailViewer.hiliteIconPosition (HiliteIconPosition : "before" : IR)
    // @include dataBoundComponent.hiliteIconPosition
    // @group hiliting
    // @visibility external
    //<

    //> @attr detailViewer.hiliteIconSize (number : 12 : IRW)
    // @include dataBoundComponent.hiliteIconSize
    // @see hiliteIconWidth
    // @see hiliteIconHeight
    // @see DetailViewerField.hiliteIconSize
    // @group hiliting
    // @visibility external
    //<

    //> @attr detailViewer.hiliteIconWidth (number : null : IRW)
    // @include dataBoundComponent.hiliteIconWidth
    // @group hiliting
    // @visibility external
    //<

    //> @attr detailViewer.hiliteIconHeight (number : null : IRW)
    // @include dataBoundComponent.hiliteIconHeight
    // @group hiliting
    // @visibility external
    //<

    //> @attr   detailViewer.hiliteIconLeftPadding (number : 2 : IRW)
    // @include dataBoundComponent.hiliteIconLeftPadding
    // @group hiliting
    // @visibility external
    //<

    //> @attr   detailViewer.hiliteIconRightPadding (number : 2 : IRW)
    // @include dataBoundComponent.hiliteIconRightPadding
    // Can be overridden at the field level
    // @group hiliting
    // @visibility external
    //<

    //> @attr detailViewerField.canHilite (boolean : null : IRW)
    // @include listGridField.canHilite
    // @group hiliting
    // @visibility external
    //<

    //> @attr detailViewerField.hiliteIconPosition (HiliteIconPosition : null : IR)
    // When +link{detailViewer.hiliteIcons} are present, where the hilite icon will be placed
    // relative to the field value.  See +link{type:HiliteIconPosition}.
    // Overrides +link{detailViewer.hiliteIconPosition}
    // @group hiliting
    // @visibility external
    //<

    //> @attr detailViewerField.hiliteIconSize (number : null : IRW)
    // Default width and height of +link{detailViewer.hiliteIcons, hilite icons} in this field.
    // Takes precedence over hiliteIconWidth, hiliteIconHeight and hiliteIconSize specified at
    // the component level.
    // Can be overridden via +link{hiliteIconWidth,hiliteIconWidth} and +link{hiliteIconHeight,hiliteIconHeight}
    // @group hiliting
    // @see detailViewer.hiliteIconSize
    // @see detailViewerField.hiliteIconWidth
    // @see detailViewerField.hiliteIconHeight
    // @visibility external
    //<

    //> @attr detailViewerField.hiliteIconWidth (number : null : IRW)
    // Width for hilite icons for this field.
    // Overrides +link{detailViewer.hiliteIconSize}, +link{detailViewer.hiliteIconWidth}, and
    // +link{detailViewerField.hiliteIconSize}.
    // @group hiliting
    // @visibility external
    //<

    //> @attr detailViewerField.hiliteIconHeight (number : null : IRW)
    // Height for hilite icons for this field.
    // Overrides +link{detailViewer.hiliteIconSize}, +link{detailViewer.hiliteIconHeight}, and
    // +link{DetailViewerField.hiliteIconSize}.
    // @group hiliting
    // @visibility external
    //<

    //> @attr   detailViewerField.hiliteIconLeftPadding (number : null : IRW)
    // How much padding should there be on the left of +link{detailViewer.hiliteIcons, hilite icons}
    // for this field?
    // Overrides +link{detailViewer.hiliteIconLeftPadding}
    // @group hiliting
    // @visibility external
    //<

    //> @attr   detailViewerField.hiliteIconRightPadding (number : null : IRW)
    // How much padding should there be on the right of +link{DetailViewer.hiliteIcons, hilite icons}
    // for this field?
    // Overrides +link{detailViewer.hiliteIconRightPadding}
    // @group hiliting
    // @visibility external
    //<


    //> @method detailViewerField.showIf
    //
    // If specified on a field, this method is evaluated at draw time to determine whether or
    // not to show this particular field.
    // <p>
    // This method can be specified either as a function or a string that will be
    // auto-converted to a function.
    //
    // @param viewer (DetailViewer) The DetailViewer
    // @param valueList (List of DetailViewerRecord)
    //
    // @return (boolean) true to show the field, false to not show it.
    //
    // @visibility external
    //<

    //> @attr detailViewerField.type (String : null : IR)
    //
    // Specifies the type of this DetailViewerField.  By default (value is <code>null</code>)
    // the field shows a field title on the left and the field value on the right.  There are
    // four special values for this attribute:
    // <ul>
    // <li>"header" - If you specify type "header", the field spans both the field name and
    // field value columns and contains text defined in the +link{DetailViewerField.value}
    // attribute with the style specified by +link{DetailViewer.headerStyle}.  You can use this
    // field type as a titled separator.
    // <li>"separator" - If you specify type "separator", the field spans both the field name
    // and the field value columns with no text, and is styled using the style specified via
    // +link{DetailViewer.separatorStyle}.  The height of the separator field can be controlled
    // via +link{DetailViewerField.height}.
    // <li>"image" For viewing, a thumbnail image is rendered in the field.  The URL of the
    // image is the value of the field, and should be absolute. The size of the image is
    // controlled by +link{attr:DetailViewerField.imageSize},
    // +link{attr:DetailViewerField.imageWidth}, +link{attr:DetailViewerField.imageHeight}
    // <li><p>"link" For viewing, a clickable html link (using an HTML anchor tag: &lt;A&gt;)
    // is rendered in the field.  The target URL is the value of the field, which is also the
    // default display value.  You can override the display value by setting
    // +link{attr:detailViewerRecord.linkText} or +link{attr:detailViewerField.linkText}.</p>
    // <p>
    // Clicking the link opens the URL in a new window by default.  To change this behavior,
    // you can set <code>field.target</code>, which works identically to the "target"
    // attribute on an HTML anchor (&lt;A&gt;) tag.  See +link{detailViewerField.target} for
    // more information.</p></li>
    // </ul>
    //
    // @visibility external
    //<

    //> @attr detailViewerField.title (HTMLString : null : IR)
    //
    // The title of the field as displayed on the left-hand side.  If left unspecified, the
    // title of the field is derived by looking up the value of
    // +link{DetailViewer.fieldIdProperty} on this field.  So, by default, the title of a field
    // is the value of its "name" property.
    //
    // @see DetailViewer.fieldIdProperty
    // @visibility external
    //<

    //> @attr detailViewerField.valueMap (object : null : IR)
    //
    // A property list (or an expression that evaluates to a property list)
    // specifying a mapping of internal values to display values for the field (row).
    //
    // @visibility external
    //<

    //> @attr detailViewerField.escapeHTML (boolean : null : IR)
    // By default HTML values in DetailViewer cells will be interpreted by the browser.
    // Setting this flag to true will causes HTML characters to be escaped, meaning the
    // raw value of the field (for example <code>"&lt;b&gt;AAA&lt;/b&gt;"</code>) is displayed
    // to the user rather than the interpreted HTML (for example <code>"<b>AAA</b>"</code>)
    //
    // @visibility external
    //<

    //> @attr detailViewerField.value (HTMLString : "undefined" : IR)
    //
    // When a field specifies its +link{detailViewerField.type} to be "header", the value of
    // this attribute specifies the header text.
    //
    // @visibility external
    //<


    //> @attr detailViewerField.width (Number : null : IR)
    //
    // @visibility internal
    //<

    //> @attr detailViewerField.height (Number : null : IR)
    //
    // For +link{DetailViewerField.type}: <code>"separator"</code>, this attribute specifies
    // the height of the separator.
    //
    // @visibility external
    //<

    //>    @method    detailViewerField.getCellStyle()
    // Optional method to return the CSS class for cells in this field. If specified, this method
    // will be called from +link{detailViewer.getCellStyle()}, and should return a css class name.
    //
    //        @param    value        (string) actual value of this cell
    //        @param    field      (object)    field object for this cell
    //        @param    record      (object) record object for this cell
    //      @param  viewer      (DetailViewer) the viewer instance to which this cell belongs
    //
    //        @return    (CSSStyleName)    CSS style for this cell
    // @group    appearance
    // @visibility external
    //<


    //>    @attr    detailViewerField.cellStyle (CSSClassName : null : IRW)
    // If specified, cells in this field will be rendered using this css className rather than
    // +link{detailViewer.cellStyle}
    // @visibility external
    //<

    //>    @attr    detailViewerField.printCellStyle (CSSClassName : null : IRW)
    // If specified, when generating print HTML for this detailViewer,
    // cells in this field will be rendered using this css className rather than
    // +link{detailViewer.printCellStyle}
    // @visibility external
    //<


    //> @attr detailViewerField.showFileInline    (boolean : null : [IR])
    // For a field of type:"imageFile", indicates whether to stream the image and display it
    // inline or to display the View and Download icons.
    //
    // @visibility external
    //<

    //> @attr detailViewerField.canExport (Boolean : null : IR)
    //    Dictates whether the data in this field be exported.  Explicitly set this
    //  to false to prevent exporting.  Has no effect if the underlying
    //  +link{dataSourceField.canExport, dataSourceField} is explicitly set to
    //  canExport: false.
    //
    // @visibility external
    //<

    //>    @attr    detailViewer.fieldIdProperty (string : "name" : IRWA)
    // Name of the field in the DetailViewerRecord which specifies the data property for that record.
    // @visibility external
    //<
    fieldIdProperty:"name",

    //> @attr detailViewerField.includeFrom (string : null : [IR])
    // Indicates this field's values come from another, related DataSource.
    // The individual field will inherit settings such as +link{DetailViewerField.type,field.type}
    // and +link{DetailViewerField.title,field.title} from the related DataSource just like
    // fields from the primary DataSource.
    //
    // @visibility crossDS
    //<

    // Multi-record display
    // --------------------------------------------------------------------------------------------

    //>    @attr    detailViewer.recordsPerBlock        (number : 1 : [IRW])
    //          The number of records to display in a block. A block is a horizontal row on a page
    //          containing one or more records, as specified by the value of recordsPerBlock. The
    //          height of a block is equal to the height of a single record. The default setting of
    //          1 causes each record to appear by itself in a vertical row. Setting recordsPerBlock
    //          to 2 would cause records to appear side by side in groups of two.
    //          Use a value of "*" to indicate all records.
    // @group appearance
    // @visibility external
    //<
    recordsPerBlock:1,

    //>    @attr detailViewer.blockSeparator (HTMLString : "<br><br>" : [IRW])
    // A string (HTML acceptable) that will be written to a page to separate blocks.
    // @group appearance
    // @visibility external
    //<
    blockSeparator:"<br><br>",

    // Empty values
    // --------------------------------------------------------------------------------------------

    //>    @attr    detailViewer.showEmptyField        (Boolean : true : IRWA)
    // Whether to show the field when the value is null
    // @group appearance
    // @visibility external
    //<
    showEmptyField:true,

    //> @attr detailViewerField.emptyCellValue (HTMLString : null : IR)
    // The value to display for a cell whose value is null or the empty
    // string after applying formatCellValue and valueMap (if any).
    // <p>
    // This is the field-specific attribute.  You may also set the emptyCellValue at the viewer
    // level to define the emptyCellValue for all empty fields in the viewer.
    //
    // @group appearance
    // @see detailViewer.emptyCellValue
    // @visibility external
    //<

    //>    @attr    detailViewer.emptyCellValue        (HTMLString : "&nbsp;" : IRWA)
    // Text to show for an empty cell
    // @group appearance
    // @visibility external
    //<
    emptyCellValue:"&nbsp;",

    // Labels
    // --------------------------------------------------------------------------------------------
    //>    @attr detailViewer.labelPrefix (HTMLString : "" : IRW)
    // text to put before a label
    // @group labels
    // @visibility external
    //<
    labelPrefix:"",

    //>    @attr detailViewer.labelSuffix (HTMLString : ":" : IRW)
    // text to put after a label
    // @group labels
    // @visibility external
    //<
    labelSuffix:":",

    //> @attr detailViewer.labelAlign (Alignment : null : IRW)
    // Horizontal alignment of value-labels in this viewer.  If unspecified, defaults to
    // <code>"left"</code> when in RTL mode and <code>"right"</code> otherwise.
    // @group values
    // @visibility external
    //<
    //labelAlign: null,

    //> @attr detailViewer.valueAlign (Alignment : null : IRW)
    // Horizontal alignment of values in this viewer.  If unspecified, defaults to
    // <code>"right"</code> when in RTL mode and <code>"left"</code> otherwise.
    // @group values
    // @visibility external
    //<
    //valueAlign: null,

    //> @attr detailViewer.wrapLabel (Boolean : false : IRW)
    // Should the label be allowed to wrap, or be fixed to one line no matter how long
    // @group labels
    // @visibility external
    //<

    //> @attr detailViewer.wrapValues (Boolean : true : IR)
    // Whether values should be allowed to wrap by default, or should be shown on one line
    // regardless of length.
    //
    // @group labels
    // @visibility external
    //<
    wrapValues: true,

    // internal property used by tileGrid to force table size to be width 100%
    useInnerWidth: true,
    // internal property to clip cell values
    clipValues: false,

    // CSS styles
    // --------------------------------------------------------------------------------------------

    //>    @attr detailViewer.styleName (CSSStyleName : "detailViewer" : IRW)
    // CSS style for the component as a whole.
    // @group appearance
    // @visibility external
    //<
    styleName:"detailViewer",

    //>    @attr detailViewer.blockStyle (CSSStyleName : "detailBlock" : IRW)
    // CSS style for each block (one record's worth of data).
    // @group appearance
    // @visibility external
    //<
    blockStyle:"detailBlock",

    //>    @attr    detailViewer.labelStyle        (CSSStyleName : "detailLabel" : IRW)
    //            CSS style for a normal detail label
    // @group appearance
    // @visibility external
    //<
    labelStyle:"detailLabel",

    //>    @attr    detailViewer.cellStyle        (CSSStyleName : "detail" : IRW)
    //            CSS style for a normal value
    // @group appearance
    // @visibility external
    //<
    cellStyle:"detail",

    //>    @attr    detailViewer.headerStyle        (CSSStyleName : "detailHeader" : IRW)
    //            CSS style for a header
    // @group appearance
    // @visibility external
    //<
    headerStyle:"detailHeader",

    //> @attr detailViewer.printCellStyle (CSSStyleName : null : IRW)
    // Optional CSS style for a cell in printable HTML for this component. If unset
    // +link{detailViewer.cellStyle} will be used for printing as well as normal presentation.
    // @group printing
    // @visibility external
    //<

    //> @attr detailViewer.printLabelStyle (CSSStyleName : null : IRW)
    // Optional CSS style for a label cell in printable HTML for this component. If unset
    // +link{detailViewer.labelStyle} will be used for printing as well as normal presentation.
    // @group printing
    // @visibility external
    //<

    //> @attr detailViewer.printHeaderStyle (CSSStyleName : null : IRW)
    // Optional CSS style for a header in printable HTML for this component. If unset
    // +link{detailViewer.headerStyle} will be used for printing as well as normal presentation.
    // @group printing
    // @visibility external
    //<

    //>    @attr    detailViewer.separatorStyle        (CSSStyleName : "detail" : IRW)
    //            CSS style for a separator
    // @group appearance
    // @visibility external
    //<
    separatorStyle:"detail",

    //>    @attr    detailViewer.cellPadding        (number : 3 : [IRW])
    //          The amount of empty space, in pixels, surrounding each detailViewer value in its
    //          cell.
    //<
    cellPadding:3,

    //> @attr detailViewer.dateFormatter (DateDisplayFormat : null : [IRW])
    // How should Date type values be displayed in this DetailViewer by default?
    // <P>
    // This property specifies the default DateDisplayFormat to apply to Date values
    // displayed in this grid for all fields except those of +link{detailViewerField.type,type "time"}
    // (See also +link{detailViewer.timeFormatter}).<br>
    // If +link{detailViewer.datetimeFormatter} is specified, that will be applied by default
    // to fields of type <code>"datetime"</code>.
    // <P>
    // Note that if +link{detailViewerField.dateFormatter} or +link{detailViewerField.timeFormatter} are
    // specified those properties will take precedence over the component level settings.
    // <P>
    // If unset, date values will be formatted according to the system wide
    // +link{Date.setNormalDisplayFormat(),normal display format}.
    //
    // @visibility external
    //<
    //dateFormatter:null,


    //> @attr detailViewer.datetimeFormatter (DateDisplayFormat : null : [IRW])
    // Display format to use for fields specified as type 'datetime'.  Default is to use the
    // system-wide default long ("normal") date time format, configured via
    // +link{Date.setNormalDatetimeDisplayFormat()}.  Specify any
    // valid +link{type:DateDisplayFormat} to change the display format for datetimes used by this
    // viewer.
    // <smartclient>
    // May be specified as a function. If specified as  a function, this function will
    // be executed in the scope of the Date
    // and should return the formatted string.
    // </smartclient>
    // <P>
    // May also be specified at the field level via
    // +link{detailViewerField.dateFormatter}
    //
    // @see listGridField.dateFormatter
    // @group appearance
    // @visibility external
    //<

    //>    @attr detailViewer.timeFormatter (TimeDisplayFormat : null : [IRW])
    // Display format to use for fields specified as type 'time'.  May also be specified at
    // the field level via +link{detailViewerField.timeFormatter}.<br>
    // If unset, time fields will be formatted based on the system wide
    // +link{Time.setNormalDisplayFormat()}
    // @group appearance
    // @visibility external
    //<

    //> @attr detailViewer.linkTextProperty (string : "linkText" : [IRW])
    // Property name on a record that will hold the link text for that record.
    // <p>
    // This property is configurable to avoid possible collision with data values in the
    // record.
    // <p>
    // Use +link{detailViewerField.linkTextProperty} if you have more than one link field and
    // the fields' records do not use the same property to store the linkText.
    // @see attr:detailViewerField.linkText
    // @see attr:detailViewerField.linkTextProperty
    // @visibility external
    //<
    linkTextProperty : "linkText",



    // Empty Message
    // --------------------------------------------------------------------------------------------

    //>    @attr    detailViewer.showEmptyMessage        (Boolean : true : IRWA)
    // Show +link{attr:detailViewer.emptyMessage} when there is no data to display?
    // @see emptyMessage
    // @group emptyMessage
    // @visibility external
    //<
    showEmptyMessage:true,

    //>    @attr detailViewer.emptyMessage (HTMLString : "No items to display." : IRW)
    //          The string to display in the body of a detailViewer with no records.
    // @group emptyMessage
    // @visibility external
    //<
    emptyMessage:"No items to display.",

    //>    @attr    detailViewer.emptyMessageStyle        (CSSStyleName : "normal" : IRWA)
    // CSS style to display this message in
    // @group emptyMessage
    // @visibility external
    //<
    emptyMessageStyle:"normal",

    //> @attr detailViewer.loadingMessage (HTMLString : "&nbsp;${loadingImage}" : IRW)
    // The string to display in the body of a detailViewer which is loading records.
    // Use <code>"&#36;{loadingImage}"</code> to include +link{Canvas.loadingImageSrc,a loading image}.
    // @group emptyMessage
    // @visibility external
    //<
    loadingMessage:"&nbsp;${loadingImage}",

    //>    @attr    detailViewer.loadingMessageStyle        (CSSStyleName : "normal" : IRWA)
    // CSS style to use for the +link{loadingMessage}.
    // @group emptyMessage
    // @visibility external
    //<
    loadingMessageStyle:"normal",

    // ---------------------------------------------------------------------------------------
    // About two values worth of data.   Keeps the DV from taking up the 100px default height
    // without being unexpectedly small when it has no data.
    defaultHeight:35 ,

    showLabel: true
});


// add methods
isc.DetailViewer.addMethods({


//>    @method    detailViewer.initWidget()    (A)
//            initializes the list of fields
//            sets up the data (if specified)
//
//        @param    [all arguments]    (object)    objects with properties to override from default
//<
initWidget : function () {
    // default alignments - non-RTL is labels to the right, values to the left
    if (this.labelAlign == null) this.labelAlign = this.isRTL() ? "left" : "right";
    if (this.valueAlign == null) this.valueAlign = this.isRTL() ? "right" : "left";
    // call the superclass function
    this.Super("initWidget",arguments);

    // set field state if necessary, call setFields() otherwise
    if (this.fieldState != null) this.setFieldState(this.fieldState);
    else this.setFields(this.fields);

    // create context menu for field picker
    if (this.canPickFields) {
        this.contextMenu = isc.Menu.create({
            data: [ this.createFieldPickerWindowMenuItem(this.configureFieldsText) ]
        });
    }
},

//>    @method    detailViewer.setData()  ([])
// Sets the data displayed by this detail viewer.
//
//      @visibility external
//        @param    newData        (object or array)    new data to be displayed
//<
setData : function (newData) {

    // clear old observation
    if (this.data) this.ignore(this.data, "dataChanged");

    this.invalidateUserCache();

    // remember the new data
    this.data = newData;

    if (this.data && this.data.dataChanged) {
        this.observe(this.data, "dataChanged", "observer.dataChanged()");
    }

    // and mark the viewer as dirty so it'll be redrawn
    this.markForRedraw("new data");
},

dataChanged : function () {
    // call setFields() on dataChanged - causes showIf to be re-evaluated
    this.setFields(this.completeFields);
    this.invalidateUserCache();
    this.applyHilites();
    this.markForRedraw();
},

//>    @method    detailViewer.getData()    (A)
//            return the data to be displayed
//
//        @return    (Object)    data for this widget - either Object or Array
//<
getData : function () { return this.data; },


//>    @method    detailViewer.getFields()    (A)
//            return the list of fields to display
//
//        @return    (List of DetailViewerField)    array of objects to display
//<
getFields : function () { return this.fields; },


//>    @method    detailViewer.getInnerHTML()    (A)
//            return the HTML for this widget
//        @return    (string)    HTML to display
//<
getInnerHTML : function () {
    // get the data to display
    var valueList = this.getData();

    //>DEBUG
    if (this.fields == null || this.fields.length == 0) {
        return "Note: you must define detailViewer.fields to specify what to display!";
    }
    //<DEBUG

    // If the data is a ResultSet, poke the ResultSet to fetch data and return the loading
    // message.  Note that if fetchData() is called, this isn't the codepath that causes the
    // initial fetch - see DataBoundComponent.requestVisibleRows.
    if (isc.ResultSet != null && isc.isA.ResultSet(valueList) && !valueList.lengthIsKnown()) {
        // request only the first row.  If this ResultSet is using fetchMode:"paged" (not the
        // default for DetailViewer) and has already issued a request for data, asking for
        // anything beyond the current rs.resultSize will initiate additional fetches, possibly
        // for rows that don't exist, but the ResultSet doesn't know that while
        // !lengthIsKnown().
        valueList.get(0);
        return this.loadingMessageHTML();
    }

    if (valueList == null || (valueList.getLength && valueList.getLength() == 0)) {
        return this.emptyMessageHTML();
    }

    // normalize the data into an array
    if (!isc.isA.List(valueList)) valueList = [valueList];

    // With DV, we have a situation where a failed load results in lengthIsKnown being true,
    // but the data actually consists of a single loading marker.  The upshot of this is that
    // the DetailViewer displays an empty record, which is probably fair enough (since the
    // load probably failed because the record doesn't exist).  However, if the load failed
    // because we're offline, we want to show that specifically, so treat it as a special case
    if (Array.isLoading(valueList.get(0)) && this.isOffline())  {
        return this.emptyMessageHTML();
    }

    // if there's only one item or we're supposed to show all columns together
    if (valueList.getLength() == 1 || this.recordsPerBlock == "*") {
        // call the blockHTML routine with all items
        return this.getBlockHTML(valueList);
    } else {
        // otherwise call it for each item separately
        var output = isc.StringBuffer.newInstance();
        for (var startRow = 0; startRow < valueList.getLength(); startRow += this.recordsPerBlock) {
            output.append(this.getBlockHTML(valueList.getRange(startRow, startRow + this.recordsPerBlock)), this.blockSeparator);
        }
        return output.toString();
    }
},

//>    @method    detailViewer.getBlockHTML()    (A)
// Return the HTML for either a single object or a set of objects where each object gets one
// column.
//        @return    (string)    HTML to display
//<
getBlockHTML : function (valueList) {

    if (valueList.getLength == null) {
        var newArray = [];
        for (var i = 0; i < valueList.length; i++) {
            newArray[i] = valueList[i];
        }
        valueList = newArray;
    }

    // how many separate value objects are we dealing with ?
    var numValues = valueList.getLength();
    // start the table to display the output
    var output = "<TABLE BORDER=0 CELLSPACING=0 CLASS='" + this.blockStyle +
            "' CELLPADDING='" + this.cellPadding +
            "' style='width:" + (this.useInnerWidth && !this.isPrinting
                                 ? this.getInnerWidth() + "px"
                                 : "100%") +
            (this.clipValues ? ";table-layout:fixed" : "") +
            "'>";


    // output the data

    // get the list of fields to output
    var fields = this.fields;

    // iterate through each of the keys in detailFields and output the info for each field
    for (var fieldNum = 0, fieldLength = fields.length; fieldNum < fieldLength; fieldNum++) {
        var field = fields[fieldNum];
        if (!field || field.hidden || field.visible == false) continue;

        // if the field has a showIf property
        if (field.showIf) {
            // CALLBACK API:  available variables:  "viewer,valueList"
            // Convert a string callback to a function

            if (!isc.isA.Function(field.showIf)) {
                isc.Func.replaceWithMethod(field, "showIf", "viewer,valueList");
            }

            // skip this if the showIf returns false
            if (field.showIf(this, valueList) == false) continue;
        }

        // MAE: if we don't want to show fields that have empty/null values
        // check the appropriate values and skip if they are all empty/null
        // This does not apply to headers and separators
        var type = field.type ? field.type : "";
        if (type != "separator" && type != "header" && !this.showEmptyField) {
            var valuesAreEmpty = true;
            for (var i = 0; i < valueList.getLength(); i++) {
                var value = valueList.get(i)[field[this.fieldIdProperty]]
                if (!(value == null || value == "")) {
                    valuesAreEmpty = false;
                    break;
                }
            }
            // if no values were found, continue to the next field
            if (valuesAreEmpty) continue;
        }

        // if there is a specific output function for this field, call that
        if (field.output) {
            // CALLBACK API:  available variables:  "fieldNum,field,valueList"
            // Convert a string callback to a function
            if (!isc.isA.Function(field.output)) {
                isc.Func.replaceWithMethod(field, "output", "fieldNum,field,valueList");
            }
            output += field.output(fieldNum, field, valueList);
        } else {
            // output this particular field
            output += this.outputItem(fieldNum, field, valueList);
        }
    }
    // end the table
    output += "</TABLE>";
    // and return the output
    return output;
},

//>    @method    detailViewer.fieldIsVisible()
// Check whether a field is currently visible
//
// @param    field  (String | DetailViewerField)    field to be checked
// @return (boolean) true if the field is currently visible, false otherwise.
// @visibility external
//<
fieldIsVisible : function (field) {
    var fieldObj = field;
    // If passed a field ID, look for it in the completeFields array rather than the fieldsArray
    // as it is may be defined, but not visible
    if (!isc.isAn.Object(fieldObj)) fieldObj = this.getSpecifiedField(field);

    return this.fields.contains(fieldObj);
},

//>    @method    detailViewer.outputItem()    (A)
// Output one row of the data as HTML
//        @param    fieldNum        (number)        number of the field to output
//        @param    field        (object)        pointer to the field to output
//        @param    valueList    (array)            list of values to output
//
//        @return    (string)    HTML output
//<
outputItem : function (fieldNum, field, valueList) {

    var type = (field.type ? field.type : "value"),
        // functionName == name of a function to call to ouput this particular type of object
        functionName = "output_"+type,
        output = ""
    ;
    // if a function by that name cannot be found, default to output_value
    if (!this[functionName]) functionName = "output_value";
    // start the table row
    output += "<TR"
            + (this.rowClass != null ? " CLASS='" + this.rowClass + "'" : "")
            + ">";

    // output
    output += this[functionName](fieldNum, field, valueList);

    // end the row
    output += "</TR>\r";

    // return the output
    return output;
},


//
//    OUTPUT HTML FOR DIFFERENT TYPES OF OBJECTS
//


//>    @method    detailViewer.output_value()    (A)
//            output a normal value for each field in valueList
//        @param    fieldNum        (number)        number of the field to output
//        @param    field        (object)        pointer to the field to output
//        @param    valueList    (array)            list of values to output
//
//        @return    (string)    HTML output
//<
output_blob : function (fieldNum, field, valueList) {
    return this.output_binary(fieldNum, field, valueList);
},
output_upload : function (fieldNum, field, valueList) {
    return this.output_binary(fieldNum, field, valueList);
},
output_binary : function (fieldNum, field, valueList) {
    // output the label
    var output = "<TD WIDTH=10% CLASS='" +
            (this.isPrinting ? this.printLabelStyle || this.labelStyle : this.labelStyle)
            + "' style='text-align:" + this.labelAlign + ";'"
            + (this.wrapLabel ? ">" : " NOWRAP><NOBR>")
            + this.labelPrefix + (field.title ? field.title : field[this.fieldIdProperty]) + this.labelSuffix
            + (this.wrapLabel ? "" : "<\/NOBR>")
            + "<\/TD>"
    ;

    // iterate for each object in valueList, outputing the object[field[this.fieldIdProperty]]
    for (var i = 0; i < valueList.getLength(); i++) {

        var record = valueList.get(i),
            index = this.getData().indexOf(record),
            filenameField = this.dataSource ?
                                isc.DS.getDataSource(this.dataSource).getFilenameField(field.name) :
                                field.name + "_filename",
            name = record[filenameField],
            viewAction = "'" + this.getID() +".view",
            dlAction = "'" + this.getID() +".download",
            completion = "";
            if (field && field.name) {
                completion = "Cell(" + index + ", \"" + field.name + "\")'";
            } else {
                completion = "Row(" + index + ")'";
            }
            var viewIconHTML = isc.Canvas.imgHTML("[SKIN]actions/view.png", 16, 16, null,
                            "style='cursor:"+isc.Canvas.HAND+"' onclick="+viewAction+completion);
            var viewIconHTML = isc.Canvas.imgHTML("[SKIN]actions/view.png", 16, 16, null,
                        "style='cursor:"+isc.Canvas.HAND+"' onclick="+viewAction+completion),
            downloadIconHTML = isc.Canvas.imgHTML("[SKIN]actions/download.png", 16, 16, null,
                        "style='cursor:"+isc.Canvas.HAND+"' onclick="+dlAction+completion),
            value = viewIconHTML + "&nbsp;" + downloadIconHTML +
                (name ? "&nbsp;" + name : "");

        // output the value as a cell
        output += "<TD CLASS='" + this.getCellStyle(value, field, record, this) + "'>"
                  + value + "<\/TD>";
    }
    return output;

},

viewRow : function (index) {
    isc.DS.get(this.dataSource).viewFile(this.getData().get(index));
},

downloadRow : function (index) {
    isc.DS.get(this.dataSource).downloadFile(this.getData().get(index));
},

viewCell : function (index, fieldName) {
    isc.DS.get(this.dataSource).viewFile(this.getData().get(index), fieldName);
},

downloadCell : function (index, fieldName) {
    isc.DS.get(this.dataSource).downloadFile(this.getData().get(index), fieldName);
},

// given an Array of records (valueList) output one complete DetailViewer row, which will have
// multiple data columns if recordsPerBlock > 1
output_value : function (fieldNum, field, valueList) {

    // output the label
    var output;
    if (this.showLabel) {
        output = "<TD WIDTH="+(this.labelWidth != null ? this.labelWidth : "10%")+" CLASS='" +
                (this.isPrinting ? this.printLabelStyle || this.labelStyle : this.labelStyle)
                + "' style='text-align:" + this.labelAlign + ";'"
            + (this.wrapLabel ? ">" : " NOWRAP><NOBR>")
            + this.labelPrefix + (field.title ? field.title : field[this.fieldIdProperty]) + this.labelSuffix
            + (this.wrapLabel ? "" : "<\/NOBR>")
            + "<\/TD>"
        ;
    } else {
        output = "";
    }
    // resolve field level valueMap reference strings to objects before going into the for loop
    if (field.valueMap && isc.isA.String(field.valueMap))
        field.valueMap = this.getGlobalReference(field.valueMap);

    // iterate for each object in valueList, outputing the object[field.name]
    for (var i = 0; i < valueList.getLength(); i++) {
        var record = valueList.get(i),
            onclick = "",
            formattedValue;
        if (field.type == "image") {
            // if any of field.imageWidth/Height/Size are set as strings, assume they are property
            // names on the record
            var dimensions = isc.Canvas.getFieldImageDimensions(field, record);

            var src = this.getCellValue(record, field), prefix =
                field.imageURLPrefix || field.baseURL || field.imgDir;

            if (src == this._resolveEmptyDisplayValue(field)) {
                formattedValue = this._resolveEmptyDisplayValue(field);
            } else {
                formattedValue = this.imgHTML(src, dimensions.width, dimensions.height, null,
                    field.extraStuff, prefix, field.activeAreaHTML);
            }
        } else if (field.type == "link") {
            // The value of the field is the URL for a link.  The URL will also be used for
            // the text of the link, unless it is overridden by a linkText property on the
            // field or on the record.
            var target = field.target || "_blank",
                linkTextProperty = field.linkTextProperty || this.linkTextProperty,
                value = this.getCellValue(record, field),
                linkText = record[linkTextProperty] || field.linkText || value,
                href = value;

            if (href == this._resolveEmptyDisplayValue(field)) {
                formattedValue =  this._resolveEmptyDisplayValue(field);
            } else {
                // Each field may add a prefix and/or suffix onto the URL.
                if (field.linkURLPrefix) {
                    href = field.linkURLPrefix + href;
                }
                if (field.linkURLSuffix) {
                   href = href + field.linkURLSuffix;
                }
                if (target == "javascript") {
                    // target is "javascript" - make the link inert and have the field's click
                    // event fired instead
                    href = "javascript:void";
                    onclick = " onclick=\"if(window." + this.ID + ") return " +
                        this.ID + "._linkClicked(event,'" + field.name + "');\"";
                }

                formattedValue = this.linkHTML(href, linkText, target);
            }

        } else {
            // NOTE: calls formatCellValue()
            formattedValue = this.getCellValue(record, field);
        }

        // determine the cell style
        var rawValue = this.getRawValue(record,field);
        var cellStyle;
        if (field.getCellStyle) {
            cellStyle = field.getCellStyle(rawValue, field, record, this);
        } else {
            cellStyle = (this.getCellStyle(rawValue, field, record, this) || this.cellStyle);
        }

        // put together a STYLE attribute
        var styleStr = " style='";
        if (this.clipValues) styleStr += "overflow:hidden;";
        styleStr += "text-align:" + this.valueAlign;

        // allow custom CSS text per field
        if (this.getCellCSSText) {
            var cssText = this.getCellCSSText(rawValue, field, record, this);
            if (cssText != null) styleStr += isc.semi + cssText;
        }

        // output the value as a cell
        styleStr += "'";
        output += "<TD CLASS='" + cellStyle + "'" + styleStr +
            (field.height?"HEIGHT='"+field.height+"' ":"") +
            onclick +
            (this.wrapValues ? ">" : " NOWRAP><NOBR>") +
            formattedValue +
            (this.wrapValues ? "" : "<\/NOBR>") +
            "<\/TD>";
    }
    return output;
},

_linkClicked : function (event, fieldName) {
    var field = this.fields.find("name", fieldName) || {},
        mustCancel = false
    ;
    if (event.target == "javascript" || field.target == "javascript") {
        mustCancel=true;
        if (field.click) field.click();
    }

    if (mustCancel) {

        if (!isc.Browser.isIE) {
            event.preventDefault();
        }

        return false;
    }
    return true;
},


//>    @method    detailViewer.getCellCSSText()
// Return CSS text for styling this cell, which will be applied in addition to the CSS class
// for the cell, as overrides.
// <p>
// "CSS text" means semicolon-separated style settings, suitable for inclusion in a CSS
// stylesheet or in a STYLE attribute of an HTML element.
//
// @param value (any) actual value of this cell
// @param field (DetailViewerField)    field object for this cell
// @param record (Record) record object for this cell
// @param viewer (DetailViewer) the viewer instance to which this cell belongs
// @return (CSSText) CSS text to add to this cell
//
// @group appearance
// @visibility external
//<
getCellCSSText : function (value, field, record, viewer) {

    return this.getRecordHiliteCSSText(record, "", field, true);
},

//>    @method    detailViewer.getCellStyle()
// Return the CSS class for a cell. Default implementation calls
// +link{detailViewerField.getCellStyle(), getCellStyle()} on the field if defined, otherwise
// returns +link{detailViewer.cellStyle,this.cellStyle}
// @param value (any) actual value of this cell
// @param field (DetailViewerField)    field object for this cell
// @param record (Record) record object for this cell
// @param viewer (DetailViewer) the viewer instance to which this cell belongs
// @return (CSSStyleName) CSS style for this cell
// @group appearance
// @visibility external
//<
// <P>
// Note: if +link{detailViewer.printCellStyle} is specified this will be used as the default
// styling for cells instead of <code>this.cellStyle</code> when generating printable HTML for
// this component.
getCellStyle : function (value, field, record, viewer) {
    if (field) {
        if (field.getCellStyle) return field.getCellStyle(value,field,record,viewer);
        if (this.isPrinting && field.printCellStyle) {
            return field.printCellStyle;
        }
        if (field.cellStyle) {
            return field.cellStyle;
        }
    }
    return (this.isPrinting && this.printCellStyle != null) ? this.printCellStyle
                                                              : this.cellStyle;
},

//> @method DetailViewer.formatCellValue()
// Optional method to format the value to display for cells in this DetailViewer.
// Note that if +link{detailViewerField.formatCellValue()} is specified this method will not
// be called for cells within that field.
// @param value (string) the raw value of the cell (may be formatted by
//   +link{detailViewerField.formatCellValue()}
// @param record (detailViewerRecord) the record being displayed
// @param field (detailViewerField) the field being displayed
// @visibility external
//<

getSelectedRecord : function() {
    return this.data.get(0);
},

// getCellValue - method to actually get the value for a record.
// Called from 'output_value', which handles writing the <TD> tags around the value and
// outputting a whole block of records.
// Can be overridden by a user.
// Also if 'getCellValue()' is specified at a field level, will apply it to this cell.

getCellValue : function (record, field) {

    // get the value of this key for that field
    var value = this.getRawValue(record,field);
    if (isc.isA.String(field.formatCellValue)) {
        field.formatCellValue = isc.Func.expressionToFunction("value,record,field,viewer",
                                                              field.formatCellValue);
    }
    if (field.getCellValue != null) {
        if (isc.isA.String(field.getCellValue)) {
            field.getCellValue = isc.Func.expressionToFunction("value,record,field,viewer",
                                                                field.getCellValue);
        }
        // note the 'value' passed into field.getCellValue() is the raw value, not valueMapped, etc.
        // This matches the ListGrid's field-level getCellValue behavior.
        value = field.getCellValue(value, record, field, this);
        if (field.formatCellValue) value = field.formatCellValue(value, record, field, this);
    } else {

        // if the col has an 'valueMap' parameter, treat the value as a key in the valueMap
        if (field.valueMap != null) value = isc.getValueForKey(value, field.valueMap);

        if (field.formatCellValue) value = field.formatCellValue(value, record, field, this);
    }

    // Support formatCellValue if specified.
    // - to match ListGrid behavior, don't run both field.formatCellValue and DV.formatCellValue()
    if (field.formatCellValue == null && this.formatCellValue) {
        value = this.formatCellValue(value, record, field);
    } else {
        // if no value was specified, output the emptyCellValue
        if (value == null || isc.is.emptyString(value)) {
            value = this._resolveEmptyDisplayValue(field);
        } else {
            // _formatDataType ensures that non string values get formatted as strings as appropriately
            value = this._formatDataType(record, field, value);

            // field.escapeHTML is a boolean - if true, escape HTML chars in the value such as "<".
            var escapeHTML = field.escapeHTML;
            // asHTML was old name
            if (escapeHTML == null) escapeHTML = field.asHTML;
            if (escapeHTML) value = value.asHTML();
        }
    }

    // handle formula and summary fields
    if (field) {
        if (field.userFormula) value = this.getFormulaFieldValue(field, record);
        else if (field.userSummary) value = this.getSummaryFieldValue(field, record);
        else if (field.type=="imageFile") {
            var filenameField = this.dataSource ?
                        isc.DS.getDataSource(this.dataSource).getFilenameField(field.name) :
                        field.name + "_filename";
            if (record[filenameField] != null) {
                if (field.showFileInline != false) {
                    if (!record[field[this.fieldIdProperty] + "_imgURL"]) {
                        var dimensions = isc.Canvas.getFieldImageDimensions(field, record),
                            image = this.getDataSource().getFileURL(record,
                                                                field[this.fieldIdProperty]);
                        value = record[field[this.fieldIdProperty] + "_imgURL"] =
                            this.imgHTML(image, dimensions.width, dimensions.height,
                                         null, null, isc.Canvas._$allowRelativeSrc);
                    } else
                        value = record[field[this.fieldIdProperty] + "_imgURL"];
                } else {
                    value = this.getViewDownloadHTML(field, record);
                }
            }
        } else if (field.showFileInline == true) { // non-imageFile field
            this.logWarn("getCellValue(): Unsupported field-type for showFileInline: "+field.type);
        }
        if (isc.isA.Number(value)) {
            if (isc.SimpleType.inheritsFrom(field.type, "float") &&
                (field.decimalPrecision != null || field.decimalPad != null))
            {
                value = isc.Canvas.getFloatValueAsString(value, field.decimalPrecision,
                                                         field.decimalPad);
            } else if (field.precision != null) {
                value = isc.Canvas.getNumberValueAsString(value, field.precision, field.type);
            }
        }
    }

    // apply hilites to capture htmlBefore/after
    var hilites = this.getFieldHilites(record, field);
    if (hilites != null) {
        value = this.applyHiliteHTML(hilites, value);
        // Finally, apply the hiliteIcon if present
        value = this.applyHiliteIcon(hilites, field, value);
    }

    return value;
},

getViewDownloadHTML : function (field, record) {

    if (record == null) return null;

    var filenameField = this.dataSource ?
                isc.DS.getDataSource(this.dataSource).getFilenameField(field.name) :
                field.name + "_filename",
        name = record[filenameField];


    if (!field.filenameSuppressed && (name == null || isc.isA.emptyString(name))) {
        return "&nbsp;";
    }

    var viewIconHTML = isc.Canvas.imgHTML("[SKIN]actions/view.png", 16, 16, null,
            "style='cursor:"+isc.Canvas.HAND+
            "' onclick='"+this.getID()+".viewFile("+record+","+field+")'");
    var downloadIconHTML = isc.Canvas.imgHTML("[SKIN]actions/download.png", 16, 16, null,
            "style='cursor:"+isc.Canvas.HAND+
            "' onclick='alert('running');"+this.getID()+".downloadFile("+record+","+field+")'");

    return "<nobr>" + viewIconHTML + "&nbsp;" + downloadIconHTML + "&nbsp;" + name + "</nobr>";
},

viewFile : function (record, field) {
    isc.DS.get(this.dataSource).viewFile(record, field.name);
},

downloadFile : function (record, field) {
    isc.DS.get(this.dataSource).downloadFile(record, field.name);
},

// _formatDataType, format a cell value based on data type.
// At this point we've checked for field.formatValue etc. Current implementation:
// - if a formatter is defined on the SimpleType make use of it
// - otherwise format dates according to standard DBC rules:
//  - use field.dateFormatter or field.timeFormatter if specified
//  - otherwise use component.dataFormatter, component.timeFormatter, component.datetimeFormatter
//    depending on field type.
_$date:"date",
_formatDataType : function (record, field, value, isMultipleElement) {

    if (!isMultipleElement && field && field.multiple && isc.isA.Array(value)) {
        var values = [];
        for (var i = 0; i < value.length; i++) {
            values[i] = this._formatDataType(record, field, value[i], true);
        }
        return values.join(field.multipleValueSeparator || ", ");
    }

    // Apply declarative FormatString if present
    if (field && (isc.isA.Number(value) || isc.isA.Date(value)) && field.format) {
        return isc.isA.Number(value) ? isc.NumberUtil.format(value, field.format)
                                     : isc.DateUtil.format(value, field.format);
    }

    // Format date values, according to standard DBC rules
    // Respect field level dateFormatter/timeFormatter if specified
    // Otherwise respect component level dateFormatter/datetimeFormatter/timeFormatter depending
    // on specified field-type.
    if (isc.isA.Date(value)) {
        if (this._formatAsTime(field)) {
            var isLogicalTime = isc.SimpleType.inheritsFrom(field.type, "time");
            value = isc.Time.toTime(value, this._getTimeFormatter(field), isLogicalTime);
        } else {
            var formatter = this._getDateFormatter(field);
            // If the field is a logical date field
            //  use short formatter, since the "time" part of
            // the date has no real meaning (and no long formatters suppress time).
            // Otherwise use the long format by default for DetailViewers.
            if (isc.SimpleType.inheritsFrom(field.type, "date") &&
                !isc.SimpleType.inheritsFrom(field.type, "datetime"))
            {

                value = value.toShortDate(formatter, false);
            } else {
                value = value.toNormalDatetime(formatter);
            }
        }
    }

    // if the field has a valueMap, get the display value from it
    if (field.valueMap && field.valueMap[""+value+""] != null) {
        value = field.valueMap[""+value+""];
    }

    // If normalDisplayFormatter is specified on the simpleType make use of it
    if (field._normalDisplayFormatter != null) {
        value = field._simpleType.normalDisplayFormatter(value, field, this, record);
    }

    return isc.iscToLocaleString(value);
},

_formatAsTime : function (field) {
    if (field == null) return false;
    if (field.dateFormatter == null && field.timeFormatter != null) return true;
    if (field.timeFormatter == null && field.dateFormatter != null) return false;
    return isc.SimpleType.inheritsFrom(field.type, "time");
},

_getDateFormatter : function (field) {
    if (field.dateFormatter) return field.dateFormatter;
    if (field.displayFormat != null && isc.SimpleType.inheritsFrom(field.type, "date")) {
        return field.displayFormat;
    }
    if (this.datetimeFormatter != null && isc.SimpleType.inheritsFrom(field.type, "datetime")) {
        return this.datetimeFormatter;
    }
    return this.dateFormatter;
},

_getTimeFormatter : function (field) {
    if (field.timeFormatter) return field.timeFormatter;
    if (field.displayFormat != null && isc.SimpleType.inheritsFrom(field.type, "time")) {
        return field.displayFormat;
    }
    return this.timeFormatter;
},

//> @method detailViewer.getRecordIndex()
// @param record (Record) the record whose index is to be retrieved
// @return index (Number) index of the record, or -1 if not found
// @include dataBoundComponent.getRecordIndex
// @visibility external
//<
getRecordIndex : function (record) {
    var result = this.Super('getRecordIndex', arguments);
    if (result==-1) result = 0;
    return result;
},


//>    @method    detailViewer.output_header()    (A)
//            output a header field
//        @param    fieldNum        (number)        number of the field to output
//        @param    field        (object)        pointer to the field to output
//        @param    valueList    (array)            list of values to output
//
//        @return    (string)    HTML output
//<
output_header : function (fieldNum, field, valueList) {
    return "<TD COLSPAN=" + (valueList.getLength() + 1) + " CLASS='" +
    (this.isPrinting && this.printHeaderStyle ? this.printHeaderStyle : this.headerStyle) +
    "'>"+field.value+"</TD>";
},


//>    @method    detailViewer.output_separator()    (A)
//            output a separator field
//        @param    fieldNum        (number)        number of the field to output
//        @param    field        (object)        pointer to the field to output
//        @param    valueList    (array)            list of values to output
//
//        @return    (string)    HTML output
//<
output_separator : function (fieldNum, field, valueList) {
    var width = (field.width == null ? field.defaultSeparatorWidth : field.width),
        height = (field.height == null ? field.defaultSeparatorHeight : field.height)
    ;
    return "<TD COLSPAN=" + (valueList.getLength() + 1) + " CLASS='" + this.separatorStyle + "'>"
            + isc.Canvas.spacerHTML(width, height)
            + "</TD>";
},


//>    @method    detailViewer.getEmptyMessage()    (A)
//            return the empty message to display when no data was specified
//            this is a function so you can override it in complex cases
//            in simple cases, just returns this.emptyMessage
//
//        @return    (string)    HTML output
//<
getEmptyMessage : function () {
    return this.emptyMessage;
},
getLoadingMessage : function () {
    return this.loadingMessage == null ? "&nbsp;" : this.loadingMessage.evalDynamicString(this, {
        loadingImage: this.imgHTML(isc.Canvas.loadingImageSrc,
                                   isc.Canvas.loadingImageSize,
                                   isc.Canvas.loadingImageSize)
        });
},


//>    @method    detailViewer.emptyMessageHTML()    (A)
// Return the message to show if the component has no data. Default implementation returns a
// centered +link{detailViewer.emptyMessage} or "&amp;nbsp;" if showEmptyMessage is false.  If
// the component has no data because the browser is offline, we instead display the
// +link{dataBoundComponent.offlineMessage} or "&amp;nbsp;" if showOfflineMessage is false
// @return    (string)    HTML output
// @visibility external
//<
emptyMessageHTML : function () {

    if (this.isOffline()) {
        if (!this.showOfflineMessage) return "&nbsp;";
    } else {
        if (!this.showEmptyMessage) return "&nbsp;";
    }

    return "<TABLE WIDTH=100%>"
            + "<TR><TD CLASS='" + this.emptyMessageStyle + "' ALIGN=CENTER><BR><BR>"
            + (this.isOffline() ? this.offlineMessage : this.getEmptyMessage())
            + "<\/TD><\/TR><\/TABLE>";

},

//>    @method    detailViewer.loadingMessageHTML()    (A)
//            return the message to display while the data is loading
//
//        @return    (string)    HTML output
//<
loadingMessageHTML : function () {
    return "<TABLE WIDTH=100%>"
            + "<TR><TD CLASS='" + this.loadingMessageStyle + "' ALIGN=CENTER><BR><BR>"
            + this.getLoadingMessage()
            + "<\/TD><\/TR><\/TABLE>";
},

setFieldState : function (fieldState) {
    if (fieldState == null && this.fieldState != null) {
        if (isc.isA.String(this.fieldState)) {
            fieldState = this.evalViewState(this.fieldState, "fieldState")
        }
    } else fieldState = this.evalViewState(fieldState, "fieldState");

    this.completeFields = this._setFieldState(fieldState, true);
    this.setFields(this.completeFields);
    this.markForRedraw();
    this.fieldStateChanged();
},

// minimal implementation of setFields()
setFields : function (newFields) {
    if (this.completeFields == null || this.fields == null) this.fields = [];

    // bind the passed-in fields to the DataSource and store
    this.completeFields = this.bindToDataSource(newFields);

    if (this.completeFields == null) this.completeFields = [];

    this.deriveVisibleFields();
    this.invalidateUserCache();
},

// determine which fields should be shown, and add them to the visible fields array.
// (Used as an internal helper - developers should call 'refreshFields' instead)
deriveVisibleFields : function () {
    // NOTE: we use setArray() so that this.fields remains the same array instance.
    this.fields.setArray(this.getVisibleFields(this.completeFields));
},

getVisibleFields : function (fields) {
    var valueList = this.getData(),
        returnFields = fields.duplicate();
    for (var i=0; i<fields.length; i++) {
        var field = fields.get(i);
        if (!this.fieldShouldBeVisible(field, valueList) ||
            field.visible==false) returnFields.remove(field);
    }
    return returnFields;
},

fieldShouldBeVisible : function (field, valueList) {
    // evaluate a showIf expression if present
    if (field.showIf != null) {
        // CALLBACK API:  available variables:  "viewer,valuesList"
        // Convert a string callback to a function

        if (field.showIf == this._$false || field.showIf == this._$falseSemi) return false;
        isc.Func.replaceWithMethod(field, "showIf", "viewer,valueList");
        if (!field.showIf(this, valueList)) return false;
    }
    return true;
},

// Formula/summary -related overrides from DBC
getTitleFieldValue : function (record) {
    var titleField = this.getDataSource().getTitleField(),
        title = this.getCellValue(record, this.getDataSource().getField(titleField));

    return title;
},

// DBC level override to call local getCellValue implementation - Formula/Summary builders
getStandaloneFieldValue : function (record, fieldName) {
    var field = this.getField(fieldName),
        value = this.getCellValue(record, field);
    // In DetailViewer we apply hilite CSS to the cell.
    // Therefore write a span with the appropriate cssText around the actual value
    // in order to pick up the hilite styling
    value = this.addHiliteSpan(record, field, value);
    return value;
},

// basic show and hide methods
hideField : function (fieldName) {
    this.toggleField(fieldName, false);
},
showField : function (fieldName) {
    this.toggleField(fieldName, true);
},
toggleField : function (fieldName, showNow) {
    var field = this.getField(fieldName);

    field.showIf = showNow ? "true" : "false";
    field.visible = showNow;
    this.setFields(this.getAllFields());
    this.markForRedraw();
    this.fieldStateChanged();
},

//>    @method    detailViewer.getField()    (A)
// Return a field by fieldName
//
//        @return    (DetailViewerField) requested field or null
//<
getField : function (fieldName) {
    var allFields = this.getAllFields(),
        fields = this.fields,
        field;

    if (isc.isAn.Object(fieldName) && fieldName[this.fieldIdProperty] != null) {
        // passed a field-object, check it's real by finding it by name
        field = allFields.find(this.fieldIdProperty, fieldName[this.fieldIdProperty]) ||
            fields.find(this.fieldIdProperty, fieldName[this.fieldIdProperty]);
    } else if (isc.isA.Number(fieldName)) {
        field = allFields[fieldName] || fields[fieldName];
    } else {
        field = allFields.find(this.fieldIdProperty, fieldName) ||
            fields.find(this.fieldIdProperty, fieldName)
    }

    return field;
},

// Overridden from DBC - use 'getCellValue' to return the "formatted" value
getFormattedValue : function (record, fieldName, value) {
    return this.getCellValue(record, this.getSpecifiedField(fieldName));
},
//> @method detailViewer.getPivotedExportData()
// Export visual description of DetailViewer data into a form suitable for external
// processing.
// @param settings (Object) contains configuration settings for the export, including:<br/>
//        includeHiddenFields (Boolean) - controls if hidden fields should be exported<br/>
//        allowedProperties (Array) optional array of CSS property names (camelCaps format)
//             constraining the allowed properties to be returned
// @return value (String) exported data
//<
// * Data is exported as an array of objects, with one object per visual row of the
//   DetailViewer grid - meaning one row per field.
// * The title of each visible field is mapped to the property "title" for each object.
// * Each record's value for the corresponding field is mapped to the properties
//   "value1", "value2", ..., up to the number of records specified in this.recordsPerBlock.
//   Records extending beyond this.recordsPerBlock are not exported.
// * Additionally, if CSS hilighting styles are present on a record's field, the CSS text is
//   converted into an object mapping CSS properties in camelCaps format to CSS values, and the
//   object is stored in <property name>$style.
// * Null record values are converted to empty strings.
//
// Example object:
//  [
//  { title: "Foo Fighter", value1: "1", "value1$style": { backgroundColor: "#f00000" } },
//  { title: "bar", value1: "baz" },
//  { title: "xyzzy", value1: "" },
//  { title: "Summary Field", value1: "1 --- baz", "value1$style": { font-weight: "bold" } }
//  ]
getPivotedExportData : function (settings) {
    var exportOutput = [],
        fields = this.getAllFields(),
        data = this.data,
        includeHiddenFields,
        allowedProperties,
        alwaysExportExpandedStyles
        ;

    if (isc.isA.Object(settings)) {
        includeHiddenFields = settings.includeHiddenFields;
        allowedProperties = settings.allowedProperties;
        alwaysExportExpandedStyles = settings.alwaysExportExpandedStyles;
    }
    if (isc.isA.ResultSet(data)) data = data.getAllLoadedRows();
    if (!isc.isA.Array(data)) data = [data];

    for (var fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
        var field = fields[fieldIndex],
            exportObject = {},
            recordsPerBlock = this.recordsPerBlock;

        exportObject.title = field.title || field.name;
        if (isc.isA.String(exportObject.title)) exportObject.title = this.htmlUnescapeExportFieldTitle(exportObject.title);

        // Implement default value of 1 and "*" -> unbounded
        if (recordsPerBlock == null) recordsPerBlock = 1;
        if (recordsPerBlock == "*") recordsPerBlock = 100000;

        if ((!this.fields.contains(field)) && !includeHiddenFields) continue;

        for (var rowIndex = 0;
             rowIndex < recordsPerBlock && rowIndex < data.getLength();
             rowIndex++)
        {
            var record = data[rowIndex],
                fieldNum = this.getFieldNum(field.name),
                exportProp = "value" + (rowIndex+1),
                styleProp = exportProp + "$style";

            var value = this.getExportFieldValue(record, field.name, fieldNum);

            if (!(value == null || value == "&nbsp;")) exportObject[exportProp] = value;

            this.addDetailedExportFieldValue(exportObject, styleProp, record, field, fieldNum,
                                             allowedProperties, alwaysExportExpandedStyles);
            if (exportObject[styleProp] == null || exportObject[styleProp] == "&nbsp;")
                delete exportObject[styleProp];
        }
        exportOutput.push(exportObject);
    }
    return exportOutput;
},


//> @type DetailViewerViewState
// An object containing the stored grouping information for a detailViewer.
// Note that this object is not intended to be interrogated directly, but may be stored
// (for example) as a blob on the server for state persistence across sessions.
//
// @group viewState
// @visibility external
//<

//>    @method    detailViewer.getViewState()
// Returns a snapshot of the current view state of this DetailViewer.<br>
// This includes the field, sort and hilite state of the grid, returned as a
// +link{type:DetailViewerViewState} object.<br>
// This object can be passed to +link{detailViewer.setViewState()} to reset this detail viewer's vew state
// to the current state (assuming the same data / fields are present in the detail viewer).<br>
// @group viewState
// @see type:DetailViewerViewState
// @see detailViewer.setViewState();
// @visibility external
// @return (DetailViewerViewState) current view state for the detail viewer.
//<
getViewState : function (returnObject) {

    var state = {
        field:this.getFieldState(),
        sort:this.getSortState(),
        hilite:this.getHiliteState()
    };

    // Available so TG can call Super() and get an object back
    if (returnObject) return state;
    return "(" + isc.Comm.serialize(state,false) + ")";
},

//>    @method    detailViewer.setViewState()
// Reset this detail viewer's view state to match the +link{type:DetailViewerViewState} object passed in.<br>
// Used to restore previous state retrieved from the detail viewer by a call to
// +link{detailViewer.getViewState()}.
//
// @param viewState (DetailViewerViewState) Object describing the desired view state for the
// detail viewer
// @group viewState
// @see detailViewer.getViewState()
// @visibility external
//<
setViewState : function (state) {
    state = this.evalViewState(state, "viewState")
    if (!state) return;

    // Order is somewhat important - for example show fields before potentially sorting
    // by them, etc
    if (state.field) this.setFieldState(state.field);
    this.setSortState(state.sort);
    this.setHiliteState(state.hilite);
},


//>    @attr detailViewer.showDetailFields (Boolean : true : IR)
// @include dataBoundComponent.showDetailFields
//<

//> @attr detailViewer.fieldPickerWindow (AutoChild FieldPickerWindow : null : IR)
// Instance of +link{FieldPickerWindow} used if +link{canPickFields} is set.
// @visibility external
//<

fieldPickerWindowDefaults : {
    autoParent: "none",
    _constructor: "FieldPickerWindow"
},

//> @attr detailViewer.canPickFields (Boolean : false : IR)
// If set, right-clicking on the DetailViewer will show a context menu that offers to bring up a
// +link{FieldPicker} for configuring which fields are displayed and their order.
// @visibility external
//<
canPickFields: false,

//> @attr detailViewer.configureFieldsText (String : "Configure Fields..." : [IR])
// The title for the Configure Fields menu item.
// @group i18nMessages
// @visibility external
//<
configureFieldsText: "Configure Fields...",


//> @attr detailViewer.fieldPickerFieldProperties (Array of String : null : IR)
// Names of properties on +link{DetailViewerField} for which the +link{fieldPicker} should
// show an editing interface, for convenience.
// <P>
// For example, specify ["decimalPad", "decimalPrecision"] to allow end users to modify
// +link{detailViewerField.decimalPad} and +link{detailViewerField.decimalPrecision} respectively.
// @visibility external
//<
fieldPickerFieldProperties: null

});    // END isc.DetailViewer.addMethods()

// Register stringMethods for this class - instance methods that can be defined as strings using
// specified keywords (replaced by arguments in the function created)
isc.DetailViewer.registerStringMethods({

    getCellValue:"record,field",
    getCellStyle:"value,field,record,viewer",
    getCellCSSText:"value,field,record,viewer",
    formatCellValue:"value,record,field,viewer",
    fieldStateChanged:""
});




//>    @class    GridRenderer
//
// A flexible, high-speed table that offers consistent cross-platform sizing, clipping, and events.
//
//  @treeLocation Client Reference/Foundation
//  @visibility external
//<

isc.ClassFactory.defineClass("GridRenderer", "Canvas");

isc.GridRenderer.addClassProperties({

    //>    @type    CellState
    // Appearance of the cell -- used to set to different visual states.
    // Also used as a suffix to gridRenderer.baseStyle to set CSS styles.
    // @group appearance
    // @see    gridRenderer.getCellStyle()
    SELECTED:"Selected",        //    @value    isc.gridRenderer.SELECTED        Cell is selected.
    DISABLED:"Disabled",        //    @value    isc.gridRenderer.DISABLED        Cell is disabled.
    OVER:"Over",                //    @value    isc.gridRenderer.OVER            Mouse is over the cell.
    //<

    //>    @attr    gridRenderer.standardStyleSuffixes    (array : array of strings : IR)
    //        Array of the 12 standard cell style suffix strings ("Over", "SelectedOver", etc.)
    //      to allow quicker calculation of cell styles.
    //        @see    gridRenderer.getCellStyle()
    //<
    standardStyleSuffixes:[
                 "",
                 "Over",
                 "Selected",
                 "SelectedOver",
                 "Disabled",
                 "DisabledOver",
                 "DisabledSelected",
                 "DisabledSelectedOver",
                 "Dark",
                 "OverDark",
                 "SelectedDark",
                 "SelectedOverDark",
                 "DisabledDark"
     ]
});

isc.GridRenderer.addProperties({
showFocusOutline: false,

//>    @attr gridRenderer.totalRows (number : 0 : [IRW])
// Total number of rows in the grid.<br><br>
//
// NOTE: in order to create a valid grid, you must either provide a totalRows value or implement
// getTotalRows()
//
// @see method:getTotalRows
// @visibility external
//<
totalRows : 0,

//>    @attr gridRenderer.showAllRows (boolean : false : [IRA])
// Whether all rows should be drawn all at once, or only rows visible in the viewport.
// <P>
// Drawing all rows causes longer initial rendering time, but allows smoother vertical scrolling.
// With a very large number of rows, showAllRows will become too slow.
// <P>
// See also +link{drawAheadRatio} and +link{drawAllMaxCells}.
//
// @group performance
// @visibility external
//<
//showAllRows : false,

//>    @attr gridRenderer.virtualScrolling (boolean : null : [IRA])
// When incremental rendering is switched on and there are variable record heights, the virtual
// scrolling mechanism manages the differences in scroll height calculations due to the
// unknown sizes of unrendered rows to make the scrollbar and viewport appear correctly.
// <P>
// virtualScrolling is switched on automatically when fixedRowHeights is false but you should
// switch it on any time there are variable record heights.
// @visibility external
//<
//virtualScrolling:true,

//>    @attr gridRenderer.showAllColumns (boolean : false : [IRA])
// Whether all columns should be drawn all at once, or only columns visible in the viewport.
// <P>
// Drawing all columns causes longer initial rendering time, but allows smoother horizontal
// scrolling.  With a very large number of columns, showAllColumns will become too slow.
//
// @group performance
// @visibility external
//<
//showAllColumns : false,

//>@attr gridRenderer.drawAllMaxCells (integer : 250 : IRWA)
// If drawing all rows would cause less than <code>drawAllMaxCells</code> cells to be rendered,
// the full dataset will instead be drawn even if +link{listGrid.showAllRecords,showAllRecords}
// is false and the viewport size and +link{drawAheadRatio} setting would normally have caused
// incremental rendering to be used.
// <P>
// The <code>drawAllMaxCells</code> setting prevents incremental rendering from being used in
// situations where it's really unnecessary, such as a 40 row, 5 column dataset (only 200
// cells) which happens to be in a grid with a viewport showing only 20 or so rows.
// Incremental rendering causes a brief "flash" during scrolling as the visible portion of the
// dataset is redrawn, and a better scrolling experience can be obtained in this situation by
// drawing the entire dataset up front, which in this example would have negligible effect on
// initial draw time.
// <P>
// <code>drawAllMaxCells:0</code> disables this features.  You may want to disable this feature
// if performance is an issue and:
// <ul>
// <li> you are very frequently redraw a grid
// <li> you do a lot of computation when rendering each cell (eg formulas)
// <li> you are showing many grids on one screen and the user won't scroll most of them
// </ul>
//
// @group performance
// @visibility external
//<
drawAllMaxCells:250,


//> @attr gridRenderer.recordCanSelectProperty (string : "canSelect" : [IRA])
// If set to false on a record, selection of that record is disallowed.
//<
recordCanSelectProperty: "canSelect",

//> @attr gridRenderer.isSeparatorProperty (string : "isSeparator" : [IRA])
// If this property is defined on some record, render the record as a separator row.
//<
// Documented at the ListGrid level only. ListGrids will pass the isSeparatorProperty through
// to their body.
isSeparatorProperty:"isSeparator",

//> @attr gridRenderer.singleCellValueProperty (string : "singleCellValue" : [IRA])
// If this property is defined on some record, render the record as a single cell (spanning all
// columns).
//<
// Documented at the ListGrid level only. ListGrids also implement getCellValue() which
// will display record.singleCellValue as the value of the cell.
// ListGrids will pass the singleCellValueProperty through to their body.
singleCellValueProperty:"singleCellValue",

//> @attr gridRenderer.instantScrollTrackRedraw (boolean : true : IRW)
// If true, if the user clicks on the scroll buttons at the end of the track or clicks once on
// the scroll track, there will be an instant redraw of the grid content so that the user
// doesn't see any blank space.  For drag scrolling or other types of scrolling, the
// +link{scrollRedrawDelay} applies.
//
// @group performance
// @visibility external
//<
instantScrollTrackRedraw:true,

//> @attr gridRenderer.scrollRedrawDelay (integer : 75 : IRW)
// While drag scrolling in an incrementally rendered grid, time in milliseconds to wait
// before redrawing, after the last mouse movement by the user. This delay may be
// separately customized for mouse-wheel scrolling via +link{scrollWheelRedrawDelay}.
// <P>
// See also
// +link{gridRenderer.instantScrollTrackRedraw} for cases where this delay is skipped.
//
// @group performance
// @visibility external
//<
scrollRedrawDelay:75,

//> @attr gridRenderer.scrollWheelRedrawDelay (Integer : null : IRW)
// While scrolling an incrementally rendered grid, using the mouseWheel, time in
// milliseconds to wait before redrawing, after the last mouseWheel movement by the user.
// If not specified +link{scrollRedrawDelay} will be used as a default for both
// drag scrolling and mouseWheel scrolling.
// <P>
// See also
// +link{gridRenderer.instantScrollTrackRedraw} for cases where this delay is skipped.
//
// @group performance
// @visibility external
//<
//scrollWheelRedrawDelay:null,

//>    @attr gridRenderer.drawAheadRatio (float : 1.3 : [IRWA])
// How far should we render rows ahead of the currently visible area?  This is expressed as a
// ratio from viewport size to rendered area size.
// <P>
// Tweaking drawAheadRatio allows you to make tradeoffs between continuous scrolling speed vs
// initial render time and render time when scrolling by large amounts.
// <P>
// NOTE: Only applies when showAllRows is false.
//
// @group performance
// @visibility external
//<
drawAheadRatio : 1.3,

//>    @attr gridRenderer.quickDrawAheadRatio (float : 1.0 : [IRWA])
// Alternative to +link{drawAheadRatio}, to be used when the user
// is rapidly changing the grids viewport (for example drag scrolling through the grid).
// If unspecified +link{drawAheadRatio} will be used in all cases
// @group performance
// @visibility external
//<
quickDrawAheadRatio:1.0,

//>    @attr gridRenderer.cellHeight        (number : 20 : [IRW])
// The default height of each row in pixels.
//
// @see gridRenderer.getRowHeight()
// @visibility external
// @group cellStyling
//<
cellHeight:20,

//>    @attr gridRenderer.fixedRowHeights (boolean : true : IRWA)
// Should we vertically clip cell contents, or allow rows to expand vertically to show all
// contents?
// <P>
// If we allow rows to expand, the row height as derived from
// +link{gridRenderer.getRowHeight(),getRowHeight()} or the
// default +link{cellHeight} is treated as a minimum.
//
// @group cellStyling
// @visibility external
//<

// Note: We can have variable height rows even with fixedRowHeights set to true, if we
// have embedded components (which cause rows to expand) for example.

fixedRowHeights:true,
//enforceVClipping:null,
shouldEnforceVClipping : function () {
    return this.fixedRowHeights && (this.enforceVClipping || this.wrapCells);
},



//>    @attr    gridRenderer.fixedColumnWidths        (boolean : true : IRWA)
// Should we horizontally clip cell contents, or allow columns to expand horizontally to
// show all contents?<br><br>
//
// If we allow columns to expand, the column width is treated as a minimum.
//
// @group    sizing
// @visibility external
//<

fixedColumnWidths:true,

//>    @attr    gridRenderer.autoFit              (boolean : false : IRWA)
// If true, make columns only wide enough to fit content, ignoring any widths specified.
// Overrides fixedFieldWidths.
//
// @group    sizing
// @visibility external
//<
//autoFit:false,

//>    @attr    gridRenderer.wrapCells        (boolean : false : IRWA)
// Should content within cells be allowed to wrap?
// @group cellStyling
// @visibility external
//<
//wrapCells:false,

//>    @attr    gridRenderer.cellSpacing        (number : 0 : [IRW])
// The amount of empty space, in pixels, between each cell.
// @group cellStyling
// @visibility internal
//<
cellSpacing:0,

//>    @attr    gridRenderer.cellPadding        (number : 2 : [IRW])
// The amount of empty space, in pixels, surrounding each value in its cell.
// @group cellStyling
// @visibility external
//<
cellPadding:2,

//>    @attr    gridRenderer.canSelectOnRightMouse  (boolean : true : [RW])
//  If true, rightMouseDown events will fire 'selectOnRightMouseDown()' for the appropriate cells.
// @group    events
// @visibility external
//<
canSelectOnRightMouse:true,

// Hover
// ---------------------------------------------------------------------------------------

//>    @attr gridRenderer.canHover (boolean : null : [RW])
// If true, cellHover and rowHover events will fire and then a hover will be shown (if not
// canceled) when the user leaves the mouse over a row / cell unless the corresponding field has
// +link{ListGridField.showHover,showHover} set to false. If unset or null, the hover will be
// shown if the corresponding field has showHover:true. If false, then hovers are disabled.
// <p>
// Note that standard hovers override +link{showClippedValuesOnHover,clipped value hovers}. Thus,
// to enable clipped value hovers, canHover must be unset or null and the corresponding field
// must have showHover unset or null as well.
// @group    events
// @visibility external
// @see cellHover()
// @see rowHover()
// @see showHover
// @see showClippedValuesOnHover
//<

//>    @attr gridRenderer.showHover (boolean : null : [RW])
// If true, and canHover is also true, when the user hovers over a cell, hover text will pop up
// next to the mouse.  The contents of the hover is determined by +link{cellHoverHTML()}.
// @group    events
// @visibility external
// @see canHover
// @see cellHoverHTML()
//<

//> @attr gridRenderer.showClippedValuesOnHover (Boolean : null : IRWA)
// If true and a cell's value is clipped, then a hover containing the full cell value is
// enabled.
// <p>
// Note that standard cell hovers override clipped value hovers. Thus, to enable clipped value
// hovers, +link{canHover,canHover} must be unset or null and the corresponding field must have
// +link{ListGridField.showHover,showHover} unset or null as well.
// @group events
// @see canHover
// @see cellValueHoverHTML()
// @visibility external
//<

// can be set to false to cause Hover to be per-row instead of per-cell
hoverByCell:true,

// CSS styles
// --------------------------------------------------------------------------------------------


backgroundColor:"white",

// style applied to the table element.  NOTE: don't expose: styling of a grid should be
// accomplish by styling the surrounding DIV, where we can use the standard methodology to
// detect borders, margins, etc (eg getInnerHeight())
tableStyle:"listTable",

//>    @attr    gridRenderer.baseStyle        (CSSStyleName : "cell" : [IR])
// The base name for the CSS class applied to cells. This style will have "Dark",
// "Over", "Selected", or "Disabled" appended to it according to the state of the cell.
// @visibility external
// @group cellStyling
// @see method:getCellStyle()
// @see method:getBaseStyle()
//<
baseStyle:"cell",

//> @attr gridRenderer.alternateRowStyles (boolean : false : [IRW])
// Whether alternating rows should be drawn in alternating styles, in order to create a "ledger"
// effect for easier reading.  If enabled, the cell style for alternate rows will have "Dark"
// appended to it.
// @visibility external
// @group cellStyling
//<
//alternateRowStyles:false,

//> @attr gridRenderer.alternateRowFrequency (number : 1 : [IRW])
// The number of consecutive rows to draw in the same style before alternating, when
// +link{gridRenderer.alternateRowStyles, alternateRowStyles} is true.
// @visibility external
// @group cellStyling
//<
alternateRowFrequency:1,

//> @attr gridRenderer.alternateColumnStyles (boolean : false : [IRW])
// Whether alternating columns should be drawn in alternating styles, in order to create a
// vertical "ledger" effect for easier reading.  If enabled, the cell style for alternate
// columns will have "Dark" appended to it.
// @visibility external
// @group cellStyling
//<
//alternateColumnStyles:false,

//> @attr gridRenderer.alternateColumnFrequency (number : 1 : [IRW])
// The number of consecutive columns to draw in the same style before alternating, when
// +link{gridRenderer.alternateColumnStyles, alternateColumnStyles} is true.
// @visibility external
// @group cellStyling
//<
alternateColumnFrequency:1,

//> @attr gridRenderer.emptyCellValue (string : "&nbsp;" : IRW)
// Value to show in empty cells (when getCellValue returns null).
// @group cellStyling
// @visibility external
//<
emptyCellValue:"&nbsp;",


// Empty messages (what to do if no data is present)
// --------------------------------------------------------------------------------------------
//>    @attr    gridRenderer.showEmptyMessage        (boolean : true : [IRW])
// Indicates whether the text of the emptyMessage property should be displayed if no data is
// available.
//      @visibility external
//      @group  emptyMessage
//      @see    emptyMessage
//<

//>    @attr    gridRenderer.emptyMessage        (string : "No data to display" : IRW)
// The string to display in the body of a listGrid with an empty data array, if
// showEmptyMessage is true.
// @group emptyMessage, i18nMessages
// @visibility external
//      @see    gridRenderer.showEmptyMessage
//      @see    gridRenderer.emptyMessageStyle
//<
emptyMessage: "No data to display",

//>    @attr gridRenderer.emptyMessageTableStyle (CSSStyleName : null : IRW)
// CSS styleName for the table as a whole if we're showing the empty message
// @group emptyMessage
// @visibility external
//<

//>    @attr gridRenderer.emptyMessageStyle (CSSStyleName : null : IRW)
// The CSS style name applied to the emptyMessage string if displayed.
// @group emptyMessage
// @visibility external
//<

//>    @attr gridRenderer.showOfflineMessage (boolean : true : [IRW])
// Indicates whether the text of the offlineMessage property should be displayed if no data
// is available because we are offline and there is no suitable cached response
// @visibility external
// @group emptyMessage, offlineGroup
// @see offlineMessage
//<

//>    @attr gridRenderer.offlineMessageStyle (CSSStyleName : null : IRW)
// The CSS style name applied to the offlineMessage string if displayed.
// @group emptyMessage, offlineGroup
// @visibility external
//<

//>    @attr gridRenderer.offlineMessage (string : "No data available while offline" : IRW)
// The string to display in the body of a listGrid with an empty data array, if
// showOfflineMessage is true and the data array is empty because we are offline and there
// is no suitable cached response
// @group offlineGroup, emptyMessage, i18nMessages
// @visibility external
// @see showOfflineMessage
// @see offlineMessageStyle
//<
offlineMessage: "No data available while offline",

//> @attr gridRenderer.fastCellUpdates (boolean: true : IRWA)
//
// <b>Note: This property only has an effect in Internet Explorer</b>
// <P>
// Advanced property to improve performance for dynamic styling of gridRenderer cells in
// Internet Explorer, at the expense of slightly slower initial drawing, and some
// limitations on supported styling options.
// <P>
// <code>fastCellUpdates</code> speeds up the dynamic styling system used by rollovers,
// selections, and custom styling that calls +link{gridRenderer.refreshCellStyle()}, at
// the cost of slightly slower draw() and redraw() times.
// <P>
// Notes:
// <ul>
// <li>When this property is set, ListGrid cells may be styled using the
//     +link{listGrid.tallBaseStyle}. See +link{listGrid.getBaseStyle()} for
//     more information.</li>
// <li>If any cell styles specify a a background image URL, the URL will be resolved relative
//     to the page location rather than the location of the CSS stylesheet. This means cell
//     styles with a background URL should either supply a fully qualified path, or the
//     background image media should be made available at a second location for IE.</li>
// <li>fastCellUpdates will not work if the styles involved are in an external stylesheet loaded
//     from a remote host. Either the stylesheet containing cell styles needs to be loaded
//     from the same host as the main page, or the cell styles need to be inlined in the html
//     of the bootstrap page.</li>
// <li>fastCellUpdates will not work if the css styles for cells are defined in
//     a <code>.css</code> file loaded via <code>@import</code>. Instead the <code>.css</code>
//     file should be loaded via a <code>&lt;link ...&gt;</code> tag.</li>
// </ul>
// @visibility external
//<

// This default is overridden in ListGrid
fastCellUpdates:true,

//> @method gridRenderer.setFastCellUpdates()
// Setter for +link{gridRenderer.fastCellUpdates}. Has no effect in browsers other than
// Internet Explorer.
// @param fastCellUpdates (boolean) whether to enable fastCellUpdates.
// @visibility external
//<
setFastCellUpdates : function (fcu) {
    if (fcu && !isc.Browser.isIE) {
        this.fastCellUpdates = false;
        //this.logInfo("fastCellUpdates was enabled - this has no effect " +
        //    "in browsers other than Internet Explorer. Disabling.");
        return;
    }
    if (fcu == this.fastCellUpdates) return;
    this.fastCellUpdates = fcu;
    this.markForRedraw();
},




// Standard Canvas settings
// --------------------------------------------------------------------------------------------
overflow:"auto",


_avoidRedrawFlash:true,

canFocus:true

//>Animation
// If a px / second speed is specified for a row animation, cap it at a maximum
// (Inherited from LG / TG if specified there)
,animateRowsMaxTime:1000,
//<Animation


//>    @attr gridRenderer.snapToCells (boolean : false : IRW)
// Should drag-and-drop operations snap the dragged object into line with the nearest cell?
//
// @group dragdrop
// @visibility external
//<
snapToCells: false,

//> @attr gridRenderer.snapInsideBorder (boolean : false : IRW)
// If true, snap-to-cell drops will snap the dropped object inside the selected cell's border.
// If false, snap-to-cell drops will snap the dropped object to the edge of the selected cell,
// regardless of borders
//
// @group dragdrop
// @see    GridRenderer.snapToCells
// @visibility external
//<
snapInsideBorder: false,


snapHDirection: isc.Canvas.BEFORE,
snapVDirection: isc.Canvas.BEFORE

});

isc.GridRenderer.addMethods({


initWidget : function () {
    // Make sure we have columnWidths set up - we rely on this for some methods
    if (!this._fieldWidths) this.setColumnWidths([]);

    if (this.selection) this.setSelection(this.selection);

    // If we're overflow visible, we have to write out our entire body content.
    if (this.overflow == isc.Canvas.VISIBLE) {
        this.showAllRows = true;
    }

    // turn virtualScrolling on if it's unset and we have variable-height rows
    if (!this.fixedRowHeights && this.virtualScrolling == null) this.virtualScrolling = true;
    // default for virtualScrolling is now null, so if it's been turned on, also turn
    // fixedRowHeights off so that virtualScrolling has an effect
    //if (this.fixedRowHeights && this.virtualScrolling) this.fixedRowHeights = false;
    if (!this.fixedRowHeights && !this.showAllRows) {


        //this._avoidRedrawFlash = true;

        if (this.showCustomScrollbars == false) {
            this.logInfo("Variable height records cannot be used with native scrollbars;" +
                         " setting showCustomScrollbars:true on this GridRenderer and using" +
                         " the special 'NativeScrollbar' class as a scrollbarConstructor.");
            this.showCustomScrollbars = true;
            this.scrollbarConstructor = "NativeScrollbar";
        }

    }

    // initialize fastCellUpdates via the setter.
    // Disables this attribute where not supported
    this.setFastCellUpdates(this.fastCellUpdates);

},


removeColumnSizerDelay: 100,
removeColumnSizer : function(removeCompletely) {
    if (this._columnSizer) {
        if (removeCompletely) {
            this._columnSizer.clear();
            delete this._columnSizer;
        } else {
            this._columnSizer.setContents("&nbsp;");
        }
    }
},

destroy : function () {
    this.clearSelection();
    this.removeColumnSizer(true);
    this.Super("destroy", arguments);
},

shouldShowAllColumns : function () {

    if (this.showAllColumns) return true;

    // have to force rendering all columns, otherwise, row heights would vary with drawn
    // columns.

    if (!this.fixedRowHeights && !this.showAllRows) return true;
    if (this.overflow == isc.Canvas.VISIBLE) {
        return true;
    }
    return false;
},

// Empty Message handling
// --------------------------------------------------------------------------------------------

isEmpty : function () { return false; },

_showEmptyMessage : function (startCol,endCol) {
    return this.getEmptyMessageHTML(startCol,endCol, this.grid.isOffline());
},

//>    @method    gridRenderer.getEmptyMessageHTML()    ([A])
//            Return the HTML to show if there's nothing in the list
//        @group    drawing
//        @return    (HTMLString)    HTML for the empty message layer
//<
getEmptyMessageHTML : function (startCol,endCol,offline) {
    if (!offline) {
        if (!this.showEmptyMessage)    return "&nbsp;";
    } else {
        if (!this.showOfflineMessage) return "&nbsp;";
    }
    if (this.isPrinting) {
        if (startCol == null) startCol = 0;
        if (endCol == null) endCol = this.fields ? this.fields.getLength() -1 : 0;


        var showHeader = !(this._printingChunk && this.printChunkOnly);

        return "<TABLE role='presentation' cellspacing=0 style='width:100%'" +
                (this.emptyMessageTableStyle?(" class='" + this.emptyMessageTableStyle + "'"):"") +
                ">" +
                (showHeader ? this.getPrintHeaders(startCol, endCol+1) : "") +
                "<TR><TD  ALIGN=CENTER class='" +
                (offline ? this.offlineMessageStyle : this.emptyMessageStyle) +
                "' colspan='" + ((endCol-startCol)) + "'>" +
                (offline ? this.getOfflineMessage() : this.getEmptyMessage())
                + "</TD></TR>" +
                (showHeader ? this.getPrintFooters(startCol, endCol+1) : "") +
                "</TABLE>";
    }

    // Always ensure the empty message fills the viewport.
    // Respect flag to match the empty message size to the specified field widths -
    // if the specified field sizes exceed the viewport size, expand the empty message
    // to accommodate it.
    var width = this.getInnerWidth(),
        extraWidth = 0;
    if (this.expandEmptyMessageToMatchFields && this._fieldWidths) {
        extraWidth = this._fieldWidths.sum() - width;
        if (extraWidth < 0) extraWidth = 0;
    }
    if (this.applyHSpaceToEmptyMessage) {
        if (this.leftSpace != null) extraWidth += this.leftSpace;
        if (this.rightSpace != null) extraWidth += this.rightSpace;
    }

    // Note that if the GR is scrollable, we want the empty message to be visible /
    // centered when scrolled to 0/0 so table into 2 cells, centering the empty message
    // within the first cell which spans the viewport
    var splitTable = extraWidth && this.overflow != isc.Canvas.VISIBLE;

    var vPad = this._vPad;
    // this._vPad may be undefined (for example, when drawing the "[Empty menu]" submenu).
    if (vPad == null) {
        vPad = 0;

        if (isc.Browser.isStrict &&
            ((isc.Browser.isMoz && isc.Browser.version >= 17) ||
             (isc.Browser.isSafari || isc.Browser.isIE)))
        {
            vPad = (this.fixedRowHeights ? 0 : this.cellPadding * 2);
            if (this.emptyMessageTableStyle) vPad += isc.Element._getVBorderSize(this.emptyMessageTableStyle);
        }
    }

    var sb = isc.StringBuffer.create();
    sb.append(
            "<TABLE role='presentation' BORDER=0 MARGIN=0 CELLSPACING='", this.cellSpacing,
                "' CELLPADDING='", this.cellPadding,
                (this.emptyMessageTableStyle?("' CLASS='" + this.emptyMessageTableStyle):null),
                "' style='width:",(width+extraWidth),"px;",

                (isc.Browser.isSafari ? "'"
                                      : "' HEIGHT=100%"),
            "><TR><TD ALIGN=CENTER CLASS='",
            (offline ? this.offlineMessageStyle : this.emptyMessageStyle),
            "' style='padding-left:0px;padding-right:0px;height:" + (this.cellHeight - vPad) + "px'>",
            // NOTE: empty message can't be too tall, or it will introduce vscrolling in
            // shorter grids
            (offline ? this.getOfflineMessage() : this.getEmptyMessage()),
            (extraWidth && splitTable ? "<br>" + isc.Canvas.spacerHTML(width,1) : null),
            "</TD>"
    );
    if (extraWidth && splitTable) {
        sb.append("<TD style='padding-left:0px;padding-right:0px;'>",
                  isc.Canvas.spacerHTML(extraWidth, 1), "</TD>");
    }
    sb.append("</TR></TABLE>");
    return sb.release();
},


//>    @method    gridRenderer.getEmptyMessage()    ([A])
//        @group    drawing
//            return the text for the empty message
//            you can ovveride this function if your data has additional semantics
//                (eg: initial conditions, loading, filtering, etc)
//        @return    (string)    empty message
//<
getEmptyMessage : function () {
    return this.emptyMessage;
},

//>    @method    gridRenderer.getOfflineMessage()    ([A])
//        @group    drawing
//            return the text for the offline message.  The default implementation returns the
//          value of the containing Grid's offlineMessage property;
//            you can ovveride this function if your data has additional semantics
//                (eg: initial conditions, loading, filtering, etc)
//        @return    (string)    offline message
//<
getOfflineMessage : function () {
    return this.grid.offlineMessage;
},

// Drawing
// --------------------------------------------------------------------------------------------



// use a rel-pos div to apply a z-index to the content.
// required for the ability to float embedded components above or below the table
_$zIndexDivTemplate:["<DIV id='",
                     ,  // 1 [_getZIndexDivID()]
                     "' style='" +

                        (isc.Browser.isMoz && isc.Browser.version == 18 ? "display:inline;" : "") +

                        "position:relative;z-index:",
                     ,  // 3 [getTableZIndex()]
                     ";width:",
                     ,  // 5 width
                     "px'>",

                     isc.Browser.isIE ? "<form action='javascript:void(0)' onsubmit='return false;'>" : null, // 7
                     ,  // 8 [table html]
                     ,  // 9 [optional filler div]
                     isc.Browser.isIE ? "</form>" : null,
                     "</DIV>"],
_$fillerDiv:"<table style='position:absolute;top:0px;font-size:1px;height:100%;width:100%;z-index:1;overflow:hidden;visibility:hidden;'><tr><td>&nbsp;</td></tr></table>",
_getZIndexDivID : function () {
    return this._getDOMID("zIndexDiv");
},
_getZIndexDiv : function () {
    return this.getDocument().getElementById(this._getZIndexDivID());
},
getInnerHTML : function () {
    var tableHTML = this.getTableHTML(),
        template = this._$zIndexDivTemplate;
    template[1] = this._getZIndexDivID();
    template[3] = this.getTableZIndex();
    // When the grid is (or will) show the sorter, but the grid body is not showing a
    // vertical scrollbar, we want to increase the scrollWidth of the grid body by the width
    // of the sorter button so that if the last field is wider than the viewport width, its
    // header button won't be partially occluded by the sorter.
    //
    // This requires innerSizeChanged() to be overridden to update the CSS width whenever the
    // grid body shows or hides a vertical scrollbar.
    //
    // Note: Only applies when we're showing custom scrollbars.

    var grid = this.grid,
        width = this._fieldWidths.sum() - this.getHBorderSize();
    if (this.showCustomScrollbars && !this.frozen && grid != null && grid._showSortButton() &&
        !this.vscrollOn)
    {
        width += grid._getSorterWidth();
    }
    template[5] = width;
    template[8] = tableHTML;

    if (isc.Browser.isMoz) template[9] = this._$fillerDiv;
    return template.join(isc.emptyString);
},

innerSizeChanged : function (reason, b,c,d) {
    var returnVal = this.invokeSuper(isc.GridRenderer, "innerSizeChanged", reason, b,c,d);

    var zIndexDiv = this._getZIndexDiv();
    if (zIndexDiv != null) {
        var grid = this.grid,
            width = this._fieldWidths.sum() - this.getHBorderSize();
        if (this.showCustomScrollbars && !this.frozen && grid != null && grid._showSortButton() &&
            !this.vscrollOn)
        {
            width += grid._getSorterWidth();
        }
        zIndexDiv.style.width = width + isc.px;
    }

    return returnVal;
},

isFastScrolling : function () {
    return this.isDragScrolling() || this.isRepeatTrackScrolling() || this.isMouseWheelScrolling();
},

shouldUseQuickDrawAheadRatio : function () {
    // NOTE: useQuickDrawAheadRatio is a flag you can flip on temporarily when there's some
    // reason other than fast scrolling that you want quick redraws (eg column drag resize)
    return this.useQuickDrawAheadRatio || this.isFastScrolling();
},


doneFastScrolling : function () {
    // if our last redraw was caused by fast scrolling we will have applied the quick
    // draw ahead ratio when determining which records to draw. In this case we now want
    // to redraw with the standard drawAhead ratio so short distance scrolls around this area
    // will requrie fewer redraws
    var redrawRequired = this._appliedQuickDrawAhead;
    if (redrawRequired) {
        // set the flag to suppress drawAhead direction calculation. This ensures that we
        // add draw-ahead rows in all directions on the theory that the user is done scrolling
        // large increments in  one direction.
        // We clear this flag when the delayed redraw() actually fires
        this._suppressDrawAheadDirection = true;
        this.markForRedraw("Done Fast scrolling.");
    }
},

// given a range elements (rows or cols) currently visible in the viewport, apply the
// drawAheadRatio to determine the range to draw.  The "drawAheadRatio" is a fraction (>1) of
// the viewport. "scrollForward" is the scrolling direction: true (forward), false (backward),
// or null (unknown)
addDrawAhead : function (startIndex, endIndex, maxIndex, scrollForward, vertical) {


    // figure out how many elements we intend to draw
    var useQuickDrawAhead = this.shouldUseQuickDrawAheadRatio(),
        ratio = useQuickDrawAhead && this.quickDrawAheadRatio != null ?
                                             this.quickDrawAheadRatio : this.drawAheadRatio,
        numToDraw = Math.ceil((endIndex - startIndex) * ratio);
    // respect the flag to suppress the drawAhead scrolling direction logic
    if (this._suppressDrawAheadDirection) scrollForward = null;

    if (scrollForward != null) {
        // we know the scroll direction; render extra elements in the current direction of
        // scrolling
        if (scrollForward) endIndex = startIndex + numToDraw;
        else startIndex = endIndex - numToDraw;
    } else {
        // we haven't been scrolled yet; if we're flush at the beginning (very common), render
        // ahead forward
        if (startIndex == 0) endIndex = numToDraw;
        else {
            // otherwise, render extra rows on either side
            var extraElements = Math.ceil((numToDraw - (endIndex - startIndex))/2);
            startIndex -= extraElements;
            endIndex += extraElements;
        }
    }

    // clamp ends of the range to 0 / maxIndex
    if (startIndex < 0) {   // shift both ends of the range forward so startIndex = 0
        endIndex -= startIndex;
        startIndex = 0;
    }

    if (endIndex >= maxIndex) { // shift both ends of the range back so endIndex < maxIndex
        var offset = endIndex - (maxIndex -1);
        startIndex = Math.max(0, (startIndex - offset));
        endIndex = Math.max(0, maxIndex - 1);
    }



    // store a flag indicating whether this redraw used the special 'quick draw ahead' code
    // this is checked in doneFastScrolling to determine whether a redraw is required.
    if (useQuickDrawAhead) this._appliedQuickDrawAhead = true;
    else delete this._appliedQuickDrawAhead;

    return [startIndex, endIndex];
},

// Helper - returns 1 if this is the frozen body of a ListGrid, zero otherwise.

_isFrozenBody:function() {
    if (this.grid && this.grid.frozenBody == this) return 1;
    return 0;
},

getExtraRowHeight : function (startRow, endRow) {
    var total = 0;
    for (var rowNum = startRow; rowNum < endRow; rowNum++) {
        var height = this.getRowHeight(this.getCellRecord(rowNum, 0), rowNum, this._isFrozenBody()),
            extraHeight = (height - this.cellHeight);
        if (extraHeight > 0) {
            //this.logWarn("rowNum: " + rowNum +
            //             " in range: " + [this._firstDrawnRow, this._lastDrawnRow] +
            //             " with extraHeight: " + extraHeight);
            total += extraHeight;
        }
    }
    return total;
},

getDrawArea : function (colNum) {
    // Figure out what rows should be drawn
    // --------------------------------------------------------------------------------------------
    var totalRows = this.getTotalRows(), startRow, endRow, vScrollForward;

    // figure out if we should show all cells in case the total displayable cells are less than
    // drawAllMaxCells
    var totalCells = totalRows * this.fields.length,
        showAllCells = totalCells <= this.drawAllMaxCells &&
                       !isc.EH.dragging && !this.isAnimating() &&
                       !(this.parentElement && this.parentElement.isAnimating());

    if (this.showAllRows || showAllCells) {
        // draw all rows
        startRow = 0;
        endRow = Math.max(totalRows - 1, 0);
    } else {
        // ordinary incremental rendering
        var rowArr = this._getDrawRows();
        startRow = rowArr[0];
        endRow = rowArr[1];
        // just for logging
        vScrollForward = rowArr[2]
    }

    // Figure out which columns to draw
    // --------------------------------------------------------------------------------------------
    var startCol, endCol, totalCols = this.fields.length, hScrollForward;
    if (colNum != null) {
        // a column number was specified, draw that column only (needed for legacy Nav4 support, and
        // for column auto-sizing)
        startCol = colNum;
        endCol = colNum + 1;
    } else if (showAllCells || this.shouldShowAllColumns()) {
        // draw all columns
        startCol = 0;
        endCol = totalCols - 1;
    } else {
        // incremental rendering
        var visibleColumns = this.getVisibleColumns();
        // detect scrolling direction: true (forward), false (backward), or null (unknown)
        hScrollForward = (this.lastScrollLeft == null ? null :
                          this.lastScrollLeft < this.getScrollLeft());

        var drawAheadRange = this.addDrawAhead(visibleColumns[0], visibleColumns[1],
                                               totalCols, hScrollForward);

        startCol = drawAheadRange[0];
        endCol = drawAheadRange[1];
    }

    // figure out the appropriate chunk size on first draw ever
    if (this.cacheDOM && !this._rowChunkSize) {
        this._rowChunkSize = endRow - startRow;
        this._colChunkSize = endCol - startCol;
    }



    return [startRow, endRow, startCol, endCol];
},

_getDrawRows : function () {

        // figure out which rows we need to draw to minimally fill the viewport
        var visibleRows = this._getViewportFillRows();

        // detect scrolling direction: true (forward), false (backward), or null (unknown)
        var vScrollForward = (this.lastScrollTop == null ? null :
                             this.lastScrollTop < this.getScrollTop());

        var totalRows = this.getTotalRows();

        // Note: addDrawAhead will add the draw-ahead rows (rows drawn offscreen for
        // scrolling), and clamp the ends of the drawn range to the ends of the data (ensuring
        // we don't end up with startRow < 0, or endRow > (totalRows-1)
        var drawAheadRange = this.addDrawAhead(visibleRows[0], visibleRows[1],
                                               totalRows, vScrollForward, true);

        //this.logWarn("draw range: " + this._getViewportFillRows() + " fwd:" + vScrollForward +
        //             ", after adding drawAhead:" + drawAheadRange);

        // just for logging - return whether we added the fwd scroll
        drawAheadRange[2] = vScrollForward;

        return drawAheadRange;

},




// get the row at the coordinate, as a floating point number representing a partial distance
// through the row
getRowCoordinate : function (coord) {
    var rowNum = this.getEventRow(coord),
        // get our offset into it
        rowTop = this.getRowTop(rowNum),
        offsetIntoRow = coord - rowTop,
        rowHeight = this.getRowSize(rowNum),
        percentIntoRow = offsetIntoRow / rowHeight;

    // detect inconsistency between getEventRow and getRowTop()
    //if (offsetIntoRow < 0 || offsetIntoRow > rowHeight) {
    //    this.logWarn("*******************************\n" +
    //                 ", coord: " + coord +
    //                 ", eventRow: " + rowNum +
    //                 ", rowTop: " + rowTop +
    //                 ", offsetIntoRow: " + offsetIntoRow +
    //                 ", rowSize: " + rowHeight +
    //                 ", firstDrawn: " + this._firstDrawnRow +
    //                 ", lastDrawn: " + this._lastDrawnRow +
    //                 ", heights: " + this._getDrawnRowHeights());
    //}

    return rowNum + percentIntoRow;
},


// override to interpret ratio in terms of rowNum instead of scrollTop vs scrollHeight
scrollToRatio : function (vertical, ratio, reason,a,b) {
    if (!vertical || !this._isVirtualScrolling) {
        return this.invokeSuper(isc.GridRenderer, "scrollToRatio", vertical,ratio,reason,a,b);
    }

    var maxRow = this.getTotalRows() - 1,
        exactRowNum = ratio * maxRow,
        rowNum = Math.floor(exactRowNum),
        rowOffset = Math.round((exactRowNum - rowNum) * this.getRowSize(rowNum));


    this._targetRow = rowNum;
    this._rowOffset = rowOffset;
    this._scrollToTargetRow(reason || "scrollToRatio");

    // if scrolling to that position makes us dirty, setup to scroll to the indicated target
    // row during redraw
    if (this.isDirty()) {
        this._scrollRatio = ratio;
        this._targetRow = rowNum;
        this._rowOffset = rowOffset;
    }
},

// override to return ratio in terms of rowNum instead of scrollTop vs scrollHeight
getScrollRatio : function (vertical,b,c,d) {

    if (!vertical || !this._isVirtualScrolling) {
        return this.invokeSuper(isc.GridRenderer, "getScrollRatio", vertical,b,c,d);
    }
    if (this.isDirty() && this._scrollRatio != null) return this._scrollRatio;

    // if there are 0 or 1 rows, we're at the top
    var maxRow = this.getTotalRows() - 1;

    if (maxRow <= 0) return 0;

    var scrollTop = this.getScrollTop(),
        topCoord = this.getRowCoordinate(scrollTop),
        ratio = topCoord / maxRow;

    //this.logWarn("getScrollRatio: " + ratio +
    //             ", maxRow: " + maxRow +
    //             ", topCoord: " + topCoord);

    return Math.min(1,ratio);
},

// show a fixed-size thumb in virtualScrolling mode.  Otherwise thumb size fluctuates
// meaninglessly.
getViewportRatio : function (vertical,b,c,d) {
    if (!vertical || !this._isVirtualScrolling) {
        return this.invokeSuper(isc.GridRenderer, "getViewportRatio", vertical,b,c,d);
    }
    var avgRowHeight = this._viewRatioRowHeight || this.getAvgRowHeight();

    return Math.min(1, (this.getViewportHeight() / avgRowHeight) / this.getTotalRows());
},

// take some drawn row that is likely to remain drawn, and store the position it should be in
// relative to the viewport, so that if we have to redraw, we can match user expectation by
// placing rows where the user expects.
_storeTargetRow : function (scrollTop, delta) {

    // don't pick up a target row during the special scroll that places us on the target row
    if (this._literalScroll) return;

    // if we're empty just bail
    if (this.isEmpty()) return;

    // according to scrolling direction, pick the row at the top or bottom of the viewport as
    // the row most likely to remain onscreen
    var viewportEdge,
        targetRow,
        maxRow = this.getTotalRows()-1;
    if (delta > 0) {
        // scrolling down
        viewportEdge = scrollTop + this.getViewportHeight();
        targetRow = this.getEventRow(viewportEdge);
        // If there is no row at the end of the viewport (possible with the spacer we write out for
        // virtual scrolling to work), clamp to the last actual row in the data set.
        if (targetRow == -2 && maxRow >= 0) {
            targetRow = maxRow;
        }
    } else {
        viewportEdge = scrollTop;
        targetRow = this.getEventRow(viewportEdge);
    }
    var newScrollTop = scrollTop;


    if (targetRow < 0 || targetRow > maxRow) {
        this._targetRow = maxRow;
        this._rowOffset = 0;
        newScrollTop = this.getRowTop(maxRow);
    } else {
        this._targetRow = targetRow;
        // how far into the target row the top of the viewport should be (positive means more
        // of row is scrolled offscreen)
        this._rowOffset = scrollTop - this.getRowTop(this._targetRow) + delta;
        //var drawArea = this.getDrawArea();
        //if (targetRow < drawArea[0] || targetRow > drawArea[1]) {
        if (Math.abs(this._rowOffset) > this.getViewportHeight()) {
            // happens only if we are programmatically scrolled by a large amount to a totally
            // new viewport, in which case anchoring to a row from the old viewport is useless
            // and could lead to surprises (eg if scrolled near max, scrolling to 10px should
            // place us on 10px into the first row but might do something different if we
            // calculate coordinates based on drawn row sizes in a viewport near the end).
            this.logInfo("storeTargetRow: targetRow: " + targetRow +
                         " with offset: " + this._rowOffset +
                         //" wouldn't fall within draw range: " + [drawArea[0],drawArea[1]] +
                         ", clearing", "virtualScrolling");
            this._rowOffset = this._targetRow = null;
        }
    }



    return newScrollTop;
},

// scroll the previously stored target row into the stored position
_scrollToTargetRow : function (reason) {
    var targetRow = this._targetRow,
        offset = this._rowOffset;

    var scrollTop = this.getRowTop(targetRow) + offset;


    this._literalScroll = true;

    this._scrollHeight = null;
    this.scrollTo(null, scrollTop, reason || "targetRow");

    this._literalScroll = false;

    // stop reporting last requested scroll ratio since we've now scrolled to match the
    // requested ratio
    this._scrollRatio = null;
},

scrollIntoView : function (x,y, width, height, xPosition, yPosition, animated, callback, alwaysCenter, source) {
    if ((source != null) && (source != this)) {
        // This method call from a child widget
        return;
    }

    this.invokeSuper(isc.GridRenderer, "scrollIntoView", x,y, width, height, xPosition, yPosition, animated, callback, alwaysCenter, source);
},

// if we're rendering rows/cols incrementally, we may need to redraw on scroll
scrollTo : function (left, top, reason, animating) {
    if (isc._traceMarkers) arguments.__this = this;


    if (this._isVirtualScrolling && top != null && reason != "nativeScroll") {
        var oldScrollTop = this.getScrollTop(),
            delta = top - oldScrollTop;

        if (delta != 0) {
            this._storeTargetRow(oldScrollTop, delta);
            top = Math.min(top, this.getRowTop(this.getTotalRows()-1));
        }
    }


    this.invokeSuper(isc.GridRenderer, "scrollTo", left,top, reason, animating);

    // don't check for the need to redraw if we're already dirty.  Optimization: for
    // scroll-and-scroll-back situations, we could avoid a redraw by undirtying ourselves
    if (this.isDirty() || this._scrollFromRedraw) return;

    // if we're only drawing rows near the viewport..
    if (!this._delayedRedraw && this._needAxisRedraw()) {

        this.redrawOnScroll(!this.isFastScrolling() && this.instantScrollTrackRedraw);
    }
},

_getRedrawOnScrollCallback:function () {
    if (this._redrawOnScrollCallback == null) {
        var _this = this;
        this._redrawOnScrollCallback = function () {
            delete this._pendingScrollRedrawFromWheel;

            // fire a synthetic mouseMove - this will update the
            // mouseOver row, etc
            _this._fireSyntheticMouseMove();

            _this.redraw("scrolled")
        };
    }
    return this._redrawOnScrollCallback;
},

redrawOnScroll : function (immediate) {

    // Suppress redrawOnScroll during rowHeight animation.
    // If we're shrinking a row, and we're scrolled to the end we expect to see scrolls
    // occur natively during the animation - we don't want to cut it short and redraw

    if (this._rowHeightAnimation) {
        return;
    }
    if (immediate) {
        this.redraw("scrolled");
    } else {
        var isMouseWheelScrolling = this.isMouseWheelScrolling(),
            delay = this.getScrollRedrawDelay(isMouseWheelScrolling);

        if (delay == 0) {
            this.markForRedraw("scrolled");
        } else {
            this.fireOnPause("scrollRedraw",
                this._getRedrawOnScrollCallback(),
                delay);
            this._pendingScrollRedrawFromWheel = isMouseWheelScrolling;
        }
    }

    this._scrollRedraw = true;
},

getScrollRedrawDelay : function (isMouseWheelScrolling) {
    var delay = this.scrollRedrawDelay;
    if (this.scrollWheelRedrawDelay == null) return delay;

    if (isMouseWheelScrolling == null) {
        isMouseWheelScrolling = this.isMouseWheelScrolling();
    }
    if (isMouseWheelScrolling) {
        delay = this.scrollWheelRedrawDelay;
    }
    return delay;
},

_needRowRedraw : function () {
    if (this.showAllRows) return false;

    // we have a range of records that have been drawn, from grid._firstDrawnRow to
    // grid._lastDrawnRow (updated in getTableHTML).  See if the new viewport falls
    // completely into the drawn range.
    // NOTE: we use visible rows rather than viewport fill rows because by using
    // actual rendered row height we can avoid some redraws when we have several viewports
    // worth of drawn data due to tall rows.
    // Note also that visible rows is only an approximation if asked about an undrawn area,
    // which is fine, because all we care about is whether the new viewport falls
    // completely within the drawn range.
    var visibleRows = this.getVisibleRows(),
        firstVisible = visibleRows[0],
        lastVisible = visibleRows[1];

    // check that the last visible row doesn't exceed the total number of rows we will
    // draw.  NOTE: -1 because totalRows is a count and lastVisible is an index.
    var totalRows = this.getTotalRows();
    if (lastVisible > totalRows-1) lastVisible = totalRows-1;

    var needRedraw = (firstVisible < this._firstDrawnRow || lastVisible > this._lastDrawnRow);




    return needRedraw;
},

_needColumnRedraw : function () {
    // if we're only drawing columns near the viewport..
    if (this.shouldShowAllColumns()) return false;

    var visibleCols = this.getVisibleColumns(),
        firstVisible = visibleCols[0],
        lastVisible = visibleCols[1],
        needRedraw = (firstVisible < this._firstDrawnCol || lastVisible > this._lastDrawnCol);


    return needRedraw;
},

_needAxisRedraw : function () {
    return this._needRowRedraw() || this._needColumnRedraw();
},

// disable incremental rendering when overflow:visible is set on the fly
setOverflow : function (overflow) {
    if (overflow == isc.Canvas.VISIBLE) {
        this.showAllRows = true;
    }

    return this.Super("setOverflow", arguments);
},


// Cache DOM mode
// ---------------------------------------------------------------------------------------
// Mode that caches rendered chunks of the grid area to avoid redrawing as a user revisits the
// same area of the grid without having changed anything.  Currently incomplete.

// === cacheDOM mode limitations
// - can't have fixedRecordHeights:false
// - does not support row animation
// - doesn't work with rowSpans
// - shouldn't use with full-row inline edit and large number of columns
// - doesn't support startSpace / endSpace



getRowChunkNum : function (logicalRowNum) {
    return Math.round(logicalRowNum / this._rowChunkSize);
},

getColChunkNum : function (logicalColNum) {
    return Math.round(logicalColNum / this._colChunkSize);
},

getTableChunk : function (rowChunkNum, colChunkNum) {
    var tableCache = this._tableCache;
    if (!tableCache) return;

    // if row and col are unpassed, return the chunk at 0,0
    rowChunkNum = rowChunkNum || 0;
    colChunkNum = colChunkNum || 0;

    var colCache = tableCache[rowChunkNum];
    return colCache ? colCache[colChunkNum] : null;
},

getTableChunkAt : function (logicalRowNum, logicalColNum) {
    var rowChunkNum = this.getRowChunkNum(logicalRowNum),
        colChunkNum = this.getColChunkNum(logicalColNum),
        tableElem = this.getTableChunk(rowChunkNum, colChunkNum);

    if (tableElem != null) {
        // semi-hack: set the offsets used in getTableElement() to find physical cells from
        // logical cells
        this._firstDrawRow = rowChunkNum * this._rowChunkSize;
        this._firstDrawnCol = colChunkNum * this._colChunkSize;
        return tableElem;
    }
},

// We need to ensure the table cache is clear after we've reset our inner HTML
// - otherwise we'll be pointing to stale elements.
// Clear the cache before we update inner HTML and
// also set a flag so any inadvertant calls to code that would re-cache coming
// from getInnerHTML will not re-cache stale elements.
// note: _updateInnerHTML is called if we have no children, otherwise
// _updateParentHTML updates the innerHTML

_updateInnerHTML : function (a,b,c,d) {
    if (this.cacheDOM) {
        this.drawVisibleChunks();
    } else {
      this._clearTableCache();
      this._suppressTableCaching = true;

       this.invokeSuper(isc.GridRenderer, "_updateInnerHTML", a,b,c,d);

       delete this._suppressTableCaching;
    }
},

_updateParentHTML : function (a,b,c,d) {
    this._clearTableCache();
    this._suppressTableCaching = true;
    this.invokeSuper(isc.GridRenderer, "_updateParentHTML", a,b,c,d);
    delete this._suppressTableCaching;
},

// in cacheDOM mode, this is called in lieu of normal redraw
drawVisibleChunks : function () {
    // figure out what undrawn chunks are visible and draw them
    var visibleRows = this.getVisibleRows(),
        visibleCols = this.getVisibleColumns(),
        startRowChunk = this.getRowChunkNum(visibleRows[0]),
        startColChunk = this.getColChunkNum(visibleCols[0]),
        endRowChunk = this.getRowChunkNum(visibleRows[1]),
        endColChunk = this.getColChunkNum(visibleCols[1]);

    for (var rowChunk = startRowChunk; rowChunk < endRowChunk; rowChunk++) {
        for (var colChunk = startColChunk; colChunk < endColChunk; colChunk++) {
            if (this.getTableChunk(rowChunk, colChunk) == null) {
                this.logWarn("drawing chunk: " + [rowChunk, colChunk]);
                this.renderTableChunk(rowChunk, colChunk);
            }
        }
    }

    var newHTML = this.getTableHTML();
},

renderTableChunk : function (rowChunkNum, colChunkNum) {
    // figure out geometry of table to draw
    var startRow = rowChunkNum * this._rowChunkSize,
        endRow = startRow + this._rowChunkSize,
        startCol = colChunkNum * this._colChunkSize,
        endCol = startCol + this._colChunkSize;

    // draw new table chunk
    var html = this.getTableHTML([startCol, endCol], startRow, endRow),
        tableElem = isc.Element.insertAdjacentHTML(this.getHandle(), "beforeEnd", html, true);

    //this.logWarn("html form chunk: " + [rowChunkNum, colChunkNum] +
    //             "\n" + html +
    //             "\nelement: " + this.echo(tableElem));

    // cache the table element
    var tableCache = this._tableCache = this._tableCache || [],
        colCache = tableCache[rowChunkNum] = tableCache[rowChunkNum] || [];
    colCache[colChunkNum] = tableElem;
},

//>Animation Row Animation support
// ---------------------------------------------------------------------------------------
// Methods to animate a show / hide of multiple rows

//> @method gridRenderer.startRowAnimation()
// Animates a show / hide of rows by growing the rows into view.
// Note: the rows to be shown/hidden should already be in the data, and the calling function
// is responsible for any manipulation to the data / redraw at the end of this method.
// @param show (boolean) are we showing or hiding rows?
// @param startRow (number) first row in range to be shown/hidden
// @param endRow (number) last row in range to be shown/hidden
// @param [callback] (callback) callback to fire when animation completes
// @param [speed] (number) speed for the animation in pixels / second
// @param [duration] (number) if speed is not set, number of milliseconds for the animation to take
// @param [effect] (string) optional acceleration effect for the animation
// @param [slideIn] (boolean) if specified, the rows will appear to slide into view rather than
//                            being revealed
//<
// additional param indicates this was called from the listGrid - we use this to ensure
// we fire the callback in the listGrid's scope
startRowAnimation : function (show, startRow, endRow, callback, speed, duration, effect, slideIn,
                              fromListGrid, isDelayed) {
    // Always call finishRowAnimation - this will no op if there is no current/pending
    // row animation in progress
    this.finishRowAnimation();
    if (!this.isDrawn() || !this.isVisible()) {
        if (callback != null) {
            var target = fromListGrid ? this.parentElement : this;
            target.fireCallback(callback);
        }
        return;
    }
    if (show == null) show = true;
    if (startRow == null) startRow = 0;
    if (endRow == null) endRow = this.getTotalRows() - 1;

    if (startRow == endRow) {
        this.logWarn("startRowAnimation passed empty row range, aborting: " +
                     [startRow, endRow]);
        return;
    }


    var canRedraw = this.readyToRedraw("animating show / hide of rows", false);
    if (!canRedraw) {
        this._delayedRowAnimationArgs = [show, startRow, endRow, callback, speed,
                                         duration, effect, slideIn, fromListGrid];
        this._delayedRowAnimation = isc.Timer.setTimeout(
                                        {target:this, methodName:"_delayedStartRowAnimation"},
                                        0
                                    );
        return;
    }


    // redraw, placing an entire subtable with the rows to be animated inside a single row, and
    // measure the size of the rows we're going to reveal or hide


    // When doing a 'slide-in' animation, we have to write out every row to be revealed since
    // they'll all scroll past the user's nose.
    // Have a check for this being a huge number of rows so we don't hit performance issues
    // generating this initial HTML!
    if ((endRow-startRow) > this.maxAnimateSlideInRows) slideIn = false;
    // set up the this._slideInAnimationRows attribute - this allows us to determine
    // whether we need to write out every row in the fragment
    this._slideInAnimationRows = slideIn;
    var fragmentHeight = this._initializeShowHideRow(show, startRow, endRow, callback, fromListGrid);

    // Use animateRowHeight to grow or shrink the height
    this.animateRowHeight(this._animatedShowStartRow,
                         // NOTE: animate all the way down to zero, so there is no lurch
                         // between the final frame of the animation and the subsequent redraw
                         (show ? fragmentHeight : 0),
                         {target:this, methodName:"_rowShowComplete"},
                         speed, duration, effect, slideIn);
},

// maximum number of rows to be animated into view using a 'slide' animation
// this kind of animation requires every row be rendered so we limit this to ensure
// we don't hit a performance roadblock writing out thousands of records' HTML!
maxAnimateSlideInRows:100,


// Helper to start delayed row animation
_delayedStartRowAnimation : function () {

    if (this._delayedRowAnimationArgs == null) {
        this.logWarn("Unable to perform delayed row animation - bailing");
        return;
    }

    var argsArr = this._delayedRowAnimationArgs,
        show = argsArr[0],
        startRow=argsArr[1],
        endRow = argsArr[2],
        callback = argsArr[3],
        speed = argsArr[4],
        duration = argsArr[5],
        effect = argsArr[6],
        slideIn = argsArr[7],
        fromListGrid = argsArr[8];

    this._delayedRowAnimationArgs = null;
    this._delayedRowAnimation = null;
    // The additional param indicates that the row animation is delayed

    this.startRowAnimation(show, startRow, endRow, callback, speed, duration, effect, slideIn,
                           fromListGrid, true);
},


// helper method to redraw the GR in its state at the beginning of the show/hide row animation
// Returns the height of the table fragment to be written into the animation row.
_initializeShowHideRow : function (show, startRow, endRow, callback, fromListGrid) {

    // If we've already been called and performed a redraw to set up the animated row table,
    // just return the height of the rows to animate
    // This allows us to separate the initial redraw (rendering the animation rows in a clippable
    // div) from the row animation.

    var fragmentHeight = 0;
    if (this._animatedShowStartRow == startRow && this._animatedShowEndRow == endRow) {

        // check the anmiation cell only
        var animationCell = this.getTableElement(this._animatedShowStartRow, 0),
            clipDiv = this._getCellClipDiv(animationCell);

        if (!clipDiv) {
            fragmentHeight = (endRow - startRow) * this.cellHeight;
        } else fragmentHeight = clipDiv.scrollHeight;

    } else {

        // hang a flag onto this table so we know where the fragment gets written into the normal
        // table.
        this._animatedShowStartRow = startRow;
        this._animatedShowEndRow = endRow;

        // if we're hiding visible rows, we can look at their drawn heights now
        if (!show) {
            var heights = this._getDrawnRowHeights();
            for (var i = startRow; i < endRow; i++) {
                fragmentHeight += heights[i];
            }

            // used when writing out the row containing the fragment
            this._animatedShowRowHeight = fragmentHeight;
            // This redraw writes out the single animation row with an entire table inside it
            this.redraw("initializing animated hide row");

        // In this case we're going to show rows that are currently undrawn.
        } else {
            this._animatedShowRowHeight = 1;
            this.redraw("initializing animated show row");
            // At this point we have written out the fragment and it's clipped by the containing
            // cell / div
            var animationCell = this.getTableElement(this._animatedShowStartRow, 0),
                clipDiv = this._getCellClipDiv(animationCell);

            if (!clipDiv) {
                fragmentHeight = (endRow - startRow) * this.cellHeight;
            } else fragmentHeight = clipDiv.scrollHeight;

        }

        if (this.isDirty()) this.redraw("Initializing row animation requires second redraw");
    }

    this._animatedShowCallback = {callback:callback,
                                  target:(fromListGrid ? this.parentElement : this)};

    return fragmentHeight;
},

// finishRowAnimation - synchronously short-cut to the end of the current row show/hide
// animation, and fire the callback.
finishRowAnimation : function () {
    // a currently running rowAnimation (show/hide rows) implies a running rowHeightAnimation -
    // finishing that will jump to the approprate size and fire the callback to finish the
    // show/hide animation
    if (this._animatedShowStartRow != null) {
        this.finishAnimateRowHeight();

    } else {
        // In this case we're not running a show/hide row animation - but we may have set up
        // a delayed one

        if (this._delayedRowAnimation != null) {

            // don't fire the delayed animation
            isc.Timer.clearTimeout(this._delayedRowAnimation);

            var args = this._delayedRowAnimationArgs,
                show = args[0], startRow = args[1], endRow = args[2],
                callback = args[3], duration = args[4], fromListGrid = args[5];

            delete this._delayedRowAnimationArgs;
            delete this._delayedRowAnimation;

            if (!this.readyToRedraw()) {
                this.logWarn("Finish row animation called while Grid is not ready to redraw. " +
                             "GridRenderer HTML will not be updated when callback fires.", "animation");
                var target = fromListGrid ? this.parentElement : this;
                if (callback) target.fireCallback(callback);

            } else {
                // redraw the GR with the single animation row containing the table fragment
                // at the start of the animation height
                var fragmentHeight = this._initializeShowHideRow(show, startRow, endRow, callback, fromListGrid);
                // set the height to the final height of that row and fire the 'complete' method
                // to fire callbacks / clean up vars (rather than ever performing an animation
                this.setRowHeight(startRow, (show ? fragmentHeight : 1));
                this._rowShowComplete();
            }
        }
    }
},

// Fired when animated show / hide of rows completes.
_rowShowComplete : function () {
    var callback = this._animatedShowCallback;
    delete this._animatedShowCallback;
    delete this._animatedShowStartRow;
    delete this._animatedShowEndRow;
    delete this._animatedShowRowHeight;
    // We stored the callback as an object {target:... callback:...}
    // This allows us to fire the callback on the ListGrid if that's where the method was
    // originally called from
    if (callback && callback.callback) callback.target.fireCallback(callback.callback);
},

//> @method gridRenderer.animateRowHeight()
// Will animate a resize of a row to the specified height, firing a callback when the resize
// is complete
// @param rowNum (number) Row to resize
// @param toHeight (number) new height for the row
// @param [callback] (callback) Callback to fire when animation completes
// @param [speed] (number) Speed for the animation (pixels / second)
// @param [duration] (number) duration of the resize in ms
// @param [effect] (string) Optionally an acceleration effect can be specified - if not specified
//                          default is to do a smooth animation (no acceleration)
// @param [slideIn] (boolean) if specified, rows will appear to slide into view rather than
//                            being revealed
//<
// Additional param 'fromListGrid' indicates this was fired from the ListGrid, so we should
// fire the callback in that scope
_$none:"none",
animateRowHeight : function (rowNum, toHeight, callback, speed, duration, effect,
                             slideIn, fromListGrid) {
    // If we're not drawn, no need to try to animate since this is a visual update only
    if (!this.isDrawn()) {
        if (callback) {
            var target = (fromListGrid ? this.parentElement : this);
            target.fireCallback(callback);
        }
        return;
    }

    // simultaneous row height animations not currently supported
    if (this._rowHeightAnimation != null) {
        this.logInfo("early finish of row animation, because new animation started",
                     "animation")
        this.finishAnimateRowHeight();
    }

    var fromHeight = this.getRowSize(rowNum);

    // If speed (pixels / second) is specified, it takes precedence over duration
    if (speed != null) {
        var change = (toHeight - fromHeight);
        if (change < 0) change = 0 - change;

        duration = Math.round((change / speed) * 1000);
        // Don't let the animation exceed a maximum
        if (duration > this.animateRowsMaxTime) duration = this.animateRowsMaxTime;
    }

    this._rowAnimationInfo = {
        _rowNum:rowNum,
        _fromHeight:fromHeight,
        _toHeight:toHeight,
        _callback:callback,
        _slideIn:slideIn,
        _fromList:fromListGrid
    }

    effect = (effect || this._$none);
    if (this.logIsInfoEnabled("animation")) {
        this.logInfo("starting row animation, row:" + rowNum  +
                    ", duration: " + duration + ", effect: " + effect,
                     "animation")
    }
    this._rowHeightAnimation = this.registerAnimation(
                                    {target:this, method:this._fireRowAnimation},
                                    duration, effect
                               );
    // suppress adjustOverflow until the row animation completes. This will avoid unnecessary
    // scrollbars from showing up
    if (this.overflow == isc.Canvas.AUTO || this.overflow == isc.Canvas.SCROLL)
        this._suppressAdjustOverflow = true;
},

_fireRowAnimation : function (ratio) {

    var info = this._rowAnimationInfo;

    if (info == null) return;

    var rowNum = info._rowNum,
        rowHeight = this._getRatioTargetValue(info._fromHeight, info._toHeight, ratio);

    if (isc.Browser.isSafari && info._fromHeight > info._toHeight)
        this._forceRowRefreshForAnimation = true;
    // pass in explict "" as className so we don't adjust sizing for the standard row styling
    // (which won't be applied to this row during animation)
    this.setRowHeight(rowNum, rowHeight, null, isc.emptyString, true, true, true);
    if (isc.Browser.isSafari) delete this._forceRowRefreshForAnimation;

    if (info._slideIn) {
        var clipDiv = this._getCellClipDiv(this.getTableElement(rowNum,0));
        if (clipDiv) {
            var scrollHeight = clipDiv.scrollHeight,
                offsetHeight = clipDiv.offsetHeight;
            if (scrollHeight > offsetHeight) clipDiv.scrollTop = scrollHeight - offsetHeight;
            else clipDiv.scrollTop = 0;
        }
    }

    // Fire the completion callback in a separate thread - this means if it does a lot of
    // processing we shouldn't see a visual pause before the native repaint at the full-size
    if (ratio == 1) {
        isc.Timer.setTimeout({target:this, methodName:"_rowAnimationComplete"}, 0);
    }
},

// Fired when we're done with a row resize animation
_rowAnimationComplete : function () {
    // In screen reader mode, after a row animation completes, we'll want to put the focus
    // back on the last focus row, unless some other widget was focused while the row animation
    // was running.
    if (isc.screenReader) {
        var focusCanvas = this.ns.EH.getFocusCanvas();
        this._putNativeFocusInRow(this.getNativeFocusRow(), focusCanvas != null && focusCanvas !== this);
    }

    // allow standard adjustOverflow to resume
    delete this._suppressAdjustOverflow;
    this.adjustOverflow("row animation complete");


    var info = this._rowAnimationInfo;

    delete this._rowHeightAnimation;
    delete this._rowAnimationInfo;

    if (info && info._callback) {
        var target = info._fromList ? this.parentElement : this;
        target.fireCallback(info._callback);
    }
},


//> @method gridRenderer.finishAnimatingRowHeight()
// Completes any row height animation currently in progress and fires the callback from that
// animation.<br>
// May be fired automatically to avoid (unsupported) overlapping animations, etc.
//<
// Leave this as unexposed for now
finishAnimateRowHeight : function () {

    if (!this._rowHeightAnimation) return;

    // cancel upcoming animation cycles
    this.cancelAnimation(this._rowHeightAnimation);

    // Simply firing the "last step" of the rowHeight animation will jump to the appropriate
    // height and fire the callback

    this._fireRowAnimation(1);
},

//<Animation

// When printing we need to write out embedded components' printHTML directly in our table HTML
_getPrintChildren : function () {
    return this._embeddedComponents;
},


// returns the tableHTML for printing.
// Used direclty by ListGrid.getPrintHTML()

getTablePrintHTML : function (colNum, startRow, endRow, discreteCols, asyncCallback) {
    return this.getTableHTML(colNum, startRow, endRow, discreteCols, asyncCallback);
},



// Should we suppress focus in the widget handle entirely in favor of focussing in rows when
// in screenReader mode?
screenReader_suppressHandleFocus:true,

draw : function () {
    // don't write tabIndex/ focus/blur handlers onto the handle if we're
    // going to write them onto a row element.
    if (isc.screenReader && this.screenReader_suppressHandleFocus) {
        this.clipHandleIsFocusHandle = this.isEmpty();
    }
    return this.Super("draw", arguments);
},


handleKeyDown : function (event, eventInfo) {
    var rv = this.Super("handleKeyDown", arguments);
    if (rv != false && isc.screenReader && this.screenReader_suppressHandleFocus) {
        var mask = isc.EH.clickMaskUp(),
            hardMaskUp = false;
        if (mask) {
            var masks = isc.EH.clickMaskRegistry;
            for (var i = 0; i < masks.length; i++) {
                if (isc.EH.isHardMask(masks[i])) {
                    hardMaskUp = true;
                    break;
                }
            }
        }
        if (!hardMaskUp) {
            var keyName = event.keyName;
            if (keyName == "Tab") {
                this._focusInNextTabElement(!isc.EH.shiftKeyDown())
                return false;
            }
        }
    }
    return rv;
},

// allowRowSpanning: If set to false we never call getRowSpan even if the method is present.
// This is a grid passthrough attribute - defaulted to false at the ListGrid level.

allowRowSpanning:true,


closeNOBRs:false,

_getScreenReaderCellSeparatorID : function () {
    return this._getDOMID("screenReaderCellSeparator");
},
_getScreenReaderRowSeparatorID : function () {
    return this._getDOMID("screenReaderRowSeparator");
},

// returns the innerHTML for the table
// If passed a startRow / endRow, it will return just the HTML for that fragment of the table.
// asyncCallback / isAsync is required for printing only. This allows us to handle the
// embedded components generating their printHTML asynchronously
getTableHTML : function (colNum, startRow, endRow, discreteCols, asyncCallback, isAsync) {
    if (isc._traceMarkers) arguments.__this = this;
    //>DEBUG
    // timing
    var t0 = isc.timeStamp();
    //<DEBUG

    // show empty message
    if (this.isEmpty()) {
        // clear drawn area
        this._firstDrawnRow = this._lastDrawnRow =
                this._firstDrawnCol = this._lastDrawnCol = null;

        // note that if we're printing, showEmptyMessage handles embedding the
        // printHeaders / printFooters directly in the generated message text.
        return this._showEmptyMessage();
    }

    // If we're printing and we have embedded components we need to get their printHTML
    // and plug it into the cells directly
    if (this.isPrinting && (!this._printingChunk || startRow == 0)) {

        var printComponents = this._getPrintChildren();

        if (printComponents != null && printComponents.length > 0) {

            for (var i = 0; i < printComponents.length; i++) {
                var component = printComponents[i];
                if (component._gridBodyPrintHTML != null) continue;
                var printComponentContext = {
                        component:component,
                        colNum:colNum, startRow:startRow, endRow:endRow, descreteCols:discreteCols,
                        asyncCallback:asyncCallback
                    };
                // this.logWarn("calling getPrintHTML on embedded component:" + printComponents[i]);
                var printCHTML = printComponents[i].getPrintHTML(
                                    this.printProperties,
                                    asyncCallback == null ? null
                                        : {target:this, methodName:"gotComponentPrintHTML",
                                            context:printComponentContext}
                                 );
                if (printCHTML != null) {
                    component._gridBodyPrintHTML = printCHTML;
                } else {
                    // If the printComponent generates its printHTML asynchronously,
                    // we will be notified and fire the gotComponentPrintHTML() method
                    // when that returns, at which point we'll re-run this method, skipping
                    // the component(s) for which we already have HTML and ultimately firing
                    // the asyncCallback.
//                    this.logWarn("GR.getTableHTML() - getPrintHTML for component:" +
//                        component + " went asynchronous", "printing");
                    return null;
                }
            }
        }

        // at this point we've generated HTML for all our print components and stored it
        // We'll write it directly into the cells / rows below.
    }

    var fragment = (startRow != null && endRow != null),
        rangeStart = startRow != null ? startRow : 0,
        rangeEnd = endRow != null ? endRow : this.getTotalRows();

    // Figure out rows and columns to actually draw
    // ----------------------------------------------------------------------------------------

    var drawRect = this._getTableHTMLDrawArea(startRow, endRow, true),
        grid = this.grid;
    startRow = drawRect[0];
    endRow = drawRect[1];

    if (!fragment) {
        // If virtualScrolling is enabled, turn it on unless we're showing all rows.
        var showingAllRows = (startRow == 0 && endRow == this.getTotalRows());
        if (this.virtualScrolling) {
           this._isVirtualScrolling = !showingAllRows;
        }
        // if we've stopped virtual scrolling, clean up those pointers
        if (!this._isVirtualScrolling) {
            // _targetRow / _rowRatio is used when virtual scrolling to ensure redraws maintain
            // apparant scroll position.
            delete this._targetRow;
            delete this._rowRatio;
            // _scrollRatio is similarly used when doing a scrollToRatio
            delete this._scrollRatio;
            // _viewRatioHeight is used to determine scrollbar thumb size when virtual scrolling.
            // (We don't technically need to clear this - it's ignored if _isVirtualScrolling is
            // false and would be reset when virtual scrolling was reintroduced)
            delete this._viewRatioHeight
        }
    }

    // always refresh _firstDrawnCol / _lastDrawnCol
    // This may be required even if we're rendering a fragment as  in some cases we
    // asynchronously
    // fetch fragments (EG printing HTML) and the rendered area may have changed (EG
    // shouldPrint:false fields)
    if (!this._gettingAutoSizeHTML) {
    this._firstDrawnCol = drawRect[2];
    this._lastDrawnCol = drawRect[3];
    }

    // colNum can be passed to render one column only - used for auto-sizing
    // or if passed an array can specify a specific set of columns - used for
    // rendering an entire row (without spacers), for (EG) showing row HTML as a drag-tracker
    // discreteCols parameter implies the colNum array passed in is a set of specific cols
    // to render (used when we're determining auto-size of a set of discontiguous columns) -
    // in this case startCol / endCol aren't actually going to be used
    var colsArray = colNum != null && isc.isAn.Array(colNum),
        startCol, endCol;
    if (!colsArray) discreteCols = false;

    if (colNum != null) {
        if (colsArray) {
            startCol = colNum[0];
            endCol = colNum[1] + 1;
         } else {
            startCol = colNum;
            endCol = colNum +1;
        }
    } else {
        startCol = this._firstDrawnCol;
        endCol = this._lastDrawnCol + 1;
    }

    var colNums;
    if (discreteCols) colNums = colNum;
    else {
        colNums = [];
        for (var i = startCol; i < endCol; i++) {
            colNums[colNums.length] = i;
        }
    }

    // total columns we'll be drawing, for colSpans
    var numCols = colNums.length;

    // if "colNum" has been passed such that we are returning the HTML for just one column, we
    // are essentially in showAllColumns mode in the sense that we don't want to adding
    // padding/margins to compensate for unrendered columns
    var showAllColumns = (this.shouldShowAllColumns() || colNum != null);

    // Draw
    // ---------------------------------------------------------------------------------------

    var output = isc.StringBuffer.create(),
        fields = this.fields,
        sizes = this._fieldWidths;

    // remember the specified width of the first column when we draw.  This helps us prevent
    // unnecessary redraw on resize; see setColumnWidths()
    this._colWidthAtDraw = colNums[0] != 0 ? null : this._fieldWidths[0];

    var leftColPad, rightColPad, totalHorizontalWidth, padType;
    if (!showAllColumns || this.leftSpace != null || this.rightSpace != null) {
        leftColPad = (this.leftSpace != null) ? this.leftSpace : 0;
        rightColPad = (this.rightSpace != null) ? this.rightSpace : 0;

        // figure out size of columns to left and right of visible area
        if (!showAllColumns) {
            leftColPad += this._fieldWidths.slice(0, startCol).sum();
            rightColPad += this._fieldWidths.slice(endCol, this._fieldWidths.length).sum();
        }
        totalHorizontalWidth = this._fieldWidths.sum()

        //this.logWarn("column pads: " + [leftColPad, rightColPad] + " type:" + padType);
        padType = (this.cacheDOM || (isc.Browser.isIE && !(isc.Browser.isIE8 || isc.Browser.isIE9))
            ? "margin" : "padding");
        //padType = "padding";
    }
    var autoFit = this.autoFit;

    var widthHTML = "";
    if (colNum != null) {
        if (!autoFit && this.fixedColumnWidths) {
            // if rendering just one column, size it to 100% of it's containing Canvas, since
            // the Canvas will be sized to the column width
            widthHTML = " WIDTH=100%";
        }
    } else if (this.isPrinting && this.autoFit) {
        // when printing, autoFit should mean full screen
        widthHTML = " WIDTH=100%";

    } else if ((isc.Browser.isIE8Strict || isc.Browser.isMoz || isc.Browser.isSafari)
                && !autoFit)
    {
        // total size of the table we're drawing (NOTE: may be larger or smaller than the body
        // Canvas, since the body Canvas is a viewport on to this table)
        var tableWidth = this._fieldWidths.slice(startCol, endCol).sum();

        widthHTML = " WIDTH=" + tableWidth;
    }

    // output a blank spacer in a DIV that is as tall as all the records before the table.
    // This causes the scrollable area to be as large as if we were drawing all records,
    // so the thumb is the correct size and scrolling works as expected.

    // In some cases we explicitly specify additional space to show above / below the
    // rows in a GridRenderer (this.startSpace / endSpace)
    // If this.startSpace is non null, add it to the calculated height of the undrawn
    // start rows.
    // Note that in this case the range of rows is shifted down - already handled by
    // _getViewportFillRows
    var startSpacerHeight = this.startSpace || 0;
    if (fragment) startSpacerHeight = 0;
    if (startRow != rangeStart) {
        var undrawnRowHeight = ((startRow - rangeStart) * this.getAvgRowHeight());
        this._startRowSpacerHeight = undrawnRowHeight;
        startSpacerHeight += undrawnRowHeight;
    } else {
        this._startRowSpacerHeight = 0;
    }



    var canResizeSpacerDivs = true;
    var totalHeight = (rangeEnd - rangeStart) * this.getAvgRowHeight();
    if (isc.Browser.isIE) {
        if (totalHeight > 1300000) canResizeSpacerDivs = false;
    }
    if (!fragment) this._canResizeSpacerDivs = canResizeSpacerDivs;


    if (totalHeight > 10000000) {
        this.logWarn("This grid is showing " + (rangeEnd - rangeStart).toLocalizedString()
            + " rows. Due to native rendering limitations, grids with this many rows"
            + " may not appear correctly on all browsers. Consider filtering the data"
            + " displayed to the user to reduce the total number of rows displayed at a time."
            + " This will improve usability as well as avoiding unpredictable behavior.");
    }

    if (!this.cacheDOM && !this.isPrinting) {
        // If the space is zero sized, we still want to write out the spacer div so we can handle
        // setStartSpace() etc without a redraw
        // In IE specifying the height as zero px won't work, so set display none instead to ensure
        // the spacer takes up no space
        // Give the spacer DIV an ID so we can look at it's height, etc. later.
        // When we resize this on the fly (in setStartSpace()) we'll set display back to the default
        // (inline) if necessary.

         output.append("<DIV style='width:1px;");
         if (canResizeSpacerDivs) {
             output.append("height:", startSpacerHeight, "px;overflow:hidden;");
         }
         if (startSpacerHeight == 0) output.append("display:none;");
         output.append("' ");

         if (fragment || this.isPrinting) {
             output.append(">");
         } else {
             output.append(" ID="+ this.getID()+ "_topSpacer>");
         }
         output.append(isc.Canvas.spacerHTML(1, startSpacerHeight), "</DIV>");
    }

    //
    //    output the start table tag
    //
    // XXX: If height of the list is screwy in IE5 until the cursor passes over it,
    //            we should set the height of the table explicitly
    // Note: We divide large tables into chunks so we can assemble the HTML in separate threads
    // (avoids script is running slowly message). Avoid writing out the outer table tags for every
    // chunk so the HTML ends up in a single table.
    if (!this._printingChunk || (startRow == 0 && !this.printChunkOnly)) {
        if (isc.screenReader) {
            output.append("<div id='", this._getScreenReaderCellSeparatorID(), "' style='display:none'>",
                          this.screenReaderCellSeparator, "</div>",
                          "<div id='", this._getScreenReaderRowSeparatorID(), "' style='display:none'>",
                          this.screenReaderRowSeparator, "</div>");
        }
        output.append(

            "<TABLE", (isc.Browser.isIE && isc.screenReader ? " unselectable='on'" : null), " role='presentation' BORDER=0",
            widthHTML,
            ((!fragment && !this.isPrinting) ? " ID=" + this.getTableElementId() : null),
            (this.tableStyle && isc.Browser.isDOM ?
             " CLASS='" + this.tableStyle + this._$singleQuote : isc._emptyString),
            " CELLSPACING=" , this.cellSpacing,
            " CELLPADDING=" , this.cellPadding,
            " STYLE='",

            (isc.Browser.isDOM && !autoFit && this.fixedColumnWidths ?
             "table-layout:fixed;overflow:hidden;" : ""),

            (!showAllColumns ?
                padType + (this.isRTL() ? "-right: " : "-left:") + leftColPad + "px;" +
                padType + (this.isRTL() ? "-left:" : "-right:") + rightColPad + "px;"
            : ""),


            (this.cacheDOM && this._startRowSpacerHeight > 0
                ? "margin-top:" + this._startRowSpacerHeight + "px;" : ""),

            // if we plan to scroll immediately after draw, draw the table as hidden, so we don't
            // momentarily see it in the wrong scroll position
            (this._targetRow != null && !(isc.Browser.isIE && this._avoidRedrawFlash)
                && !this.isPrinting ? "visibility:hidden;" : ""),
            "'>",

            (isc.Browser.isMoz ? "<TBODY></TBODY>" : "")
        );


        var vPad = 0, hPad = 0,
            // get style we'll use on the first record, used for sizing calculations
            firstRecordStyle = this._getFirstRecordStyle();

        // In strict mode / HTML5 mode we have to knock off specified vertical padding/border
        // to get the height to write into table cells

        if (isc.Browser.isStrict &&
            ((isc.Browser.isMoz && isc.Browser.version >= 17) ||
             (isc.Browser.isSafari || isc.Browser.isIE)))
        {

            if ((isc.Browser.isIE && isc.Browser.version < 8) ||
                (isc.Browser.isSafari && isc.Browser.safariVersion < 530))
            {
                hPad = this._getCellHBorderPad();
            }

            vPad = (this.fixedRowHeights ? 0 : this.cellPadding * 2);
            vPad += isc.Element._getVBorderSize(firstRecordStyle);
        }
        // store pad amounts since they are needed on cell refresh
        this._vPad = vPad;
        this._hPad = hPad;

        output.append("<colgroup>");


        if (!autoFit && isc.Browser.isDOM) {

            for (var i = 0; i < colNums.length; i++) {
                output.append("<COL WIDTH=" , (sizes[colNums[i]] - hPad), ">");
            }
        }

        output.append("</colgroup>");


        output.append("<TBODY>");
    }


    var cellHeight = this.cellHeight,
        // Do we need to write a DIV into the cell (See comments in _writeDiv())
        writeDiv = this._writeDiv(cellHeight),
        nowrap = !this.wrapCells && !isc.Browser.isIE8Strict,
        skipNOBR = !nowrap || writeDiv,
        cellWrapHTML = (skipNOBR ? "" : "<NOBR>"),
        cellWrapHTMLClose = (!this.closeNOBRs || skipNOBR ? "" : "</NOBR>")
    ;

    var singleCells = 0;

    // Draw rows
    // --------------------------------------------------------------------------------------------
    if (isc.Browser.isDOM) {





        // template of cell HTML
        var cellHTML = [],
            ariaSlot = 1, heightAttrSlot = 2, heightSlot = 3, alignSlot = 5,
            valignAttrSlot = 6, valignSlot = 7, widthSlot = 8,
            minHeightCSSSlot = 10, cssStartSlot = 11, styleSlot = 18,
            cellIDSlot, cellIDs, divStartSlot = 21, cellValueSlot = 24;
        cellHTML[0] = "<TD";

        if (isc.Browser.isIE && isc.screenReader) cellHTML[0] += " unselectable='on'";
        // [1] ARIA attributes if enabled
        // [2] height attribute, if set (per row)
        // [3] height value, if set (per row)
        cellHTML[4] = " ALIGN=";
        // [5] align (per col) and rowSpan (per cell) if necessary
        // [6] valign attribbute, if set (per cell)
        // [7] valign value, if set (per cell)
        // [8] width (per col) OR colspan when drawing a one-cell row (per row).
        //     Ends with an open STYLE='
        //cellHTML[9] = ";filter:Alpha(opacity=100);";
        // [gap]
        // [10] min-height css text - used for rows with shouldFixRowHeight() == false where
        //     this.fixedRowHeights as a whole is true
        // [11] cssText range start (per cell)
        // [12] used if this.fastCellUpdates is true!
        // [gap]
        cellHTML[16] = skipNOBR && !this.wrapCells ? ";white-space: nowrap;" : null;
        // [17] fastCellUpdates: close STYLE attribute; no CLASS attribute will be written
        // [17] normal: close STYLE attribute, start CLASS attribute
        cellHTML[17] = this.fastCellUpdates ? "' " : "' class=";
        // [18] cell style (when not using fastCellUpdates)
        // don't write out cell element id's for fragments - it's possible that we'd end up with
        // duplicate IDs that way.
        if (!fragment && !this.isPrinting && (isc.screenReader || this.getCellElementId)) {
            cellHTML[19] = " id=";
            cellIDSlot = 20;
        }
        // [20] cell ID (per cell, optional)
        // [21] DIV start to force correct cellHeight, if necessary (per table)
        // [22] rest of DIV to force column width if writeDiv (per column)
        cellHTML[23] = ">" + cellWrapHTML; // ">" + wrap (per table)
        // [24] value range start (per cell)
        // [gap]
        cellHTML[30] = cellWrapHTMLClose + (writeDiv ? "</DIV></TD>" : "</TD>");

        var rowStart = "<TR",
            rowEnd = "</TR>",
            gt = ">",
            heightAttr = " HEIGHT=",
            valignAttr = " VALIGN=";

        if (grid && grid.canDragRecordsOut && grid.useNativeDrag) {
            rowStart += " draggable='true'";
        }


        if (isc.Browser.isMobileWebkit) rowStart += " onmousedown='return true;' style='-webkit-tap-highlight-color: rgba(0,0,0,0)'";

        // make row elements programmatically focuseable

        var tabIndexString;
        if (isc.screenReader) {
            tabIndexString = " tabIndex=-1";
        }
        // whether to write ARIA attributes
        var ariaEnabled = isc.Canvas.ariaEnabled();

        // these are used only when cells have rowSpans (only possible if getRowSpan() has been
        // defined)

        // colNum -> number of remaining cells to skip (for columns where a cell spans into the
        // current row)
        var cellSkips = [],
            // number of cells that will be skipped in this row.  Increased when spans start,
            // decreased when they end
            skipCount = 0,
            // colNum -> start row of rowSpanning cell (for columns where a cell spans into the
            // current row)
            cellSkipSourceRows = [];

        // rowSpanning: outerSpanCount is a map of logical rowNum -> count of number of DOM cells that
        // have actually been written into the first column - the "visual" row number if
        // spanning cells in the leftmost column are taken to define "rows".
        var outerSpanCount = this._outerSpanCount = {};

        this._cacheColumnHTML(colNums, autoFit, hPad, writeDiv);

        if (this.isPrinting && (!this._printingChunk || (startRow == 0 && !this.printChunkOnly))) {

            output.append(this.getPrintHeaders(startCol, endCol));
        }

        var isFrozenBody = this._isFrozenBody();
        if (cellIDSlot != null) {
            cellIDs = new Array(colNums.length);
        }
        // output each record in turn
        for (var rowNum = startRow; rowNum < endRow; rowNum++) {
            //>Animation

            var isAnimationRow = (!fragment && this._animatedShowStartRow == rowNum);
            //<Animation

            if (cellIDSlot != null) {
                var physicalRowNum = rowNum - startRow;
                for (var i = 0; i < colNums.length; ++i) {
                    var colNum = colNums[i],
                        physicalColNum = colNum - startCol;
                    if (this.getCellElementId) {
                        cellIDs[physicalColNum] = this.getCellElementId(rowNum, physicalRowNum, colNum, physicalColNum);
                    } else {

                        cellIDs[physicalColNum] = this.ID + "_"+"cell" + rowNum + "_" + colNum;
                    }
                }
            }

            // get a pointer to the record for this row.
            // NOTE: record can be null.  The various routines below (eg getCellValue) are
            // expected to handle this.
            var record = this.getCellRecord(rowNum);

            // If this row is a separator or is not loaded yet, we draw a single cell with
            // COLSPAN set to extend across the entire table.
            var drawRecordAsSingleCell = //>Animation
                                         isAnimationRow ||   //<Animation
                                         this._drawRecordAsSingleCell(rowNum, record);
            // start the table row
            output.append(rowStart);
            if (tabIndexString != null) {
                // Assign the correct tabIndex plus focus/blur handlers to the single row
                // that should get native focus if we're not allowing native focus to go to the handle
                if (this.screenReader_suppressHandleFocus) {
                    if (this.getNativeFocusRow() == rowNum) {
                        output.append(" tabIndex=", this.getTabIndex(),
                                    isc.Canvas._onFocus, this._getNativeFocusHandlerString(),
                                    isc.Canvas._onBlur, this._getNativeBlurHandlerString());
                    } else {
                        output.append(tabIndexString);
                    }
                } else {
                    output.append(tabIndexString);
                }
            }
            if (!fragment && !this.isPrinting  && this.getRowElementId) {
                output.append(" ID=", this.getRowElementId(rowNum, rowNum-startRow));
            }
            if (ariaEnabled) {
                var rowRole = this.getRowRole && this.getRowRole(rowNum, record);
                if (rowRole) output.append(" role='", rowRole, "'");

                // output attributes such as "selected"
                var rowState;
                if (this.getRowAriaState != null) {
                    rowState = this.getRowAriaState(rowNum, record);
                } else if (cellIDSlot != null && isc.screenReader) {
                    rowState = { labelledby: null };
                }
                if (rowState != null) {
                    if (cellIDSlot != null && isc.screenReader && rowState.labelledby == null) {
                        var labelledByIDs = cellIDs;

                        if (isc.isA.TreeGrid && isc.isA.TreeGrid(grid)) {
                            var treeFieldNum = grid._treeFieldNum,
                                treeFieldBody = grid.getFieldBody(treeFieldNum);
                            if (treeFieldBody === this) {
                                var localTreeColNum = grid.getLocalFieldNum(treeFieldNum);
                                if (startCol <= localTreeColNum && localTreeColNum < endCol) {
                                    labelledByIDs = cellIDs.duplicate();
                                    labelledByIDs[localTreeColNum - startCol] = grid._getTreeCellValueID(rowNum);
                                }
                            }
                        }
                        rowState.labelledby = (labelledByIDs.join(" " + this._getScreenReaderCellSeparatorID() + " ") + " " +
                                               this._getScreenReaderRowSeparatorID());
                    }
                    output.append(isc.Canvas.getAriaStateAttributes(rowState));
                }
            }
            output.append(gt);

            // set per-row pieces of cell HTML

            // establish row height to clip content (fixedRecordHeights:true) or as a minimum
            // (fixedRowHeights:false)

            // use the getRowHeight function if it's defined, otherwise use the cellHeight
            // property
            var rowHeight = //>Animation
                            isAnimationRow ? this._animatedShowRowHeight :
                            //<Animation
                            (this.getRowHeight != null ?
                             this.getRowHeight(record, rowNum, isFrozenBody) :
                             cellHeight);

            // If this widget has a 'shouldFixRowHeight()' method, check whether that returns
            // false (enables override of 'fixedRowHeights' on a per-row basis - currently
            // only used internally, for row-level editing of ListGrids)
            var fixedRowHeight;
            if (isAnimationRow) {
                fixedRowHeight = true;
            } else {
                fixedRowHeight = this.fixedRowHeights;


                if (fixedRowHeight && this.shouldFixRowHeight != null) {
                    fixedRowHeight = (this.shouldFixRowHeight(record, rowNum) != false);
                }
            }

            //this.logWarn("rowNum: " + rowNum +
            //             ", rowHeight: " + rowHeight +
            //             ", this.fixedRowHeights: " + this.fixedRowHeights +
            //             ", this row isFixed: " + fixedRowHeight);

            // If this row is of fixed height, write the height out into the TD
            if (fixedRowHeight) {
                // write a height attribute to enforce height
                cellHTML[heightAttrSlot] = heightAttr;
                cellHTML[heightSlot] = rowHeight - vPad;
                cellHTML[minHeightCSSSlot] = null;

            // If the row can expand with content, avoid writing a height into the TD -
            // use the min-height CSSText instead
            } else {
                // don't write a height attribute at all
                cellHTML[heightAttrSlot] = null;
                cellHTML[heightSlot] = null;

                // Apply min css height to per-cell css...
                if (!drawRecordAsSingleCell &&
                    rowHeight == this.cellHeight && !this.fixedRowHeights)
                {
                    // null it out, already handled by CSS that establishes cell height as a
                    // minimum (in the "widthHTML" slot)
                    // Note that if we're drawing as single cell, the widthHTML slot contains
                    // colspan instead, so in this case we need min height CSSText
                    cellHTML[minHeightCSSSlot] = null;
                } else {
                    cellHTML[minHeightCSSSlot] = this._getMinHeightCSSText(record,rowNum);
                }
            }

            if (writeDiv) {
                // this method returns css text to set the height for the DIV

                cellHTML[divStartSlot] = ">" + this._$cellClipDivStart +
                       this._getCellDivCSSHeight(rowHeight, record, rowNum, isAnimationRow);
                if (nowrap) cellHTML[divStartSlot] += "white-space:nowrap;";
            }

            // If we're drawing the record as a single cell, figure out which cells it's spanning

            var singleCellSpan = drawRecordAsSingleCell ?
                                    this._getSingleCellSpan(record,rowNum,startCol,endCol) : null;
            //if (skipCount > 0) {
            //    this.logWarn("rowSpan start rows for row: " + rowNum +
            //                 ": " + cellSkipSourceRows);
            //}

            // output each cell
            for (var i = 0; i < colNums.length; i++) {
                colNum = colNums[i];

                if (this.useCellRecords) record = this.getCellRecord(rowNum, colNum);

                var field = fields[colNum],
                    cellRecord = record;
                if (cellRecord == null) cellRecord = this.getCellRecord(rowNum, colNum);

                if (cellSkips[colNum] > 0) {
                    // this cell will be skipped due to a rowSpanning cell in a previous row

                    // record the start row of the rowSpanning cell
                    field._rowSpans[rowNum] = cellSkipSourceRows[colNum];
                    //this.logWarn("recording start row: " + field._rowSpans[rowNum] +
                    //             " at " + rowNum);

                    // copy down the rowNum as determine by spanning
                    if (colNum == 0) {
                        outerSpanCount[rowNum] = rowNum > 0 ? outerSpanCount[rowNum-1] : 0
                    }

                    // reduce the count of cells remaining to be skipped due to this rowSpanning
                    // cell
                    cellSkips[colNum]--;

                    if (cellSkips[colNum] == 0) {
                        // we don't have to skip any more cells due to this rowSpanning cell
                        skipCount--;
                        // so clear the colNum -> rowNum with rowSpan cell mapping
                        cellSkipSourceRows[colNum] = null;
                    }
                    continue;
                } else if (colNum == 0) {
                    // no spanning in left column, increment row-span based count
                    outerSpanCount[rowNum] = rowNum > 0 ? outerSpanCount[rowNum-1]+1 : 0
                }

                // per column HTML (align)
                cellHTML[alignSlot] = this.getCellAlign(record, field, rowNum, colNum);

                // per column valign
                var vAlign = this.getCellVAlign(record, field, rowNum, colNum);
                if (vAlign != null) {
                    cellHTML[valignAttrSlot] = valignAttr
                    cellHTML[valignSlot] = vAlign;
                }

                if (singleCellSpan != null && (colNum == singleCellSpan[0])) {
                    // note singleCells variable used for logging only
                    singleCells++;

                    // HTML to cause the cell to span several cells for
                    // drawRecordAsSingleCell case
                    cellHTML[widthSlot] = this._getTDSpanHTML(singleCellSpan[1]-singleCellSpan[0]);

                    // If we're writing out a DIV, we need to close the "'" around the style
                    if (writeDiv) {
                        cellHTML[divStartSlot + 1] = this._$singleQuote;
                    }

                    // We'll write out the rest of the HTML, then increment i to jump to the
                    // end of the span

                } else {
                    // per column HTML (width)
                    // XXX Actually a misnomer - this includes some height information too
                    cellHTML[widthSlot] = field._widthHTML;

                    // we have a row span function, write rowSpan into the table cell (after
                    // the specified width)
                    if (this.allowRowSpanning && this.getRowSpan) {
                        var rowSpan = this._getRowSpan(record, rowNum, colNum);
                        if (rowSpan > 1) {
                            var rowSpanText = " ROWSPAN=" + rowSpan;
                            // piggyback the rowSpan on the alignment slot
                            if (cellHTML[alignSlot] != null)
                                cellHTML[alignSlot] += rowSpanText;
                            else
                                cellHTML[alignSlot] = rowSpanText;
                            // set up to skip outputting cells in this column
                            cellSkips[colNum] = rowSpan - 1;
                            skipCount++;

                            // field._rowSpans:
                            // - is a map of rowNum -> starting rowNum of cell that spans into
                            //   that cell.
                            // - only contains rowSpanning cells; other slots have the value
                            //   undefined.
                            // - exists only on field objects where there are rowSpanning
                            //   cells
                            if (field._rowSpans == null) field._rowSpans = {};
                            field._rowSpans[rowNum] = rowNum;

                            // remember the start row of the rowSpanning cell
                            cellSkipSourceRows[colNum] = rowNum;

                            if (colNum == 0) {
                                outerSpanCount[rowNum] = rowNum > 0 ?
                                                outerSpanCount[rowNum-1]+1 : 0
                            }

                        }
                    }

                    cellHTML[ariaSlot] = null;
                    if (ariaEnabled) {
                        var cellRole = this.getCellRole && this.getCellRole(rowNum, colNum, record);
                        if (cellRole) {
                            cellHTML[ariaSlot] = " role='" + cellRole + "'";
                        }
                        var cellState = this.getCellAriaState && this.getCellAriaState(rowNum, colNum, record);
                        if (cellState != null) {
                            var cellStateAttributes = isc.Canvas.getAriaStateAttributes(cellState);
                            var currentAriaSlotValue = cellHTML[ariaSlot];
                            cellHTML[ariaSlot] = (currentAriaSlotValue == null
                                                  ? cellStateAttributes
                                                  : currentAriaSlotValue + cellStateAttributes);
                        }
                    }

                    if (writeDiv) {
                        // also closes the style= attribute
                        cellHTML[divStartSlot + 1] = field._divWidthHTML;
                    } else {
                        // Note - if we're not writing a DIV, we're not in the middle of a style
                        // attribute, so no need for "'"
                        cellHTML[divStartSlot + 1] = null;
                    }

                }

                // set per-cell pieces of cell HTML
                // -------------------------------------------------------------------------

                // cell style (CSS classname)
                var cellStyle = this.getCellStyle(record, rowNum, colNum),
                    // cell CSS text (direct value for STYLE attribute)
                    customCSSText = (this.getCellCSSText ?
                                     this.getCellCSSText(record, rowNum, colNum) :
                                     null);
                //>Animation
                // always have the animation row cell have no padding / border, since the
                // table written into it already has padding / border for each cell.
                if (isAnimationRow) {
                    var nopad = "padding:0px;border:0px;";
                    if (customCSSText) customCSSText += ";" + nopad
                    else customCSSText = nopad;
                }
                //<Animation



                if (!this.fastCellUpdates) {
                    // in normal mode, write CLASS=[CSS className] and write custom CSS text into a
                    // STYLE attribute
                    cellHTML[cssStartSlot] = customCSSText == null ? null : String.asAttValue(customCSSText);
                    cellHTML[styleSlot] = cellStyle;
                } else {

                    var styleText = this._getEscapedStyleText(cellStyle);
                    cellHTML[cssStartSlot] = styleText;
                    cellHTML[cssStartSlot + 1] = customCSSText == null ? null : String.asAttValue(customCSSText);


                }

                // cell value (HTML contents)
                //>Animation
                if (isAnimationRow) {
                    // Set a flag so getTableHTML() is aware that the fragment its returning
                    // is to be used in the animated show/hide row.
                    this._writingAnimatedShowRows = true;
                    var tableHTML = this.getTableHTML(null, this._animatedShowStartRow,
                                                                this._animatedShowEndRow);
                    delete this._writingAnimatedShowRows;


                    if (!writeDiv) {
                        cellHTML[cellValueSlot] = isc.SB.concat(this._$cellClipDivStart,
                                                                this._getCellDivCSSHeight(rowHeight, record, rowNum, isAnimationRow),
                                                                this._$singleQuote, this._$rightAngle, tableHTML,
                                                                "</DIV>");
                    } else {
                        cellHTML[cellValueSlot] = tableHTML;
                    }

                } else    //<Animation


                cellHTML[cellValueSlot] = this._getCellValue(record, rowNum, colNum);

                if (cellIDSlot != null) {
                    cellHTML[cellIDSlot] = cellIDs[colNum - startCol];
                }

                output.append(cellHTML);

                // if the record has an embedded component update its row/colNum now
                if (!fragment && cellRecord != null && this.grid._hasEmbeddedComponents(cellRecord)) {

                    // avoid calling this method multiple times if one record spans several
                    // cells (IE one record / row)
                    var ecs = this.grid._getEmbeddedComponents(cellRecord);
                    if (ecs[0] && ecs[0].rowNum == null) {
                        this.updateEmbeddedComponentCoords(ecs, cellRecord, rowNum, colNum);
                    }
                }

                // see the first cell's HTML, as a sample
                //if (!this._gotSample) {
                //    alert("cellHTML: " + cellHTML.join(""));
                //    this._gotSample = true;
                //}

                if (drawRecordAsSingleCell && (colNum == singleCellSpan[0])) {
                    // increase the counter - well skip the rest of the colNums in the
                    // array

                    i += singleCellSpan[1] - singleCellSpan[0];
                }
            }
            // end the table row
            output.append(rowEnd);

            // If we're printing and there are embedded components that span the entire row,
            // write them into a separate cell

            if (this.isPrinting && record && this.grid._hasEmbeddedComponents(record)) {
                var ecs = this.grid._getEmbeddedComponents(record);
                for (var ecIndex = 0; ecIndex < ecs.length; ecIndex++) {
                    var ec = ecs[ecIndex];
                    if (ec._currentColNum == null && ec._gridBodyPrintHTML != null) {
                        output.append(rowStart, gt, '<td colspan="',numCols,'">',
                            ec._gridBodyPrintHTML, "</td>", rowEnd);
                        delete ec._gridBodyPrintHTML;
                    }
                }
            }

            //>Animation
            // Skip the rows between animationStartRow and animationEndRow, since they'll be
            // written into a single row
            if (isAnimationRow) {
                rowNum = this._animatedShowEndRow -1;
            }
            //<Animation
        }
    }

    // output the end table tag
    if (!this._printingChunk || (endRow == this.getTotalRows() && !this.printChunkOnly)) {
        if (this.grid && this.isPrinting) {
            output.append(this.getPrintFooters(startCol, endCol));
        }
        output.append("</TBODY></TABLE>");
    }



    var tailRecords = rangeEnd - endRow,
        virtualScrolling = (!fragment && this._isVirtualScrolling);

    // ignore endSpace if this.cacheDOM is true - not currently supported in that mode.
    var endSpacerHeight = this.cacheDOM ? 0 : (this.endSpace || 0);
    // Ignore endSpace if a fragment was requested.

    if (fragment) {
        endSpacerHeight = 0;
    }
    // reset this._endRowSpacerHeight
    if (!this._gettingAutoSizeHTML) this._endRowSpacerHeight = 0;
    if (!this.showAllRows && (tailRecords != 0 || virtualScrolling)) {
        var endRowSpacerHeight = tailRecords * this.getAvgRowHeight();

        if (virtualScrolling && tailRecords == 0 && !fragment) {
            var minHeight = this.getViewportHeight();
            if (endRowSpacerHeight < minHeight) {
                endRowSpacerHeight = minHeight;
            }
        }

        if (!this._gettingAutoSizeHTML) this._endRowSpacerHeight = endRowSpacerHeight;
        endSpacerHeight += endRowSpacerHeight;
    }
    // NOTE: setting overflow:hidden allows later code to shrink the spacer
    // without rewriting the spacer content
    if (!this.cacheDOM && !this.isPrinting) {

        output.append("<DIV style='width:1px;");
        if (canResizeSpacerDivs) {
            output.append("height:", endSpacerHeight, "px;overflow:hidden;");
        }
        if (endSpacerHeight == 0) output.append("display:none;");
        output.append("' ");

        if (fragment || this.isPrinting) {
            output.append(">");
        } else {
            output.append(" ID="+ this.getID()+ "_endSpacer>");
        }
        output.append(isc.Canvas.spacerHTML(1, endSpacerHeight), "</DIV>");

    }

    //>DEBUG timing
    if (this.logIsDebugEnabled("gridHTML")) {
        var totalTime = (isc.timeStamp() - t0),
            numCells = (numCols * (endRow - startRow)),
            perCell = (totalTime / numCells),
            perSecond = (1000 / perCell);

        // toFixed appears not to be supported in Safari
        if (perCell.toFixed != null) perCell = perCell.toFixed(2);
        if (perSecond.toFixed != null) perSecond = perSecond.toFixed(2);

        this.logDebug("getTableHTML: columns " + (discreteCols ? colNums
                                                    : startCol + "->" + (endCol-1)) +
                      ", rows " + startRow + "->" + (endRow-1) +
                      ", time: " + totalTime + "ms (" +
                      numCells + " cells at " +
                      perCell + "ms per cell, " +
                      perSecond + " cells per second), " +
                      "spacerHeights: [" + [startSpacerHeight, endSpacerHeight] + "], " +
                      "left/right pad: [" + [leftColPad, rightColPad] + "], " +
                      singleCells + " single cell rows",
                      "gridHTML");
    }
    //<DEBUG

    var result = output.release(false);

    if (isAsync) {
        if (asyncCallback != null) {
            this.fireCallback(asyncCallback, "HTML,callback", [result,asyncCallback]);
        }
        return null;
    }

    // now return the output
    return result;
},

_getTableHTMLDrawArea : function (startRow, endRow, setFirstAndLastDrawnRow) {
    var fragment = (startRow != null && endRow != null);

    // Figure out rows and columns to actually draw
    // ----------------------------------------------------------------------------------------

    var drawRect = this.getDrawArea(),
        grid = this.grid,
        scrollRowNum;

    if (grid) {
        if (grid._scrollCell) {
            scrollRowNum = (grid._scrollCell == null ? 0 :
                isc.isAn.Array(grid._scrollCell) ? grid._scrollCell[0] : grid._scrollCell);
        } else if (grid.data && grid.data.getFirstUsedIndex && drawRect[0] == 0) {
            scrollRowNum = grid.data.getFirstUsedIndex();
        }
        if (scrollRowNum) {
            var diff = drawRect[1] - drawRect[0],
                lastRow = scrollRowNum + diff,
                totalRows = this.getTotalRows();

            if (lastRow >= totalRows) {
                scrollRowNum -= (lastRow - (totalRows - 1))
                lastRow = totalRows - 1;
            }
            if (scrollRowNum < 0) scrollRowNum = 0;
            drawRect[0] = scrollRowNum;
            drawRect[1] = lastRow;
        }
    }

    if (!fragment) {
        var firstDrawnRow = drawRect[0],
            lastDrawnRow = drawRect[1];
        //>Animation
        // If we're doing an animated show/hide of some rows, we need to write out enough rows
        // to fill the viewport when the rows to be animated are sized at zero height (will
        // happen either initially or at the end of the draw).
        if (this._animatedShowStartRow != null) {
            lastDrawnRow += (this._animatedShowEndRow - this._animatedShowStartRow);
            var totalRows = this.getTotalRows();
            if (lastDrawnRow >= totalRows) lastDrawnRow = totalRows - 1;
        }
        //<Animation

        // NOTE: _lastDrawnRow/Col are the last row/col to
        // be drawn, logic below renders up to but not including endCol/endRow
        startRow = firstDrawnRow;
        endRow = lastDrawnRow + 1;

        if (setFirstAndLastDrawnRow) {
            this._firstDrawnRow = firstDrawnRow;
            this._lastDrawnRow = lastDrawnRow;
        }


    } else {
        var viewportTop = drawRect[0],
            viewportEnd = drawRect[1] + 1;

        //>Animation
        // A common use of table fragments is animating folder open/close, where we write out
        // a bunch of child rows and animate them into view.
        // In this case if we're scrolling the rows into view and we're inside the viewport,
        // each of the rows will be seen by the user so we can't skip writing any content
        // as we don't want to show blank spacers to the user.
        if (this._writingAnimatedShowRows) {
            // All off the bottom or the top - just draw a big spacer so the scrollbar adjusts
            if (viewportTop > endRow || viewportEnd < startRow) {
                startRow = endRow;
            } else {
                if (!this._slideInAnimationRows) {
                    startRow = Math.max(startRow,viewportTop);
                    endRow = Math.min(endRow,viewportEnd);
                }

            }
        }
        //<Animation
    }
    drawRect[0] = startRow;
    drawRect[1] = endRow;
    return drawRect;
},

// Arbitrary HTML to write out *inside the table* before the first row and after the
// last row, when printing.
// Used to render headers (and footers) by ListGrids - overridden in GridBody class.
getPrintHeaders : function (startCol, endCol) {
    return "";
},
getPrintFooters : function (startCol, endCol) {
    return "";
},

setFocus : function (focus, reason) {
    // in screenreader mode, we put focus into the current focus row element of the grid when
    // focus is called.
    // on mousedown, we explicitly call widget.focus() -- ensure that in this case we put focus
    // into the mouse-down row, not into whatever the current focus row happens to be.
    if (isc.screenReader && reason == "focus on mousedown") {
        var eventRow = this.getEventRow();
        if (eventRow != null && !this.isEmpty()) {
            // Handle the user clicking on the end spacer in a scrolled grid (can come up if
            // virtual scrolling is enabled). In this case we want native focus to go to the
            // last real row, not to a row that's scrolled out of view.
            if (eventRow == -2) eventRow = this.getTotalRows()-1;
            if (eventRow >= 0) {
                this._putNativeFocusInRow(eventRow, true);
            }
        }
    }
    return this.Super("setFocus", arguments);
},

// Helper to put native focus into a row if we're in screenReader mode.

_putNativeFocusInRow : function (rowNum, suppressElementFocus) {

    var updateElement = this.screenReader_suppressHandleFocus;
    // if necessary, clear the tabIndex/focus and blur handlers from the current focus row
    if (updateElement) this._updateHandleForFocus(false);
    this._nativeFocusRow = rowNum;
    // assign the tabIndex / focus/blur handlers to new focus row
    if (updateElement) this._updateHandleForFocus(true);

    // update this.clipHandleIsFocusHandle

    if (this.screenReader_suppressHandleFocus) {
        this.clipHandleIsFocusHandle = this.isEmpty();
    }

    // Treat "suppressElementFocus:false" as saying explicitly force focus into
    // the row - otherwise only actually focus if we as a widget have logical focus.
    // That allows us to set up the row as a valid tabstop (without forcing focus into it)
    if (suppressElementFocus == null) suppressElementFocus = !this.hasFocus;
    if (suppressElementFocus) {
        return;
    }


    var visibleRows = this._getVisibleRows();
    if (!(visibleRows[0] <= rowNum && rowNum <= visibleRows[1])) {
        return;
    }

    var element = this.getFocusHandle();
    if (element) element.focus();



    if (!updateElement) isc.EH._focusCanvas = null;
    element.focus();
    if (!updateElement) isc.EH._focusCanvas = this;

},


getFocusHandle : function () {
    if (isc.screenReader) {
        var rowNum = this.getNativeFocusRow();
        var row = this.getTableElement(rowNum);
        if (row != null) return row;
    }
    return this.Super("getFocusHandle", arguments);
},

// When printing, we call 'getPrintHTML()' on each embedded component.
// This method may run asynchronously.
// This callback is fired when this happens. It stores the print HTML on the component
// temporarily and calls 'getTableHTML()' passing in the callback we were passed.
gotComponentPrintHTML : function (HTML, callback) {

    var context = callback.context,
        component = context.component;

    if (context.asyncCallback == null) {
        return;
    }

    component._gridBodyPrintHTML = HTML;


    return this.getTableHTML(context.colNum, context.startRow, context.endRow,
        context.discreteCols, context.asyncCallback, true);
},


// When we write out the per cell HTML using templating, in fastCellUpdates:true mode,
// we write out style='<style text from css class definition>'
// This helper method will convert any single quotes to HTML entities so that they don't
// terminate the style attribute in the written-out HTML.

_escapedStyleText:{},
_getEscapedStyleText : function (styleName) {
    if (this._escapedStyleText[styleName] != null) return this._escapedStyleText[styleName];
    //this.logWarn("escaping:" + styleName);
    var styleText = isc.Element.getStyleText(styleName, true);

    this._escapedStyleText[styleName] = String.asAttValue(styleText);
    return this._escapedStyleText[styleName];
},


// Methods to return cell alignment
// Overridden on the gridbody class
getCellVAlign : function (record, field, rowNum, colNum) {
    return null;
},

getCellAlign : function (record, field, rowNum, colNum) {
    return field.cellAlign || field.align;
},


// draw record as single cell does not always span the entire row - in the ListGrid, if we have
// a checkbox field we want to show it on records even where singleCellValue is true
// startCol / endCol passed in are the start/end cols we're currently rendering
_getSingleCellSpan : function (record, rowNum, startCol, endCol) {
    return [startCol, endCol];
},

// This is some innerHTML written into the <TD for single cell values, to govern it's COLSPAN
// This is extremely time critical so only create a new string once for each 'span' we have
// requested
_getTDSpanHTML : function (span) {
    if (!isc.GridRenderer._tdSpanHTML) {
        isc.GridRenderer._tdSpanHTML = {_fixedRowHeights:{},
                                      _varRowHeights:{}};
    }
    var cache = this.fixedRowHeights ? isc.GridRenderer._tdSpanHTML._fixedRowHeights
                                      : isc.GridRenderer._tdSpanHTML._varRowHeights;
    if (cache[span]) return cache[span];
    else {
        return cache[span] = " COLSPAN=" + span  + " STYLE='" +
                           (this.fixedRowHeights ? "padding-top:0px;padding-bottom:0px;" : "");
    }
},



// Returns the base style of the first record. Used to calculate sizing for cells based on
// the style's padding etc. [making the assumption that the padding etc is constant across potential
// cell styles]
_getFirstRecordStyle : function () {
    var grid = this.grid,
        rowNum = 0;

    if (grid) {
        if (grid._scrollCell) {
            rowNum = grid._scrollCell == null ? 0 :
                isc.isAn.Array(grid._scrollCell) ? grid._scrollCell[0] : grid._scrollCell;
        } else if (grid.data && grid.data.getFirstUsedIndex) {
            rowNum = grid.data.getFirstUsedIndex();
        }
    }

    return (this.getBaseStyle != null ?
            this.getBaseStyle(this.getCellRecord(rowNum,0), 0, 0) :
            this.baseStyle);
},

_cacheColumnHTML : function (colNums, autoFit, hPad, writeDiv) {
    var fields = this.fields,
        sizes = this._fieldWidths;


    // compute per-column HTML
    for (var i = 0; i < colNums.length; i++) {
        var colNum = colNums[i],
            field = fields[colNum];

        field._rowSpans = null; // clear old rowSpan info

        // NOTE: this slot must end in "STYLE='" so that the next slot can be arbitrary CSS text
        if (autoFit) {
            // don't write widths
            field._widthHTML = (isc.Browser.isIE && !isc.Browser.isIEStrict) ? " STYLE='" : " STYLE='OVERFLOW:hidden;";
            // have to reset this HTML in case settings change
            field._divWidthHTML = this._$singleQuote;
        } else {

            var styleStart = isc.Browser.isIE8Strict ? " STYLE='overflow:hidden;" :
                                                       " STYLE='";
            // NOTE: Moz and Win IE5 require that we set the widths of columns in their
            // cells as well as in the COL tags or things won't always display correctly.
            field._widthHTML = (isc.Browser.isIE ?
                                " WIDTH=" + (sizes[colNum] - hPad) + styleStart :
                                " STYLE='" + this._getCSSTextForColWidth(colNum));

            if (writeDiv) {
                field._divWidthHTML = this._getFieldDivWidthHTML(colNum) +
                                      this._$singleQuote;
            }


        }

        // when fixedRowHeights is false, we want the cellHeight to act as a minimum height.

        if (!this.fixedRowHeights) {
            field._widthHTML += this._getMinHeightCSSText();
        }


        if (this.fixedRowHeights) field._widthHTML += "padding-top:0px;padding-bottom:0px;";
    }
},

//> @method gridRenderer.cellValueIsClipped() ([A])
// Is the value in a given cell clipped?
// @param rowNum (number) row number of the cell
// @param colNum (number) column number of the cell
// @return (Boolean) null if there is no cell at the given row, column; otherwise, whether the
// value in the specified cell is clipped.
// @see cellValueHover()
// @visibility external
//<
cellValueIsClipped : function (rowNum, colNum) {
    var cellElem = this.getTableElement(rowNum, colNum);
    var clipDiv;
    if (!this._writeDiv(this.cellHeight)) clipDiv = cellElem;
    else clipDiv = this._getCellClipDiv(cellElem);
    return this._cellValueIsClipped(clipDiv);
},
_cellValueIsClipped : function (clipDiv) {
    if (clipDiv == null) return null;
    return isc.Element.getClientWidth(clipDiv) < clipDiv.scrollWidth;
},

// _writeDiv() : Do we need to write a DIV into the grid cells?

// this is re-used for every clipDiv
_$cellClipDivStart:"<DIV role='presentation' cellClipDiv=true style='overflow:hidden;" + isc.Browser._textOverflowPropertyName + ":ellipsis;",

// In some cases we need to write a DIV to clip cells either vertically or horizontally; writing
// overflow hidden and specifying a height or width simply doesn't cause clipping.
_writeDiv : function (cellHeight) {




    return (isc.Browser.isSafari ||

            (isc.Browser.isOpera && !this.autoFit &&
             (this.fixedColumnWidths || this.fixedRowHeights)) ||

           (isc.Browser.isMoz && isc.Browser.geckoVersion >= 20040113 &&
            this.fixedColumnWidths && !this.autoFit) ||


                (this.shouldEnforceVClipping() &&
                    // Moz or IE Strict (Safari covered above - always on)
                    (isc.Browser.isMoz ||
                        (isc.Browser.isIE && (isc.Browser.isStrict || isc.Browser.version >= 10))
                    )
                )
            );
},

// Should the record be drawn as a single cell, spanning all the cols in the table?
_drawRecordAsSingleCell : function (rowNum, record) {
    //!DONTCOMBINE
    // If this row is a separator or is not loaded yet, we draw a single cell with COLSPAN
    // set to extend across the entire table.
    return (record &&
            (record[this.singleCellValueProperty] != null || record[this.isSeparatorProperty] ||

             (Array.isLoading(record) &&
                !(isc.Browser.isSafari && (rowNum == 0 || rowNum == this._firstDrawnRow)) )
            )
           );
},

// Method to return the CSS text to specify the height of the div written into a cell

_useMaxHeightForCellDivCSSHeight : function () {
    return (isc.Browser.isMoz || isc.Browser.isSafari || (isc.Browser.isIE &&
                                                          ((isc.Browser.version >= 7 && isc.Browser.isStrict) ||
                                                           isc.Browser.version >= 10)));
},
_getCellDivCSSHeight : function (rowHeight, record, rowNum, isAnimationRow) {
    // vertically clip if..
    var shouldWriteHeight =
                            //>Animation it's the animation row on a row reveal animation,
                            // where if we don't clip, we'll just reveal the new rows
                            // immediately
                            isAnimationRow ||  //<Animation
                            // we're in a situation where we normally enforce clipping on every
                            // row (see comments in _writeDiv)
                            (this.shouldEnforceVClipping() &&
                            // we don't have an implementation of shouldFixRowHeight() that is
                            // telling us not to clip this individual row.  Note this is
                            // currently only used internally in order to allow the edit row to
                            // expand.
                             (this.shouldFixRowHeight == null ||
                              this.shouldFixRowHeight(record, rowNum) != false));




    if (shouldWriteHeight) {

        // Note: since we override padding-top and padding-bottom, we should not need
        // to adjust for it in the height of the DIV if fixedRowHeights is true.
        var divHeight = rowHeight - 2*this.cellSpacing -

                (isAnimationRow ? 0 : 2);
        if (rowNum == this._editRowNum) {
            for (var i = 0; i < this._editRowForm.getItems().length; i ++) {
                divHeight = Math.max(this._editRowForm.getItems()[i].getHeight(), this._editRowForm.getItems()[i].iconHeight);
            }
        }



        // avoid writing out a negative height

        if (divHeight < 1) divHeight = 1;
        return (this._useMaxHeightForCellDivCSSHeight() ? "MAX-HEIGHT:" : "HEIGHT:") + divHeight + "px;";
    }

    return isc._emptyString;
},

// Method to return the CSS text for the width of the div written into a cell.
// NOTE: Assumed to be written in as part of a STYLE=... attribute.
_getFieldDivWidthHTML : function (colNum) {

    // No need to set a width if we don't have fixed column widths - we already have a minimum
    // from the size written into the TD, and it breaks calculation of scroll width in Safari.
    if (!this.fixedColumnWidths || this.autoFit) return isc.emptyString;

    return "WIDTH:" + this.getInnerColumnWidth(colNum) + "px;";
},

_getMinHeightCSSText : function (record, rowNum) {


    var height = (rowNum != null ? this.getRowHeight(record, rowNum, this._isFrozenBody()) : this.cellHeight),
        IE = isc.Browser.isIE, strict = isc.Browser.isStrict;

    if (strict) height -= this._vPad;

    if (IE && !strict && !(this.autoFit || !this.fixedColumnWidths)) {
        return "MIN-HEIGHT:" + height + "px;"
    }
    return "HEIGHT:" + height + "px;"
},

_getCSSTextForColWidth : function (colNum) {
    if (isc.Browser.isIE || this.autoFit) return isc._emptyString;


    if (this._colWidthCSSText == null) {
        this._colWidthCSSText = [];
        for (var i = 0; i < this._fieldWidths.length; i++) {
            var width = this._fieldWidths[i];

            this._colWidthCSSText[i] = "WIDTH:" + width +
                (this.fixedColumnWidths ? "px;OVERFLOW:hidden;" : "px;");
        }
    }
    return this._colWidthCSSText[colNum];
},

// No default data model for GRs - this is implemented at the ListGrid level
getCellRecord : function (rowNum, colNum) {
    return null;
},

findRowNum : function (record) {
    return -1;
},

findColNum : function (record) {
    return -1;
},


_$divStart:"<div>", _$divEnd:"</div>",
_$spacerDivTemplate:[
    ,                                         // [0] value
    "<div style='height:",                    // [1]
    ,,,,,                                     // [2-6] extraHeight
    "px;line-height:0px'>&nbsp;</div>"        // [7]
],
_getCellValue : function (record, rowNum, colNum) {
    //!DONTCOMBINE
    var isPrinting = this.isPrinting,
        value = this.getCellValue(record, rowNum, colNum, this);
    // If a record has an associated component to display, add a spacer underneath the record
    // to force the contents to draw above the component.

    if (!isPrinting && this._writeEmbeddedComponentSpacer(record)) {
        var details = this._getExtraEmbeddedComponentHeight(record),
            spacerDivTemplate = this._$spacerDivTemplate;
        if (details.allWithin) {
            if (details.extraHeight && (details.extraHeight > this.cellHeight)) {
                spacerDivTemplate[0] = value;
                isc._fillNumber(spacerDivTemplate, (details.extraHeight - Math.max(isc.ListGrid.getInstanceProperty("cellHeight"), this.cellHeight)),
                                2, 5, false);
                value = spacerDivTemplate.join(isc.emptyString);

                //isc.logWarn("In _getCellValue:  details are "+isc.echoAll(details));
            }
        } else if (details.extraHeight && details.extraHeight > 0) {
            spacerDivTemplate[0] = value;
            isc._fillNumber(spacerDivTemplate, details.extraHeight, 2, 5, false);
            value = spacerDivTemplate.join(isc.emptyString);

            //isc.logWarn("In _getCellValue:  details are "+isc.echoAll(details));
        }

    // write embedded components right into cells if printing...
    } else if (record && this.grid._hasEmbeddedComponents(record)) {
        var colNumOffset = 0,
            frozenColNumOffset = 0;
        if (isPrinting) {
            colNumOffset = this.grid._embeddedComponentColNumOffset;
            frozenColNumOffset = this.grid._frozenEmbeddedComponentColNumOffset;
        }
        var components = this.grid._getEmbeddedComponents(record);
        for (var i = 0, len = (components ? components.length : 0); i < len; ++i) {
            var component = components[i];
            if (component == null) continue;
            var currentColNum = component._currentColNum;
            if (isPrinting && currentColNum != null) {
                currentColNum += (component._wasFrozen ? frozenColNumOffset : colNumOffset);
            }
            if (currentColNum != colNum) {
                continue;
            }

            var isWithin = (component.embeddedPosition == this._$within);

            var cPrintHTML = component._gridBodyPrintHTML;
            if (cPrintHTML != null) {
                var nonemptyCPrintHTML = (cPrintHTML != isc.emptyString);
                if (isWithin && nonemptyCPrintHTML) {
                    cPrintHTML = this._$divStart + cPrintHTML + this._$divEnd;
                }
                if (value == this.emptyCellValue && nonemptyCPrintHTML) {
                    value = cPrintHTML;
                } else {
                    value += cPrintHTML;
                }
                // clean that property up as we go
                delete component._gridBodyPrintHTML;
            }
        }
    }

    return value;
},
_writeEmbeddedComponentSpacer : function (record) {
    return (record && this.grid._hasEmbeddedComponents(record));
},

getCellValue : function (record, rowNum, colNum) {
    return this.emptyCellValue;
},

// Specifying Table Geometry
// --------------------------------------------------------------------------------------------


//>    @method gridRenderer.getTotalRows()
// Return the total number of rows in the grid.<br><br>
//
// NOTE: in order to create a valid grid, you must either provide a totalRows value or implement
// getTotalRows()
//
// @return (number)
// @see attr:totalRows
// @visibility external
//<
getTotalRows : function () {
    return this.totalRows;
},

// NOTE: this.fields and the GridRenderer
// fields are currently used for the following config:
// - column width
// - column header alignment
// - column cell alignment
// It may be more appropriate to handle the above via a getColumnWidth()/getAlign interface.

//> @method gridRenderer.setColumnWidth()
// Sets the width of a single column.
//
// @param colNum (number) the number of the column to resize
// @param newWidth (number) the new width
//
// @visibility external
//<
setColumnWidth : function (colNum, newWidth) {
    this.fields[colNum].width = this._fieldWidths[colNum] = newWidth;
    this._colWidthCSSText = null;
    this.markForRedraw("setColumnWidth");
},

//> @method gridRenderer.setColumnWidths()
//
// Sets the width of all columns in the grid.
//
// @param newWidths (Array) array of new widths - one for each column.
//
// @visibility external
//<
setColumnWidths : function (columnWidths) {
    var oldWidths = this._fieldWidths;

    // copy the widths so we aren't affected if the caller subsequently changes the array
    this._fieldWidths = columnWidths.duplicate();
    this._colWidthCSSText = null;

    if (oldWidths != null && columnWidths != null && oldWidths.length == columnWidths.length) {
        // same number of columns

        // widths unchanged means no need to redraw
        if (oldWidths == columnWidths) return;
        var changed = false;
        for (var i = 0; i < oldWidths.length; i++) {
            if (oldWidths[i] != columnWidths[i]) changed = true;
        }
        if (!changed) return;


        if (!this.fixedColumnWidths && !this.wrapCells && this.isDrawn() &&
            columnWidths.length == 1) {

                // NOTE: for the oldMinimum, we want the specified size as of the last time we
                // drew; in the meantime, resizes that did not cause a redraw may have changed
                // the logical column width
                var oldMinimum = this._colWidthAtDraw || oldWidths[0],
                    newMinimum = columnWidths[0],
                    renderedSize = this.getColumnSize(0);

                // If the drawn size was the old minimum, a change in minimum requires a
                // redraw: a lower minimum means the content might draw smaller, and a higher
                // minimum means it must draw larger.
                // If the drawn size is less than the new minimum, the column will have to
                // expand to that new minimum.
                // Therefore the drawn size will change unless the rendered size exceeded the
                // old minimum and exceeds or is equal to the new minimum.
                //this.logWarn("oldMinimum: " + oldMinimum + ", newMinimum: " + newMinimum +
                //             ", renderedSize: " + renderedSize);
                if ((oldMinimum == newMinimum) ||
                    (renderedSize > oldMinimum && renderedSize >= newMinimum)) {
                    return;
                }
        }
    }

    this.markForRedraw("setColumnWidths");
},

shouldRedrawOnResize : function (deltaX, deltaY, animating) {
    if (this.redrawOnResize != null) return this.redrawOnResize;

    // quick hack: if we are being resized in a Layout because some other member is animating
    // don't redraw until the animation completes, otherwise our redraw will make the animation
    // lurch.
    if (isc.isA.ListGrid(this.parentElement) &&
        isc.isA.Layout(this.parentElement.parentElement))
    {
        var siblings = this.parentElement.parentElement.getMembers();
        if (siblings && siblings.map("isAnimating").or()) return false;
    }

    // redraw if our new size reveals more rows or columns
    if (this._needRowRedraw() || this._needColumnRedraw()) return true;

    // if we're showing the empty message we need to redraw since we write a static width / height
    // into the empty message
    if (this.isEmpty()) return true;


    return false;
},


// string methods: getRowSpan

getRowHeight : function (record, rowNum) {
    var height = this.updateHeightForEmbeddedComponents(record, rowNum, this.cellHeight);
    return height;
},

updateHeightForEmbeddedComponents : function (record, rowNum, height) {

    if (record && this.grid._hasEmbeddedComponents(record)) {
        var details = this._getExtraEmbeddedComponentHeight(record, rowNum);
        if (details.allWithin && details.extraHeight > 0) {
            height = Math.max(height, details.extraHeight);
            //this.logWarn("in updateHeightForEmbeddedComponents ("+this.grid+"): details are "+isc.echoAll(details)+"\nheight is "+height);
        } else {
            height += details.extraHeight;
            //this.logWarn("in updateHeightForEmbeddedComponents ("+this.grid+"): details are "+isc.echoAll(details)+"\nheight is "+height);
        }
    }
    return height;
},

_getExtraEmbeddedComponentHeight : function (record, rowNum) {
    var components = this.grid._getEmbeddedComponents(record) || [],
        maxComponentHeight = 0,
        allWithin = true,
        isPrinting = this.isPrinting
    ;

    for (var i = 0; i < components.length; i++) {
        var component = components[i];

        if (!component) continue;
        // when printing, we write colspanning components into a separate row

        if (isPrinting) continue;

        // mark the component with the row it currently appears in
        if (rowNum != null) component._currentRowNum = rowNum;

        var isWithin = (component.embeddedPosition == this._$within);
        if (!isWithin) allWithin = false;

        var tempHeight = component.getVisibleHeight();
        var componentHeight = (isWithin ? (tempHeight > this.cellHeight ? tempHeight : 0) : tempHeight);
        // expand the row so that the component appears under the normal cells
        if (component._percent_height != null) {
            component.height = component._percent_height;
            componentHeight = this.cellHeight;
        }
        var origHeight = component.specifiedHeight;
        if (isWithin && origHeight && isc.isA.String(origHeight) && origHeight.contains("%"))
            componentHeight = 0;
        if (componentHeight > maxComponentHeight) {
            maxComponentHeight = componentHeight;
        }
    }

    return { allWithin: allWithin, extraHeight: maxComponentHeight };
},

_getRowSpan : function (record, rowNum, colNum) {
    if (!this.allowRowSpanning || !this.getRowSpan) return 1;
    var span = this.getRowSpan(record, rowNum, colNum);
    return Math.min(span, this.getTotalRows() - rowNum);
},

//> @method gridRenderer.getCellStartRow()
// When using +link{getRowSpan,row spanning}, returns the row number where a row-spanning cell
// starts.
// <P>
// For example, if row 2 col 0 spans 3 cells, <code>getCellStartRow()</code> for row 2 col 0,
// row 3 col 0, row 4 col 0 will all return 2, because that's the row when spanning starts.
//
// @param rowNum (int) row number of cell for which the start row should be returned
// @param colNum (int) column number of cell for which the start row should be returned
// @return (int) row number where spanning starts
// @visibility external
//<
getCellStartRow : function (rowNum, colNum) {
    var field = this.fields[colNum];
    if (field == null) return null;

    var spans = field._rowSpans;

    // no spanning at/above this cell
    if (spans == null || spans[rowNum] == null) return rowNum;

    //this.logWarn("span at row/col: " + [rowNum, colNum] + " start row: " + spans[rowNum]);

    return spans[rowNum];
},

//> @method gridRenderer.getCellRowSpan()
// When using +link{getRowSpan,row spanning}, returns the number of cells spanned by the cell
// at the given coordinates.
// <P>
// If the passed coordinates are in the middle of a series of spanned cells, the row span of
// the spanning cell is returned.  For example, if row 2 col 0 spans 3 cells, calls to
// <code>getCellRowSpan()</code> for row 2 col 0, row 3 col 0, row 4 col 0 will all return 3.
// <P>
// This method returns row span information for the current rendered cells.  In contrast, if
// the grid is about to be redrawn, a call to <code>getRowSpan()</code> may return row span
// values for how the grid is about to be drawn.  Also, user-provided getRowSpan() functions
// are not required to operate properly when called outside of the grid rendering loop.
// <P>
// <b>Note:</b> This method is a utility method for developers - it is not called
// directly by the grid rendering path and therefore is not intended for override. To
// set up custom row-spanning behavior, override +link{getRowSpan()} instead.
//
// @param rowNum (int) row number of cell to return the row span for
// @param colNum (int) column number of cell to return the row span for
// @return (int) number of cells spanned by the cell that spans through these coordinates
// @visibility external
//<
getCellRowSpan : function (rowNum, colNum) {
    var spans = this.fields[colNum]._rowSpans;
    if (spans == null) return 1; // no spanning on this field

    var startRow = this.getCellStartRow(rowNum, colNum);

    // iterate down rows until we hit a different span start row
    var currentRow = rowNum + 1,
        // span extends at least from startRow to rowNum
        spannedCells = rowNum - startRow + 1;
    // see how much further this span extends
    while (currentRow <= this._lastDrawnRow &&
           spans[currentRow] == startRow)
    {
        currentRow++;
        spannedCells++;
    }
    return spannedCells;
},

// Embedded Components
// --------------------------------------------------------------------------------------------

// You can call addEmbeddedComponent to associate a component with a given record and row or cell
// The "position" attribute specifies how the component should appear
// - "expand" (the default): After being added, the component will appear "embedded" in the
//   row, beneath the row's data.
// - "within": The component will appear aligned with the top left edge of the row or cell (may use
//   snapTo to specify different edge to attach to). If percentage sizing is specified, component
//   will size to percentage of record.
//
// NOTE: embedded components are currently only supported for uses of the gridRenderer that
// return some record for each row.  We could associate components with row numbers, but in
// most uses an embedded component changes row number (eg ListGrid sort, TreeGrid
// expand/collapse), so we'd need the caller to tell us about rowNum changes.
//


// Method to actually attach a component to a record
_$within:"within",
_$expand:"expand",
_$cell:"cell",
addEmbeddedComponent : function (component, record, rowNum, colNum, position) {
    if (position == null) position = this._$expand;
    // if position is "expand", or fixedRowHeights is false (and the
    // embedded component height > specified row height) we may expand records.
    var mayChangeRowHeight = ((position == this._$expand) || !this.fixedRowHeights);

    // instantiate the component if it's passed as just properties
    if (!isc.isA.Canvas(component)) {
        component.autoDraw = false;

        var cons = isc.ClassFactory.getClass(component._constructor);
        if (cons == null) cons = isc.Canvas;

        component = cons.create(component);
    }

    var moveOnly = false;
    // if addEmbeddedComponent is called twice on the same comp, remove before embedding!
    if (this._embeddedComponents && this._embeddedComponents.contains(component)) {
        // already embedded at the right spot = a no op
        // Note:
        if (this.grid._hasEmbeddedComponents(record) && this.grid._getEmbeddedComponents(record).contains(component) &&
            component.embeddedPosition == position &&
            component._currentRowNum == rowNum && component._currentColNum == colNum)
        {
            return;
        }
        // we can avoid a redraw if
        // position is within, this.fixedRowHeights is true,
        // and position is unchanged
        // (and we're not dirty already)
        if (position == component.embeddedPosition && !mayChangeRowHeight) {
            moveOnly = !this.isDirty();
        }
        // third param to suppress clear / redraw - we'll take care of that
        this.removeEmbeddedComponent(component.embeddedRecord, component, true);

    } else if (!mayChangeRowHeight) {
        moveOnly = !this.isDirty();
    }

    // Make the record hang onto the component
    this.grid._addEmbeddedComponent(record, component);

    // add the component to the list of embedded components.
    if (this._embeddedComponents == null) this._embeddedComponents = [];
    this._embeddedComponents.add(component);

    component.embeddedPosition = position;
    component.embeddedRecord = record;

    // set up the current row / colNum passed in
    // note that if we redraw to render the embedded component these will be recalculated
    // anyway, but by setting up an initial currentColNum we ensure the component is
    // embedded in a cell rather than a row!
    component._currentRowNum = rowNum;
    component._currentColNum = colNum;
    // for frozen columns, mark the component with the id of the GridBody it's being stored in
    component._embedBody = this.getID();

    // if position == "within" we'll handle percentage sizing and snapTo ourselves
    // unexposed flag to disable standard snapTo / percent sizing logic
    component.percentBox = "custom";

    // add it as a child (which will force a draw, and give us a size) - hide it first so it
    // doesn't appear and then get moved into place
    // temporarily suppress adjustOverflow while we do this so we don't show huge
    // scrollbars if the thing is sized to 100% wide or tall
    if (component.parentElement != this) {
        var wasSuppressingAO = this._suppressAdjustOverflow;
        this._suppressAdjustOverflow = true;
        component.hide();
        this.addChild(component);
        if (wasSuppressingAO == null) delete this._suppressAdjustOverflow;
    }

    this.observe(component, "resized", function (deltaX, deltaY) {
        this._handleEmbeddedComponentResize(component, deltaX, deltaY);
    });

    // don't redraw the component when the grid redraws, otherwise we'll be redrawing embedded
    // components continually during scrolling.  NOTE: it may be that this should be the
    // default for parents that have a mixture of content and children.
    component.__oldRedrawWithParent = component._redrawWithParent;
    component._redrawWithParent = false;

    // prevent bubbling of most mouse events while the component is embedded.  We still want
    // mouseWheel events to bubble or it feels like scrolling is broken when embeddedComponents
    // scroll under the mouse.  We prevent other events because otherwise,
    // cellClick, recordClick et al will fire while the component is embedded, which is usually
    // wrong.
    component._origBubbleMouseEvents = component.bubbleMouseEvents;
    if (!component.bubbleMouseEvents) {
        // prevent most mouse events from bubbling
        component.bubbleMouseEvents = ["mouseDown", "mouseUp", "click", "doubleClick", "contextClick"];
    }

    // If the component is going to expand the row we'll need a redraw
    // Also, if we don't know the rowNum / colNum for the record, this will get picked up at redraw
    // time
    // Otherwise we just move the canvas into place.
    if (moveOnly && (rowNum == -1 || colNum == -1)) {
        moveOnly = false;
    }
    if (moveOnly) {
        this.placeEmbeddedComponent(component);
    } else {
        // redraw, which will draw the row at the new height and place the component
        this.markForRedraw("added embedded component");
    }
    return component;
},

_handleEmbeddedComponentResize : function (component, deltaX, deltaY) {
    var position = component.embeddedPosition;



    // if the embedded component resizes vertically, redraw so the row becomes the right size
    if (position != this._$within) {
        if (deltaY!=null && deltaY!=0) this.markForRedraw('embedded component resized');

    // If positioned within the cell, respond to resized (EG adjustOverlow) by
    // repositioning so snapTo continues to work...
    } else {
        this.placeEmbeddedComponent(component);
    }

},

// updateEmbeddedComponentCoords() called when we render out a record with an embedded component
// rowNum / colNum indicate the rowNum/colNum on which the record has been rendered
// (colNum is ambiguous on 1 record/row model)
// Default behavior leaves colNum untouched. Note that if we have one record per cell we might
// want to update colNum here, but we don't know that about our data model - rely on overrides
// to achieve this if required.
updateEmbeddedComponentCoords : function (components, record, rowNum, colNum) {
    components.setProperty("_currentRowNum", rowNum);
},

// place an embedded component over the correct row.
// Ideally this would only be called on sort, dataChanged, etc -- currently being called
// on every body redraw (may impact performance when incremental scrolling, for example)
placeEmbeddedComponent : function (component) {
    var rowNum = component._currentRowNum;
    if (rowNum == null || (this._firstDrawnRow == null || this._lastDrawnRow == null)
                    || (rowNum < this._firstDrawnRow || rowNum > this._lastDrawnRow))
    {
        // row is no longer drawn - clear component

        if (component.isDrawn()) component.clear();
        return;
    }

    if (!component.removeOnHideField &&
            component._currentFieldName && !this.grid.getField(component._currentFieldName))
    {
        // if we're trying to place a component that's attached to a field which is not visible,
        // eg. following a call to grid.hideField(), clear the component
        if (component.isDrawn()) component.clear();
        return;
    }

    // Note: If we're not fitting to a specific col, and we're showing
    // horizontal scrollbars,  we could fit to the viewport or
    // fit to the content (span the entire grid).
    // We'll fit to the viewport - standard usage in this case is a kind of
    // floating sub-component that we want to be visible for interactions.

    var record = component.embeddedRecord,
        position = component.embeddedPosition,
        colNum = component._currentColNum,
        topOrigin = this.getRowTop(rowNum),
        leftOrigin = (colNum != null && colNum >= 0) ? this.getColumnLeft(colNum) : null,
        // to make component snap to right of visible area, use getInnerWidth() to demarcate
        // the snap area, or the sum of all column widths, whichever is smaller
        width = (colNum != null && colNum >= 0) ? this.getColumnWidth(colNum) :
                            Math.min(this.getInnerWidth(), this._fieldWidths.sum()) ;
    if (leftOrigin == null) {
        if (!this.isRTL()) {
            leftOrigin = this.getScrollLeft();
        } else {
            // _shiftScrollLeftOrigin will give us back the scroll position adjusted
            // to the child abs-coord space - so hard left becomes a negative value and
            // hard right becomes zero.
            leftOrigin = this._shiftScrollLeftOrigin(this.getScrollLeft(), true);
        }
    }

//        this.logWarn("Placing embedded component " + component + ", row/col:" + [rowNum,colNum]
//                + ", top/left cell origin:" + [topOrigin,leftOrigin] + ", position:" + position,
//                "embeddedComponents");
    if (position == this._$within) {
        // Respect "snapTo" if specified
        // *Note: we are suppressing standard canvas percent sizing and snap-to behavior
        // so we can explicitly size / position based on cell coordinates
        var snapTo = this.getEmbeddedComponentSnapTo(component, record, rowNum, colNum),
            snapEdge = component.snapEdge || snapTo;

        // figure out sizes before placing!
        var height;
        if (this.allowRowSpanning && this.getRowSpan) {
            // Assume this method only runs for actual rendered rows (so if there's
            // a span, we're the first spanned cell.
            // If colNum is null we're looking at a per-row component - just size
            // to fit the left-most column
            var span = this.getRowSpan(record, rowNum, colNum==null ? 0 : colNum);
            if (span == null) span = 1;
            height = 0;
            for (var i = 0; i < span; i++) {
                height += this.getRowSize(rowNum + i);
            }
        } else {
            height = this.getRowSize(rowNum);
        }
        var cpw = component._percent_width,
            cph = component._percent_height,
            cw, ch;
        // for "within" components we want the "bottom" to be the bottom of the row
        // content only -- IE we don't want to center over the row as expanded to
        // accomodate "expand" type components.
        // Adjust the size to account for this
        var expandedComponentDelta = 0,
            components = this.grid._getEmbeddedComponents(record);
        for (var i = 0; i< components.length; i++) {
            var expComponent = components[i];

            if (expComponent == null) continue;
            var isWithin = (expComponent.embeddedPosition == this._$within);
            if (isWithin) continue;
            var componentHeight = expComponent.getVisibleHeight();
            if (componentHeight > expandedComponentDelta) {
                expandedComponentDelta = componentHeight;
            }
        }
        height -= expandedComponentDelta;

        // If positioned offset from the left, shrink the target space
        if (component.snapOffsetLeft) width -= component.snapOffsetLeft;

        if (isc.isA.String(cpw) && cpw.endsWith("%")) {
            cw = Math.round((parseInt(cpw) * width) / 100);
        }
        if (isc.isA.String(cph) && cph.endsWith("%")) {
            ch = Math.round((parseInt(cph) * height) / 100);
        }

        var compHeight = ch != null ? ch : component.getHeight(),
            compWidth = cw != null ? cw : component.getWidth();

        if (ch || cw) {
            component.resizeTo(cw, ch);
             // retain percentages so we reflow correctly!
            component._percent_width = cpw;
            component._percent_height = cph;
        }
        // pass row/column dimensions to snapToEdge in lieu of a canvas
        isc.Canvas.snapToEdge([leftOrigin, topOrigin, width, height], snapTo, component, snapEdge);

    } else {
        // NOTE: if you need multiple "expand" components to expand a single row, generally
        // you're expected to use a Stack or Layout to manage them.

        // float at the bottom of the row, rather than the top

        //topOrigin += this.cellHeight;
        component.moveTo(leftOrigin, topOrigin);

        // Percent specified height on an "expand" component is unusual.
        // If encountered, treat as a percentage of standard cell-height
        var cw, ch,
            cph = component._percent_height;
        if (isc.isA.String(cph) && cph.endsWith("%")) {
            ch = Math.round((parseInt(cph) * this.cellHeight) / 100);
        }



        // Note that resizing horizontally (only) may still
        // adjust the visibleHeight of the component due to overflow
        cw = width;

        component.resizeTo(cw, ch);
    }

    var showing = this.isDrawn();
    if (showing && !component.isDrawn()) component.draw();

    // at this point we can measure the component to see if it forces vertical expansion of the
    // row
    // Note that the row's height as written into the dom is set via getRowHeight() which already
    // checks the 'visibleHeight' of all embedded components, so the cases we're catching here are
    // if the visibleHeight was misreported before this method ran.
    // We've seen this happen when the component overflows its specified size, specifically:
    //  - if the setWidth() call above caused an already drawn component to reflow and overflow
    //    vertically in a different manner
    //  - if the component was scrolled out of view and clear()'d [cleared components return
    //    specified height from adjustOverflow], and scrolling back into view is draw()ing it again.
    //
    // Note: We observe the 'resized' method of the embedded component, so when resizing due
    // to the component size changing (such as adjustOverflow from the setWidth call above)
    // we get a markForRedrawCall() which in turn runs '_placeEmbeddedComponents()' and resizes/
    // repositions all embedded components.


    // If we're currently in the process of redrawing however, we'll be marked as dirty so this
    // if resized trips here in response to the above setWidth or draw(), markForRedraw will have
    // no effect - catch this case (via an isDirty() check) and explicitly size the
    // row to the new desired height.
    // NOTE: when we redraw we run through '_placeEmbeddedComponents' which positions/sizes
    // all embedded components. We do this in rowNum order, so if there are subsequent embedded
    // components rendered into the grid these should get shifted down automatically if a row
    // above them is expanded by this method.


//    if (position != this._$within) {
        var redrawing = this.isDirty(),
            expectedRowHeight = this.getRowHeight(record,rowNum,this._isFrozenBody()),
            // we need to size the row if
            // - the resized observation didn't trip and mark us as dirty
            // - we are already mid-redraw so being marked as dirty had no effect

            needsResize = !this.isDirty() || redrawing;

        if (needsResize && (expectedRowHeight != this.getRowSize(rowNum))) {
            this.setRowHeight(rowNum, expectedRowHeight, record);
            // refreshing the content ensures we re-write the spacer, which causes the
            // content to top-align properly
            this.refreshRow(rowNum);
        }
//    }

    if (showing) {
        if (position != this._$within) {
            var offset = this.getDrawnRowHeight(rowNum) - component.getVisibleHeight() - 1;
            component.moveTo(null, this.getRowTop(rowNum) + offset);
        }
        if (!component.isVisible()) {
            if (this.shouldAnimateEmbeddedComponent(component)) {
                component.animateShow();
            } else {
                component.show();
            }
        }
    }

    this.updateEmbeddedComponentZIndex(component);

},


alignSnapToMap:{
    left:{
        top:"TL",
        center:"L",
        bottom:"BL"
    },
    right:{
        top:"TR",
        center:"R",
        bottom:"BR"
    },
    center:{
        top:"T",
        center:"C",
        bottom:"B"
    }
},


getEmbeddedComponentSnapTo : function (component, record, rowNum, colNum) {
    if (component.snapTo != null) return component.snapTo;
    if (colNum == null) {
        return "TL"
    }

    var align = this.getCellAlign(record, this.fields[colNum], rowNum, colNum) || "center",
        valign = this.getCellVAlign(record, this.fields[colNum], rowNum, colNum) || "center";

    var result = this.alignSnapToMap[align][valign];
    //this.logWarn("result:"+ result);
    return result;
},

// should this embeddedComponent animate show?
shouldAnimateEmbeddedComponent : function (component) {
    return false;
},

// update the zindex of embedded components. Overridden at the LG level by default
updateEmbeddedComponentZIndex : function (component) {
},

getEmbeddedComponent : function (record, colNum) {
    // support specifying rowNum instead

    if (isc.isA.Number(record)) record = this.getCellRecord(record, 0);

    var components = this.grid._getEmbeddedComponents(record);
    if (components == null) return;

    var component = null;

    if (isc.isA.Number(colNum))
        component = components.find({_currentColNum: colNum, _embedBody: this.getID()});
    else
        component = components.find({_embedBody: this.getID()});

    return component;
},

removeEmbeddedComponent : function (record, component, suppressRedraw) {
    // support specifying rowNum instead

    if (isc.isA.Number(record)) record = this.getCellRecord(record, 0);

    var components = this.grid._getEmbeddedComponents(record);
    if (components == null) return;

    // support specifying component by colNum
    if (isc.isA.Number(component))
        component = components.find({_currentColNum: component, _embedBody: this.getID()});

    if (!component) // a single expansion component
        component = components.find({ _embedBody: this.getID() });

    if (!components.contains(component)) return;

    if (this.isObserving(component, "resized")) {
        this.ignore(component, "resized"); // stop watching for resizes
    }
    this.grid._removeEmbeddedComponent(record, component);
    //if (this.grid._getEmbeddedComponents(record).length == 0) this.grid._setEmbeddedComponents(record, null);
    if (this._embeddedComponents) this._embeddedComponents.remove(component);

    // reset redraw w/parent flag to original setting
    component._redrawWithParent = component.__oldRedrawWithParent;
    component.__oldRedrawWithParent = null;

    // reset bubbleMouseEvents setting
    component.bubbleMouseEvents = component._origBubbleMouseEvents;

    var expand = component.embeddedPosition == this._$expand;
    component.embeddedPosition = null;
    component._currentRowNum = null;
    component._currentColNum = null;
    component._embedBody = null;

    // suppress redraw - used when an embedded component is just being shifted to another record
    if (suppressRedraw) {
        // hide even if we don't clear/draw -- this ensures we re-animate if appropriate
        component.hide();
        return;
    }

    if (component.isExpansionComponent && this.grid.expansionComponentPoolingMode != "none") {
        // this is an expansionLayout component
        var exComp = component.members[0];
        if (this.grid.expansionComponentPoolingMode == "destroy") {
            // "destroy" pooling mode
            if (!exComp.isStockComponent) {
                // custom component - remove the custom component from the wrapper layout prior
                // to destroying the layout - don't destroy the custom component,
                // unless grid.destroyCustomExpansionComponents or comp.destroyOnUnembed
                // have been set to true
                if (!this.grid.destroyCustomExpansionComponents && !exComp.destroyOnUnembed) {
                    component.removeMember(exComp);
                    exComp.deparent();
                }
            }
            component.destroy();
        }
    } else {
        if (component.dontAutoDestroy) {
            this.removeChild(component);
        } else if (component.destroyOnUnEmbed) {
            component.destroy();
        } else {
            // clear it and clear up references to the record
            this.removeChild(component);
        }
    }

    // no need to redraw if the component didn't affect the size of any content
    if (expand) {
        this.markForRedraw("removed embedded component");
    }

},

// before each redraw, clear the property holding the rowNum where the component was found.  Hence
// we ensure that if a component isn't found during rendering it gets hidden.
// Leave the colNum intact -
_resetEmbeddedComponents : function () {
    var components = this._embeddedComponents;
    if (components == null) return;
    components.setProperty("_currentRowNum", null);
},

// ensure all embedded components are in the right place.  Called after every redraw.
_placeEmbeddedComponents : function () {
    var components = this._embeddedComponents;
    if (components == null) return;
    // sort by current row num. This means we place the components in the order in which they're
    // drawn within the table. If their heights change and they expand their containing rows, they
    // will therefore also change the top coords of subsequent rows
    components.sortByProperty("_currentRowNum", true);
    for (var i = 0; i < components.length; i++) {
        this.placeEmbeddedComponent(components[i]);
    }
},

// Apply a known z-index to the table so we can float embedded components below it if necessary
getTableZIndex : function () {
    // default Canvas range starts around 200000
    // Give the table a zindex of 1000 - widgets will still float above it by default (even when
    // sendToBack() is called), but we can explicitly force them to appear below it if necessary
    return 1000;
},

// Cell Styling
// --------------------------------------------------------------------------------------------



//> @attr   gridRenderer.recordCustomStyleProperty  ( "customStyle" : string : IRW)
// Denotes the name of a property that can be set on records to display a custom style.
// For example if this property is set to <code>"customStyle"</code>, setting
// <code>record.customStyle</code> to a css styleName will cause the record in question to
// render out with that styling applied to it.  Note that this will be a static
// style - it will not be modified as the state of the record (selected / over etc) changes.
// @see gridRenderer.getCellStyle()
// @visibility external
//<
recordCustomStyleProperty:"customStyle",

//> @attr gridRenderer.showSelectedStyle ( boolean : true : IRW )
// Should the "Selected" style be applied to selected records?
// @see gridRenderer.getCellStyle()
// @visibility external
//<
showSelectedStyle:true,

//>    @method    gridRenderer.getCellStyle()
// Return the CSS class for a cell. By default this method has the following implementation:<br>
// - return any custom style for the record (see +link{GridRenderer.recordCustomStyleProperty})
//   if defined.<br>
// - create a style name based on the result of +link{gridRenderer.getBaseStyle()} and the
//   state of the record.<br>
// The state of the record is indicated by adding a suffix to the base style.
// There are four independent boolean states, which are combined in the order given:
// <ol>
// <li>"Disabled" : whether the cell is disabled; enable by setting the "enabled" flag on record
//     returned by getCellRecord
// <li>"Selected" : whether cell is selected; enable by passing a Selection object as "selection"
// <li>"Over" : mouse is over this cell; enable with showRollovers
// <li>"Dark" : alternating color bands; enable with alternateRowStyles
// </ol>
// For example, with a baseStyle of "myCell", a cell which is selected, which the mouse is over,
// and which is in a dark-colored band will get a styleName of myCellSelectedOverDark.
// <p>
// Cell Styles customizable by:
// <ul>
// <li>attaching a custom style to a record by setting
//    <code>record[this.recordCustomStyleProperty]</code> to some valid CSS style name.
// <li>modifying the base style returned by getBaseStyle() [see that method for further
//     documentation on this]
// <li>overriding this function
// </ul>
// <p>
// "Selected" style can be ignored by setting +link{gridRenderer.showSelectedStyle} to false.
//
//        @param    record        (ListGridRecord)    record object for this row and column
//        @param    rowNum      (number)    number of the row
//        @param    colNum      (number)    number of the column
//
//        @return    (CSSStyleName)    CSS style for this cell
// @group    appearance
// @visibility external
//<
getCellStyle : function (record, rowNum, colNum) {
    // Allow a record to apply it's own style - ignoring our styling code
    if (record && record[this.recordCustomStyleProperty] != null) {
        return record[this.recordCustomStyleProperty];
    }

    // If not using an entirely custom style, determine the cell state and
    // get the appropriate suffix.
    var styleIndex = this.getCellStyleIndex(record, rowNum, colNum);

    return this.getCellStyleName(styleIndex, record, rowNum, colNum);
},


getCellStyleName : function (styleIndex, record, rowNum, colNum) {

    var standardSuffixes = isc.GridRenderer.standardStyleSuffixes;

    // Are we dynamically determining the baseStyle from this.getBaseStyle() ?
    // If so, concat baseStyle with the appropriate suffix
    if (this.getBaseStyle) {

        var baseStyle = this.getBaseStyle(record, rowNum, colNum);
        // check if the baseStyle returned is exactly the same String instance as
        // this.baseStyle, in which case we can use the precomputed style combinations.  This
        // would happen if someone defined a custom getBaseStyle that usually returns
        // this.baseStyle, infrequently returning special values.
        if (baseStyle !== this.baseStyle) {

            // append the appropriate suffix to the baseStyle
            if (styleIndex == 0) return baseStyle; // styleIndex 0 is the empty suffix
            return baseStyle + standardSuffixes[styleIndex];
        }
    }

    // In this case we're using the default baseStyle

    // Cache the entire set of cellStyles
    if (!this._cellStyles) {
        this._cellStyles = [];
        for (var i = 0; i < standardSuffixes.length; i++) {
            this._cellStyles[i] = this.baseStyle + standardSuffixes[i];
        }
    }
    // return the style
    return this._cellStyles[styleIndex];
},

// return the index of the current state.  The index is a bitfield containing flags for each of
// the mutually exclusive style states: Over, Selected, Disabled, and Dark (Ledger).  The
// purpose of computing an index rather than computing the string directly is for speed.
getCellStyleIndex : function (record, rowNum, colNum) {

    // Note - we have an array of the 12 applicable suffixes in gridRenderer.standardSuffixes
    //
    // 0 = baseStyle
    // 1 = Over(1)
    // 2 = Selected(2)
    // 3 = Selected(2) + Over(1)
    // 4 = Disabled(4)
    // 5 = Disabled(4) + Over(1)
    // 6 = Disabled(4) + Selected(2)
    // 7 = Disabled(4) + Selected(2) + Over(1)
    // 8 = Dark(8)
    // 9 = Dark(8) + Over(1)
    // 10 = Dark(8) + Selected(2)
    // 11 = Dark(8) + Selected(2) + Over(1)
    // 12 = Dark(8) + Disabled(4)
    //
    // NOTE: By default, disabled is actually mutually exclusive with Selected and Over states,
    // so states 5-7 never happen and no style declaration is required.

    var styleIndex = 0;     // base style

    var useAlternateStyle = true;
    if (this.grid != null) {
        var field = this.grid.getField(this.grid.getFieldNumFromLocal(colNum, this));
        useAlternateStyle = !field ? true : field.showAlternateStyle != false;
    }

    // if alternating record or column styles, see if the cell is in a dark band
    if (useAlternateStyle) {
        var isOdd = false;
        if (this.alternateRowStyles) {
            if (this.useRowSpanStyling) {
                // rowSpan-sensitive styling: style based on "rows" determined by spans of
                // left-most cell.  So if first two left-most DOM cells span 4 and 6 rows, first 4
                // rows will be normal, next 6 dark.
                var spanRowNum = this.getSpanningRowNum(rowNum);
                isOdd = (Math.floor(spanRowNum / this.alternateRowFrequency) % 2 == 1);
                if (isOdd) styleIndex += 8;
            } else {
                isOdd = (Math.floor(rowNum / this.alternateRowFrequency) % 2 == 1);
                if (isOdd) styleIndex += 8;
            }
        }
        // if alternating column styles, see if the cell is in a dark band
        if (this.alternateColumnStyles && !isOdd) {
            isOdd = (Math.floor(colNum / this.alternateColumnFrequency) % 2 == 1);
            if (isOdd) styleIndex += 8;
        }
    }

    // Disabled?
    if (!this.cellIsEnabled(rowNum, colNum, record)) {
        styleIndex += 4;

    // Not disabled - check for selected and/or over
    } else {

        // if we're over the row or cell - add 1 to get the Over style

        if (this.shouldShowRollOver(rowNum, colNum) && !this.isPrinting &&
            rowNum == this.lastOverRow &&
            (!this.useCellRollOvers || colNum == this.lastOverCol))
        {
            styleIndex += 1;
        }

        // if selection is enabled, see if the cell is selected
        if (this.showSelectedStyle && this.selectionEnabled()) {
            var isSelected;
            if (this.canSelectCells) {
                var startRow = rowNum;
                if (this.useRowSpanStyling) startRow = this.getCellStartRow(rowNum, colNum);
                isSelected = this.selection.cellIsSelected(startRow, colNum);
            } else {
                isSelected = this.selection.isSelected(record, true);
            }
            // if the cell is selected, add 2 to get the Selected style
            if (isSelected) styleIndex += 2;
        }
    }
    return styleIndex;
},

// when rowSpanning is being used, returns the visual rowNum: the rowNum if the left-most
// column's cells are treated as row boundaries
getSpanningRowNum : function (rowNum) {
    var outerSpanCount = this._outerSpanCount;
    // if we are part of a ListGrid with frozen columns, the first column is in the frozen
    // body
    if (this.grid && this.grid.frozenBody) {
        var outerSpanCount = this.grid.frozenBody._outerSpanCount;
    }
    // TODO: support incremental rendering: go back to last non-empty entry before the
    // requested rowNum, call getRowSpan() for all intervening rows and record results.

    return outerSpanCount != null ? outerSpanCount[rowNum] : rowNum;
},

//>    @method    gridRenderer.cellIsEnabled()    ([A])
// Whether this cell should be considered enabled.  Affects whether events will fire for the
// cell, and the default styling behavior in getCellStyle.
//
// @group    selection, appearance
//
// @param    rowNum    (number)    row number for the cell
// @param    colNum    (number)    column number of the cell
// @return    (boolean)    whether this record is enabled or not
// @visibility external
//<

cellIsEnabled : function (rowNum, colNum, record) { return true; },

// Element IDs
// --------------------------------------------------------------------------------------------

//> @method gridRenderer.getTableElementId()  ([A])
// Get the DOM ID that should be used for the table element.  For integration with legacy test
// scripts.
// @visibility testAutomation
//<
getTableElementId : function () {
    return this.getCanvasName() + "table";
},

//> @method gridRenderer.getRowElementId()  ([A])
// Get the DOM ID that should be used for a row element.  For integration with legacy test
// scripts.
//
// When using incremental rendering, the <code>rowNum</code> param represents the
// rowNum in virtual coordinates, and the <code>physicalRowNum</code> param represents the
// index that the row will ultimately have in table.rows.
//
// @param    rowNum  (number)          <b>virtual</b> row number
// @param    physicalRowNum (number)   <b>physical</b> row number
// @visibility testAutomation
//<


//> @method gridRenderer.getCellElementId()  ([A])
// Get the DOM ID that should be used for a cell element.  For integration with legacy test
// scripts.
//
// When using incremental rendering, the <code>rowNum</code> and <code>colNum</code> params
// represents virtual coordinates, and the <code>physicalRowNum</code> param represents the
// index that the row/cell will ultimately have in table.rows or row.cells.
//
// @param    rowNum         (number)   <b>virtual</b> row number
// @param    physicalRowNum (number)   <b>physical</b> row number
// @param    colNum         (number)   <b>virtual</b> col number
// @param    physicalColNum (number)   <b>physical</b> col number
// @visibility testAutomation
//<

// Table Manipulation
// --------------------------------------------------------------------------------------------

getDOMTable : function (logicalRowNum, logicalColNum) {
    if (this.cacheDOM) return this.getTableChunkAt(logicalRowNum, logicalColNum);

    // bail out fast if asked for a cell that isn't currently rendered
    // this method is called from 'getTableElement()', so rowNum / colNum may be null
    if ((logicalRowNum != null &&
         (logicalRowNum - this._firstDrawnRow < 0 ||
          logicalRowNum > this._lastDrawnRow))
        ||
        (logicalColNum != null &&
         (logicalColNum - this._firstDrawnCol < 0 ||
          logicalColNum > this._lastDrawnCol))
       )
         return null;

    var table = this._tableElement;
    if (table == null) {
        var tableName = this.getTableElementId();
        var table = isc.Element.get(tableName);

        if (table == null) return null;
        // If we're mid-redraw, don't re-cache the current table element
        if (this._suppressTableCaching) {

            this.logInfo("getTableElement() called while updating table HTML. " +
                "This call may be invalid as the table is being rewritten in the DOM. " +
                "Suppressing caching of the current element.", "redrawing");
            return table;
        }
    }
    // cache table element
    return this._tableElement = table;
},

//>    @method    gridRenderer.getTableElement()    ([A])
//        Get the element for the TD that holds a particular cell, specified as row/column indices.
//
//        If called with no parameters, returns the table itself.
//        If called with rowNum only, returns the row element
//        If called with colNum, returns a particular cell.
//
//        In all cases, returns null if the table, row, or cell cannot be found.
//
//        NOTE: calling this repeatedly is expensive as it makes multiple DOM lookups.
//
//        @param    [rowNum]    (number)    Record number to get cell for.
//                                        You DO NOT need subtract the startRow from this.
//        @param    [colNum]    (number)    Field number to get cell for.
//        @return    (DOMElement)            Table, row or cell of the list body table.
//                                        Returns null if the element can't be obtained.
//        @group    drawing
//<

getTableElement : function (logicalRowNum, logicalColNum) {
    var table = this.getDOMTable(logicalRowNum, logicalColNum);

    if (logicalRowNum == null) return table;

    if (!table) return null;

    // if we're using incremental rendering, the HTML we draw only contains _firstDrawnRow ->
    // _lastDrawnRow, so when asked for row X we subtract _firstDrawnRow to find the
    // corresponding DOM element.  "rowNum" is now the rendered row number in the DOM table
    var rowNum = logicalRowNum - (this._firstDrawnRow > 0 ? this._firstDrawnRow : 0);


    if (rowNum < 0) {
        //this.logWarn("bailing on negative rowNum");
        return null;
    }

    var row;
    // use cached row lookup
    if (this._rowElements != null) row = this._rowElements[rowNum];
    if (row == null) row = table.rows[rowNum];


    if (row == null) return null;

    // cache row lookup (invalidated on body redraw)
    // If we're mid-redraw, don't re-cache the current table element
    if (!this._suppressTableCaching) {
        if (this._rowElements == null) this._rowElements = [];
        this._rowElements[rowNum] = row;
    }

    if (logicalColNum == null) return row;

    // for incremental column rendering: if we're not drawing all columns, the DOM will contain
    // cells only for the columns we actually draw.
    var colNum = logicalColNum - this._firstDrawnCol;


    if (colNum < 0) {
        //this.logWarn("bailing on negative colNum");
        return null;
    }


    if (this.allowRowSpanning && this.getRowSpan) {

        var startRow = this.getCellStartRow(logicalRowNum, colNum);
        if (startRow != rowNum) {
            //this.logWarn("detected spanning cell extending from " + [startRow, colNum] +
            //             " to " + [logicalRowNum, colNum]);

            // cell starts in a previous row - switch to the row that contains this cell
            rowNum = startRow;
            row = this.getTableElement(startRow);
        }

        if (row.childNodes.length < (this._lastDrawnCol - this._firstDrawnCol + 1)) {
            // this row has less cells than the number of columns we drew, indicating cells from
            // previous rows spanned into this row (but not at this column, which we already
            // checked).

            // figure out many cells are missing up to the column we're interested in
            var skips = 0;
            for (var i = 0; i < colNum; i++) {
                if (this.fields[i]._rowSpans != null &&
                    this.fields[i]._rowSpans[rowNum] != null &&
                    this.fields[i]._rowSpans[rowNum] != rowNum) skips++;
            }
            //this.logWarn("in row: " + rowNum + " skipping " + skips + " cells");

            // and adjust the column number appropriately
            colNum -= skips;
        }
    }

    // actually got all the way to the cell level -- return the appropriate cell
    return row.childNodes[colNum];
},

// Cell Style and HTML Updates
// --------------------------------------------------------------------------------------------

//>    @method    gridRenderer._updateCellStyle()
// Update the CSS styling for a cell.  Will also update the row's height, and the cell's
// innerHTML if appropriate.
//
//        @group    appearance
//        @param    [record]        (object)    reference to the record object who's style is being set
//        @param    rowNum          (number)    row number of the cell
//      @param  colNum          (number)    col number of the cell
//        @param    cell            (DHTML object)    reference to the HTML table cell element
//      @param  [className]     (string)    name of the CSS class of the style to be used
//
//<
_updateCellStyle : function (record, rowNum, colNum, cell, className) {

    // get the DOM cell object if not provided
    if (cell == null) cell = this.getTableElement(rowNum, colNum);
    if (cell == null) return; // cell not currently drawn

    if (record == null) record = this.getCellRecord(rowNum, colNum);
    // determine the CSS style className if not provided
    if (className == null) className = this.getCellStyle(record, rowNum, colNum);

//this.logWarn("setting: " + [rowNum, colNum] + " to className:" + className);

    if (this.fastCellUpdates) {

        cell.style.cssText = this._getCompleteCellCSSText(record, rowNum, colNum, className);
    } else {
        // update the classname on the DOM cell object
        if (cell.className != className) cell.className = className;
        // If this.getCellCSSText has been defined, set cell.style.cssText

        if (this.getCellCSSText) {
            // Use this._getCompleteCellCSSText
            // This handles the overflow settings for Moz, converting the
            // getCellCSSText stringMethod to a method, etc.
            cell.style.cssText = this._getCompleteCellCSSText(record, rowNum, colNum, className)
        }
    }
    // if aspects of styling are inncorporated into the cell's innerHTML, refresh the cell
    if (this.shouldRefreshCellHTML(record, rowNum, colNum)) {
        this.refreshCellValue(rowNum, colNum);
    }


    if (!this.isDrawn()) return;

    if (!this.suppressRowHeightUpdate) this._updateRowHeight(record, rowNum, className);
},

_updateRowHeight : function (record, rowNum, className) {

    var shouldClip = this.fixedRowHeights &&
                          (this.shouldFixRowHeight == null ||
                           this.shouldFixRowHeight(record, rowNum) != false),
        newHeight = (this.getRowHeight != null ? this.getRowHeight(record, rowNum, this._isFrozenBody())
                                                  : this.cellHeight);

    this.setRowHeight(rowNum, newHeight, record, className, shouldClip);

},

_$nobr:"NOBR",
_$cellClipDiv:"cellClipDiv",

_getCellClipDiv : function (cellElement) {
    if (cellElement == null) return null;

    var div = cellElement.firstChild;
    if (!div) return null;
    // In IE the first child of a cell is actually a NOBR element - we need to look inside that
    // to get the cell clip div
    if (div.tagName == this._$nobr) div = div.firstChild;
    if (div &&
        (div.cellClipDiv ||
         (div.getAttribute && div.getAttribute(this._$cellClipDiv)) ) )
    {
        return div;
    }
    return null;
},

//> @method gridRenderer.setRowHeight()
// Sets the height of some row to the height passed in.
// This is a styling effect only - a redraw will revert to the height as derived from
// this.cellHeight / this.getRowHeight()
// @param rowNum (number) rowNum to set height on
// @param newHeight (number) height for the row
//<
// Additional params are not required, but make the method more efficient
// Also used by showInlineEditor to make a row overflow:visible for tall editors
_$height:"height",
_$minHeight:"minHeight",
setRowHeight : function (rowNum, newHeight, record, className, shouldClip, instantOverflow) {
    var firstDrawnCol = this._firstDrawnCol,
        lastDrawnCol = this._lastDrawnCol;

    if (shouldClip == null) {
        if (record == null) record = this.getCellRecord(rowNum, firstDrawnCol);
        shouldClip = this.fixedRowHeights &&
                                          (this.shouldFixRowHeight == null ||
                                           this.shouldFixRowHeight(record, rowNum) != false);
    }


     var firstCell = this.getTableElement(rowNum, firstDrawnCol),
         currentSpecifiedHeight = firstCell ? parseInt(firstCell.height) : null,
         heightChanged
    ;

    if (!isc.isA.Number(currentSpecifiedHeight)) currentSpecifiedHeight = null;

    if (isc.Browser.isStrict &&
        ((isc.Browser.isMoz && isc.Browser.version >= 17) ||
         (isc.Browser.isSafari || isc.Browser.isIE))
    ) {
        if (record == null) record = this.getCellRecord(rowNum, firstDrawnCol);
        var cellStyle = className;
        if (cellStyle == null) cellStyle = this.getCellStyle(record, rowNum, firstDrawnCol)

        newHeight -= isc.Element._getVBorderSize(cellStyle);
        if (!this.fixedRowHeights) newHeight -= (this.cellPadding *2)

    }

    // if we were previously clipping and will not any longer
    if ((!shouldClip && currentSpecifiedHeight != null) ||
        // or we're changing the specified height (clipped or not)
        (currentSpecifiedHeight != newHeight &&
        !(currentSpecifiedHeight == null && newHeight == isc.emptyString)))
    {
        // the height of the cell (therefore the row) has changed
        heightChanged = true;
    }
    // If the height of this row has changed, we need to update (or clear) the specified
    // heights of each cell in the row.

    if (!heightChanged) return;


    var numericHeight = isc.isA.Number(newHeight);
    if (numericHeight && newHeight <=0) newHeight = shouldClip ? 0 : 1;

//    this.logWarn("height changed for cell in row: " + rowNum +
//                 ", currentSpecifiedHeight: " + currentSpecifiedHeight +
//                 ", shouldClip?:" + shouldClip +
//                 " (derived from firstCell.height: " + firstCell.height + ")" +
//                 ", newHeight: " + newHeight);

    var currentRow = this.getTableElement(rowNum);
    // Don't crash if passed a bad row num

    if (currentRow == null) return;

    if (newHeight == 0 && shouldClip) {
        currentRow.style.display = "none";
        //var firstCell = this.getTableElement(rowNum, firstDrawnCol);
        //this.logWarn("first row height is: " + firstCell.offsetHeight);
    } else {
//TODO: when printing sub-grids, we can get crashes here because currentRow is null - needs a looksee
//   } else if (currentRow != null) {
        // should theoretically be "table-row", but IE doesn't currently support that value,
        // and they all seem to accept ""
        currentRow.style.display = isc.emptyString;

        var cssProp = (!isc.Browser.isIE || isc.Browser.isStrict) ? this._$height
                                                                  : this._$minHeight,
            cellHeight = numericHeight ? newHeight + isc.px : newHeight,
            clipDivHeight = shouldClip ? cellHeight : isc.emptyString;
        for (var i = firstDrawnCol; i <= lastDrawnCol; i++) {
            var currentCell = this.getTableElement(rowNum, i);
            if (currentCell) {
                if (shouldClip) {
                    currentCell.height = cellHeight;
                    currentCell.style[cssProp] = isc.emptyString;
                } else {
                    currentCell.height = isc.emptyString;
                    currentCell.style[cssProp] = cellHeight;
                }

                var clipDiv = this._getCellClipDiv(currentCell);
                if (clipDiv) {
                    if (this._useMaxHeightForCellDivCSSHeight()) {
                        clipDiv.style.maxHeight = clipDivHeight;
                    } else {
                        clipDiv.style.height = clipDivHeight;
                    }
                }
            }
        }

    }


    if (isc.Browser.isSafari && this._forceRowRefreshForAnimation) {
        var row = this.getTableElement(rowNum);
        if (row != null) {
            row.innerHTML = row.innerHTML;
        }
    }

    // clear the cache of rowHeights since at least this one has changed, and mark for
    // adjustOverflow as the overall height of the body will have changed too.
    this._clearTableCache();

    if (instantOverflow) {
        this.adjustOverflow("cell height changed");
    } else {
        this._markForAdjustOverflow("cell height changed");
    }

},

//> @method gridRenderer._getCompleteCellCSSText() (I)
//
//  Returns complete CSS text for a cell.
//
//  If this.fastCellUpdates is true, this method will return both the raw CSS text associated
//  with the style, and any custom CSS text set up by the public getCellCSSText() method
//  If false, this method returns no style CSS text, and just falls through to getCellCSSText()
//
//        @param    record        (ListGridRecord)    record for this row or cell
//        @param    rowNum      (number)    row number
//        @param    colNum      (number)    column number
//      @param  [style]     (string)    CSS class style name to apply
//
//        @return    (CSSText)    CSS text to style this cell
//  @group    appearance
//<
_$semi:";",
_$zeroVPadding:"padding-top:0px;padding-bottom:0px;",
_$overflowHidden:"overflow:hidden;",
_getCompleteCellCSSText : function (record, rowNum, colNum, className) {
    var cssText = null;
    // Make sure top and bottom padding are set to zero if fixedRowHeights is true

    if (this.fixedRowHeights) cssText = this._$zeroVPadding;
    else {
        cssText = this._getMinHeightCSSText(record,rowNum);
    }

    if (isc.Browser.isIE8Strict) {
        if (cssText == null) cssText = this._$overflowHidden;
        else cssText += this._$overflowHidden;
    }

    // For Moz, pre-pend the width and overflow cssText

    if (isc.Browser.isMoz || isc.Browser.isSafari) {
        if (cssText == null) cssText = this._getCSSTextForColWidth(colNum);
        else cssText += this._getCSSTextForColWidth(colNum);
    }

    if (this.fastCellUpdates) {
        // figure out the style for this cell if not provided
        if (className == null) className = this.getCellStyle(record, rowNum, colNum);
        //this.logWarn("_getCompleteCellCSSText style: " + className);

        // get CSS text for this style
        var styleText = isc.Element.getStyleText(className, true);

        if (styleText == null && isc.Page._remoteStyling) {
            this.logInfo("fastCellUpdates set to true but this page loads styles from a " +
                "remote stylesheet. This is unsupported - disabling fastCellUpdates.");
            this.fastCellUpdates = false;
            this.redraw();
        }
        if (cssText != null) cssText += styleText;
        else cssText = styleText;
    }

    // Get any custom CSSText derived from this.getCellCSSText
    if (this.getCellCSSText) {
        var customCSSText = this.getCellCSSText(record, rowNum, colNum)

        if (customCSSText != null) {
            // Ensure the custom css text ends with a semi

            if (!customCSSText.endsWith(this._$semi)) {
                customCSSText += this._$semi;
            }
            if (cssText != null) cssText += customCSSText
            else cssText = customCSSText
        }
    }

    // If we skipped NOBR tags, write out the css equivalent.
    if (isc.Browser.isIE8Strict && !this.wrapCells) {
        cssText += ";white-space: nowrap;"
    }
    return cssText;
},

// does this cell need to update its HTML in order to show hiliting/styling
shouldRefreshCellHTML : function (record, rowNum, colNum) {
    return this.showHiliteInCells;
},


// Helper method to check that we can safely refresh a cell (or row) without delaying
_readyToRefreshCell : function (rowNum, colNum) {

    if ((isc.EH._handlingMouseUp || isc.EH._handlingMouseDown) && isc.EH.lastEvent.target == this) {

        var eventRow = this.getEventRow();
        if (eventRow != rowNum) return true;

        if (colNum != null) {
            var eventCol = this.getEventColumn();
            if (colNum != eventCol) return true;
        }
        // If the event occurred on the same row (and col for a cell), we can't redraw in
        // the same thread
        return false;
    }


    var EH = this.ns.EH;
    if (EH._handlingTouchEventSequence()) {
        var cellElem = this.getTableElement(rowNum, colNum);
        if (cellElem != null) {
            if (isc.Browser.isMobileWebkit || isc.Browser.isChrome) {
                var mouseDownEvent = EH.mouseDownEvent;
                if (mouseDownEvent != null &&
                    mouseDownEvent.DOMevent.target != null &&
                    cellElem.contains(mouseDownEvent.DOMevent.target))
                {
                    return false;
                }
            }
        }
    }

    return true;
},

//> @method gridRenderer.refreshCellValue() ([A])
// Update just cell value without updating cell styling.
// @param rowNum (number) Row number of the cell to refresh
// @param colNum (number) Column number of the cell to refresh
//<
refreshCellValue : function (rowNum, colNum) {
    // get a pointer to the cell, if possible
    var cell = this.getTableElement(rowNum, colNum);
    if (!cell) return; // cell not currently drawn

    // If we need to delay the refresh, fire again after a delay
    if (!this._readyToRefreshCell(rowNum, colNum)) {
        this.delayCall("refreshCellValue", [rowNum, colNum]);
        return;
    }

    var record = this.getCellRecord(rowNum, colNum),
        field = this.fields[colNum]
    ;

    // Allow refreshing of null records - this may occur with separator rows, loading rows,
    // etc.
    if (!field) {
        //>DEBUG
        this.logDebug("refreshCell called for invalid field " + colNum); //<DEBUG
        return;
    }

    // update the cell's innerHTML: Use the helper methods we made use of in getTableHTML() to
    // write in any additional innerHTML such as DIV tags in the cell, etc.

    // use a StringBuffer rather than normal concatenation
    var sb = isc.StringBuffer.create();

    // determine whether we need to write a DIV for this cell
    var writeDiv = this._writeDiv(rowHeight),
        nowrap = !this.wrapCells && !isc.Browser.isIE8Strict;
    if (writeDiv) {
        // cellclipdivstart includes an open style=' attr
        sb.append(this._$cellClipDivStart);

        // height enforcement
        var rowHeight = (this.getRowHeight != null ? this.getRowHeight(record, rowNum, this._isFrozenBody())
                                                   : this.cellHeight);
        sb.append(this._getCellDivCSSHeight(rowHeight, record, rowNum));

        // width enforcement (unless colspan *)
        var drawRecordAsSingleCell = this._drawRecordAsSingleCell(rowNum, record);
        if (!drawRecordAsSingleCell) {
            sb.append(this._getFieldDivWidthHTML(colNum));
        }

        if (nowrap) sb.append("white-space:nowrap;");

        sb.append("'>");
    }

    // NOBR tags if we're not wrapping cells and we're not writing out `white-space:nowrap' on
    // a cell div.
    var writeNOBR = nowrap && !writeDiv;
    if (writeNOBR) {
        sb.append("<NOBR>");
    }
    // Get the actual value for the cell
    sb.append(this._getCellValue(record, rowNum, colNum));
    // close the NOBR tag if necessary
    if (writeNOBR && this.closeNOBRs) {
        sb.append("</NOBR>");
    }
    // close the DIV if necessary
    if (writeDiv) {
        sb.append("</DIV>");
    }
    // Actually apply the innerHTML to the innerHTML of the cell.
    cell.innerHTML = sb.release();
},

//>    @method    gridRenderer.setCellStyle()
// Set the CSS class of a record
//        @group    appearance
//
//        @param    rowNum (number)    row number to set class of
//        @param    colNum    (number) column number to set class of
//        @param    [className]    (CSSStyleName)    name of the CSS class to set to; if not specified,
//                                          will use getCellStyle()
//<
setCellStyle : function (rowNum, colNum, className) {
    // Just fall through to setRowStyle
    return this.setRowStyle(rowNum, className, colNum);
},


//>    @method    gridRenderer.setRowStyle()
// Set the CSS class of a record
//        @group    appearance
//
//        @param    rowNum (number)    record number to set class of.  This takes this._firstDrawnRow into account
//        @param    [className]    (CSSStyleName)    name of the CSS class to set to; if not specified, will
//                                          use getCellStyle()
//        @param    [colNum]    (number)        column number to set class of.  If not specified, will
//                                          set all columns in that row.
//<
setRowStyle : function (rowNum, className, colNum) {
    if (isc._traceMarkers) arguments.__this = this;
    // navigate into the DOM and change the contents of the native table cells

    // if the rowNum is null, use this.selection.lastSelectionItem
    if (rowNum == null || rowNum < 0) {
        this.logWarn("setRowStyle: bad rowNum: " + rowNum);
        return false;
    }

    // verify that we've drawn the table
    var cell = this.getTableElement(rowNum, colNum);

    if (cell == null) {
        // when incremental rendering is on, this is a normal condition indicating that we are
        // trying to update some row/cell that has been scrolled out of view, hence no longer
        // exists.  NOTE: don't log, we might be calling this for thousands of unrendered cells.
        //this.logDebug("setRowStyle(): cell (" + rowNum + "," + colNum + ") not present");
        return false;
    }

    var record = this.getCellRecord(rowNum, colNum);

    // for eg, rows that are about to be completely refreshed anyway
    if (record && record._ignoreStyleUpdates) {
        return;
    }


    // if a colNum was specified, update just the individual cell (we got a pointer to it
    // above)
    if (colNum != null) {
        this._updateCellStyle(record, rowNum, colNum, cell, className);
    } else {

        var row = this.getTableElement(rowNum);
        if (row != null) {
            var td = "TD",
                firstCol = (!this.shouldShowAllColumns() ? this._firstDrawnCol : 0),
                lastCol = (!this.shouldShowAllColumns() ? this._lastDrawnCol : this.fields.length-1),
                // If incremental rendering is enabled, the indices of the cells in the DOM
                // will not match the colNum of the cell being updated.
                renderedCellNum = 0;

            var recursive = this.suppressRowHeightUpdate;
            this.suppressRowHeightUpdate = true;

            for (var fieldNum = firstCol; fieldNum <= lastCol; fieldNum++, renderedCellNum++) {

                var cell;
                // If we're showing columns separately, we'll style the whole table for the
                // column (Nav case)
                if (this.showColumnsSeparately || this.cacheDOM) {
                    cell = this.getTableElement(rowNum, fieldNum);


                // Otherwise we'll style the individual cells in the row.
                } else {
                    cell = row.childNodes[renderedCellNum];

                }
                if (cell == null) continue;

                // Pass in the optional record object, className and cell objects to avoid them
                // being re-calculated.
                this._updateCellStyle(record, rowNum, fieldNum, cell, className);
            }

            if (!recursive) {
                delete this.suppressRowHeightUpdate;
                this._updateRowHeight(record, rowNum, className);

            }
        }
    }

    // return true to indicate that we were able to update the cell(s)
    return true;
},

//>    @method    gridRenderer.refreshCellStyle()
// Refresh the styling of an individual cell without redrawing the grid.
// <P>
// The cell's CSS class and CSS text will be refreshed, to the current values returned by
// getCellStyle() and getCellCSSText() respectively.
// <P>
// The cell's contents (as returned by getCellValue()) will <b>not</b> be refreshed.  To
// refresh both styling and contents, call refreshCell() instead.
//
//        @group    appearance
//        @param    rowNum (number)    row number of cell to refresh
//        @param    colNum    (number) column number of cell to refresh
//
// @see refreshCell() to update cell contents too
// @visibility external
//<
// NOTE:
// - className param not public because we don't persist the change
//        @param    [className]    (CSSStyleName)    name of the CSS class to set to; if not specified,
//                                          will use getCellStyle()
refreshCellStyle : function (row, col, className) {
    // this is a synonym for setCellStyle();
    // We could also fall through to refreshCellStyles() but this would force us to create an
    // array object to pass in.
    return this.setCellStyle(row, col, className);
},

//>    @method        gridRenderer.refreshCell()    ([A])
// Refresh an individual cell without redrawing the grid.
// <P>
// The cell's value, CSS class, and CSS text will be refreshed, to the current values returned
// by getCellValue(), getCellStyle() and getCellCSSText() respectively.
//
//        @group    appearance
//        @param    rowNum (number)    row number of cell to refresh
//        @param    colNum    (number) column number of cell to refresh
//
// @see refreshCellStyle() to update just styling
// @visibility external
//<
refreshCell : function (rowNum, colNum) {
    this.refreshCellStyle(rowNum, colNum);
    // refresh the value too unless it's already been refreshed as part of styling
    if (!this.shouldRefreshCellHTML()) this.refreshCellValue(rowNum, colNum);
},

//>    @method        gridRenderer.refreshRow()    ([A])
// Refresh an entire row of cells without redrawing the grid.
// <P>
// The cells' values, CSS classes, and CSS text will be refreshed, to the current values
// returned by getCellValue(), getCellStyle() and getCellCSSText() respectively.
//
//        @group    appearance
//        @param    rowNum (number)    row number of cell to refresh
//
// @see refreshCellStyle() to update just styling
// @see refreshCell()
// @visibility external
//<
refreshRow : function (rowNum) {
    if (!this._readyToRefreshCell(rowNum)) {
        this.delayCall("refreshRow", [rowNum]);
    }
    for (var i = 0; i < this.fields.length; i++) {
        this.refreshCell(rowNum, i);
    }
},

//>    @method    gridRenderer.refreshCellStyles()    ([A])
//  @group    selection, appearance
//
//  Update the style of a list of cells. (Used to show selection changes when cell selection is
//  enabled)
//
//        @param    cellList    (Array)
//              Array of [rowNum, colNum] array pairs.
//        @param    [className] (CSSStyleName)
//              Name of the CSS class to set to; if not specified, will use getCellStyle()
//
//        @return    (boolean)    true == actually updated now, false == will update later
//
//<
refreshCellStyles : function (cellList, className) {
    //>DEBUG
    this.logDebug("refreshing cell styles: " + cellList.length + " cells");
    //<DEBUG

    //this.logWarn("refresh cells: " + this.echoAll(cellList));

    // NOTE: this is a very time critical method, as it is called every mouseMove during drag
    // selection.

    for (var i = 0; i < cellList.length; i++) {
        var rowNum = cellList[i][0],
            colNum = cellList[i][1];



        var cell = this.getTableElement(rowNum, colNum);
        if (cell == null) {
            // when incremental rendering is on, this is a normal condition indicating that we
            // are trying to update some row/cell that has been scrolled out of view, hence no
            // longer exists.  NOTE: don't log, we might be calling this for thousands of
            // unrendered cells.
            //this.logDebug("setRowStyle(): cell (" + rowNum + "," + colNum + ") not present");
            continue;
        } else {
            // no need to pass in the record object, this will be calculated in _updateCellStyle()
            this._updateCellStyle(null, rowNum, colNum, cell, className);
        }
    }
    // return true to indicate that we were able to update the cell(s)
    return true;
},

// Size Detection
// --------------------------------------------------------------------------------------------

//> @method gridRenderer.getCellPageRect() ([A])
// Returns the page offsets and size of the cell at the passed row and column.  If auto-sizing
// is enabled, sizes are not definitive until the grid has finished drawing, so calling this
// method before drawing completes will return the configured column sizes.
// @param rowNum (number) row index of the cell
// @param colNum (number) column index of the cell
// @return (Array of Integer) the page rect of the passed cell
// @group sizing, positioning
// @visibility external
//<
getCellPageRect : function (rowNum, colNum) {
    return [
        this.getColumnPageLeft(colNum),
        this.getRowPageTop(rowNum),
        this.getColumnSize(colNum),
        this.getRowSize(rowNum)];
},

//> @method gridRenderer.getColumnLeft() ([A])
// Return the left coordinate (in local coordinate space) of a particular column.
// @param colNum (Integer) number of the column
// @return (Integer) left coordinate of the passed colNum
// @group sizing, positioning
// @visibility external
//<
getColumnLeft : function (colNum) {
    // Note: we don't have to worry about undrawn columns because this._fieldWidths has all
    // column widths, not just the drawn ones.

    // textDirection: we calculate field sizes from right to left in RTL mode

    if (this.isRTL()) {
        return this.getViewportWidth() - this._fieldWidths.sum(0, colNum+1);
    } else {
        // otherwise return the width of fields 0-colNum
        return this._fieldWidths.sum(0, colNum);
    }
},


//> @method gridRenderer.getColumnPageLeft() ([A])
// Return the left coordinate for a given column number as a GLOBAL coordinate
// @param colNum (Integer) number of the column
// @return (Integer) page left offset of the passed colNum, or null if undrawn or no such column
// @group sizing, positioning
// @visibility external
//<
getColumnPageLeft : function (colNum) {
    var columnLeft = this.getColumnLeft(colNum);
    if (columnLeft == null) return null;
    var left = this.getPageLeft() - this.getScrollLeft() + columnLeft;
    if (this.isRTL()) {
        if (this.vscrollOn) left += this.getScrollbarSize();
        left += this.getScrollWidth() - this.getViewportWidth();
    }
    return left;
},


//> @method gridRenderer.getColumnWidth() ([A])
// Return the width of a particular column.
// @param colNum (number) number of the column.
// @return (number) width
// @group sizing, positioning
// @visibility external
//<
getColumnWidth : function (colNum) {
    // return the width of the column from the _fieldWidths property
    return this._fieldWidths[colNum];
},

//>    @method    gridRenderer.getInnerColumnWidth()    ([A])
//        Return the width of a particular column adjusted for this.cellPadding / cellSpacing.
//        @group    sizing, positioning
//        @param    colNum    (number)    number of the column.
//        @return    (number)    inner width
//<
getInnerColumnWidth : function (colNum) {
    var width = this.getColumnWidth(colNum);
    if (width == null) return null;

    // Note: cell spacing still breaks alignment with ListGrid headers in both firefox and IE.
    // However this is a non-exposed feature for now
    return (width - (2* this.cellSpacing + this._getCellHBorderPad()));
},

// method to get, and cache horizontal cell padding size (based on this.cellPadding and styling)
// Used for sizing the cell-level clipping div, etc.

_getCellHBorderPad : function (recalc) {
    if (!recalc && this._cellHBorderPad != null) return this._cellHBorderPad;

    var firstStyle = this._getFirstRecordStyle(),
        padLeft = isc.Element._getLeftPadding(firstStyle, true),
        padRight = isc.Element._getRightPadding(firstStyle, true),
        border = isc.Element._getHBorderSize(firstStyle);

    if (padLeft == null) padLeft = this.cellPadding;
    if (padRight == null) padRight = this.cellPadding;

    this._cellHBorderPad = (padLeft + padRight + border);
    return this._cellHBorderPad;
},

//>    @method    gridRenderer.getRowTop()    ([A])
// Returns the top coordinate for a given row number, relative to the top of body content.  Use
// +link{getRowPageTop()} for a page-relative coordinate.
// <P>
// This method is reliable only for rows that are currently drawn, which is generally only rows
// that are visible in the viewport.  If row heights vary (see <code>fixedRowHeights</code>),
// coordinates for rows that are not currently shown are rough approximations.
//
// @param rowNum (int)
// @return (int) Y-coordinate
// @group positioning
// @visibility external
//<
getRowTop : function (rowNum) {
    // undrawn rows before or after the drawn area are treated as having fixed height
    if (rowNum < this._firstDrawnRow) return this.getAvgRowHeight() * rowNum;

    var undrawnHeight = this._getUndrawnHeight(),
        drawnHeights = this._getDrawnRowHeights();

    if (rowNum > this._lastDrawnRow) {
        // undrawn rows after the drawn area are treated as having fixed height
        return undrawnHeight + drawnHeights.sum() +
                    (((rowNum-1) - this._lastDrawnRow) * this.getAvgRowHeight());
    }
    // otherwise return the sum of heights of records 0-rowNum
    return undrawnHeight + drawnHeights.sum(0, rowNum - this._firstDrawnRow);
},

//>    @method    gridRenderer.getRowPageTop()    ([A])
// Returns the Y-coordinate for a given row number as a page-relative coordinate.  See
// +link{getRowTop()}.
//
// @param rowNum (int)
// @return (int) Y-coordinate
// @group positioning
// @visibility external
//<
getRowPageTop : function (rowNum) {
    return this.getPageTop() + this.getTopBorderSize() +
                (this.getRowTop(rowNum)- this.getScrollTop());
},

//>    @method    gridRenderer.getRowSize()    ([A])
// Get the drawn height of a row.
//
// @param rowNum (number)
//
// @return (number) height
// @group sizing, positioning
// @deprecated As of SmartClient 8.0, use +link{gridRenderer.getDrawnRowHeight}.
//<
getRowSize : function (rowNum) {
    return this.getDrawnRowHeight(rowNum);
},

//>    @method    gridRenderer.getDrawnRowHeight() ([A])
// Get the drawn height of a row.
//
// @param rowNum (number)
//
// @return (number) height
// @group sizing, positioning
//<
getDrawnRowHeight : function (rowNum) {

    // treat all undrawn rows as though they were cellHeight tall
    if (this._firstDrawnRow == null || this._lastDrawnRow == null
        || rowNum < this._firstDrawnRow || rowNum > this._lastDrawnRow)
    {
        return this.getAvgRowHeight();
    }

    var visibleRowNum = rowNum - this._firstDrawnRow,
        heights = this._getDrawnRowHeights();
    return heights[visibleRowNum];
},

//>    @method    gridRenderer.getColumnSize()    ([A])
// Get the drawn width of a column.
//
// @param colNum (number)
//
// @return (number) width in pixels
// @group sizing, positioning
//<
// NOTE: this function must be named getColumnSize because getColumnWidth refers to specified
// width.
getColumnSize : function (colNum) {
    if ((this.fixedFieldWidths && !this.autoSize) ||
        (colNum < this._firstDrawnCol || colNum > this._lastDrawnCol))
    {
        // fixed sizes, or not rendered; return specified size
        return this.getColumnWidth(colNum);
    }
    var visibleColNum = colNum - this._firstDrawnCol,
        widths = this.getColumnSizes();
    return widths[visibleColNum];
},


// get the total height of all rows that are not currently drawn because they are above the
// viewport (and out of drawAhead range)
_getUndrawnHeight : function () {
    return this._firstDrawnRow * this.getAvgRowHeight();
},

// get the heights of all drawn rows
_getDrawnRowHeights : function () {
    //!DONTCOMBINE
    if (this._rowHeights != null) return this._rowHeights;

    // create a new array and store it in the _recordHeights slot
    var heights = this._rowHeights = [];

    // make sure that the table is defined by checking to make sure it exists
    //    -- if it isn't defined, return an empty list
    var table = this.getTableElement();
    if (!table || !table.rows) {
        // otherwise delete the recordHeights so we'll calculate them again
        // since this is being called prematurely (???)
        delete this._rowHeights;
        return heights;
    }




    var oldSafari = isc.Browser.isSafari && isc.Browser.safariVersion < 500;

    var rowRange = this.getDrawnRows(), //this._lastDrawnRow - this._firstDrawnRow + 1,
        drawnRows = rowRange[1] - rowRange[0] + 1,
        nonZeroHeight = false,
        firstRowBCR = null,
        prevHeightsSum = 0,
        isRTL = this.isRTL();
    for (var rowNum = 0; rowNum <= drawnRows; rowNum++) {
        var row = this.cacheDOM ? this.getTableElement(rowNum + this._firstDrawnRow) : table.rows[rowNum];

        if (row == null) {
            /*empty*/


        } else if (isc.Browser._hasGetBCR) {
            var rowBCR = isc.Element.getBoundingClientRect(row);
            if (firstRowBCR == null) firstRowBCR = rowBCR;
            heights[rowNum] = Math.round((rowBCR.bottom - firstRowBCR.top) - prevHeightsSum);


        } else if (this.allowRowSpanning && this.getRowSpan) {
            heights[rowNum] = row.offsetHeight;

        } else {

            var checkAllCellHeights =
                (oldSafari &&
                    (this.fixedRowHeights == false ||
                        (this.shouldFixRowHeight != null &&
                         this.shouldFixRowHeight(this.getCellRecord(rowNum), rowNum) == false )
                     )
                 ),
                cell, safariCellArray = [];

            if (!oldSafari || !checkAllCellHeights) {

                cell = row.lastChild;
            } else {
                for (var k = 0; k < row.childNodes.length; k++) {
                    safariCellArray[k] = row.childNodes[k]
                }
            }


            if (checkAllCellHeights) {
                heights[rowNum] = 0;
                for (var cellNum = 0; cellNum < safariCellArray.length; cellNum ++) {
                    var currentCell = safariCellArray[cellNum],
                        height = currentCell.offsetHeight;


                    var specifiedHeight = parseInt(currentCell.style ? currentCell.style.height
                                                                     : null);
                    if (isc.Browser.isStrict) {

                        if (this.cellPadding) specifiedHeight += this.cellPadding;
                        specifiedHeight += isc.Element._getVBorderPad(currentCell.className);
                    }

                    if (isc.isA.Number(specifiedHeight) && specifiedHeight > height)
                        height = specifiedHeight;

                    if (height > heights[rowNum]) heights[rowNum] = height;
                }
                heights[rowNum] += this.cellSpacing;

            } else if (cell) {

                if (!oldSafari) {
                    heights[rowNum] = cell.offsetHeight;
                } else {
                    // In Safari the offsetHeight is often misreported, and can't exceed
                    // the specified height for the cell, so use the specified height
                    // directly

                    var cellHeight = parseInt(cell.height);
                    if (cellHeight != null && isc.isA.Number(cellHeight)) {
                        if (isc.Browser.isStrict) {

                            cellHeight += isc.Element._getVBorderSize(cell.className);
                        }
                    } else {
                        cellHeight = cell.offsetHeight || 0;
                    }

                    heights[rowNum] = cellHeight;
                }

                heights[rowNum] += this.cellSpacing;
            }
        }

        var height = heights[rowNum];
        if (height > 0) {
            nonZeroHeight = true;
            prevHeightsSum += height;
        }
    }

    // add the cellSpacing to the first record.  This makes it so when the cursor is in the
    // spacing region, it actually goes to the lower record, which looks better than it
    // going to the upper record
    heights[0] += this.cellSpacing;


    if (!nonZeroHeight) {
        this.logWarn("row heights not yet available; returning all zeroes");
        this._rowHeights = null;
    }


    if (isc.Browser.isSafari && !isc.Page.isLoaded()) this._rowHeights = null;

    return heights;
},


//>    @method    gridRenderer.getColumnSizes()    ([A])
//    Get rendered column widths
//        @group    sizing, positioning
//        @return    (boolean)    null | false
//<
// NOTE: sets sets gridRenderer._renderedColumnWidths
getColumnSizes : function () {
     if (this._renderedColumnWidths != null) return this._renderedColumnWidths;

    // If undrawn, don't cache potentially incorrect values.
    if (!this.isDrawn()) return this._fieldWidths.duplicate() || [];

    if (this.fixedColumnWidths && isc.Browser.version >= 5) {
         return (this._renderedColumnWidths = this._fieldWidths.duplicate());
    } else {
        // inspect the DOM to determine rendered widths


        // create a new array and store it in the _recordHeights slot
        var widths = this._renderedColumnWidths = [];

        // get the first row in the table to test to see if it's drawn
        var row = this.getTableElement(this._firstDrawnRow);
        // if the row isn't defined,
        if (row == null) {
            // use the fieldWidths as specified in the settings
            this._renderedColumnWidths = widths.concat(this._fieldWidths);
            return this._renderedColumnWidths;
        }


        var    sizeDelta = (isc.Browser.isMac ? this.cellSpacing : 0);

        // iterate for all of the fields of the table that have been drawn, getting the sizes
        for (var colNum = 0; colNum < this.fields.length; colNum++) {
            var cell;
            if (this.showColumnsSeparately) {
                cell = this.getTableElement(this._firstDrawnRow,colNum);
            // Note leading not - This code will fire for safari 1.2, and other browsers
            } else if (!(isc.Browser.isSafari && isc.Browser.safariVersion < 125)) {
                cell = row.childNodes[colNum];
            }

            if (cell) {
                widths[colNum] = cell.offsetWidth + sizeDelta;
            } else {
                widths[colNum] = this._fieldWidths[colNum];
            }
        }
        // NOTE: we do this only in the case where we're not setting fixed widths
        this.innerWidth = this.getTableElement().offsetWidth;

        return widths;
    }
},

// Event Row/Col
// --------------------------------------------------------------------------------------------

//>    @method    gridRenderer.getEventRow()
// Returns the row number of the most recent mouse event.
//        @group    events, selection
//
//        @param [y] (Integer) optional y-coordinate to obtain row number, in lieu of the y
//                        coordinate of the last mouse event
//
//        @return    (int)    row number, or -2 if beyond last drawn row
//      @visibility external
//<
getEventRow : function (y) {
    // if we're empty always return rowNum -2 (beyond the end of any valid data)

    if (this.isEmpty()) return -2;

    // If a y-coordinate was not passed, get it from the offset of the last event
    if (y == null) y = this.getOffsetY();

    // if we're showing a start spacer, knock that off from the event coordinate so we can figure
    // out which row we hit
    if (this.startSpace) y -= this.startSpace;

    var undrawnHeight = this._getUndrawnHeight();
    // if it's a coordinate before the drawn area, treat all offscreen rows as fixed height
    if (y <= undrawnHeight) return Math.floor(y / this.getAvgRowHeight());

    var remainder = y - undrawnHeight,
        heights = this._getDrawnRowHeights();

    // check visible rows.  Note that if it's past the end of the visible rows, inWhichPosition
    // returns -2, and so do we
    var drawnRowNum = this.inWhichPosition(heights, remainder),
        pos;
    if (drawnRowNum >= 0) {
        pos = this._firstDrawnRow + drawnRowNum;
    } else {
        // assume the rest of the rows are fixed height
        var pastDrawnRows = remainder - heights.sum();
        pos = this._lastDrawnRow + 1 + Math.floor(pastDrawnRows / this.getAvgRowHeight());
        // Avoid returning a number higher than our total number of rows
        if (pos >= this.getTotalRows()) pos = -2;
    }

    //this.logWarn("getEventRow(" + (y == null ? this.getOffsetY() : y) + "): " +
    //             " rowHeights:" + heights +
    //             " drawn range: " + [this._firstDrawnRow, this._lastDrawnRow] +
    //             ", undrawnHeight: " + undrawnHeight +
    //             ", eventRow:" + pos);

    //this.logWarn("getEventRow(" + (y == null ? this.getOffsetY() : y) + "): " + pos);

    return pos;
},

//>    @method    gridRenderer.getEventColumn()
// Returns the column number of the most recent mouse event.
//        @group    events, selection
//
//        @param [x] (Integer) optional x-coordinate to obtain column number for, in lieu of the x
//                        coordinate of the last mouse event
//
//        @return    (int)    column number, or -2 if beyond last drawn column
//      @visibility external
//<
getEventColumn : function (x) {

    var widths = this.getColumnSizes();

    // If a x-coordinate was not passed, get it from the offset of the last event
    if (x == null) x = this.getOffsetX();

    // In RTL mode, if the grid is leaving a scrollbar gap but the vertical scroll bar is not
    // showing on the body, we need to make an adjustment by the scrollbar size because the
    // left coordinate of the body is not offset by the scrollbar size.
    var textDirection = this.getTextDirection(),
        grid;

    if (!this.frozen &&
        textDirection == isc.Page.RTL &&
        !this.vscrollOn &&
        (grid = this.grid) != null &&
        grid._shouldLeaveScrollbarGap())
    {

        x -= this.getScrollbarSize();
    }

    return this.inWhichPosition(widths, x, textDirection);
},

// getFocusRow / col for keypress events. Overridden at the ListGrid gridBody level.

getFocusRow : function () {
    return 0;
},
getFocusCol : function () {
    return 0;
},

// Helper to return the row that should get native focus in screenReader mode
// will basically match the focusRow- but could differ from it if (EG) the focus row isn't actually
// drawn.
getNativeFocusRow : function () {

    var rowNum = this._nativeFocusRow;
    if (rowNum == null) rowNum = this.getFocusRow();

    // default to top of viewport if we don't have a row already, or it is undrawn.

    var rows = this.getDrawnRows();
    if (rows != null && rows.length > 0 &&
        (rowNum == null || rowNum < rows[0] || rowNum > rows[1])) {
        rowNum = this._getViewportFillRows()[0];
        // if first row is partially offscreen jump to next row to avoid auto-scroll
        if (this.getRowTop(rowNum) < this.getScrollTop()) rowNum += 1;
    }
    return rowNum;

},

//>    @method    gridRenderer.getNearestRowToEvent()
//            Returns the nearest row to the event coordinates
//        @group    events, selection
//      @visibility external
//<
getNearestRowToEvent : function () {
    var rowNum = this.getEventRow();
    if (rowNum < 0) {
        var visibleRows = this.getVisibleRows();
        if (rowNum == -1) return visibleRows[0];
        if (rowNum == -2) return visibleRows[1];
    }
    return rowNum;
},

//>    @method    gridRenderer.getNearestColToEvent()
//            Returns the nearest column to the event coordinates
//        @group    events, selection
//      @visibility external
//<
getNearestColToEvent : function () {
    var colNum = this.getEventColumn();
    if (colNum < 0) {
        var visibleColumns = this.getVisibleColumns();
        if (colNum == -1) return visibleColumns[0];
        if (colNum == -2) return visibleColumns[1];
    }
    return colNum;
},

// Note: viewport rows / visible rows / drawn rows
// =================
// There is a distinction here between:
// - "rows we need to draw to fill the viewport" (getViewportFillRows)
// - "drawn rows that are currently visible in the viewport" (getVisibleRows)
// - "rows we've actually drawn" (this._firstDrawnRow -> this._lastDrawnRow)
//
// With drawAheadRatio > 1, rows we've drawn clearly differ from the other two.  With variable
// height cells, viewportFillRows differ from visible rows since we don't know how tall the
// cells will be before we draw them; the last viewportFillRow may actually be rendered below
// the viewport.  With fixedRowHeights:false and a drawAheadRatio > 1, the first viewport fill
// row may be below the top of the viewport.
//
// Generally:
// - we use the viewportFillRows only to determine how many rows to draw / whether to redraw
// - we use _firstDrawnRow/_lastDrawnRow to do DOM manipulation
// - event handling code that cares about the viewport (particularly D&D) uses visible rows

_getViewportFillRows : function () {

    var viewportHeight = this.getViewportHeight(),
        avgRowHeight = this.getAvgRowHeight()
    ;

    // scrollTop/cellHeight is the number of rows that fit into the space above the viewport, so
    // round down for the index of the first row to stick into the viewport
    var firstVisible = Math.floor(this.getScrollTop() / avgRowHeight);



    var nRecords;
    if (this.autoFitData == "both" || this.autoFitData == "vertical") {
        nRecords = this.getTotalRows() - firstVisible;

        var maxRecords = this.autoFitMaxRecords;
        if (maxRecords != null && maxRecords < nRecords) nRecords = maxRecords;

        var totalHeight = this.getAutoFitMaxHeight();
        if (totalHeight != null) {
            var maxRecordsByHeight = Math.ceil(totalHeight / this.cellHeight);
            if (maxRecordsByHeight < nRecords) nRecords = maxRecordsByHeight;
        }
    } else {
        nRecords = Math.ceil(viewportHeight / this.cellHeight);
    }
    // [firstVisible, lastVisible] is an *inclusive* range, so subtract one here
    var lastVisible = firstVisible + nRecords - 1;

    // if we're showing an explicit spacer at the top, it'll shift the rows down
    // take that into account now
    if (this.startSpace) {
        var spaceRows = Math.floor(this.startSpace / avgRowHeight);
        firstVisible = Math.max(0, firstVisible - spaceRows);
        lastVisible = Math.max(0, lastVisible - spaceRows);
    }

    // Are we virtual scrolling? Don't rely on this._isVirtualScrolling in case data or
    // fixedRowHeights, etc has changed
    var vscrolling = this.virtualScrolling && this._targetRow != null;
    if (vscrolling) {
        if (firstVisible == 0 && lastVisible >= (this.getTotalRows()-1)) vscrolling = false;
    }

    if (!vscrolling) return [firstVisible, lastVisible];


    // when using virtual scrolling, calculate the rows that need to be drawn to fill the
    // viewport based on the target row that needs to be scrolled into view (scrollTop is
    // irrelevant)

    //this.logWarn("calculating viewport based on targetRow: " + this._targetRow +
    //             ", row offset: " + this._rowOffset);

    var startCoord = this._targetRow;
    // if we have a large negative offset (targetRow will be well below viewport), ensure
    // enough rows are rendered above the targetRow
    if (this._rowOffset < 0) startCoord += Math.floor(this._rowOffset / this.cellHeight);
    if (startCoord < 0) startCoord = 0;
    var endCoord = startCoord + Math.ceil(viewportHeight / this.cellHeight);

    return [startCoord, endCoord];

},

// Arbitrary average row height for incremental rendering and variable row heights
// default to a typical row height given a few lines of wrapping text
// Used whenever we need to know / estimate how tall undrawn rows will render


avgRowHeight:60,
getAvgRowHeight : function () {
    return this.fixedRowHeights ? this.cellHeight : Math.max(this.cellHeight,this.avgRowHeight);
},

//>    @method    gridRenderer.getVisibleRows()
// Get the rows that are currently visible in the viewport, as an array of
// [firstRowNum, lastRowNum].
// <p>
// If the grid contains no records, will return [-1,-1].  Will also return [-1,-1] if called at
// an invalid time (for example, data is in the process of being fetched - see
// +link{ResultSet.lengthIsKnown()}).
//
// @return (Array of int)
// @visibility external
//<
// NOTE: the viewport can extend beyond the last row or column, in which case the last row or
// column is reported as the last visible.
getVisibleRows : function () {
    if (isc.ResultSet && isc.isA.ResultSet(this.data) && !this.data.lengthIsKnown()) {
        return [-1,-1];
    }
    return this._getVisibleRows();
},
_getVisibleRows : function () {
    var scrollTop = this.getScrollTop();
    var rows = [
        this.getEventRow(scrollTop),
        this.getEventRow(scrollTop + this.getInnerHeight())
    ];
    // viewport extends beyond last row
    if (rows[1] == -2) {
        var totalRows = this.getTotalRows();
        if (totalRows == 0 || rows[0] < 0) {
            // empty data, or some other condition that caused getEventRow() to return
            // something invalid
            rows[0] = -1;
            rows[1] = -1;
        } else {
            // return the *index of* the last row
            rows[1] = this.getTotalRows() - 1;
        }
    }
    return rows;
},

//>    @method    gridRenderer.getVisibleColumns()
// Get the currently visible columns, as an array of [leftColumnNum, rightColumnNum]
//<
getVisibleColumns : function () {

    var widths = this._fieldWidths;
    if (this.overflow == isc.Canvas.VISIBLE) return [0, widths.length-1];

    var scrollPos = this.getScrollLeft();

    if (this.isRTL()) {

        var maxScroll = this.getScrollWidth() - this.getInnerWidth(),
            scrollPos = maxScroll - scrollPos;
    }

    var firstCol = this.inWhichPosition(widths, scrollPos),
        lastCol = this.inWhichPosition(widths, scrollPos + this.getInnerWidth());

    //this.logWarn("scrollLeft: " + scrollPos +
    //             ", firstCol: " + firstCol +
    //             ", lastCol: " + lastCol);

    if (lastCol == -2) lastCol = this._fieldWidths.length - 1;
    return [firstCol, lastCol];

    // maxScroll - scrollLeft
    // 0 - 0: 0, works with non-reversed traversal
    // max - max: 0, works
    // max - 0:


},

//>    @method    gridRenderer.getDrawnRows()
// Get the rows that are currently drawn (exist in the DOM), as an array of [firstRowNum,
// lastRowNum].
// <P>
// The drawn rows differ from the +link{getVisibleRows,visibleRows} because of
// +link{drawAheadRatio,drawAhead}.  The drawn rows are the appropriate range to consider if
// you need to, eg, using +link{refreshCell()} to update all the cells in a column.
// <P>
// If the grid is undrawn or the +link{emptyMessage} is currently shown, returns
// [null,null];
//
// @return (Array)
// @visibility external
//<
getDrawnRows : function () {
    if (this.cacheDOM) return this.getVisibleRows();
    return [this._firstDrawnRow, this._lastDrawnRow]
},

// Synthetic Row/Cell Events (over/out/hover/contextClick)
// --------------------------------------------------------------------------------------------

// shouldShowRollOver
// Should we show the "over" styling for this row when the mouse goes over it?
// By default this is always true if this.showRollOver is true.
shouldShowRollOver : function (rowNum, colNum) {
    // NOTE: colNum may be null.

    return (this.showRollOver && !this._rowAnimationInfo);
},

// called whenever the current row needs to be updated to reflect a change in the rollOver
// state.  This includes both the rollover *leaving* a row (rollover appearance needs to be
// cleared) and entering the row.
updateRollOver : function (rowNum, colNum) {
    this.setRowStyle(rowNum, null, (this.useCellRollOvers ? colNum : null));
},

// We fire synthetic events such as 'cellMouseOver' or 'rowMouseOver' on mouseOver of a cell.
// The handling for this is very similar for each event type -
//  on X event, if cellX is defined fire it.  If rowX is defined fire that.
// (If both are defined, we fire both).

// NOTE: checking for valid (>=0) row and col coordinates is necessary because the table can be
// drawn smaller than the area of the containing GridRenderer Canvas (eg in the LV, with a small
// number of records), so the mouse can be within the GridRenderer Canvas without being over the
// table as such.

// NOTE: most of the pseudo events do not have default handlers - meaning they can be defined
// without the developer having to call "Super".  The only case where we currently would
// require an override to call Super is if the developer overrides
// 'selectOnMouseDown(record,rowNum,colNum)' or 'selectOnMouseUp(record,rowNum,colNum)'.



// Override the (unexposed) startHover() method to be a no-op
// This is called by the EH directly to show hover on mouseOver - we're handling our hovers on
// mouseOver of specific cells etc.
startHover : function () { },

//>    @method    gridRenderer.mouseMove()    ([A])
//        @group    events
//            Generate cell/row over/out events
//        @return    (boolean)    false if same cell/row as before
//<
mouseMove : function (arg1, arg2) {

    if (this._suppressEventHandling()) return;

    // check for keyboard-navigation-induced scrolls
    if (this._suppressNextMouseMove) {
        this._suppressNextMouseMove = false;
        return;
    }

    var rowNum = this.getEventRow(),
        colNum = this.getEventColumn();

    // If we're pending a redraw from scroll, and the mouse is currently over an
    // undrawn part of the table, suppress standard mouseMove handling.
    // We don't want to fetch data which is just being scrolled through, or attempt to
    // redraw to refresh styling, etc.
    // (This is most relevant to mouseWheel scrolling where the mouse is over the body
    // of the grid).

    var pendingScrollRedraw = this.pendingActionOnPause("scrollRedraw"),
        drawnRows = this.getDrawnRows();
    if (pendingScrollRedraw && drawnRows &&
        (rowNum < drawnRows[0] || rowNum > drawnRows[1]))
    {
        return;
    }


    // On rollOver of cells we do a couple of things:
    // - highlight the cell by applying "Over" styling
    // - fire cell level events ("rowOver", "rowOut", and also hover events if appropriate).
    // The "Over" styling subsystem is also coopted at the ListGrid level for visual feedback with
    // keyboard navigation.
    // Track the current "Over" styled cell separately from the last "cellOver" event cell - this
    // is required to avoid an obscure bug where if a cell is highlighted with the "Over" style
    // by keyboard navigation or focus, and then the user rolls over it, the appropriate
    // rowOver event never fires.

    var lastStyleRow = this.lastOverRow,
        lastMouseRow = this.lastMouseOverRow,
        lastStyleCol = this.lastOverCol,
        lastMouseCol = this.lastMouseOverCol;

    //this.logWarn("row: " + rowNum + ", col: " + colNum);
    var validRowCol = (rowNum >= 0 && colNum >= 0),
        notSameStyleRowCol = !(rowNum == lastStyleRow && colNum == lastStyleCol),
        notSameMouseRowCol = !(rowNum == lastMouseRow && colNum == lastMouseCol),


        requireRecord = validRowCol && (
            notSameStyleRowCol || notSameMouseRowCol || this.cellMove || this.rowMove),
        record = requireRecord && this.getCellRecord(rowNum, colNum),

        hasNewCell = (validRowCol && requireRecord && this.cellIsEnabled(rowNum, colNum, record));

    // Styling:
    if (notSameStyleRowCol) {

        // If we were showing "over" styling for another row/cell, clear it.

        if (lastStyleRow != null && lastStyleCol != null) {
            this.lastOverRow = null;
            this.lastOverCol = null;

            // If we're not over a valid column (we're too far to the right of the listGrid)
            // consider this a row change, for the purposes of restyling correctly
            if (rowNum != lastStyleRow || colNum < 0 || this.useCellRollOvers) {


                this.updateRollOver(lastStyleRow, lastStyleCol, hasNewCell);
            }
        }

        // And show the over style for the new cell:
        if (hasNewCell) {

            this.lastOverRow = rowNum;
            this.lastOverCol = colNum;

            if (lastStyleRow != rowNum || this.useCellRollOvers) {
                // show rollover hiliting
                if (this.shouldShowRollOver(rowNum, colNum)) {
                    this.updateRollOver(rowNum, colNum);
                }
            }
        }
    }

    // handle situations when we return cursor from inner components to a cell of current grid
    var returnedBackToCurrentGrid = this._lastTarget != arg1.target && (arg1.target == this);
    this._lastTarget = arg1.target;

    // Events (including hovers):
    if (notSameMouseRowCol || returnedBackToCurrentGrid) {

        if (lastMouseRow != null && lastMouseCol != null) {

            this.lastMouseOverRow = null;
            this.lastMouseOverCol = null;

            // Once again - if new colNum < 0, we are to the right of the rightmost column, so
            // call stopHover even if we're hovering by row.
            if ((rowNum != lastMouseRow || colNum < 0 || this.hoverByCell) &&
                this.getCanHover() && !this.keepHoverActive)
            {
                this.stopHover();
            }

            var lastMouseRecord = this.getCellRecord(lastMouseRow, lastMouseCol);

            // support field.cellOut, cell.cellOut?
            if (this.cellOut) {
                this.cellOut(lastMouseRecord, lastMouseRow, lastMouseCol);
            }
            if (rowNum != lastMouseRow && this.rowOut) {
                this.rowOut(lastMouseRecord, lastMouseRow, lastMouseCol);
            }
        }

        if (hasNewCell) {

            this.lastMouseOverRow = rowNum;
            this.lastMouseOverCol = colNum;

            if (rowNum != lastMouseRow || this.hoverByCell) {
                // set hover action
                if (validRowCol && this.getCanHover()) {
                    var hoverDelay = this.getCellHoverDelay(rowNum, colNum);
                    isc.Hover.setAction(this, this._cellHover, [rowNum, colNum], hoverDelay);

                }
            }

            // support field.cellOver, cell.cellOver?
            if (this.cellOver) {
                this.cellOver(record, rowNum, colNum);
            }

            if (rowNum != lastMouseRow && this.rowOver) {
                this.rowOver(record, rowNum, colNum);
            }
        }
    }

    if (validRowCol) {
        // cellMove / rowMove
        // Not currently exposed - used internally to update hovers for validation error icons
        // in ListGrid.
        if (this.cellMove) {
            this.cellMove(record, rowNum, colNum);
        }
        if (this.rowMove) {
            this.rowMove(record, rowNum, colNum);
        }
    }

    // If this mouseMove was tripped by the user scrolling (see Canvas.scrolled),
    // and we're pending a scroll-redraw, reset the scroll redraw timer now

    if (pendingScrollRedraw && this._firingSyntheticMouseMove) {
        var delay = this.getScrollRedrawDelay(this._pendingScrollRedrawFromWheel);
        this.fireOnPause("scrollRedraw",
            this._getRedrawOnScrollCallback(),
            delay);
    }
},

// Support suppressing mouse/keyboard event handling at certain times.
_suppressEventHandling : function () {
    //>Animation
    // If we're in the process of animate-resizing a row just suppress all event handling!
    if (this._rowAnimationInfo != null) return true;
    //<Animation
    return false;
},

//>    @method    gridRenderer.mouseOut()    ([A])
//        @group    events
//            call _cellOut or _rowOut if appropriate
//        @return    (boolean)    false if no hiliting
//<
mouseOut : function () {
    // Don't suppress mouseOut handling even if this._suppressEventHandling returns true
    // - we don't want the list stuck in an "over" state

    // if the mouseOut occurred by the mouse going over a child of an embedded component, don't
    // hide rollover / fire mouseOut methods.
    var target = isc.EH.getTarget();
    if (this._embeddedComponents) {
        var components = this._embeddedComponents;
        for (var i = 0; i < components.length; i++) {
            if (!components[i]) continue;
            if (components[i].contains(target, true)) {
                return;
            }
        }
    }
    // Note that we'll still get a bubbled mouseout when the user rolls out of the embedded
    // component so we won't get stuck in an 'over' state.

    // If the target == this, we're still over this widget.
    // This can happen if we're starting to drag - in this case continue as with any other
    // mouseOut (killing the hover is technically unnecessary as in this case as
    // EH.handleDragStart() always calls Hover.clear(), but we also want to clear up over-styling
    // etc.)
    // It can also happen if we're in the process of being masked by the clickMask
    // which explicitly calls 'mouseOut' on the target, allowing us to clean up state.
    //
    // Otherwise this event was bubbled from the user rolling off an embedded component back
    // into the body and we can ignore it.
    if (target == this && !isc.EH.getDragTarget() && !isc.EH._showingClickMask) {
        return;
    }

    // clear any hover timer/window
    if (this.getCanHover()) this.stopHover();
    // if we were previously over a valid cell, reset the style for that cell and fire
    // cellOut / rowOut

    if (this.lastOverRow != null && this.lastOverCol != null) {

        var lastOverRow = this.lastOverRow,
            lastOverCol = this.lastOverCol;

        this.lastOverRow = null;
        this.lastOverCol = null;

        // clear rollover hiliting
        if (this.shouldShowRollOver(lastOverRow, lastOverCol)) {
            this.updateRollOver(lastOverRow, lastOverCol);
        }
    }

    if (this.lastMouseOverRow != null && this.lastMouseOverCol != null) {

        var lastOverRow = this.lastMouseOverRow,
            lastOverCol = this.lastMouseOverCol,
            lastOverRecord = this.getCellRecord(lastOverRow, lastOverCol);

        this.lastMouseOverRow = null;
        this.lastMouseOverCol = null;

        // support field.cellOut, cell.cellOut?
        if (this.cellOut) {
            this.cellOut(lastOverRecord, lastOverRow, lastOverCol);
        }
        if (this.rowOut) {
            this.rowOut(lastOverRecord, lastOverRow, lastOverCol);
        }
    }

},


_getShowClippedValuesOnHover : function () {
    return this.showClippedValuesOnHover;
},

getCellHoverDelay : function (rowNum, colNum) {
    return this.hoverDelay;
},

// support field.cellHover, cell.cellHover, field.showHover, cell.showHover?
_cellHover : function (rowNum, colNum) {
    //!DONTCOMBINE
    var record = this.getCellRecord(rowNum, colNum);

    if (!this.shouldFireCellHover(record, rowNum, colNum)) return;

    // call user-defined handler and bail (don't show hover window) if it returns false
    var returnVal,
        cellValueIsClipped = this.cellValueIsClipped(rowNum, colNum);
    if (this._getShowClippedValuesOnHover() && cellValueIsClipped &&
        this.cellValueHover && this.cellValueHover(record, rowNum, colNum) == false)
    {
        returnVal = false;
    }
    if (this.cellHover && this.cellHover(record, rowNum, colNum) == false) returnVal = false;
    if (this.rowHover && this.rowHover(record, rowNum, colNum) == false) returnVal = false;

    if (returnVal == false) return;

    // show hover window if enabled
    if (this.showHover) this._showHover(record, rowNum, colNum, cellValueIsClipped);
},

// If we get a cellHover timer event over what appears to be an invalid cell (no record),
// don't fire the notification methods.

shouldFireCellHover : function (record, rowNum, colNum) {
    return record != null;
},

defaultCellValueHoverHTML : function (record, rowNum, colNum) {
    return this._getCellValue(record, rowNum, colNum);
},

_getHoverProperties : function (record, rowNum, colNum) {
    if (this.grid) {
        var isFrozenBody = this.grid.frozenBody && this == this.grid.frozenBody,
            frozenLength = !isFrozenBody && this.grid.frozenBody ? this.grid.frozenFields.length : 0,
            field = colNum != null ? this.grid.fields[colNum + frozenLength] : null
        ;
        if (field) {
            return {
                align: field.hoverAlign != null ? field.hoverAlign : this.hoverAlign,
                baseStyle: field.hoverStyle != null ? field.hoverStyle : this.hoverStyle,
                height: field.hoverHeight != null ? field.hoverHeight : this.hoverHeight,
                hoverDelay: field.hoverDelay != null ? field.hoverDelay : this.hoverDelay,
                opacity: field.hoverOpacity != null ? field.hoverOpacity : this.hoverOpacity,
                moveWithMouse: field.hoverMoveWithMouse != null ? field.hoverMoveWithMouse :
                                this.hoverMoveWithMouse,
                valign: field.hoverVAlign != null ? field.hoverVAlign : this.hoverVAlign,
                width: field.hoverWidth != null ? field.hoverWidth : this.hoverWidth,
                wrap: field.hoverWrap != null ? field.hoverWrap : this.hoverWrap
            };
        }
    }
    return this.Super("_getHoverProperties");
},

_showHover : function (record, rowNum, colNum, cellValueIsClipped) {
    var properties = this._getHoverProperties(record, rowNum, colNum);
    var content = this._getCellHoverComponent(record, rowNum, colNum);
    if (!content) {
        // Prefer the standard cell hover if customized.
        var useStandardCellHover = false,
            isCellHoverSuppressed = false;
        if (this.cellHoverHTML._isPassthroughMethod && this.grid) {
            var grid = this.grid,
                gridCol = grid.getFieldNumFromLocal(colNum, this),
                field = grid.getField(gridCol);
            useStandardCellHover = (grid.canHover ||
                                    (field.showHover && grid.canHover != false) ||
                                    grid.cellHoverHTML != grid.getClass().getInstanceProperty("cellHoverHTML") ||
                                    (grid.canHover == null && field.showHover)) &&
                                   field.showHover != false;
            isCellHoverSuppressed = grid._isCellHoverSuppressed(rowNum, gridCol);
        }

        if (!isCellHoverSuppressed) {
            if (!useStandardCellHover) {
                if (this._getShowClippedValuesOnHover() && cellValueIsClipped) {
                    content = this.cellValueHoverHTML(record, rowNum, colNum, this.defaultCellValueHoverHTML(record, rowNum, colNum));
                }
            } else {
                content = this.cellHoverHTML(record, rowNum, colNum);
            }
        }
    }
    isc.Hover.show(content,
                   properties,
                   this.cellHoverBoundary(rowNum, colNum),
                   this.getHoverTarget());
},

_getCellHoverComponent : function (record, rowNum, colNum) {
},

// getHoverTarget() - returns the 'targetCanvas' passed to Hover.show() in _showHover()
// This allows the developer to call 'updateHover()' on that canvas to change the hover content HTML
// override in LG bodies to point to the ListGrid
getHoverTarget : function () {
    return this;
},

cellHoverHTML : function (record, rowNum, colNum) {
    return null;
},

cellValueHoverHTML : function (record, rowNum, colNum, defaultHTML) {
    return null;
},

getCellHoverComponent : function (record, rowNum, colNum) {
    return null;
},


cellHoverBoundary : function (rowNum, colNum) {

    return null;

},

// generate cell/row contextClick events
showContextMenu : function () {
    if (this._suppressEventHandling()) return false;

    var rowNum = this.getEventRow(),
        colNum = this.getEventColumn();

    // If this came from a keyboard event, use the keyboard focus row / col
    var keyboardEvent = isc.EH.isKeyEvent();
    if (keyboardEvent) {
        rowNum = this.getFocusRow(),
        colNum = this.getFocusCol();
    }

    var validRowCol = (rowNum >= 0 && colNum >= 0),
        record = validRowCol && this.getCellRecord(rowNum, colNum);
    if (validRowCol && this.cellIsEnabled(rowNum, colNum, record)) {
        var returnVal;
        if (this.cellContextClick)
            if (this.cellContextClick(record, rowNum, colNum) == false) returnVal = false;

        if (this.rowContextClick)
            if (this.rowContextClick(record, rowNum, colNum) == false) returnVal = false;

        // Legacy:
        if (this.recordContextClick)
            if (this.recordContextClick(record, rowNum, colNum) == false) returnVal = false;

        if (returnVal == false) return false;
    }

    return this.Super("showContextMenu");
},




// Selection
// --------------------------------------------------------------------------------------------

setSelection : function (selection) {
    this.selection = selection;

    // update cell/row styling on selection change
    if (this.selection.isA("CellSelection")) {
        this.observe(this.selection, "selectionChanged", function () {
            this._cellSelectionChanged(this.selection.changedCells);
        });
    } else {
        this.observe(this.selection, "setSelected", function () {
            this._setSelectedObservation(this.selection);
        });
    }
},

clearSelection : function () {
    if (this.selection) {
        if (this.isObserving(this.selection, "selectionChanged"))
            this.ignore(this.selection, "selectionChanged");
        if (this.isObserving(this.selection, "setSelected"))
            this.ignore(this.selection, "setSelected");

        delete this.selection;
    }
},

_cellSelectionChanged : function (cellList) {
    //this.logWarn("cellSelectionChanged with list: " + this.echoFull(cellList));
    // call user-defined handler and bail (don't hilite cells) if it returns false
    if (this.cellSelectionChanged) {
        if (this.cellSelectionChanged(cellList) == false) return false;
    }
    // refresh the affected cells to visually indicate selection
    this.refreshCellStyles(cellList);
},

// setSelected was fired on the selection object.
// If the selection actually changed call _rowSelectionChanged to fire notification methods and
// refresh the row in question.
// setSelected may be called without changing selection. This is actually done internally
// to manage cascading selection sync.
_setSelectedObservation : function (selection) {
    var changed = false;


    // If the state changed, we need to refresh
    if (!!selection.lastSelectionPreviousState != !!selection.lastSelectionState) {
        changed = true;
    // We show partial selection in some cases in tree-grids.
    // If the partial selected state changed, we also need to refresh
    } else if (selection.lastSelectionState &&
                (!!selection.lastSelectionPartialValue != !!selection.lastSelectionPreviousPartialValue))
    {
        changed = true;
    }


    if (changed) {
        this._rowSelectionChanged(
            selection.lastSelectionItem,
            !!selection.lastSelectionState,
            selection.cascadeSyncOnly
        );
    }
},

_rowSelectionChanged : function (record, state, cascadeSyncOnly) {
    if (!cascadeSyncOnly) {
        // call user-defined handler and bail (don't hilite rows) if it returns false.

        if (this.handleSelectionChanged(record, state) == false) {
            return false;
        }

    }

    if (this.selection._selectingList) {
        this.markForRowSelectionRefresh();
        return;
    }

    // refresh the affected records to visually indicate selection

    var selection = this.selection,
        lastItem = selection.lastSelectionItem,
        rowNum = selection.data.indexOf(lastItem,
                                        this._firstDrawnRow,
                                        this._lastDrawnRow);
    if (rowNum == -1) rowNum = selection.data.indexOf(lastItem);

    if (rowNum == -1) return;
    this.updateRowSelection(rowNum);
},

markForRowSelectionRefresh : function () {
    this.markForRedraw("Selection changed");
},

handleSelectionChanged : function (record,state) {
    if (this.selectionChanged) return this.selectionChanged(record,state);
},

updateRowSelection : function (rowNum) {
    this.setRowStyle(rowNum);
},

selectionEnabled : function () {
    return this.selection != null;
},

canSelectRecord : function (record) {
    return (record != null && record[this.recordCanSelectProperty] !== false);
},

//>    @method    gridRenderer.mouseDown()    ([A])
//        @group    events, selection
//            handle a mouseDown event
//        @return    (boolean)    false if record is disabled
//<
_getMouseDownCell : function () {
    return [this.getEventRow(), this.getEventColumn()];
},
mouseDown : function () {
    if (this._suppressEventHandling()) return;

    var cell = this._getMouseDownCell(),
        rowNum = cell[0],
        colNum = cell[1];

    // not over a cell - just bail
    if (!(rowNum >= 0 && colNum >= 0)) return;

    var record = this.getCellRecord(rowNum, colNum);

    // if the record is explicitly disabled, kill the event
    if (!this.cellIsEnabled(rowNum, colNum, record)) return false;

    // hang onto the rowNum / colNum to see if we get a click over the same cell
    this._mouseDownRow = rowNum;
    this._mouseDownCol = colNum;

    // remember the location of the click too.

    this._mouseDownX = isc.EH.getX();
    this._mouseDownY = isc.EH.getY();

    // call user-defined synthetic mouseDown event handler
    if (!isc.EH.rightButtonDown()) {
        return this._cellMouseDown(record, rowNum, colNum);
    } else {
        return this._cellRightMouseDown(record, rowNum, colNum);
    }

},


rightMouseDown : function () {
    // required to handle remembering which record the mouse went down over. Also fires
    // _cellRightMouseDown()
    return this.mouseDown();
},

_cellMouseDown : function (record, rowNum, colNum) {

    var returnVal;

    if (this.cellMouseDown && (this.cellMouseDown(record, rowNum, colNum) == false)) returnVal = false;
    if (this.rowMouseDown && (this.rowMouseDown(record, rowNum, colNum) == false)) returnVal = false;
    // legacy
    if (this.recordMouseDown && this.recordMouseDown(rowNum, colNum) == false) returnVal = false;

    if (returnVal == false) return false;

    // perform selection
    this.selectOnMouseDown(record, rowNum, colNum);
},


selectOnMouseDown : function (record, rowNum, colNum) {

    if (!this.selectionEnabled()) return true;

    //this.logWarn("mouseDown at: " + [rowNum, colNum]);

    if (rowNum >= 0 && colNum >= 0 && this.canSelectRecord(record) &&
        !this._shouldSelectOnMouseUp()) {
        this._updateSelectionOnMouseUp = true;
        var selectionChanged = this.selection.selectOnMouseDown(this, rowNum, colNum);
        if (selectionChanged && this.fireSelectionUpdated && isc.isA.Function(this.fireSelectionUpdated)) {
            this.fireSelectionUpdated();
        }
    }

},

_shouldSelectOnMouseUp : function () {
    if (isc.EH.dragTarget != null && isc.EH.dragOperation == isc.EH.DRAG_SCROLL) {
        return true;
    }
    return false;
},

_cellRightMouseDown : function (record, rowNum, colNum) {
    // currently there are no cell / row level rightMouseDown handlers, but this is where we would
    // call them if there were.

    // perform right-mouse style selection
    if (this.canSelectOnRightMouse) this.selectOnRightMouseDown(record, rowNum, colNum);

},

// We override Canvas._scrolled() here to prevent keyboard-navigation-triggered scrolling from
// firing a synthetic mouse event which can corrupt the tracking of the navigation location.
// If navigation is not in progress, we simply call the parent (Canvas) method.
_scrolled : function (deltaX, deltaY) {

    if (this.grid && this.grid._handlingKeyboardNavigation) {
        // some browsers trigger extra native mouse move events; suppress them
        if (isc.Browser.nativeMouseMoveOnCanvasScroll) {
            this._suppressNextMouseMove = true;
            isc.Page.setEvent(isc.EH.MOUSE_MOVE, this, "once", "_suppressMouseMove");
        }
        this._fireParentScrolled(this, deltaX, deltaY);
        if (this.scrolled) this.scrolled(deltaX, deltaY);
    } else {
        this.Super("_scrolled", arguments);
    }
},

// Do nothing unless the mouse move event is not the one that we expect to suppress;
// in that case, clear the flag so it gets handled normally like any other event
_suppressMouseMove : function () {
    var target = isc.EH.lastEvent.target;
    if (target != this) this._suppressNextMouseMove = false;
},

// Default implementation is just to do 'selectOnMouseDown' - override if you want something else.
selectOnRightMouseDown : function (record, rowNum, colNum) {
    this.selectOnMouseDown(record, rowNum, colNum);
},

//>    @method    gridRenderer.mouseUp()    ([A])
//        @group    events,    selection
//            handle a mouseUp event
//        @return    (boolean)    false if no hiliting; true otherwise
//<
mouseUp : function () {

    if (this._suppressEventHandling()) return;

    var rowNum = this.getEventRow(),
        colNum = this.getEventColumn();

    // not over a cell - just bail
    if (!(rowNum >=0 && colNum >=0)) return;

    var record = this.getCellRecord(rowNum, colNum);

    // if the record is explicitly disabled, just return
    if (!this.cellIsEnabled(rowNum, colNum, record)) return;

    // call user-defined cell / row level mouseUp handler
    var returnVal;

    if (this.cellMouseUp && (this.cellMouseUp(record, rowNum, colNum) == false)) returnVal = false;
    if (this.rowMouseUp && (this.rowMouseUp(record, rowNum, colNum) == false)) returnVal = false;
    // legacy
    if (this.recordMouseUp && this.recordMouseUp(rowNum, colNum) == false) returnVal = false;

    if (returnVal == false) return returnVal;

    this.selectOnMouseUp(record, rowNum, colNum);

},

selectOnMouseUp : function (record, rowNum, colNum) {
    if (!this.selectionEnabled()) return true;

    //this.logWarn("mouseUp at: " + [rowNum, colNum]);
    if (rowNum >= 0 && colNum >= 0) {
        var selectionChanged = false;
        // If we didn't select on mouseDown, fire both 'selectOnMouseDown' and 'selectOnMouseUp'
        // to update the selection

        if (this._shouldSelectOnMouseUp()) {
            this._updateSelectionOnMouseUp = true;
            selectionChanged = this.selection.selectOnMouseDown(this, rowNum, colNum);
        }

        if (this.grid) this.grid._dontRefreshSelection = true;
        if (this.selection.selectOnMouseUp(this, rowNum, colNum)) selectionChanged = true;
        if (this.grid) this.grid._dontRefreshSelection = null;

        // only refresh if the selection actually changed - otherwise we'll end up redrawing
        // on every click on a selected row, etc.
        if (selectionChanged) {
            this.markForRedraw();
            if (this._updateSelectionOnMouseUp) {
                if (this.fireSelectionUpdated && isc.isA.Function(this.fireSelectionUpdated)) {
                    this.fireSelectionUpdated();
                }
                if (this.grid.getCurrentCheckboxField() != null) {
                    this.grid.updateCheckboxHeaderState();
                }
                this._updateSelectionOnMouseUp = null;
            }
        }
    }

},

//>    @method    gridRenderer.click()  ([A])
//        @group    events
//            handle a click event
//          fires cell or row level click handler
//
//        @return    (boolean)    false if the event was cancelled by some handler
//<
click : function () {
    if (this._suppressEventHandling()) return;

    var rowNum = this.getEventRow(),
        colNum = this.getEventColumn();
    return this._rowClick(rowNum, colNum);
},

// _rowClick - fire rowClick and cellClick handlers
_rowClick : function (rowNum, colNum) {
    // Clear out old _clickRow/ _clickCol, which are now out of date.
    // [These will be set to meaningful values if the event occurred over a valid cell].
    this._clickRow = this._clickCol = null;

    var mdR = this._mouseDownRow;

    // if the click occurred over a different record from the previous mousedown, just bail

    if (mdR != null && rowNum != mdR) {
        if (isc.EH.getX() == this._mouseDownX) {
            rowNum = this._mouseDownRow;
        } else {
            // Don't return false - we don't want to suppress click from bubbling.
            return;
        }
    }
    if (isc.EH.getY() == this._mouseDownY && this._mouseDownCol != null) {
        colNum = this._mouseDownCol;
    }

    // no record - just bail
    if (!(rowNum >=0 && colNum >=0)) return;

    var record = this.getCellRecord(rowNum, colNum);

    // if the record is explicitly disabled, return false to kill doubleClick etc
    if (!this.cellIsEnabled(rowNum, colNum, record)) return false;

    // record the click cell details for double-click events to check
    this._clickRow = rowNum;

    var returnVal;
    // only fire cellClick if it was on the same column as well as the same row
    if (!this._cellClick(record, rowNum, colNum)) returnVal = false;
    if (this.rowClick && (this.rowClick(record, rowNum, colNum) == false))
        returnVal = false;

    // clear out the old mouseDown row
    // Note - this method is fired _after_ mouseUp, so we can't clear these values out there.
    this._mouseDownRow = null;

    return returnVal;

},

// _cellClick - fire cellClick handlers on the specified row/col
_cellClick : function (record, rowNum, colNum) {


    // Assertion - this method is only called when we have already verified that the click
    // occurred over the same row as the last mousedown
    if (this._mouseDownCol != colNum) {
        // Clearing out this._clickCol avoids the possibility of the doubleClick handler firing
        // over this cell which never received a first click.
        this._clickCol = null;
        return;
    }
    // update _clickCol so double clicks can determine whether they occurred over the
    // same column as the previous click.
    this._clickCol = colNum;

    this._mouseDowncol = null;
    return !(this.cellClick && (this.cellClick(record, rowNum, colNum) == false));
},

//>    @method    gridRenderer.doubleClick()  ([A])
//        @group    events
//            handle a doubleClick event
//          fires cell or row level doubleClick handler
//
//        @return    (boolean)    false if the event was cancelled by some handler
//<
doubleClick : function () {
    if (this._suppressEventHandling()) return;

    var rowNum = this.getEventRow(),
        colNum = this.getEventColumn();

    // no record - just bail
    if (!(rowNum >= 0 && colNum >= 0)) return;

    var record = this.getCellRecord(rowNum, colNum);

    // if the record is explicitly disabled, kill the event
    if (!this.cellIsEnabled(rowNum, colNum, record)) return false;

    // If the double click occurred over a different row from the previous click, fire
    // rowClick / cellClick over the new row.
    if (rowNum != this._clickRow) {
        return this._rowClick(rowNum, colNum);
    }

    // call user-defined cell / row level click and mouseUp handler
    var handlerReturn;

    // the click occurred over a different col from the last click, fire a single click on
    // that cell (but not that row)
    // NOTE: this means if the user double clicks within a row, but the clicks land on different
    // columns we'll get a single click on each cell, and a double click on the row.
    if (colNum != this._clickCol) {
        handlerReturn = this._cellClick(record, rowNum, colNum);

    // otherwise fire a double click handler on the cell
    } else if (this.cellDoubleClick && (this.cellDoubleClick(record, rowNum, colNum) == false))
    {
       handlerReturn = false;
    }

    if (this.rowDoubleClick  && (this.rowDoubleClick(record, rowNum, colNum) == false))
        handlerReturn = false;

    // clear out the temp vars -- we don't want to fire a click on mouseUp after this doubleclick
    // (this is fired before mouseUp) and no need to hang onto the clickRow / col.
    this._mouseDownRow = this._mouseDownCol = null;
    this._clickRow = this._clickCol = null;

    if (handlerReturn == false) return false;

},

dragStart : function () {
    var EH = isc.EH,
        event = EH.lastEvent;
    // Try to normalize the appearance of the default native drag tracker image. In browsers
    // that support the EH.setDragTrackerImage() API, this will set the native drag tracker
    // image to the table row wherever the user starts the drag (e.g. if the user starts
    // the drag on an icon, then the browser will use the icon as the drag tracker image).

    if (event.target == this &&
        event.eventType == "dragStart")
    {
        var dt;
        try {
            dt = event.DOMevent.dataTransfer;
        } catch (e) {

            dt = null;
        }

        if (dt != null && dt.setDragImage != null) {
            var rowNum = this.getNearestRowToEvent(),
                rowElement = this.getTableElement(rowNum);
            if (rowElement != null) {
                var offsets = isc.Element.getOffsets(rowElement),
                    x = EH.getX(),
                    y = EH.getY();
                dt.setDragImage(rowElement, x - offsets.left, y - offsets.top);
            }
        }
    }
},

//>    @method    gridRenderer.dragMove()    ([A])
//        @group    events, dragging
//            drag move event
//        @return    (boolean)
//<
// XXX
// We may want to add handling for row and cell level rowDragMove() and cellDragMove() handlers.
// If we do this we would also add row and cell level dragStart / dragStop / dropMove / drop, etc.
// - Default (internal) implementation would handle dragSelection if this.canDragSelect
//   Not worrying about this for now.
dragMove : function () {

    if (this._suppressEventHandling() || !this.selectionEnabled() || !this.canDragSelect)
        return true;

    var rowNum = this.getNearestRowToEvent(),
        colNum = this.getNearestColToEvent();

    //this.logWarn("selectOnDragMove: " + [rowNum, colNum]);
    this.selection.selectOnDragMove(this, rowNum, colNum);
},

dragStop : function () {
    this.fireSelectionUpdated();
},

// Override Drag/drop snap-to-grid functionality from Canvas

// suppress drag offset when snap dragging to cells.
noSnapDragOffset : function (dragTarget) {
    return this.snapToCells;
},
getHSnapPosition : function (localCoordinate, dir) {
    if ( ! this.snapToCells) {
        return this.Super("getHSnapPosition", arguments);
    }
    var EH = this.ns.EH,
        direction = dir || this.snapHDirection,
        col = this.snapHGap ? Math.floor(localCoordinate / this.snapHGap) : this.getEventColumn(localCoordinate),
        beforeLeft = this.snapHGap ? (col * this.snapHGap) : this.getColumnLeft(col),
        beforeRight = this.snapHGap ? beforeLeft + this.snapHGap : this.getColumnLeft(col) + this.getColumnSize(col),
        afterCol = this.snapHGap ? col + 1 : this.getEventColumn(beforeRight + 1),
        afterLeft;

    if (afterCol >= 0 ) {
        afterLeft = this.snapHGap ? afterCol * this.snapHGap : this.getColumnLeft(afterCol);
    } else {
        afterLeft = beforeLeft;
    }
    var halfway = beforeLeft + (this.snapHGap ? this.snapHGap : this.getColumnSize(col)) / 2;

    // Fix up for cell borders if necessary
    if (this.snapInsideBorder) {
        var lb = isc.Element._getLeftBorderSize(this.baseStyle)
        var rb = isc.Element._getRightBorderSize(this.baseStyle)
        beforeLeft += lb;
        beforeRight -= rb;
        afterLeft += lb;
    }

    // For resize, always extend the drag-target to cover the current "over" cell
    if (EH.dragOperation == EH.DRAG_RESIZE) {
        var goingLeft = isc.EH.resizeEdge.contains("L");
        return goingLeft ? beforeLeft : beforeRight;
    } else {
        if (direction == isc.Canvas.BEFORE) {
            return beforeLeft;
        } else if (direction == isc.Canvas.AFTER) {
            return afterLeft;
        } else {
            // If we're exactly inbetween, go left
            if (localCoordinate <= halfway) {
                return beforeLeft;
            } else {
                return afterLeft;
            }
        }
    }

},

getVSnapPosition : function (localCoordinate, dir) {

    if ( ! this.snapToCells) {
        return this.Super("getVSnapPosition", arguments);
    }

    // this almost works...repositioning gets thrown off when moving up. May be worth exploring
    // at some point
    //if (this.snapVGap) {
    //    return this.Super("getVSnapPosition", localCoordinate, dir) + gridInnerPageTop;
    //}
    var EH = this.ns.EH,
        direction = dir || this.snapVDirection,
        // for snapVGap, row is just a snapVGap sized chunk of space
        row = this.snapVGap ? Math.floor(localCoordinate / this.snapVGap) : this.getEventRow(localCoordinate),
        // top coordinate of row
        beforeTop = this.snapVGap ? (row * this.snapVGap) : this.getRowTop(row),
        // bottom coordinate of row
        beforeBot = this.snapVGap ? beforeTop + this.snapVGap : this.getRowTop(row) + this.getRowSize(row),
        afterRow = this.snapVGap ? row + 1 : this.getEventRow(beforeBot + 1),
        afterTop;
    if (afterRow >= 0 ) {
        afterTop = this.snapVGap ? afterRow * this.snapVGap : this.getRowTop(afterRow);
    } else {
        afterTop = beforeTop;
    }
    var halfway = beforeTop + (this.snapVGap ? this.snapVGap : this.getRowSize(row)) / 2;


    // Fix up for borders if necessary
    if (this.snapInsideBorder) {
        var tb = isc.Element._getTopBorderSize(this.baseStyle)
        var bb = isc.Element._getBottomBorderSize(this.baseStyle)
        //this.logWarn("tb: " + tb + ", bb: " + bb);
        beforeTop += tb;
        beforeBot -= bb;
        afterTop += tb;
    }


    if (EH.dragOperation == EH.DRAG_RESIZE) {
       var goingUp = isc.EH.resizeEdge.contains("T");
       return goingUp ? beforeTop : beforeBot;
    } else {
        if (direction == isc.Canvas.BEFORE) {
            return beforeTop;
        } else if (direction == isc.Canvas.AFTER) {
            return afterTop;
        } else {
            // If we're exactly inbetween, go up
            if (localCoordinate <= halfway) return beforeTop;
            else return afterTop;
        }
    }
},

// AutoSizing
// --------------------------------------------------------------------------------------------

//>    @method    gridRenderer.getColumnAutoSize()    ([A])
//        @group    sizing, positioning
//      Get the size this column needs to be in order to accommodate it's contents.
//
//      Can only be called after draw()
//
//      NOTE: if using partial table rendering (showAllRows:false), this is the size for the
//      currently visible contents of the column
//<
getColumnAutoSize : function (columnNum, startRow, endRow) {
    if (this.isEmpty()) {
        return null;
    }
    // create an offscreen Canvas to do sizing in
    var columnSizer = this._columnSizer = this._columnSizer || isc.Canvas.create({
                               // flag to simplify debugging!
                               columnSizer:true,
                               top:-1000,
                               width:1, height:1,
                               overflow:"hidden",
                               autoDraw:false,
                               _generated:true
                           });

    // get HTML for a table containing only this column, written without column widths and
    // with no text wrapping
    var autoFit = this.autoFit,
        wrapCells = this.wrapCells;

    this.autoFit = true;
    this.wrapCells = false;

    // pass in startRow / endRow
    // If not explicitly specified, just use the current draw area
    // Passing this parameter in avoids us writing a (unnecessary in this case) spacer
    // above / below the cell values and will turn on the "fragment" logic in getTableHTML()
    // which avoids writing out DOM IDs on the various parts.
    if (startRow == null || endRow == null) {
        var drawRect = this.getDrawArea();
        startRow = drawRect[0];
        // remember drawRect is inclusive, we want exclusive
        endRow = drawRect[1]+1;
    }

//    this.logWarn("Logic to get col autoSize running:" + [columnNum, startRow, endRow]);
    // set a flag so we can write out different HTML for the sizer if necessary - such
    // as using inactive HTML for edit items in a listGrid
    this._gettingAutoSizeHTML = true;
    columnSizer.setContents(this.getTableHTML(columnNum,startRow,endRow, true));

    delete this._gettingAutoSizeHTML;

    this.autoFit = autoFit;
    this.wrapCells = wrapCells;

    // draw the table and figure out how large it is
    if (!columnSizer.isDrawn()) {
        columnSizer.draw();
    } else {
        if (columnSizer.isDirty()) columnSizer.redraw();
        if (columnSizer._delayedAdjustOverflow) columnSizer.adjustOverflow("Check autoFit column sizing");
    }

    var returnVal;
    if (isc.isA.Array(columnNum)) {
        // We're going to have to reach into the table.
        var table,
            nodes = columnSizer.getHandle().childNodes;
        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].tagName.toLowerCase() == "table") {
                table = nodes[i];
                break;
            }
        }
        var rowNum = this._getValidAutoFitRowNum();
        if (table && table.rows[rowNum]) {
            var firstRow = table.rows[rowNum],
                cells = firstRow.childNodes,
                numCells = cells.length;
            returnVal = [];


            if (isc.Browser.isMoz && isc.Browser.hasTextOverflowEllipsis) {
                for (var i = 0; i < numCells; ++i) {
                    returnVal[i] = Math.ceil(cells[i].getBoundingClientRect().width);
                }

            } else if (isc.Browser.isIE10) {
                var doc = firstRow.ownerDocument;

                var origMsCSSOMElementFloatMetrics = doc.msCSSOMElementFloatMetrics;
                doc.msCSSOMElementFloatMetrics = true;
                for (var i = 0; i < numCells; ++i) {
                    returnVal[i] = Math.ceil(cells[i].offsetWidth);
                }
                doc.msCSSOMElementFloatMetrics = origMsCSSOMElementFloatMetrics;

            } else {
                for (var i = 0; i < numCells; ++i) {
                    returnVal[i] = cells[i].offsetWidth;

                    if (isc.Browser.hasTextOverflowEllipsis) {
                        var c = Math.ceil(returnVal[i]);

                        returnVal[i] = (returnVal[i] == c ? c + 1 : c);
                    }
                }
            }
        }

    } else {
        returnVal = columnSizer.getScrollWidth();
    }

    // No need to immediately clear the columnSizer: It's drawn offscreen already and if we clear we'll have
    // to re-draw next time we size a column which can end up with a huge number of expensive
    // clear-draw's for a grid with lots of columns. Set up a timer to remove it after a period of time to
    // allow for other columns to make use of it in the meantime.

    var _this = this;
    this.fireOnPause("removeColumnSizer",
            function () { _this.removeColumnSizer(); },
            this.removeColumnSizerDelay);

    return returnVal;
},

// Overridden in ListGrid to handle the case where we're grouped and so have some
// col-spanning cells in group header rows (which need to be skipped)
_getValidAutoFitRowNum : function () {
    return 0;
},


// Table Cache Clearing
// --------------------------------------------------------------------------------------------

// clear anything we've cached about the HTML table we draw
redraw : function (a,b,c,d) {
    this._resetEmbeddedComponents();
    this.invokeSuper(isc.GridRenderer, "redraw", a,b,c,d);
    // if we're redrawing in response to the end of 'fast scrolling', the suppresDrawAhead flag
    // will have been set in markForRedraw()
    // clear this now
    delete this._suppressDrawAheadDirection;

},

modifyContent : function () {

    // resize / place embedded components before
    // - restoring virtual scrolling
    // - adjusting overflow
    // Both of these need to know the drawn heights of rows (including E.C's)
    // If we're performing an animated show/hide of row, don't attempt to place embedded
    // components until it completes
    if (!this._animatedShowStartRow) this._placeEmbeddedComponents();

    if (this._targetRow != null) {

        this._scrollFromRedraw = true;
        this._scrollToTargetRow("scrollToRow in modifyContent");
        this._scrollFromRedraw = null;

        // show the table element, which is drawn as hidden so we can scroll before we make it
        // visible, to prevent it showing the wrong scroll position briefly
        var tableElement = this.getTableElement();
        if (tableElement) tableElement.style.visibility = "inherit";


    }

    if (this._isVirtualScrolling) {
        // shrink the endSpacer, if any, to avoid scrolling when unnecessary.
        var totalRowHeight = this._getDrawnRowHeights().sum();
        if (totalRowHeight < this.getViewportHeight()) {
            this._endRowSpacerHeight = 0;
            var spacer = isc.Element.get(this.getID() + "_endSpacer"),
                height = this._endRowSpacerHeight + (this.endSpace || 0);
            if (spacer) {

                if (height == 0) spacer.style.display = "none"
                else spacer.style.display = "";
                spacer.style.height = height + "px";
            }
            // overflow:hidden, so no need to rewrite div content
            //this.logWarn("shrank spacer: " + totalRowHeight);
        }


        var visibleRows = this.getVisibleRows(),
            numVisibleRows = Math.max(1, visibleRows[1] - visibleRows[0]),
            trueRatio = numVisibleRows/this.getTotalRows(),
            approxRatio = this.getViewportRatio(true);
        //this.logWarn("viewportHeight: " + this.getViewportHeight() +
        //             ", visibleRows: " + visibleRows +
        //             ", approxRatio: " + approxRatio +
        //             ", trueRatio: " + trueRatio);
        if (isc.isA.Number(trueRatio) &&
            ((approxRatio == 1 && trueRatio < 1) ||
             approxRatio/trueRatio > 1.25)
           )
        {
            this._viewRatioRowHeight = Math.max(this.cellHeight,
                                         Math.round(this.getViewportHeight() / numVisibleRows));
            //this.logWarn("set average row height to: " + this._viewRatioRowHeight);
        }
    }

    /*
    // resize the bottom spacer to keep the scrollHeight constant
    var totalRows = this.getTotalRows(),
        oldScrollHeight = this._oldScrollHeight;
    // if we've got the same size dataset as before, and we're not drawing all the way to the
    // end
    if (this._lastTotalRows == totalRows && this._lastDrawnRow < totalRows-1) {
        var newScrollHeight = this.getScrollHeight(true),
            spacer = isc.Element.get(this.getID() + "_endSpacer"),
            spacerHeight = parseInt(spacer.offsetHeight),
            newSpacerHeight = spacerHeight + (oldScrollHeight-newScrollHeight);

        this.logWarn("adding extraHeight.  oldScrollHeight: " + oldScrollHeight +
                     ", lastDrawnRow: " + this._lastDrawnRow +
                     ", newScrollHeight: " + newScrollHeight +
                     ", spacerHeight: " + spacerHeight +
                     ", newSpacerHeight: " + newSpacerHeight);

        if (newSpacerHeight < 0) {
            this.logWarn("************** NOT ENOUGH ROOM to adjust spacer");
            newSpacerHeight = this.cellHeight;
        }
        spacer.style.height = newSpacerHeight + "px";
    }
    this._lastTotalRows = totalRows;
    */


},


// Helper to update the explicit space above / below the rows in the grid
setStartSpace : function (value) {
    if (!isc.isA.Number(value) || value == this.startSpace) return;
    var reduction = this.startSpace && this.startSpace > value;
    this.startSpace = value;
    if (!this.isDrawn()) return;
    var height = value + this._startRowSpacerHeight,
        spacer = isc.Element.get(this.getID() + "_topSpacer");
    if (spacer) {
        if (height == 0) spacer.style.display = "none";
        else spacer.style.display = ""; // default (== "inline")


        if (this._canResizeSpacerDivs) {
            spacer.style.height = height + "px";
        }
        // overflow:hidden so don't have to rewrite contents if we're shrinking
        if (!reduction || !this._canResizeSpacerDivs) {
            spacer.innerHTML = isc.Canvas.spacerHTML(1,height);
        }
        this._markForAdjustOverflow();
    }
    // If there was no spacer we must be in cacheDOM mode where we don't currently
    // support startSpace / endSpace

},

setEndSpace : function (value) {
    if (!isc.isA.Number(value) || value == this.endSpace) return;
    var reduction = this.endSpace && this.endSpace > value;
    this.endSpace = value;
    if (!this.isDrawn()) return;
    var height = value + this._endRowSpacerHeight,
        spacer = isc.Element.get(this.getID() + "_endSpacer");
    if (spacer) {
        if (height == 0) spacer.style.display = "none";
        else spacer.style.display = ""; // default (== "inline")
        if (this._canResizeSpacerDivs) spacer.style.height = height + "px";
        if (!reduction || !this._canResizeSpacerDivs) {
            spacer.innerHTML = isc.Canvas.spacerHTML(1,height);
        }
        this._markForAdjustOverflow();
    }
    // If there was no spacer we must be in cacheDOM mode where we don't currently
    // support startSpace / endSpace
},

setLeftSpace : function (value) {
    if (this.leftSpace == value) return;
    this.leftSpace = value;
    this.redraw();
},

setRightSpace : function (value) {
    if (this.rightSpace == value) return;
    this.rightSpace = value;
    this.redraw();
},

clear : function () {
    this.Super("clear", arguments);
    this.removeColumnSizer(true);
    this._clearTableCache();
    // if we're cleared before the delayed redraw from the end of fast scrolling fires,
    // clear the suppressDrawAheadDirection flag
    delete this._suppressDrawAheadDirection;
},

// clear anything we've cached about the HTML table we draw
_clearTableCache : function () {
    // drop our cache of HTML row elements
    this._rowElements = null;
    this._tableElement = null;

    // clear the cached table geometry information so it'll be recalculated the next time it's
    // asked for
    delete this._renderedColumnWidths;
    delete this._undrawnHeight;
    delete this._rowHeights;

    this._scrollRedraw = false;
}

});
isc.GridRenderer._gridAPIs = {
    // customizing cell values
    // --------------------------------------------------------------------------------------------
    //>    @method    gridRenderer.getCellRecord()
    // Return the record that holds the value for this cell.
    // <P>
    // Implementing <code>getCellRecord</code> is optional: the actual HTML placed into each
    // grid cell comes from <code>getCellValue</code>, and a valid grid can be created without any
    // notion of "records" at all.
    // <p>
    // If you do implement <code>getCellRecord</code>, the value you return is passed to you as the
    // "record" parameter in other methods.
    //
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (object)    record for this cell
    // @visibility external
    //<
    getCellRecord : "rowNum,colNum",

    //>    @method    gridRenderer.getCellValue()
    // Return the HTML to display in this cell.  Implementing this is required to get a non-empty
    // grid.
    //
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (string)    HTML to display in this cell
    // @visibility external
    //<
    getCellValue : "record,rowNum,colNum,gridBody",

    //>    @method    gridRenderer.findRowNum()
    // Given a record displayed in this grid, find and return the rowNum in which the record
    // appears.
    // <P>
    // As with +link{gridRenderer.getCellRecord()} implementing this method is optional as a valid
    // grid may be created without any notion of records.
    //
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @return    (number) index of the row containing the record or -1 if not found
    // @visibility external
    //<
    findRowNum : "record",
    //>    @method    gridRenderer.findColNum()
    // Given a record displayed in this grid, find and return the colNum in which the record
    // appears.
    // <P>
    // As with +link{gridRenderer.getCellRecord()} implementing this method is optional as a valid
    // grid may be created without any notion of records, or records may not be displayed in a
    // single column (as with the +link{class:ListGrid,ListGrid} class where each record is
    // displayed in an entire row.
    //
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @return    (number) index of the column containing the record or -1 if not found
    // @visibility external
    //<
    findColNum : "record",

    // customizing cell styling
    // --------------------------------------------------------------------------------------------
    //>    @method    gridRenderer.getBaseStyle() ([A])
    // Return the base stylename for this cell.  Default implementation just returns this.baseStyle.
    // See +link{listGrid.getCellStyle,getCellStyle()} for a general discussion of how to style cells.
    //
    // @see getCellStyle()
    //
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (CSSStyleName)    CSS class for this cell
    // @visibility external
    //<
    getBaseStyle : "record,rowNum,colNum",

    // getCellStyle doc'd above
    getCellStyle : "record,rowNum,colNum",

    //>    @method    gridRenderer.getCellCSSText() ([A])
    // Return CSS text for styling this cell, which will be applied in addition to the CSS class
    // for the cell, as overrides.
    // <p>
    // "CSS text" means semicolon-separated style settings, suitable for inclusion in a CSS
    // stylesheet or in a STYLE attribute of an HTML element.
    //
    // @see getCellStyle()
    //
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (string)    CSS text for this cell
    // @visibility external
    //<
    getCellCSSText : "record,rowNum,colNum",

    // doc'd above
    cellIsEnabled : "rowNum,colNum",

    // customizing table geometry
    // --------------------------------------------------------------------------------------------
    //>    @method    gridRenderer.getRowHeight()
    // Return the height this row should be.  Default is this.cellHeight. If
    // +link{GridRenderer.fixedRowHeights} is false, the row may be rendered taller than this
    // specified size.
    // <P>
    // If records will be variable height,
    // you should switch on +link{gridRenderer.virtualScrolling, virtualScrolling}.
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number
    // @return    (number)    height in pixels
    // @visibility external
    //<
    // Undocumented 'isFrozenBody' param used by the ListGrid
    getRowHeight : "record,rowNum,isFrozenBody",

    //>    @method    gridRenderer.getRowSpan() ([A])
    // Return how many rows this cell should span.  Default is 1.
    // <P>
    // NOTE: if using horizontal incremental rendering, <code>getRowSpan()</code> may be called for
    // a rowNum <b>in the middle of a spanning cell</b>, and should return the remaining span from
    // that rowNum onward.
    // <P>
    // NOTE: if a cell spans multiple rows, getCellRecord/Style/etc will be called with the topmost
    // row coordinates only.
    //
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (number)    number of cells to span
    // @visibility external
    //<
    getRowSpan : "record,rowNum,colNum",

    // synthetic row/cell events
    // --------------------------------------------------------------------------------------------

    //>    @method    gridRenderer.cellOut() ([A])
    // Called when the mouse pointer leaves a cell
    //
    // @group   events
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    cellOut : "record,rowNum,colNum",

    //>    @method    gridRenderer.cellOver() ([A])
    // Called when the mouse pointer enters a cell
    //
    // @group   events
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    cellOver : "record,rowNum,colNum",

    //>    @method    gridRenderer.rowOut() ([A])
    // Called when the mouse pointer leaves a row
    //
    // @group   events
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    rowOut : "record,rowNum,colNum",

    //>    @method    gridRenderer.rowOver() ([A])
    // Called when the mouse pointer enters a row
    //
    // @group   events
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    rowOver : "record,rowNum,colNum",

    //>    @method    gridRenderer.cellMove() ([A])
    // Called when the mouse pointer moves within a cell
    //
    // @group   events
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility internal
    //<
    cellMove : "record,rowNum,colNum",

    //>    @method    gridRenderer.rowMove() ([A])
    // Called when the mouse pointer moves within a row
    //
    // @group   events
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility internal
    //<
    rowMove : "record,rowNum,colNum",


    //>    @method    gridRenderer.cellContextClick() ([A])
    // Called when a cell receives a contextclick event.
    //
    // @group   events
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    cellContextClick : "record,rowNum,colNum",

    //>    @method    gridRenderer.rowContextClick() ([A])
    // Called when a row receives a contextclick event.
    // @group   events
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    rowContextClick : "record,rowNum,colNum",
    // legacy support
    recordContextClick : "record,recordNum,fieldNum",

    //>    @method    gridRenderer.cellMouseDown() ([A])
    // Called when a cell receives a mousedown event.
    //
    // @group   events
    // @param   record  (ListGridRecord)    cell record as returned by getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    cellMouseDown : "record,rowNum,colNum",

    //>    @method    gridRenderer.rowMouseDown() ([A])
    // Called when a row receives a mousedown event.
    //
    // @group   events
    // @param  record   (ListGridRecord)    record object returned from 'getCellRecord()'
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    rowMouseDown : "record,rowNum,colNum",

    // legacy
    recordMouseDown : "recordNum,fieldNum",

    //>    @method    gridRenderer.cellMouseUp() ([A])
    // Called when a cell receives a mouseup event.
    //
    // @group   events
    // @param   record  (ListGridRecord)    Record object (retrieved from getCellRecord(rowNum, colNum))
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    cellMouseUp : "record,rowNum,colNum",

    //>    @method    gridRenderer.rowMouseUp() ([A])
    // Called when a row receives a mouseup event.
    //
    // @group   events
    // @param   record  (ListGridRecord)    Record object returned from getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    rowMouseUp : "record,rowNum,colNum",
    recordMouseUp : "recordNum,fieldNum",

    //>    @method    gridRenderer.selectOnMouseDown() ([A])
    // Called when a cell / record receives a mouseDown event, if no cell / row level mouseDown
    // handlers return false.
    // Default implementation handles selection by calling this.selection.selectOnMouseDown()
    //
    // @group   selection
    // @param   record  (ListGridRecord)    Record object returned from getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    //<
    selectOnMouseDown : "record,rowNum,colNum",

    //>    @method    gridRenderer.selectOnRightMouseDown() ([A])
    // Called when a cell / record receives a right mouseDown event, if this.canSelectOnRightMouse
    // is true.
    // Default implementation handles selection by calling this.selectOnMouseDown()
    //
    // @group   selection
    // @param   record  (ListGridRecord)    Record object returned from getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @see selectOnMouseDown()
    //<
    selectOnRightMouseDown : "record,rowNum,colNum",

    //>    @method    gridRenderer.selectOnMouseUp() ([A])
    // Called when a cell / record receives a mouseUp event, if no cell / row level mouseUp
    // handlers return false.
    // Default implementation handles selection by calling this.selection.selectOnMouseUp()
    //
    // @group   selection
    // @param   record  (ListGridRecord)    Record object returned from getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    //<
    selectOnMouseUp : "record,rowNum,colNum",

    //>    @method    gridRenderer.cellClick() ([A])
    // Called when a cell receives a click event.
    //
    // @group   events
    // @param   record  (ListGridRecord)    Record object returned from getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    cellClick : "record,rowNum,colNum",

    //>    @method    gridRenderer.cellDoubleClick() ([A])
    // Called when a cell receives a double click event.
    //
    // @group   events
    // @param   record  (ListGridRecord)    Record object returned from getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    cellDoubleClick : "record,rowNum,colNum",

    //>    @method    gridRenderer.rowClick() ([A])
    // Called when a row receives a click event.
    //
    // @group   events
    // @param   record  (ListGridRecord)    Record object returned from getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    rowClick : "record,rowNum,colNum",

    //>    @method    gridRenderer.rowDoubleClick() ([A])
    // Called when a row receives a double click event.
    //
    // @group   events
    // @param   record  (ListGridRecord)    Record object returned from getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    rowDoubleClick : "record,rowNum,colNum",

    // Hover events
    // --------------------------------------------------------------------------------------------
    //>    @method    gridRenderer.cellHover() ([A])
    // Called when the mouse hovers over a cell if this.canHover is true.
    //  Returning false will suppress the hover text from being shown if this.showHover is true.
    //
    // @group   events
    // @see     canHover
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    //<
    cellHover : "record,rowNum,colNum",

    //> @method gridRenderer.cellValueHover() ([A])
    // Optional stringMethod to fire when the user hovers over a cell and the value is clipped.
    // If this.showClippedValuesOnHover is true, the default behavior is to show a hover canvas
    // containing the HTML returned by cellValueHoverHTML(). Return false to suppress this default
    // behavior.
    //
    // @group events
    // @param record (ListGridRecord) cell record as returned by getCellRecord()
    // @param rowNum (number) row number for the cell
    // @param colNum (number) column number of the cell
    // @return (boolean) false to suppress the standard hover
    // @see showClippedValuesOnHover
    // @see cellValueIsClipped()
    // @see cellValueHoverHTML()
    // @visibility external
    //<
    cellValueHover : "record,rowNum,colNum",

    //>    @method    gridRenderer.rowHover() ([A])
    // Called when the mouse hovers over a row if this.canHover is true.
    //  Returning false will suppress the hover text from being shown if this.showHover is true.
    //
    // @group   events
    // @see     canHover
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event (default behavior of showing the hover)
    // @visibility external
    //<
    rowHover : "record,rowNum,colNum",

    //>    @method    gridRenderer.cellHoverHTML() ([A])
    // StringMethod to dynamically assemble an HTML string to show in a hover window over the
    // appropriate cell/record when this.canHover and this.showHover are both true.
    // Called when the mouse hovers over a cell.
    //
    // @group events
    // @param record (ListGridRecord) cell record as returned by getCellRecord
    // @param rowNum (number) row number for the cell
    // @param colNum (number) column number of the cell
    // @return (HTMLString) the html to be shown inside the hover for this cell
    // @see canHover
    // @see showHover
    // @visibility external
    //<
    cellHoverHTML : "record,rowNum,colNum",

    //> @method gridRenderer.cellValueHoverHTML() ([A])
    // Returns the HTML that is displayed by the default cellValueHover handler. Return null or
    // an empty string to cancel the hover.
    // <smartgwt><p>Use <code>setCellValueHoverFormatter()</code> to provide a custom
    // implementation.</smartgwt>
    //
    // @group events
    // @param record (ListGridRecord) cell record as returned by getCellRecord()
    // @param rowNum (number) row number for the cell
    // @param colNum (number) column number of the cell
    // @param defaultHTML (HTMLString) the HTML that would have been displayed by default
    // @return (HTMLString) HTML to be displayed in the hover. If null or an empty string, then the hover
    // is canceled.
    // @see cellValueHover()
    // @visibility external
    //<
    cellValueHoverHTML : "record,rowNum,colNum,defaultHTML",

    //>    @method    gridRenderer.getCellHoverComponent() ([A])
    // StringMethod to dynamically create a Canvas-based component to show as a hover window
    // over the appropriate cell/record when this.canHover and this.showHover are both true and
    // when an override of getCellHoverComponent() is present.
    // Called when the mouse hovers over a cell.
    //
    // @group   events
    // @param    record  (ListGridRecord)    cell record as returned by getCellRecord
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (Canvas)    a Canvas to be shown as the hover for this cell
    // @see canHover
    // @see showHover
    // @visibility external
    //<
    getCellHoverComponent : "record,rowNum,colNum",

    // selection notification
    // --------------------------------------------------------------------------------------------

    //>    @method    gridRenderer.selectionChanged() ([A])
    // Called when (row-based) selection changes within this grid. Note this method fires for
    // each record for which selection is modified - so when a user clicks inside a grid this
    // method will typically fire twice (once for the old record being deselected, and once for
    // the new record being selected).
    // <P>
    // NOTE: For updating other components based on selections or triggering selection-oriented
    // events within an application, see the
    // +link{dataBoundComponent.selectionUpdated(),selectionUpdated()} event
    // which is likely more suitable.  Calls to +link{selection.getSelection(),getSelection()}
    // from within this event may not return a valid set of selected records if the event has
    // been triggered by a call to +link{selection.selectAll(),selectAll()} or
    // +link{selection.deselectAll(),deselectAll()} - in this case use the
    // +link{dataBoundComponent.selectionUpdated(),selectionUpdated()} event instead.
    //
    // @param    record  (ListGridRecord)    record for which selection changed
    // @param    state   (boolean)    New selection state (true for selected, false for unselected)
    // @group selection
    // @visibility external
    //<
    selectionChanged : "record,state",

    // Doc'd as DataBoundComponent.selectionUpdated
    selectionUpdated : "record,recordList",

    //>    @method    gridRenderer.cellSelectionChanged() ([A])
    // Called when (cell-based) selection changes within this grid.
    //
    // @param    cellList    (array) Array of cells whos selected state was modified.
    // @return    (boolean)   Returning false will prevent the GridRenderer styling from being updated
    //                      to reflect the selection change.
    // @group selection
    // @visibility external
    //<
    cellSelectionChanged : "cellList",

    // IDs for legacy test tools; JSDoc above
    getRowElementId : "rowNum,physicalRowNum",
    getCellElementId : "rowNum,physicalRowNum,colNum,physicalColNum",

    // Row Heights and Embedded Components
    // ---------------------------------------------------------------------------------------
    shouldFixRowHeight : "record,rowNum",

    updateEmbeddedComponentZIndex : "component",
    updateEmbeddedComponentCoords : "component,record,rowNum,colNum",

    // WAI ARIA
    // ---------------------------------------------------------------------------------------
    getRowRole : "rowNum,record",
    getRowAriaState : "rowNum,record",
    getCellRole : "rowNum,colNum,record",
    getCellAriaState : "rowNum,colNum,record"
};
isc.GridRenderer.registerStringMethods(isc.GridRenderer._gridAPIs);





isc.defineClass("SelectionOrRollOverCanvas", "Canvas").addProperties({
    cssPointerEvents: "none"
});

//>    @class    ListGrid
// A ListGrid is a +link{DataBoundComponent} that displays a list of objects in a grid, where
// each row represents one object and each cell in the row represents one property.
//
//  @implements DataBoundComponent
//  @treeLocation Client Reference/Grids
//  @visibility external
//<
// Make ListGrid a subclass of VLayout. This allows us to change the order of the sub-components
// - show summary row between header and body, etc.

isc.ClassFactory.defineClass("ListGrid", "VLayout", "DataBoundComponent");

// Synonym for backCompat.  NOTE: define an alias rather than make a subclass, otherwise, attempts
// to skin the class using the old name would only affect the subclass!
isc.addGlobal("ListViewer", isc.ListGrid);
// define groups for documentation purposes

    //> @groupDef data
    //<

    //> @groupDef databinding
    // DataBinding means the automatic, highly customizable process of 'binding' a UI component
    // to a DataSource, so that a UI component displays, edits and saves DataSource records
    // using appropriate formatters, editors, validation rules, and persistence logic.
    //
    // @see interface:DataBoundComponent
    // @title DataBinding
    //<

    //> @groupDef sorting
    //<

    //> @groupDef editing
    // Data being displayed by a grid may be edited within the grid, by showing editing
    // interfaces embedded inside the cells of the grid.
    // <P>
    // <b>Enabling editing</b>
    // <P>
    // Editing is enabled when +link{listGrid.canEdit,canEdit} is <code>true</code>.  When enabled,
    // the user can begin editing via the
    // +link{listGrid.editEvent,editEvent}, typically click or double-click.  Editing can also be triggered
    // programmatically by a call to +link{listGrid.startEditing,startEditing()} or
    // +link{listGrid.startEditingNew,startEditingNew()}.
    // <P>
    // <b>New record creation</b>
    // <P>
    // By default, editing is restricted to existing records.  Setting +link{listGrid.listEndEditAction} to
    // "next" allows the user to create new records by simply navigating off the end of the dataset
    // with the keyboard.  Editing of new records can also be initiated with
    // +link{listGrid.startEditingNew()}, for example, from a button outside the grid.  See the
    // +link{group:unsavedRecords,Unsaved Records Overview} for special concerns when dealing
    // with unsaved records.
    // <P>
    // <b>Saving changes</b>
    // <P>
    // Saving of changes is triggered automatically when the user navigates out of the row or cell
    // being edited (based on +link{listGrid.saveByCell}) or when the user ends editing.   For
    // a "mass update" interface, automatic saving of changes can be disabled entirely via
    // +link{listGrid.autoSaveEdits,autoSaveEdits:false}, in which case a manual call to
    // +link{listGrid.saveEdits,saveEdits()} or +link{listGrid.saveAllEdits,saveAllEdits()} is required
    // to trigger saving.
    // <P>
    // If a grid has no DataSource, saving means that the properties of the +link{ListGridRecord}s
    // in +link{listGrid.data,grid.data} are directly changed.
    // <P>
    // For a grid with a DataSource, saving will be accomplished by using DataSource "update"
    // operations for existing records, and DataSource "add" operations for new records.  If multiple
    // records have been edited and +link{listGrid.saveAllEdits,saveAllEdits()} is called,
    // +link{rpcManager.startQueue,request queuing} will be automatically used to enable all
    // edits to be saved in one HTTP turnaround (if using the SmartClient Server).
    // <P>
    // By default, a grid will send only updated fields and primaryKey fields as part of
    // +link{dsRequest.data} so that the server can discern which fields the user actually changed.
    // However, the grid always includes the original field values in the
    // dsRequest as +link{dsRequest.oldValues}.
    // <P>
    // Note that although it is possible to load DataSource data without actually declaring a
    // +link{dataSourceField.primaryKey,primaryKey field}, a primaryKey must be declared for
    // editing and saving.  The values of primaryKey fields is how SmartClient identifies the
    // changed record to the server.
    // <P>
    // <b>Validation</b>
    // <P>
    // Any time saving is attempted, validation is automatically triggered.  Values entered by the
    // user will be checked against the +link{listGridField.validators} and the
    // +link{dataSourceField.validators}. Any invalid values abort an attempted save.
    // <P>
    // Similar to editing and saving, validation can be done on row transitions or on cell
    // transitions by setting +link{listGrid.validateByCell,validateByCell}, or can be disabled entirely
    // via +link{listGrid.neverValidate,neverValidate:true}.
    // <P>
    // <b>Editability of cells</b>
    // <P>
    // Editors will either be shown for the complete row or for a single cell based on
    // +link{listGrid,editByCell,editByCell}.  Whether a cell can be edited can be controlled on a
    // per field basis by setting +link{listGridField.canEdit,field.canEdit}, or on a per-record basis
    // by setting +link{listGrid.recordEditProperty,recordEditProperty} on a
    // +link{ListGridRecord,record}, or can be controlled on an arbitrary, programmatic basis via
    // an override of +link{listGrid.canEditCell()}.
    // <P>
    // Cells which are not editable just display the cell's current value.
    // <P>
    // <b>Keyboard Navigation</b>
    // <P>
    // Full keyboard navigation is supported by default, including Tab and Shift-Tab to navigate
    // between cells in a row, and Up Arrow and Down Arrow to traverse rows.  Several properties
    // on both grids and fields, all named *EditAction, control navigation behavior of certain keys
    // (eg Enter).
    // <P>
    // You can use +link{listGrid.startEditing,startEditing(<i>rowNum</i>, <i>colNum</i>)} to
    // programmatically move editing to a particular cell, for example, during a
    // +link{listGridField.changed,field.changed()} event.
    // <P>
    // <b>editValues (unsaved changes)</b>
    // <P>
    // The term "editValues" means changes that the user has made to the dataset which have not
    // been saved.  The grid manages and stores editValues separately from the data itself in order
    // to allow the user to revert to original values, and in order to enable to grid to send only
    // updated fields to the server.
    // <P>
    // Because editValues are stored separately, if you directly access the dataset (eg via
    // <code>grid.getData().get()</code>) you will see the records without the user's unsaved changes.
    // Many APIs exist for retrieving and managing editValues (search for editValue).
    // For the common case of needing to access the record-as-edited, you can call
    // +link{listGrid.getEditedRecord,grid.getEditedRecord(rowNum)}.
    // <P>
    // When accessing and manipulating edited data, you should think carefully about whether
    // you want to be working with the original data or with the edited version.  Values entered
    // by the user may not have been validated yet, or may have failed validation, hence you may
    // find a String value in a field of type "date" or "int", which could cause naive formatters or
    // totaling functions to crash.
    // <P>
    // Setting editValues via APIs such as +link{listGrid.setEditValue()} is fully equivalent
    // to the user making changes to data via the editing UI.  If you <i>also</i> allow editing
    // external to the grid, setting editValues is one way to combine changes from external
    // editors into the grid's edits, so that you can do a single save.
    // <P>
    // <b>Customizing Cell Editors</b>
    // <P>
    // When a cell is being edited, the editor displayed in the cell will be a +link{class:FormItem}.
    // The editor type for the cell will be determined by +link{listGrid.getEditorType()} based on the
    // specified +link{ListGridField.editorType} or +link{ListGridField.type, data type} for the field in
    // question.
    // <P>
    // You can customize the editor by setting +link{listGridField.editorProperties} to a set of
    // properties that is valid for that FormItem type.  Custom FormItem classes are also allowed,
    // for example, you may use +link{formItem.icons} to create an icon that launches a separate
    // +link{Dialog} in order to provide an arbitrary interface that allows the user to select the
    // value for a field.
    // <P>
    // <b>Events</b>
    // <P>
    // Editing triggers several events which you can provide handlers for in order to customize
    // editing behavior.  Some of the most popular are +link{listGridField.change,field.change()},
    // +link{listGridField.changed,field.changed()} for detecting changes made by the user,
    // +link{listGrid.cellChanged()} for detecting changes that have been successfully saved,
    // and +link{listGrid.editorEnter,editorEnter} and +link{listGrid.editorExit,editorExit()}
    // for detecting user navigation during editing.
    // <P>
    // <smartclient>
    // You can also install event handlers directly on the FormItem-based editors used in the grid
    // via +link{listGridField.editorProperties,editorProperties} as mentioned above.  When handling
    // events on items, or which involve items, be aware that in addition to standard
    // +link{FormItem} APIs, editors have the following properties:
    // <P>
    // - <code>rowNum</code>: The rowNum of the record being edited.<br>
    // - <code>colNum</code>: The colNum of the cell being edited.<br>
    // - <code>grid</code>: A pointer back to the listGrid containing the record.
    // </smartclient>
    // <smartgwt>
    // <code>ListGridField.setEditorType()</code> can be used to customize the editors shown
    // for each field, including providing FormItem-specific event handlers.  However,
    // ListGrid-provided event APIs should be used wherever possible (for example, use
    // <code>EditorEnterEvent</code> rather than <code>FocusEvent</code>).  If, in a FormItem
    // event handler, you need access to the ListGrid, you can either declare the event handler
    // as a Java "inner class" in a scope where the ListGrid is available as a final variable,
    // or you can use <code>event.getItem().getContainerWidget()</code>.  Note the ListGrid APIs
    // +link{listGrid.getEditRow,getEditRow()} and +link{listGrid.getEditCol,getEditCol()}
    // indicate what cell is being edited.
    // <P>
    // For more dynamic editor customization, include changing the type of editor used on a
    // per-row basis, use +sgwtlink{listGrid.setEditorCustomizer()}.
    // <P>
    // <b>NOTE:</b> with both APIs, in effect several FormItems are generated from the
    // customized FormItem you provide - see the docs for
    // +link{DataSourceField.editorType()} for special coding patterns that apply in this
    // case.
    // </smartgwt>
    // <P>
    // <b>Binary Fields</b>
    // <P>
    // The ListGrid will automatically show "view" and "download" icon buttons for binary field
    // types (see +link{type:ListGridFieldType}).  However, you cannot use an upload control
    // embedded within a ListGrid row to upload files (regardless of whether you use FileItem or
    // UploadItem).  This is because, in the browser's security model, native HTML upload
    // controls cannot be re-created and populated with a chosen file, and the ListGrid needs
    // to be able to re-create editors at any time in order to handle loading data on scroll,
    // scrolling editors in and out of view, adding new rows, changing sort direction, and
    // other use cases.
    // <P>
    // However you <i>can</i> create an editor with a +link{formItem.icons,FormItem icon} that
    // pops up a separate Window containing a FileItem in a DynamicForm, so long as the form in
    // the Window saves the uploaded file immediately rather than trying to have the grid
    // perform the save.
    //
    // @title Grid Editing
    // @treeLocation Client Reference/Grids/ListGrid
    // @visibility external
    //<



    //> @groupDef unsavedRecords
    // APIs such as +link{listGrid.startEditingNew(),startEditingNew()} or
    // +link{listGrid.listEndEditAction,listEndEditAction:"next"} allow editing records that have not
    // been saved to the server.  These unsaved records are special in several ways:
    // <ul>
    // <li> there is no actual Record object in the dataset for them: <code>getRecord(rowNum)</code>
    // will return null, instead, <code>getEditValues(rowNum)</code> allows access to field values for
    // the unsaved record
    // <li> rows for editing these records always appear at the end of the grid and do not sort with
    // other rows
    // <li> because unsaved records lack an actual Record object and lack a
    // +link{dataSourceField.primaryKey} value, they have limited functionality: they cannot be
    // selected, and do not support +link{listGrid.showRecordComponents} and certain other features.
    // </ul>
    // <P>
    // If you need to work with unsaved records and have all ListGrid features apply to them, this is
    // usually a sign that you should re-think your UI for adding new records.  Consider the following
    // approaches - which works best will depend on the application:
    // <ul>
    // <li> actually save a new record to persistent storage, then start editing it.  This has the
    // advantage that the user will never lose data by exiting the application with unsaved
    // records, which can be important if there is a lot of data entry before the record is ready to
    // save (for example, a new issue report in an issue-tracking applications, or a new blog entry).
    // This is also a good approach if the user may want to get a unique ID for the new record
    // right away (again useful for a new issue report or blog entry).
    // <P>
    // If values for several fields are required before the record should be visible on other screens
    // or to other users, you can add a field to the record to flag it as incomplete so that it is not
    // shown on other screens.  Alternatively, require certain fields to be entered via an external
    // form or dialog before the record is added to the grid.
    // <P>
    // Saving a new record and editing it can be done via +link{DataSource.addData()} followed by a call to
    // +link{listGrid.startEditing()} once the record has been saved.
    // <li> edit new records via a separate +link{DynamicForm,form} instead, possibly in a modal
    // +link{Window} - then unsaved records never need to be shown in the grid.  Similar to the
    // approach above, this modal form might have only certain minimum fields to make a valid
    // new record, then further editing could continue in the grid.
    // <li> use a +link{dataSource.clientOnly,clientOnly DataSource} so that records can be saved
    // immediately without contacting the server.  This is a good approach if several unsaved records
    // need to be manipulated by multiple components before they are finally saved.
    // <li> use +link{DataSource.updateCaches()} with an "add" DSResponse to cause a new record to be
    // added to the grid due to +link{ResultSet,automatic cache synchronization}.  At this point the
    // grid will believe the record exists on the server and it will be treated like any other saved
    // record.  This means your server code will need to handle the fact that the ListGrid will submit
    // "update" DSRequests for any subsequent edits.
    // </ul>
    // <b>NOTE about validation:</b> by design, SmartClient assumes that any record that has been
    // saved is valid and does not validate field values that appear in records loaded from the
    // server.  This includes records added to a clientOnly DataSource via
    // +link{DataSource.setCacheData()} as well as records added due to a call to
    // +link{DataSource.updateCaches()}.
    // <P>
    // Usually the best approach is to avoid this situation by editing such records in a form or other
    // control until they are valid rather than showing invalid records in a grid.  However, if such
    // records need to be considered invalid, one approach is to take field values and add them as
    // editValues via +link{listGrid.setEditValues()}.  At this point the ListGrid will consider the
    // values as user edits and will validate them.
    //
    // @title Handling Unsaved Records
    // @visibility external
    //<

    //> @groupDef imageColumns
    // Columns that show images either as their only appearance or in addition to text.
    //<

    //> @groupDef formulaFields
    // Fields with values calculated from other fields in the grid.
    //<

isc.defineClass("GridBody", isc.GridRenderer).addProperties({
    // suppress adjustOverflow while pending a redraw so we can resize smaller without seeing
    // a scrollbar flash in and out of existence
    adjustOverflowWhileDirty:false,


    _redrawToFixIEFocusScrollArtifacts:isc.Browser.isIE && isc.Browser.version > 9,

    initWidget : function () {
        this.Super("initWidget", arguments);


        if (isc.screenReader) this._redrawToFixIEFocusScrollArtifacts = false;
    },

    canSelectRecord : function(record) {
        return this.grid.canSelectRecord(record);
    },

    // adjustOverflow() - overridden to support 'autoFitData' behavior
    adjustOverflow : function (reason, a,b,c,d) {
        // If we get naively called while undrawn just call Super which will bail.
        if (!this.isDrawn()) return this.Super("adjustOverflow", arguments);
        // we call 'getDelta' from this method which can fall back through to 'adjustOverflow'
        // Avoid infinite looping if we hit this case.
        if (this._calculatingDelta) return;


        if (this.grid._updatingRecordComponents) {
            return this.Super("adjustOverflow", arguments);
        }


        var grid = this.grid;


        if (grid == null) return this.Super("adjustOverflow", arguments);

        var data = grid.data, isLoading = false;;

        if (isc.isA.ResultSet(data) && !data.lengthIsKnown()) {
            if (grid.emptyMessageHeight == null) {
                return this.invokeSuper(isc.GridBody, "adjustOverflow", reason,a,b,c,d);
            }
            isLoading = true;
        }

        var initialWidth = this.getWidth(), initialHeight = this.getHeight();
        var fitVertical = (this.autoFitData == "both"),
            fitHorizontal = fitVertical,
            frozen = grid && grid.frozenFields != null,
            isFrozenBody = frozen && grid && (grid.frozenBody == this);

        if (!fitVertical) fitVertical = (this.autoFitData == "vertical");
        if (!fitHorizontal) fitHorizontal = (this.autoFitData == "horizontal");
        // If we have frozen fields, the frozen body never shows scrollbars and always
        // gets sized to match the widths of the fields it contains (done as part of
        // setBodyFieldWidths). Don't worry about trying to run special auto-fit logic
        // on the frozen body.
        // - We do run auto-fit logic on the unfrozen body and take the size of the frozen
        //   body into account when doing so.
        // - We do still need to ensure the header layout is sized correctly when the frozen
        //   body is resized

        if (fitHorizontal || fitVertical) {
            var height, width, rowHeights, hscrollOn, vscrollOn, dX, dY;

            if (fitVertical) {
                var minHeight = this.grid.getAutoFitMinBodyHeight();
                height = minHeight;

                var totalRows = isLoading ? 0 : this.getTotalRows(),
                    rows = totalRows;

                rowHeights = 0;
                // ignore autoFitMaxRecords if set to zero - this means fit to all records!
                if (this.autoFitMaxRecords) {
                    rows = Math.min(rows, this.autoFitMaxRecords);
                }
                if (rows > 0) {
                    // We need to handle variable rowHeights so we're going to have to look at
                    // the table element to determine the heights - we already have a method to
                    // do that
                    var drawnRowHeights = this._getDrawnRowHeights();
                    // If we have any undrawn rows assume calculated sizes

                    var firstDrawnRow = this._firstDrawnRow,
                        lastDrawnRow = this._lastDrawnRow;



                    // fdr / ldr unset implies no drawn rows - set such that we calculate
                    // theoretical heights only
                    if (this._firstDrawnRow == null) {
                        firstDrawnRow = rows;
                        lastDrawnRow = rows;
                    }
                    // _isFrozenBody defined in GridRenderer
                    var isFrozenBody = this._isFrozenBody();

                    if (firstDrawnRow > 0) {
                        firstDrawnRow = Math.min(firstDrawnRow, rows);
                        for (var i = 0; i < firstDrawnRow; i++) {
                            rowHeights += this.getRowHeight ?
                                            this.getRowHeight(this.grid.getRecord(i), i, isFrozenBody)
                                            : this.cellHeight;
                        }
                    }
                    var lastLogicalRow = rows-1;
                    if (lastDrawnRow < lastLogicalRow) {
                        for (var i = lastDrawnRow+1; i < lastLogicalRow+1; i++) {
                            rowHeights += this.getRowHeight ?
                                            this.getRowHeight(this.grid.getRecord(i), i, isFrozenBody)
                                            : this.cellHeight;
                        }
                    }
                    // Measure the rendered rows and add up the heights.
                    // Note that getDrawnRowHeights() just returns an array of the heights of
                    // rendered rows so the first drawn row is the first entry in the array, not
                    // the _firstDrawnRow'th entry
                    lastDrawnRow = Math.min(lastDrawnRow, lastLogicalRow);
                    for (var i = 0; i <= lastDrawnRow-firstDrawnRow; i++) {
                        rowHeights += drawnRowHeights[i];
                    }
                    // If we are clipping off any rows we know we have a v-scrollbar
                    vscrollOn = totalRows > rows;

                    // Treat autoFitMaxHeight:0 as unspecified - resize as large as necessary
                    var autoFitMaxHeight = this.getAutoFitMaxHeight();
                    if (autoFitMaxHeight && rowHeights > autoFitMaxHeight) {
                        rowHeights = autoFitMaxHeight;
                        vscrollOn = true;
                    }
//                     this.logWarn("total rows to show:"+ rows +
//                      ", rendered:" + [this._firstDrawnRow,this._lastDrawnRow] +
//                      ", rowHeights total up to:"+ rowHeights +
//                      ", current height:" + this.getHeight() +
//                      ", body height based on ListGrid specified height:" + height);

                } else {
                    // The emptyMessage renders in the available space. If emptyMessageHeight
                    // is explicitly set, leave that much space for it.

                    if (this.grid.emptyMessageHeight != null) {
                        rowHeights = this.grid.emptyMessageHeight;
                    }
                }


                // add some extra height if autoFitExtraRecords is set

                if (this.autoFitExtraRecords && this.autoFitExtraRecords > 0) {
                    var extraHeight = Math.round(this.autoFitExtraRecords * this.cellHeight);
                    rowHeights += extraHeight;
                }

            } else {
                vscrollOn = this.getScrollHeight() > this.getHeight();
            }

            if (fitHorizontal && !isFrozenBody) {
                var width = this.grid.getInnerWidth(),
                    frozenBodyWidth;

                if (frozen) {
                    var frozenWidths = this.grid.getFrozenSlots(this.grid._fieldWidths);
                    frozenBodyWidth = frozenWidths.sum();
                    width -= frozenBodyWidth;

                    // if the frozenWidths exceed the specified width for the grid as a whole,
                    // apply an arbitrary small positive min width for the unfrozen body
                }


                // Note that we're calling getColumnSizes on the GridRenderer
                // So if we the LG is frozen body this gives us the cols within the
                // appropriate body, not the total set of cols in the grid.
                var colSizes = this.getColumnSizes(),
                    contentWidth = colSizes.sum();
                if (this.autoFitMaxColumns) {
                    var maxCols = this.autoFitMaxColumns;
                    // bit of a hack - how to deal with maxCols specified as a number <= the
                    // number of frozen fields.
                    // For now we just enforce at least one unfrozen field
                    if (frozen) {
                        maxCols = Math.max(1, maxCols-this.grid.frozenFields.length);
                    }

                    if (maxCols < colSizes.length) {
                        colSizes = colSizes.slice(0, maxCols);
                    }
                }

                var colWidths = colSizes.sum();
                if (this.autoFitMaxWidth) {
                    var maxWidth = this.autoFitMaxWidth;
                    if (frozen) maxWidth = Math.max(20, maxWidth - frozenBodyWidth);
                    colWidths = Math.min(maxWidth, colWidths);
                }
                hscrollOn = (this.overflow == isc.Canvas.SCROLL) ? true :
                            (this.overflow == isc.Canvas.AUTO) ? (contentWidth > Math.max(width, colWidths)) :
                            false;

            } else {
                hscrollOn = this.overflow == isc.Canvas.SCROLL ? true :
                            this.overflow == isc.Canvas.AUTO  ? this.getScrollWidth() > this.getWidth() :
                            false;
            }
            // Now we know if we have an h-scrollbar, adjust height and width for scrollbars /
            // borders / margin if appropriate
            if (fitVertical && rowHeights != null) {
                rowHeights += this.getVBorderPad() + this.getVMarginSize();
                if (hscrollOn) {
                    rowHeights += this.getScrollbarSize();
                    var autoFitMaxHeight = this.getAutoFitMaxHeight()
                    if (autoFitMaxHeight && rowHeights > autoFitMaxHeight) {
                        rowHeights = autoFitMaxHeight;
                    }
                }
                // Resize vertically if rowHeights (+ border etc) > the auto fit min height
                // (which is derived from the ListGrid's specified height)
                if (rowHeights > height) {
                    height = rowHeights;
                    this._vAutoFit = true;
                } else {
                    if (this._vAutoFit) delete this._vAutoFit;
                }
            }
            if (fitHorizontal && !isFrozenBody && colWidths != null) {

                colWidths += this.getHBorderPad() + this.getHMarginSize();
                if (vscrollOn || this.alwaysShowVScrollbar) {
                    colWidths += this.getScrollbarSize();
                    if (this.autoFitMaxWidth) {
                        var maxWidth = this.autoFitMaxWidth;
                        if (frozen) maxWidth = Math.max(20, maxWidth - frozenBodyWidth);
                        colWidths = Math.min(maxWidth, colWidths);
                    }
                }
                // Resize horizontally if colWidths > width
                if (colWidths > width) {
                    width = colWidths;
                    this._hAutoFit = true;
                } else {
                    if (this._hAutoFit) delete this._hAutoFit;
                }
            }

            // Calculate the delta with our current size.
            this._calculatingDelta = true;
            dY = this.getDelta(this._$height, height, this.getHeight());

            dX = this.getDelta(this._$width, width, this.getWidth());
            delete this._calculatingDelta;
            // If necessary resize to accommodate content!
            if (dY != null || dX != null) {
                this.resizeBy(dX, dY, null, null, true);
            }

            // if width change != null, resize header to match body
            // Note that if isFrozenBody is true we skipped the dX calculation so
            // always resize the headerLayout to match

            if (dX != null || (isFrozenBody && fitHorizontal)) {
                var lg = this.grid,
                    scrollbarSize = lg._shouldLeaveScrollbarGap() ? lg.body.getScrollbarSize() : 0,
                    headerWidth = width - scrollbarSize,
                    totalHeaderWidth = headerWidth;
                if (frozen && lg.headerLayout) {

                    if (isFrozenBody) {
                        totalHeaderWidth = this.getWidth() + lg.body.getWidth();
                        // If we go past the autoFitMaxWidth limit, run adjustOverflow on the body
                        // to force it to shrink/start scrolling
                        if (lg.autoFitMaxWidth != null &&
                            (totalHeaderWidth + lg.getHBorderPad() +
                                lg.getHMarginSize() > lg.autoFitMaxWidth))
                        {
                            // don't bother to go on and resize the header - we'll do that
                            // when the body adjust overflow method runs
                            return lg.body.adjustOverflow();
                        }
                        totalHeaderWidth -= scrollbarSize;

                    } else {
                        totalHeaderWidth = headerWidth + lg.frozenBody.getWidth();
                    }
                    lg.headerLayout.setWidth(totalHeaderWidth);
                }

                // We can skip resizing the frozen header - this is handled in setBodyFieldWidths
                if (!isFrozenBody) {
                    var header = lg.header;

                    if (header && header.isDrawn()) {
                        header.setWidth(headerWidth);
                    }
                }
            }

        // if autoFitData is null but we don't match our 'specified size', assume the property
        // has been modified and reset to specified size
        }

        // catch the case where autoFitData has been cleared in either direction and
        // reset to specified size.
        var verticalChanged = (!fitVertical && this._vAutoFit),
            horizontalChanged = (!fitHorizontal && this._hAutoFit);
        if (verticalChanged || horizontalChanged) {
            delete this._vAutoFit;
            delete this._hAutoFit;

            var standardHeight = verticalChanged ? this.grid.getAutoFitMinBodyHeight() : null,
                standardWidth = horizontalChanged ?
                                (!frozen ? this.grid.getInnerWidth() :
                                    (this.grid.getInnerWidth() - this.grid.frozenBody.getWidth()) )
                                                  : null;

            this.resizeTo(standardWidth,standardHeight);
            // reset userHeight / userWidth to 100%, so future resizes to the LG cause the
            // body to also resize.
            if (!fitVertical) this._userHeight = "100%";
            if (!fitHorizontal) this._userWidth = "100%";
            // reset field widths on the grid to resize the header to match the body
            this.grid._updateFieldWidths("autoFitData mode changed");

            // reset field widths on the grid to resize the header to match the body
            this.grid._updateFieldWidths("autoFitData mode changed");
        }
        var returnVal = this.invokeSuper(isc.GridBody, "adjustOverflow", reason, a,b,c,d);
        // if size changed, refresh recordComponents to account for new draw area
        if ((fitVertical || fitHorizontal) &&
            (this.getWidth() != initialWidth || this.getHeight() != initialHeight))
        {
            this.grid.updateRecordComponents(true);
        }

        // Fire the "bodyOverflowed" observation. This updates frozen body end space and
        // summary row body right space to so these can keep in sync with body scrolling
        // even though the viewport sizes are different.
        if (!isFrozenBody) this.grid.bodyOverflowed();
        return returnVal;
    },

    getAutoFitMaxHeight : function () {
        return this.grid ? this.grid.getAutoFitMaxBodyHeight() : null;
    },

    // When determining auto-fit-field-widths (drawn size of columns) we render out an
    // offscreen tester containing tableHTML and look at the various cells' widths in a row.
    // If the grid is grouped, we need to choose a row which isn't the group header
    // since that contains col-spanning cells
    _getValidAutoFitRowNum : function () {
        var grid = this.grid;
        if (this.grid && this.grid.isGrouped) {
            var rowNum = 0;
            while (rowNum < this.grid.getTotalRows()) {
                var record = this.grid.getRecord(rowNum);
                if (record == null || !this.grid.isGroupNode(record)) {
                    return rowNum;
                }
                rowNum++;
            }
        }
        return 0;
    },


    resizeBy : function (deltaX, deltaY, animating, suppressHandleUpdate, autoFitSize) {

        // autoFitSize parameter: When autoFitData is true for this grid, we resize the
        // body to fit the data, and pass in the autoFitSize parameter to this method.
        // In the case of an explicit resize outside the autoFitData system, hang onto the
        // specified size so we can reset to it if the data shrinks, etc
        if (!autoFitSize) {
            this._specifiedWidth = this.getWidth() + (deltaX != null ? deltaX : 0);
        }

        // Note that return value of resizeBy indicates whether the size actually changed
        var returnVal = this.invokeSuper(isc.GridBody, "resizeBy",
                                deltaX, deltaY, animating, suppressHandleUpdate, autoFitSize);
        // we usually update _userWidth/_userHeight as part of layout.childResized to
        // store the explicit width, which then stops the member reacting to the layout's
        // subsequent resizes.
        // However, if we're autoFitting the (unfrozen) body to content, we want a
        // subsequent resize of the grid as a whole to still cause the body to expand
        // further.
        // Therefore yank out this _userSize flag in this case.
        if (autoFitSize && deltaX != null && !this.frozen) {
            delete this._userWidth;
        }
        return returnVal;
    },

    // context menus (NOTE: ListGrid-level handling is identical for cell vs row context click)
    cellContextClick : function (record, rowNum, colNum) {
        var gridColNum = this.grid.getFieldNumFromLocal(colNum, this);
        return this.grid._cellContextClick(record, rowNum, gridColNum);
    },

    // Override _rowClick: If a record is marked as disabled this suppresses all events, but
    // if the user clicks in the "remove" field of an already removed record we actually want
    // to react to this and unmarkAsRemoved()
    _rowClick : function (rowNum, colNum) {
        if (!this.grid) return;
        var returnVal;
        var gridColNum = this.grid.getFieldNumFromLocal(colNum, this);

        var rec = rowNum >= 0 ? this.grid.getRecord(rowNum) : null,
            field = gridColNum >= 0 ? this.grid.getField(gridColNum) : null,
            isRemoveClick = false
        ;
        if (!isc.isA.RecordEditor(this.grid) && field && field.isRemoveField) {
            // If the user clicks inside the remove field on a group or summary row, don't fire
            // the remove-click!
            isRemoveClick = !rec || !(rec._isGroup || rec.isGroupSummary || rec.isGridSummary);
        }

        if (isRemoveClick) {
            if (rowNum >= 0) {
                this.grid.removeRecordClick(rowNum,colNum);
                returnVal = false;
            }
        } else {
            returnVal = this.Super("_rowClick", arguments);
        }
        return returnVal;
    },

    getCellHoverDelay : function (rowNum, colNum) {
        if (!this.grid) return;
        var gridColNum = this.grid.getFieldNumFromLocal(colNum, this);
        return this.grid.getCellHoverDelay(this.grid.getCellRecord(rowNum,colNum), rowNum, colNum);
    },


    _getCellHoverComponent : function (record, rowNum, colNum) {
        if (this.grid && isc.isA.ListGrid(this.grid)) {
            var gridColNum = this.grid.getFieldNumFromLocal(colNum, this);
            return this.grid._getCellHoverComponent(record, rowNum, gridColNum);
        }
    },


    // this ensures that if we're not showing any records we can still scroll the header fields
    // into view.
    expandEmptyMessageToMatchFields:true,
    applyHSpaceToEmptyMessage:true,

    getInnerHTML : function () {
        // call bodyDrawing on the LG if we are the primary body
        this.grid.bodyDrawing(this);
        return this.Super("getInnerHTML", arguments);
    },

    // Override _canFocus to check for whether we have any data.
    _canFocus : function () {
        var canFocus = this.Super("_canFocus", arguments);
        if (canFocus && this.grid && !this.grid.canFocusInEmptyGrid && this.isEmpty()) {
            return false;
        }
        return canFocus;
    },

    // ------------------------------------------------------
    //
    // PrintHTML
    // This needs some tweaking to handle the following:
    // - printHTML can be generated asynchronously in 2 ways:
    //  - if number of rows exceeds printMaxRows we use timers to break up the HTML generation
    //  - if we have embeddedComponents fetching their printHTML may also be asynchronous
    //
    // In either case, 'getTableHTML()' will be fired more than once, asynchronously.
    // In the case of async embedded component printHTML generation, this is the standard
    // mechanism - see 'gotComponentPrintHTML' in GridRenderer.
    // In the case of splitting the printing into chunks, the _printingChunk
    // flag will be set and startRow/endRow will be shifted, then getTableHTML will be called
    // on a timer, repeatedly until all the rows' HTML is generated.
    //
    // We need to fire the 'prepareBodyForPrinting' and 'bodyDonePrinting' methods on the ListGrid
    // around each of these blocks - this is required as the ListGrid relies on the body to
    // handle generating header HTML and if there are frozen fields, HTML from the frozen
    // body, and does so by setting various flags on the GR body which'll be read by
    // getTableHTML()
    //


    getTablePrintHTML : function (context) {
        // context contains startRow, endRow, callback, printProperties, printWidths
        var startRow = context.startRow,
            endRow = context.endRow,
            totalRows = endRow != null ? (endRow - startRow) : this.getTotalRows(),
            maxRows = this.printMaxRows,
            printWidths = context.printWidths,
            printProps = context.printProps;

        var asyncPrintCallback = {
            target:this,
            methodName:"gotTablePrintHTML",
            printContext:context,
            printCallback:context.callback
        }

        context.callback = asyncPrintCallback;

        if (maxRows < totalRows) {
            this.logDebug("get table print html - breaking HTML into chunks", "printing");
            if (startRow == null) startRow = context.startRow = 0;
            if (endRow == null) endRow = context.endRow = this.getTotalRows();
            this.getPrintHTMLChunk(context);

            return null;
        }

        // No chunks - can only be asynchronous due to getTableHTML directly going asynch
        // to get embeddedComponentHTML
        var suspendPrintingContext = this.grid._prepareBodyForPrinting(printWidths, printProps);
        var printHTML = this.getTableHTML(null, startRow, endRow, null, asyncPrintCallback);

        // restore settings
        this.grid._bodyDonePrinting(suspendPrintingContext);
        return printHTML;
    },

    gotTablePrintHTML : function (HTML, asyncCallback) {
        var callback = asyncCallback.printCallback;
        if (callback) {
            this.fireCallback(callback, "HTML,callback", [HTML,callback]);
        }
    },

    // This is called repeatedly, asynchronously for each "chunk"
    // The first chunk may include fetches for component tableHTML so can also be asynchonous
    // itself.

    getPrintHTMLChunk : function (context, returnSynchronous) {

        var suspendPrintingContext = this.grid._prepareBodyForPrinting(context.printWidths);
        // printing chunk flag - used by the GR to avoid writing out the outer table tags for each
        // chunk.
        this._printingChunk = true;

        // Second flag to indicate we are printing chunks. This is used only by
        // gotComponentPrintHTML() to reset the _printingChunk flag before calling
        // getTableHTML
        this._gettingPrintChunkHTML = true;

        var startRow = context.startRow,
            endRow = context.endRow,
            maxRows = this.printMaxRows,
            callback = context.callback;

        this.currentPrintProperties = context.printProps;

        if (!context.html) context.html = [];

        var chunkEndRow = context.chunkEndRow = Math.min(endRow, (startRow + maxRows)),
            chunkHTML = this.getTableHTML(null, startRow, chunkEndRow, null,
                {target:this, methodName:"gotPrintChunkHTML",
                    printContext:context, printCallback:context.callback
                });

        // restore settings
        this.grid._bodyDonePrinting(suspendPrintingContext);
        this._printingChunk = false;

        // chunkHTML will only be null if getTableHTML went asynchronous - can happen on the
        // first chunk while retrieving embedded componentHTML
        if (chunkHTML != null) {
            delete this._gettingPrintChunkHTML;
            this.gotPrintChunkHTML(chunkHTML, {printContext:context});
            if (returnSynchronous) {
                return chunkHTML;
            }
        }
    },
    gotPrintChunkHTML : function (HTML, callback) {
        var context = callback.printContext,
            startRow = context.startRow,
            endRow = context.endRow,
            chunkEndRow = context.chunkEndRow,
            maxRows = this.printMaxRows,
            gotHTMLCallback = context.callback;

        context.html.add(HTML);

        if (chunkEndRow < endRow) {
            context.startRow = chunkEndRow;
            return this.delayCall("getPrintHTMLChunk", [context], 0);
        }

        if (gotHTMLCallback != null) {
            var html = context.html.join(isc.emptyString);
            this.fireCallback(gotHTMLCallback, "HTML,callback", [html,gotHTMLCallback]);
        }
    },

    // In GridRenderer.getTableHTML(), when printing, we generate all embedded components'
    // print HTML up front, then slot it into the actual HTML for the table.
    // component printHTML may be asynchronously generated in which case this callback is
    // fired when we have the component HTML - default implementation re-runs getTableHTML
    // which now recognizes it's got component HTML and continues to get the actual table
    // HTML then fire the async callback.
    // Overridden to call 'prepareBodyForPrinting()' on the grid and reset the '_printingChunk'
    // flag if necessary
    gotComponentPrintHTML : function (HTML, callback) {

        var asyncCallback = callback.context.asyncCallback,
            context = asyncCallback.printContext;

        var printWidths = context.printWidths;

        var suspendPrintingContext = this.grid._prepareBodyForPrinting(printWidths);
        if (this._gettingPrintChunkHTML) {
            this._printingChunk = true;
        }

        var HTML = this.Super("gotComponentPrintHTML", arguments);
        if (this._printingChunk) delete this._printingChunk;

        if (HTML != null) {
            delete this._gettingPrintChunkHTML;
        } else {
            this.grid._bodyDonePrinting(suspendPrintingContext);
        }

    },

    // override getPrintHeaders / getPrintFooters to return the
    // already calculated HTML set up by the calling grid.

    getPrintHeaders : function (startCol, endCol) {
        var HTML = this._printHeadersHTML;
        // Lazily clean up this attribute.

        delete this._printHeadersHTML;
        return HTML == null ? "" : HTML;
    },

    getPrintFooters : function (startCol, endCol) {
        var HTML = this._printFootersHTML;
        delete this._printFootersHTML;
        return HTML == null ? "" : HTML;
    },

    // Row Spanning Cells
    // ----------------------------




    refreshCellValue : function (rowNum, colNum) {
        var lg = this.grid;
        if (rowNum >= 0 && colNum >= 0 && lg && lg.allowRowSpanning && lg.useRowSpanStyling)
        {
            // If we're asked to refresh a logical cell that's not rendered
            // (actually spanned by another cell) we could either refuse or refresh the
            // spaning cell. Refresh the spanning cell in case its value is calculated
            // from the cell in question.
            var startRow = this.getCellStartRow(rowNum, colNum);
            if (startRow != rowNum) {
                rowNum = startRow;
            }
        }
        isc.GridRenderer._instancePrototype.refreshCellValue.call(this, rowNum, colNum);
    },


    // Cell Alignment
    // ---------------------------------------------------------------------------------------

    // cellAlignment - override to account for the fact that with frozen fields, body
    // colNum may be offset from ListGrid colNum
    getCellVAlign : function (record, field, rowNum, colNum) {
        if (this.grid && this.grid.getCellVAlign) {
            var gridColNum = this.grid.getFieldNumFromLocal(colNum, this);
            return this.grid.getCellVAlign(record, rowNum, gridColNum);
        }
    },
    getCellAlign : function (record, field, rowNum, colNum) {

        if (this.grid && this.grid.getCellAlign != null) {
            var gridColNum = this.grid.getFieldNumFromLocal(colNum, this);
            return this.grid.getCellAlign(record, rowNum, gridColNum);

        } else return field.cellAlign || field.align;
    },

    // Single Cell rows
    // ---------------------------------------------------------------------------------------

    // if this is removed, DONTCOMBINE directive no longer needed in GridRenderer.js
    _drawRecordAsSingleCell : function (rowNum, record,c) {
        var lg = this.grid;
        if (lg.showNewRecordRow && lg._isNewRecordRow(rowNum)) return true;

        return isc.GridRenderer._instancePrototype.
            _drawRecordAsSingleCell.call(this, rowNum,record,c);
        //return this.Super("_drawRecordAsSingleCell", arguments);
    },

    // showSingleCellCheckboxField()
    // If this record is showing a single cell value, should a checkbox field also show up next
    // to the record?
    showSingleCellCheckboxField : function (record) {
        var lg = this.grid;
        return lg && lg.showSingleCellCheckboxField(record);
    },

    // This method is called on records where _drawRecordAsSingleCell is true
    // returns the start/end col the single cell value should span.
    // Typically just spans all the cells we render out but if we're showing the
    // checkbox field we may want to NOT span over that field
    _getSingleCellSpan : function (record, rowNum, startCol, endCol) {
        // Span all columns if we're not showing a checkbox field

        if (rowNum == this._animatedShowStartRow ||
            !this.showSingleCellCheckboxField(record) ||
            (this.grid && this.grid.frozenBody != null && this.grid.frozenBody != this))
        {
            return [startCol,endCol];
        }


        return [Math.max(startCol, 1), endCol];
    },

    // Scrolling / Scroll Sync
    // ---------------------------------------------------------------------------------------

    // Have v-scrolling occur on the frozen body on mouseWheel
    // This essentially duplicates the mouseWheel handler at the Canvas level for
    // widgets with visible scrollbars.
    mouseWheel : function () {
        if (this.frozen && this.grid != null) {
            var wheelDelta = this.ns.EH.lastEvent.wheelDelta;
            var scrollTo = this.scrollTop + Math.round(wheelDelta * isc.Canvas.scrollWheelDelta);
            // Scroll the main body (we'll scroll in response to that) rather than
            // scrolling the frozen body directly.
            this.grid.body.scrollTo(null, scrollTo, "frozenMouseWheel");
            return false;
        }
        return this.Super("mouseWheel", arguments);
    },

    // Override _getDrawRows()
    // Have the frozen body rely on the unfrozen body to handle drawAhead / quickDrawAhead
    // etc and keep set of drawn rows in synch

    _getDrawRows : function () {
        if (this.frozen && this.grid) {
            var grid = this.grid;
            return grid.body._getDrawRows();
        }
        return this.Super("_getDrawRows", arguments);
    },

    // doneFastScrolling: ensure *both* bodies redraw without draw-ahead direction
    doneFastScrolling : function () {
        // we only expect to see this fire on the unfrozen body - the frozen body doesn't
        // show a scrollbar so won't get the thumb drag which initializes this method
        if (!this.frozen && this.grid != null && this.grid.frozenBody != null) {

            var redrawFrozenBody = this._appliedQuickDrawAhead;
            this.Super("doneFastScrolling", arguments);
            if (redrawFrozenBody) {
                this.grid.frozenBody._suppressDrawAheadDirection = true;
                this.grid.frozenBody.markForRedraw("Done fast scrolling on unfrozen body");
            }
        }
    },

    // observe the scroll routine of the body so we can sync up
    scrollTo : function (left, top, reason, animating) {
        if (isc._traceMarkers) arguments.__this = this;
        // Clamp the positions passed in to the edges of the viewport
        // (avoids the header from getting out of synch with the body.)

        if (left != null) {

            var maxScrollLeft = this.getScrollWidth() - this.getViewportWidth();
            left = Math.max(0, Math.min(maxScrollLeft, left));
        }
        if (top != null) {

            var maxScrollTop = this.getScrollHeight() - this.getViewportHeight();
            top = Math.max(0, Math.min(maxScrollTop, top));
        }
        var lg = this.grid;


        this.invokeSuper(isc.GridBody, "scrollTo", left,top,reason,animating);

        //this.logWarn("body.scrollTo: " + this.getStackTrace());
        // dontReport when we're being called in response to bodyScrolled
        // observation!
        var dontReport = this._noScrollObservation;
        if (!dontReport) lg.bodyScrolled(left, top, this.frozen);

        // If the body scrolled without forcing a redraw, ensure any visible edit form
        // items are notified that they have moved.

        if (!this.isDirty() && lg._editorShowing) {
            lg._editRowForm.itemsMoved();
        }

    },

    // Embedded Components
    // ---------------------------------------------------------------------------------------

    // embedded components can be per row or per cell.
    // When per-cell the GR APIs act by colNum only, not by field name.
    // However for us to handle field reorder, show/hide, etc it's useful to hang fieldName
    // onto the embeddedComponents as well
    addEmbeddedComponent : function (component, record, rowNum, colNum, position) {
        var comp = this.invokeSuper(isc.GridBody, "addEmbeddedComponent", component, record,
                                    rowNum, colNum, position);
        if (component._currentColNum != null && component._currentColNum != -1 && this.grid) {
            var grid = this.grid,
                colNum = component._currentColNum,
                gridColNum = grid.getFieldNumFromLocal(colNum, this),
                fieldName = grid.getFieldName(gridColNum);

            component._currentFieldName = fieldName;

            // set up a map of embedded components per column (fieldName)
            // This will make lookup quicker.
            if (grid._columnComponentsMap == null) {
                grid._columnComponentsMap = {};
            }
            if (grid._columnComponentsMap[fieldName] == null) {
                grid._columnComponentsMap[fieldName] = {};
            }
            grid._columnComponentsMap[fieldName][component.getID()] = true;
            // skip "%" sized components since they should never expand a field.
            if (component._percent_width == null) {
                grid._fieldComponentWidthsChanged(fieldName);
            }
        }

        return component;
    },
    removeEmbeddedComponent : function (record, component, suppressRedraw) {
        var grid = this.grid;
        if (grid) {
            var fieldName = component._currentFieldName;
            if (fieldName != null) {
                if (grid._columnComponentsMap && grid._columnComponentsMap[fieldName]) {
                    delete grid._columnComponentsMap[fieldName][component.getID()];
                }
                if (component._percent_width == null) {
                    grid._fieldComponentWidthsChanged(fieldName);
                }
            }

            //component._currentFieldName = null;
        }
        this.invokeSuper(isc.GridBody, "removeEmbeddedComponent", record, component, suppressRedraw);
    },


    // Override _getExtraEmbeddedComponentHeight() / upateHeightForEmbeddedComponents to
    // respect listGrid.recordComponentHeight if specified, even if there are no
    // embedded components for this record.

    updateHeightForEmbeddedComponents : function (record, rowNum, height) {

        if (record && !this.grid._hasEmbeddedComponents(record) && this.grid.showRecordComponents
            && this.grid.recordComponentHeight != null)
        {
            // Reimplementing the superClass version, except that this logic is running even
            // when there are no embeddedComponents on the row.
            var details = this._getExtraEmbeddedComponentHeight(record, rowNum);
            if (details.allWithin && details.extraHeight > 0) {
                height = Math.max(height,details.extraHeight);
                //this.logWarn("in updateHeightForEmbeddedComponents ("+this.grid+"): details are "+isc.echoAll(details)+"\nheight is "+height);
            } else {
                height += details.extraHeight;
                //this.logWarn("in updateHeightForEmbeddedComponents ("+this.grid+"): details are "+isc.echoAll(details)+"\nheight is "+height);
            }

            return height;
        }

        return this.invokeSuper(isc.GridBody, "updateHeightForEmbeddedComponents", record, rowNum, height);
    },

    _getExtraEmbeddedComponentHeight : function (record, rowNum) {
        var heightConfig = this.invokeSuper(isc.GridBody, "_getExtraEmbeddedComponentHeight",
                                        record, rowNum);
        if (this.grid.showRecordComponents && this.grid.recordComponentHeight != null) {
            heightConfig.extraHeight = Math.max(heightConfig.extraHeight,
                                            this.grid.recordComponentHeight);
        }
        return heightConfig;
    },
    _writeEmbeddedComponentSpacer : function (record) {
        if (record && this.grid && this.grid.showRecordComponents
            && this.grid.recordComponentHeight != null)
        {
            return true;
        }
        return this.invokeSuper(isc.GridBody, "_writeEmbeddedComponentSpacer", record);
    },

    _placeEmbeddedComponents : function () {

        if (this.grid && this.grid._autoFittingFields) {
            return;
        }
        return this.Super("_placeEmbeddedComponents", arguments);
    },

    getAvgRowHeight : function () {
        if (this.grid) return this.grid.getAvgRowHeight(this);
        return this.Super("getAvgRowHeight", arguments);
    },

    // override shouldShowAllColumns() - we can avoid showing all columns if row height
    // is variable *only* because of an expansion component expanding the entire row since
    // the heights won't vary per-cell.
    shouldShowAllColumns : function () {
        if (this.showAllColumns) {
            return true;
        }
        if (!this.fixedRowHeights && !this.showAllRows) {
            if (this.grid.canExpandRecords && this.grid._specifiedFixedRecordHeights) {
                return false;
            }
            return true;
        }
        if (this.overflow == isc.Canvas.VISIBLE) {
            return true;
        }
        return false;

    },

    // Editing
    // ---------------------------------------------------------------------------------------

    //> @method listGrid.markForRedraw()
    // @include canvas.markForRedraw
    // @visibility external
    //<

    // Redraw overridden:
    // - Update the editRow form items (we don't create more items than we need when
    //   rendering incrementally)
    // - Update the values in the edit row form.
    redraw : function (reason,b,c,d) {

        // since we're doing a full redraw, cancel pending refreshCell() calls
        if (this._pendingCellRefreshTimer) this.cancelPendingCellRefresh();

        // flag to note we're redrawing - this is used by getDrawnFields()
        this._redrawing = true;

        // If alwaysShowEditors is marked as true, but editorShowing is false it implies our
        // attempt to start editing on draw() failed - presumably there were no
        // editable cells in view.
        // See if we can start editing now in this case
        var lg = this.grid;
        if (lg.alwaysShowEditors && !lg._editorShowing) {
            // pull stashed value for target edit cell if provided
            // by scrollCellIntoView
            var rowNum, colNum;
            if (lg._editCellAfterRedraw) {
                rowNum = lg._editCellAfterRedraw[0];
                colNum = lg._editCellAfterRedraw[1];
                delete lg._editCellAfterRedraw;
            }
            lg.startEditing(rowNum,colNum,true,null,true);
        }


        var editForm = lg._editRowForm,
            editing = lg._editorShowing,
            editColNum, editRowNum, editRecord,
            completeWidths,
            fieldsToRemove;

        // If the grid is showing inactive Editor HTML for any cells, we'll clear it
        // (and potentially regenerate it) as part of redraw(). Notify the grid so it can clear
        // up inactive contexts stored on the edit form items

        lg._clearingInactiveEditorHTML();

        // if body redraw came from data change, folder opening, or resizing of content,
        // it's likely to introduce a v-scrollbar.
        // If leaveScrollbarGap is false, call '_updateFieldWidths()' before the redraw occurs so
        // we leave a gap for the v-scrollbar, rather than redrawing with both V and H scrollbar,
        // then resizing the fields and redrawing without an H-scrollbar.
        if (!lg.leaveScrollbarGap && lg.predictScrollbarGap && (this.overflow == isc.Canvas.AUTO)) {
            var vScrollOn = this.vscrollOn,

                vScrollWillBeOn = !lg.isEmpty() &&
                                  (lg.getTotalRows() * lg.cellHeight)  > this.getInnerHeight();

            if (vScrollOn != vScrollWillBeOn) {
                // ensure we don't try to recalculate field widths a second time by clearing
                // the _fieldWidthsDirty flag
                delete this._fieldWidthsDirty;
                lg._updateFieldWidths("body redrawing with changed vertical scroll-state");

            }
        }

        var suppressRowElementFocus = false;

        if (editing) {

            this.logInfo("redraw with editors showing, editForm.hasFocus: " +
                         editForm.hasFocus, "gridEdit");
            editColNum = lg.getEditCol();

            // See comments near _storeFocusForRedraw() for how edit form item focus is handled
            // on redraw
            this._storeFocusForRedraw();

            // This will add the new edit items corresponding to the newly displayed fields
            // and return the items that need to be removed (after the body is actually redrawn,
            // which will hide them)
            // It also fires the "redrawing" notification on any items that are about to be redrawn
            fieldsToRemove = this._updateEditItems();
        } else if (isc.screenReader) {
            var focusCanvas = this.ns.EH.getFocusCanvas();
            // Suppress focusing the row element if some other widget is focused.
            suppressRowElementFocus = focusCanvas != null && focusCanvas !== this;

        }

        // refresh field widths if necessary
        if (this._fieldWidthsDirty != null) {

            var fwReason = this._fieldWidthsDirty;
            delete this._fieldWidthsDirty;
            lg._updateFieldWidths(fwReason);
        }
        // store the new drawArea
        var newDrawArea = this.getDrawArea();


        var grid = this.grid,
            drawArea = this._oldDrawArea;

        if (!drawArea) drawArea = this._oldDrawArea = [0,0,0,0];

        var grid = this.grid,
            firstRecord = grid._getCachedCellRecord(newDrawArea[0]),
            lastRecord = grid._getCachedCellRecord(newDrawArea[1]),
            dataPresent = (firstRecord != null && lastRecord != null);

        if (dataPresent && !drawArea.equals(newDrawArea)) {
            // the old and new drawAreas differ and the extents of the new data are present -
            // fire the notification method and update the stored _oldDrawArea

            if (!this.frozen) {
                grid._drawAreaChanged(drawArea[0], drawArea[1], drawArea[2], drawArea[3], this);
                this._oldDrawArea = newDrawArea;
            }
        }


        delete this._drawnEditItems;

        this.invokeSuper(isc.GridBody, "redraw", reason,b,c,d);

        // clear the "redrawing" flag since the HTML is now up to date
        delete this._redrawing;

        // Always update all recordComponents on redraw().
        // don't rely on the draw area changing since we may be showing the same set of
        // rowNum/colNums but having underlying data or field meaning changes.
        // Note: updateRecordComponents() updates components in frozen and unfrozen bodies.
        // If this is a redraw of the frozen body, don't call updateRecordComponents() if
        // the redraw was tripped by scrolling or data change as in this case we'll also
        // get a redraw of the unfrozen body which can handle updating the RC's.
        // (DO still call the method in other cases as it may imply the fields in the frozen
        // body have changed, etc).
        // NOTE 2014: do this *after* calling Super() so that the drawn rows match the
        // data
        if (!this.frozen && !this._suppressRecordComponentsUpdate) {
            grid.updateRecordComponents();
        }
        this._suppressRecordComponentsUpdate = false;

        if (editing) {
            // Remove the items that correspond to fields that are no longer rendered in the DOM
            if (fieldsToRemove != null && fieldsToRemove.length > 0) {
                editForm.removeItems(fieldsToRemove);
            }

            // Fire the method to notify form items that they have been drawn() / redrawn()
            // or cleared()

            lg._editItemsDrawingNotification(null, true, this);

            /*
            var itemColArray = [],
                items = lg._editRowForm.getItems();
            for (var i =0; i < items.length; i++) {
                itemColArray.add(items[i].colNum + " - " + items[i].name);
            }
            this.logWarn("After redraw - edit form covers these cols:" + itemColArray);
            */


            lg.updateEditRow(lg.getEditRow());

            // If the editRowForm is currently marked as having focus, or we took focus from it
            // before a redraw and no other widget has subsequently picked up focus, restore focus
            // to it
            // This catches both a simple redraw and the case where the user scrolls the edit item
            // out of view, it is cleared due to incremental rendering, then scrolls back into view
            // and it gets redrawn

            if (editForm.hasFocus ||
                (this._editorSelection && isc.EH.getFocusCanvas() == null))
            {
                this._restoreFocusAfterRedraw(editColNum);
            } else {
                delete this._editorSelection;
            }

        } else {
            if (editForm != null) {
            // notify the form that it's items have been cleared() (will no-op if they're
            // not currently drawn)
            lg._editItemsDrawingNotification(null, null, this);


            }

            // _nativeFocusRow was remembered last time putNativeFocusInRow was called

            if (isc.screenReader) {
                this._putNativeFocusInRow(this.getNativeFocusRow(), suppressRowElementFocus);
            }
        }

    },

    redrawOnScroll : function (immediate) {
        if (this.frozen) this._suppressRecordComponentsUpdate = true;
        return this.Super("redrawOnScroll", arguments);
    },


    // force redraw on setDisabled() if we're showing an edit form to ensure we
    // redraw the items in enabled/disabled state
    setHandleDisabled : function (disabled) {
        var lg = this.grid;
        if (this.isDrawn() && lg && lg._editorShowing) {
            this.markForRedraw("Grid body disabled while editing");
        }
        return this.Super("setHandleDisabled", arguments);
    },

    // Add edit items corresponding to newly displayed fields (displayed due to incremental
    // rendering)
    // If any fields are to be hidden, do not remove these here, but return them in an array so
    // they can be removed from the form after the redraw completes
    // Note that the order of items in the form will not match the order of fields necessarily -
    // acceptable since developers will be interacting with the items' colNum attribute rather than
    // index in the edit form fields array.

    _updateEditItems : function () {

        // We keep the set of items in the editForm in synch with the set of
        // visible columns for performance.
        // Determine which items need to be created or removed here.
        var lg = this.grid, editForm = lg.getEditForm(),
            fieldsToRemove = [],
            editItems = editForm.getItems();
        if (!lg.editByCell) {

            // set up the vars used in creating form items
            var editRowNum = lg.getEditRow(),
                editRecord = lg.getRecord(editRowNum),

                completeWidths = lg.getEditFormItemFieldWidths(editRecord);

            // Determine what fields are rendered into the body
            // If we have frozen columns, we will always be showing them, in addition to whatever
            // fields are visible
            var editItems = editForm.getItems(),
                itemNames = editItems.getProperty(this.fieldIdProperty),
                fields = lg.getDrawnFields(),
                fieldNames = fields.getProperty(this.fieldIdProperty);

            // minor optimization - if possible, avoid iterating through both arrays
            var lengthsMatch = editItems.length == fields.length,
                changed = false;

            // fields that are no longer drawn should be removed
            for (var i = 0; i < editItems.length; i++) {
                // don't actually remove the items until they have been removed from the DOM via
                // redraw
                var index = fieldNames.indexOf(itemNames[i]);
                if (index == -1) {
                    changed = true;
                    fieldsToRemove.add(editItems[i]);
                } else {
                    // If we're keeping the item, just update width, and notify the item we're
                    // about to redraw
                    editItems[i].width = completeWidths[editItems[i].colNum];
                    editItems[i]._size = null;
                    editItems[i].redrawing();
                }
            }
            // newly rendered fields should be added
            if (!lengthsMatch || changed) {
                var editedVals = lg.getEditedRecord(editRowNum, 0);
                for (var i = 0; i < fields.length; i++) {
                    if (!itemNames.contains(fieldNames[i])) {

                        var colNum = lg.fields.indexOf(fields[i]);
                        var item = lg.getEditItem(
                                        fields[i],
                                        editRecord, editedVals, editRowNum,
                                        colNum, completeWidths[colNum]
                                   );
                        editForm.addItem(item);
                    }
                }
                // Keep the edit form items in the same order in the items array as
                // they are in the ListGrid.

                editForm.items.sortByProperty("colNum", Array.ASCENDING);
            }
        }
        // if editByCell is true this is not necessary - we consistantly have the editForm contain
        // only the necessary cell
        return fieldsToRemove;
    },

    // _storeFocusForRedraw()
    // called when the edit form is showing and the body is being redrawn.
    // remember the current focus state / selection of the edit form so we can reset it after
    // redrawing the item in the DOM
    // blur the item (suppressing the handler if the item will be refocused after redraw)





    _storeFocusForRedraw : function () {

        var lg = this.grid,
            editForm = lg.getEditForm(),
            editColNum = lg.getEditCol();
        if (editForm.hasFocus) {
            var focusItem = editForm.getFocusSubItem();
            if (focusItem) {
                focusItem.updateValue();
                var origFocusItem = focusItem;

                // We may be focused in a sub item, in which case we need to use the
                // parentItem to get the field name wrt our fields array
                while (focusItem.parentItem != null) {
                    focusItem = focusItem.parentItem;
                }

                // blur the focus item before removing it from the DOM.
                // If canEditCell for the current focus item returns false, we will
                // not redisplay it at the end of this method, so allow it to fire the
                // standard blur-handler

                if (!lg.canEditCell(focusItem.rowNum, focusItem.colNum) ||
                    editColNum != focusItem.colNum) {

                    editForm.blur();
                } else {

                    if (focusItem.hasFocus) {
                        // remember the current selection, so we can reset it after the redraw
                        // and refocus. [will have no effect if the item is not a text-item]
                        focusItem.rememberSelection();
                        this._editorSelection =
                            [focusItem._lastSelectionStart, focusItem._lastSelectionEnd];
                    }
                    editForm._blurFocusItemWithoutHandler();

                }

            }
        }

        editForm._setValuesPending = true;
    },

    // If the editForm is visible during a body redraw() this method ensures that after the
    // redraw completes, and the form items are present in the DOM, focus / selection is restored
    // to whatever it was before the redraw
    _restoreFocusAfterRedraw : function (editColNum) {
        var lg = this.grid,
            editForm = lg.getEditForm(),
            editItem = editForm.getItem(lg.getEditorName(lg.getEditRow(), editColNum));
        if (editItem != null && editItem.isDrawn()) {
            var scrollLeft = lg.body.getScrollLeft(),
                scrollTop = lg.body.getScrollTop(),
                viewportWidth = lg.body.getViewportWidth(),
                viewportHeight = lg.body.getViewportHeight(),
                rect = editItem.getRect(),
                // If we are partially out of the viewport, don't put focus into item -
                // forces a native scroll which can interfere with user scrolling.
                // Note: partially out of viewport actually could be ok for text items
                // where focus will only cause a scroll if the actual text is offscreen.

                outOfViewport = rect[0] < scrollLeft ||
                                rect[1] < scrollTop ||
                                rect[0] + rect[2] > (scrollLeft + viewportWidth) ||
                                rect[1] + rect[3] > (scrollTop + viewportHeight);

            if (!outOfViewport) {
                // Avoid selecting the focused value - we don't want rapid keypresses
                // to kill what was previously entered
                editForm._focusInItemWithoutHandler(editItem);
                // Reset the selection / text insertion point to whatever was
                // remembered before the redraw.

                if (this._editorSelection && this._editorSelection[0] != null) {
                    editItem.setSelectionRange(this._editorSelection[0], this._editorSelection[1]);
                }
                // clear up the _editorSelection flag so we don't try to restore focus again on
                // scroll
                delete this._editorSelection;
            }
        }
    },
    // If programmatic 'focus' from "syntheticTabIndex" (IE - tab keypress when the clickMask is up)
    // is called on the ListGrid body and the editor is showing, it
    // makes sense to focus in the edit form.
    focusAtEnd : function (start) {
        var grid = this.grid,
            editForm = grid ? grid.getEditForm() : null;
        if (editForm) {
            editForm.focusAtEnd(start);
        } else {
            return this.Super("focusAtEnd", arguments);
        }
    },

    // Override cellMove: We need to be able to show validation error HTML
    cellMove : function (record,rowNum,colNum) {
        // If the event is bubbled from an embedded child widget no need to check for
        // icons
        if (isc.EH.lastEvent.target == this) {

        var nativeTarget = isc.EH.lastEvent ? isc.EH.lastEvent.nativeTarget : null;
        if (nativeTarget && nativeTarget.getAttribute != null &&
            (nativeTarget.getAttribute("isErrorIcon") == "true"))
        {
            // adjust for frozen fields
            colNum = this.grid.getFieldNumFromLocal(colNum, this);
            if (this.grid._overErrorIcon != null) {
                var lastRow = this.grid._overErrorIcon[0],
                    lastCol = this.grid._overErrorIcon[1];
                if (lastRow != rowNum || lastCol != colNum) {
                    this.grid._handleErrorIconOut();
                }
            }

            if (this.grid._overErrorIcon == null) {
                this.grid._handleErrorIconOver(rowNum,colNum);
            }
        } else {
            if (this.grid._overErrorIcon != null) {
                this.grid._handleErrorIconOut();
            }
        }
        }
    },

    cellOut : function (record, rowNum, colNum) {
        if (this.grid._overErrorIcon != null) {
            this.grid._handleErrorIconOut();
        }
    },

    // Override shouldShowRollOver to avoid styling the current edit cell with the over
    // style.
    // This avoids the issue where if you roll over the edit form items, the rollover style
    // would flash off as the body receives a mouseout (looks very weird).
    // Also - support showing the rollOver styling for the current focus row even if
    // showRollOver is false.
    // We'll still use the rollOver style name in this case, but it improves keyboard accessibility
    // without requiring the normal rollOver effects.
    shouldShowRollOver : function (rowNum, colNum,a,b) {

        //if (!this.invokeSuper(isc.GridBody, "shouldShowRollOver", rowNum,colNum,a,b)) return false;

        var hiliteOnFocus = this.grid.hiliteRowOnFocus;
        if (hiliteOnFocus == null) hiliteOnFocus = this.grid.showRollOver;

        if ((!this.grid.showRollOver &&
             (!hiliteOnFocus || (this._lastHiliteRow != rowNum)))
            || this._rowAnimationInfo)
        {
            return false;
        }

        // don't show rollover for the edit row if editing the whole row
        var lg = this.grid;
        if (lg._editorShowing && !lg.editByCell && rowNum == lg._editRowNum) return false;

        return true;
    },

    updateRollOver : function (rowNum, colNum, movingToNewCell) {

        var lg = this.grid;

        if (lg.showRollOverCanvas || lg.showRollUnderCanvas) {
            // movingToNewCell param passed when the user rolled off one cell and over another
            // and this method is being called to clear the first cell hilight.
            // we can no-op in this case since we'll update the rollOverCanvas on the subsequent
            // call to this method, and that will avoid a clear/draw cycle (and flash)

            if (!movingToNewCell) {
                var leaving = !(this.lastOverRow == rowNum && this.lastOverCol == colNum);
                var gridColNum = lg.getFieldNumFromLocal(colNum, this);
                lg.updateRollOverCanvas(rowNum, gridColNum, leaving);
            }
            // no support for frozen body / rollOverCanvas yet

        }

        this.setRowStyle(rowNum, null, this.canSelectCells || this.useCellRollOvers ?
                         colNum : null);

        // frozen fields: if there is another body, force its rollover row to match and
        // update it
        var otherBody = (this == lg.body ? lg.frozenBody : lg.body);
        if (otherBody && !this.useCellRollOvers) {
            otherBody.lastOverRow = this.lastOverRow;
            otherBody.lastOverCol = this.lastOverCol;
            otherBody.setRowStyle(rowNum, null, (this.useCellRollOvers ? colNum : null));
        }
    },

    _selectCellOnMouseDown : function (record, rowNum, colNum) {
        // remember the last cell clicked (used for keyboard navigation)
        // (Note: we use the same cell used for selection rather than the actual position
        // of the event as that's where the user will see the visual indication)
        this._lastSelectedRow = rowNum;
        this._lastSelectedCol = colNum;

        this.grid._lastSelectedBody = this;

        //this.logWarn("mouseDown at: " + [rowNum, colNum]);

        if (this.useRowSpanStyling) {
            // rowSpan-sensitive selection
            var gridSelection = this.grid.selection;

            // selected cells are designed by the coordinates where spanning starts.
            // NOTE: this means that calling isSelected() with return false for any cell
            // coordinates where no cell exists in the DOM (because it was spanned over by a
            // rowSpanning cell in a previous row).  This is true even when the spanning cell
            // that eliminated that DOM cell is in facet selected.
            var startRow = this.getCellStartRow(rowNum, colNum);

            // for ctrl-click just select/deselect the clicked cell
            if (isc.EH.modifierKeyDown()) {
                var gridColumn = this.grid.getFieldNumFromLocal(colNum, this);
                gridSelection.selectOnMouseDown(this, startRow, gridColumn);
                return;
            }

            // deselect everything - shift modifiers, dragging or other cases not supported in
            // this mode
            gridSelection.deselectAll();

            // select all cells to the right that are partially or wholly spanned by the
            // clicked cell

            var gridBody, startCol, span,
                mode = this.rowSpanSelectionMode;

            if (mode == "forward" || mode == null) {
                // field num where the click landed, in terms of listGrid.fields
                startCol = this.grid.getFieldNumFromLocal(colNum, this);
                // cells spanned by the clicked cell
                span = this.getCellRowSpan(startRow, colNum);
            } else if (mode == "outerSpan") {
                // optionally, start the selection from the span of the first column, which
                // creates a selection behavior similar to the default row-level selection
                // behavior, based on the concept of the span in the first column defining the
                // "row"
                startCol = 0;
                gridBody = this.grid.getFieldBody(0);
                startRow = gridBody.getCellStartRow(rowNum, 0);
                span = gridBody.getCellRowSpan(startRow, 0);
            } else if (mode == "both") {
                // use the span of the clicked cell
                span = this.getCellRowSpan(startRow, colNum);
                // but go through all columns
                startCol = 0;
            } else {
                this.logWarn("unrecognized rowSpanSelectionMode: '" + mode + "'");
                return;
            }

            //this.logWarn("start cell: " + [startRow, startCol] +
            //             " spans: " + span);
            // for each column to the right of the starting column
            for (var column = startCol; column < this.grid.fields.length; column++) {
                // get the body that contains the field and the field's index within that body
                var bodyToSelect = this.grid.getFieldBody(column),
                    localFieldNum = this.grid.getLocalFieldNum(column);

                // for each row spanned by the starting cell
                for (var i = startRow; i < startRow+span; i++) {
                    // when selecting, select the coordinates of the beginning of the span
                    var cellStartRow = bodyToSelect.getCellStartRow(i, localFieldNum);
                    gridSelection.selectCell(cellStartRow, column);
                    // and skip past cells that were spanned over
                    i += (bodyToSelect.getCellRowSpan(cellStartRow, localFieldNum) - 1);
                }
            }
            return;
        }

        // if we're in the body, select rows or cells
        this.selection.selectOnMouseDown(this, rowNum, colNum);
    },

    // override selectOnMouseDown/Up to disable selection when a row is clicked anywhere
    // besides the checkbox when selectionAppearance is checkbox.
    selectOnMouseDown : function (record, rowNum, colNum) {
        var shouldSelect = true,
            selApp = this.grid.selectionAppearance,
            cbSel = (selApp == "checkbox");
        if (cbSel) {
            // if frozen fields are showing, the cb field will show up in the frozen body!
            if ((this.grid.frozenFields != null && this.grid.frozenBody != this) ||
                (this.grid.getCheckboxFieldPosition() != colNum))
            {
                shouldSelect = false;
            }
        }

        if (shouldSelect) {
            if (this.canSelectCells) this._selectCellOnMouseDown(record, rowNum, colNum);
            else this.invokeSuper(isc.GridBody, "selectOnMouseDown", record, rowNum, colNum);
        }

        if (isc.screenReader) {
            // In screen reader mode, if canSelectCells is enabled, we want to hilite the cell;
            // otherwise, we will want to hilite the entire row.
            // _putNativeFocusInRow() calls _hiliteRecord(), so we will need to clear this hilite.
            this._putNativeFocusInRow(rowNum);
            if (this.canSelectCells) {
                var lastRow = this.lastOverRow,
                    lastCol = this.lastOverCol;
                this.lastOverRow = null;
                this.lastOverCol = null;
                this.updateRollOver(lastRow, lastCol);
                this.grid._lastKeyboardHiliteBody = this;
                this.grid._hiliteCell(rowNum, colNum);
            }
        }
    },

    // When showing the edit clickMask, a mouseDown will clear the mask and end editing
    // before normal mouse-down processing.
    // Override _getMouseDownCell to ensure, even if this caused the row-height to change,
    // we return the cell coords before the change of row-height.
    _getMouseDownCell : function () {
        if (this._maskedMouseDownCell != null) {
            var cell = this._maskedMouseDownCell;
            // clear the property so it doesn't impact future mouseDowns
            this._maskedMouseDownCell = null;
            return cell;
        }
        return this.Super("_getMouseDownCell", arguments);
    },

    // Override mouseUp.
    // If the mouseUp occurred over a CanvasItem, ignore it.

    mouseUp : function () {
        var target = isc.EH.getTarget();

        if (this.grid && target != null && target != this && this.grid._editorShowing) {
            var editForm = this.grid.getEditForm();
            while (target != this && target != null && target != editForm) {
                if (target.canvasItem && editForm.items.contains(target.canvasItem)) {
                    return;
                }
                target = isc.isA.FormItem(target) ? target.containerWidget : target.parentElement;
            }
        }
        return this.Super("mouseUp", arguments);
    },

    selectOnMouseUp : function (record, rowNum, colNum) {
        var cbColNum = this.grid.getCheckboxFieldPosition(),
            selApp = this.grid.selectionAppearance;
        if (selApp != "checkbox" || (selApp == "checkbox" && cbColNum == colNum)) {
            this.invokeSuper(isc.GridBody, "selectOnMouseUp", record, rowNum, colNum);
        }
    },

    // Override handleSelectionChanged() to fire our viewStateChanged method
    handleSelectionChanged : function (record,state) {
        if (this.grid.suppressSelectionChanged) return;
        var returnVal = this.Super("handleSelectionChanged", arguments);
        this.grid.handleViewStateChanged();
        return returnVal;
    },

    setSelection : function (selection) {
        this.clearSelection();
        this.Super("setSelection", arguments);
    },

    clearSelection : function () {
        var selection = this.selection;
        this.Super("clearSelection", arguments);
        // if selection not inherented from ListGrid, destroy it
        if (selection && selection.isA("DependentCellSelection")) {
            selection.destroy();
        }
    },

    _setSeparateCellSelection : function (selection, firstCol) {
        this.clearSelection();
        if (selection) {
            this.selection = selection.getDependentCellSelection(this.fields.length);
            this.selection._updateDependency(firstCol);
            this.observe(this.selection, "selectionChanged", function () {
                this._cellSelectionChanged(this.selection.changedCells);
            });
        }
    },

    // When refreshing cellStyle, notify our edit items that the underlying cell style changed
    // so they can update if necessary
    _updateCellStyle : function (record, rowNum, colNum, cell, className, a,b,c) {
        this.invokeSuper(isc.GridBody, "_updateCellStyle", record, rowNum,colNum,cell,className,a,b,c);
        var lg = this.grid;
        if (lg && lg.getEditRow() == rowNum) {
            var fieldName = lg.getFieldName(lg.getFieldNumFromLocal(colNum, this)),
                form = lg.getEditForm(),
                item = form ? form.getItem(fieldName) : null;
            if (item && item.gridCellStyleChanged) {
                if (className == null) className = this.getCellStyle(record,rowNum,colNum);
                item.gridCellStyleChanged(record, rowNum, colNum, className);
            }
        }

    },

    // hovers: override getHoverTarget to return a pointer to our grid - this allows
    // the developer to call 'updateHover' directly on the grid.
    getHoverTarget : function () {
        return this.grid;
    },

    // direct keyPresses to the ListGrid as a whole to implement arrow navigation,
    // selection, etc

    keyPress : function (event, eventInfo) {
        return this.grid.bodyKeyPress(event, eventInfo);
    },

    // getters for the current keyboard focus row for key-events.

    getFocusRow : function () {
        return this.grid.getFocusRow();
    },
    getFocusCol : function () {
        var colNum = this.grid._getKeyboardClickNum();
        return this.grid.getLocalFieldNum(colNum);
    },

    // Override _focusChanged to implement 'editOnFocus' - start editing the first
    // editable cell if appropriate.
    // See comments in 'editOnFocus' jsdoc comment for details of how this should work.
    _focusChanged : function (hasFocus) {

        // use the Super implementation to set up this.hasFocus BEFORE we further
        // manipulate focus due to editing.
        var returnVal = this.Super("_focusChanged", arguments);

        var lastEvent = isc.EH.lastEvent;

        // if we're acquiring focus because we're in the middle of a click sequence on the body,
        // the mouse handlers will correctly start editing or place focus on whatever row was hit, and we
        // should do nothing
        if (lastEvent.target == this &&
              (lastEvent.eventType == isc.EH.MOUSE_DOWN ||
               lastEvent.eventType == isc.EH.MOUSE_UP ||
               lastEvent.eventType == isc.EH.CLICK ||
               lastEvent.eventType == isc.EH.DOUBLE_CLICK)) return returnVal;

        // otherwise, entering focus due to a key event (tab, shift-tab) or something else (programmatic
        // including re-focus due to clickMask blur).
        var editCell,
            parent = this.grid;
        if (hasFocus && parent.isEditable()) {

            // editOnFocus enabled, but not currently editing
            if (parent.editOnFocus && parent.isEditable() &&
                parent.getEditRow() == null)
            {
                if (this.logIsInfoEnabled("gridEdit")) {
                    this.logInfo("Editing on focus: eventType: " + lastEvent.eventType +
                                 ", lastTarget " + lastEvent.target, "gridEdit");
                }

                // If we're explicitly suppressing edit on focus, don't start editing.
                if (parent._suppressEditOnFocus) {
                    delete parent._suppressEditOnFocus;
                } else {
                    // this'll run through every cell in every record until it finds one that's
                    // editable
                    var editCell = parent.getFocusCell();

                    var editCellValid = isc.isAn.Array(editCell) && editCell[0] != null &&
                            editCell[0] >= 0 && editCell[1] != null && editCell[1] >= 0 &&
                            parent.canEditCell(editCell[0], editCell[1]);
                    if (!editCellValid) {
                        editCell = parent.findNextEditCell(0,0,true,true);
                    }

                    if (editCell != null) {
                        parent.handleEditCellEvent(editCell[0], editCell[1], isc.ListGrid.FOCUS);
                }
            }
        }
        }

        // In screenReader mode, if focus is moving into the grid but we're not going into editing mode,
        // put focus onto the row element rather than onto the GR body.

        var hiliteOnFocus = parent.hiliteRowOnFocus;
        if (hiliteOnFocus == null) hiliteOnFocus = parent.showRollOver;
        if (isc.screenReader) {
            if (hasFocus) {
                if (editCell == null) {
                    var lastEvent = isc.EH.lastEvent,
                        eventType = lastEvent.eventType;

                    // find the last hilited row if there is one
                    var rowNum = this.getNativeFocusRow();

                    //this.logWarn("focus entering body - focusing in native row: " + rowNum +
                    //             ", focus row was: " + parent.getFocusRow());
                    this._putNativeFocusInRow(rowNum);
                }
            } else {
                parent.clearLastHilite();
            }
        // Even if we don't have screenReader mode enabled, hilite the current keyboard
        // target row on focus (but don't trigger a click or select it).
        // Also call clearLastHilite() on blur, so we don't show orphaned "over"
        // styling when the user takes focus from the grid
        } else if (hiliteOnFocus) {
            if (hasFocus) {
                if (editCell == null) {
                    if (parent.canSelectCells) {
                        parent._navigateToNextCell(0, 1, true, true, "focus");
                    } else {
                        parent._navigateToNextRecord(1, true, "focus", true);
                    }
                }
            } else {
                // If the user mouseDowned on an embedded widget, such as a rollOverCanvas,
                // don't clear the hilight or we'll lose the roll-over canvas altogether.
                var mouseDownTarget = isc.EH.mouseDownTarget(),
                    eventType = isc.EH.lastEvent.eventType;
                if (eventType != isc.EH.MOUSE_DOWN || !this.contains(mouseDownTarget)) {
                parent.clearLastHilite();
            }
            }

        }
        return returnVal;
    },

    // override putNativeFocusInRow to ensure we hilight the focus row
    _putNativeFocusInRow : function (rowNum, suppressFocus) {
        var parent = this.grid;
        // if suppressFocus is passed, we're not actually focussing into the row so don't
        // hilite the row.
        if (parent && parent.hiliteOnNativeRowFocus && !suppressFocus) parent._hiliteRecord(rowNum);
        return this.Super("_putNativeFocusInRow", arguments);
    },

    // override updateRowSelection to update selectionCanvas if necessary
    updateRowSelection : function (rowNum) {
        var lg = this.grid;
        if (!lg) return;



        if (lg.showSelectionCanvas || lg.showSelectionUnderCanvas) lg.updateSelectionCanvas();
        if (lg._dontRefreshSelection) {
            return;
        }

        this.invokeSuper(isc.GridBody, "updateRowSelection", rowNum);

        if (isc.Canvas.ariaEnabled() && lg.selection) {
            this.setRowAriaState(rowNum, "selected", lg.selection.isSelected(lg.getRecord(rowNum), true));
        }

        // with selectionAppearance:"checkbox", detect all rows selected and update checkbox in
        // header
        if (lg.getCurrentCheckboxField() != null) {
            var cellNum = lg.getCheckboxFieldPosition();
            if (lg) {
                lg.refreshCell(rowNum, cellNum);
                lg.updateCheckboxHeaderState();
            }

        } else if (lg.getTreeFieldNum && lg.selectionAppearance == "checkbox") {
            // in the TreeGrid, refresh the tree cell because that's where the checkbox is shown
            var treeCellNum = lg.getTreeFieldNum();
            lg.refreshCell(rowNum, treeCellNum);
        }
    },

    // Fired when selecting a list of entries (for every row)
    // Default implementation marks for redraw
    // Also set up to update SelectionCanvas when the thread completes.
    markForRowSelectionRefresh : function () {
        var lg = this.grid;
        if (!lg) return;
        if (lg.showSelectionCanvas || lg.showSelectionUnderCanvas) {
            lg.fireOnPause("updateSelectionCanvasFromRowRefresh", "updateSelectionCanvas");
        }
        if (lg.getCurrentCheckboxField() != null) {
            lg.fireOnPause("updateCheckboxHeaderFromRowRefresh", "updateCheckboxHeaderState");
        }

        if (lg._dontRefreshSelection) {
            return;
        }
        // This will mark the body for redraw (refreshing the actual styling / checkboxes)
        this.invokeSuper(isc.GridBody, "markForRowSelectionRefresh");
    },

    // ditto with _cellSelectionChanged
    _cellSelectionChanged : function (cellList,b,c,d) {
        var lg = this.grid;
        if (lg != null &&
            (lg.showSelectionCanvas || lg.showSelectionUnderCanvas))
        {
            lg.updateSelectionCanvas();
        }
        return this.invokeSuper(isc.GridBody, "_cellSelectionChanged", cellList, b,c,d);
    },

    // remove any dynamic references that point to us if we're being destroyed
    destroy : function () {
        var grid = this.grid;
        if (this == grid._lastSelectedBody)       grid._lastSelectedBody       = null;
        if (this == grid._lastKeyboardHiliteBody) grid._lastKeyboardHiliteBody = null;
        this.Super("destroy", arguments);
    },

    // Embedded components
    // -----------------------

    // animateShow selectionCanvas / rollOverCanvas if appropriate
    shouldAnimateEmbeddedComponent : function (component) {
        var grid = this.grid;
        if (component == grid.selectionCanvas) return grid.animateSelection;
        if (component == grid.selectionUnderCanvas) return grid.animateSelectionUnder;
        if (component == grid.rollOverCanvas) return grid.animateRollOver;
        if (component == grid.rollUnderCanvas) return grid.animateRollUnder;

        return false;
    },


    _handleEmbeddedComponentResize : function (component, deltaX, deltaY) {
        this.Super("_handleEmbeddedComponentResize", arguments);

        // Notify the grid - allows us to update the other body if we're showing
        // both a frozen and an unfrozen body
        this.grid._handleEmbeddedComponentResize(this, component, deltaX, deltaY);
    },

    // Override draw() to scroll to the appropriate cell if 'scrollCellIntoView' was called
    // before the body was drawn/created
    // Also update the edit form item rows if we're already editing.
    draw : function (a,b,c,d) {
        var lg = this.grid;

        if (lg.getEditRow() != null) {

            var rowNum = lg.getEditRow(),
                record = lg.getRecord(rowNum),
                fieldNum = lg.getEditCol(),
                form = lg._editRowForm,
                items = lg.getEditRowItems(record, rowNum, fieldNum, lg.editByCell),
                liveItems = form.getItems();

            var setItems = liveItems == null || items.length != liveItems.length;
            if (!setItems) {
                var liveItemNames = liveItems.getProperty("name");
                for (var i = 0; i < items.length; i++) {
                    if (!liveItemNames.contains(items[i].name)) {
                        setItems = true;
                        break;
                    }
                }
            }
            if (setItems) {
                this.logDebug("calling setItems on form from body draw","gridEdit");
                form.setItems(items);
            } else {
                this.logDebug("Skipping setItems() on form from body draw", "gridEdit");
            }


            form._setValuesPending = true;

        }


        delete this._drawnEditItems;

        this.invokeSuper(isc.GridBody, "draw", a,b,c,d);

        // If we are showing any edit form items, notify them that they have been written
        // into the DOM.

        if (lg._editRowForm) {
            lg._editItemsDrawingNotification(null, null, this);
        }
        // Tell the form to update its values (setItemValues())
        // (do this after the items have been notified that they're drawn to ensure items'
        // element values are set)
        lg.updateEditRow(lg.getEditRow());

        if (lg._scrollCell != null) {
            var scrollRowNum = isc.isAn.Array(lg._scrollCell) ? lg._scrollCell[0] : lg._scrollCell,
                scrollColNum = isc.isAn.Array(lg._scrollCell) ? lg._scrollCell[1] || 0 : 0,
                xPosition = isc.isAn.Array(lg._scrollCell) ? lg._scrollCell[2] : null,
                yPosition = isc.isAn.Array(lg._scrollCell) ? lg._scrollCell[3] : null
            ;
            lg.scrollToCell(scrollRowNum, scrollColNum, xPosition, yPosition);
            delete lg._scrollCell;
        }

        // Call 'updateRecordComponents()' on initial draw to set up recordComponents
        // If this is a ResultSet rather than an array, the updateRecordComponents method
        // will be able to skip all records and we'll render out the components on redraw.
        this.grid.updateRecordComponents();

        if (!this._updatingExpansionComponents) this.grid.updateExpansionComponents();
    },

    // rerun ListGrid-level layout if the body's scrolling state changes, to allow sizing
    // the header appropriately
    layoutChildren : function (reason,a,b,c) {
        this.invokeSuper(isc.GridBody, "layoutChildren", reason,a,b,c);
        // This method may be called with "scrolling state change" when a bodyLayout is
        // currently undrawn but drawing out its children - we've seen this in FF 3
        // In this case bail now since if _updateFieldWidths() is fired on an undrawn body it
        // bails, leaving the body mis sized

        if (!this.isDrawn() || (this.grid.frozenFields && !this.grid.bodyLayout.isDrawn())) {
            return;
        }
        var isScrollStateChanged = (reason == "scrolling state changed"),
            isNewScrollbars = (reason == "introducing scrolling");
        if (isScrollStateChanged || isNewScrollbars) {

            if (this.isRTL() && !this._animatedShowStartRow) {
                this._placeEmbeddedComponents();
            }
        }

        if (isScrollStateChanged) {

            if (this._rowHeightAnimation == null) {
                this.grid.layoutChildren("body scroll changed");
                delete this._scrollbarChangeDuringAnimation;



            } else {
                this._scrollbarChangeDuringAnimation = true;
            }
        }
    },

    // Override rowAnimationComplete to call layoutChildren on the ListGrid if
    // scrollbars were introduced or cleared during animation.
    _rowAnimationComplete : function () {
        this.Super("_rowAnimationComplete", arguments);
        if (this._scrollbarChangeDuringAnimation) {
            this.grid.layoutChildren("body scroll changed during animation");
            delete this._scrollbarChangeDuringAnimation;
        }
    },


    // Override moved to notify any edit form items that they have moved.
    handleMoved : function (a,b,c,d) {
        this.invokeSuper(isc.GridBody, "handleMoved", a,b,c,d);

        var lg = this.grid;
        if (lg._editorShowing) {
            lg._editRowForm.itemsMoved();
        }

    },

    handleParentMoved : function (a,b,c,d) {
        this.invokeSuper(isc.GridBody, "handleParentMoved", a,b,c,d);
        var lg = this.grid;
        if (lg._editorShowing) {
            lg._editRowForm.itemsMoved();
        }
    },

    // Override show() / hide() / parentVisibilityChanged() / clear() to notify the Edit
    // form items that they have been shown / hidden.
    setVisibility : function (newVisibility,b,c,d) {
        this.invokeSuper(isc.GridBody, "setVisibility", newVisibility,b,c,d);
        var lg = this.grid;
        if (lg._editorShowing) lg._editRowForm.itemsVisibilityChanged();
    },

    parentVisibilityChanged : function (newVisibility,b,c,d) {
        this.invokeSuper(isc.GridBody, "parentVisibilityChanged", newVisibility,b,c,d);
        var lg = this.grid;
        if (lg._editorShowing) lg._editRowForm.itemsVisibilityChanged();
    },

    clear : function () {
        var lg = this.grid;
        lg._clearingInactiveEditorHTML();


        delete this._drawnEditItems;
        this.Super("clear", arguments);
        if (lg._editorShowing) {
            // If we're showing the editRow form, notify the items that they have
            // been removed from the DOM.
            lg._editItemsDrawingNotification(null, null, this);

            // Separate mechanism to notify the form that items are no longer visible.

            lg._editRowForm.itemsVisibilityChanged();
        }
    },

    // also notify the edit form items of z index change
    zIndexChanged : function () {
        this.Super("zIndexChanged", arguments);
        var lg = this.grid;
        // Note: setZIndex can be called at init time to convert "auto" to a numeric
        // zIndex - we therefore can't assume that we've been added to the ListGrid as
        // a child yet.
        if (lg && lg._editorShowing) lg._editRowForm.itemsZIndexChanged();

    },
    parentZIndexChanged : function (a,b,c,d) {
        this.invokeSuper(isc.GridBody, "zIndexChanged", a,b,c,d);
        var lg = this.grid;
        if (lg._editorShowing) lg._editRowForm.itemsZIndexChanged();
    },

    // Implement 'redrawFormItem()' - if one of the edit form items asks to redraw
    // we can simply refresh the cell rather than having the entire body redraw
    redrawFormItem : function (item, reason) {
        var lg = this.grid;
        if (lg && (item.form == lg._editRowForm)) {
            // determine which cell
            var row = lg.getEditRow(), col = lg.getColNum(item.getFieldName());

            // If the user has edited the cell, or setValue() has been called on the item
            // we don't want a call to redraw() on the item to drop that value
            if (lg.getEditCol() == col) {
                lg.storeUpdatedEditorValue();
            }

            if (row >= 0 && col >= 0) lg.refreshCell(row, col, false, true);

        } else
            return this.markForRedraw("Form Item Redraw " + (reason ? reason : isc.emptyString));
    },


    sizeFormItem : function (item) {
        var lg = this.grid;
        var width = item.width,
            finalWidth;

        if (isc.isA.String(width)) {
            var fieldWidths = lg.getEditFormItemFieldWidths(item.record),
                fieldWidth = fieldWidths[lg.getFieldNum(item.getFieldName())];
             if (width == "*") {
                finalWidth = fieldWidth;
             } else if (width[width.length-1] == "%") {
                var percentWidth = parseInt(width);
                if (isc.isA.Number(percentWidth)) {
                    finalWidth = Math.floor(fieldWidth * (percentWidth / 100));
                }
            }
        }

        var height = item.height,
            finalHeight;
        if (isc.isA.String(height)) {
            var cellHeight = lg.cellHeight;
            if (width == "*") {
                finalHeight = cellHeight;
            } else if (height[height.length-1] == "%") {
               var percentHeight = parseInt(height);
               if (isc.isA.Number(percentHeight)) {
                   finalHeight = Math.floor(cellHeight * (percentHeight / 100));
               }
            }
        }
        // Hang the calculated values on the _size attribute as we do when running
        // normal stretch-resize policy in form items.

        if (finalHeight != null || finalWidth != null) {
            item._size = [finalWidth == null ? item.width : finalWidth,
                          finalHeight == null ? item.height : finalHeight];
        }

    },

    //>Animation
    // Override startRowAnimation - if doing a delayed redraw to kick off a row animation
    // to close an open folder, we need to temporarily open the folder again to get the
    // appropriate HTML for the animation rows.
    startRowAnimation : function (show, startRow, endRow, callback, speed, duration,
                                  effect, slideIn, delayed)
    {
        this.finishRowAnimation();

        var shouldOpenFolder = (delayed && (this._openFolder != null)),
            tg = this.grid;

        if (shouldOpenFolder) {
            var wasSuppressed = tg._suppressFolderToggleRedraw;
            tg._suppressFolderToggleRedraw = true;
            tg.data.openFolder(this._openFolder);
            tg._suppressFolderToggleRedraw = wasSuppressed;
        }
        this.Super("startRowAnimation", arguments);
        if (shouldOpenFolder) {
            var wasSuppressed = tg._suppressFolderToggleRedraw;
            tg._suppressFolderToggleRedraw = true;
            tg.data.closeFolder(this._openFolder);
            tg._suppressFolderToggleRedraw = wasSuppressed;
        }
        delete this._openFolder;
    }
    //<Animation
});


isc.ListGrid.addClassProperties({


    //>    @type SortArrow
    //            Do we display an arrow for the sorted field ?
    //            @group    sorting, appearance
    //    @value    "none"   Don't show a sort arrow at all.
    //    @value    "corner" Display sort arrow in the upper-right corner (above the scrollbar) only.
    CORNER:"corner",
    //    @value    "field"  Display sort arrow above each field header only.
    FIELD:"field",
    //    @value    "both"   Display sort arrow above each field header AND in corner above scrollbar.
    //BOTH:"both", // NOTE: Canvas establishes this constant
    // @visibility external
    //<
    // NOTE: Canvas established the constant NONE ( == "none")

    //> @type ReorderPosition
    //  Controls where a drag-item should be dropped in relation to the target row
    //  @group dragdrop
    //  @value  ListGrid.BEFORE  Drop the drag-item before the target-row
    BEFORE:"before",
    //  @value  ListGrid.AFTER   Drop the drag-item after the target-row
    AFTER:"after",
    //  @value  ListGrid.OVER    Drop the drag-item over (onto) the target-row
    OVER:"over",
    // @visibility external
    //<

        //> @type RecordDropAppearance
        // Controls how ListGrid record drop events report their
        // +link{listGrid.getRecordDropPosition(),dropPosition}, and where the drop indicator will be displayed
        // if appropriate.
        //
        // @value ListGrid.OVER When the user drops onto a record, dropPosition will always be "over"
        // @value ListGrid.BETWEEN When the user drops onto a record, dropPosition will be either
        //   "before" or "after" depending on whether the mouse was over the top or bottom of
        //   the target record
        BETWEEN: "between",
        // @value ListGrid.BOTH When the user drops onto a record, if the drop occurs centered over the
        //   record, the dropPosition will be reported as "over", otherwise it will be
        //   "before" or "after" depending on whether the mouse was over the top or bottom of the
        //   target record.
        // @value ListGrid.BODY No dropPosition will be reported
        BODY:"body",
        //
        // @visibility external
        //<

        //> @type RecordDropPosition
        // Position of a +link{listGrid.recordDrop} operation with respect to the target record.
        // @value ListGrid.OVER User dropped directly onto the record
        // @value ListGrid.BEFORE User dropped before the record
        // @value ListGrid.AFTER User dropped after the record
        // @value ListGrid.NONE Drop position is not over a record
        //
        // @visibility external
        //<

    //> @type RowEndEditAction
    //  While editing a ListGrid, what cell should we edit when the user attempts to navigate
    //  into a cell past the end of an editable row, via a Tab keypress, or a programmatic
    //  saveAndEditNextCell() call?
    //
    // @value   "same"   navigate to the first editable cell in the same record
    // @value   "next"   navigate to the first editable cell in the next record
    // @value   "done"   complete the edit.
    // @value   "stop"   Leave focus in the cell being edited (take no action)
    // @value   "none"   take no action
    //
    // @visibility external
    // @group editing
    // @see ListGrid.rowEndEditAction
    //
    //<

    //>    @type EnterKeyEditAction
    // What to do when a user hits enter while editing a cell
    // @value "done" end editing (will save edit values if +link{listGrid.autoSaveEdits}
    //  is true).
    // @value "nextCell" edit the next editable cell in the record
    // @value "nextRow" edit the same field in the next editable record
    // @value "nextRowStart" edit the first editable cell in next editable record
    //
    // @group editing
    // @visibility external
    //<

    //> @type EscapeKeyEditAction
    // What to do if the user hits escape while editing a cell.
    // @value "cancel" cancels the current edit and discards edit values
    // @value "done" end editing (will save edit values if +link{listGrid.autoSaveEdits}
    //  is true).
    // @value "exit" exit the editor (edit values will be left intact but not saved).
    //
    // @group editing
    // @visibility external
    //<

    //>    @type EditCompletionEvent
    //            What event / user interaction type caused cell editing to complete.
    //            @visibility external
    //            @group    editing
    //
    //          @value  isc.ListGrid.CLICK_OUTSIDE  User clicked outside editor during edit.
    //          @value  isc.ListGrid.CLICK  User started editing another row by clicking on it
    //          @value  isc.ListGrid.DOUBLE_CLICK  User started editing another row by double
    //                               clicking
    //          @value  isc.ListGrid.ENTER_KEYPRESS Enter pressed.
    //          @value  isc.ListGrid.ESCAPE_KEYPRESS    User pressed Escape.
    //          @value  isc.ListGrid.UP_ARROW_KEYPRESS  Up arrow key pressed.
    //          @value  isc.ListGrid.DOWN_ARROW_KEYPRESS    down arrow key.
    //          @value  isc.ListGrid.TAB_KEYPRESS   User pressed Tab.
    //          @value  isc.ListGrid.SHIFT_TAB_KEYPRESS   User pressed Shift+Tab.
    //          @value  isc.ListGrid.EDIT_FIELD_CHANGE      Edit moved to a different field (same row)
    //          @value  isc.ListGrid.PROGRAMMATIC   Edit completed via explicit function call
    // @visibility external
    //<
    CLICK_OUTSIDE:"click_outside",
    CLICK:"click",
    DOUBLE_CLICK:"doubleClick",
    ENTER_KEYPRESS:"enter",
    ESCAPE_KEYPRESS:"escape",
    UP_ARROW_KEYPRESS:"arrow_up",
    DOWN_ARROW_KEYPRESS:"arrow_down",
    // left/right only used in conjunction with moveEditorOnArrow
    LEFT_ARROW_KEYPRESS:"arrow_left",
    RIGHT_ARROW_KEYPRESS:"arrow_right",
    TAB_KEYPRESS:"tab",
    SHIFT_TAB_KEYPRESS:"shift_tab",
    EDIT_FIELD_CHANGE:"field_change",
    EDIT_ROW_CHANGE:"row_change",
    PROGRAMMATIC:"programmatic",
    // Focus is not a valid edit completion event - focusing in the grid can start an edit
    // if editOnFocus is true but this should not kill an existing edit.
    FOCUS:"focus",

    // GridRenderer passthrough
    // --------------------------------------------------------------------------------------------

    // the following properties, when set on the ListGrid, are applied to the GridBody
    _gridPassthroughProperties : [
        // pass it a selection object (this enables selection behaviors)
        "selection",
        "selectionType",
        "canSelectCells",
        "canDragSelect",
        "canSelectOnRightMouse",
        "recordCanSelectProperty",

        // D&D
        "canDrag",
        "canAcceptDrop",
        "canDrop",

        // table geometry
        "autoFit",
        "wrapCells",
        "cellSpacing",
        "cellPadding",
        "cellHeight",
        "enforceVClipping",
        // autoFitData behavior implemented on GridBody class, not GR class
        "autoFitData",
        "autoFitMaxRecords",
        "autoFitMaxWidth",
        "autoFitMaxColumns",
        "autoFitMaxHeight",
        "autoFitExtraRecords",

        "allowRowSpanning",

        // incremental rendering
        // "showAllRecords" -> showAllRows done elsewhere
        "showAllColumns",
        "drawAllMaxCells",
        "drawAheadRatio",
        "quickDrawAheadRatio",
        "instantScrollTrackRedraw",
        "scrollRedrawDelay",
        "scrollWheelRedrawDelay",

        // printing
        "printMaxRows",

        //>Animation
        // If we're doing a speed rather than duration based row animation allow the cap to
        // be specified on the ListGrid / TreeGrid
        // (Note that this is documented in the TreeGrid class).
        "animateRowsMaxTime",
        //<Animation

        // documented by default setting
        "fastCellUpdates",

        // rollover
        "showRollOver",
        "useCellRollOvers",

        // hover
        "canHover",
        "showHover",
        "showClippedValuesOnHover",
        "hoverDelay",
        "hoverWidth",
        "hoverHeight",
        "hoverAlign",
        "hoverVAlign",
        "hoverStyle",
        "hoverOpacity",
        "hoverMoveWithMouse",

        "hoverByCell",
        "keepHoverActive",
        "cellHoverOutset",

        // empty message
        "showEmptyMessage",
        "emptyMessageStyle",
        "emptyMessageTableStyle",

        // offline message
        "showOfflineMessage",
        "offlineMessageStyle",

        // special presentation of records
        "useCellRecords",
        "singleCellValueProperty",
        "isSeparatorProperty",

        // Focus things -- note no need to pass tabIndex through - layouts should auto-manage
        // their members' tab-orders correctly
        "accessKey",
        "canFocus",
        "_useNativeTabIndex",
        "tableStyle",
        "baseStyle",
        "recordCustomStyleProperty",
        "showSelectedStyle",

        // whether to use rowSpan-oriented cell styling behaviors
        "useRowSpanStyling",
        // selection mode when rowSpanning is active
        "rowSpanSelectionMode",

        "showFocusOutline"
    ],

    // the following methods, when called on the LV, will call the same-named method on the
    // GridRenderer (this.body).
    _lv2GridMethods : [
        // this makes it easier to override getCellStyle at the LV level, since you can call
        // these helpers as this.getCellStyleName()
        "getCellStyleName",
        "getCellStyleIndex",

        "_getShowClippedValuesOnHover",

        // setFastCellUpdates explicitly handled
        // in a method which keeps lg.fcu in synch with the
        // body property value
        //"setFastCellUpdates",

        // checking table geometry
        "getRowTop",
        "getRowPageTop",
        "getRowSize",
        "getDrawnRowHeight",

        // row span information
        "getCellStartRow",
        "getCellRowSpan",

        //> @method listGrid.getVisibleRows
        // @include gridRenderer.getVisibleRows()
        // @return (Array of Integer)
        // @visibility external
        //<
        "getVisibleRows",

        //> @method listGrid.getDrawnRows
        // @include gridRenderer.getDrawnRows()
        // @visibility external
        //<
        "getDrawnRows"
    ],

    // styling

    //>    @method    listGrid.getCellStyle()
    // @include gridRenderer.getCellStyle()
    // @see listGrid.getBaseStyle()
    //<

    //>    @method    listGrid.getCellCSSText()
    // @include gridRenderer.getCellCSSText()
    // @example addStyle
    //<

    // refresh
    //>    @method    listGrid.refreshCellStyle()
    //  @include    gridRenderer.refreshCellStyle()
    //<

    // events
    //>    @method    listGrid.cellOver()
    // @include gridRenderer.cellOver()
    //<
    //>    @method    listGrid.rowOver()
    // @include gridRenderer.rowOver()
    //<

    //>    @method    listGrid.cellOut()
    // @include gridRenderer.cellOut()
    //<
    //>    @method    listGrid.rowOut()
    // @include gridRenderer.rowOut()
    //<

    //>    @method    listGrid.cellHover()
    // @include gridRenderer.cellHover()
    //<
    //> @method listGrid.cellValueHover() ([A])
    // @include gridRenderer.cellValueHover()
    //<
    //>    @method    listGrid.rowHover()
    // @include gridRenderer.rowHover()
    //<
    //>    @method    listGrid.cellHoverHTML()
    // @include gridRenderer.cellHoverHTML()
    //<
    //> @method listGrid.cellValueHoverHTML()
    // @include gridRenderer.cellValueHoverHTML()
    //<

    //>    @method    listGrid.cellContextClick()
    // @include gridRenderer.cellContextClick()
    // @example cellClicks
    //<
    //>    @method    listGrid.rowContextClick()
    // @include gridRenderer.rowContextClick()
    // @example recordClicks
    //<

    //>    @method    listGrid.cellMouseDown()
    // @include gridRenderer.cellMouseDown()
    //<
    //>    @method    listGrid.rowMouseDown()
    // @include gridRenderer.rowMouseDown()
    //<

    //>    @method    listGrid.cellMouseUp()
    // @include gridRenderer.cellMouseUp()
    //<
    //>    @method    listGrid.rowMouseUp()
    // @include gridRenderer.rowMouseUp()
    //<

    //>    @method    listGrid.cellClick()
    // Called when a cell receives a click event.
    // <P>
    // Note that returning false from this method will not prevent any
    // specified +link{listGrid.rowClick} handler from firing.
    //
    // @group   events
    // @param   record  (ListGridRecord)    Record object returned from getCellRecord()
    // @param    rowNum    (number)    row number for the cell
    // @param    colNum    (number)    column number of the cell
    // @return    (boolean)    whether to cancel the event
    // @visibility external
    // @example cellClicks
    //<

    //>    @method    listGrid.cellDoubleClick()
    // @include gridRenderer.cellDoubleClick()
    // @example cellClicks
    //<

    // Geometry
    //>    @method    listGrid.getRowTop()
    // @include gridRenderer.getRowTop()
    // @visibility external
    //<

    //>    @method    listGrid.getRowPageTop()
    // @include gridRenderer.getRowPageTop()
    // @visibility external
    //<

    // ListGrid / GridBody passthroughs
    // ---------------------------------------------------------------------------------------

    // the following methods, when called on the GridRenderer used as LV.body, call the same-named
    // method on the ListGrid instance itself
    _grid2LVMethods : [

        "getTotalRows",
        "isEmpty",
        "cellIsEnabled",
        "willAcceptDrop",

        // passed scroll change notification through
        "scrolled",

        // native element naming
        "getTableElementId",
        "getRowElementId",
        "getCellElementId",

        // shouldFixRowHeight - enables us to override the ListGrid level 'fixedRecordHeights'
        // for individual rows
        "shouldFixRowHeight",

        "getEmptyMessage",
        "getCanHover",
        // bubble stopHover on the GR up to stopHover here.
        "stopHover",

        "updateEmbeddedComponentZIndex"

        // NOTE: These methods pick up their parameters from the stringMethodRegistry on the
        // GridRenderer class. If expanding this list ensure that any methods that take parameters
        // are registered as stringMethods on that class
    ],

    // used by _invokeKeyboardCopyPasteShortcut for copy/paste between ListGrids
    _cellClipboard : null
});

isc.ListGrid.addClassMethods({
    makeBodyMethods : function (methodNames) {
        var funcTemplate = this._funcTemplate;
        if (funcTemplate == null) {
            funcTemplate = this._funcTemplate = [
                ,


                "this.grid._passthroughBody = this;" +
                "var returnVal = this.grid.",,"(",,");" +
                "this.grid._passthroughBody=null;" +
                "return returnVal;"];
        }

        var methods = {};

        for (var i = 0; i < methodNames.length; i++) {
            var methodName = methodNames[i],
                argString = isc.GridRenderer.getArgString(methodName),


                isCellIsEnabled = (methodName == "cellIsEnabled");

            if (isc.contains(argString, "colNum")) {
                // if there's a colNum argument, map it to the field index in the master
                funcTemplate[0] = "if (this.fields[colNum]) colNum = this.fields[colNum].masterIndex;"

            } else if (isc.isAn.emptyString(argString)) {
                // if there are no arguments, pass the body itself as a means of identifying
                // the calling body
                argString = "body";
                funcTemplate[0] = "body = this;";
            } else {
                funcTemplate[0] = null;
            }
            if (isCellIsEnabled) {
                var checkRecord = "if (record === undefined) record = this.grid.getCellRecord(" + argString + ");";
                funcTemplate[0] = (funcTemplate[0] ? funcTemplate[0] + checkRecord : checkRecord);
            }

            // create a function that routes a function call to the target object
            funcTemplate[2] = methodName;
            funcTemplate[4] = (isCellIsEnabled ? "record," + argString : argString);
            var functionText = funcTemplate.join(isc.emptyString);

            //this.logWarn("for method: " + methodName + " with argString :"  + argString +
            //             " function text is: " + functionText);

            var method = methods[methodName] = isc._makeFunction(
                (isCellIsEnabled ? argString + ",record" : argString), functionText);
            method._isPassthroughMethod = true;
        }

        return methods;
    },


    classInit : function () {
        // create functions to have methods on the ListGrid's body call methods on the ListGrid
        // itself.  This is partly legacy support: the way to customize body rendering used to
        // be to install functions that controlled body rendering directly on the ListGrid
        // itself.

        // make certain grid methods appear on the LV for convenience, so you don't have to go
        // this.body.someGridMethod()


        this.addMethods(isc.ClassFactory.makePassthroughMethods(
            this._lv2GridMethods, "body"));

        // ----------------------------------------------------------------------------------------
        // create methods that can be installed on the body to call methods on the LV itself, for:
        var passthroughMethods = {};

        // - handlers (like cellOver) and overrides (like getCellCSSText) that we allow to be
        //   defined on the LV but are really grid APIs
        var gridAPIs = isc.getKeys(isc.GridRenderer._gridAPIs),
            passthroughMethods = isc.ListGrid.makeBodyMethods(gridAPIs);

        // - methods the grid needs to fulfill as the drag/drop target, which are really implemented
        //   on the LV
        isc.addProperties(passthroughMethods,
                          isc.ListGrid.makeBodyMethods(this._grid2LVMethods));

        this._passthroughMethods = passthroughMethods;



        // create methods on the ListGrid to act as Super implementations for per-instance
        // overrides of methods where we want to call the original GridRenderer implementation
        // as Super.
        var passBackMethods = {},
            funcTemplate = [
                ,
                // _passthroughBody is set up by the body function that called back up the
                // the grid method - if present, we use it to ensure we call the original
                // implementation on the correct body.
                "var _passthroughBody = this._passthroughBody || this.body;" +
                " if (_passthroughBody == null) {" +
                    "return;" +
                "}" +
                "if(_passthroughBody.__orig_",,")return _passthroughBody.__orig_",,"(",,")"],
            origPrefix  = "__orig_",
            gridProto = isc.GridRenderer.getPrototype();
        for (var i = 0; i < gridAPIs.length; i++) {
            var methodName = gridAPIs[i],
                argString = isc.GridRenderer.getArgString(methodName);
            if (isc.ListGrid.getInstanceProperty(methodName) == null) {

                if (isc.contains(argString, "colNum")) {
                    // if there's a colNum argument, map it to the field index in the body

                    funcTemplate[0] = "if (colNum != null && colNum >= 0) colNum = this.getLocalFieldNum(colNum);"
                } else {
                    funcTemplate[0] = null;
                }
                funcTemplate[2] = funcTemplate[4] = methodName;
                funcTemplate[6] = argString

                passBackMethods[methodName] = isc._makeFunction(argString,
                    funcTemplate.join(isc.emptyString));
                // XXX this would also work, but imposes another Super call penalty, and is
                // odd (call to Super from outside of the object)
                //"return this.body.Super('" + methodName + "', arguments);");
            }

            gridProto[origPrefix + methodName] = gridProto[methodName];
        }
        this._passBackMethods = passBackMethods;
        this.addMethods(passBackMethods);

    },

    // retrieve possibly sorted list of coordinates from a coordinate record
    _getCoordinateList : function (coordinateRecord, sortByCoordinate) {
        var list = [];
        for (var coordinate in coordinateRecord) {
            if (coordinateRecord.hasOwnProperty(coordinate)){
                list.add(parseInt(coordinate));
            }
        }
        if (sortByCoordinate) {
            list.sort(function (a, b) { return a - b; });
        }
        return list;
    }
});

// add default properties to the class
isc.ListGrid.addProperties( {

    //>    @attr listGrid.styleName (CSSStyleName : "listGrid" : IRW)
    // Default CSS class for the ListGrid as a whole.
    // @group appearance
    // @visibility external
    //<
    styleName:"listGrid",

    //>    @attr listGrid.data (List of ListGridRecord : null : IRW)
    // A list of ListGridRecord objects, specifying the data to be used to populate the
    // ListGrid.  In ListGrids, the data array specifies rows.
    // <p>
    // When using a +link{DataSource}, rather than directly provide <code>data</code>, you will
    // typically call +link{fetchData()} instead, which will automatically establish
    // <code>data</code> as a +link{ResultSet} (see the +link{fetchData()} docs for details).
    // <p>
    // If you call <code>fetchData</code>, any previously supplied <code>data</code> is
    // discarded.  Also, it is not necessary to call <code>setData()</code> after calling
    // +link{ListGrid.fetchData()}.
    // <p>
    // When calling <code>setData()</code><smartgwt>,
    // if <code>data</code> is provided as a RecordList or ResultSet</smartgwt>, direct changes to
    // the list such as adding or removing records will be automatically observed and the
    // ListGrid will redraw in response.  Direct changes to individual Records will not be
    // automatically observed and require calls to +link{refreshCell()} or +link{refreshRow()}
    // to cause the ListGrid to visually update.  Calling methods such as
    // +link{ListGrid.updateData()}, +link{removeData()} or +link{addData()} always causes
    // automatic visual refresh.
    //
    // @group data
    // @see ListGridRecord
    // @setter setData()
    // @visibility external
    // @example inlineData
    // @example localData
    //<

    // useCellRecords - Is our data model going to be one record per cell or one record per row?
    useCellRecords:false,

    //> @object ListGridRecord
    // A ListGridRecord is a JavaScript Object whose properties contain values for each
    // +link{ListGridField}.  A ListGridRecord may have additional properties which affect the
    // record's appearance or behavior, or which hold data for use by custom logic or other,
    // related components.
    // <p>
    // For example a ListGrid that defines the following fields:
    // <pre>
    // fields : [
    //     {name: "field1"},
    //     {name: "field2"}
    // ],
    // </pre>
    // Might have the following data:
    // <pre>
    // data : [
    //     {field1: "foo", field2: "bar", customProperty:5},
    //     {field1: "field1 value", field2: "field2 value", enabled:false}
    // ]
    // </pre>
    // Each line of code in the <code>data</code> array above creates one JavaScript Object via
    // JavaScript {type:ObjectLiteral,object literal} notation.  These JavaScript Objects are
    // used as ListGridRecords.
    // <P>
    // Both records shown above have properties whose names match the name property of a
    // ListGridField, as well as additional properties.  The second record will be disabled due to
    // <code>enabled:false</code>; the first record has a property "customProperty" which will
    // have no effect by default but which may be accessed by custom logic.
    // <P>
    // After a ListGrid is created and has loaded data, records may be accessed via
    // +link{listGrid.data}, for example, listGrid.data.get(0) retrieves the first record.
    // ListGridRecords are also passed to many events, such as
    // +link{ListGrid.cellClick,cellClick()}.
    // <P>
    // A ListGridRecord is always an ordinary JavaScript Object regardless of how the grid's
    // dataset is loaded (static data, java server, XML web service, etc), and so supports the
    // normal behaviors of JavaScript Objects, including accessing and assigning to properties
    // via dot notation:
    // <pre>
    //     var fieldValue = record.<i>fieldName</i>;
    //     record.<i>fieldName</i> = newValue;
    // </pre>
    // <P>
    // Note however that simply assigning a value to a record won't cause the display to be
    // automatically refreshed - +link{listGrid.refreshCell()} needs to be called.  Also,
    // consider +link{group:editing,editValues vs saved values} when directly modifying
    // ListGridRecords.
    // <P>
    // See the attributes in the API tab for the full list of special properties on
    // ListGridRecords that will affect the grid's behavior.
    //
    // @treeLocation Client Reference/Grids/ListGrid
    // @see ListGrid.data
    // @inheritsFrom Record
    // @visibility external
    //<



    //> @attr listGrid.recordEnabledProperty (string : "enabled" : IR)
    // Property name on a record that will be checked to determine whether a record is enabled.
    // <P>
    // Setting this property on a record will effect the visual style and interactivity of
    // the record.  If set to <code>false</code> the record (row in a +link{ListGrid} or
    // +link{TreeGrid}) will not highlight when the mouse moves over it, nor will it respond to
    // mouse clicks.
    //
    // @see listGridRecord.enabled
    // @example disabledRows
    // @visibility external
    //<

    recordEnabledProperty: "enabled",

    //> @attr listGridRecord.enabled (boolean : null : IR)
    //
    // Default property name denoting whether this record is enabled. Property name may be
    // modified for some grid via +link{listGrid.recordEnabledProperty}.
    //
    // @visibility external
    // @example disabledRows
    //<

    //> @attr listGrid.canExpandRecordProperty (string : "canExpand" : IR)
    // Property name on a record that will be checked to determine whether a record can be
    // expanded.
    //
    // @see listGridRecord.canExpand
    // @group expansionField
    // @visibility external
    //<
    canExpandRecordProperty: "canExpand",

    //> @attr listGridRecord.canExpand (boolean : null : IR)
    //
    // Default property name denoting whether this record can be expanded. Property name may be
    // modified for the grid via +link{listGrid.canExpandRecordProperty}.
    //
    // @group expansionField
    // @visibility external
    //<

    //> @attr listGrid.recordCanRemoveProperty (String : "_canRemove" : IRA)
    // If set to false on a record and +link{ListGrid.canRemoveRecords,canRemoveRecords} is
    // true, removal of that record is disallowed in the UI. The icon in the remove field
    // is not shown.
    // @group  editing
    // @visibility external
    //<
    recordCanRemoveProperty:"_canRemove",

    //> @attr listGridRecord._canRemove (boolean : null : IRW)
    //
    // Default property name denoting whether this record can be removed. Property name may be
    // modified for the grid via +link{listGrid.recordCanRemoveProperty}.
    //
    // @group  editing
    // @visibility external
    //<

    //> @attr listGridRecord.isSeparator (boolean : null : IR)
    //
    // Default property name denoting a separator row.<br>
    // When set to <code>true</code>, defines a horizontal separator in the listGrid
    // object. Typically this is specified as the only property of a record object, since a
    // record with <code>isSeparator:true</code> will not display any values.<br>
    // Note: this attribute name is governed by +link{ListGrid.isSeparatorProperty}.
    // @visibility external
    //<

    //> @attr listGridRecord.customStyle (CSSStyleName : null : IRW)
    // Name of a CSS style to use for all cells for this particular record.
    // <P>
    // Note that using this property assigns a single, fixed style to the record, so rollover
    // and selection styling are disabled.  To provide a series of stateful styles for a record
    // use +link{listGridRecord._baseStyle} instead.
    // <P>
    // See +link{listGrid.getCellStyle()} for an overview of various ways to customize styling,
    // both declarative and programmatic.
    // <P>
    // If this property is changed after draw(), to refresh the grid call
    // +link{listGrid.refreshRow()} (or +link{listGrid.markForRedraw()} if several rows are
    // being refreshed).
    // <P>
    // If your application's data uses the "customStyle" attribute for something else, the
    // property name can be changed via +link{listGrid.recordCustomStyleProperty}.
    //
    // @visibility external
    //<

    //> @attr listGridRecord._baseStyle (CSSStyleName : null : IRW)
    // Name of a CSS style to use as the +link{listGrid.baseStyle} for all cells for this
    // particular record.
    // <P>
    // The styleName specified with have suffixes appended to it as the record changes state
    // ("Over", "Selected" and so forth) as described by +link{listGrid.getCellStyle()}.  For a
    // single, fixed style for a record, use +link{listGridRecord.customStyle} instead.
    // <P>
    // See +link{listGrid.getCellStyle()} for an overview of various ways to customize styling,
    // both declarative and programmatic.
    // <P>
    // If this property is changed after draw(), to refresh the grid call
    // +link{listGrid.refreshRow()} (or +link{listGrid.markForRedraw()} if several rows are
    // being refreshed).
    // <P>
    // If your application's data uses the "_baseStyle" attribute for something else, the
    // property name can be changed via +link{listGrid.recordBaseStyleProperty}.
    //
    // @visibility external
    //<

    //> @attr listGridRecord.singleCellValue (HTML : null : IRW)
    // Default property name denoting the single value to display for all fields of this row.
    // If this property is set for some record, the record will be displayed as a single
    // cell spanning every column in the grid, with contents set to the value of this
    // property.<br>
    // Note: this attribute name is governed by +link{ListGrid.singleCellValueProperty}.
    // @visibility external
    //<


    //> @attr listGridRecord.canDrag (boolean : null : IR)
    //
    // When set to <code>false</code>, this record cannot be dragged. If canDrag is false for
    // any record in the current selection, none of the records will be draggable.
    //
    // @visibility external
    //<

    //> @attr listGridRecord.canAcceptDrop (boolean : null : IR)
    //
    // When set to <code>false</code>, other records cannot be dropped on (i.e., inserted
    // via drag and drop) immediately before this record.
    //
    // @visibility external
    //<

    //> @attr listGridRecord.linkText (string : null : IRW)
    //
    //  The HTML to display in this row for fields with fieldType set to link. This overrides
    //  +link{attr:listGridField.linkText}.
    //
    //  @see type:ListGridFieldType
    //  @see type:FieldType
    //  @see attr:listGridField.linkText
    //  @see attr:listGrid.linkTextProperty
    //  @group  display_values
    //  @visibility external
    //<

    // Animation
    // ---------------------------------------------------------------------------------------
    // These apply to ListGrid grouping, which basically makes the data model into a Tree where animation
    // is applied for folder open/close.

    //> @attr listGrid.animateFolders (Boolean : true : IRW)
    // If true, when folders are opened / closed children will be animated into view.
    // <P>
    // For a ListGrid, this property applies when +link{ListGrid.canGroupBy,grouping} is enabled.
    // @group animation
    // @visibility animation
    //<
    animateFolders:true,

    //> @attr listGrid.animateFolderMaxRows (integer : null : IRW)
    // If +link{animateFolders} is true for this grid, this number can be set to designate
    // the maximum number of rows to animate at a time when opening / closing a folder.
    // <P>
    // For a ListGrid, this property applies when +link{ListGrid.canGroupBy,grouping} is enabled.
    // @see treeGrid.getAnimateFolderMaxRows()
    // @group animation
    // @visibility external
    //<

    //> @attr listGrid.animateFolderTime (number : 100 : IRW)
    // When animating folder opening / closing, if +link{treeGrid.animateFolderSpeed} is not
    // set, this property designates the duration of the animation in ms.
    // <P>
    // For a ListGrid, this property applies when +link{ListGrid.canGroupBy,grouping} is enabled.
    // @group animation
    // @visibility animation
    // @see listGrid.animateFolderSpeed
    //<
    animateFolderTime:100,

    //> @attr listGrid.animateFolderSpeed (number : 3000 : IRW)
    // When animating folder opening / closing, this property designates the speed of the
    // animation in pixels shown (or hidden) per second. Takes precedence over the
    // +link{treeGrid.animateFolderTime} property, which allows the developer to specify a
    // duration for the animation rather than a speed.
    // <P>
    // For a ListGrid, this property applies when +link{ListGrid.canGroupBy,grouping} is enabled.
    // @group animation
    // @visibility animation
    // @see listGrid.animateFolderTime
    //<
    animateFolderSpeed:3000,

    //> @attr listGrid.animateFolderEffect (AnimationAcceleration : null : IRW)
    // When animating folder opening / closing, this property can be set to apply an
    // animated acceleration effect. This allows the animation speed to be "weighted", for
    // example expanding or collapsing at a faster rate toward the beginning of the animation
    // than at the end.
    // <P>
    // For a ListGrid, this property applies when +link{ListGrid.canGroupBy,grouping} is enabled.
    // @group animation
    // @visibility animation
    //<

    //> @attr listGrid.animateRowsMaxTime (number : 1000 : IRW)
    // If animateFolderSpeed is specified as a pixels / second value, this property will cap
    // the duration of the animation.
    // <P>
    // For a ListGrid, this property applies when +link{ListGrid.canGroupBy,grouping} is enabled.
    // @group animation
    // @visibility animation_advanced
    //<
    animateRowsMaxTime:1000,

    // external: doc'd on TreeGrid
    shouldAnimateFolder : function (folder) {
        if (!this.animateFolders || !this.isDrawn()) return false;

        if (this.autoFitData == "vertical" || this.autoFitData== "both") return false;

        var children;
        if (this.data.isFolder(folder)) {
            // Since we are only checking whether there are opened children, don't apply sorting
            // (the dontUseNormalizer parameter is true).
            children = this.data.getOpenList(folder, null, null, null, null, null, null, true);
        }

        // No children - bit arbitrary whether we "animate" or not!

        if (children == null || children.length <= 1) return false;
        return (children.length <= this.getAnimateFolderMaxRows());
    },

    // external: doc'd on TreeGrid
    getAnimateFolderMaxRows : function () {
        var maxRows = this.animateFolderMaxRows;
        if (maxRows == null) {
            var vfRs = this.body ? this.body._getViewportFillRows() : [0,0];
            maxRows = Math.min(75, (vfRs[1]-vfRs[0]) * 3);
        }
        return maxRows
    },


    // DataBinding
    // ----------------------------------------------------------------------------------------

    //>    @attr listGrid.fields (Array of ListGridField : null : [IRW])
    // An array of field objects, specifying the order, layout, formatting, and
    // sorting behavior of each field in the listGrid object.  In ListGrids, the fields
    // array specifies columns.  Each field in the fields array is a ListGridField object.
    // Any listGrid that will display data should have at least one visible field.
    // <p>
    // If +link{ListGrid.dataSource} is also set, this value acts as a set of overrides as
    // explained in +link{attr:DataBoundComponent.fields}.
    //
    // @see    ListGridField
    // @see    setFields()
    // @group databinding
    // @visibility external
    // @example listGridFields
    // @example mergedFields
    //<

    //> @attr listGrid.defaultFields (Array of ListGridField Properties : null : IRA)
    // An array of listGrid field configuration objects.  When a listGrid is initialized, if this
    // property is set and there is no value for the <code>fields</code> attribute, this.fields will
    // be defaulted to a generated array of field objects duplicated from this array.
    // <P>
    // This property is useful for cases where a standard set of fields will be displayed
    // in multiple listGrids - for example a subclass of ListGrid intended to display a particular
    // type of data:<br>
    // In this example we would not assign a single +link{listGrid.fields} array directly to the
    // class via <code>addProperties()</code> as every generated instance of this class would
    // then point to the same fields array object. This would cause unexpected behavior such as
    // changes to the field order in one grid effecting other grids on the page.<br>
    // Instead we could use <code>addProperties()</code> on our new subclass to set
    // <code>defaultFields</code> to a standard array of fields to display. Each generated instance
    // of the subclass would then show up with default fields duplicated from this array.
    // @visibility external
    //<

    //>    @attr    listGrid.dataSource        (DataSource or ID : null : IRW)
    // @include dataBoundComponent.dataSource
    //<

    //> @attr listGrid.autoFetchDisplayMap (Boolean : true : [IRW])
    // If true, for fields where +link{listGridField.optionDataSource} is specified,
    // a valueMap will be automatically created by making a +link{dataSource.fetchData()} call
    // against the specified dataSource and extracting a valueMap from the returned records
    // based on the displayField and valueField.
    // <P>
    // If set to false, valueMaps will not be automatically fetched.  In this case, setting
    // field.optionDataSource is effectively a shortcut for setting optionDataSource on
    // the editor via +link{listGridField.editorProperties}.
    // <P>
    // Can also be disabled on a per-field basis with +link{listGridField.autoFetchDisplayMap}.
    //
    // @group display_values
    // @see listGridField.autoFetchDisplayMap
    // @see listGridField.optionDataSource
    // @visibility external
    //<
    autoFetchDisplayMap:true,

    //> @attr listGrid.warnOnUnmappedValueFieldChange (Boolean : true : IRWA)
    // If a field has +link{listGridField.displayField} specified and has no
    // +link{listGridField.optionDataSource}, this field will display the value from the
    // <code>displayField</code> of each record by default (for more on this behavior
    // see +link{listGridField.optionDataSource}).
    // <P>
    // If such a field is editable, changing the edit value for the field on some record,
    // without updating the edit value for the associated display field on the same record
    // would mean the user would continue to see the unchanged display field value.
    // Developers can resolve this situation by programmatically setting an edit value for
    // the display field as well as the data field, or avoid it by specifying an optionDataSource
    // and ensuring +link{listGrid.autoFetchDisplayMap} is true, or setting an explicit valueMap
    // for the field.
    // <P>
    // By default, when the edit value on a field with a specified displayField and
    // no optionDataSource is set, we log a warning to notify the developer. This warning may
    // be disabled by setting <code>warnOnUnmappedValueFieldChange</code> to <code>false</code>.
    // <P>
    // Note: There are actually a couple of cases in which the system will automatically
    // derive a new display-field value and apply it to the record:
    // <ol><li>If the edit value was changed by a user actually editing the record
    // (rather than a programmatic call to setEditValue()), and the edit-item had
    // a valueMap or optionDataSource set, we automatically pick up the display value from
    // that item and store it as an edit-value for the displayField of the record</li>
    //     <li>If the listGrid has a loaded record in its data set whose valueField value matches
    // the edit value for the valueField, we automatically apply the displayField value from that
    // record as an edit value for the displayField on the newly edited record.</li></ol>
    // In either case, the display value for the record is updated automatically
    // (and the warning would not be logged).
    // @visibility external
    //<
    warnOnUnmappedValueFieldChange:true,

    //or <code>autoFetchDisplayMap</code> is false at
    // the +link{listGrid.autoFetchDisplayMap,listGrid}
    // or +link{listGridField.autoFetchDisplayMap,field} level, the field will display the
    // record value from the +link{

    //> @attr ListGrid.saveLocally (boolean : null : IRA)
    // For grids with a specified +link{ListGrid.dataSource}, this property can be set to
    // <code>true</code> to prevent the grid from attempting to save / retrieve data from the
    // server.  In this case the grid's data should be specified as an array via
    // the +link{listGrid.data} attribute, and the datasource will simply act as a schema to
    // describe the set of fields visible in the grid.
    // +link{listGrid.canEdit,Inline edits}, or removals via the
    // +link{listGrid.canRemoveRecords} mechanism will update the local data array rather than
    // attempting to perform operations against the dataSource.
    // @see useRemoteValidators
    // @visibility external
    // @group databinding
    //<


    //> @attr ListGrid.saveRequestProperties (DSRequest Properties : null : IRWA)
    // For editable grids with a specified +link{listGrid.dataSource}, where
    // +link{listGrid.saveLocally} is false, this attribute may be used to specify standard
    // DSRequest properties to apply to all save operations performed by this grid (whether
    // triggered by user interaction, or explicit saveEdits or saveAllEdits call).
    // <P>
    // An example usage would be to customize the prompt displayed while saving is in progress
    // if +link{listGrid.waitForSave} is true.
    // <P>
    // Note that for more advanced customization of save operations,
    // +link{dataBoundComponent.addOperation} and +link{dataBoundComponent.updateOperation}
    // are available to developers, allowing specification of an explicit +link{operationBinding}
    // for the add / update operation performed on save.
    //
    // @visibility external
    // @group dataBinding
    // @group editing
    //<

    //> @attr ListGrid.useRemoteValidators (boolean : null : IRWA)
    // If +link{listGrid.saveLocally} is specified, but this grid is bound to a DataSource which
    // includes remote field validators, by default edits will be saved synchronously and
    // these validators will not be executed.<br>
    // Set this property to <code>true</code> to ensure these remote validators are called when
    // saving edits in saveLocally mode. Note that since these remote validators need to run on
    // the server, saving with this property set is asynchronous, even though the data that
    // ultimately gets updated is already present on the client.
    // @visibility external
    // @group databinding
    //<

    //>    @attr listGrid.useAllDataSourceFields (boolean : null : IRW)
    // @include dataBoundComponent.useAllDataSourceFields
    // @group databinding
    //<

    //>    @attr listGrid.showDetailFields (Boolean : true : IR)
    // Whether to include fields marked <code>detail:true</code> from this component's
    // <code>DataSource</code>.
    // <P>
    // Fields may also be included directly in this component's <code>fields</code> array in
    // which case they will be present regardless of the <code>detail</code> attribute.
    // <p>
    // When this property is <code>true</code>, <code>ListGrid</code>s will include all
    // detail fields, but they will be initially hidden.  The user may show
    // these fields via the default header context menu (+link{listGrid.showHeaderContextMenu}).
    // Setting this property to false will completely exclude all detail fields from the list
    // grid's fields array, such that they cannot be shown by the user or programmatically.
    // <P>
    // To override the visibility of individual fields, use the standard
    // +link{listGrid.showField()}, +link{listGrid.hideField()} and +link{listGridField.showIf}
    // APIs, for example, set showIf:"true" to show a detail field initially.
    //
    // @group databinding
    // @visibility external
    //<
    showDetailFields:true,

    //> @attr ListGrid.titleField (string : see below : IRW)
    // Best field to use for a user-visible title for an individual record from this grid.
    // If +link{ListGrid.dataSource} is non null, this property may be specified on the
    // dataSource instead.
    // <p>
    // If not explicitly set, titleField looks for fields named "title", "name", and "id"
    // in that order.  If a field exists with one of those names, it becomes the titleField.
    // If not, then the first field is designated as the titleField.
    //  @visibility external
    //<


    //> @attr listGrid.dataProperties (ResultSet : null :IRWA)
    // For databound ListGrids, this attribute can be used to customize the +link{ResultSet}
    // object created for this grid when data is fetched
    // @group databinding
    // @visibility external
    //<

    // Grouping
    // ---------------------------------------------------------------------------------------

    //> @object groupNode
    //
    // An auto-generated subclass of +link{TreeNode} representing the group nodes
    // in a grouped +link{ListGrid}.
    //
    // @see listGrid.groupBy()
    // @treeLocation Client Reference/Grids/ListGrid
    // @group grouping
    // @visibility external
    //<

    //> @attr groupNode.groupMembers (Array of ListGridRecord or GroupNode : see below : R)
    // Array of ListGridRecord that belong to this group, or, for multi-field grouping, array
    // of groupNodes of subgroups under this groupNode.
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<

    //> @attr groupNode.groupTitle (HTML : see below : R)
    // The computed title for the group, which results from +link{listGridField.getGroupTitle()}
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<

    //> @attr groupNode.groupValue (any : see below : R)
    // The value from which groups are computed for a field,
    // which results from +link{listGridField.getGroupValue()}
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<

    //> @attr listGrid.originalData (object : null : R)
    // When grouped, a copy of the original ungrouped data.
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<

    //> @attr listGrid.groupTree (AutoChild Tree : null : R)
    // The data tree that results from a call to  +link{listGrid.groupBy()}.
    // This will be a +link{ResultTree} if +link{listGrid.dataSource} is
    // present, otherwise it will be a +link{Tree}.
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<

    //> @type GroupStartOpen
    // Possible values for the state of ListGrid groups when groupBy is called
    //
    // @value "all" open all groups
    // @value "first" open the first group
    // @value "none" start with all groups closed
    // @visibility external
    //<

    //> @attr listGrid.groupStartOpen (GroupStartOpen | Array : "first" : IRW)
    // Describes the default state of ListGrid groups when groupBy is called.
    //
    // Possible values are:
    // <ul>
    // <li>"all": open all groups
    // <li>"first": open the first group
    // <li>"none": start with all groups closed
    // <li>Array of group values that should be opened
    // </ul>
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<
    groupStartOpen:"first",

    //> @attr listGrid.canCollapseGroup (Boolean : true : IR)
    // Can a group be collapsed/expanded? When true a collapse/expand icon is shown
    // (+link{groupIcon,groupIcon}) and clicking the icon or double-clicking the group title
    // will collapse or expand the group.
    //
    // When false the group icon is not shown and double-clicking on the title does
    // not change group state. Additionally +link{groupStartOpen,groupStartOpen} is
    // initialized to "all".
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<

    canCollapseGroup:true,

    //> @attr listGrid.groupTitleField (String : null : IR)
    // When a list grid is +link{listGrid.groupBy(),grouped}, each group shows
    // under an auto generated header node. By default the title of the group will be
    // shown, with a hanging indent in this node, and will span all columns in the grid.
    // Setting this property causes the titles of auto-generated group nodes to appear as
    // though they were values of the designated field instead of spanning all columns
    // and record values in the designated groupTitleField will appear indented under
    // the group title in a manner similar to how a TreeGrid shows a Tree.
    // <P>
    // Note if +link{listGrid.showGroupSummaryInHeader} is true, the header nodes will not show
    // a single spanning title value by default - instead they will show the summary values for
    // each field. In this case, if groupTitleField is unset, a
    // +link{listGrid.showGroupTitleColumn,groupTitleColumn} can be automatically generated to
    // show the title for each group.
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<

    getGroupTitleField : function () {
        return this.groupTitleField;
    },

    //> @attr listGrid.showGroupTitleColumn (Boolean : true : IR)
    // If this grid is +link{listGrid.groupBy(),grouped} and +link{listGrid.showGroupSummaryInHeader}
    // is true, instead of group header nodes showing up with a single cell value spanning the full
    // set of columns, summaries for each field will show up in the appropriate columns of the
    // header node.
    // <P>
    // In this case there are 2 options for where the group title will show up. Developers may
    // specify an existing field to put the title values into via +link{listGrid.groupTitleField}.
    // If no groupTitleField is specified, this property may be set to <code>true</code>
    // which causes a <code>groupTitleColumn</code> to be automatically generated.
    // Each group header will show the group title in this column (records within the group will
    // not show a value for this column). The column appears in the leftmost position within the
    // grid (unless +link{listGrid.showRowNumbers} is true, in which case this column shows up
    // in the second-leftmost position), and by default will auto-fit to its data.
    // <P>
    // To customize this field, developers may modify
    // +link{listGrid.groupTitleColumnProperties}
    // <smartclient>or
    // +link{listGrid.groupTitleColumnDefaults} at the class level.</smartclient>
    // @visibility external
    //<
    showGroupTitleColumn:true,

    //> @attr listGrid.groupTitleColumnProperties (ListGridField properties : null : IR)
    // Custom properties for the automatically generated <code>groupTitleColumn</code>.
    // <P>
    // See +link{listGrid.showGroupTitleColumn} for an overview of the groupTitleColumn.
    // @visibility external
    //<
    //groupTitleColumnProperties:null,

    //> @attr listGrid.groupTitleColumnDefaults (ListGridField properties : object : IR)
    // Default properties for the automatically generated <code>groupTitleColumn</code>.
    // Default object includes properties to enable autoFitWidth to group title values.
    // <P>
    // To modify the behavior or appearance of this column, developers may set
    // +link{listGrid.groupTitleColumnProperties} at the instance level, or override this
    // object at the class level. If overriding this object, we recommend using
    // +link{class.changeDefaults()} rather than replacing this object entirely.
    // <P>
    // See +link{listGrid.showGroupTitleColumn} for an overview of the groupTitleColumn.
    // @visibility external
    //<
    groupTitleColumnDefaults:{
        canEdit:false,
        canFilter:false,
        canHide:false,
        canReorder:false,
        showDefaultContextMenu:false,
        autoFreeze:true,

        sortNormalizer:function (recordObject,fieldName,context) {
            return recordObject.groupTitle;
        },

        autoFitWidth:true,
        autoFitWidthApproach:"value",
        title:"&nbsp;"
    },

    // We actually show the special group title column if
    // - we're showing the group summary in the header
    // - we have no explicitly specified group title field
    // - the showGroupTitleColumn flag is true
    showingGroupTitleColumn : function () {
        return (this.isGrouped && this.showGroupSummary && this.showGroupSummaryInHeader
                && this.showGroupTitleColumn && this.getGroupTitleField() == null);
    },

    // groupTitleColumnName: This could be modified to display an actual field within the
    // grid data, but the developer might as well use groupTitleField instead.
    // Leaving unexposed for now.
    groupTitleColumnName:"groupTitle",

    getGroupTitleColumn : function () {
        var grid = this;
        var groupTitleColumn = isc.addProperties(
            {   _isGroupTitleColumn:true,
                // 'grid' available through closure
                getAutoFreezePosition: function () { return grid.getGroupTitleColumnPosition() }
            },
            this.groupTitleColumnDefaults,
            this.groupTitleColumnProperties
        );

        if (groupTitleColumn.name == null) {
            groupTitleColumn.name = this.groupTitleColumnName;
        }
        return groupTitleColumn;
    },

    getGroupTitleColumnPosition : function () {
        // This is really just a sanity check - we don't expect to be calling this method when
        // we're not showing the special groupTitleColumn
        if (!this.showingGroupTitleColumn()) return -1;


        var pos = 0;
        if (this.shouldShowRowNumberField()) pos++;
        if (this.shouldShowCheckboxField()) pos++;
        if (this.shouldShowExpansionField()) pos++;
        return pos;
    },

    singleCellGroupHeaders : function () {
        return this._singleCellGroupHeaders(this.showGroupSummary, this.showGroupSummaryInHeader);
    },
    _singleCellGroupHeaders : function (showGroupSummary, showGroupSummaryInHeader) {
        if (this.getGroupTitleField() != null) return false;
        if (showGroupSummary && showGroupSummaryInHeader) return false;

        return true
    },

    //> @attr listGrid.showGroupSummaryInHeader (Boolean : false : IRW)
    // If this grid is +link{listGrid.groupBy(),grouped}, and +link{listGrid.showGroupSummary}
    // is true, setting this property causes field summary values for each group to be displayed
    // directly in the group header node, rather than showing up at the bottom of each
    // expanded group.
    // <P>
    // Note that this means the group header node will be showing multiple field values
    // rather than the default display of a single cell spanning all columns containing the
    // group title. Developers may specify an explicit +link{listGrid.groupTitleField}, or
    // rely on the automatically generated +link{listGrid.showGroupTitleColumn,groupTitleColumn}
    // to have group titles be visible in as well as the summary values.
    // <P>
    // Also note that multi-line group summaries are not supported when showing
    // the group summary in the group header. If multiple
    // +link{listGridField.summaryFunction,field summary functions} are defined for some field
    // only the first will be displayed when this property is set to true.
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<
    showGroupSummaryInHeader:false,

    //> @attr listGrid.showCollapsedGroupSummary (Boolean : false : IRW)
    // Should group summaries be visible when the group is collapsed?
    // <P>
    // This property only applies to +link{listGrid.groupBy(),grouped} grids showing
    // +link{listGrid.showGroupSummary,group summary rows}. When set to true, the
    // group summary row(s) for each group will show up under the group header nodes when
    // the group is collapsed, or at then end of the grouped set of data if the group
    // is expanded.
    // <P>
    // This property has no effect if +link{showGroupSummaryInHeader} is true.
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<
    showCollapsedGroupSummary:false,

    //> @method listGridField.getGroupValue()
    // Return the value which records should be grouped by.
    // <P>
    // All records for which getGroupValue() returns the same value appear in the same
    // group.  Default is the result of +link{listGrid.getCellValue}.
    // <P>
    // While any type of value may be returned, avoiding the use of string values may
    // result in improved performance. In this case, +link{listGridField.getGroupTitle()}
    // may be implemented to map a numeric group value into a legible string.
    //
    // @param   value (any)   raw value for the cell, from the record for the row
    // @param   record (ListGridRecord)
    //   Record object for the cell. Note: If this is a new row that has not been saved, in an
    //   editable grid, it has no associated record object. In this case the edit values will
    //   be passed in as this parameter (see +link{listGrid.getEditValues()})
    // @param   field (Object)    Field object for which to get group value
    // @param   fieldName (String)    The name of the field
    // @param   grid (ListGrid) the ListGrid displaying the cell
    // @return (any) Group value to which this record belongs
    //
    // @see listGrid.groupBy()
    // @see listGridField.getGroupTitle()
    // @group grouping
    // @visibility external
    // @example customGrouping
    //<

    //> @method listGridField.getGroupTitle()
    // Return the title that should be shown to the user for the group with the
    // <code>groupValue</code> passed as a parameter.
    // <P>
    // Default title is the groupValue itself.
    //
    // @group grouping
    //
    // @param   groupValue (any)   the value from the group is created, the result of
    //  +link{listGridField.getGroupValue()}
    // @param   groupNode (groupNode) the node in the grid containing the group.
    // @param   field (Object)    Field object for which to get group value
    // @param   fieldName (String)    The name of the field
    // @param   grid (ListGrid) the ListGrid displaying the cell
    // @return (any) Group value to which this record belongs
    //
    // @see listGrid.groupBy()
    // @see listGridField.getGroupValue()
    // @visibility external
    // @example customGrouping
    //<

    //> @attr listGridField.groupingModes (ValueMap : null : IR)
    // If set, provides several possible styles of grouping that are valid for this field.  For
    // example, a field of type:"date" might be able to be grouped by week, month, or by day of
    // week.
    // <P>
    // If <code>groupingModes</code> are present and
    // +link{listGrid.canGroupBy,grouping is enabled}, the menu for this field includes a
    // submenu of possible grouping modes generated from the <code>groupingModes</code> valueMap.
    // When the user selects a particular grouping mode,
    // +link{listGridField.groupingMode,field.groupingMode} is set to the user's chosen mode,
    // and this choice can be detected via the <code>field</code> parameter to
    // +link{listGridField.getGroupValue()} in order to provide different modes of grouping.
    // <P>
    // The user may also choose to group records without specifying a grouping mode, in this case,
    // the +link{listGridField.defaultGroupingMode} is used.
    // <P>
    // Note that <code>getGroupValue</code>, <code>groupingModes</code> et al can be specified on
    // +link{SimpleType} declarations, and the different grouping modes that are offered
    // automatically for various common types are defined
    // +link{class:GroupingMessages, this way}.
    //
    // @group grouping
    // @visibility external
    //<

    //> @attr listGridField.groupingMode (identifier : null : IR)
    // For a field that allows multiple +link{listGridField.groupingModes,grouping modes},
    // the current grouping mode.
    // <P>
    // This property is set when a user chooses a particular grouping mode, and may be set on
    // ListGrid creation to affect the initial grouping.
    //
    // @group grouping
    // @visibility external
    //<

    //> @attr listGridField.defaultGroupingMode (identifier : null : IR)
    // Default groupingMode used when the user does not specify a mode or grouping is triggered
    // programmatically and +link{listGridField.groupingMode,field.groupingMode} is unset.
    // See +link{listGridField.groupingModes,field.groupingModes}.
    //
    // @group grouping
    // @visibility external
    //<

    //> @attr listGridField.groupPrecision (integer : null : IR)
    // For fields of type:"float" or derived from float, number of digits after the decimal point
    // to consider when grouping.
    // <P>
    // For example, <code>groupPrecision:2</code> indicates that 45.238 and 45.231 group together,
    // but 45.22 and 45.27 are separate.
    // <P>
    // See also +link{listGridField.groupGranularity,groupGranularity} for grouping by broader
    // ranges.
    //
    // @group grouping
    // @visibility external
    //<

    //> @attr listGridField.groupGranularity (integer : null : IR)
    // Granularity of grouping for numeric fields.
    // <P>
    // Groups will be formed based on ranges of values of size <code>groupGranularity</code>.  For
    // example, if groupGranularity were 1000, groups would be 0-1000, 1000-2000, etc.
    //
    // @group grouping
    // @visibility external
    //<

    //> @attr listGridField.canHilite (boolean : null : IRW)
    // Determines whether this field can be hilited.  Set to false to prevent this
    // field from appearing in HiliteEditor.
    //
    // @visibility external
    //<

    //> @attr listGridField.showHilitesInGroupSummary (Boolean : null : IRW)
    // Determines whether hiliting for this field is shown in a group summary.
    // Set to false to prevent this field from showing hilite in a group summary.
    // <P>
    // All hilites in group summary rows can be controlled with the
    // +link{listGrid.showHilitesInGroupSummary} property.
    //
    // @visibility external
    //<

    //> @attr listGridField.canGroupBy (Boolean : true : IRW)
    // Determines whether this field will be groupable in the header context menu.
    //
    // @see listGrid.groupBy()
    // @visibility external
    //<

    //> @attr listGridField.canSortClientOnly (Boolean : false : IRW)
    // When true, this field can only be used for sorting if the data is entirely client-side.
    //
    // @visibility external
    //<

    //> @attr listGridField.showDefaultContextMenu (Boolean : true : IRW)
    // When set to false, this field will not show a context menu in its header.
    //
    // @visibility external
    //<

    //> @attr listGridField.filterOperator (OperatorId : null : IR)
    // With the +link{listGrid.showFilterEditor,FilterEditor} showing, the +link{Operator} to
    // use when matching values for this field.
    // <P>
    // Note that you can set all FilterEditor fields to default to either substring or exact
    // match via +link{listGrid.autoFetchTextMatchStyle,autoFetchTextMatchStyle}, but if you want a mix of exact vs substring
    // match on different fields, you need to use this property, and your ListGrid will produce
    // +link{AdvancedCriteria} rather than the simpler +link{Criteria} format.  This is
    // automatically and transparently handled by the SmartClient Server's SQLDataSource and
    // HibernateDataSource in Power Edition or above, but if you have your own filtering
    // implementation, it will need to be able to handle AdvancedCriteria.
    // @visibility external
    //<

    //> @attr listGridField.canExport (Boolean : null : IR)
    //    Dictates whether the data in this field be exported.  Explicitly set this
    //  to false to prevent exporting.  Has no effect if the underlying
    //  +link{dataSourceField.canExport, dataSourceField} is explicitly set to
    //  canExport: false.
    //
    // @visibility external
    //<

    //> @attr listGridField.exportRawValues (Boolean : null : IR)
    //    Dictates whether the data in this field should be exported raw by
    // +link{listGrid.exportClientData, exportClientData()}.  If set to true for a
    // field, the values in the field-formatters will not be executed for data in this field.
    // Decreases the time taken for large exports.
    //
    // @visibility external
    //<

    //> @attr listGridField.summaryValue (HTMLString : null : IRW)
    // The value to display for a ListGridField when it appears in the +link{listGrid.summaryRow,summaryRow}.  The
    // default for normal fields is null and for special fields, like the +link{listGrid.checkboxField,checkboxField},
    // the default is to show a blank value (a non-breaking space).
    // @visibility external
    //<

    //> @attr listGrid.groupNodeStyle (String : "groupNode" : IRW)
    // The CSS style that +link{listGrid.groupBy,group} rows will have.
    // <P>
    // Note that this is not a +link{listGrid.getBaseStyle(),base style}, so, if this
    // property is set, group nodes will not show stateful styling
    // (different styles for +link{listGrid.showRollOver},
    // +link{listGrid.alternateRecordStyles}, etc). To enable stateful styling for
    // groupNodes, set this property to <code>null</code> and specify a
    // +link{groupNodeBaseStyle}
    //
    // @group grouping
    // @see group:grouping
    // @visibility external
    //<
    groupNodeStyle: "groupNode",

    //> @attr listGrid.groupNodeBaseStyle (String : null : IRW)
    // +link{listGrid.getBaseStyle(),Base style} for +link{listGrid.groupBy,group} rows.
    // <P>
    // Note that this property has no effect if +link{listGrid.groupNodeStyle} is
    // non null.
    //
    // @group grouping
    // @see group:grouping
    // @visibility external
    //<
    groupNodeBaseStyle: null,

    //> @attr listGrid.groupIcon (SCImgURL : "[SKINIMG]/TreeGrid/opener.gif" : IRW)
    // The URL of the base icon for the group icons in this listGrid. Default value may
    // be overridden by the +link{skinning,current skin}.
    //
    // @group grouping
    // @see group:grouping
    // @visibility external
    //<
    groupIcon: "[SKINIMG]/TreeGrid/opener.gif",

    //> @attr listGrid.groupIconSize (Number : 16 : IRW)
    // Default width and height of group icons for this ListGrid.
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<
    groupIconSize: 16,

    //> @attr listGrid.groupIndentSize (Number : 20 : IRW)
    // Default number of pixels by which to indent subgroups relative to parent group.
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @see listGrid.getGroupNodeHTML
    // @visibility external
    //<
    groupIndentSize: 20,

    //> @attr listGrid.groupLeadingIndent (Number : 10 : IRW)
    // Default number of pixels by which to indent all groups.
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @see listGrid.getGroupNodeHTML
    // @visibility external
    //<
    groupLeadingIndent: 10,

    //> @attr listGrid.canGroupBy (Boolean : true : IRW)
    // If false, grouping via context menu will be disabled.
    //
    // @group grouping
    // @see listGrid.groupBy()
    // @visibility external
    //<
    canGroupBy: true,

    //> @attr listGrid.groupByMaxRecords (int : 1000 : IRW)
    // Maximum number of records to which a groupBy can be applied. If there are more records,
    // grouping will not be available via the default header context menu, and calls to
    // +link{listGrid.groupBy()} will be ignored.
    // <P>
    // The maximum exists because ListGrid grouping is performed in-browser, hence requires loading of
    // all records that match the current filter criteria before records can be grouped.  The default
    // maximum represents a number of records which are safe to load in legacy browsers such as Internet
    // Explorer 8 (modern browsers can handle far more), and is also a good upper limit from the
    // perspective of loading data from a database.
    // <P>
    // Going beyond this limit can cause "script running slowly" errors from legacy browsers (as well as
    // high database load).  To build an interface for grouping that handles arbitrary data volume, use
    // a TreeGrid with +link{treeGrid.loadDataOnDemand} with server-side grouping code.
    //
    // @group grouping
    // @see groupBy
    // @visibility external
    //<
    groupByMaxRecords: 1000,

    //> @attr listGrid.groupByAsyncThreshold (int : 50 : IRW)
    // When grouping is requested with this number of records or more, an asynchronous approach is
    // used to avoid the browser showing a "script is running slowly.." message prompting the
    // user to stop execution of JavaScript.
    // <p>
    // Note that +link{groupByMaxRecords} must be set at least as high as +link{groupByAsyncThreshold}
    // or asynchronous grouping will never be used.
    // <p>
    // During asynch grouping, interactivity is blocked and the +link{asynchGroupingPrompt} is shown
    // to the user, then hidden when grouping completes; +link{groupByComplete} then fires.
    // <p>
    // Note that this async processing covers grouping <b>only</b> - it does not cover whole grid or
    // per-group summaries, client-side sort or filter, or other operations that may cause the browser
    // to show the "script is running slowly" prompt when working with very large sets of records in a
    // grid.
    // <p>
    // At this time, there is no generally effective way to avoid this warning dialog appearing with very
    // large datasets in Microsoft's Internet Explorer (IE).  IE's severely flawed detection algorithm for
    // runaway scripts has been shown to interrupt computations after only 0.2 seconds elapsed time
    // even if the computation would have finished in 0.3 seconds.  Optimizations that reduce
    // execution time can sometimes trigger the "script running slowly" dialog sooner.  Since not
    // every operation can reasonably be made asychronous, the current recommendation is to avoid
    // working with overly large datasets until the affected versions of IE are obsoleted.
    // @visibility external
    //<
    groupByAsyncThreshold: 50,

    //> @attr listGrid.showAsynchGroupingPrompt (Boolean : null : IR)
    // If set to false, do not show the +link{asynchGroupingPrompt} dialog during
    // +link{groupByAsyncThreshold,asynchronous grouping}.
    // @visibility external
    //<

    //> @attr listGrid.asynchGroupingPrompt (HTMLString : "${loadingImage}&nbsp;Grouping data..." : IR)
    // The prompt to display while interactivity is blocked during +link{groupByAsyncThreshold,asynchronous grouping}.
    // @group i18nMessages
    // @visibility external
    //<
    asynchGroupingPrompt: "${loadingImage}&nbsp;Grouping data...",

    //> @attr listGrid.isGrouped (boolean : false : R)
    // True if this listgrid is grouped, false otherwise
    //
    // @group grouping
    // @visibility external
    // @see     groupBy
    //<

    //> @attr listGrid.nullGroupTitle (String : '-none-' : IRW)
    // Default alias to use for groups with no value
    //
    // @group grouping
    // @visibility external
    // @see     groupBy
    //<
    nullGroupTitle: "-none-",

    //> @attr listGrid.groupByField (String | Array of String : see below : IR)
    // List of fields to group grid records. If only a single field is used, that field
    // may be specified as a string. After initialization, use +link{listGrid.groupBy()}
    // to update the grouping field list, instead of modifying groupByField directly.
    // @group grouping
    // @visibility external
    // @see groupBy
    // @example dynamicGrouping
    //<


    // ----------------------
    // Value icons
    // The valueIcons object is a mapping between values and image URLs - when specified
    // we show the valueIcon image either next to, or instead of the normal cell value.

    //> @attr listGridField.valueIcons (object : null : IRW)
    // This property is a mapping between data values to +link{SCImgURL, SCImgURLs} for icons to display
    // for those data values.  For example, given a field named "status" with possible values
    // "Normal", "Slow", "Offline", the follow definition would show various icons for that
    // field:
    // <P>
    // <pre>
    // fields : [
    //     { name:"status",
    //       valueIcons: {
    //           Normal : "greenIcon.png",
    //           Slow : "yellowIcon.png",
    //           Offline : "redIcon.png"
    //       }
    //     },
    //     ... other fields ...
    // ]
    // </pre>
    // <P>
    // <code>valueIcons</code> can either be displayed alongside the normal value or can
    // replace the normal field value so that only the icon is shown.  See
    // +link{listGridField.showValueIconOnly}.
    // <P>
    // If inline editing is enabled for this field, editors displayed for this field will also
    // show valueIcons.  This may be overridden by explicitly setting
    // +link{listGridField.editorValueIcons}.
    // <P>
    // Note that the following attributes related to valueIcon styling will also be picked up
    // by the editor from the ListGridField object unless explicitly specified via the
    // equivalent <code>editor_</code> attributes:<br>
    // +link{listGridField.valueIconWidth}<br>
    // +link{listGridField.valueIconHeight}<br>
    // +link{listGridField.valueIconSize}<br>
    // +link{listGridField.valueIconLeftPadding}<br>
    // +link{listGridField.valueIconRightPadding}<br>
    // +link{listGridField.imageURLPrefix}<br>
    // +link{listGridField.imageURLSuffix}
    //
    // @group imageColumns
    // @visibility external
    //<

    //> @attr listGrid.valueIconSize (number : 16 : IRW)
    // Default width and height of value icons for this ListGrid.
    // Can be overridden at the listGrid level via explicit +link{ListGrid.valueIconWidth} and
    // +link{ListGrid.valueIconHeight}, or at the field level via +link{ListGridField.valueIconSize},
    // +link{ListGridField.valueIconWidth} and {ListGridField.valueIconHeight}
    // @visibility external
    // @group imageColumns
    // @see ListGrid.valueIconWidth
    // @see ListGrid.valueIconHeight
    // @see ListGridField.valueIconSize
    //<
    valueIconSize:16,


    //> @attr listGrid.valueIconWidth (number : null : IRW)
    // Width for value icons for this listGrid.
    // Overrides +link{ListGrid.valueIconSize}.
    // Can be overridden at the field level
    // @group imageColumns
    // @visibility external
    //<

    //> @attr listGrid.valueIconHeight (number : null : IRW)
    // Height for value icons for this listGrid.
    // Overrides +link{ListGrid.valueIconSize}.
    // Can be overridden at the field level
    // @group imageColumns
    // @visibility external
    //<

    //> @attr listGridField.valueIconSize (number : null : IRW)
    // Default width and height of value icons in this field.
    // Takes precedence over valueIconWidth, valueIconHeight and valueIconSize specified at
    // the ListGrid level.
    // Can be overridden via +link{ListGridField.valueIconWidth} and {ListGridField.valueIconHeight}
    // @visibility external
    // @group imageColumns
    // @see ListGrid.valueIconSize
    // @see ListGridField.valueIconWidth
    // @see ListGridField.valueIconHeight
    //<

    //> @attr listGridField.valueIconWidth (number : null : IRW)
    // Width for value icons for this listGrid field.
    // Overrides +link{ListGrid.valueIconSize}, +link{ListGrid.valueIconWidth}, and
    // +link{ListGridField.valueIconSize}.
    // @group imageColumns
    // @visibility external
    //<

    //> @attr listGridField.valueIconHeight (number : null : IRW)
    // Height for value icons for this listGrid field.
    // Overrides +link{ListGrid.valueIconSize}, +link{ListGrid.valueIconHeight}, and
    // +link{ListGridField.valueIconSize}.
    // @group imageColumns
    // @visibility external
    //<

    //> @attr   listGridField.valueIconLeftPadding (number : null : IRW)
    // How much padding should there be on the left of valueIcons for this field
    // Overrides +link{listGrid.valueIconLeftPadding}
    // @group imageColumns
    // @see ListGridField.valueIcons
    // @visibility external
    //<

    //> @attr   listGridField.valueIconRightPadding (number : null : IRW)
    // How much padding should there be on the right of valueIcons for this field
    // Overrides +link{listGrid.valueIconRightPadding}
    // @group imageColumns
    // @see ListGridField.valueIcons
    // @visibility external
    //<

    //> @attr listGridField.editorValueIcons (object : null : IRW)
    // When some cell in this field is being edited, setting this property will specify the
    // value icons to display in the cell's editor. If unset, the editor's valueIcons
    // will be determined in the same way as it would be for a static cell.
    // @group imageColumns
    // @visibility external
    //<

    //> @attr listGridField.editorValueIconWidth (number : null : IRW)
    // When some cell in this field is being edited, setting this property will specify the
    // width for value icons in the cell's editor. If unset, the editor's valueIcon width and
    // height will be determined in the same way as it would be for a static cell.
    // @group imageColumns
    // @visibility external
    //<

    //> @attr listGridField.editorValueIconHeight (number : null : IRW)
    // When some cell in this field is being edited, setting this property will specify the
    // height for value icons in the cell's editor. If unset, the editor's valueIcon width and
    // height will be determined in the same way as it would be for a static cell.
    // @group imageColumns
    // @visibility external
    //<

    //> @attr listGridField.showValueIconOnly (boolean : null : IRW)
    // If this field has a valueIcons property specified, setting this property causes
    // the valueIcon for each value to be displayed in the cell without also showing the
    // record's value for the field.
    // <P>
    // If unset the default behavior is to show the icon only if an explicit valueMap is
    // specified as well in addition to a valueIcons map, otherwise show both the valueIcon and
    // value for the cell.
    // <P>
    // Note that if this field is editable +link{FormItem.showValueIconOnly} will be passed
    // through to editors displayed in this field.
    //
    // @group imageColumns
    // @see listGridField.valueIcons
    // @see listGridField.suppressValueIcon
    // @visibility external
    //<

    // NOTE: showValueIconOnly: the use cases are:
    // - represent a value as an icon only to minimize space
    // - show text, but add an icon as decoration, either to all values, or to emphasize some
    //   values for quicker scanning
    // The property 'showValueIconOnly' allows the developer to explicitly show the valueIcon
    // with or without text.  If showValueIconOnly is unset, we make the assumption that:
    // - if the field is *not* constrained to a fixed set of values (has no valueMap), there's
    //   no way to have icons for all the values, so the purpose of the icons is to add
    //   emphasis to certain values [so we show both text and images]
    // - otherwise the developer has an icon for every possible value, so there is no need for
    //   the value to also be displayed - we size the field large enough to accommodate the icon
    //   only, and suppress the text.

    //> @attr   listGridField.suppressValueIcon (boolean : null : IRW)
    // If this field has a valueIcons property specified, setting this property to true will
    // prevent the valueIcon being written out into this field's cells.
    // <P>
    // Note this property may also be set to false to avoid showing the standard
    // +link{listGrid.booleanTrueImage} and +link{listGrid.booleanFalseImage} for fields of type
    // <code>boolean</code>.
    //
    // @group imageColumns
    // @see listGridField.valueIcons
    // @see listGridField.showValueIconOnly
    // @visibility external
    //<

    //> @attr   listGridField.valueIconOrientation (string : null : IRW)
    // If we're showing a valueIcon for this field should it appear to the left or the right
    // of the text?  By default the icon will appear to the left of the textual value -
    // set this to "right" to show the icon on the right of the text.
    // Has no effect if +link{listGridField.showValueIconOnly} is true
    // @visibility external
    // @group imageColumns
    //<


    //> @attr   listGrid.valueIconLeftPadding (number : 2 : IRW)
    // How much padding should there be on the left of valueIcons by default
    // Can be overridden at the field level
    // @group imageColumns
    // @see ListGridField.valueIcons
    // @visibility external
    //<
    valueIconLeftPadding:2,

    //> @attr   listGrid.valueIconRightPadding (number : 2 : IRW)
    // How much padding should there be on the right of valueIcons by default
    // @group imageColumns
    // Can be overridden at the field level
    // @see ListGridField.valueIcons
    // @visibility external
    //<
    valueIconRightPadding:2,

    // ------------
    // Hilite Icons
    // ------------

    //> @attr listGrid.hiliteIcons (Array of String : ["[SKINIMG]/Dialog/notify.png", "[SKINIMG]/Dialog/warn.png", "[SKINIMG]/actions/approve.png"] : IR)
    // @include dataBoundComponent.hiliteIcons
    // @group hiliting
    // @visibility external
    //<

    //> @attr listGrid.hiliteIconPosition (HiliteIconPosition : "before" : IR)
    // @include dataBoundComponent.hiliteIconPosition
    // @group hiliting
    // @visibility external
    //<

    //> @attr listGrid.hiliteIconSize (number : 12 : IRW)
    // @include dataBoundComponent.hiliteIconSize
    // @group hiliting
    // @see hiliteIconWidth
    // @see hiliteIconHeight
    // @see ListGridField.hiliteIconSize
    // @visibility external
    //<

    //> @attr listGrid.hiliteIconWidth (number : null : IRW)
    // @include dataBoundComponent.hiliteIconWidth
    // @group hiliting
    // @visibility external
    //<

    //> @attr listGrid.hiliteIconHeight (number : null : IRW)
    // @include dataBoundComponent.hiliteIconHeight
    // @group hiliting
    // @visibility external
    //<

    //> @attr   listGrid.hiliteIconLeftPadding (number : 2 : IRW)
    // @include dataBoundComponent.hiliteIconLeftPadding
    // @group hiliting
    // @visibility external
    //<

    //> @attr   listGrid.hiliteIconRightPadding (number : 2 : IRW)
    // @include dataBoundComponent.hiliteIconRightPadding
    // @group hiliting
    // @visibility external
    //<

    //> @attr listGridField.hiliteIconPosition (HiliteIconPosition : null : IR)
    // When +link{listGrid.hiliteIcons} are present, where the hilite icon will be placed
    // relative to the field value.  See +link{type:HiliteIconPosition}.
    // Overrides +link{listGrid.hiliteIconPosition}.
    // @group hiliting
    // @visibility external
    //<

    //> @attr listGridField.hiliteIconSize (number : null : IRW)
    // Default width and height of +link{listGrid.hiliteIcons, hilite icons} in this field.
    // Takes precedence over hiliteIconWidth, hiliteIconHeight and hiliteIconSize specified at
    // the component level.
    // Can be overridden via +link{ListGridField.hiliteIconWidth} and
    // +link{ListGridField.hiliteIconHeight}
    // @group hiliting
    // @see ListGrid.hiliteIconSize
    // @see ListGridField.hiliteIconWidth
    // @see ListGridField.hiliteIconHeight
    // @visibility external
    //<

    //> @attr listGridField.hiliteIconWidth (number : null : IRW)
    // Width for hilite icons for this field.
    // Overrides +link{listGrid.hiliteIconSize}, +link{listGrid.hiliteIconWidth}, and
    // +link{ListGridField.hiliteIconSize}.
    // @group hiliting
    // @visibility external
    //<

    //> @attr listGridField.hiliteIconHeight (number : null : IRW)
    // Height for hilite icons for this field.
    // Overrides +link{listGrid.hiliteIconSize}, +link{listGrid.hiliteIconHeight}, and
    // +link{ListGridField.hiliteIconSize}.
    // @group hiliting
    // @visibility external
    //<

    //> @attr   listGridField.hiliteIconLeftPadding (number : null : IRW)
    // How much padding should there be on the left of +link{DataBoundComponent.hiliteIcons, hilite icons}
    // for this field?
    // Overrides +link{listGrid.hiliteIconLeftPadding}
    // @group hiliting
    // @visibility external
    //<

    //> @attr   listGridField.hiliteIconRightPadding (number : null : IRW)
    // How much padding should there be on the right of +link{DataBoundComponent.hiliteIcons, hilite icons}
    // for this field?
    // Overrides +link{listGrid.hiliteIconRightPadding}
    // @group hiliting
    // @visibility external
    //<




    //> @attr   ListGridField.imageURLPrefix (string : null : IRWA)
    // If this field has type [+link{type:ListGridFieldType}] set to <code>"image"</code>
    // and the URL for the image displayed is not absolute, the path of the URL will be relative
    // to this string<br>
    // Alternatively, if this field displays any valueIcons, this prefix will be applied to
    // the beginning of any +link{ListGridField.valueIcons} when determining the
    // URL for the image.
    // @group imageColumns
    // @visibility external
    // @example imageType
    //<

    //> @attr   ListGridField.imageURLSuffix (string : null : IRWA)
    // If any cells in this field are showing a value icon (see: +link{ListGridField.valueIcons})
    // or this is has +link{type:ListGridFieldType} set to <code>"image"</code>, this the value
    // of this property will be appended to the end of the URL for the icon displayed.<br>
    // Typical usage might be to append a file type such as <code>".gif"</code> to the
    // filename of the image.<br>
    // For editable fields, this property will also be passed through to any editors as
    // +link{FormItem.imageURLSuffix}.
    // @group imageColumns
    // @visibility external
    // @example imageType
    //<

    //> @attr   ListGridField.editorImageURLPrefix (string : null : IRWA)
    // When some cell in this field is being edited, this property can be used to apply
    // an explicit +link{FormItem.imageURLPrefix} to the editor in question.
    // This can be used to modify the valueIcons within the editor.<br>
    // If unset, but +link{ListGridField.imageURLPrefix} is specified, that will be used
    // instead.
    // @group editing
    // @visibility external
    //<

    //> @attr   ListGridField.editorImageURLSuffix (string : null : IRWA)
    // When some cell in this field is being edited, this property can be used to apply
    // an explicit +link{FormItem.imageURLSuffix} to the editor in question.
    // This can be used to modify the valueIcons within the editor.<br>
    // If unset, but +link{ListGridField.imageURLPrefix} is specified, that will be used
    // instead.
    // @group editing
    // @visibility external
    //<

      //>    @attr    listGrid.imageSize (number : 16 : IRW)
    // Default size of thumbnails shown for fieldTypes image and imageFile.  Overrideable on a
    // per-field basis via +link{attr:ListGridField.imageSize} or
    // +link{attr:ListGridField.imageWidth}/+link{attr:ListGridField.imageHeight}
    //
    // @group imageColumns
    // @visibility external
    //<
    imageSize: 16,

      //>    @attr    listGridField.imageSize (number : 16 : IRW)
    // Size of images shown for fieldTypes image and imageFile in this field.
    // This setting overrides the global ListGrid default +link{attr:ListGrid.imageSize}.
    // <P>
    // If set to a String, assumed to be a property on each record that specifies the image
    // height.  For example, if <code>field.imageSize</code> is "logoSize",
    // <code>record.logoSize</code> will control the size of the image.
    //
    // @see attr:ListGridField.imageWidth
    // @see attr:ListGridField.imageHeight
    //
    // @group imageColumns
    // @visibility external
    //<

      //>    @attr    listGridField.imageWidth (number : 16 : IRW)
    // Width of images shown for fieldTypes image and imageFile in this field.
    // <P>
    // If set to a String, assumed to be a property on each record that specifies the image
    // width.  For example, if <code>field.imageWidth</code> is "logoWidth",
    // <code>record.logoWidth</code> will control the width of the image.
    //
    // @see attr:ListGrid.imageSize
    // @see attr:ListGridField.imageSize
    // @see attr:ListGridField.imageHeight
    //
    // @group imageColumns
    // @visibility external
    //<

      //>    @attr    listGridField.imageHeight (number : 16 : IRW)
    // Height of image shown for fieldTypes image and imageFile in this field.
    // <P>
    // If set to a String, assumed to be a property on each record that specifies the image
    // height.  For example, if <code>field.imageHeight</code> is "logoHeight",
    // <code>record.logoHeight</code> will control the height of the image.
    //
    // @see attr:ListGrid.imageSize
    // @see attr:ListGridField.imageSize
    // @see attr:ListGridField.imageWidth
    //
    // @group imageColumns
    // @visibility external
    //<

    // ListGridField
    // ---------------------------------------------------------------------------------------

    //  -- Define the 'listGridField' pseudo class for doc

    //>    @object ListGridField
    // An ordinary JavaScript object containing properties that configures the display of
    // and interaction with the columns of a +link{ListGrid}.
    //
    // @see ListGrid.fields
    // @see ListGrid.setFields
    // @treeLocation Client Reference/Grids/ListGrid
    // @visibility external
    //<

    //> @type ListGridFieldType
    // ListGrids format data for viewing and editing based on the <i>type</i> attribute of the
    // field.  This table describes how the ListGrid deals with the various built-in types.
    //
    // @value "text"    Simple text rendering for view.  For editing a text entry field is shown.
    // If the length of the field (as specified by the +link{attr:dataSourceField.length}
    // attribute) is larger than the value specified by +link{attr:listGrid.longTextEditorThreshold}, a
    // text input icon is shown that, when clicked on (or field is focused in) opens a larger
    // editor that expands outside the boundaries of the cell (textarea by default, but
    // overrideable via +link{ListGrid.longTextEditorType}).
    //
    // @value "boolean" For viewing and editing a checkbox is shown with a check mark for the
    // <code>true</code> value and no check mark for the <code>false</code> value. This behavior
    // may be suppressed by setting link{listGridField.suppressValueIcon} for the field. See
    // +link{ListGrid.booleanTrueImage} for customization.
    //
    // @value "integer" A whole number, e.g. <code>123</code>. Consider setting
    // +link{listGridField.editorType,editorType} to use a +link{SpinnerItem}.
    //
    // @value "float" A floating point (decimal) number, e.g. <code>1.23</code>.
    // Consider setting +link{listGridField.editorType,editorType} to use a +link{SpinnerItem}.
    //
    // @value "date" Field value should be a <code>Date</code> instance representing a logical
    // date, with no time of day information.  See +link{group:dateFormatAndStorage} for
    // details of the logical date type and how it is represented and manipulated.
    // <P>
    // Dates will be formatted using +link{listGridField.dateFormatter,ListGridField.dateFormatter}
    // if specified, otherwise
    // +link{ListGrid.dateFormatter,ListGrid.dateFormatter}.
    // If both these attributes are unset, dates are formatted
    // using the standard +link{Date.setShortDisplayFormat(),short display format} for dates.
    // <P>
    // For editing, by default a +link{DateItem} is used with +link{DateItem.useTextField} set
    // to true, providing textual date entry plus a pop-up date picker. The
    // +link{DateItem.dateFormatter, dateFormatter} and +link{DateItem.inputFormat, inputFormat}
    // for the editor will be picked up from the ListGridField, if specified.
    //
    // @value "time" Field value should be a <code>Date</code> instance representing a logical
    // time, meaning time value that does not have a specific day and also has no timezone.  See
    // +link{group:dateFormatAndStorage} for details of the logical time type and how it is
    // represented and manipulated.
    // <P>
    // Times will be formatted using +link{listGridField.timeFormatter,ListGridField.timeFormatter}
    // if specified,
    // otherwise +link{ListGrid.timeFormatter,ListGrid.timeFormatter}.
    // <P>
    // If both these attributes are unset, times are formatted using the standard
    // +link{Time.shortDisplayFormat,short display format} for times.
    // <P>
    // For editing, by default a +link{TimeItem} is used. The
    // +link{TimeItem.timeFormatter, timeFormatter} for the editor will be picked up from
    // the ListGridField, if specified.
    //
    // @value "datetime" Field value should be a <code>Date</code> instance representing a
    // specific date and time value.  See +link{group:dateFormatAndStorage} for details of the
    // datetime type and how it is represented and manipulated.
    // <P>
    // Dates will be formatted using +link{listGridField.dateFormatter,ListGridField.dateFormatter}
    // if specified, otherwise
    // +link{ListGrid.datetimeFormatter,ListGrid.datetimeFormatter}.
    // If both these attributes are unset, dates are formatted
    // using the standard +link{Date.setShortDatetimeDisplayFormat(),short display format} for
    // datetime values.
    // <P>
    // For editing, by default a +link{DateTimeItem} is used, providing textual date entry plus
    // a pop-up date picker.  The +link{DateItem.dateFormatter, dateFormatter} and
    // +link{DateItem.inputFormat, inputFormat} for the editor will be picked up from the
    // ListGridField, if specified.
    //
    // @value "sequence" Same as <code>text</code>
    //
    // @value "link"     Renders a clickable html link (using an HTML anchor tag: &lt;A&gt;).
    // The target URL is the value of the field, which is also the default display value.  You
    // can override the display value by setting +link{attr:listGridRecord.linkText} or
    // +link{attr:listGridField.linkText}.
    // <P>
    // Clicking the link opens the URL in a new window by default.  To change this behavior,
    // you can set <code>field.target</code>, which works identically to the "target"
    // attribute on an HTML anchor (&lt;A&gt;) tag.  See +link{listGridField.target} for more
    // information.
    // <P>
    // In inline edit mode, this type works like a text field.
    // <P>
    // To create a link not covered by this feature, consider using
    // +link{listGridField.formatCellValue()} along with +link{Canvas.linkHTML()}, or simply
    // +link{listGrid.getCellStyle,styling the field} to look like a link, and providing
    // interactivity via +link{listGridField.recordClick,field.recordClick()}.
    //
    // @value "image"   Renders a different image in each row based on the value of the field.  If
    // this URL is not absolute, it is assumed to be relative to
    // +link{ListGridField.imageURLPrefix} if specified. The size of the image is controlled by
    // +link{attr:listGridField.imageSize}, +link{attr:listGridField.imageWidth},
    // +link{attr:listGridField.imageHeight} (and by the similarly-named global default
    // attributes on the ListGrid itself).
    // <P>
    // You can also specify the following attributes on the field: <code>activeAreaHTML</code>, and
    // <code>extraStuff</code> - these are passed to +link{method:canvas.imgHTML} to generate the
    // final URL.
    // <P>
    // Note if you want to display icons <b>in addition to</b> the normal cell value, you
    // can use +link{listGridField.valueIcons,valueIcons} instead.
    //
    // @value "icon" Shows +link{listGridField.icon,field.icon} in every cell, and also in the
    // header.  Useful for a field that is used as a button, for example, launches a detail
    // window or removes a row.  Implement a +link{listGridField.recordClick,field.recordClick}
    // to define a behavior for the button.
    // <P>
    // NOTE: for a field that shows different icons depending on the field value, see
    // +link{listGridField.valueIcons}.
    // <P>
    // <code>type:"icon"</code> also defaults to a small field width, accommodating just the icon
    // with padding, and to a blank header title, so that the header shows the icon only.
    // <P>
    // +link{listGridField.iconWidth,field.iconWidth} and related properties configure
    // the size of the icon both in the header and in body cells.
    // <P>
    // If you want the icon to appear only in body cells and not in the header, set
    // +link{listGridField.cellIcon,field.cellIcon} instead, leaving field.icon null.
    //
    // @value "binary"  For viewing, the grid renders a 'view' icon (looking glass) followed by a
    // 'download' icon and then the name of the file is displayed in text.  If the user clicks the
    // 'view' icon, a new browser window is opened and the file is streamed to that browser
    // instance, using +link{dataSource.viewFile()}.  For images and other file types with
    // known handlers, the content is typically displayed inline - otherwise the browser will
    // ask the user how to handle the content.  If the download icon is clicked,
    // +link{dataSource.downloadFile()} is used to cause the browser to show a "save" dialog.
    // There is no inline editing mode for this field type.
    //
    // @value "imageFile"   Same as <code>binary</code>
    //
    // @value "summary" Show a calculated summary based on other field values within the
    //  current record. See +link{listGridField.recordSummaryFunction} for more information
    //
    // @value "any"       Fields of this type can contain any data value and have no default
    // formatting or validation behavior. This is useful as the
    // +link{SimpleType.inheritsFrom,parent type} for SimpleTypes
    // where you do not want any of the standard validation or formatting logic
    // to be inherited from the standard built-in types.
    //
    // @value "localeInt" An integer number with locale-based formatting, e.g. <code>12,345,678</code>.
    // See +link{group:localizedNumberFormatting,Localized Number Formatting}
    // for more info.
    //
    // @value "localeFloat" A float number with locale-based formatting, e.g. <code>12,345.67</code>.
    // See +link{group:localizedNumberFormatting,Localized Number Formatting}
    // for more info.
    //
    // @value "localeCurrency" A float number with locale-based formatting and using currency
    // symbol, e.g. <code>$12,345.67</code>.
    // See +link{group:localizedNumberFormatting,Localized Number Formatting}
    // for more info.
    //
    // @value "phoneNumber" A telephone number.  Uses +link{formItem.browserInputType} "tel" to
    // hint to the device to restrict input.  On most mobile devices that have
    // software keyboards, this cause a specialized keyboard to appear which
    // only allows entry of normal phone numbers.  When displayed read-only,
    // a "phoneNumber" renders as an HTML link with the "tel:" URL scheme,
    // which will invoke the native phone dialing interface on most mobile
    // devices.  In addition, the CSS style "sc_phoneNumber" is applied.
    // <p>
    // By default, "phoneNumber" fields do not include validators, however the
    // following validator definition would limit to digits, dashes and the
    // "+" character:
    // xml:
    // <p>
    //     &lt;validator type="regexp" expression="^(\(?\+?[0-9]*\)?)?[0-9_\- \(\)]*$"
    //         errorMessage="Phone number should be in the correct format e.g. +#(###)###-##-##" /&gt;
    //     <smartclient>
    // <p>
    // or directly in JavaScript:
    // <p>
    // <pre>
    // {type:"regexp", expression:"^(\\(?\\+?[0-9]*\\)?)?[0-9_\\- \\(\\)]*$",
    //     errorMessage:"Phone number should be in the correct format e.g. +#(###)###-##-##"}
    // </pre>
    // </smartclient>
    // <smartgwt>
    // <p>
    // or directly in Java:
    // <p>
    // <pre>
    // RegExpValidator v = new RegExpValidator();
    // v.setType(ValidatorType.REGEXP);
    // v.setErrorMessage("Phone number should be in the correct format e.g. +#(###)###-##-##");
    // v.setExpression("^(\\(?\\+?[0-9]*\\)?)?[0-9_\\- \\(\\)]*$");
    // </pre>
    // </smartgwt>
    // and adding "#" and "*" to the regular expressions above would allow for
    // users to enter special keys sometimes used for extension numbers or
    // pauses
    //
    // @see attr:listGridField.type
    // @see type:FieldType
    // @visibility external
    // @example gridsDataTypes
    //<

    //> @attr listGridField.type (ListGridFieldType : "text" : [IR])
    //  ListGrids picks a renderer for the view and edit mode of a field based on this attribute.
    //  See +link{ListGridFieldType} for a summary of how types are rendered.
    //
    //  @see type:ListGridFieldType
    //  @see type:FieldType
    //  @group  appearance
    //  @visibility external
    //<

    //> @attr listGridField.name (identifier : null : [IR])
    // Name of this field.  Must be unique within this ListGrid as well as a valid JavaScript identifier,
    // as specified by ECMA-262 Section 7.6 (the <smartclient>+link{String.isValidID()}</smartclient>
    // <smartgwt>StringUtil.isValidID()</smartgwt> function can be used to test whether
    // a name is a valid JavaScript identifier).
    // <P>
    // The name of field is also the property in each record which holds the value for that
    // field.
    // <P>
    // If a +link{listGrid.dataSource} is specified and the DataSource has a field with the
    // same name, the ListGridField and DataSourceField are merged so that properties on the
    // ListGridField
    //
    // @group data
    // @visibility external
    //<

    //> @attr listGridField.dataPath (string : null : [IRA])
    // dataPath for this field. This property allows the grid to display details of nested data
    // structures in a flat list of columns.
    // @group data
    // @visibility external
    //<

    //> @attr listGridField.title (string : null : [IRW])
    // A title for this field, to display in the header for the field and in other
    // contexts such as the +link{listGrid.canPickFields,menu for picking visible fields}.
    // <P>
    // Note: if you want to use HTML tags to affect the display of the header, you should do so
    // via +link{listGridField.headerTitle} instead so that other places where the title
    // appears in the UI are not affected.  For example, you might set <code>headerTitle</code>
    // to an empty string to suppress the header title on a narrow column, but you would retain
    // the normal title in the <code>title</code> property to avoid a blank menu item in the
    // field picker menu, +link{databoundComponent.editHilites,hilite editor} and other contexts.
    // <P>
    // Alternately you can specify a +link{getFieldTitle()} method on the field to return the
    // HTML for the field title.
    //
    // @group  appearance
    // @see method:listGridField.getFieldTitle()
    // @visibility external
    //<

    //> @attr listGridField.showTitle (boolean : null : [IRW])
    // This property may be set to <code>false</code> to explicitly suppress display of
    // the field title in the column header button for the field.
    // @visibility external
    //<

    //> @method listGridField.getFieldTitle()
    // If your derivation of the field title is more complex than specifying a static string,
    // you can specify a getFieldTitle() method on your field to return the title string.
    // Otherwise you can use the +link{title} attribute on the field to specify the title.
    // <P>
    // You can use +link{listGrid.setFieldProperties,setFieldProperties()} to dynamically
    // update the title.
    //
    // @param viewer (ListGrid) pointer back to the ListGrid
    // @param fieldNum (number) index of this field in the grid's fields array.
    // @return    (string) Field title.
    // @group appearance
    // @see attr:listGridField.title
    // @visibility external
    //<

    //> @attr listGridField.wrap (Boolean : null : [IRW])
    // Should the field title wrap if there is not enough space horizontally to
    // accommodate it. (Note that this is a soft-wrap - if set the title will wrap
    // at word boundaries).
    // <P>
    // If unset, default behavior is derived from +link{listGrid.wrapHeaderTitles}.
    //
    // @visibility external
    //<

    //> @attr listGridField.hoverWrap (Boolean : null : IRW)
    // This property may be set to customize the <code>wrap</code> attribute for the
    // canvas shown when the mouse hovers over cells in this field. Note that this causes a
    // soft-wrap - if set, the hover text will wrap at word boundaries.
    // <P>
    // If unset, default behavior is derived from +link{listGrid.headerHoverWrap}.
    // @visibility external
    //<

    //> @attr listGridField.hoverWidth (Integer : null : IRW)
    // Specifies the width of the canvas shown when the mouse hovers over cells in this field.
    // <P>
    // If unset, default behavior is derived from +link{listGrid.headerHoverWidth}.
    // @visibility external
    //<

    //> @attr listGridField.target (string : "_blank" : IRW)
    // By default, clicking a link rendered by this item opens it in a new browser window.  You
    // can alter this behavior by setting this property.  The value of this property will be
    // passed as the value to the <code>target</code> attribute of the anchor tag used to render
    // the link.
    // <P>
    // If you set listGridField.target to "javascript", the default behavior is to catch and
    // consume mouse-clicks that would result in the link being followed.  Instead, the
    // +link{listGrid.cellClick()} event is fired for the containing cell.
    //
    // @visibility external
    //<

//> @method listGridField.showIf()
// An optional +link{group:stringMethods,stringMethod} which if provided, is evaluated to
    // conditionally determine whether this field should be displayed.
    // Evaluated on initial draw, then reevaluated on explicit
// calls to <code>listGrid.refreshFields()</code> or <code>listGrid.setFields()</code>.
    // <P>
    // Use <code>+link{listGridField.hidden,hidden}:true</code> or <code>showIf:"false"</code>
    // to set a ListGrid field to initially hidden.<br>
    // The user will still be able to show the field via a context menu.
    // This may be suppressed by setting +link{listGridField.canHide} to false, or by
    // setting +link{listGrid.canPickFields} to false to suppress the
    // field-picker entirely.
    // <P>
    // Note that explicit calls to +link{listGrid.showField,grid.showField()} or hideField()
    // will wipe out the <code>showIf</code> expression, as will the end user showing and
    // hiding columns via the +link{listGrid.showHeaderContextMenu,header contextMenu}.
    // <P>
    // Also note that fields marked as +link{DataSourceField.detail,detail:true} will be hidden by
    // default even if +link{ListGrid.showDetailFields} is <code>true</code>. To show detail fields
    // inherited from a DataSource, include an explicit field definition for the field and
    // set this property to return <code>true</code>.
    //
    // @param list (ListGrid) A pointer to the listGrid containing the field
    // @param field (ListGridField) the ListGridField object
    // @param fieldNum (integer) the index of the field
    // @return (boolean) whether the field should be shown
    //
    // @group appearance
    // @see method:ListGrid.refreshFields
    // @visibility external
    //<

    //> @attr listGridField.hidden  (Boolean : null : IR)
    // Marks field as initially hidden.<br>
    // The user will still be able to show the field via a context menu.
    // This may be suppressed by setting +link{listGridField.canHide} to false, or by
    // setting +link{listGrid.canPickFields} to false to suppress the
    // field-picker entirely.
    // <p>
    // To mark a field as completely hidden (not shown to a user at all, in any component), set
    // +link{DataSourceField.hidden} instead.
    // <P>
    // See +link{listGrid.discardEditsOnHideField} and +link{listGrid.dropCriteriaOnHideField}
    // for behavior when hiding fields while +link{listGrid.canEdit,editing}.
    //
    // @group appearance
    // @visibility external
    //<

    //> @attr listGridField.frozen (boolean : null : IR)
    // Whether this field should be "frozen" for the purposes of horizontal scrolling.  See
    // +link{group:frozenFields}.
    // @group frozenFields
    // @visibility external
    //<

    //> @attr listGridField.canFreeze (boolean : null : IR)
    // Whether this field should display freezing/unfreezing options in its header context menu.
    // See +link{group:frozenFields}.
    // @see method:listGrid.getHeaderContextMenuItems()
    // @group frozenFields
    // @visibility external
    //<

    //> @attr listGridField.autoFreeze (boolean : null : IR)
    // Whether this field should be automatically frozen when other fields are frozen.  When
    // true, the field will be automatically frozen to the extreme of the grid.  The
    // automatically generated +link{listGrid.checkboxField, checkbox},
    // +link{listGrid.expansionField, expansion} and
    // +link{listGrid.rowNumberField, rowNumber} fields are examples of fields that specify
    // <code>autoFreeze: true</code>.
    // <P>
    // You can control the position of this field in the array of frozen fields by providing a
    // +link{listGridField.getAutoFreezePosition} implementation.
    // @group frozenFields
    // @visibility external
    //<

    //> @method listGridField.getAutoFreezePosition()
    // When a field has +link{listGridField.autoFreeze,autoFreeze} set to true, developers can
    // implement this method to indicate where in the frozen-fields array this field should
    // appear.
    // <P>
    // Some automatically generated fields, such as
    // +link{listGrid.rowNumberField, rowNumberField},
    // +link{listGrid.expansionField, expansionField} and
    // +link{listGrid.checkboxField, checkboxField}, provide default implementations of this
    // method.
    // @return (number) the index at which this autoFreeze field should appear in the frozen body
    // @group frozenFields
    // @visibility external
    //<

    //> @attr listGridField.canHide (boolean : null : IR)
    // If set to false, this field will be omitted from the column picker that appears in the
    // header context menu when +link{listGrid.canPickFields} is enabled.  This means that the
    // end user will not be able to hide it if it's currently shown, or show it if it's
    // currently hidden.
    // <P>
    // If this property is set to <code>false</code>, and the
    // +link{listGrid.useAdvancedFieldPicker,advanced field picker} is shown, if the field
    // is +link{hidden}, the field will not show in the list of available fields. If the
    // field is visible, it will be displayed in the list of currently visible fields, but
    // the advanced field picker user interface will disallow hiding it.
    //
    // @see method:listGrid.getHeaderContextMenuItems()
    // @group appearance
    // @visibility external
    //<

    //> @attr listGridField.canDragResize (boolean : null : IR)
    // Whether this field can be dragResized using the mouse.  If unset, the default behavior
    // is governed by +link{listGrid.canResizeFields}.
    // @visibility external
    //<

    //> @attr listGridField.canReorder (boolean : null : IR)
    // Whether this field can be reordered using the mouse.  If unset, the default behavior is
    // governed by +link{listGrid.canReorderFields}.  Note that setting this property to
    // <code>false</code> will lock this field from being moved - that is, the user is
    // prevented from moving this field directly by dragging with the mouse, or by dropping another
    // field onto this field.  However, unless this field is at the extreme edge of the grid,
    // or all fields between it and the extreme edge of the grid are also
    // <code>canReorder: false</code>, (ie, if there are unlocked fields on either side of this
    // field), then it is possible that this locked field may be
    // reordered automatically, as a result of the user dragging one unlocked field onto another
    // unlocked field.
    // @visibility external
    //<

    //> @attr listGridField.ignoreKeyboardClicks (boolean : null : IRW)
    // If the user is navigating through the grid using the keyboard, record click or double click
    // events may be generated via keyboard interactions (see +link{listGrid.generateClickOnSpace},
    // +link{listGrid.generateClickOnEnter}, +link{listGrid.generateDoubleClickOnSpace},
    // +link{listGrid.generateDoubleClickOnEnter} and +link{listGrid.arrowKeyAction}).
    // <P>
    // These synthetic events have both a target row and column.
    // Setting this flag to true ensures that this field will never be considered the target for
    // a keyboard click event.
    // @group events
    // @visibility external
    //<



    //> @attr listGridField.excludeFromState
    // @include dataSourceField.excludeFromState
    // @see ListGrid.getViewState()
    //<

    // Grid, Group and Record-level summaries
    // ---------------------------------------------------------------------------------------

    //> @attr listGridField.showGridSummary (Boolean : null : IR)
    // If +link{listGrid.showGridSummary} is true, should this field show a summary value.
    // If unset, this field will show a summary value in the summary row if an
    // explicit +link{listGridField.summaryFunction} is specified or if a
    // +link{SimpleType.getDefaultSummaryFunction(),default summary function} is defined
    // for the specified field type.
    // @visibility external
    //<

    //> @attr listGridField.showGroupSummary (boolean : null : IR)
    // If +link{listGrid.showGroupSummary} is true, should this field show a summary value
    // in a summary row when the grid is grouped?
    // If unset, this field will show a summary value in the summary row if an
    // explicit +link{listGridField.summaryFunction} is specified or if a
    // +link{SimpleType.getDefaultSummaryFunction(),default summary function} is defined
    // for the specified field type.
    // @visibility external
    //<

    //> @attr listGridField.summaryFunction (SummaryFunction  or Array of SummaryFunction : null : IR)
    // If +link{listGrid.showGridSummary} or +link{listGrid.showGroupSummary} is true,
    // this attribute can be used to specify
    // an explicit +link{type:SummaryFunction} for calculating the summary value to
    // display.
    // <P>
    // If an array of summaryFunctions is specified, they will be executed in turn and the
    // grid will show multiple summary rows at the grid or group level (or both)
    // containing the resulting values.
    // @visibility external
    //<

    //> @attr listGridField.summaryValueTitle (String : null : IR)
    // If +link{listGrid.showGridSummary} or +link{listGrid.showGroupSummary} is true and the
    // +link{listGridField.summaryFunction} is set to <code>"title"</code>, this attribute may be
    // set to a string to display in the group and/or grid summary. If unspecified the
    // +link{listGridField.title} will show up in the summary.
    // @visibility external
    //<

    //> @method listGridField.getGridSummary() [A]
    // If +link{listGrid.showGridSummary} is true, and this method is specified it will be
    // called to generate the summary value to be displayed in the grid summary row. Note that
    // this is called instead of making use of the +link{listGridField.summaryFunction}.
    // <P>
    // As with +link{listGrid.getGridSummary()} this method may return an array of results -
    // in this case each result will show up in a separate row in the +link{listGrid.summaryRow}
    // grid.
    // <P>
    // If this grid is grouped, and +link{listGrid.showGroupSummary} is true, this method
    // will be passed a third parameter - an array of group-level summaries.
    // @param records (Array of ListGridRecord) records for which a summary is being generated
    // @param field (ListGridField) pointer to the field for which summary value is being generated
    // @param [groupSummaries] (Array of objects) If this grid is grouped and
    //  +link{listGrid.showGridSummary} is specified, this parameter contains an array of already-
    //  calculated summary values for each group in the grid. Each element in this array will
    //  be an object containing calculated summary values for each field in the grid, as well as
    //  a specified groupValue and groupName, allowing the developer to determine which group this
    //  summary value comes from
    // @return (any) summary value to display.
    // @visibility external
    //<

    //> @attr listGridField.formatGridSummary (stringMethod : null : IR)
    // Optional stringMethod to format the summary value displayed
    // in the +link{listGrid.showGridSummary,grid summary}.
    // Takes a single parameter <code>value</code> and should return the formatted version
    // of that value. If specified this will be applied instead of any formatting logic applied
    // via +link{listGridField.formatCellValue()}, +link{listGrid.formatCellValue()}, etc.
    // <P>
    // Note that for fields with a specified summary function of "count", if no custom formatting
    // is applied, we default to formatting the count value by appending
    // <code>field.pluralTitle</code> if defined, otherwise <code>field.title</code> to the
    // numeric count value returned by the standard count function. To change this behavior for
    // such fields, specify an explicit 'formatGridSummary' and/or 'formatGroupSummary' method
    // @visibility external
    //<

    //> @method listGridField.getGroupSummary() [A]
    // If +link{listGrid.showGroupSummary} is true, and this method is specified it will be
    // called to generate the field summary value to be displayed for each group level summary row.
    // Note that this is called instead of making use of the +link{listGridField.summaryFunction}.
    // <P>
    // This method may return an array of results - in this case the group will show multiple summary
    // rows, with each entry in the array showing up in a different record.
    //
    // @param records (Array of ListGridRecord) records for which a summary is being generated
    //  (so all records in the group).
    // @param field (ListGridField) pointer to the field for which summary value is being generated
    // @param [groupNode] (object) object with specified groupValue and groupName for this group
    // @return (any) summary value to display
    // @visibility external
    //<

    //> @attr listGridField.formatGroupSummary (stringMethod : null : IR)
    // Optional stringMethod to format the group level summary values for this field displayed via
    // +link{listGrid.showGroupSummary}.
    // Takes a single parameter <code>value</code> and should return the formatted version
    // of that value.  If specified this will be applied instead of any formatting logic applied
    // via +link{listGridField.formatCellValue()}, +link{listGrid.formatCellValue()}, etc.
    // <P>
    // Note that for fields with a specified summary function of "count", if no custom formatting
    // is applied, we default to formatting the count value by appending
    // <code>field.pluralTitle</code> if defined, otherwise <code>field.title</code> to the
    // numeric count value returned by the standard count function. To change this behavior for
    // such fields, specify an explicit 'formatGridSummary' and/or 'formatGroupSummary' method
    // @visibility external
    //<

    //> @method listGridField.getRecordSummary() [A]
    // Only applies to +link{listGridFieldType,summary-type} fields. If specified, this
    // method will be called to generate the record summary value to be displayed for each row
    // in this field.  When this method is called, current values for other
    // +link{listGridFieldType,summary-type} fields have not yet been stored on the record, so
    // you must recalculate such fields yourself (and possibly cache them in a common location)
    // if needed.
    // <P>
    // Note that if specified, this is called instead of making use of the
    // +link{listGridField.recordSummaryFunction}.
    // <P>
    // If +link{listGrid.showGridSummary} or +link{listGrid.showGroupSummary} is true, this
    // field's value in the summary row[s] will still be calculated by calling this method.
    // In this case, the record object passed in will contain summary values for each field.
    // If custom handling is required for this case, it may be detected by checking the
    // record object's +link{listGridRecord.isGroupSummary} and +link{listGridRecord.isGridSummary}
    // attributes.
    // @param record (ListGridRecord) record for which a summary is being generated
    // @param field (ListGridField) pointer to the summary type field
    // @return (any) summary value to display
    // @visibility external
    //<

    //> @attr listGridField.recordSummaryFunction (RecordSummaryFunction : null : IR)
    // Only applies to fields of type <code>"summary"</code>.
    // Fields of this type will display a calculated value based on the other field values
    // within the current record.
    // <P>
    // This attribute specifies how the summary field value will be calculated. See
    // +link{type:RecordSummaryFunction} for valid options.
    // <P>
    // A subset of the ListGrid's fields will be passed to the RecordSummaryFunction.
    // Which fields to include is determined based on +link{listGridField.includeInRecordSummary}
    // <P>
    // If +link{listGrid.showGridSummary} or +link{listGrid.showGroupSummary} is true, this
    // field's value in the summary row[s] will still be calculated by calling this method.
    // In this case, the record object passed in will contain summary values for each field.
    // If custom handling is required for this case, it may be detected by checking the
    // record object's +link{listGridRecord.isGroupSummary} and +link{listGridRecord.isGridSummary}
    // attributes.
    // @visibility external
    //<

    //> @attr listGridField.partialSummary (boolean : null : IR)
    // Only applies to fields of type <code>"summary"</code>.
    // This attribute is set on a summary field, when calculating the summary value from
    // some record, the summary function will only be passed the fields before this summary field.
    // This may be useful for displaying running totals across a record.
    // <P>
    // Note that this feature would typically be used with
    // +link{listGrid.canReorderFields,canReorderFields:false}
    // @visibility external
    //<

    //> @attr listGridField.includeInRecordSummary (boolean : null : IR)
    // If a listGrid is showing a field of type summary, should this field be passed to the
    // recordSummaryFunction when calculating the summary value to display.
    // If unset, fields are included if they are of type "integer" or "float" only (since most
    // summary functions perform numeric calculations). See also
    // +link{listGridField.includeInRecordSummaryFields}.
    // @visibility external
    //<

    //> @attr listGridField.includeInRecordSummaryFields (array of fieldNames : null : IR)
    // If this listGrid has any fields of type <code>"summary"</code> and
    // this field will be +link{listGridField.includeInRecordSummary,included} in summary calculations
    // by default, this attribute provides an opportunity to explicitly specify which summary fields
    // the record should be displayed in.
    // <P>
    // Specified as an array of fieldNames. If set, this field value will only be included for
    // record summary value calculations for summary fields whos name is included in this array.
    // @visibility external
    //<

    //> @attr listGridField.applyAfterSummary (Boolean : null : IRW)
    // If +link{listGridField.userFormula} is set for this field, and this grid is showing
    // +link{listGrid.showGroupSummary,group summaries} or a
    // +link{listGrid.showGridSummary,grid summary}, this property determines what field value
    // should be present in those summary rows. Should the field apply the user-formula to the
    // calculated summary row, or should it apply a standard grid or group summary to the
    // user-formula values displayed in the grid?
    // <P>
    // Default behavior may be specified at the grid level via +link{listGrid.applyFormulaAfterSummary}
    // @visibility external
    //<

    // Header button icons
    // ---------------------------------------------------------------------------------------
    // Include all relevant docs from StatefulCanvas

    //> @attr listGridField.icon (SCImgURL: null : [IR])
    // Optional icon to show next to the title for this field.
    // Should be set to a URL to an image. Relative paths will be evaluated starting at
    // the imgDir of this component. This URL is partial - it may be updated to indicate
    // the current disabled (etc) state of the field.
    // <p>
    // If +link{listGridField.type,field.type} is set to "icon", this icon will also be shown
    // in every cell of this field - see also +link{listGridField.cellIcon,field.cellIcon}.
    // <p>
    // To change this property after fields have been passed to +link{listGrid.setFields()},
    // use +link{listGrid.setFieldIcon()}.
    //
    // @visibility external
    //<

    //> @attr listGridField.iconSize (integer : null : [IR])
    // If +link{listGridField.icon} is specified, this property can be used to specify the
    // size of the icon to be displayed in the ListGrid header button.
    // (See +link{StatefulCanvas.iconSize})
    // @see listGridField.icon
    // @visibility external
    //<

    //> @attr listGridField.iconWidth (integer : null : [IR])
    // If +link{listGridField.icon} is specified, this property can be used to specify the
    // width of the icon to be displayed in the ListGrid header button.
    // (See +link{StatefulCanvas.iconWidth})<br>
    // If this field is editable, and +link{ListGridField.editorIconWidth} is unset, this
    // property will be passed onto the editors for this field as +link{FormItem.iconWidth},
    // which will effect the default size for +link{ListGridField.icons, icons} displayed
    // in the editor.
    // @see listGridField.icon
    // @see listGridField.icons
    // @visibility external
    //<

    //> @attr listGridField.iconHeight (integer : null : [IR])
    // If +link{listGridField.icon} is specified, this property can be used to specify the
    // height of the icon to be displayed in the ListGrid header button.
    // (See +link{StatefulCanvas.iconHeight})<br>
    // If this field is editable, and +link{ListGridField.editorIconHeight} is unset, this
    // property will be passed onto the editors for this field as +link{FormItem.iconWidth},
    // which will effect the default size for +link{ListGridField.icons, icons} displayed
    // in the editor.
    // @see listGridField.icon
    // @see listGridField.icons
    // @visibility external
    //<

    //> @attr listGridField.iconOrientation (string : "left" : [IR])
    // If this field is showing an icon, should it appear to the left or right of the title?<br>
    // Valid options are <code>"left"</code> or <code>"right"</code>
    // @see listGridField.icon
    // @visibility external
    //<
    // iconOrientation JS doc not included from statefulCanvas as that refers to
    // setIconOrientation(), and we don't have an exposed way to get at the ListGrid field
    // header button at runtime.

    //> @attr listGridField.iconSpacing (int : 6 : [IR])
    // @include statefulCanvas.iconSpacing
    // @see listGridField.icon
    // @visibility external
    //<

    //> @attr listGridField.showDisabledIcon (Boolean : true : [IR])
    // @include statefulCanvas.showDisabledIcon
    // @see listGridField.icon
    // @visibility external
    //<

    //> @attr listGridField.showRollOverIcon (Boolean : false : [IR])
    // @include statefulCanvas.showRollOverIcon
    // @see listGridField.icon
    // @visibility external
    //<

    //> @attr listGridField.showFocusedIcon (Boolean : false : [IR])
    // @include statefulCanvas.showFocusedIcon
    // @see listGridField.icon
    // @visibility external
    //<

    //> @attr listGridField.showDownIcon (Boolean : false : [IR])
    // @include statefulCanvas.showDownIcon
    // @see listGridField.icon
    // @visibility external
    //<

    //> @attr listGridField.showSelectedIcon (Boolean : false : [IR])
    // @include statefulCanvas.showSelectedIcon
    // @see listGridField.icon
    //  @visibility external
    //<

    //> @attr listGridField.cellIcon (SCImgURL : null : [IR])
    // For a field of type:"icon" only, set the icon that appears in body cells.  Unless
    // setting +link{listGridField.icon,field.icon}, setting field.cellIcon will not show an
    // icon in the header.
    // <p>
    // To change this property after fields have been passed to +link{listGrid.setFields()},
    // use +link{listGrid.setFieldCellIcon()}.
    //
    // @visibility external
    //<

    //> @attr listGridField.showFileInline (boolean : null : [IR])
    // For a field of type:"imageFile", indicates whether to stream the image and display it
    // inline or to display the View and Download icons.
    //
    // @visibility external
    //<

    //> @attr listGridField.format (FormatString : null : IR)
    // +link{FormatString} for numeric or date formatting.  See +link{dataSourceField.format}.
    // @group exportFormatting
    // @visibility external
    //<

    //> @attr listGridField.exportFormat (FormatString : null : IR)
    // +link{FormatString} used during exports for numeric or date formatting.  See
    // +link{dataSourceField.exportFormat}.
    // @group exportFormatting
    // @visibility external
    //<

    // FormItem icons
    // ---------------------------------------------------------------------------------------

    //> @attr listGridField.icons (Array of FormItemIcon Properties: null : [IRA])
    // If this field is editable, this property can be used to specify
    // +link{FormItem.icons, icons} to be displayed in the editors displayed for this field
    // @group editing
    // @visibility external
    //<

    //> @attr listGridField.editorIconWidth (number : null : [IRA])
    // If this field is editable, this property will be passed to editors displayed for
    // cells within this field as +link{FormItem.iconWidth}.<br>
    // If this property unset, the iconWidth property from the editor can be picked up from
    // +link{listGridField.iconWidth} instead.
    // @see listGridField.icons
    // @group editing
    // @visibility external
    //<

    //> @attr listGridField.editorIconHeight (number : null : [IRA])
    // If this field is editable, this property will be passed to editors displayed for
    // cells within this field as +link{FormItem.iconHeight}.<br>
    // If this property unset, the iconHeight property from the editor can be picked up from
    // +link{listGridField.iconHeight} instead.
    // @see listGridField.icons
    // @group editing
    // @visibility external
    //<

    //> @attr listGridField.defaultIconSrc (string : null : [IRA])
    // If this field is editable, this property will be passed to editors displayed for
    // cells within this field as +link{FormItem.defaultIconSrc}.
    // @see listGridField.icons
    // @group editing
    // @visibility external
    //<

    //> @attr listGridField.iconPrompt (string : null : [IRA])
    // If this field is editable, this property will be passed to editors displayed for
    // cells within this field as +link{FormItem.iconPrompt}.
    // @see listGridField.icons
    // @group editing
    // @visibility internal
    //<

    //> @attr listGridField.iconHSpace (string : null : [IRA])
    // If this field is editable, this property will be passed to editors displayed for
    // cells within this field as +link{FormItem.iconHSpace}.
    // @see listGridField.icons
    // @group editing
    // @visibility internal
    //<

    //> @attr listGridField.iconVAlign (string : null : [IRA])
    // If this field is editable, this property will be passed to editors displayed for
    // cells within this field as +link{FormItem.iconVAlign}.
    // @see listGridField.icons
    // @group editing
    // @visibility external
    //<

    // editor picker icon

    //> @attr listGridField.showPickerIcon (boolean : null : [IRA])
    // If this field is editable, this property will be passed to editors displayed for
    // cells within this field as +link{FormItem.showPickerIcon}.
    // @group editing
    // @visibility pickerIcon
    //<

    //> @attr listGridField.pickerIconSrc (string : null : [IRA])
    // If this field is editable, this property will be passed to editors displayed for
    // cells within this field as +link{FormItem.pickerIconSrc}.
    // @group editing
    // @visibility pickerIcon
    //<

    //> @attr listGridField.pickerIconWidth (integer : null : [IRA])
    // If this field is editable, this property will be passed to editors displayed for
    // cells within this field as +link{FormItem.pickerIconWidth}.
    // @group editing
    // @visibility pickerIcon
    //<

    //> @attr listGridField.pickerIconHeight (integer : null : [IRA])
    // If this field is editable, this property will be passed to editors displayed for
    // cells within this field as +link{FormItem.pickerIconHeight}.
    // @group editing
    // @visibility pickerIcon
    //<

    // Summary Title
    // ---------------------------------------------------------------------------------------

    //> @attr listGridField.summaryTitle (string : null : [IRWA])
    // Optional long summary title for this field, provided in addition to
    // +link{listGridField.title}. This gives the developer an option to use a very short,
    // or empty title for the ListGrid column (where space may be a factor), but have a longer
    // value available to be used elsewhere.
    // <p>
    // By default this value will be used for the title of the context-menu item
    // for showing/hiding the listGrid field when the user right-clicks on the ListGrid header.
    //
    // @group appearance
    // @see attr:listGridField.title
    // @deprecated Rather than customizing the summaryTitle developers should typically use
    //  the +link{listGridField.headerTitle} attribute to show a different
    //  title in the column header button than the title used elsewhere.
    // @visibility external
    //<

    //> @method listGridField.getSummaryTitle() [A]
    // Optional string method to return a long summary title for this field, if a dynamic
    // summary title is required for this field.
    //
    // @param viewer (listGrid) pointer back to the ListGrid
    // @param field (listGridField) pointer to the field object
    // @group appearance
    // @see attr:listGridField.summaryTitle
    // @see attr:listGridField.title
    // @deprecated Rather than customizing the summaryTitle developers should typically use
    //  the +link{listGridField.headerTitle} attribute to show a different
    //  title in the column header button than the title used elsewhere.
    // @visibility external
    //<

    // Header Appearance
    // ---------------------------------------------------------------------------------------

    //> @attr listGridField.width (Number or String : "*" : [IRW])
    // The width of this field, specified as either an absolute number of pixels,
    // a percentage of the remaining space like "25%", or "*" to split remaining space among
    // all fields which have "*".
    // <P>
    // See also +link{listGrid.minFieldWidth} to ensure no field goes below a minimum size.
    // <P>
    // Use +link{listGrid.resizeField} to programmatically change field width after creation.
    // <P>
    // Use +link{listGrid.getFieldWidth} to access the rendered field width after
    // the ListGrid is drawn.
    //
    // @group appearance
    // @visibility external
    //<

    //> @attr   listGridField.align (Alignment : null : [IRW])
    // Horizontal alignment for field's column header: "left", "right"
    // or "center". Applied to the column header title and cells by default. A separate
    // alignment for cells can be specified via +link{listGridField.cellAlign}.<br>
    // If null, values are left-aligned. If this field is editable, the
    // alignment of cells in the body will also be reflected in any editors for the field.
    //  @group  appearance
    //  @visibility external
    //<

    //> @attr listGridField.headerBaseStyle (CSSClass : null : [IRW])
    // Custom base style to apply to this field's header button instead of
    // +link{listGrid.headerBaseStyle}.<br>
    // Note that depending on the header button constructor, you may have to override
    // +link{listGridField.headerTitleStyle} as well.
    // @group appearance
    // @visibility external
    //<

    //> @attr listGridField.headerTitleStyle (CSSClass : null : [IRW])
    // Custom titleStyle to apply to this field's header button instead of
    // +link{listGrid.headerTitleStyle}.<br>
    // Note that this will typically only have an effect if
    // +link{listGrid.headerButtonConstructor} is set to +link{class:StretchImgButton} or a subclass
    // thereof.
    // @group appearance
    // @visibility external
    //<

    //> @attr listGridField.headerTitle (string : null : IR)
    // Optional title for the header button for this field. If specified this will be
    // displayed in the header button instead of +link{listGridField.title} or
    // +link{listGridField.name}. Set to an empty string to suppress the title in the
    // header button entirely.
    // @group appearance
    // @see listGridField.title
    // @visibility external
    //<



    // Header Spans
    // ---------------------------------------------------------------------------------------
    // - known limitations
    //   - can't reorder a column to before or after a spanned set of columns, if the spanned
    //   columns are at the start or end of the visible fields.
    //   - several uses of this.Super(), instead of the faster this.invokeSuper() approach.
    //   Attempt to use invokeSuper() failed, likely because the header is not a discrete
    //   class, but an instance of Toolbar, and my guess (Alex) is that Class.invokeSuper()
    //   doesn't handle this particular case.


    //> @attr listGrid.headerSpans (Array of HeaderSpan : null : IRW)
    // Header spans are a second level of headers that appear above the normal ListGrid headers,
    // spanning one or more listGrid fields in a manner similar to a column-spanning cell in an
    // HTML table.
    // <P>
    // A header span can be created by simply naming the fields the header should span.  The
    // example below creates a headerSpan that spans the first two fields of the ListGrid.
    // <smartclient>
    // <pre>
    //    isc.ListGrid.create({
    //        headerHeight:40,
    //        fields : [
    //            { name:"field1" },
    //            { name:"field2" },
    //            { name:"field3" }
    //        ],
    //        headerSpans : [
    //            {
    //                fields: ["field1", "field2"],
    //                title: "Field 1 and 2"
    //            }
    //        ]
    //    });
    // </pre>
    // </smartclient>
    // <smartgwt>
    // <pre>
    //      ListGrid grid = new ListGrid();
    //      grid.setHeaderHeight(40);
    //      grid.setFields(new ListGridField[] {
    //          new ListGridField("field1"),
    //          new ListGridField("field2"),
    //          new ListGridField("field3")
    //      });
    //      grid.setHeaderSpans(new HeaderSpan[] {
    //          new HeaderSpan("Field 1 and 2", new String[] {"field1", "field2"})
    //      });
    // <pre>
    // </smartgwt>
    // Header spans can be nested, allowing fields to be grouped by multiple levels of
    // granularity. See +link{headerSpan.spans} for further information on nesting spans.
    // <P>
    // Header spans will automatically react to resizing of the headers they span, and will be
    // hidden automatically when all of the spanned fields are hidden.
    // <P>
    // Header spans appear in the +link{listGrid.header,header} area of the ListGrid, sharing space
    // with the existing headers, so it's typical to set +link{listGrid.headerHeight} to
    // approximately double its normal height when using headerSpans, or if using nested header
    // spans, the default header height multiplied by the number of levels of header spans to be
    // shown.
    // <P>
    // See +link{headerSpan} for many properties that allow the control of the appearance of
    // headerSpans.
    // <smartclient>
    // Note that headerSpans are created via the +link{AutoChild} pattern, hence
    // you can change the SmartClient component being used, or any of its properties.
    // </smartclient>
    // <P>
    // Neither headerSpans themselves nor the fields within them may be drag reordered, but other
    // unspanned headers may be.
    // <P>
    // A span can only span adjacent fields - if a span is defined and the spanned fields don't
    // sit next to each other in the specified fields array, the fields array will be automatically
    // reorded to match the order specified in the span's +link{headerSpan.fields} array.
    // <P>
    // Note that headerSpans primarily provide a visual cue for grouping multiple headers
    // together.  If you have an OLAP, data "cube" or multi-dimensional data model, the
    // +link{CubeGrid} component is the right choice.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr listGrid.headerSpanHeight (integer : null : IR)
    // Default height for a +link{listGrid.headerSpans,headerSpan} with no height specified.
    // <P>
    // If <code>headerSpanHeight</code> is not specified (the default), headerSpans will be 1/2
    // of +link{listGrid.headerHeight}.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr listGrid.headerSpanVAlign (vAlign : "center" : IR)
    // Default alignment for +link{listGrid.headerSpans,headerSpans} with no
    // +link{headerSpan.valign} specified.
    //
    // @group headerSpan
    // @visibility external
    //<
    headerSpanVAlign: "center",

    //> @attr listGrid.unspannedHeaderVAlign (vAlign : null : IR)
    // When +link{listGrid.headerSpans,headerSpans} are in use, this property sets the default
    // vertical alignment for for fields which do <b>not</b> have a headerSpan.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr listGrid.headerSpanConstructor (SCClassName : null : IR)
    // +link{SCClassName,SmartClient Class} to use for headerSpans.  Typically a +link{Button} or
    // +link{StretchImgButton} subclass.
    // <P>
    // If unset, headerSpans will be created using the +link{listGrid.headerButtonConstructor}.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr listGrid.headerSpan (MultiAutoChild StatefulCanvas : null : IR)
    // +link{listGrid.headerSpans,headerSpans} are created via the +link{AutoChild} pattern, hence
    // <code>headerSpanConstructor</code>, <code>headerSpanDefaults</code> and
    // <code>headerSpanProperties</code> are valid.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @object HeaderSpan
    // A header span appears as a second level of headers in a ListGrid, spanning one or more
    // ListGrid columns and their associated headers.
    // <P>
    // See +link{listGrid.headerSpans}.
    // <P>
    // In addition to the properties documented here, all other properties specified on the
    // headerSpan object will be passed to the +link{Class.create,create()} method of the
    // +link{listGrid.headerSpanConstructor}.  This allows you to set properties such as
    // +link{button.baseStyle} or +link{stretchImgButton.src} directly in a
    // <code>headerSpan</code>.
    //
    // @group headerSpan
    // @treeLocation Client Reference/Grids/ListGrid
    // @visibility external
    //<

    //> @attr headerSpan.name (identifier : null : IR)
    // Name for this headerSpan, for use in APIs like +link{listGrid.setHeaderSpanTitle()}.
    // <P>
    // Name is optional, but if specified, must be unique for this ListGrid (but not globally
    // unique) as well as a valid JavaScript identifier, as specified by ECMA-262 Section 7.6
    // (the <smartclient>+link{String.isValidID()}</smartclient><smartgwt>StringUtil.isValidID()</smartgwt>
    // function can be used to test whether a name is a valid JavaScript identifier).
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr headerSpan.fields (Array of String : null : IR)
    // List of fields that this header spans.  Fields should be identified by their value for
    // +link{listGridField.name}.
    // <P>
    // Developers may define multiple levels of header-spans by specifying +link{headerSpan.spans}
    // however a span cannot be specified with both <code>fields</code> and <code>spans</code>.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr headerSpan.spans (Array of HeaderSpan : null : IR)
    // This property allows developer to "nest" header spans, grouping fields together by
    // multiple layers of granularity.
    // <P>
    // For example a group of fields could be nested within two layers of header spans as follows:
    // <smartclient>
    // <pre>
    // { title:"Europe", spans:[
    //      {title:"France", fields:["Paris", "Lyon"]},
    //      {title:"UK", fields:["London", "Glasgow"]},
    //      {title:"Spain", fields:["Barcelona"]}
    //  ]
    // }
    // </pre>
    // </smartclient>
    // <smartgwt>
    // <pre>
    //      HeaderSpan france = new HeaderSpan("France", new String[] {"Paris", "Lyon"});
    //      HeaderSpan uk = new HeaderSpan("UK", new String[] {"London", "Glasgow"});
    //      HeaderSpan spain = new HeaderSpan("Spain", new String[] {"Barcelona"});
    //
    //      HeaderSpan europe = new HeaderSpan();
    //      europe.setTitle("Europe");
    //      europe.setSpans(france, uk, spain);
    // </pre>
    // </smartgwt>
    // Note that a span definition can not include both <code>spans</code>
    // and +link{headerSpan.fields,fields}.
    // @group headerSpan
    // @visibility external
    //<

    //> @attr headerSpan.title (String : null : IR)
    // A title for this headerSpan, to display in the headerSpan button for this headerSpan
    // and in other contexts such as the +link{listGrid.canPickFields,menu for picking visible fields}.
    //
    // Note: if you want to use HTML tags to affect the display of the header, you should do so
    // via +link{headerSpan.headerTitle} instead so that other places where the title
    // appears in the UI are not affected.  Refer to discussion at +link{listGridField.title}.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr headerSpan.headerTitle (String : null : IR)
    // Optional title for the headerSpan button for this headerSpan. If specified this will be
    // displayed in the headerSpan button instead of +link{headerSpan.title}. Set to an empty
    // string to suppress the title in the header button entirely.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr headerSpan.headerBaseStyle (CSSClass : null : [IRW])
    // Custom base style to apply to the header button created for this span instead
    // of +link{listGrid.headerBaseStyle}.
    // <P>
    // Note that depending on the header button constructor, you may have to specify
    // +link{headerSpan.headerTitleStyle} as well.
    // @group appearance
    // @visibility external
    //<

    //> @attr headerSpan.headerTitleStyle (CSSClass : null : [IRW])
    // Custom titleStyle to apply to the header button created for this span instead of
    // +link{listGrid.headerTitleStyle}.
    // <p>
    // Note that this will typically only have an effect if
    // +link{listGrid.headerButtonConstructor} is set to +link{class:StretchImgButton} or a subclass
    // thereof.
    // @see headerSpan.headerBaseStyle
    // @group appearance
    // @visibility external
    //<

    //> @attr headerSpan.height (integer : null : IR)
    // Height of this headerSpan.  Defaults to +link{listGrid.headerSpanHeight}.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr headerSpan.valign (VerticalAlignment: null : IR)
    // Vertical alignment of the title of this headerSpan.
    // <P>
    // Defaults to listGrid.headerSpanVAlign if unset.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr headerSpan.align (Alignment: "center" : IR)
    // Horizontal alignment of the title of this headerSpan.
    //
    // @group headerSpan
    // @visibility external
    //<

    //> @attr listGrid.showTreeColumnPicker (Boolean : true : IR)
    // When +link{listGrid.headerSpans} are in use, whether to show a hierarchical column picker
    // that includes both headerSpans and normal headers, with normal headers indented under
    // headerSpans similarly to how a +link{TreeGrid} displays a Tree.
    // <P>
    // If <code>showTreeColumnPicker</code> is false, no column picker will be shown on the
    // headerSpan itself, and the column picker for a clicked on a normal field header will include
    // only normal fields.
    //
    // @group headerSpan
    // @visibility external
    //<
    showTreeColumnPicker: true,


    // Cell Styling
    // ---------------------------------------------------------------------------------------

    //> @attr listGridField.cellAlign (Alignment : null : [IRW])
    // Horizontal alignment for cells in this field's column: "left", "right"
    // or "center".<br>
    // If null, alignment is derived from +link{ListGridField.align}. If this field is editable,
    // the alignment of cells in the body will also be reflected in any editors for the field.
    //  @group  appearance
    //  @visibility external
    //<

    //> @attr listGrid.reverseRTLAlign (Boolean : true : [IRW])
    // If a page is rendered in +link{isc.Page.isRTL(),RTL mode}, should
    // cell alignments specified +link{listGridField.cellAlign} be reversed (so
    // an <code>align:"right"</code> field will have content aligned on the left and
    // vice versa)?
    // <P>
    // This is true by default to match user expectation that text flows from
    // start-to end and is aligned with the start of text flow (left in LTR mode,
    // right in RTL mode) by default. May be set to false to have the specified
    // alignments be taken literally in RTL mode.
    // @group RTL
    // @visibility external
    //<
    reverseRTLAlign:true,


    //> @attr   listGridField.baseStyle (CSSClass : null : [IRW])
    //  Custom base style to apply to all cells in this field instead of +link{ListGrid.baseStyle}
    //  To override the baseStyle at the row level, use
    //  +link{ListGrid.recordBaseStyleProperty, record[listGrid.recordBaseStyleProperty]}
    //  instead.
    // @see ListGrid.recordBaseStyleProperty
    // @group appearance
    // @visibility external
    // @example gridCells
    //<

    // Sorting (per field)
    // ---------------------------------------------------------------------------------------

    //> @attr   listGridField.canSort (Boolean : true : [IRW])
    //  Enables or disables sorting by this column. If false, interactive
    //  sorting via header-clicks or menu-items will have no effect, but direct scripted calls
    //  to +link{ListGrid.sort, sort()} or +link{ListGrid.setSort, setSort()} will work as
    //  expected.
    //  @group  sorting
    //  @see    method:ListGrid.sort
    //  @see    attr:ListGrid.canSort
    //  @visibility external
    //<

    //> @attr listGridField.sortDirection (SortDirection : null : [IRW])
    // Specifies the default sorting direction for this column. If specified on the
    // +link{listGrid.sortField,default sort field} for the listGrid, sorting occurs
    // automatically, otherwise this will be the default direction when the user clicks the
    // field header, or calls +link{ListGrid.sort()} without specifying an explicit sort
    // direction.
    // <P>
    // Overrides ListGrid.sortDirection
    //  @group  sorting
    //  @see type:SortDirection
    //  @visibility external
    //<

    //> @method listGridField.sortNormalizer() (A)
    // Optional function to return the value that should be used when sorting this field.
    // <P>
    // Note that, if the dataset exceeds +link{ListGrid.dataPageSize} and hence paging is
    // introduced, the grid relies on the server to provide sorting, and the sortNormalizer
    // will no longer be called.
    //
    // @param recordObject    (object)    record to normalize
    // @param fieldName       (string)    name of the field on which sorting occurred
    // @param context (ListGrid) A pointer back to the list grid displaying this field will
    //   be available as the <code>context</code> argument. Note that you can also get a pointer
    //   to the field definition object by calling <code>context.getField(fieldName)</code>
    //  @return (any)   normalized value for sorting
    //  @group  sorting
    //  @visibility external
    //  @example dataTypes
    //<

    // Editing (per field)
    // ----------------------------------------------------------------------------------------

    //> @attr listGridField.canEdit (boolean : null : [IRW])
    // Can this field be edited?  May be overridden by setting the 'canEdit' property at the
    // listGrid level. If not explicitly set and this grid is bound to a dataSource, the
    // +link{listGrid.canEditFieldAttribute} may be used to set default editability at the field level.
    // <p>
    // For more dynamic control over whether fields can be edited, see
    // +link{listGrid.canEditCell}.
    //
    // @group  editing
    // @see attr:listGrid.canEdit
    // @see attr:listGrid.recordEditProperty
    // @see method:listGrid.canEditCell
    // @visibility external
    // @example disableEditing
    //<


    //> @attr listGridField.alwaysShowEditors (boolean : null : [IRW])
    // When this attribute is set, editors will be rendered into every row of the grid for
    // this field, rather than showing up in a single record at a time.
    // This attribute is only valid when +link{listGrid.editByCell} is false
    // @group editing
    //<



    //> @attr listGridField.defaultValue (any : null : [IRW])
    // If this field +link{listGridField.canEdit, can be edited}, this property can be used to
    // specify a default value for this field's editor when adding new rows to the grid.
    // @see listGrid.startEditingNew()
    // @group editing
    // @visibility external
    //<

    //> @method listGridField.defaultDynamicValue()
    // If this field +link{listGridField.canEdit, can be edited}, this property can be used to
    // set a dynamic default value which will show up in editors for this field.
    // Will be applied to the editor for the field as +link{FormItem.defaultDynamicValue}
    //
    // @param    item    (FormItem)  The editor for the cell itself (also available as "this").
    //                              Note that in addition to the standard FormItem APIs available
    //                              on the editor, it also has:<br>
    //                              - a pointer back to the containing listGrid
    //                              [<code>item.grid</code>]<br>
    //                              - the colNum being edited [<code>item.colNum</code>]<br>
    //                              - the rowNum being edited [<code>item.rowNum</code>]
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param   values  (Object)      the current set of values for the form as a whole
    // @group editing
    // @visibility external
    //<

    //> @attr   listGridField.enterKeyEditAction (EnterKeyEditAction : "done" : [IRW])
    // What to do when a user hits enter while editing this field?<br>
    // Overrides the <code>enterKeyEditAction</code> as specified at the listGrid level while
    // focus is in this field.
    //  @group  editing
    //  @visibility external
    //<

    //> @attr   listGridField.escapeKeyEditAction (EscapeKeyEditAction : "cancel" : [IRW])
    // What to do when a user hits escape while editing this field?<br>
    // Overrides the <code>escapeKeyEditAction</code> as specified at the listGrid level while
    // focus is in this field.
    //  @group  editing
    //  @visibility external
    //<


    //> @attr   listGridField.nextTabColNum (number : null : [IRWA])
    // If specified, when the user hits tab while editing this field, editing will move to the
    // specified colNum in the next row (or the first editable field after it), rather than the
    // next editable field in this row.
    //  @group  editing
    //  @visibility advancedInlineEdit
    //<

    //> @attr   listGridField.previousTabColNum (number : null : [IRWA])
    // If specified, when the user hits shift+tab while editing this field, editing will move
    // to the specified colNum in the previous row (or the first editable field before it),
    // rather than the previous editable field in this row.
    //  @group  editing
    //  @visibility advancedInlineEdit
    //<

    //> @attr   listGridField.editorType (FormItem className : null : [IRWA])
    //      Name of form item class to use for the form item created to edit this field.
    //      (Only used if this field is editable).<br>
    //      Note: If this is not specified, the edit-form item type may be derived from the
    //      <code>editorType</code> property, typically inherited from datasource fields, or
    //      from the <code>type</code> of the field (showing the appropriate form item for
    //      the data-type). See the +link{group:editing} overview for more on editing ListGrid
    //      fields.
    //  @group  editing
    //  @see attr:listGrid.canEdit
    //  @visibility external
    //  @example customEditors
    //<
    // link to editing group documentation included as that describes the additional
    // "rowNum", "colNum" and "grid" properties stored on the editor.

    //> @attr   listGridField.editorProperties (FormItem properties : null : [IRWA])
    // Properties to apply the the form item created to edit this field. (Only used if
    // this field is editable).
    // <P>
    // For example, if you have a field "shoeSize" with +link{dataSourceField.editorType} set
    // to "SpinnerItem" in order to use a SpinnerItem as your field editor, and you want to pass the
    // +link{spinnerItem.step} property to the created SpinnerItem:
    // <pre>
    //    fields : [
    //        { name:"shoeSize", editorType:"SpinnerItem",
    //          editorProperties : { step:0.5 } },
    //        ... other fields ...
    //    ]
    // </pre>
    //
    // @group editing
    // @visibility external
    // @example customEditors
    //<

    //> @attr   listGrid.modalEditing (boolean : null : [IRWA])
    //      If this property is true, any mouse click outside of the open cell editors
    //      will end editing mode, hiding the cell editors and saving any changes to those
    //      cell values.
    // @group  editing
    // @visibility external
    // @example modalEditing
    //<

    //> @method listGridField.editorEnter (A)
    // Callback fired when the user first starts editing a cell.
    // <P>
    // This callback is typically used to establish dynamic default values via
    // +link{listGrid.setEditValue()} or +link{listGrid.setEditValues()}.
    //
    // @param record (ListGridRecord) record for the cell being edited.  <b>Will be null</b>
    //                                for a new, unsaved record.
    // @param value (any) value for the cell being edited
    // @param rowNum (int)    row number for the cell
    // @param colNum (int)    column number of the cell
    // @param grid (ListGrid) ListGrid to which this field belongs
    // @group editing
    // @visibility external
    //<

    //> @method listGridField.editorExit (A)
    // Callback fired when the user attempts to navigate away from the current edit cell,
    // or complete the current edit.<br>
    // Return false from this method to cancel the default behavior (Saving / cancelling the
    // current edit / moving to the next edit cell)
    //
    // @param   editCompletionEvent (EditCompletionEvent)  What interaction triggered this
    //                                                          edit cell exit
    // @param    record     (ListGridRecord)    record for the cell being edited
    // @param    newValue   (any)    new value for the cell being edited
    // @param    rowNum       (int)    row number for the cell
    // @param    colNum       (int)    column number of the cell
    // @param   grid    (ListGrid)  ListGrid to which this field belongs
    // @return  (boolean)   Returning false from this method will cancel the default behavior
    //                      (for example saving the row) and leave the editor visible and focus
    //                      in this edit cell.
    //  @group  editing
    //  @see listGrid.editorExit
    // @visibility external
    //<

    //> @method listGridField.cellChanged()
    // Callback fired when field changes value as the result of a cell edit.  Fired only on
    // successful save of edit, when the new value doesn't match the value before editing.<br>
    // <p>
    // Same signature as +link{method:listGrid.cellChanged()}, but defined on a per-field
    // basis.
    //
    // @param    record     (ListGridRecord)    record for the cell being changed
    // @param    newValue   (any)    new value for the cell
    // @param    oldValue   (any)    old value for the cell
    // @param    rowNum       (number)    row number for the cell
    // @param    colNum       (number)    column number of the cell
    // @param    grid       (ListGrid)    grid where cell was changed.
    //
    // @group  editing
    // @see method:listGrid.cellChanged()
    // @visibility external
    //<

    //> @attr listGridField.validators (Array of Validator : null : [IRW])
    // Array of +link{class:Validator} objects for this field.  When the user edits cells in
    // this field, these validators will be applied to the edited value.<br>
    // Note: for databound listGrids, this property may be specified on the
    // +link{class:DataSourceField}, enabling both client and server side validation.
    // @see class:Validator
    // @see listGridField.required
    // @group gridValidation
    // @visibility external
    // @example dataValidation
    //<

    //> @attr listGridField.validateOnChange (boolean : null : [IRW])
    // If set to true, any +link{listGridField.validators} for this field will be run whenever
    // the value of the field is changed.
    // <P>
    // Analogous to the +link{FormItem.validateOnChange} property.
    // @group gridValidation
    // @visibility external
    //<

    //> @attr listGridField.required (boolean : null : [IRW])
    // When the user edits cells in this field, is this value required to be non-empty
    // in order for validation to pass.<br>
    // Note: for databound listGrids, this property may be specified on the
    // +link{class:DataSourceField}, enabling both client and server side validation.
    //
    // @see listGridField.validators
    // @group gridValidation
    // @visibility external
    //<

    //> @attr listGridField.displayFormat (varies : null : [IRWA])
    // For fields of type <code>"date"</code>, set this property to a valid
    // +link{dateDisplayFormat} to specify how the date should be formatted.<br>
    // For fields of type <code>"time"</code>, set this property to a valid
    // +link{type:TimeDisplayFormat, TimeDisplayFormat} to specify how the time should be formatted.<br>
    // If unset, display format may be set at the listGrid level via +link{ListGrid.dateFormatter}
    // or +link{ListGrid.timeFormatter}.
    // <p>
    // If this field is editable the displayFormat will also be passed to the editor created
    // to edit this field.  For dates you may also need to set +link{listGridField.inputFormat}.
    //
    // @see listGridField.inputFormat
    // @see listGrid.dateFormatter
    // @see listGrid.timeFormatter
    // @deprecated Use +link{listGridField.dateFormatter} and +link{listGridField.timeFormatter}
    //  instead.
    // @visibility external
    //<

    //> @attr listGridField.dateFormatter (DateDisplayFormat : null : [IRW])
    // Display format to use for date type values within this field.
    // <P>
    // The +link{listGridField.timeFormatter} may also be used to format underlying Date values as
    // times (ommitting the date part entirely). If both <code>dateFormatter</code> and
    // <code>timeFormatter</code> are specified on a field, for
    // fields specified as +link{listGridField.type,type "time"} the
    // <code>timeFormatter</code> will be used, otherwise the <code>dateFormatter</code>
    // <P>
    // If <code>field.dateFormatter</code> and <code>field.timeFormatter</code> is unspecified,
    // date display format may be defined at the component level via
    // +link{ListGrid.dateFormatter,ListGrid.dateFormatter}, or for fields of type <code>"datetime"</code>
    // +link{ListGrid.datetimeFormatter,ListGrid.datetimeFormatter}. Otherwise the
    // default is to use the system-wide default short date format, configured via
    // +link{Date.setShortDisplayFormat()}.  Specify any valid +link{type:DateDisplayFormat} to
    // change the format used by this item.
    // <P>
    // If this field is editable the dateFormatter will also be passed to the editor created
    // to edit this field. Note that you can also specify an explicit +link{listGridField.inputFormat}
    // which will be passed through to the editor as well, though this is not typically required
    // as the input format should be automatically derived by the SmartClient system
    // for standard DateDisplayFormats.
    //
    // @see listGrid.dateFormatter
    // @see listGrid.datetimeFormatter
    // @see listGridField.timeFormatter
    // @visibility external
    //<

    //>    @attr listGridField.timeFormatter (TimeDisplayFormat : null : [IRWA])
    // Time-format to apply to date type values within this field.  If specified, any
    // dates displayed in this field will be formatted as times using the appropriate format.
    // This is most commonly only applied to fields specified as type <code>"time"</code> though
    // if no explicit +link{listGridField.dateFormatter} is specified it will be respected for other
    // fields as well.
    // <P>
    // If unspecified, a timeFormatter may be defined
    // +link{ListGrid.timeFormatter,at the component level} and will be respected by fields
    // of type <code>"time"</code>.
    // <P>
    // If this field is editable, the timeFormatter will also be passed to the editor created to
    // edit this field as +link{formItem.timeFormatter}.
    //
    // @group appearance
    // @visibility external
    //<
    //timeFormatter:null

    //> @attr listGridField.decimalPrecision (number : null : [IRW])
    // @include dataSourceField.decimalPrecision
    //
    // @group appearance
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr listGridField.decimalPad (number : null : [IRW])
    // @include dataSourceField.decimalPad
    //
    // @group appearance
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr listGridField.inputFormat (DateInputFormat : null : [IRWA])
    // For fields of type <code>"date"</code> or <code>"datetime"</code>, if this is an editable
    // listGrid, this property allows you to specify the +link{DateItem.inputFormat, inputFormat}
    // applied to the editor for this field.
    // @see listGridField.dateFormatter
    // @visibility external
    //<

    //> @attr listGridField.isRemoveField (boolean : null : [IRA])
    // If set to true and +link{listGrid.canRemoveRecords} is true, this field will be rendered
    // as the remove-field for this grid. In most common usage scenarios this field will essentially
    // be a placeholder indicating where the remove field should be rendered, meaning properties
    // other than <code>isRemoveField</code>, such as <code>name</code> or <code>title</code>, may
    // be left unset.
    // @see listGrid.canRemoveRecords
    // @visibility external
    //<

    //> @method listGridField.recordClick()
    //
    // Executed when this field is clicked on.  Note that if +link{ListGrid.recordClick()} is
    // also defined, it will be fired for fields that define a recordClick handler if the
    // field-level handler returns true. Return false to prevent the grid-level handler from firing.
    //
    // @param    viewer        (ListGrid)    the listGrid that contains the click event
    // @param    record        (ListGridRecord)    the record that was clicked on
    // @param    recordNum    (number)    number of the record clicked on in the current set of
    //                                  displayed records (starts with 0)
    // @param    field        (ListGridField)    the field that was clicked on (field definition)
    // @param    fieldNum    (number)    number of the field clicked on in the listGrid.fields
    //                                  array
    // @param    value       (any)    value of the cell (after valueMap, etc. applied)
    // @param    rawValue    (any)    raw value of the cell (before valueMap, etc applied)
    // @return    (boolean)    false to stop event bubbling
    //
    // @group    events
    //
    // @see method:listGrid.recordClick()
    // @visibility external
    // @example recordClicks
    //<

    //> @method listGridField.recordDoubleClick()
    //
    // Executed when this field is double-clicked.  Note that if
    // +link{ListGrid.recordDoubleClick()} is also defined, it will be fired for fields that define
    // a recordDoubleClick handler if the field-level handler returns true. Return false to prevent
    // the grid-level handler from firing.
    //
    //
    // @param    viewer        (ListGrid)    the listGrid that contains doubleclick event
    // @param    record        (ListGridRecord)    the record that was double-clicked
    // @param    recordNum    (number)    number of the record clicked on in the current set of
    //                                  displayed records (starts with 0)
    // @param    field        (ListGridField)    the field that was clicked on (field definition)
    // @param    fieldNum    (number)    number of the field clicked on in the listGrid.fields
    //                                  array
    // @param    value       (object)    value of the cell (after valueMap, etc. applied)
    // @param    rawValue    (object)    raw value of the cell (before valueMap, etc applied)
    // @return    (boolean)    false to stop event bubbling
    //
    // @group    events
    //
    // @see method:listGrid.recordClick()
    // @visibility external
    // @example recordClicks
    //<

    // Filtering
    // ---------------------------------------------------------------------------------------

    //> @attr   listGridField.canFilter (boolean : null : [IRW])
    //      If showing a filter row for this ListGrid, should the filter criteria for this
    //      field be editable
    //  @group  filterEditor
    //  @visibility external
    //  @example disableFilter
    //<

    //> @attr listGridField.filterEditorValueMap (object : null : [IRW])
    //  If this listGrid is showing a filter row, this property can be used to specify a
    //  mapping of internal data to/from display values to be in the appropriate filter
    //  row form item.
    //  @visibility external
    //  @group filterEditor
    //<

    //> @attr   listGridField.filterEditorType (FormItem className : null : [IRWA])
    //      If this ListGrid is showing a filter row, this property can be used to
    //      specify the form item class to use for the filter form item associated with this
    //      field
    //      (Only used if this field is not canFilter:false).<br>
    //      Note: If this is not specified, the edit-form item type may be derived from the
    //      'editorType' property, typically inherited from datasource fields, or from the
    //      'type' of the field (showing the appropriate form item for the data-type).
    //  @group  filterEditor
    //  @visibility external
    //<

    //> @attr   listGridField.defaultFilterValue (any : null : [IRWA])
    // If this ListGrid is showing a filter row, this property can be used to apply a default
    // value to show in the filter editor for this field.
    // @group filterEditor
    // @visibility external
    //<


    //> @attr   listGridField.filterEditorProperties (FormItem properties : null : [IRWA])
    // If this ListGrid is showing a filter row
    // (+link{listGrid.showFilterEditor,showFilterEditor}:true), this property
    // can be used to specify properties for the appropriate filter form item.
    // @group filterEditor
    // @visibility external
    //<

    //>    @attr     listGridField.filterOnKeypress (boolean : null : [IRWA])
    // If we're showing the filterEditor (listGrid.showFilterEditor is true), this property
    // determines whether this list should be filtered every time the user edits the value of
    // the filter editor for this field.
    // @group filterEditor
    // @visibility external
    //<

    //> @attr listGrid.fetchDelay (number : 300 : IRWA)
    // If we're showing the filterEditor (+link{listGrid.showFilterEditor} is true), and we're
    // re-filtering on every keypress (+link{listGrid.filterOnKeypress} is true), this
    // property is the delay in milliseconds between the user changing the filter and the
    // filter request being kicked off. If multiple changes are made to the filter
    // within this fetch delay, only the most recent will actually cause a re-filter
    // @group filterEditor
    // @visibility external
    //<
    fetchDelay:300,

    //>    @attr listGridField.shouldPrint (boolean : null : IRW)
    // Whether this field should be included in the printable representation of the grid.
    //
    // @group printing
    // @visibility external
    //<

    // AutoComplete
    // ---------------------------------------------------------------------------------------

    //>    @attr listGridField.autoComplete (AutoComplete : null : IRW)
    // Whether to do inline autoComplete when editing this field.
    // <p>
    // If unset, defaults to listGrid.autoComplete
    //
    // @see listGrid.autoComplete
    // @visibility autoComplete
    //<

    //>    @attr listGridField.uniqueMatch (boolean : null : IRW)
    // When autoComplete is enabled, whether to offer only unique matches to the user.
    // <p>
    // If unset, defaults to listGrid.uniqueMatch.
    //
    // @see listGrid.uniqueMatch
    // @visibility autoComplete
    //<
    uniqueMatch:true,


    // Formatting (per field)
    // --------------------------------------------------------------------------------------------

    //> @method listGridField.getCellValue()
    // A stringMethod which returns the cell value to display for this field for some record.
    // If defined, called by ListGrid.getCellValue().  Called in the scope of the field object.
    //
    // Deprecated as of Jan 12 05 in favor of +link{listGridField.formatCellValue()}, because
    // 'getCellValue()' is a lower-level API which handles (for example) returning the HTML
    // for editors within the cell.
    //
    // @param  viewer  (ListGrid)  the ListGrid for which we're returning a cellValue
    // @param  record  (object)    the current record object
    // @param  recordNum   (nubmer)    row-index of the current record
    // @param  field   (listGridField) current field object
    // @param  fieldNum    (number)    column-index of the current field
    // @param  value   (any)   unformatted value for this field, determined via
    //                          ListGrid.getRawCellValue()
    // @see    method:listGrid.getCellValue
    // @see method:listGridField.formatCellValue
    // @group  display_values
    // @visibility external
    // @return (any)   value to display in the ListGrid cell
    // @deprecated As of SmartClient 5.5, use +link{listGridField.formatCellValue}.
    //<

    // We provide separate formatters for the raw value displayed in a static cell, and the
    // value displayed in an editor.
    // This makes sense because:
    // - developers are likely to want to apply different formats - for example including some
    //   raw HTML in the static value, but not in the value displayed in a text based editor.
    // - the more common 'formatCellValue()' needs no parser to convert from the formatted value
    //   back to the raw value
    // If a developer wishes to apply the same formatter in both cases, the suggested approach
    // would be to write a single formatter function and have it be called from both methods.

    //> @method listGridField.formatCellValue()
    // Return the HTML to display in cells of this field.
    // <P>
    // Given the raw value for this field as taken from the record Formatter to apply to the
    // static values displayed in cells for this field.
    // <P>
    // <i>Example usage</i>: formatting a currency value stored in cents (so "100" to "$1.00")<br>
    // The value passed to this method is the raw value for the cell.<br>
    // Takes precedence over <code>formatCellValue</code> defined at the grid level for cells
    // in this field.
    // <P>
    // Note: this formatter will not be applied to the values displayed in cells being edited.
    // The +link{listGridField.formatEditorValue,formatEditorValue()} is provided for that purpose.
    //
    // @group display_values
    //
    // @param   value (any)   raw value for the cell, from the record for the row
    // @param   record   (ListGridRecord)
    //   Record object for the cell. Note: If this is a new row that has not been saved, in an
    //   editable grid, it has no associated record object. In this case the edit values will
    //   be passed in as this parameter (see +link{listGrid.getEditValues()})
    // @param   rowNum  (number)    row number for the cell
    // @param   colNum  (number)    column number for the cell.
    // @param   grid    (ListGrid) the ListGrid displaying the cell
    // @return (String) HTML to display in the cell
    //
    // @see listGrid.formatCellValue()
    // @see listGridField.formatEditorValue()
    // @visibility external
    // @example formatValues
    //<

    //> @attr listGridField.escapeHTML (boolean : null : IRW)
    // By default HTML values in ListGrid cells will be interpreted by the browser.
    // Setting this flag to true will causes HTML characters to be escaped, meaning the
    // raw value of the field (for example <code>"&lt;b&gt;AAA&lt;/b&gt;"</code>) is displayed
    // to the user rather than the interpreted HTML (for example <code>"<b>AAA</b>"</code>)
    // @visibility external
    //<

    //> @attr listGridField.linkText (String : null : IRW)
    // The HTML to display in cells of this field if the fieldType is set to link.
    // <P>
    // This property sets linkText that will be the same for all records.  You can set linkText
    // on a per-record basis via +link{attr:listGridRecord.linkText}.
    //
    //  @see type:ListGridFieldType
    //  @see type:FieldType
    //  @see attr:listGridRecord.linkText
    //  @see attr:listGrid.linkTextProperty
    //  @see attr:listGridField.linkTextProperty
    //  @group  display_values
    //  @visibility external
    //  @example linkImage
    //<

    //> @attr listGridField.linkTextProperty (string : null : IRW)
    // Name of the property in a ListGridRecord that holds the HTML to display in cells of this
    // field if the fieldType is set to "link".
    //
    //  @see type:ListGridFieldType
    //  @see type:FieldType
    //  @see attr:listGridRecord.linkText
    //  @see attr:listGridField.linkText
    //  @see attr:listGrid.linkTextProperty
    //  @group  display_values
    //  @visibility external
    //<

    //> @attr listGridField.linkURLPrefix (string : null : IRWA)
    // If this field has type [+link{type:ListGridFieldType}] set to <code>"link"</code>,
    // setting this property will apply a standard prefix to the link URL for cells in this field.
    // @visibility external
    //<


    //> @attr listGridField.linkURLSuffix (string : null : IRWA)
    // If this field has type [+link{type:ListGridFieldType}] set to <code>"link"</code>,
    // setting this property will apply a standard suffix to the link URL for cells in this field.
    // @visibility external
    //<

    // --------------------
    // Editing

    //> @method listGridField.formatEditorValue
    // Return the value to display in cells of this field which are being edited.
    // <P>
    // <i>Example usage</i>: converting a stored value in cents (100) to a dollar-and-cents
    // value in the editor (1.00)
    // <P>
    // The value passed to this method is the raw value for the cell.
    // <P>
    // <code>formatEditorValue</code> takes precedence over +link{listGrid.formatEditorValue()}
    // defined at the grid level for cells in this field.
    // <P>
    // To convert the formatted value displayed within an editor back to a raw value, implement
    // +link{listGridField.parseEditorValue} as well.
    //
    // @group editing
    //
    // @param   value (any)   raw value for the cell being edited
    // @param   record   (ListGridRecord)
    //   Record object for the cell. Note: If this is a new row that has not been saved, in an
    //   editable grid, it has no associated record object. In this case the edit values will
    //   be passed in as this parameter.
    // @param   rowNum  (number)    row number for the cell
    // @param   colNum  (number)    column number for the cell.
    // @param   grid    (ListGrid instance) A pointer to the ListGrid displaying the cell
    // @return (any) formatted value to display in the editor
    //
    // @see listGridField.formatCellValue()
    // @see listGrid.formatEditorValue()
    // @see listGridField.parseEditorValue()
    //
    // @visibility external
    //<

    //> @method listGridField.parseEditorValue
    // Method used to convert the value displayed in an editor for some cell in this field into
    // a raw value for saving.<br>
    // Takes precedence over <code>parseEditorValue</code> defined at the grid level.
    //
    // @group editing
    //
    // @param   value (any)   value displayed in the editor for the cell
    // @param   record (object) record object for the row being edited. May be null if this
    //                          is a new row being added to the end of the list.
    // @param   rowNum  (number)    row number for the cell
    // @param   colNum  (number)    column number for the cell.
    // @param   grid    (ListGrid instance) A pointer to the ListGrid displaying the cell
    // @return (any) raw value for the field derived from formatted value in editor
    // @see listGrid.parseEditorValue()
    // @see listGridField.formatEditorValue()
    // @visibility external
    //<

    //> @attr listGridField.valueMap (Array of Object : null : IRW)
    // Array of legal values for this field, or an Object where each property maps a stored
    // value to a user-displayable value.<br>
    // Note that if this field is editable (see +link{listGrid.canEdit},
    // +link{listGridField.canEdit}), editors displayed for this field will pick up their
    // valueMap either from this value or from +link{listGridField.editorValueMap}.
    // <P>
    // See also +link{dataSourceField.valueMap}.
    //
    // @group display_values
    // @see ListGrid.setValueMap()
    // @see ListGrid.getDisplayValue()
    // @visibility external
    // @example listType
    //<

    //> @attr listGridField.sortByMappedValue (boolean : null : IRW)
    // If +link{listGridField.valueMap} is set, and the grid is +link{listGrid.setSort(),sorted}
    // by this field, should the data be sorted by the underlying data value or the
    // mapped display value. If unset, will sort by display value. Set to <code>false</code>
    // to sort by underlying data value. Note that this has no effect if
    // a +link{listGridField.sortNormalizer} has been specified.
    // @visibility external
    //<

    //> @attr listGridField.multiple (Boolean : null : IR)
    // Indicates that this field should always be Array-valued. This property will be
    // passed through to the generated edit-item when editing the field - so if
    // +link{listGridField.valueMap} is set, the default editor will be a +link{SelectItem} with
    // +link{SelectItem.multiple} set to true.
    // <P>
    // Note that for databound grids it typically makes sense to set +link{DataSourceField.multiple}
    // rather than setting the property directly on the ListGridField object.
    // @group editing
    // @visibility external
    //<

    //> @attr listGridField.editorValueMap (Array or Object : null : IRW)
    // A valueMap to use for editors shown for this field.  By default if this is not
    // specified +link{listGridField.valueMap,field.valueMap} will be used instead.
    // <P>
    // Dynamic valueMaps can be provided by implementing +link{listGrid.getEditorValueMap()}.
    //
    // @group editing
    // @visibility external
    // @see listGrid.getEditorValueMap()
    // @see listGrid.setEditorValueMap()
    //<

    //> @method listGridField.change()
    // If this field is editable, any +link{formItem.change, change} handler specified
    // on the ListGridField will be passed onto the editors for this field.
    // <P>
    // Note that if +link{listGridField.canToggle} is true, the user may change the value of
    // a boolean field without going into edit mode by single clicking on the field. In this
    // case the +link{listGridField.change()} and +link{listGridField.changed()} handlers will
    // fire but the <code>form</code> and <code>item</code> parameters will be null.
    //
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param    item    (FormItem)    the editor (form item) itself (also available as "this").
    //                              Note that in addition to the standard FormItem APIs available
    //                              on the editor, it also has:<br>
    //                              - a pointer back to the containing listGrid
    //                              [<code>item.grid</code>]<br>
    //                              - the colNum being edited [<code>item.colNum</code>]<br>
    //                              - the rowNum being edited [<code>item.rowNum</code>]
    // @param   value   (any)         The new value of the form item
    // @param   oldValue    (any)     The previous value of the form item
    // @return (Boolean) The change may be cancelled <smartclient>by returning false</smartclient>
    // @see listGridField.changed()
    // @see listGrid.cellChanged()
    // @group editing
    // @visibility external
    //<

    //> @method listGridField.changed()
    // If this field is editable, any +link{formItem.changed, changed} handler specified
    // on the ListGridField will be passed onto the editors for this field.
    // Note that if +link{listGridField.canToggle} is true, the user may change the value of
    // a boolean field without going into edit mode by single clicking on the field. In this
    // case the +link{listGridField.change()} and +link{listGridField.changed()} handlers will
    // fire but the <code>form</code> and <code>item</code> parameters will be null.
    //
    // @param    form    (DynamicForm) the managing DynamicForm instance
    // @param    item    (FormItem)    the editor (form item) itself (also available as "this").
    //                              Note that in addition to the standard FormItem APIs available
    //                              on the editor, it also has:<br>
    //                              - a pointer back to the containing listGrid
    //                              [<code>item.grid</code>]<br>
    //                              - the colNum being edited [<code>item.colNum</code>]<br>
    //                              - the rowNum being edited [<code>item.rowNum</code>]
    // @param   value   (any)         The current value (after the change).
    // @see listGridField.change()
    // @see listGrid.cellChanged()
    // @group editing
    // @visibility external
    //<

    //> @attr listGridField.emptyCellValue (HTMLString : "&nbsp;" : IRW)
    // The value to display for a cell whose value is null or the empty string after
    // applying +link{listGridField.formatCellValue(),formatting} and valueMap (if any).
    // <p>
    // This is the field-specific attribute.  You may also set the emptyCellValue at the grid
    // level to define the emptyCellValue for all empty fields in the grid.
    //
    // @group display_values
    // @see listGrid.emptyCellValue
    // @visibility external
    // @example emptyValues
    //<

    // Field.optionDataSource
    // --------------------------------------------------------------------------------------------

    //> @attr listGridField.autoFetchDisplayMap (boolean : null : [IRW])
    // If true, automatically fetches records and derives a valueMap from
    // +link{listGridField.optionDataSource}.
    // <p>
    // Same as +link{listGrid.autoFetchDisplayMap}, but defined on a per-field basis.
    //
    // @group display_values
    // @see listGrid.autoFetchDisplayMap
    // @visibility external
    //<

    //> @attr listGridField.displayValueFromRecord (boolean : null : IRWA)
    // If a +link{listGridField.displayField} is set, should this field show record values from
    // the <code>displayField</code>?
    // <P>
    // If +link{listGridField.displayField} is specified, and there is no separate
    // +link{listGridField.optionDataSource}, by default we will show display-field values
    // from the same record. Setting this property to false would disable this behavior.
    // <P>
    // Alternatively, if there is a +link{listGridField.optionDataSource} (and
    // +link{listGridField.autoFetchDisplayMap} is false), the displayField would be ignored
    // for the field and the underlying +link{listGridField.name,record[fieldName] value} would
    // be displayed to the user. This property may be set to true to override this behavior and
    // pick up values from the <code>displayField</code> for display in this field even when there
    // is an optionDataSource set.
    // <P>
    // Note that this property has no effect on fields with an explicitly specified valueMap, or
    // with an optionDataSource where +link{listGridField.autoFetchDisplayMap} is true.
    // @visibility external
    //<

    //> @attr listGridField.optionTextMatchStyle (TextMatchStyle : null : [IR])
    // For fields with an +link{listGridField.optionDataSource}, where
    // +link{listGridField.autoFetchDisplayMap} is true, this property will govern
    // the <code>textMatchStyle</code> attribute of the +link{DSRequest} parameter passed to
    // +link{DataSource.fetchData()} when retrieving the remote data set to be used as
    // a basis for this field's valueMap.
    // @group display_values
    // @visibility external
    //<

    //> @attr listGridField.optionFilterContext (DSRequest Properties : null : [IR])
    // If this field has an optionDataSource specified and
    // +link{listGridField.autoFetchDisplayMap,autoFetchDisplayMap} is set, this attribute
    // provides a way to customize the dataSource request issued to fetch the display map from
    // the option dataSource.
    // @group display_values
    // @visibility external
    //<

    //> @attr listGridField.optionOperationId (string : null : [IR])
    // If this field has an optionDataSource specified and
    // +link{listGridField.autoFetchDisplayMap,autoFetchDisplayMap} is set, this attribute
    // provides a way to customize the +link{DSRequest.operationId} passed to
    // <code>dataSource.fetchData()</code> when retrieving the display map from the option
    // dataSource.
    // @group display_values
    // @visibility external
    //<

    //> @attr listGridField.optionDataSource (String : null : [IRW])
    // Derive a +link{valueMap} by fetching records from another DataSource and extracting
    // the +link{listGridField.valueField,valueField} and
    // +link{listGridField.displayField,displayField} in the loaded records, to derive one
    // valueMap entry per record loaded from the optionDataSource.
    // <P>
    // Unlike the similar use of +link{pickList.optionDataSource} for +link{PickList,pickLists}
    // used during editing or filtering, <code>listGridField.optionDataSource</code> causes the
    // <b>entire set of records from the optionDataSource to be fetched</b>, without paging.
    // Hence listGridField.optionDataSource is appropriate only for smaller valueMaps.  For very
    // large valueMap situations, such as an accountId field that should be displayed as an
    // accountName where there are thousands of accounts, the correct approach is:
    // <ul>
    // <li> do not set listGridField.optionDataSource
    // <li> declare two fields in the DataSource, eg "accountId" and "accountName".
    // <li> Set the +link{ListGridField.displayField} attribute on the data field to the
    //      name of the display field.
    // <li> When fetching records for display in a grid, have your server send back values for
    //      both fields, but show only the data field ("accountId") in the grid.
    // </ul>
    // In this case the cells in the accountId field will show the record value from the
    // accountName field.  This approach means the valueMap will never be loaded in its
    // entirety, instead, each loaded record contains the valueMapping for that one record, as
    // a pair of fields within the record.
    // <p>
    // If you are using the SmartClient Server Framework with the SQL or JPA/Hibernate built-in
    // connectors, this entire approach can be achieved very easily using the
    // +link{dataSourceField.includeFrom} setting - see the
    // +link{dataSourceField.includeFrom,docs for includeFrom} for details.
    // <P>
    // Notes:
    // <ul>
    // <li>When using the above approach, it is key that the server return <b>both</b>
    // the underlying stored value <b>and</b> the display value, as suggested above.
    // This approach allows the +link{pickList.optionDataSource} property to be used to
    // provide paged valueMaps during inline editing and
    // +link{ListGrid.showFilterEditor,inline filtering}. This can be achieved by setting the
    // <code>optionDataSource</code> attribute on the form item used to edit the field
    // via +link{listGridField.editorProperties} (for editing) or
    // +link{listGridField.filterEditorProperties,field.filterEditorProperties} (for
    // filtering), without specifying an optionDataSource at the listGridField level.
    // Alternatively developers can use +link{listGridField.autoFetchDisplayMap} to
    // suppress the fetch against the optionDataSource at the listGrid level.</li>
    // <li>Setting <code>listGridField.optionDataSource</code> to the same dataSource as the
    // listGrid is not the same as omitting the optionDataSource setting entirely. Unless
    // +link{listGridField.autoFetchDisplayMap} has been set to explicitly disable fetching,
    // a fetch will be performed against the dataSource to build a valueMap which will be used
    // as the definitive mapping from data to display values, rather than picking up the display
    // values from the records themselves. This distinction is required to support cases where
    // the +link{listGridField.valueField} points to a different field in the grid (useful for
    // hierarchical relationships, for example), or where +link{listGridField.optionCriteria} or
    // +link{listGridField.optionOperationId} are specified and return different data from
    // the records displayed within the grid.</li>
    // <li>If a displayField is specified, with no associated optionDataSource, and the field is
    // editable, updating the edit value for the field may not automatically update the displayField
    // edit value, meaning the user may not realize the edit value has been modified.
    // If the new value came from the user editing the field, and the edit item has
    // a valueMap or optionDataSource specified, the display value is picked up automatically and
    // stored out on the displayField for the record. However if the value was set programmatically,
    // the developer should also set the edit value for the display field to ensure the displayed
    // value reflects the new edit value. Note that when this occurs, a warning will be logged
    // which can be disabled via +link{listGrid.warnOnUnmappedValueFieldChange}.</li>
    // <li>For very advanced usage a developer can use +link{listGridField.displayValueFromRecord}
    // to explicitly tell the grid whether or not to display the display field value for the record
    // in this field when a displayField is specified. See documentation on that property for more
    // information</li>
    // </ul>
    //
    // @group display_values
    // @visibility external
    //<

    //> @attr listGridField.valueField (string : null : [IRW])
    // Specifies the +link{listGridField.optionDataSource} field used to retrieve the stored
    // values that are to be mapped to the display values (specified by
    // +link{listGridField.displayField}). Note that if this field is editable this will also
    // be applied to this field's editors.
    //
    // @group display_values
    // @visibility external
    //<

    //> @attr listGridField.displayField (string : null : [IRW])
    // Specifies the +link{listGridField.optionDataSource} field used to retrieve the display
    // values that are to be mapped from the internal values specified by +link{valueField}.
    // <P>
    // If no <code>optionDataSource</code> is defined for the field, the cell will display
    // the displayField value for the current record instead of the underlying value for
    // this field.  This approach can be used for situations where field values need a stored
    // value to displayed value mapping, but the set of all possible values is too large to
    // load as a +link{ValueMap} - see +link{listGridField.optionDataSource} for more details
    // on this approach.  Note that if this field is editable this will also be applied to this
    // field's editors.  +explorerExample{largeValueMapSQL,This sample} illustrates this
    // approach achieved via a server-side SQL join.
    // <p>
    // The display value for a record with a specified <code>displayField</code> can be
    // picked up via +link{ListGrid.getDisplayValue()}.
    //
    // @group display_values
    // @visibility external
    //<

    //> @attr listGridField.sortByDisplayField (boolean : null : [IRW])
    // For a field with <code>displayField</code> configured, should client-side sorting
    // be performed on the display field value? Unless explicitly set to <code>false</code>
    // the display field value is used.
    //
    // @group display_values
    // @visibility external
    //<

    //> @attr listGridField.optionCriteria (Criteria : null : [IRW])
    // If +link{optionDataSource} is set for this ListGridField, criteria specified in this
    // attribute will be passed to the dataSource when performing the fetch operation to
    // determine data-value to display-value mappings
    // @group display_values
    // @visibility external
    //<


    // ---------

    //> @attr listGridField.includeFrom (String : null : [IR])
    // Indicates this field's values should be fetched from another, related DataSource.
    // The individual field will inherit settings such as +link{listGridField.type,field.type}
    // and +link{listGridField.title,field.title} from the related DataSource just like
    // fields from the primary DataSource.
    // <P>
    // When +link{listGrid.fetchData(),fechData()} is called, the automatically created +link{DSRequest} will
    // specify +link{dsRequest.additionalOutputs} requesting the field, and any +link{Criteria}
    // generated by the component will likewise refer to the field from the related DataSource.
    // <P>
    // It's an error to use this property if the ListGrid does not have a DataSource at all.
    // The related DataSource must be loaded or a warning will be logged and the field
    // definition ignored.
    // <P>
    // This value is expected to be set to the following format
    // <code>dataSourceID.fieldName</code> where <i>dataSourceID</i> is the ID of the
    // related dataSource and <i>fieldName</i> is the name of the field from that dataSource
    // from which you wish to retrieve values. Note that if this property is set and
    // +link{ListGridField.name,field.name} is not explicitly specified, this field's <code>name</code> will
    // default to the <i>fieldName</i> value from this property.
    // <p>
    // Note about automatic cache updates: "update" and "add" operations
    // submitted automatically by this ListGrid will include
    // <code>dsRequest.additionalOutputs</code> to ensure all data necessary
    // for cache updates is returned by the server.
    // <p>
    // If your grid shows data that can be edited elsewhere in the UI (not by
    // inline editing), to avoid problems with +link{ResultSet} automatic
    // cache synchronization, you may need to switch from using
    // <code>listGridField.includeFrom</code> to
    // <code>dataSourceField.includeFrom</code>.  This is because
    // server responses to "add" and "update" operations which are initiated
    // outside of this grid do not know about the
    // <code>listGridField.includeFrom</code> setting, and so will not
    // automatically return data for fields included in this way.  Switching
    // to <code>dataSourceField.includeFrom</code> ensures the field is
    // always included in server responses, avoiding the issue.
    //
    // @group display_values
    // @visibility crossDS
    //<

    // ----------------------------------------------------------------------------------------
    // Don't show scrollbars -- scrolling occurs in the body
    overflow:isc.Canvas.HIDDEN,

    //>    @attr    listGrid.backgroundColor        (string : "white" : IRW)
    //        @group    appearance
    //<
    backgroundColor:"white",

    //>    @attr    listGrid.minHeight        (number : 50 : IRW)
    // Minimum height for the entire list (smaller than this doesn't tend to work very well).
    //        @group    sizing
    //<
    minHeight:50,

    defaultWidth:200,

    //> @attr listGrid.fieldIdProperty (string : "name" : IRA)
    //  Property to be used as field identifier on listGridField objects.
    //  The ID of the field is also the property in each record which holds the value
    //  for that field.
    //        @group    data
    //<
    // defaulted on Canvas


    // GridRenderer properties
    // ---------------------------------------------------------------------------------------

    //>    @attr listGrid.showAllRecords (Boolean : false : [IRW])
    // @include gridRenderer.showAllRows
    // @example autofitRows
    //<
    //showAllRecords:false,

    //>    @attr listGrid.showAllColumns (Boolean : false : IR)
    // @include gridRenderer.showAllColumns
    //<
    //showAllColumns:false,

    //>    @attr listGrid.drawAllMaxCells (int : 250 : IRWA)
    // @include gridRenderer.drawAllMaxCells
    // @group performance
    // @visibility external
    //<
    drawAllMaxCells:250,


    //>    @attr listGrid.drawAheadRatio (float : 1.3 : IRW)
    // @include gridRenderer.drawAheadRatio
    // @group performance
    // @example databoundFetch
    //<
    drawAheadRatio:1.3,

    //> @attr listGrid.quickDrawAheadRatio (float : 1.0 : IRW)
    // @include gridRenderer.quickDrawAheadRatio
    // @group performance
    //<
    quickDrawAheadRatio:1.0,

    //> @attr listGrid.instantScrollTrackRedraw (Boolean : true : IRW)
    // @include gridRenderer.instantScrollTrackRedraw
    // @group performance
    // @visibility external
    //<

    //> @attr listGrid.scrollRedrawDelay (int : 75 : IRW)
    // @include gridRenderer.scrollRedrawDelay
    // @group performance
    // @visibility external
    //<
    scrollRedrawDelay:75,

    //> @attr listGrid.scrollWheelRedrawDelay (Integer : 250 : IRW)
    // While scrolling an incrementally rendered grid, using the mouseWheel, time in
    // milliseconds to wait before redrawing, after the last mouseWheel movement by the user.
    // If not specified +link{scrollRedrawDelay} will be used as a default for both
    // drag scrolling and mouseWheel scrolling.
    // <P>
    // Note that if specified, this value will typically be larger than
    // +link{scrollRedrawDelay}. From experimentation, the default setting of
    // <code>250</code> is typically enough time for a user to rapidly scroll through a
    // grid (rotating the scroll wheel with repeated flicks), without redrawing between
    // rotations, but this will differ depending on how long the redraw takes. A larger
    // delay may be warranted for grids with large numbers of columns, heavy custom
    // formatting, etc.
    // <P>
    // See also
    // +link{gridRenderer.instantScrollTrackRedraw} for cases where this delay is skipped.
    //
    // @group performance
    // @visibility external
    //<
    scrollWheelRedrawDelay:250,


    //>    @attr listGrid.virtualScrolling (boolean : null : [IRA])
    // When incremental rendering is switched on and there are variable record heights, the virtual
    // scrolling mechanism manages the differences in scroll height calculations due to the
    // unknown sizes of unrendered rows to make the scrollbar and viewport appear correctly.
    // <P>
    // virtualScrolling is switched on automatically when fixedRecordHeights is false and when
    // using the +link{listGrid.showRecordComponents,recordComponents subsystem}, as
    // recordComponents expand the rows that contain them. This flag should be manually enabled
    // when calling +link{listGrid.addEmbeddedComponent}(...) if embedded components can
    // cause record sizes to expand beyond specified cellHeight.
    // <P>
    // virtualScrolling is also automatically enabled when +link{listGrid.canExpandRecords} is true
    // to handle the fact that expanded rows may render at variable heights.
    // @visibility external
    //<

    //> @attr listGrid.dataPageSize (integer : 75 : IRW)
    // @include dataBoundComponent.dataPageSize
    // @group performance
    // @visibility external
    // @example databoundFetch
    //<

    //> @attr listGrid.dataFetchMode (FetchMode : "paged" : IR)
    // @include dataBoundComponent.dataFetchMode
    //<

    // configures ResultSet.fetchDelay, delay in MS before fetches are triggered

    dataFetchDelay : 300,

    //> @attr listGrid.body (MultiAutoChild GridRenderer : null : R)
    // GridRenderer used to render the dataset.
    // <p>
    // Note that this is a multi-instance component when there are frozen fields because in
    // addition to the primary body AutoChild, a "frozen body" AutoChild is created to render
    // the frozen portion of the dataset.
    // @see ListGrid.getBody()
    // @visibility external
    //<
    body:null,

    bodyConstructor:"GridBody",

    //>    @attr listGrid.bodyOverflow (Overflow : isc.Canvas.AUTO : [IRWA])
    // Overflow setting for the "body", that is, the area of the grid where data values are
    // rendered.
    // <P>
    // By setting both the grid itself and the body to overflow:visible, it is possible to
    // "auto-fit" to the rendered height or width of the rows.  Note that in this case
    // <code>grid.width</code> and <code>grid.height</code> act as minimums.
    //
    //      @visibility external
    //        @group    sizing
    //      @example autofitRows
    //<
    bodyOverflow:isc.Canvas.AUTO,


    //>    @attr listGrid.bodyBackgroundColor (string : "white" : IRW)
    // Background color applied to the ListGrid body (that is, the area of the grid where
    // data values are rendered).<br>
    // Note that this will typically not be visible to the user unless there are few enough
    // rows that there is visible space in the body below the last row. To style data cells,
    // override +link{ListGrid.baseStyle} instead.
    //        @group    appearance
    // @visibility external
    //<
    bodyBackgroundColor:"white",

    //>    @attr listGrid.bodyStyleName (CSSStyleName : null : IRW)
    // CSS style used for the body of this grid.  If applying a background-color to the body
    // via a CSS style applied using this property, be sure to set
    // +link{ListGrid.bodyBackgroundColor} to <code>null</code>.
    //        @group    appearance
    // @visibility external
    //<
    //bodyStyleName:null,

    // whether to allow the body and header to have different border sizes and automatically
    // adjust the body column sizes to compensate such that column boundaries line up.
    allowMismatchedHeaderBodyBorder : true,

    //>    @attr listGrid.emptyCellValue (HTMLString : "&nbsp;" : IRW)
    // The value to display for cells whose value is null or the empty string after applying
    // +link{listGrid.formatCellValue(),formatting} and valueMap (if any).
    // <p>
    // This is the grid-wide attribute.  You may also set the emptyCellValue on a per-field basis.
    //
    // @group cellStyling
    // @see listGridField.emptyCellValue
    // @visibility external
    // @example emptyValues
    //<
    emptyCellValue:"&nbsp;",

    //>    @attr listGrid.cellHeight (number : 20 : [IRW])
    // @include gridRenderer.cellHeight
    // @example multilineValues
    //<
    cellHeight:20,

    //> @attr listGrid.normalCellHeight (number : 20 : [IRWA])
    // If +link{listGrid.baseStyle} is unset, base style will be derived from
    // +link{listGrid.normalBaseStyle} if this grid has fixed row heights and
    // the specified +link{listGrid.cellHeight} matches this value. Otherwise
    // +link{listGrid.tallBaseStyle} will be used.
    // @visibility external
    //<
    normalCellHeight:20,

    //>    @attr listGrid.fixedRecordHeights (Boolean : true : IRWA)
    // Should we vertically clip cell contents, or allow rows to expand vertically to show all
    // contents?
    // <P>
    // If we allow rows to expand, the row height as derived from
    // +link{gridRenderer.getRowHeight(),getRowHeight()} or the
    // default +link{cellHeight} is treated as a minimum.
    // <P>
    // <b>NOTE:</b><ul>
    // <li>Setting fixedRecordHeights to false for +link{CubeGrid} is not supported, though a
    // similar option for the row headers is available as +link{CubeGrid.autoSizeHeaders}.
    // <li>By default, for performance reasons, clipping is not enforced for
    // some kinds of content (such as images) on all browsers.  Set
    // +link{enforceVClipping,enforceVClipping:true} to enforce clipping for
    // all types of content on all browsers.
    // </ul>
    //
    // @include gridRenderer.fixedRowHeights
    // @example autofitValues
    //<
    fixedRecordHeights: true,

    //> @attr listGrid.enforceVClipping (Boolean : false : IRW)
    // For performance reasons, even when +link{fixedRecordHeights} is set, vertical clipping
    // is not enforced by default for some kinds of content (such as images) on all browsers.
    // Set +link{enforceVClipping,enforceVClipping:true} to enforce clipping for all types of
    // content on all browsers.
    // <P>
    // This additional setting is likely to be phased out as browsers improve.
    //
    // @visibility external
    //<


    //>    @attr listGrid.fixedFieldWidths (Boolean : true : IRWA)
    // Should we horizontally clip cell contents, or allow columns to expand horizontally to
    // show all contents?
    // <P>
    // If we allow columns to expand, the column width is treated as a minimum.
    // <P>
    // NOTE: the header does not automatically respond to expanded field widths.
    // If your grid is showing a header we'd recommend developers consider
    // setting +link{listGrid.autoFitFieldWidths} rather than using this attribute.
    // @group cellStyling
    // @visibility external
    //<
    // NOTE: doc is duplicated here because in the ListGrid we need to discuss the header.
    fixedFieldWidths:true,

    // autoFit attributes

    //> @type Autofit
    // Possible values to change the behavior of how data will fill the ListGrid.
    // @value "vertical" expand vertically to accommodate records.
    // @value "horizontal" expand horizontally to accommodate fields.
    // @value "both" expand horizontally and vertically to accommodate content.
    // @group autoFitData
    // @visibility external
    //<

    //> @attr listGrid.autoFitData (Autofit : null : IRW)
    // Should this ListGrid automatically expand to accommodate the size of records and fields?
    // <P>
    // Valid settings are
    // <ul><li><code>"vertical"</code>: expand vertically to accommodate records.</li>
    //     <li><code>"horizontal"</code>: expand horizontally to accommodate fields.</li>
    //     <li><code>"both"</code>: expand horizontally and vertically to accommodate content.</li>
    // </ul>
    // How far the ListGrid will expand may be limited via the following properties:
    // +link{ListGrid.autoFitMaxHeight}, +link{ListGrid.autoFitMaxRecords},
    // +link{ListGrid.autoFitMaxWidth}, +link{ListGrid.autoFitMaxColumns}.
    // <P>
    // Note that this property causes the grid as a whole to expand to fit records or fields.
    // To have the fields or records themselves expand to fit cell contents, see
    // +link{listGrid.autoFitFieldWidths} and +link{listGrid.fixedRecordHeights}.
    // @group autoFitData
    // @visibility external
    //<

    //> @attr listGrid.autoFitMaxHeight (number : null : IRW)
    // If +link{listGrid.autoFitData} is set to <code>"vertical"</code> or <code>"both"</code> this
    // property provides an upper limit on how far the ListGrid will expand vertically to accommodate
    // its content. If content exceeds this height, scrollbars will be introduced as usual.
    // In addition to this property, +link{ListGrid.autoFitMaxRecords} allows you to limit vertical
    // expansion based on the number of rows to be rendered.
    // @group autoFitData
    // @visibility external
    //<

    //> @attr ListGrid.autoFitMaxRecords (number : 50 : IRW)
    // If +link{listGrid.autoFitData} is set to <code>"vertical"</code> or <code>"both"</code> this
    // property provides the maximum number of records for which the ListGrid will expand. If more
    // records are present, scrolling will be introduced to reach them as normal.
    // If unset, by default the ListGrid will expand to accommodate as many records as are present.
    // @group autoFitData
    // @visibility external
    //<
    autoFitMaxRecords:50,

    //> @attr ListGrid.autoFitExtraRecords (number : null : IRW)
    // If +link{listGrid.autoFitData} is set to <code>"vertical"</code> or <code>"both"</code>
    // this property specifies the number of additional records for which the grid
    // will expand. If more
    // records are present, scrolling will be introduced to reach them as normal.
    // @group autoFitData
    // @visibility external
    //<
//    autoFitExtraRecords:0,

    //> @attr listGrid.autoFitMaxWidth (number : null : IRW)
    // If +link{listGrid.autoFitData} is set to <code>"horizontal"</code> or <code>"both"</code>
    // this property provides an upper limit on how far the ListGrid will expand horizontally to
    // accommodate its content.
    // If content exceeds this width, scrollbars will be introduced as usual.
    // In addition to this property, +link{ListGrid.autoFitMaxColumns} allows you to limit
    // horizontal expansion based on the number of columns to be rendered.
    // @group autoFitData
    // @visibility external
    //<

    //> @attr ListGrid.autoFitMaxColumns (number : 50 : IRW)
    // If +link{listGrid.autoFitData} is set to <code>"horizontal"</code> or <code>"both"</code>
    // this property provides the maximum number of columns for which the ListGrid will expand.
    // If more columns are present, scrolling will be introduced to reach them as normal.
    // If unset the ListGrid will expand to accommodate as many columns as are defined for the
    // grid.
    // @group autoFitData
    // @visibility external
    //<
    autoFitMaxColumns:50,


    //> @attr listGrid.canAutoFitFields (Boolean : true : IRW)
    // Whether the user able to autofit specific columns
    // to their data and/or title via a context menu item or +link{headerAutoFitEvent}.
    // <P>
    // Autofitting can also be programmatically enabled by setting +link{listGridField.autoFitWidth}.
    // <P>
    // When canResizeFields is set to false, canAutoFitFields will also be set to false
    // @visibility external
    // @group autoFitFields
    //<
    canAutoFitFields:true,

    //>    @type AutoFitEvent
    // Event on a listGrid header to trigger auto-fit of the listgrid field.
    // @value "doubleClick" React to a double click on the listGrid header.
    // @value "click" React to a click on the listGrid header.
    // @value "none" No event will trigger auto-fit.
    // @group autoFitFields
    // @visibility external
    //<

    //> @attr listGrid.headerAutoFitEvent (AutoFitEvent : "doubleClick" : IR)
    // Event on a ListGrid header that triggers auto fitting to data and/or title.
    // <P>
    // Note that if sorting is enabled for the field and the headerAutoFitEvent is "click", both
    // sorting and autofit occur on a click.
    // @visibility external
    // @group autoFitFields
    //<
    headerAutoFitEvent:"doubleClick",

    //> @attr listGridField.autoFitWidth (Boolean : null : IR)
    // Enables autofitting to values or titles for this field.. This overrides
    // the +link{listGrid.autoFitFieldWidths} attribute on a per-field basis.
    //
    // @visibility external
    // @group autoFitFields
    //<

    //> @attr listGrid.autoFitFieldWidths (Boolean : null : IR)
    // Enables autofitting of fields to values or titles. This property may be overridden
    // on a per-field basis via +link{listGridField.autoFitWidth}.
    // <P>
    // The +link{listGrid.autoFitWidthApproach} controls whether fitting is to values, titles
    // or both. This property may also be overridden on a per field basis.
    // <P>
    // If +link{listGridField.width,field.width} is also set on the field, it will be taken as a minimum width.
    // +link{listGrid.minFieldWidth} will also be respected.
    // <P>
    // By default, the entire available width of the grid will still be used, by allocating any "extra"
    // space to specific columns - see +link{listGrid.autoFitFieldsFillViewport} for details on
    // controlling this behavior.
    // <P>
    // Autofitting will be performed:
    // <ul>
    //  <li> whenever the dataset is completely changed or rows are added or removed
    //  <li> whenever a field which is autofitting is changed
    //  <li> on a manual call to +link{listGrid.autoFitField()} or
    //       +link{listGrid.autoFitFields()}
    // </ul>
    // Auto-fitting behavior continues until the user resizes the field manually, at which
    // point it stops. The user can also perform a one-time auto-fit of fields via
    // the header context menu if +link{listGrid.canAutoFitFields} is enabled.
    // <P>
    // When autofitting to column values, +link{listGrid.getDefaultFieldWidth()} will be
    // called to determine the space required for a field's values. This method
    // uses values from the rendered set of rows to calculate the required column width, which means the
    // field width may still be smaller than values from non-rendered rows.  See
    // +link{listGrid.showAllRecords} and +link{listGrid.drawAheadRatio}) to control incremental
    // rendering of rows.
    // <P>
    // Note that for <code>icon</code> type fields, the +link{listGrid.autoFitIconFields}
    // property setting may turn on auto-fit-width behavior for specific fields by default,
    // even if <code>autoFitFieldWidths</code> is false for the grid as a whole.
    // <P>
    // Using this feature has a performance penalty roughly comparable to always rendering
    // one additional field per field where autofitting is enabled.  Specifically, enabling it
    // for all fields would be comparable to <i>both</i> doubling the number of fields
    // <i>and</i> disabling +link{listGrid.showAllColumns,horizontal incremental rendering}.
    // In a grid where only half the fields are normally visible and hence only half are
    // normally rendered, this would be roughly 4 times slower overall.
    // <P>
    // This performance penalty is a result of +link{getDefaultFieldWidth()} having to
    // render out the data set offscreen and measure the rendered content - it does not apply
    // for cases where this method can return a simple fixed values (as with icon fields).
    // <P>
    // Which fields are currently autofitting is saved as part of the
    // +link{getViewState,view state} of the ListGrid.
    //
    // @visibility external
    // @group autoFitFields
    //<

    //> @attr listGrid.autoSizeHeaderSpans (Boolean : false : IR)
    // If this listGrid has specified +link{listGrid.headerSpans}, setting this
    // attribute to true will cause spans to expand to accomodate long titles if necessary.
    // @visibility external
    // @group headerSpan
    // @group autoFitFields
    //<
    autoSizeHeaderSpans:false,


    //> @attr listGrid.autoFitClipFields (Array of String : null : IR)
    // If +link{listGrid.autoFitFieldWidths} is enabled and the calculated field sizes
    // are wide enough that horizontal scrolling would be introduced, this attribute may be
    // set to an array of fieldNames, causing those fields to be clipped rather than
    // forcing horizontal scrollbars to appear.
    // <P>
    // Note: If any +link{ListGridField.frozen,frozen columns} are included in this list they
    // will not be clipped.
    // @group autoFitFields
    // @visibility external
    //<

    //> @attr listGrid.autoFitFieldsFillViewport (Boolean : true : IR)
    // If +link{listGrid.autoFitFieldWidths} is enabled, and extra space is available after
    // autofitting all fields, should the grid automatically expand one field to fill the extra
    // space.
    // <P>
    // When enabled, the field to expand may be specified via +link{autoFitExpandField}.
    // <P>
    // Note this logic will not expand a +link{ListGridField.frozen,frozen column}.
    //
    // @group autoFitFields
    // @visibility external
    //<
    autoFitFieldsFillViewport:true,

    //> @attr listGrid.autoFitExpandField (String : null : IR)
    // The field to expand if +link{listGrid.autoFitFieldWidths} and
    // +link{autoFitFieldsFillViewport} are enabled and
    // auto-fitting will not fill all available horizontal space.
    // <P>
    // If unset, will default to the text field with the longest
    // +link{dataSourceField.length} if length is set, otherwise, the first text
    // field with no width specified.
    // <P>
    // Note that expanding +link{ListGridField.frozen,frozen columns} is not supported.
    // @group autoFitFields
    // @visibility external
    //<

    //> @type AutoFitWidthApproach
    // How should field width be determined when +link{listGridField.autoFitWidth} is true?
    // @value "value" Size field to fit to the data value(s) contained in the field.
    // @value "title" Size field to fit the field title
    // @value "both" Size field to fit either the field title or the data values in the field
    //  (whichever requires more space).
    //
    // @group autoFitFields
    // @visibility external
    //<

    //> @attr listGrid.autoFitWidthApproach (AutoFitWidthApproach : "value" : [IRW])
    // When a user requests column autofitting via the
    // +link{getHeaderContextMenuItems,header context menu} or via a
    // +link{headerAutoFitEvent,mouse gesture}, what autofit approach is used.
    // @group autoFitFields
    // @visibility external
    //<
    autoFitWidthApproach:"value",

    // If we're autoFitting to values, and we're showing a grid summary, should the
    // grid summary values be taken into account when sizing columns?
    // By default they are - set this flag to false to disable this behavior.
    includeGridSummaryInAutoFitWidth:true,

    //> @attr listGridField.autoFitWidthApproach (AutoFitWidthApproach : null : [IRW])
    // When a user requests column autofitting via the
    // +link{listGrid.getHeaderContextMenuItems,header contextmenu} or via a
    // +link{listGrid.headerAutoFitEvent,mouse gesture}, what autofit approach is used. If set, this
    // setting overrides the autoFitWidthApproach specified at the ListGrid level.
    // @group autoFitFields
    // @visibility external
    //<

    //> @type AutoFitIconFieldType
    // How should fields of +link{listGridFieldType,type:"icon"} be sized by default?
    // @value "none" Apply no special sizing to icon fields - treat them like any other
    //   field in the grid
    // @value "iconWidth" size the field to accommodate the width of the icon
    // @value "title" size the field to accommodate the title (or the width of the icon if
    //   it exceeds the width of the title.
    //
    // @group autoFitFields
    // @visibility external
    //<

    //> @attr listGrid.autoFitIconFields (AutoFitIconFieldType : "title" : [IRW])
    // SmartClient listGrids have special logic to automatically size fields that
    // are displayed as an icon - that is fields with
    // +link{listGridFieldType,type:"icon"}, fields displaying only
    // +link{listGridField.showValueIconOnly,value icons}, and boolean fields (which
    // are rendered as a checkmark type icon by default.
    // <P>
    // This attribute controls this behavior - governing whether icon fields should
    // be sized to fit their content (icon), title, or whether to disable this
    // behavior. Setting this value to <code>"title"</code> or <code>"iconWidth"</code>
    // will cause +link{listGridField.autoFitWidth} to be enabled by default for all
    // icon fields with the +link{listGridField.autoFitWidthApproach} set to
    // <code>"value"</code> or <code>"both"</code> as appropriate. Note that the
    // width required for the icons is calculated by +link{listGrid.getDefaultFieldWidth()}
    // which performs a simple calculation based on the specified icon width for these
    // types of fields.
    // <P>
    // This setting governs default behavior for icon fields - for specific fields within
    // a grid, this default behavior can be overridden by setting an explicit
    // +link{listGridField.width} or
    // explicitly enabling +link{listGridField.autoFitWidth} and setting
    // +link{listGridField.autoFitWidthApproach} on the field in question.
    // @see listGrid.autoFitFieldWidths
    // @group autoFitFields
    // @visibility external
    //<
    autoFitIconFields:"title",

    //> @attr listGrid.leaveScrollbarGap (Boolean : true : IRW)
    // Whether to leave a gap for the vertical scrollbar, even when it's not present.
    // <P>
    // Note that if leaveScrollbarGap is false and vertical scrolling is introduced, fields
    // will be resized to fit the smaller body area if possible, in order to avoid horizontal
    // scrolling also being required.
    //
    // @group appearance
    // @visibility external
    // @example autofitRows
    //<

    leaveScrollbarGap:true,

    // leaveScrollbarGap has meaning at the Layout level. We don't expect to ever show a vscrollbar
    // at the actual ListGrid level so override 'getBreadth' to avoid leaving a gap outside the
    // actual component members (header, body etc).
    getBreadth : function () {
        return this.getInnerWidth();
    },

    // if leaveScrollbarGap is false, whether to resize fields when vscrolling is introduced
    resizeFieldsForScrollbar:true,

    //>    @attr listGrid.autoFit (boolean : false : IRWA)
    // If true, make columns only wide enough to fit content, ignoring any widths specified.
    // Overrides fixedFieldWidths.
    // <P>
    // NOTE: the header does not automatically respond to expanded field widths
    //    @group    sizing
    //<

    //autoFit:false,

    //>    @attr listGrid.wrapCells (Boolean : false : IRWA)
    // Should content within cells be allowed to wrap?
    // <P>
    // Even if content is allowed to wrap, if +link{fixedRecordHeights} is set, the content
    // will be clipped off at the cell boundary.  Either set a larger, fixed +link{cellHeight}
    // to reveal more content, or set +link{fixedRecordHeights} to false to allow auto-sizing.
    //
    // @example autofitValues
    // @visibility external
    //<
    //wrapCells:false,

    //> @attr listGrid.showClippedValuesOnHover (Boolean : null : IRA)
    // @include gridRenderer.showClippedValuesOnHover
    //<
    showClippedValuesOnHover:null,

    //>    @attr listGrid.cellSpacing (number : 0 : [IRW])
    // @include gridRenderer.cellSpacing
    // @visibility internal
    //<

    cellSpacing:0,

    //>    @attr listGrid.cellPadding (number : 2 : [IRW])
    // @include gridRenderer.cellPadding
    //<
    cellPadding:2,

    //> @attr listGrid.dateFormatter (DateDisplayFormat : null : [IRW])
    // How should Date type values be displayed in this ListGrid by default?
    // <P>
    // This property specifies the default DateDisplayFormat to apply to Date values
    // displayed in this grid for all fields except those of +link{listGridField.type,type "time"}
    // (See also +link{listGrid.timeFormatter}).<br>
    // If +link{listGrid.datetimeFormatter} is specified, that will be applied by default
    // to fields of type <code>"datetime"</code>.
    // <P>
    // Note that if +link{listGridField.dateFormatter} or +link{listGridField.timeFormatter} are
    // specified those properties will take precedence over the component level settings.
    // <P>
    // If unset, date values will be formatted according to the system wide
    // +link{Date.setShortDisplayFormat(),short display format} or
    // +link{Date.setShortDatetimeDisplayFormat(),short datetime display format} for datetime type
    // fields.
    // <P>
    // If this field is editable the dateFormatter will also be passed to the editor created
    // to edit this field as +link{DateItem.dateFormatter, dateFormatter}.
    // In this case you may also need to set +link{listGrid.dateInputFormat}.
    //
    // @visibility external
    //<
    //dateFormatter:null,

    //> @attr listGrid.datetimeFormatter (DateDisplayFormat : null : [IRW])
    // Display format to use for fields specified as type 'datetime'.  Default is to use the
    // system-wide default date time format, configured via
    // +link{Date.setShortDatetimeDisplayFormat()}.  Specify any
    // valid +link{type:DateDisplayFormat} to change the display format for datetimes used by this grid.
    // <smartclient>
    // May be specified as a function. If specified as  a function, this function will be executed in the scope of the Date
    // and should return the formatted string.
    // </smartclient>
    // <P>
    // May also be specified at the field level via
    // +link{listGridField.dateFormatter}
    // <P>
    // If this field is editable the dateFormatter will also be passed to the editor created
    // to edit this field as +link{DateItem.dateFormatter, dateFormatter}.
    // In this case you may also need to set +link{listGrid.dateInputFormat}.
    //
    // @see listGridField.dateFormatter
    // @group appearance
    // @visibility external
    //<

    //> @attr listGrid.dateInputFormat (DateInputFormat : null : [IRWA])
    // If this is an editable listGrid, this property will specify the
    // +link{DateItem.inputFormat, inputFormat} applied to editors for fields of type
    // <code>"date"</code>. May be overridden per field via +link{listGridField.inputFormat}.
    // @see listGrid.dateFormatter
    // @visibility external
    //<

    // function to call appropriate date formatter
    // Note: this is executed in the scope of a field object - see 'applyFieldDefaults'
    _formatDateCellValue : function (value, field, grid, record, rowNum, colNum) {

        if (isc.isA.Date(value)) {
            // A developer may force a "date" or "datetime" type field value to be displayed as time
            // by specifying a timeFormatter and no dateFormatter on the field.
            if (grid._formatAsTime(field)) {
                var formatter = grid._getTimeFormatter(field);
                var isLogicalTime = isc.SimpleType.inheritsFrom(field.type, "time");
                return isc.Time.toTime(value, formatter, isLogicalTime);
            }

            var isDatetime = field && isc.SimpleType.inheritsFrom(field.type, "datetime"),
                isLogicalDate = !isDatetime && isc.SimpleType.inheritsFrom(field.type, "date"),
                formatter = grid._getDateFormatter(field);

            // rely on date.toShortDateTime() / toShortDate() to handle applying the
            // custom formatter if specified, otherwise picking up the appropriate system-wide
            // default for the data type.
            // The second parameter to toShortDateTime() explicitly causes the date to be displayed
            // in the custom timezone set up in Time.setDefaultDisplayTimezone
            if (isDatetime) return value.toShortDateTime(formatter, true);
            return value.toShortDate(formatter, !isLogicalDate);
        }
        return value;
    },

    // Date formatting helpers:
    // Called from formatDateCellValue() / formatTimeCellValue() [which are type-specific formatters
    // applied to fields as part of field init], and also as a catch-all for
    // Date type values in fields of some other specified data-type.
    // We use a consistent pattern across DataBoundComponents:
    // - developer can specify explicit dateFormatter / timeFormatter per field and they'll be used.
    //   if both are specified dateFormatter takes precedence except in "time" type fields.
    // - developer can specify dateFormatter, datetimeFormatter and timeFormatter per component and
    //   they'll be used if no per-field settings are found.

    // If a field has a JS Date value, should we format it as a time? True for "time" type fields
    // or fields with an explicit time formatter only.
    _formatAsTime : function (field) {
        if (field == null) return false;

        // If at the field level the timeFormatter is defined *(and there's no date formatter)
        // respect it.
        if (field.timeFormatter != null && field.dateFormatter == null) return true;
        // timeFormatter null and dateFormatter non-null --> format as date

        if (field.dateFormatter != null && field.timeFormatter == null) return false;
        return isc.SimpleType.inheritsFrom(field.type, "time");
    },

    _getDateFormatter : function (field) {

        if (field == null) return this.dateFormatter;

        if (field.dateFormatter != null) return field.dateFormatter;
        // displayFormat is back-compat at this point - only applies to fields of type
        // date or datetime (or subtypes thereof)
        if (field.displayFormat != null && isc.SimpleType.inheritsFrom(field.type, "date")) {
            return field.displayFormat;
        }

        if (this.datetimeFormatter != null && isc.SimpleType.inheritsFrom(field.type, "datetime")) {
            return this.datetimeFormatter;
        }

        return this.dateFormatter;
    },

    // This picks up *explicit* dateInputFormat to pass through to the edit-item. No need to
    // include logic to derive from the display format if no explicit input format was specified,
    // that'll be handled by the FormItem code.
    _getDateInputFormat : function (field) {
        var inputFormat;
        if (field) inputFormat = field.inputFormat
        if (!inputFormat) inputFormat = this.dateInputFormat;
        return inputFormat;
    },

    // function to call appropriate number formatter
    // If no number formatter is defined, the default formatter will be used [standard 'toString']
    // is the default
    _formatNumberCellValue : function (value, field, grid, record, rowNum, colNum) {
        if (isc.isA.Number(value)) {
            if (isc.SimpleType.inheritsFrom(field.type, "float") &&
                (field.decimalPrecision != null || field.decimalPad != null))
            {
                return isc.Canvas.getFloatValueAsString(
                        value, field.decimalPrecision, field.decimalPad);
            } else if (field.precision != null) {
                return isc.Canvas.getNumberValueAsString(value, field.precision, field.type);
            } else {
                var formatter = (field.numberFormatter || field.formatter || grid.numberFormatter);
                return value.toFormattedString(formatter);
            }
        }

        // If passed a non-number just return it
        return value;
    },

    //>    @attr listGrid.timeFormatter (TimeDisplayFormat : "toShortPaddedTime" : [IRW])
    // Display format to use for fields specified as type 'time'.  May also be specified at
    // the field level via +link{listGridField.timeFormatter}.<br>
    // If unset, time fields will be formatted based on the system wide
    // +link{Time.shortDisplayFormat}.<br>
    // If this field is editable, the timeFormatter will also be passed to the editor
    // created to edit any time type fields as +link{TimeItem.timeFormatter}
    // @group appearance
    // @visibility external
    //<
    timeFormatter:"toShortPaddedTime",

    _getTimeFormatter : function (field) {
        if (field != null) {
            if (field.timeFormatter != null) return field.timeFormatter;
            if (field.displayFormat != null && isc.SimpleType.inheritsFrom(field.type, "time")) {
                return field.displayFormat;
            }
        }
        return this.timeFormatter;
    },

    // function to call appropriate time formatter
    // Note: this is executed in the scope of a field object - see 'applyFieldDefaults'
    _formatTimeCellValue : function (value, field, grid, record, rowNum, colNum) {
        var time = value;
        if (isc.isA.String(time)) {
            // Pass in the 'validTime' param - If we're given a string which doesn't
            // parse to a time we don't want to display "12:00 am"
            time = isc.Time.parseInput(time, true);
        }
        if (isc.isA.Date(time)) {
            // If dateFormatter is set on the field, and timeFormatter is not, we respect it
            // even for fields of explicit type "time"
            if (!grid._formatAsTime(field)) {

                return time.toShortDate(grid._getDateFormatter(field), true);
            }
            var formatter = grid._getTimeFormatter(field);

            // If we're passed an invalid formatter
            return isc.Time.toTime(time, formatter, true);
        }
        return value;
    },

    _formatBinaryCellValue : function (value, field, grid, record, rowNum, colNum) {

        if (isc.isA.String(value)) return value;
        if (record == null) return null;

        var fieldName = field.name,
            ds = grid.getDataSource(),
            filenameField = (ds ? ds.getFilenameField(fieldName) : null) || fieldName + "_filename",
            fileName = record[filenameField],
            value
        ;

        if (field.type=="imageFile" && field.showFileInline == true) {
            var urlProperty = fieldName + "_imgURL";

            if (!record[urlProperty]) {
                var dimensions = isc.Canvas.getFieldImageDimensions(field, record),
                    image = grid.getDataSource().getFileURL(record, field.name);

                dimensions.width = dimensions.width || grid.imageSize;
                dimensions.height = dimensions.height || grid.imageSize;
                value = record[urlProperty] =
                    isc.Canvas.imgHTML(image, dimensions.width, dimensions.height,
                                       null, null, isc.Canvas._$allowRelativeSrc);
            } else
                value = record[urlProperty];
        } else {
            if (field.showFileInline == true) { // non-imageFile field
                this.logWarn("_formatBinaryCellValue(): Unsupported field-type for showFileInline: "+field.type);
            }


            if (!field.filenameSuppressed && (fileName == null || isc.isAn.emptyString(fileName))) {
                return this.emptyCellValue;
            }

            var viewAction = "'" + grid.getID() +".view",
                dlAction = "'" + grid.getID() +".download",
                completion = "";
            if (field && field.name) {
                completion = "Cell(" + rowNum + ", \"" + field.name + "\")'";
            } else {
                completion = "Row(" + rowNum + ")'";
            }
            var viewIconHTML = isc.Canvas.imgHTML({
                src: "[SKIN]actions/view.png",
                width: 16,
                height: 16,
                extraCSSText: "cursor:" + isc.Canvas.HAND,
                extraStuff: " onclick=" + viewAction + completion
            });
            var downloadIconHTML = isc.Canvas.imgHTML({
                src: "[SKIN]actions/download.png",
                width: 16,
                height: 16,
                extraCSSText: "cursor:" + isc.Canvas.HAND,
                extraStuff: " onclick=" + dlAction + completion
            });

            value = viewIconHTML + "&nbsp;" + downloadIconHTML +
                (fileName ? "&nbsp;" + fileName : "");
        }

        return value;
    },

    // value is rendered as an anchor - the href and name of the anchor is the value.  By default
    // opens in a new browser window - this can be overridden by setting the 'target' property on
    // the record.
    //
    // The name of the link can be overridden by setting the 'linkName' property on the record.  By
    // default we use the value.
    _$linkTemplate:[
        "<a href='",
        ,   // 1: HREF
        "' target='",
        ,   // 3: name of target window
        // onclick handler enables us to prevent popping a window if (EG) we're masked.
        //                      5: ID
        "' onclick='if(window.",     ,") return ",
                //  7:ID                         9:rowNum,     11:colNum
                         ,"._linkClicked(event,",        ,",",          ,");'>",
        ,   // 13: link text
        "</a>"
    ],
    _$doubleEscapedQuote:"\\'",
    _$_blank:"_blank",

    _formatLinkCellValue : function (value, field, grid, record, rowNum, colNum) {
        if (value == null || isc.is.emptyString(value)) return value;

        // target window
        var target = field.target ?
                        field.target.replaceAll(grid._$singleQuote, grid._$doubleEscapedQuote) :
                        grid._$_blank;
        // get the linkText property. If defined on the field, use that, otherwise
        // use the linkTextProperty from the grid.
        var linkTextProp = field.linkTextProperty ? field.linkTextProperty : grid.linkTextProperty;
        var linkText = (record && record[linkTextProp]) ? record[linkTextProp]
                                                            : field.linkText || value;

        // link URL
        var href = value;

        if (field.linkURLPrefix) href = field.linkURLPrefix + href;
        if (field.linkURLSuffix) href = href + field.linkURLSuffix;

        href = href.replaceAll(grid._$singleQuote, grid._$doubleEscapedQuote);

        if (target == "javascript") {
            // target is "javascript" - make the link inert and have the cellClick event fired
            // instead
            href = "javascript:void";
        }

        // combine
        var template = grid._$linkTemplate;
        template[1] = href;
        template[3] = target;
        var ID = grid.getID();
        template[5] = ID;
        template[7] = ID;
        template[9] = rowNum;
        template[11] = colNum;
        template[13] = linkText;

        return template.join(isc.emptyString);
    },

    _linkClicked : function (event, rowNum, colNum) {
        // don't allow the click if the cell should not be interactive.
        var record = this.getRecord(rowNum),
            mustCancel = (
                this.destroyed || !this.isDrawn() || !this.isVisible() ||
                isc.EH.targetIsMasked(this.body) ||
                !this.recordIsEnabled(record, rowNum, colNum)),
            field = this.getField(colNum);

        if (event.target == "javascript" || field.target == "javascript") {
            mustCancel=true;
            this.cellClick(record, rowNum, colNum);
        }

        if (mustCancel) {

            if (!isc.Browser.isIE) {
                event.preventDefault();
            }

            return false;
        }
        return true;
    },

    //> @attr listGrid.linkTextProperty (string : "linkText" : [IRW])
    // Property name on a record that will hold the link text for that record.
    // <P>
    // This property is configurable to avoid possible collision with data values in the
    // record.
    // <P>
    // Use +link{listGridField.linkTextProperty} if you have more than one link field and
    //
    // @see type:ListGridFieldType
    // @see type:FieldType
    // @see attr:listGridField.linkText
    // @see attr:listGridField.linkTextProperty
    // @group  display_values
    // @visibility external
    //<
    linkTextProperty : "linkText",

    // value is a URL to an image
    _formatImageCellValue : function (value, field, grid, record, rowNum, colNum) {
        // if no value is stored, just return an empty string so we don't render a broken image
        if (value == null || isc.isAn.emptyString(value)) return isc.emptyString;

        // if any of field.imageWidth/Height/Size are set as strings, assume they are property
        // names on the record

        var dimensions = isc.Canvas.getFieldImageDimensions(field, record);

        dimensions.width = dimensions.width || grid.imageSize;
        dimensions.height = dimensions.height || grid.imageSize;


        var src = value,
            prefix = field.imageURLPrefix || field.baseURL || field.imgDir;

        // If imageURLSuffix is specified, apply it to the value
        if (field.imageURLSuffix != null) src += field.imageURLSuffix;



        return isc.Canvas.imgHTML(src, dimensions.width, dimensions.height, null, field.extraStuff,
                                  prefix, field.activeAreaHTML);
    },

    // show field.icon in the cell
    _formatIconCellValue : function (value, field, grid, record, rowNum, colNum) {
        // prevent an icon from being shown in the filter editor if the field has canFilter
        if (isc.isA.RecordEditor(grid) && grid.isAFilterEditor() && field.canFilter == false) return null;

        if (field._iconHTML) return field._iconHTML;

        field._iconHTML = isc.Canvas.imgHTML(field.cellIcon || field.icon,
                                             field.iconWidth || field.iconSize || grid.imageSize,
                                             field.iconHeight || field.iconSize || grid.imageSize);
        return field._iconHTML;
    },

    // CSS styles
    // --------------------------------------------------------------------------------------------

    //> @attr listGrid.fastCellUpdates (Boolean: true : I)
    // @include gridRenderer.fastCellUpdates
    // @group performance
    //<
    // explicitly set fastCellUpdates at the LG level
    // this will be passed through to our body and allows us to check
    // this.fastCellUpdates directly rather than looking at the attribute on the body

    fastCellUpdates:isc.Browser.isIE && !isc.Browser.isIE9,

    //> @method listGrid.setFastCellUpdates()
    // @include gridRenderer.setFastCellUpdates()
    // @visibility external
    //<
    // explicit implementation keeps this.fastCellUpdates in synch with the version in the
    // body so we can check it directly in this.getBaseStyle
    setFastCellUpdates : function (fcu) {
        if (this.body != null) {
            this.body.setFastCellUpdates(fcu);
            // if the body refused to set to the specified
            // value, respect that.
            fcu = this.body.fastCellUpdates;
        }
        if (this.frozenBody != null) {
            this.frozenBody.setFastCellUpdates(fcu);
        }
        this.fastCellUpdates = fcu;
    },

    //>    @attr listGrid.baseStyle (CSSStyleName : null : [IR])
    // +link{gridRenderer.baseStyle,base cell style} for this listGrid.
    // If this property is unset, base style may be derived from +link{listGrid.normalBaseStyle}
    // or +link{listGrid.tallBaseStyle} as described in
    // +link{listGrid.getBaseStyle()}.
    // @visibility external
    // @group appearance
    //<

    //>    @attr listGrid.normalBaseStyle (CSSStyleName : "cell" : [IR])
    // "Normal" baseStyle for this listGrid. Only applies if +link{listGrid.baseStyle} is
    // set to null.
    // <P>
    // If <code>baseStyle</code> is unset, this
    // property will be used as a base cell style if the grid is showing fixed height rows, and
    // the specified cellHeight matches +link{listGrid.normalCellHeight} (and in Internet Explorer,
    // +link{listGrid.fastCellUpdates} is false). Otherwise +link{listGrid.tallBaseStyle} will
    // be used.
    // <P>
    // Having separate styles defined for fixed vs. variable height rows allows the developer
    // to specify css which is designed to render at a specific height (typically using
    // background images, which won't scale), without breaking support for styling rows
    // of variable height.
    //
    // @see listGrid.getBaseStyle()
    // @visibility external
    //<
    normalBaseStyle:"cell",

    //>    @attr listGrid.tallBaseStyle (CSSStyleName : "cell" : [IR])
    // "Tall" baseStyle for this listGrid. Only applies if +link{listGrid.baseStyle} is
    // set to null.
    // <P>
    // If <code>baseStyle</code> is unset, this
    // property will be used as a base cell style unless the grid is showing fixed height
    // rows with a specified cellHeight that matches +link{listGrid.normalCellHeight}, in
    // which case +link{listGrid.normalBaseStyle} will be used. Note that in Internet Explorer
    // if +link{listGrid.fastCellUpdates} is true, <code>tallBaseStyle</code> will also be
    // used even if the cellHeight matches the specified <code>normalCellHeight</code> for the
    // grid.
    //
    // @see listGrid.getBaseStyle()
    // @visibility external
    //<
    tallBaseStyle:"cell",




    //>    @attr listGrid.editFailedBaseStyle (CSSStyleName : null : [IRWA])
    //  A base name for the CSS class applied to cells when editing has failed.<br>
    //  If this listGrid is editable, this style will be applied to any edited cells for which
    //  validation failed.<br>
    //  As with the default 'baseStyle' property, this style will have "Dark", "Over", "Selected",
    //  or "Disabled" appended to it according to the state of the cell.<br>
    // If null, cells for which editing has failed will be rendered using the normal base style
    // classNames, but with custom CSSText applied as derived from <code>this.editFailedCSSText</code>
    // @visibility external
    // @group    appearance
    // @see baseStyle
    // @see editFailedCSSText
    //<
    editFailedBaseStyle:null,   //"cellEditFailed",

    //>    @attr listGrid.editFailedCSSText (string : "color:red;border:1px solid red;" : [IRWA])
    //  Custom CSS text to be applied to cells when editing has failed.<br>
    //  If this listGrid is editable, this css text will be applied to any edited cells for which
    //  validation failed, on top of the base style for the cell.<br>
    // For further customization of styling for cells that failed editing validation, use
    // <code>this.editFailedBaseStyle</code> instead.
    // @visibility external
    // @group    appearance
    // @see editFailedBaseStyle
    //<
    editFailedCSSText:"color:red;border:1px solid red;",

    //>    @attr listGrid.editPendingBaseStyle (CSSStyleName : null : [IRA])
    // A base name for the CSS class applied to cells containing pending (unsaved) edits<br>
    // As with the default 'baseStyle' property, this style will have "Dark", "Over", "Selected",
    // or "Disabled" appended to it according to the state of the cell.
    // <P>
    // If this property is null (the default setting), cells with pending edits will pick up
    // custom css text to be applied on top of the normal base style from
    // <code>this.editPendingCSSText</code>.
    //
    // @group appearance
    // @see baseStyle
    // @visibility external
    //<
    editPendingBaseStyle:null, //"cellEditPending",

    //>    @attr listGrid.editPendingCSSText (string : "color:#0066CC;" : [IRWA])
    // Custom CSS text to be applied to cells with pending edits that have not yet been
    // submitted.<br>
    // For further customization of styling for cells with pending edits use
    // <code>this.editPendingBaseStyle</code> instead.
    // @group appearance
    // @see editFailedBaseStyle
    // @visibility external
    //<
    editPendingCSSText:"color:#0066CC;",

    //> @attr listGrid.recordCustomStyleProperty (String : "customStyle" : IRW)
    // @include GridRenderer.recordCustomStyleProperty
    // @visibility external
    // @see listGrid.getCellStyle()
    // @see listGrid.recordBaseStyleProperty
    //<
    recordCustomStyleProperty:"customStyle",

    //> @attr listGrid.recordBaseStyleProperty (string : "_baseStyle" : [IRWA])
    // This attribute allows custom base styles to be displayed on a per-record basis.
    // To specify a custom base-style for some record set
    // <code>record[listGrid.recordBaseStyleProperty]</code> to the desired base style name -
    // for example if <code>recordBaseStyleProperty</code> is <code>"_baseStyle"</code>, set
    // <code>record._baseStyle</code> to the custom base style name.
    //
    // @visibility external
    // @group appearance
    // @see listGrid.baseStyle
    //<
    recordBaseStyleProperty:"_baseStyle",

    //> @attr listGrid.frozenBaseStyle (string : null : [IRW])
    // If this listGrid contains any frozen fields, this property can be used to apply a custom
    // baseStyle to all cells in those frozen fields. If unset, the standard base style will be
    // used for both frozen and unfrozen cells.
    // @visibility external
    // @group appearance, frozenFields
    // @see listGrid.baseStyle
    // @see listGridField.frozen
    //<

    //> @attr listGrid.shrinkForFreeze (Boolean : false : IRWA)
    // If this list grid is showing any +link{listGridField.frozen,frozen} fields, and a horizontal
    // scrollbar is visible at the bottom of the liquid columns, should an equivalent scrollbar gap
    // be left visible below the frozen columns?<br>
    // Note that if set to <code>true</code> any backgroundColor or border applied to the ListGrid
    // will show up below the bottom row of the frozen column(s).
    // @group frozenFields
    // @visibility external
    //<
    shrinkForFreeze:false,

    //> @attr listGrid.alternateRecordStyles (Boolean : false : [IRW])
    // @include gridRenderer.alternateRowStyles
    // @group cellStyling
    // @example gridCells
    //<
    //alternateRecordStyles:false,

    //> @attr listGrid.alternateFieldStyles (boolean : false : [IRW])
    // @include gridRenderer.alternateColumnStyles
    // @visibility external
    // @group cellStyling
    //<
    //alternateFieldStyles:false,

    //> @attr listGrid.alternateFieldFrequency (number : 1 : [IRW])
    // @include gridRenderer.alternateColumnFrequency
    // @visibility external
    // @group cellStyling
    //<
    alternateFieldFrequency:1,

    //> @attr listGrid.alternateBodyStyleName (CSSStyleName : null : [IRWA])
    // Optional css style to apply to the body if +link{listGrid.alternateRecordStyles} is true
    // for this grid. If unset +link{listGrid.bodyStyleName} will be used to style the body
    // regardless of the +link{listGrid.alternateRecordStyles,alternateRecordStyles} setting.
    // @visibility external
    //<

    //> @attr listGrid.alternateRecordFrequency (number : 1 : [IRW])
    // @include gridRenderer.alternateRowFrequency
    // @group cellStyling
    //<
    alternateRecordFrequency:1,

    // property you can set per-record to add custom CSSText
    recordCSSTextProperty : "cssText",

    //> @attr listGrid.includeHilitesInSummaryFields
    // @include dataBoundComponent.includeHilitesInSummaryFields
    // @visibility external
    //<

    //> @method listGrid.shouldIncludeHiliteInSummaryField()
    // @include dataBoundComponent.shouldIncludeHiliteInSummaryField
    // @visibility external
    //<

    //>    @attr listGrid.showHiliteInCells (boolean : false : IRWA)
    // When cell styling is being updated (updateCellStyle()), should the HTML content of the
    // cell also be updated?  If false, only the cell's CSS styling will be updated.
    //
    // You should turn this on if you've implemented
    // +link{listGrid.formatCellValue(),formatting} that adds styling cues to a cell
    // (like an inline image), which need be updated as the cell switches states.
    // (eg, if you would use different HTML for a selected cell's contents).
    //        @group    hiliting, drawing
    //<
    //showHiliteInCells:false,

    //> @attr listGrid.showHilitesInGroupSummary (boolean : true : IRW)
    // Determines whether hiliting for any field in this grid is shown in a group summary.
    // This setting affects all fields of the grid.
    // <P>
    // To suppress hilites for a specific field see +link{listGridField.showHilitesInGroupSummary}.
    // <P>
    // Hiliting in summary fields (columns) can be enabled by setting
    // +link{listGrid.includeHilitesInSummaryFields,includeHiliteInSummaryField} to true.
    //
    // @visibility external
    //<
    showHilitesInGroupSummary:true,

    //> @attr listGrid.hiliteCanReplaceValue (boolean : null : IR)
    // If set, end users can create advanced hiliting rules that will use the
    // +link{hilite.replacementValue} feature to cause values in hilited cells
    // to be replaced with a user-entered value.  For example, a user could create a hilite rule
    // that replaces numeric values ranging from 0.5 to 1.0 with the text "LOW".
    // <p>
    // Specifically, when the "Add Advanced Rule" button is pressed and
    // <code>hiliteCanReplaceValue</code> is true, the user will see a text entry field titled
    // "Replace value with" (+link{hiliteReplaceValueFieldTitle}) and if they enter a value, that
    // value will appear in the grid cell in lieue of the cell's original value.
    //
    // @group hiliting
    // @visibility external
    //<

    //> @attr listGrid.hiliteReplaceValueFieldTitle (string : "Replace value with" : IR)
    // Title used for the text box shown when +link{listGrid.hiliteCanReplaceValue} is set.
    // @group i18nMessages
    // @visibility external
    //<
    hiliteReplaceValueFieldTitle : "Replace value with",

    //> @attr listGrid.hiliteHTMLAfterFormat (boolean : true : IR)
    // If set to true, custom HTML applied as part of hiliting will be applied after
    // +link{listGrid.formatCellValue(),formatting} for each cell. If false, hilite
    // HTML will be applied before formatting.
    // <P>
    // This applies to the following hilite properties:
    // <ul>
    // <li>+link{Hilite.replacementValue}</li>
    // <li>+link{Hilite.htmlBefore}</li>
    // <li>+link{Hilite.htmlAfter}</li>
    // <li>+link{Hilite.htmlValue}</li>
    // </ul>
    // <P>
    // May be overridden per field via +link{listGridField.hiliteHTMLAfterFormat}
    //
    // @visibility external
    //<
    // Also the undocumented
    // <li>+link{Hilite.htmlOpposite}</li>
    hiliteHTMLAfterFormat:true,

    //> @attr listGridField.hiliteHTMLAfterFormat (Boolean : null : IR)
    // If set to true, custom HTML applied as part of hiliting will be applied after
    // +link{listGrid.formatCellValue(),formatting} for each cell in this column. If false, hilite
    // HTML will be applied before formatting.
    // <P>
    // This attribute overrides +link{listGrid.hiliteHTMLAfterFormat} as defined at the
    // component level.
    //
    // @visibility external
    //<

    //> @attr listGrid.showSelectedStyle (Boolean : true : IRW )
    // @include gridRenderer.showSelectedStyle
    //<
    showSelectedStyle : true,

    // Keyboard handling
    // ---------------------------------------------------------------------------------------

    //> @attr listGrid.generateClickOnSpace (Boolean : true : IRWA)
    // If true, when the user navigates to a cell using arrow keys and hits space,
    // the cell will respond to a click event.
    // @visibility external
    //<
    generateClickOnSpace : true,

    //> @attr listGrid.generateClickOnEnter (Boolean : false : IRWA)
    // If true, when the user navigates to a cell using arrow keys and hits Enter,
    // the cell will respond to a click event.
    // @visibility external
    //<
    //generateClickOnEnter : false,

    //> @attr listGrid.generateDoubleClickOnSpace (Boolean : false : IRWA)
    // If true, when the user navigates to a cell using arrow keys and hits Space,
    // the cell will respond to a double click event.
    // @visibility external
    //<
    // generateDoubleClickOnSpace : false,

    //> @attr listGrid.generateDoubleClickOnEnter (Boolean : true : IRWA)
    // If true, when the user navigates to a cell using arrow keys and hits Enter,
    // the cell will respond to a double click event.
    // @visibility external
    //<
    generateDoubleClickOnEnter : true,

    //> @attr listGrid.arrowKeyAction (string : null : IRWA)
    // Action to perform when the listGrid has keyboard focus (but not editing focus) and a user
    // presses the up or down arrow key. Possible values are:
    // <ul>
    // <li><code>"select"</code> : select the next row in the list (calls <code>recordClick</code> handler)</li>
    // <li><code>"focus"</code> : move focus to the next row in the list without changing the selection</li>
    // <li><code>"activate"</code> : select and activate the next row in the list (calls
    //  <code>recordDoubleClick</code> handler)</li>
    // <li><code>"none"</code> : no action</li>
    // <li> <code>null</code> : if +link{listGrid.selectionAppearance} is "checkbox", behaves as if set
    //  to "focus"; otherwise, behaves as if set to "select"</li>
    // </ul>
    // @group events
    // @visibility external
    //<
    arrowKeyAction: null,

    //> @attr ListGrid.hiliteRowOnFocus (Boolean : null : IRW)
    // When the grid body gets keyboard focus, should we highlight the current focus row,
    // using the rollover cell style?
    // <P>
    // This property may be explicitly set to control this behavior independently of
    // +link{showRollOver}.
    // Otherwise (if this property is null), we will show the roll-over styling for the
    // keyboard focus row if +link{showRollOver} is true.
    // @visibility external
    //<
//    hiliteRowOnFocus:null,

    //> @attr listGrid.showRecordComponents (boolean : null : IRWA)
    // If set to true, this listGrid should create and show an embedded component in every
    // row of the grid.
    // <P>
    // Developers using this feature should implement the
    // +link{listGrid.createRecordComponent()} and +link{listGrid.updateRecordComponent()}
    // methods.
    // <P>
    // createRecordComponent() will be called by the grid as rows are rendered to create the
    // record components to use per record or per cell.  Your implementation should return a
    // component to embed in the record passed in. Note that this method should create and
    // return a new component each time it is called.
    // <P>
    // This feature also supports reusing components in different rows in the grid. If
    // +link{listGrid.recordComponentPoolingMode} is set to <code>"recycle"</code>,
    // components created by the <code>createRecordComponent</code>
    // method will become available for reuse when they are no longer associated with a record.
    // The system will automatically store these in a pool. When a record with no
    // associated component is rendered, if there are any recordComponents in this pool,
    // the system will call +link{listGrid.updateRecordComponent()}, and pass in the component.
    // This allows the developer to apply record-specific attributes to an already created
    // component and render it out into the new record. This greatly improves performance for
    // large grids as it allows a small number of components to be created and reused rather
    // than maintaining potentially one record component for every cell in the grid.
    // <P>
    // Record components are refreshed according to the recordComponentPooling mode. If set to
    // <code>data</code> components will be maintained as long as their associated record remains
    // present in the data set (but this can require a component to be created for every record so
    // is not desirable for large data sets). Otherwise record components are refreshed as they
    // are scrolled into view. Note that you can explicitly refresh record components via
    // +link{listGrid.invalidateRecordComponents()} and +link{listGrid.refreshRecordComponent()}
    // <P>
    // NOTE: recordComponents can have an impact on row height and therefore may require
    // +link{listGrid.virtualScrolling}.
    // This is not supported in conjunction with +link{ListGridField.frozen,frozen fields}.
    // If you are using recordComponents in a listGrid with frozenFields, you can specify an
    // explicit +link{listGrid.recordComponentHeight} to ensure every row in the grid renders
    // tall enough to accommodate the recordComponents, and as such virtual scrolling is not
    // required.
    //
    // @see recordComponentPosition
    // @see showRecordComponentsByCell
    // @see recordComponentPoolingMode
    // @see showRecordComponent()
    // @see createRecordComponent()
    // @see updateRecordComponent()
    //
    // @visibility external
    //<

    //> @type EmbeddedPosition
    // How a component should be embedded within its record or cell
    // @value "expand" component should be placed underneath normal record or cell content,
    //        expanding the records.  Expanding records can result in variable height rows,
    //        in which case +link{listGrid.virtualScrolling,virtualScrolling} should be
    //        enabled.
    // @value "within" component should be placed within the normal area of the record or cell.
    //        Percentage sizes will be treated as percentages of the record and
    //        +link{Canvas.snapTo} positioning settings are also allowed and refer to the
    //        rectangle of the record or cell. Note that for components embedded within cells,
    //        cell align and vAlign will be used if snapTo is unset (so top / left alignment
    //        of cell content will map to snapTo of "TL", etc).
    // @visibility external
    //<

    //> @attr listGrid.recordComponentPosition (EmbeddedPosition : null : IRW)
    // if +link{listGrid.showRecordComponents} is true, how should the component appear within
    // the cell. Valid options are
    // <ul><li><code>"within"</code>: the component will be rendered inside the record / cell.
    //  +link{canvas.snapTo} may be set to specify where the component should render within
    //  the row or cell, and +link{canvas.snapOffsetTop} / +link{canvas.snapOffsetLeft} may
    //  be set to indent recordComponents within their parent cells.
    //  Note that if unset, the component will show up at the top/left edge
    //  for components embedded within an entire row, or for per-cell components, cell
    //  align and valign will be respected.  Note also that, when rendering components "within"
    //  cells, specified component heights will be respected and will change the height of the
    //  row.  However, if you want components to completely fill a cell at it's default height,
    //  set height: "100%" or rows will render at the default height of the component. </li>
    // <li><code>"expand"</code>: the component will be written into the cell below the
    //  normal cell content, causing the cell to expand vertically to accommodate it.
    // <li><code>null</code>: If this attribute is unset, we will default to showing
    //  recordComponents with position <code>"within"</code> if
    //  +link{showRecordComponentsByCell} is true, otherwise using <code>"expand"</code>
    //  logic.
    // </ul>
    // @see showRecordComponents
    // @visibility external
    //<

//    recordComponentPosition:"expand",


    //> @attr listGrid.showRecordComponentsByCell (boolean : null : IRWA)
    // If true, shows +link{listGrid.showRecordComponents, recordComponents} in cells, rather
    // than just in records.
    // @visibility external
    //<

    //> @type RecordComponentPoolingMode
    // The method of component-pooling to employ for +link{listGrid.showRecordComponents,recordComponents}.
    // <P>
    // @value "viewport" components are destroyed when the record is not being rendered.  Best
    //        for large datasets where embedded components differ greatly per record.
    // @value "data" components are +link{canvas.clear,clear()ed} when not in the viewport, but
    //        stay with a record until the record is dropped from cache.  Best for guaranteed
    //        small datasets.
    // @value "recycle" components are pooled and will be passed to
    //       +link{listGrid.updateRecordComponent,updateRecordComponent()} with the
    //       <code>recordChanged</code> parameter set to true.  Best for large datasets where
    //       embedded components are uniform across different records and can be efficiently
    //       reconfigured to work with a new record
    // @visibility external
    //<

    //> @attr listGrid.recordComponentPoolingMode (RecordComponentPoolingMode : "viewport" : IRWA)
    // The method of +link{type:RecordComponentPoolingMode, component-pooling} to employ for
    // +link{showRecordComponents,recordComponents}.
    // <P>
    // The default mode is "viewport", which means that recordComponents are destroyed as soon
    // their record leaves the viewport.
    // <P>
    // For the most efficient implementation, switch to "recycle" mode, which pools components
    // when records leave the viewport and re-uses them in other records.  In this mode, you
    // should implement +link{listGrid.updateRecordComponent, updateRecordComponent()} to
    // apply any changes to make reused components applicable to the new record they appear in,
    // if necessary.  If you have components of different types in different columns and still
    // want to take advantage of component recycling, you can
    // set +link{listGrid.poolComponentsPerColumn} to ensure that components intended for one
    // column are not recycled for use in another column that should have a different component.
    // <P>
    // Note that, if different records have distinctly different components embedded
    // in them, or multiple columns in each record embed different components, you should
    // leave the recordComponentPoolingMode at "viewport" if your dataset is very large or
    // use "data" otherwise.
    // @visibility external
    //<
    recordComponentPoolingMode:"viewport",

    //> @attr listGrid.poolComponentsPerColumn (Boolean : true : IRW)
    // Should recycled +link{listGrid.showRecordComponents,record components}, be pooled
    // per column or per record. Only applies if +link{listGrid.showRecordComponentsByCell} is true.
    // <P>
    // When +link{listGrid.recordComponentPoolingMode} is "recycle" and you have components of
    // different types in different columns, set this property to true to ensure that
    // components intended for one column are not recycled for use in another column that
    // should have a different component.
    // <P>
    // If no components applicable to a particular column are available in the pool, the system
    // calls +link{listGrid.createRecordComponent, createRecordComponent}.
    //
    // @visibility external
    //<

    poolComponentsPerColumn:true,

    // Rollover
    // --------------------------------------------------------------------------------------------
    //> @attr listGrid.showRollOver (Boolean : see below : IRW)
    // Should a different styling be shown for the cell that the mouse is over? By default, this is
    // <code>true</code>, except on touch devices (+link{Browser.isTouch} is <code>true</code>)
    // for which the default <code>showRollOver</code> setting is <code>false</code>.
    // <p>
    // If <code>true</code>, the cell style will have an "Over" suffix.
    // @see attr:baseStyle
    // @group appearance
    // @visibility external
    //<
    showRollOver:!isc.Browser.isTouch,

    //> @attr listGrid.useCellRollOvers (Boolean : null : IRW)
    // Are rollovers cell-level or row-level?
    // @visibility external
    //<

    //> @attr listGrid.showRollOverCanvas (Boolean : null : IRWA)
    // When enabled, when the mouse moves over a row or cell (depending on
    // +link{useCellRollOvers}), an arbitrary Canvas can be shown layered on top of the row or
    // cell (the +link{listGrid.rollOverCanvas}), layered underneath the row or cell (the
    // +link{listGrid.rollUnderCanvas}), or both.
    // <P>
    // This can be used to dynamically show controls or informational displays only on
    // rollover.  For example, controls to delete a row might appear only on rollover so they
    // do not clutter the static display, or a "rollunder" Canvas could be used to display
    // additional information that can appear behind normal cell values (like displaying
    // percent complete via as a bar of color that appears behind text values).
    // <p>
    // +link{canvas.snapTo,snapTo positioning} can be used to place the
    // rollOver/rollUnderCanvas.  With <code>useCellRollOvers</code>, positioning is relative
    // to the cell, for row-level rollOver, position is relative to the portion of the
    // row that is scrolled into view (this implies a row-level rollOver/UnderCanvas can never
    // be placed horizontally scrolled out of view, but this is possible for a cell-level
    // rollOver).
    // <p>
    // <code>snapTo</code> positioning makes it easy to do something like place a button at the
    // right edge of the grid, next to the scrollbar: just set snapTo:"R" on the
    // <code>rollOverCanvas</code>.
    // <p>
    // The rollOver/rollUnder Canvas can be a single static component (the same for all
    // cells/rows) configured via the +link{AutoChild} system, or can instead be provided
    // dynamically by implementing +link{getRollOverCanvas()} and/or +link{getRollUnderCanvas()}.
    // <p>
    // With row-level rollOvers with +link{listGridField.frozen,frozen fields}, the
    // <code>rollOverCanvas</code> and <code>rollUnderCanvas</code> appear only for non-frozen
    // fields.  This is usually not an issue for something like rollOver controls for row
    // deletion, but will prevent certain scenarios of using rollOverCanvas/rollUnderCanvas for
    // visual styling.
    // <p>
    // <code>showRollOverCanvas</code> has no effect if +link{showRollOver} is <code>false</code>.
    // @example gridRollOverReticleEffect
    // @example rolloverControls
    // @group rowEffects
    // @see ListGrid.showRollUnderCanvas
    // @visibility external
    //<
    //showRollOverCanvas:null,

    //> @attr listGrid.rollOverCanvas (AutoChild Canvas : null : RA)
    // AutoChild created and embedded in the grid if +link{ListGrid.showRollOver,showRollOver}
    // is <code>true</code> and +link{ListGrid.showRollOverCanvas,showRollOverCanvas} is
    // <code>true</code>. This component will be created and displayed above the current rollOver
    // row or cell.
    // <p>
    // The <code>rollOverCanvas</code> has the following read-only attributes set:<br>
    // - <code>this.grid</code> - a pointer to the grid<br>
    // - <code>this.record</code> - a pointer to the current roll over record object in the grid
    // @example gridRollOverReticleEffect
    // @example rolloverControls
    // @group rowEffects
    // @see ListGrid.rollUnderCanvas
    // @visibility external
    //<

    //> @attr listGrid.showRollUnderCanvas (Boolean : null : IRWA)
    // If roll overs are enabled, should the +link{ListGrid.rollUnderCanvas,rollUnderCanvas}
    // be displayed?
    // <p>
    // Use of the <code>showRollUnderCanvas</code> is enabled if +link{ListGrid.showRollOver,showRollOver}
    // is <code>true</code>, and either +link{ListGrid.showRollOverCanvas,showRollOverCanvas}
    // is <code>true</code> and <code>showRollUnderCanvas</code> is unset, or <code>showRollUnderCanvas</code>
    // is explicitly set to <code>true</code>.
    // @example gridAnimatedSelection
    // @see ListGrid.showRollOverCanvas
    // @visibility external
    //<
    //showRollUnderCanvas:null,

    //> @attr listGrid.rollUnderCanvas (AutoChild Canvas : null : RA)
    // AutoChild created and embedded in the grid if +link{ListGrid.showRollOver,showRollOver}
    // is <code>true</code>, and either +link{ListGrid.showRollOverCanvas,showRollOverCanvas}
    // is <code>true</code> and +link{ListGrid.showRollUnderCanvas,showRollUnderCanvas} is
    // unset, or <code>showRollUnderCanvas</code> is explicitly set to <code>true</code>.
    // This component will be created and displayed behind the current rollOver row or cell in the
    // page's z-order, meaning that it will only be visible if the cell styling is transparent.
    // <p>
    // The <code>rollUnderCanvas</code> has the following read-only attributes set:<br>
    // - <code>this.grid</code> - a pointer to the grid<br>
    // - <code>this.record</code> - a pointer to the current roll over record object in the grid
    // @example gridAnimatedSelection
    // @group rowEffects
    // @visibility external
    //<

    //>Animation

    //> @attr listGrid.animateRollOver (Boolean : false : IRWA)
    // If the +link{ListGrid.rollOverCanvas,rollOverCanvas} is enabled, setting this property
    // to <code>true</code> ensures that when the <code>rollOverCanvas</code> is displayed it
    // is animated into view via +link{Canvas.animateShow()}. Note that the animation effect
    // may be customized via +link{Canvas.animateShowEffect}, +link{Canvas.animateShowTime} and
    // +link{Canvas.animateShowAcceleration} set in <code>rollOverCanvasProperties</code>.
    // @group rowEffects
    // @visibility external
    //<
    animateRollOver: false,

    //> @attr listGrid.animateRollUnder (Boolean : false : IRWA)
    // If the +link{ListGrid.rollUnderCanvas,rollUnderCanvas} is enabled, setting this property
    // to <code>true</code> ensures that when the <code>rollUnderCanvas</code> is displayed it
    // is animated into view via +link{Canvas.animateShow()}. Note that the animation effect
    // may be customized via +link{Canvas.animateShowEffect}, +link{Canvas.animateShowTime} and
    // +link{Canvas.animateShowAcceleration} set in <code>rollUnderCanvasProperties</code>.
    // @example gridAnimatedSelection
    // @group rowEffects
    // @visibility external
    //<
    animateRollUnder: false,

    //<Animation

    //> @attr listGrid.showBackgroundComponents (Boolean : false : IRW)
    // If <code>true</code> this grid will create and show per-row backgroundComponents
    // as detailed +link{listGrid.backgroundComponent,here}.
    // @visibility external
    //<

    //> @attr listGrid.backgroundComponent (MultiAutoChild Canvas : null : IR)
    // Has no effect unless +link{listGrid.showBackgroundComponents} is <code>true</code>.
    // <P>
    // Canvas created and embedded in the body behind a given record.   When
    // +link{listGridRecord.backgroundComponent} is set, this autoChild canvas
    // will be constructed (if listGridRecord.backgroundComponent is not already a Canvas) and
    // its properties combined with those of listGridRecord.backgroundComponent and then
    // displayed behind a specific record in the page's z-order, meaning
    // it will only be visible if the cell styling is transparent.
    // @group rowEffects
    // @visibility external
    //<
    backgroundComponentDefaults: {
        snapTo:"TL",
        autoDraw: false,
        opacity: "50%"
    },

    // Hover
    // --------------------------------------------------------------------------------------------

    //>    @attr listGrid.canHover (boolean : null : [IRW])
    // @include gridRenderer.canHover
    // @group hovers
    // @see attr:listGrid.showHover
    // @see attr:listGridField.showHover
    // @example valueHoverTips
    //<
    // are hover events and hover popups enabled?
    //canHover:false,

    //>    @attr listGrid.showHover (Boolean : true : [IRW])
    // @include gridRenderer.showHover
    // @group hovers
    //<
    // if canHover:true, should we show hover popups?
    showHover: true,

    //> @attr listGrid.showClippedHeaderTitlesOnHover (boolean : true : [IRA])
    // If true and a header button's title is clipped, then a hover containing the full field
    // title is enabled.
    // @group hovers
    // @see ListGrid.headerTitleClipped()
    // @see ListGrid.headerHoverHTML()
    // @visibility external
    //<
    showClippedHeaderTitlesOnHover: true,

    //>    @attr listGridField.showHover (boolean : null : IRW)
    // Whether to show hovers for this field.  The default hover will be the contents of the
    // cell the user is hovering over, and can be customized via
    // +link{listGridField.hoverHTML,field.hoverHTML()}.
    // <P>
    // +link{ListGrid.canHover} can be set to true to cause hovers to be shown for all fields
    // by default.  In this case, <code>field.showHover</code> can be set to false to suppress
    // hovers for an individual field.
    // <P>
    // All hovers can be disabled, regardless of other settings, by setting
    // +link{ListGrid.showHover} to false.
    // @visibility external
    // @example valueHoverTips
    //<

    // can be set to false to cause hover to be per-row instead of per-cell
    //hoverByCell:true,

    // if canHover:true, should an active hover remain active until we leave the listGrid?
    // default behavior is to clear/deactivate the hover on each cellOut/rowOut
    //keepHoverActive:false,

    // the space between the borders of the cell and the hover, in pixels
    cellHoverOutset:5,

    // Note: hoverWidth, hoverStyle, et al will be picked up by the grid renderer when showing
    // cell hovers (handled by GridRenderer class)

    //> @attr listGrid.hoverStyle (CSSStyleName : "gridHover" : [IRWA])
    // Style to apply to hovers shown over this grid.
    // @see listGrid.showHover
    // @group hovers
    // @visibility external
    //<
    hoverStyle:"gridHover",

    //>    @attr listGridField.prompt (String : null : IR)
    // Causes a tooltip hover to appear on the header generated for this field (effectively
    // sets +link{canvas.prompt} for the header).
    //
    // @visibility external
    //<

    // Selection
    // --------------------------------------------------------------------------------------------

    //>    @attr listGrid.selection (Selection : null : [RA])
    // The Selection object associated with the listGrid.
    // @group  selection
    // @visibility external
    //<

    //> @attr listGrid.selectionAppearance (SelectionAppearance : "rowStyle" : IRW)
    // How selection of rows should be presented to the user.
    // <P>
    // For <code>selectionAppearance:"checkbox"</code> with multiple selection
    // allowed, you would typically use +link{listGrid.selectionType}:"simple" (the default).  Because
    // <code>selectionType</code> and <code>selectionAppearance</code> are unrelated,
    // the combination of <code>selectionAppearance:"checkbox"</code> and
    // <code>selectionType:"multiple"</code> results in a grid where multiple selection can
    // only be achieved via shift-click or ctrl-click.
    // <P>
    // If using <code>"checkbox"</code> for a +link{listGrid}, see also
    // +link{listGrid.checkboxField} for customization APIs.
    // <P>
    // If using <code>"checkbox"</code> for a +link{treeGrid}, an extra icon,
    // +link{treeGrid.getExtraIcon} is not supported. Additionally only
    // +link{listGrid.selectionType}:"simple" and "single" are supported.
    // You can also toggle the display of a disabled checkbox on a treegrid, displayed
    // when the node can't be selected, via +link{TreeGrid.showDisabledSelectionCheckbox}.
    // @group selection
    // @visibility external
    //<
    selectionAppearance: "rowStyle",

    //>    @attr listGrid.canSelectAll (boolean : null : [IRW])
    // Controls whether a checkbox for selecting all records appears in the header with
    // +link{listGrid.selectionAppearance, selectionAppearance} set to "checkbox"
    //
    // @group selection
    // @visibility external
    //<

    //>    @attr listGrid.selectionType (SelectionStyle : null : [IRW])
    // Defines a listGrid's clickable-selection behavior.
    // <P>
    // The default selection appearance is governed by +link{listGrid.selectionAppearance}: if
    // selectionAppearance is "checkbox", this will be "simple", otherwise, this will be
    // "multiple".
    //
    // @group    selection, appearance
    // @see type:SelectionStyle
    //      @visibility external
    // @example multipleSelect
    //<

    //>    @attr listGrid.selectionProperty (string : null : IRA)
    // If specified, the selection object for this list will use this property to mark records
    // as selected.  In other words, if this attribute were set to <code>"isSelected"</code>
    // any records in the listGrid data where <code>"isSelected"</code> is <code>true</code>
    // will show up as selected in the grid. Similarly if records are selected within the grid
    // after the grid has been created, this property will be set to true on the selected
    // records.
    //
    // @group  selection, appearance
    // @visibility external
    //<
    //selectionProperty:null,

    //> @attr listGrid.reselectOnUpdate (boolean : true : IRA)
    // If true, when an update operation occurs on a selected record in a
    // +link{listGrid.dataSource,databound} listGrid, ensure the updated record is
    // re-selected when the operation completes.
    // The +link{listGrid.reselectOnUpdateNotifications} attributes governs whether
    // +link{listGrid.selectionUpdated()} and +link{listGrid.selectionChanged()} will fire
    // when this occurs.
    // @visibility external
    //<
    // This property is passed to the selection object in DBC.createSelectionModel()
    reselectOnUpdate:true,

    //> @type SelectionNotificationType
    // Enum to indicate selection change notification types. Used by
    // +link{listGrid.reselectOnUpdateNotifications}
    // @value "none" No selection change notification should fire
    // @value "selectionChanged" +link{listGrid.selectionChanged()} should fire but
    //  +link{listGrid.selectionUpdated()} should not fire.
    // @value "selectionUpdated" +link{listGrid.selectionChanged()} and
    //  +link{listGrid.selectionUpdated()} should both fire.
    // @visibility external
    //<

    //> @attr listGrid.reselectOnUpdateNotifications (SelectionNotificationType : "selectionChanged" : IRWA)
    // if +link{listGrid.reselectOnUpdate} is true, this property governs what
    // selection changed notifications should be triggered when a selected record
    // is edited then automatically reselected when the edited data is merged into
    // the data set.
    // @visibility external
    //<
    reselectOnUpdateNotifications:"selectionChanged",

    //> @attr listGrid.recordCanSelectProperty (String : "canSelect" : IRA)
    // If set to false on a record, selection of that record is disallowed.
    // @visibility external
    //<
    recordCanSelectProperty:"canSelect",

    //> @attr listGridRecord.canSelect (boolean : null : IR)
    //
    // Default property name denoting whether this record can be selected. Property name may be
    // modified for the grid via +link{listGrid.recordCanSelectProperty}.
    //
    // @visibility external
    //<

    //> @attr listGrid.canSelectCells (Boolean : false : IR)
    // Enables cell-level selection behavior as well as
    // +link{useCellRollOvers,cell-level rollover}.
    // <P>
    // To query and manipulate cell-level selections, use +link{getCellSelection()} to retrieve
    // the +link{CellSelection}.
    // <P>
    // Note that the ListGrid has a data model of one +link{Record} per row, unlike the
    // +link{CubeGrid} which supports one +link{CellRecord} per cell.  For this reason
    // record-oriented APIs that act on the selection will act on entire Records that have
    // <i>any</i> selected cells (examples include drag and drop and transferSelectedData()).
    // <P>
    // More generally, <code>canSelectCells</code> is primarily intended to enable developers
    // to build Excel-like interactions on local datasets, by using +link{setData()} plus
    // +link{saveLocally}:true rather than record-oriented DataSources and data binding.
    // <P>
    // The following keyboard selection behaviors are enabled with this property in
    // addition to standard single-selection Arrow Key navigation:
    // <P>
    // SHIFT +        [Arrow Key]:  begin or continue incremental selection
    // <P>
    // SHIFT + CTRL + [Arrow Key]:  incremental selection to the end of row or column
    // <P>
    // CTRL  + A: select all cells (enabled only with +link{listGrid.canSelectAll})
    // <P>
    // Incremental selection allows selection of rows and columns of cells via keyboard
    // or mouse provided the shift key is down.  Behavior is designed to match Excel.
    // Thus, if a previous selection has begun, cells will be selected from that origin.
    // <P>
    // Users may also navigate through cells using the <i>Tab</i> and <i>Shift+Tab</i>
    // keypresses if +link{listGrid.navigateOnTab} is true. When a user tabs to the
    // end of the row, the +link{listGrid.rowEndEditAction} is used to determine whether
    // to shift selection to the next row, return to the beginning of the same row, or
    // simply move on through the page's tab order.
    //
    // @visibility external
    //<
    //canSelectCells:false,

    //> @attr listGrid.navigateOnTab (boolean : true : IRW)
    // If +link{listGrid.canSelectCells} is true, this property allows the user
    // to navigate through the cells of a grid using Tab and Shift+Tab keypresses.
    // When a user tabs to the
    // end of the row, the +link{listGrid.rowEndEditAction} is used to determine whether
    // to shift selection to the next row, return to the beginning of the same row, or
    // simply move on through the page's tab order.
    // @visibility external
    //<
    navigateOnTab:true,
    shouldNavigateOnTab : function () {
        return this.canSelectCells && this.navigateOnTab;
    },
    // Undocumented corollary for arrowKeyAction for navigateOnTab
    tabKeyAction:"select",

    //> @attr listGrid.useCopyPasteShortcuts (Boolean : true : IRW)
    // Enables copy/paste shortcuts, provided +link{listGrid.canSelectCells} is true
    // <p>
    // The default setting of true means that the value of +link{listGrid.canSelectCells}
    // determines whether copy/paste shortcuts are enabled.  Setting this property to
    // false disables said shortcuts unconditionally.
    // <p>
    // Copying is done via copying to a SmartClient-wide "clipboard" - <b>not</b> the OS-level
    // clipboard, which is innaccessible on some browsers - via the methods
    // +link{getSelectedCellData()} and +link{applyCellData()}.  To copy data to and
    // from applications outside of the browser, use the technique shown in the
    // +explorerExample{gridToExcel,Grid to Excel} and
    // +explorerExample{excelToGrid,Excel to Grid} samples.
    // <p>
    // The following shortcuts are available:
    // <p>
    // CTRL + D: copy cell values from top row of selected cells down to all rows
    // <p>
    // CTRL + R: copy cell values from left column of selected cells right to all columns
    // <p>
    // CTRL + C: copy selected cell values into shared clipboard
    // <p>
    // CTRL + V: paste from shared clipboard into current selection
    //
    // @visibility external
    //<
    useCopyPasteShortcuts: true,

    //> @attr listGrid.originBaseStyle (CSSStyleName : null : IRW)
    // Name of a CSS Style to use as the +link{listGrid.baseStyle} for a cell that
    // is currently a selection origin for shifted incremental cell selection.
    // Only has an effect if +link{listGrid.canSelectCells} is true.
    // <P>
    // @visibility external
    //<
    //originBaseStyle: null,

    //> @attr listGrid.copyEmptyCells (boolean : true : IRW)
    // Determines whether empty cells (those with an undefined value) are present
    // in the records generated by +link{listGrid.getSelectedCellData}, If true, an
    // empty cell in the selection will be added to the record with a null value.
    // If false, an empty cell will not be represented in the record at all.
    //<
    copyEmptyCells: true,

    //>    @attr listGrid.canDragSelect (Boolean : false : IRW)
    //  If this property is true, users can drag the mouse to select several rows or cells.
    //  This is mutually exclusive with rearranging rows or cells by dragging.
    //  @group    selection
    //  @visibility    external
    //  @example dragListSelect
    //<
    //canDragSelect:false,

    //>    @attr listGrid.canDragSelectText (Boolean : false : IRW)
    // If this property is true, users can drag the mouse to select text within grid rows.
    // This is mutually exclusive with
    // +link{canReorderRecords,rearranging rows or cells by dragging}, and with
    // +link{canDragSelect,drag selection of rows}.
    //
    // @group selection
    // @visibility external
    //<
    //canDragSelectText:false,

    //>    @attr listGrid.canDropInEmptyArea (Boolean : true : IRW)
    // If set to false, dropping over an empty part of the grid body is disallowed and the
    // no-drop indicator is displayed.
    // @group dragdrop
    // @visibility external
    //<
    //canDropInEmptyArea: true,

    //> @attr listGrid.showSelectionCanvas (Boolean : null : IRWA)
    // If +link{ListGrid.selectionType,selectionType} is set to
    // <smartclient>"single",</smartclient>
    // <smartgwt>{@link com.smartgwt.client.types.SelectionStyle#SINGLE},</smartgwt>
    // setting this property to <code>true</code> means selection will be displayed to the
    // user with the +link{ListGrid.selectionCanvas,selectionCanvas} and/or
    // +link{ListGrid.selectionUnderCanvas,selectionUnderCanvas} rather than with CSS styling.
    // <p>
    // If <code>showSelectionCanvas</code> is set to <code>true</code>, then the
    // <code>selectionUnderCanvas</code> will automatically be enabled unless
    // +link{showSelectionUnderCanvas,showSelectionUnderCanvas} is set to <code>false</code>.
    // <p>
    // NOTE: It is recommended to use the <code>selectionUnderCanvas</code> rather than the
    // <code>selectionCanvas</code> if possible because the <code>selectionCanvas</code> is
    // stacked on top of the selected record and this may interfere with event handling in rare
    // cases. If no interactive components are shown in the <code>selectionCanvas</code> and it
    // simply provides custom styling, then the <code>selectionUnderCanvas</code> should be used
    // instead.
    // <p>
    // With +link{ListGrid.canFreezeFields,frozen fields}, the <code>selectionCanvas</code>
    // is displayed only over the non-frozen fields of the selected row.
    // @group rowEffects
    // @see ListGrid.showSelectionUnderCanvas
    // @visibility external
    //<
    //showSelectionCanvas:null,

    //> @attr listGrid.selectionCanvas (AutoChild Canvas : null : RA)
    // AutoChild created and embedded in the grid if +link{ListGrid.showSelectionCanvas,showSelectionCanvas}
    // is <code>true</code> and the +link{ListGrid.selectionType,selectionType} is
    // <smartclient>"single".</smartclient>
    // <smartgwt>{@link com.smartgwt.client.types.SelectionStyle#SINGLE}.</smartgwt>
    // This component will be created and displayed above the selected record whenever the
    // selection changes.
    // <p>
    // NOTE: It is recommended to use the +link{ListGrid.selectionUnderCanvas,selectionUnderCanvas}
    // rather than the <code>selectionCanvas</code> if possible because the <code>selectionCanvas</code>
    // is stacked on top of the selected record and this may interfere with event handling in rare
    // cases. If no interactive components are shown in the <code>selectionCanvas</code> and it
    // simply provides custom styling, then the <code>selectionUnderCanvas</code> should be used
    // instead.
    // <p>
    // The <code>selectionCanvas</code> has the following read-only attributes set:<br>
    // - <code>this.grid</code> - a pointer to the grid<br>
    // - <code>this.record</code> - a pointer to the currently selected record in the grid
    // @group rowEffects
    // @see ListGrid.selectionUnderCanvas
    // @visibility external
    //<

    //> @attr listGrid.showSelectionUnderCanvas (Boolean : null : IRWA)
    // If +link{ListGrid.selectionType,selectionType} is set to
    // <smartclient>"single",</smartclient>
    // <smartgwt>{@link com.smartgwt.client.types.SelectionStyle#SINGLE},</smartgwt>
    // and either +link{showSelectionCanvas,showSelectionCanvas} is <code>true</code> and
    // <code>showSelectionUnderCanvas</code> is unset, or <code>showSelectionUnderCanvas</code>
    // is explicitly set to <code>true</code>, then selection will be displayed to the
    // user with the +link{ListGrid.selectionCanvas,selectionCanvas} and/or
    // +link{ListGrid.selectionUnderCanvas,selectionUnderCanvas} rather than with CSS styling.
    // Setting <code>showSelectionUnderCanvas</code> to <code>false</code> will disable
    // the use of the <code>selectionUnderCanvas</code>.
    // <p>
    // With +link{ListGrid.canFreezeFields,frozen fields}, the <code>selectionUnderCanvas</code>
    // is displayed only behind the non-frozen fields of the selected row.
    // @example gridRoundedSelection
    // @group rowEffects
    // @see ListGrid.showSelectionCanvas
    // @visibility external
    //<

    //showSelectionUnderCanvas:null,

    //> @attr listGrid.selectionUnderCanvas (AutoChild Canvas : null : RA)
    // AutoChild created and embedded in the grid if +link{ListGrid.showSelectionCanvas,showSelectionCanvas}
    // is <code>true</code> and +link{ListGrid.showSelectionUnderCanvas,showSelectionUnderCanvas}
    // is unset, or <code>showSelectionUnderCanvas</code> is explicitly set to <code>true</code>,
    // and the +link{ListGrid.selectionType,selectionType} is
    // <smartclient>"single".</smartclient>
    // <smartgwt>{@link com.smartgwt.client.types.SelectionStyle#SINGLE}.</smartgwt>
    // This component will be created and displayed behind the selected record whenever the
    // selection changes.
    // <p>
    // The <code>selectionUnderCanvas</code> has the following read-only attributes set:<br>
    // - <code>this.grid</code> - a pointer to the grid<br>
    // - <code>this.record</code> - a pointer to the currently selected record object in the grid
    // @example gridRoundedSelection
    // @group rowEffects
    // @see ListGrid.selectionCanvas
    // @visibility external
    //<

    //>Animation

    //> @attr listGrid.animateSelection (Boolean : false : IRWA)
    // If the +link{ListGrid.selectionCanvas,selectionCanvas} is enabled, setting
    // this property to <code>true</code> ensures that when the <code>selectionCanvas</code>
    // is displayed it is animated into view via +link{Canvas.animateShow()}. Note that the
    // animation effect may be customized via +link{Canvas.animateShowEffect},
    // +link{Canvas.animateShowTime} and +link{Canvas.animateShowAcceleration} set in
    // <code>selectionCanvasProperties</code>.
    // @group rowEffects
    // @see ListGrid.animateSelectionUnder
    // @visibility external
    //<
    animateSelection: false,

    //> @attr listGrid.animateSelectionUnder (Boolean : false : IRWA)
    // If the +link{ListGrid.selectionUnderCanvas,selectionUnderCanvas} is enabled, setting
    // this property to <code>true</code> ensures that when the <code>selectionUnderCanvas</code>
    // is displayed it is animated into view via +link{Canvas.animateShow()}. Note that the
    // animation effect may be customized via +link{Canvas.animateShowEffect},
    // +link{Canvas.animateShowTime} and +link{Canvas.animateShowAcceleration} set in
    // <code>selectionUnderCanvasProperties</code>.
    // @example gridAnimatedSelection
    // @group rowEffects
    // @see ListGrid.animateSelection
    // @visibility external
    //<
    animateSelectionUnder: false,

    //<Animation

    //> @attr listGrid.checkboxField (AutoChild ListGridField : null : IR)
    // Returns the specially generated checkbox field used when +link{selectionAppearance} is
    // "checkbox".  Created via the +link{AutoChild} pattern so that
    // <code>checkboxFieldDefaults</code> and <code>checkboxFieldProperties</code> are available
    // for skinning purposes. Note that +link{listGridField.shouldPrint} is <code>false</code>
    // for the checkboxField by default - if you want this column to show up in the grid's print
    // view, use <code>checkboxFieldProperties</code> to set this property to true.
    // <P>
    // This field will render an icon to indicate the selected state of each row, which, when
    // clicked will toggle the selection state. The icon src may be configured using
    // +link{listGrid.checkboxFieldTrueImage} and +link{listGrid.checkboxFieldFalseImage}, as
    // well as +link{listGrid.checkboxFieldImageWidth} and +link{listGrid.checkboxFieldImageHeight}.
    // <P>
    // The checkboxField can be detected by calling +link{listGrid.isCheckboxField()} on any
    // ListGridField object.
    //
    // @group checkboxField
    // @visibility external
    //<

    //> @attr listGrid.checkboxFieldTrueImage (SCImgURL : null :IRWA)
    // If +link{listGrid.selectionAppearance} is set to <code>"checkbox"</code> this property
    // determines the image to display in the checkbox field for a selected row.
    // If unset, the +link{listGrid.booleanTrueImage} will be used. Note that the special
    // value "blank" means that no image will be shown.
    // @see ListGrid.checkboxFieldFalseImage
    // @see ListGrid.checkboxFieldImageWidth
    // @see ListGrid.checkboxFieldImageHeight
    // @see ListGrid.printCheckboxFieldTrueImage
    // @group checkboxField
    // @visibility external
    //<

    //> @attr listGrid.checkboxFieldFalseImage (SCImgURL : null :IRWA)
    // If +link{listGrid.selectionAppearance} is set to <code>"checkbox"</code> this property
    // determines the image to display in the checkbox field for an unselected row.
    // If unset, the +link{listGrid.booleanFalseImage} will be used. Note that the special
    // value "blank" means that no image will be shown.
    // @see ListGrid.checkboxFieldTrueImage
    // @see ListGrid.checkboxFieldImageWidth
    // @see ListGrid.checkboxFieldImageHeight
    // @see ListGrid.printCheckboxFieldFalseImage
    // @group checkboxField
    // @visibility external
    //<

    //> @attr listGrid.checkboxFieldPartialImage (SCImgURL : null :IRWA)
    // If +link{listGrid.selectionAppearance} is set to <code>"checkbox"</code> this property
    // determines the image to display in the checkbox field for a partially selected row.
    // If unset, the +link{listGrid.booleanPartialImage} will be used. Note that the special
    // value "blank" means that no image will be shown.
    // @see ListGrid.checkboxFieldTrueImage
    // @see ListGrid.checkboxFieldImageWidth
    // @see ListGrid.checkboxFieldImageHeight
    // @see ListGrid.printCheckboxFieldPartialImage
    // @group checkboxField
    // @visibility external
    //<

    //> @attr listGrid.printCheckboxFieldTrueImage (SCImgURL : null :IRWA)
    // If set, the +link{ListGrid.checkboxFieldTrueImage} to use when +link{groupDef:printing,printing}.
    // @see ListGrid.checkboxFieldTrueImage
    // @group checkboxField
    // @group printing
    // @visibility external
    //<
    //printCheckboxFieldTrueImage:null,

    //> @attr listGrid.printCheckboxFieldFalseImage (SCImgURL : null :IRWA)
    // If set, the +link{ListGrid.checkboxFieldFalseImage} to use when +link{groupDef:printing,printing}.
    // @see ListGrid.checkboxFieldFalseImage
    // @group checkboxField
    // @group printing
    // @visibility external
    //<
    //printCheckboxFieldFalseImage:null,

    //> @attr listGrid.printCheckboxFieldPartialImage (SCImgURL : null :IRWA)
    // If set, the +link{ListGrid.checkboxFieldPartialImage} to use when +link{groupDef:printing,printing}.
    // @see ListGrid.checkboxFieldPartialImage
    // @group checkboxField
    // @group printing
    // @visibility external
    //<
    //printCheckboxFieldPartialImage:null,

    //> @attr listGrid.checkboxFieldImageWidth (integer : null : IR)
    // If +link{listGrid.selectionAppearance} is set to <code>"checkbox"</code> this property
    // may be set to govern the width of the checkbox image displayed to indicate whether a row is
    // selected. If unset, the checkboxField image will be sized to match the
    // +link{listGrid.booleanImageWidth} for this grid.
    // @group checkboxField
    // @visibility external
    //<

    //> @attr listGrid.checkboxFieldImageHeight (integer : null : IR)
    // If +link{listGrid.selectionAppearance} is set to <code>"checkbox"</code> this property
    // may be set to govern the height of the checkbox image displayed to indicate whether a row is
    // selected. If unset, the checkboxField image will be sized to match the
    // +link{listGrid.booleanImageHeight} for this grid.
    // @group checkboxField
    // @visibility external
    //<

    // Empty and loading messages
    // --------------------------------------------------------------------------------------------

    //>    @attr listGrid.showEmptyMessage (Boolean : true : [IRW])
    // @include gridRenderer.showEmptyMessage
    // @example emptyGrid
    //<
    showEmptyMessage:true,

    //>    @attr listGrid.emptyMessage (HTMLString : "No items to show." : [IRW])
    // @include gridRenderer.emptyMessage
    // @example emptyGrid
    //<
    emptyMessage:"No items to show.",

    //>    @attr listGrid.emptyMessageStyle (CSSStyleName : "emptyMessage" : [IRW])
    // The CSS style name applied to the +link{emptyMessage} if displayed.
    // @group emptyMessage
    // @visibility external
    //<
    emptyMessageStyle:"emptyMessage",

    //> @attr listGrid.filterButtonPrompt (String : "Filter" : [IR])
    // The prompt to show when the mouse hovers over the Filter button in the FilterEditor.
    // @group i18nMessages
    // @visibility external
    //<
    filterButtonPrompt : "Filter",


    //> @attr listGrid.loadingDataMessage (HTMLString : "${loadingImage}&nbsp;Loading data..." : IRW)
    // The string to display in the body of a listGrid while data is being loaded.
    // Use <code>"&#36;{loadingImage}"</code> to include +link{Canvas.loadingImageSrc,a loading image}.
    // @see loadingDataMessageStyle
    // @group emptyMessage, i18nMessages
    // @visibility external
    //<
    loadingDataMessage : "${loadingImage}&nbsp;Loading data...",

    //>    @attr listGrid.loadingDataMessageStyle (CSSStyleName : "loadingDataMessage" : [IRW])
    // The CSS style name applied to the loadingDataMessage string if displayed.
    // @group emptyMessage
    // @visibility external
    //<
    loadingDataMessageStyle: "loadingDataMessage",

    //> @attr listGrid.loadingMessage (string : "&nbsp;" : IR)
    // If you have a databound listGrid and you scroll out of the currently loaded dataset, by
    // default you will see blank rows until the server returns the data for those rows.  The
    // loadingMessage attribute allows you to specify arbitrary html that will be shown in each
    // such "blank" record while the data for that record is loading.
    //
    // @group emptyMessage, i18nMessages
    // @visibility external
    //<
    loadingMessage:"&nbsp;",

    // Separator / Single Cell rows
    // ---------------------------------------------------------------------------------------

    //> @attr listGrid.singleCellValueProperty (string : "singleCellValue" : IRW)
    // If <code>record[this.singleCellValueProperty]</code> is set for some record, the
    // record will be displayed as a single cell spanning every column in the grid, with
    // contents set to the value of <code>record[this.singleCellValueProperty]</code>.
    // @visibility external
    //<
    singleCellValueProperty:"singleCellValue",

    //> @attr listGrid.isSeparatorProperty (string : "isSeparator" : IRW)
    // If <code>record[this.isSeparatorProperty]</code> is set for some record, the
    // record will be displayed as a simple separator row.
    // @visibility external
    //<
    isSeparatorProperty:"isSeparator",

    // Filter Editor (aka QBE)
    // ---------------------------------------------------------------------------------------

    //> @attr listGrid.showFilterEditor (boolean : false : IRW)
    // Should this listGrid display a filter row.  If true, this ListGrid
    // will be drawn with a single editable row, (separate from the body) with a filter button.
    // <P>
    // Values entered into this row are used as filter criteria to filter this List's data on
    // enter-keypress or filter button click. +link{listGrid.autoFetchTextMatchStyle} determines
    // the textMatchStyle for the request passed to +link{listGrid.fetchData()}.
    // <P>
    // The default +link{formItem.operator,search operator} for an item in the filterEditor can
    // be set via a field's +link{ListGridField.filterEditorProperties,
    // filterEditorProperties}.  When this happens, calls to retrieve the criteria from the
    // grid return +link{AdvancedCriteria}.
    // <P>
    // Note that if +link{listGrid.filterData()} or +link{listGrid.fetchData()} is called
    // directly while the filter editor is showing, the filter editor values will be updated to
    // reflect the new set of criteria. If you wish to retain the user entered filter criteria
    // and modify a subset of field values programmatically, this can be achieved by
    // copying the existing set of criteria and adding other changes - something
    // like this:
    // <smartclient>
    // <pre><code>
    //   var newCriteria = myListGrid.getFilterEditorCriteria();
    //   isc.DataSource.combineCriteria(newCriteria, {
    //      field1:"new value1",
    //      field2:"new value2"
    //   });
    //   myListGrid.setCriteria(newCriteria);
    // </code></pre>
    // </smartclient>
    // <smartgwt>
    // <pre><code>
    //   Criteria newCriteria = myListGrid.getFilterEditorCriteria();
    //   newCriteria = DataSource.combineCriteria(newCriteria,
    //      new Criteria("field1", "new value1")
    //   );
    //   myListGrid.setCriteria(newCriteria);
    // </code></pre>
    // </smartgwt>
    // In this example code we're using +link{listGrid.getFilterEditorCriteria()} rather than
    // +link{listGrid.getCriteria()} - this ensures that if the user has typed a new value into
    // the filter editor, but not yet clicked the filter button, we pick up the value the user
    // entered.  This sample code uses +link{dataSource.combineCriteria()} to combine the
    // existing filterEditorCriteria with some new custom criteria.  This technique is
    // applicable to both simple and advanced criteria.
    // <P>
    // If you call <code>filterData()</code> and pass in criteria for dataSource
    // fields that are not present in the ListGrid, these criteria will continue to be applied
    // along with the user-visible criteria.
    // <P>
    // <b>filterEditor and advanced criteria</b>: If a developer calls <code>filterData()</code>
    // on a ListGrid and passes in +link{AdvancedCriteria}, expected behavior of the filter
    // editor becomes ambiguous, since AdvancedCriteria has far more complex filter
    // expression support than the ordinary filterEditor can represent.
    // <P>
    // Default behavior for AdvancedCriteria will combine the AdvancedCriteria with the values
    // in the filter editor as follows:
    // <ul>
    // <li>If the top level criteria has operator of type "and":<br>
    //  Each field in the top level
    //  criteria array for which a 'canFilter' true field is shown in the listGrid will show up
    //  if the specified operator matches the default filter behavior
    //  (based on the +link{listGrid.autoFetchTextMatchStyle}).<br>
    //  If the user enters values in the filter editor, these will be combined with the
    //  existing AdvancedCriteria by either replacing or adding field level criteria at the top
    //  level.</li>
    // <li>If the top level criteria is a single field-criteria:<br>
    //  If the field shows up in the listGrid and is canFilter:true, it will be displayed to
    //  the user (if the operator matches the default filter behavior for the field).<br>
    //  If the user enters new filter criteria in the filterEditor, they will be combined with
    //  this existing criterion via a top level "and" operator, or if the user modifies the
    //  field for which the criterion already existed, it will be replaced.</li>
    // <li>Otherwise, if there are multiple top level criteria combined with an "or" operator,
    //  these will not be shown in the filter editor. Any filter parameters the user enters will
    //  be added to the existing criteria via an additional top level "and" operator, meaning
    //  the user will essentially filter a subset of the existing criteria</li>
    // </ul>
    // <P>
    // See +link{listGrid.dropCriteriaOnHideField}
    // for behavior when hiding fields while the filter-editor is showing.
    //
    //  @group filterEditor
    //  @visibility external
    //  @example filter
    //<

    //showFilterEditor:false

    //> @attr listGrid.filterEditor (RecordEditor AutoChild : null : R)
    // If +link{listGrid.showFilterEditor} is set to true, the <code>filterEditor</code>
    // is automatically created as an AutoChild.<br>
    // Developers may customize the AutoChild using +link{filterEditorProperties}.
    //
    // @visibility external
    //<

    //> @attr listGrid.filterEditorProperties (RecordEditor properties : null : IR)
    // Properties to apply to the automatically generated +link{listGrid.filterEditor}
    // if +link{listGrid.showFilterEditor} is true.
    // @visibility external
    //<
    filterEditorDefaults : { shouldPrint:false },


    //> @attr listGrid.filterButtonProperties (Button properties : null : IR)
    // If +link{listGrid.showFilterEditor} is true, this attribute may be used to customize the
    // filter button shown to the right of the filterEditor row.
    // @visibility external
    //<

    //> @attr listGrid.filterEditorHeight (number : 22 : IRW)
    // Height for the filterEditor, if shown.
    //
    //  @group filterEditor
    //  @visibility external
    //<
    filterEditorHeight:22,

    //> @attr listGrid.autoFetchAsFilter (boolean : null : IR)
    // Determines whether +link{filterData()} or +link{fetchData()} should be called when this
    // grid is initially filtered via +link{autoFetchData}, or filtered by the user via the
    // +link{showFilterEditor,filterEditor}.
    // @group dataBinding
    // @deprecated in favor of listGrid.autoFetchTextMatchStyle
    //<
    // Note: This was exposed in the 7.0 release candidate builds only.
    // Leaving supported (deprecated) but not publicly documented
    // If set, at init time, it will set textMatchStyle to exact or substring (handled in Canvas.js)

    //> @attr listGrid.autoFetchTextMatchStyle (TextMatchStyle : "substring" : IR)
    // When this grid is initially filtered via +link{autoFetchData}, or filtered by the user
    // via the +link{showFilterEditor,filterEditor}, this attribute can be used to set the
    // <code>textMatchStyle</code> on the dsRequest passed to <code>fetchData()</code>.
    // <P>
    // To use a mixture of textMatchStyles, set an appropriate
    // +link{formItem.operator, operator} on a field's
    // +link{listGridField.filterEditorProperties, filterEditorProperties}.
    // @group dataBinding
    // @visibility external
    //<
    // implemented in canvas.getInitialFetchContext() and recordEditor.performAction()
    autoFetchTextMatchStyle:"substring",

    // Editing
    // --------------------------------------------------------------------------------------------
    //> @attr listGrid.canEdit (Boolean : null : [IRW])
    //      Can the user edit cells in this listGrid? Can be set for the listGrid, and overridden for
    //      individual fields.<br>
    //      If 'canEdit' is false at the listGrid level, fields can never be edited - in this case
    //      the canEdit property on individual fields will be ignored.<br>
    //      If 'canEdit' is set to true at the listGrid level, setting the 'canEdit' property to
    //      false at the field level will prevent the field from being edited inline.<br>
    //      If 'canEdit' is not set at the listGrid level, setting 'canEdit' to true at the field
    //      level enables the field to be edited inline.
    //      @visibility external
    //      @group  editing
    //      @see    startEditing()
    //      @see listGridField.canEdit
    //      @see listGrid.recordEditProperty
    //      @see listGrid.canEditCell()
    //      @see    fields
    //      @example editByRow
    //<
    //canEdit:null,

    //> @attr listGrid.canEditNew (Boolean : null : [IRW])
    //  Can the user add new rows?
    //  @group  editing
    //  @see attr:listGrid.canEdit
    //  @see attr:listGrid.recordEditProperty
    //  @visibility internal
    //<
    canEditNew: false,

    //> @attr listGrid.recordEditProperty (string : "_canEdit" : [IRWA])
    // Property name on a record that should be checked to determine whether the record may be
    // edited.
    // <br>
    // This property is configurable to avoid possible collision with data values in record.
    // With the default setting of "_canEdit", a record can be set non-editable by ensuring
    // record._canEdit == false.
    // <br>
    // For controlling editability for the entire grid or for a field, set grid.canEdit or
    // field.canEdit.
    //
    //  @group  editing
    //  @see attr:listGrid.canEdit
    //  @see attr:listGridField.canEdit
    //  @see method:listGrid.canEditCell
    //  @visibility external
    //<
    recordEditProperty:"_canEdit",

    //> @attr listGridRecord._canEdit (boolean : null : IR)
    //
    // Default property name denoting whether this record can be edited. Property name may be
    // modified for the grid via +link{listGrid.recordEditProperty}.
    //
    // @group  editing
    // @visibility external
    //<

    // Name for property used by internal '_testRowEditData' method to track whether
    // records have been compared to edit-data in order to map rowNums to edit values.
    // Customizable, in case of collision with record data - but unlikely to be overridden.
    editValuesTestedProperty:"_editValuesTested",

    //> @attr listGrid.alwaysShowEditors (boolean : null : [IR])
    // When this attribute is set, editors will be rendered into every row of the grid rather than
    // showing up in a single record at a time.
    // This attribute is only valid when +link{listGrid.editByCell} is false
    // @group editing
    // @visibility external
    //<

    //>    @attr listGrid.editByCell (boolean : null : [IRW])
    // Determines whether when the user edits a cell in this listGrid the entire row becomes
    // editable, or just the cell that received the edit event.
    // <P>
    // No effect if this.canEdit is false or null.
    //
    // @group editing
    // @see listGrid.canEdit
    // @example editByCell
    // @visibility external
    //<

    //>    @attr listGrid.saveByCell (boolean : null : [IRW])
    // Whether edits should be saved whenever the user moves between cells in the current edit
    // row.
    // <P>
    // If unset, defaults to +link{editByCell,this.editByCell}.
    // <P>
    // To avoid automatic saving entirely, set +link{autoSaveEdits}:false.
    //
    //  @group  editing
    //  @visibility external
    //  @see listGrid.editByCell
    //<

    //>    @attr listGrid.validateByCell (boolean : null : [IRW])
    // Whether client-side validation checks should be performed when the user moves between
    // cells in the current edit row.  If unset, defaults to +link{listGrid.editByCell}.
    // <P>
    // Note that validation always occurs when a row is to be saved, so setting
    // +link{saveByCell}:true forces validation on cell transitions.  To completely disable
    // automatic validation, set +link{neverValidate}:true.
    //
    // @group gridValidation
    // @visibility external
    // @see group:editing
    //<


    // autoValidate will disable validation on row-transitions, so validation will only
    // occur on save attempts.  Not currently exposed.
    autoValidate:true,

    //> @attr listGrid.validateOnChange (boolean : null : [IRW])
    // If true, validation will be performed on each edited cell when each editor's
    // "change" handler is fired.
    //
    // @see ListGridField.validateOnChange
    // @group gridValidation
    // @visibility external
    //<
    //validateOnChange:null

    //> @attr listGrid.neverValidate (boolean : null : [IRWA])
    // If true, validation will not occur as a result of cell editing for this grid.
    //    @group    gridValidation
    // @visibility external
    //<
    //neverValidate:null,


    //> @attr listGrid.canRemoveRecords (Boolean : false : IR)
    // If set, provide UI for the user to remove records from the grid as an additional field
    // showing the +link{listGrid.removeIcon}, which, when clicked, will call
    // +link{listGrid.removeRecordClick()} which removes the row from the data set (or if
    // +link{deferRemoval} is true changes the +link{listGrid.markRecordRemoved()} status
    // for the record). Individual records can be marked to prevent removal - see
    // +link{listGrid.recordCanRemoveProperty}.
    // <P>
    // To add a confirmation dialog before a record is removed, set
    // +link{listGrid.warnOnRemoval}.
    // <P>
    // If deferring removal, the record will appear marked with the +link{removedCSSText}
    // until the removal is committed via a call to +link{saveEdits()}.
    // Otherwise, the record will disappear from view. If +link{listGrid.animateRemoveRecord}
    // is true, the removed record will appear to shrink out of view when it is removed.
    // <P>
    // By default the field will display the +link{listGrid.removeIcon} next to each record, and
    // will be rendered as the rightmost column. Two mechanisms exist to further modify this field:
    // <ul>
    // <li>To change the position of the remove-field, include an explicitly specified field with
    //     the attribute +link{listGridField.isRemoveField,isRemoveField:true} set. This will then
    //     be used as the remove field instead of adding a field to the beginning of the set of
    //     columns.</li>
    // <li>Additional direct configuration of the remove field may be achieved by modifying
    //     +link{listGrid.removeFieldProperties}.</li>
    // </ul>
    // If +link{deferRemoval} is true, when a record is marked as removed, the the icon will
    // change to display the +link{listGrid.unremoveIcon} for this row. Clicking on this icon
    // will call +link{listGrid.unmarkRecordRemoved()} to mark the record as no longer pending
    // deletion.
    // @group databinding
    // @visibility external
    //<

    //> @attr listGrid.warnOnRemoval (Boolean : false : IRW)
    // If +link{listGrid.canRemoveRecords} is true, when the user clicks the remove icon
    // for some record, should we show a warning message
    // (defined as +link{listGrid.warnOnRemovalMessage}) and allow the user to cancel removal?
    // @visibility external
    //<
    warnOnRemoval:false,
    //> @attr listGrid.warnOnRemovalMessage (string : "Are you sure you want to delete this record?" : IRW)
    // Warning message to show the user on a click on the 'remove' icon
    // if +link{listGrid.canRemoveRecords} is true and
    // +link{listGrid.warnOnRemoval} is true.
    // @visibility external
    // @group i18nMessages
    //<
    warnOnRemovalMessage:"Are you sure you want to delete this record?",

    shouldShowRemoveField : function () {
        if (this.fieldSourceGrid != null) return this.fieldSourceGrid.shouldShowRemoveField();
        return this.canRemoveRecords;
    },

    //> @attr listGrid.recordRemovedProperty (string : "_removed" : IR)
    // Property name used as an edit-value on a record that has been marked for removal via
    // +link{listGrid.markRecordRemoved()}.
    //
    // @see listGridRecord.removed
    // @visibility internal
    //<
    // This property is internal - the "_removed" flag is obfuscated and unlikely to collide
    // with real world data but could be configued to be something different if this became an issue.
    recordRemovedProperty: "_removed",

    //> @method listGrid.markRecordRemoved()
    // Marks a record deleted such that a later call to +link{saveEdits()} or +link{saveAllEdits()}
    // will cause a "remove" +link{DSRequest} to be submitted.
    // <P>
    // A removed record is disabled and non-editable, and uses +link{removedCSSText} for its CSS
    // style, which by default will show strikethrough text.
    // <P>
    // Contrast this method with removeSelectedData(), which immediately submits a DSRequest to
    // remove the selected records from the dataset.
    // <P>
    // Records that have been marked for removal using this method may be 'unmarked' via a call to
    // +link{listGrid.unmarkRecordRemoved()}, or by discarding edit values (+link{discardEdits()}).
    //
    // @param rowNum (number) row number for the record to mark
    //
    // @group editing
    // @visibility external
    //<
    markRecordRemoved : function (rowNum, suppressRefresh) {
        if (!isc.isA.Number(rowNum)) rowNum = this.findRowNum(rowNum);
        // hide the edit form if we're mid-edit.
        if (this.getEditRow() == rowNum) this.hideInlineEditor();
        // deselect if we're selected.
        if (this.canSelectCells) {
            this.selection.deselectRow(rowNum);
        } else {
            var record= this.getRecord(rowNum);
            this.selection.deselect(record);
        }
        this.setEditValue(rowNum, this.recordRemovedProperty, true,
                          suppressRefresh, false, suppressRefresh);
        if (!suppressRefresh) this.refreshRow(rowNum);
    },

    //> @method listGrid.markRecordsRemoved()
    // Marks an array of records deleted such that a later call to +link{saveEdits()} or
    // +link{saveAllEdits()} will cause a "remove" +link{DSRequest} to be submitted.
    // <P>
    // This method is similar to +link{listGrid.markRecordRemoved} but should be more efficient
    // in avoiding unneeded duplicate refreshes due to the multiple records getting marked.
    //
    // @param records (Array of ListGridRecord or number) records or indices to mark removed
    //
    // @see listGrid.markRecordRemoved
    // @group editing
    // @visibility external
    //<
    markRecordsRemoved : function (records) {
        if (records == null) return;
        if (!isc.isAn.Array(records)) records = [records];
        for (var i = 0; i < records.length; i++) {
            this.markRecordRemoved(records[i], true);
        }
    },

    //> @method listGrid.recordMarkedAsRemoved()
    // Returns true if the specified record is marked as removed via a call to
    // +link{markRecordRemoved()}
    // @param rowNum (int) index of row to verify
    // @return (Boolean) true if the specified record has been marked for removal
    // @visibility external
    // @group editing
    //<
    recordMarkedAsRemoved : function (rowNum) {
        if (rowNum == null) return false;
        if (!isc.isA.Number(rowNum)) {

            rowNum = this.getEditSessionRowNum(rowNum);
            if (rowNum == null) return false;
        }
        return (this.getEditValue(rowNum, this.recordRemovedProperty) == true);
    },

    //> @method listGrid.unmarkRecordRemoved()
    // Reverses a previous call to +link{markRecordRemoved()}.
    // <P>
    // Note that a record that is marked for removal and then un-marked retains any uncommited
    // edits from before it was marked for removal.  These can be discarded with
    // +link{discardEdits()}.
    // @param rowNum (int) index of record to clear the 'removed'
    // @group editing
    // @visibility external
    //<
    unmarkRecordRemoved : function (rowNum, suppressRefresh) {
        if (!isc.isA.Number(rowNum)) rowNum = this.findRowNum(rowNum);
        this.clearEditValue(rowNum, this.recordRemovedProperty);
        if (!suppressRefresh) this.refreshRow(rowNum);
    },



    //>    @attr listGrid.removedCSSText (string : "text-decoration:line-through;" : [IRWA])
    //  Custom CSS text to be applied to records that have been
    // +link{listGrid.markRecordRemoved(),marked for removal}.
    // <P>
    // This CSS text will be applied on top of standard disabled styling for the cell.
    //
    // @visibility external
    // @group    appearance
    //<
    removedCSSText:"text-decoration:line-through;",

    //> @method listGrid.markSelectionRemoved()
    // Marks the currently selected records as removed, as though +link{markRecordRemoved()} had
    // been called.
    //
    // @group editing
    // @visibility external
    //<
    markSelectionRemoved : function () {
        var records = this.getSelectedRecords();
        if (records != null) {
            for (var i = 0; i < records.length; i++) {
                // We can pass the record object into markRecordRemoved even though it's doc'd
                // as taking just rowNum
                this.markRecordRemoved(records[i], true);
            }
        }
        this.markForRedraw();

    },

    //> @attr listGrid.deferRemoval (boolean : null : IR)
    // When enabled, the field shown by +link{listGrid.canRemoveRecords} causes records to be
    // marked for future removal via +link{markRecordRemoved()} instead of immediately being
    // removed.
    // <P>
    // When a record has been marked for removal, an icon in the
    // <code>canRemoveRecords</code> field allowing it to be unmarked will be displayed.
    // <P>
    // If not explicitly specified by this property, removal of records will be deferred if
    // +link{autoSaveEdits} is false for the grid.
    // @group editing
    // @visibility external
    //<

    shouldDeferRemoval : function () {
        if (this.deferRemoval != null) return this.deferRemoval;
        return !this.autoSaveEdits;
    },

    //> @attr listGrid.removeIcon (SCImgURL : "[SKIN]/actions/remove.png" : IR)
    // When +link{ListGrid.canRemoveRecords} is enabled, default icon to show in
    // the auto-generated field that allows removing records.
    // @visibility external
    //<
    removeIcon:"[SKIN]/actions/remove.png",

    //> @attr listGrid.unremoveIcon (SCImgURL : "[SKIN]/actions/undo.png" : IR)
    // When +link{ListGrid.canRemoveRecords} is enabled, this icon will be shown in the
    // auto generated field fro removing records if the record has been marked as removed via
    // +link{listGrid.markRecordRemoved()}. At this point, clicking on the icon will
    // unmark the record as removed.
    // @visibility external
    //<
    unremoveIcon:"[SKIN]/actions/undo.png",

    //> @attr listGrid.removeIconSize (Number : 16 : IRW)
    // Default width and height of +link{removeIcon,remove icons} for this ListGrid.
    //
    // @visibility external
    //<
    removeIconSize: 16,

    //> @attr listGrid.animateRemoveRecord (Boolean : true : IRW)
    // When +link{ListGrid.canRemoveRecords} is enabled, should records be animated out of view
    // when they are removed by the user?
    // @visibility external
    //<
    // When showing alternate records styles, the styles will essentially be reassigned after the
    // animation completes which means we finish our smooth animation with what looks a little like
    // a jump - not clear how to avoid this, but we could warn about this in the attribute
    // description.
    animateRemoveRecord:true,

    //> @attr listGrid.animateRemoveTime (number : 100 : IRW)
    // When animating record removal
    // +link{listGrid.animateRemoveRecord,(see animateRemoveRecord)}, if
    // +link{listGrid.animateRemoveSpeed} is not
    // set, this property designates the duration of the animation in ms.
    // @group animation
    // @visibility animation
    // @see listGrid.animateRemoveRecord
    //<
    animateRemoveTime:100,

    //> @attr listGrid.animateRemoveSpeed (number : 200 : IRW)
    // When +link{listGrid.animateRemoveRecord, animating record removal}, this property
    // designates the speed of the animation in pixels per second. Takes precedence over the
    // +link{listGrid.animateRemoveTime} property, which allows the developer to specify a
    // duration for the animation rather than a speed.
    // @group animation
    // @visibility animation
    // @see listGrid.animateRemoveRecord
    //<
    animateRemoveSpeed:200,

    //> @attr listGrid.removeFieldTitle (String : "&nbsp;" : IRWA)
    // The title to use for the +link{listGrid.removeFieldDefaults, remove field}.
    // <P>
    // By default this title is not displayed in the remove column header button as the
    // +link{listGrid.removeFieldDefaults} sets +link{listGridField.showTitle} to <code>false</code>.
    // @visibility external
    //<
    removeFieldTitle: isc.nbsp,

    //> @attr listGrid.removeFieldDefaults (ListGridField properties : {...} : IR)
    // Default configuration properties for the "remove field"
    // displayed when +link{ListGrid.canRemoveRecords} is enabled.
    // +link{classMethod:class.changeDefaults()} should be used when modifying this object.
    // <P>
    // The default configuration includes a +link{listGridField.recordClick()} handler which
    // calls +link{listGrid.removeData()} to actually perform the data removal.
    // @visibility external
    //<
    removeFieldDefaults:{
        type:"icon",
        width:21,
        showDefaultContextMenu:false,
        canEdit:false,
        canHide:false,
        canSort:false,
        canGroupBy:false,
        canFilter:false,
        showTitle:false,
        canExport: false,
        ignoreKeyboardClicks:true,
        showGridSummary: false,
        showGroupSummary: false,
        summaryValue: "&nbsp;",
        // disable this from ever being assigned as the treeField
        treeField:false

        // No need for a recordClick handler.
        // We explicitly override '_rowClick()' on the gridBody class to fire the removeRecordClick
        // event. This handles the case where the icon is marked for removal already, therefore
        // disabled, so we wouldn't get a field.recordClick notification.

        // In setFields() we override 'formatCellValue' to return the appropriate icon
        // (removeIcon / unremoveIcon).

    },

    //> @attr listGrid.removeFieldProperties (ListGridField properties : null : IR)
    // Configuration properties for the "remove field" displayed when
    // +link{ListGrid.canRemoveRecords} is enabled.
    // <smartclient>These configuration settings will be overlaid
    // on top of the +link{listGrid.removeFieldDefaults}.</smartclient>
    // @visibility external
    //<

    //>    @attr listGrid.filterByCell (boolean : null : [IRWA])
    // If we're showing the filterEditor (this.showFilterEditor is true), this property
    // determines whether this list should be filtered every time the user puts focus in
    // a different field in the filter editor.
    // @group filterEditor
    // @visibility external
    //<

    //>    @attr listGrid.filterOnKeypress (boolean : null : [IRWA])
    // If we're showing the filterEditor (this.showFilterEditor is true), this property
    // determines whether this list should be filtered every time the user modifies the value
    // in a field of the filter-editor. Can also be set at the field level.
    // @group filterEditor
    // @visibility external
    //<

    //> @attr listGrid.waitForSave (Boolean : false : [IRWA])
    // If this is an editable listGrid, this property determines whether the user will be
    // able to dismiss the edit form, or navigate to another cell while the save is in
    // process (before the asynchronous server response returns).
    //    @group    editing
    // @visibility external
    //<
    //waitForSave:false,

    //> @attr listGrid.stopOnErrors (Boolean : false : [IRWA])
    // If this is an editable listGrid, this property determines how failure to save due to
    // validation errors should be displayed to the user.
    // <P>
    // If this property is true, when validation errors occur the errors will be displayed
    // to the user in an alert, and focus will be returned to the first cell to fail validation.
    // <P>
    // If false, the cells that failed validation will be silently styled with the
    // editFailedBaseStyle.
    // <p>
    // <b>Note:</b> stopOnErrors being set to true implies that 'waitForSave' is also true.
    // We will not dismiss the editor until save has completed if stopOnErrors is true.
    //
    // @group editing
    // @see waitForSave
    // @visibility external
    //<
    //stopOnErrors:false,

    //> @attr listGrid.autoSaveEdits (Boolean : true : [IRWA])
    // If this ListGrid is editable, should edits be saved out when the user finishes editing
    // a row (or a cell if +link{ListGrid.saveByCell} is true).
    // <P>
    // The default of <code>true</code> indicates that edits will be
    // +link{saveByCell,automatically saved} as the
    // user navigates through the grid and/or +link{enterKeyEditAction,hits 'Enter'} to end
    // editing.  See the +link{group:editing,Grid Editing} overview for details.
    // <P>
    // Setting <code>autoSaveEdits</code> false creates a "mass update" / "mass delete"
    // interaction where edits will be retained for all edited cells (across rows if
    // appropriate) until +link{saveEdits()} is called to save a particular row, or
    // +link{saveAllEdits()} is called to save all changes in a batch.
    //
    // @group editing
    // @visibility external
    //<
    autoSaveEdits:true,

    // ListGrid validation error icon. Very similar API to the FormItem class validation error
    // icon.

    //> @attr ListGrid.showErrorIcons (boolean : true : IRW)
    //  If this grid is editable, and an edit has caused validation failure for some cell,
    //  should we show an icon to indicate validation failure?
    //  @group  errorIcon
    //  @visibility external
    //<
    showErrorIcons : true,

    //> @attr ListGrid.errorIconHeight (integer : 16 : IRW)
    //      Height of the error icon, if we're showing icons when validation errors occur.
    //  @group  errorIcon
    //  @visibility external
    //<
    errorIconHeight : 16,

    //> @attr ListGrid.errorIconWidth (integer : 16 : IRW)
    //      Height of the error icon, if we're showing icons when validation errors occur.
    //  @group  errorIcon
    //  @visibility external
    //<
    errorIconWidth : 16,

    //> @attr ListGrid.errorIconSrc (SCImgURL : "[SKIN]/ListGrid/validation_error_icon.png" : IRW)
    //      Src of the image to show as an error icon, if we're showing icons when validation
    //      errors occur.
    //  @group  errorIcon
    //  @visibility external
    //<
    errorIconSrc : "[SKIN]/validation_error_icon.png",

    //> @attr listGrid.confirmCancelEditing (Boolean : false : [IRW])
    // If this is an editable listGrid, when the user attempts to cancel an edit, should we
    // display a confirmation prompt before discarding the edited values for the record?
    //
    //  @visibility external
    //    @group    editing
    //<
    //confirmCancelEditing:false,

    //> @attr listGrid.cancelEditingConfirmationMessage (string : Cancelling this edit will discard unsaved changes for this record. Continue? : [IRW])
    // If this is an editable listGrid, and <code>this.confirmCancelEditing</code> is true
    // this property is used as the message to display in the confirmation dismissal prompt.
    //
    //  @visibility external
    //    @group    editing, i18nMessages
    //<
    cancelEditingConfirmationMessage:"Cancelling this edit will discard unsaved changes for this record. Continue?",

    //> @attr listGrid.confirmDiscardEdits (Boolean : true : [IRW])
    // For editable listGrids, outstanding unsaved edits when the user performs a new filter
    // or sort will be discarded. This flag determines whether we should display a confirmation
    // dialog with options to save or discard the edits, or cancel the action in this case.
    //
    //  @visibility external
    //    @group    editing
    //<
    confirmDiscardEdits:true,

    //> @attr listGrid.confirmDiscardEditsMessage (string : "This action will discard unsaved changes for this list.": [IRW])
    // If <code>this.confirmDiscardEdits</code> is true, this property can be used to customize the
    // error message string displayed to the user in a dialog with options to
    // cancel the action, or save or discard pending edits in response to sort/filter actions
    // that would otherwise drop unsaved edit values.
    // @visibility external
    // @group editing, i18nMessages
    //<
    confirmDiscardEditsMessage:"This action will discard all unsaved changes for this list.",

    //> @attr listGrid.discardEditsSaveButtonTitle (string :"Save" : IRW)
    // If +link{listGrid.confirmDiscardEdits} is true this is the title for the save button
    // appearing in the lost edits confirmation dialog. Override this for localization if necessary.
    // @visibility external
    // @group editing, i18nMessages
    //<
    discardEditsSaveButtonTitle:"Save",

    //> @attr listGrid.addNewBeforeEditing (boolean : false : IRWA)
    // When creating a new edit record via 'startEditingNew()' [or tabbing beyond the end
    // of the last editable field in a list], should we contact the server to create a
    // server-side record before editing begins?
    // @group    editing
    // @visibility advancedInlineEdit
    //<
    //addNewBeforeEditing:false,

    //> @attr listGrid.rowEndEditAction (RowEndEditAction : null : IRW)
    // If the user is editing a record in this listGrid, and attempts to navigate to a field
    // beyond the end of the row, via tab (or shift-tab off the first editable field), this
    // property determines what action to take:<ul>
    // <li>"next": start editing the next (or previous) record in the list
    // <li>"same": put focus back into the first editable field of the same record.
    // <li>"done": hide the editor
    // <li>"stop": leave focus in the cell being edited
    // <li>"none": no action
    // </ul>
    // @group editing
    // @visibility external
    //<
    //rowEndEditAction:"next",

    //> @attr listGrid.listEndEditAction (RowEndEditAction : null : IRW)
    // If the user is editing the last record in this listGrid, and attempts to navigate
    // beyond the last row either by tabbing off the last editable field, or using the down
    // arrow key, this property determines what action to take:<ul>
    // <li>"next": start editing a new record at the end of the list.
    // <li>"done": hide the editor
    // <li>"stop": leave focus in the cell being edited
    // <li>"none": no action
    // </ul>
    // <P>
    // See the +link{group:editing,Grid Editing overview} and also the
    // +link{group:unsavedRecords,Editing Unsaved Records overview} for context about how newly
    // added records behave.
    //
    // @group editing
    // @visibility external
    // @example enterNewRows
    //<

    //listEndEditAction:"stop",

    //> @attr listGrid.showNewRecordRow (boolean    : null  : [IRW])
    // If this is an editable ListGrid, this property determines whether an extra row should
    // be displayed below the last record, which can be used to add a new record to the dataset
    //<

    //>    @attr listGrid.newRecordRowMessage (string : "-- Add New Row --" : IR)
    // If this listGrid is showing the 'newRecordRow' (used for adding new rows to the end
    // of the data), this property determines what message should be displayed in this row.
    // @group editing, i18nMessages
    //<
    newRecordRowMessage:"-- Add New Row --",

    //> @attr listGrid.enterKeyEditAction (EnterKeyEditAction : "done" : IRW)
    // What to do when a user hits enter while editing a cell:
    // <ul>
    // <li>"nextCell": start editing the next editable cell in this record (or the first
    //     editable cell in the next record if focus is in the last editable cell in the row)
    // <li>"nextRow": start editing the same field in the next row (skipping any rows where
    //      that would be a non-editable cell.
    // <li>"nextRowStart": start editing the first editable cell in the next row.
    // <li>"done": hide the editor (editing is complete)
    // </ul>
    // Note that if this.autoSaveEdits is true, this may cause a save of the current edit values
    // @group    editing
    // @visibility external
    //<
    enterKeyEditAction:"done",

    //> @attr listGrid.escapeKeyEditAction (EscapeKeyEditAction : "cancel" : IRW)
    // What to do when a user hits escape while editing a cell:<ul>
    // <li>"cancel": close the editor and discard the current set of edit values
    // <li>"done": just close the editor (the edit is complete, but the edited values are retained).
    // </ul>
    // Note that if +link{autoSaveEdits} is true, this may cause a save of the current edit values
    // @group editing
    // @visibility external
    //<
    escapeKeyEditAction:"cancel",

    //> @type ListGridEditEvent
    // Event that will trigger inline editing.
    //
    // @value "click"       A single mouse click triggers inline editing
    // @value "doubleClick" A double click triggers inline editing
    // @value "none"        No mouse event will trigger editing.  Editing must be
    //                      programmatically started via +link{listGrid.startEditing()}
    //                      (perhaps from an external button) or may be triggered by
    //                      keyboard navigation if +link{listGrid.editOnFocus} is set.
    //
    // @group editing
    // @visibility external
    //<

    //> @attr listGrid.editEvent (ListGridEditEvent : "doubleClick" : IRW)
    // Event that will trigger inline editing, see +link{type:ListGridEditEvent} for options.
    // <P>
    // Note this setting has no effect unless +link{canEdit} has been set to enable editing.
    // <P>
    // See also +link{editOnFocus} and +link{startEditing}.
    //
    // @group editing
    // @visibility external
    // @example editByRow
    //<
    editEvent:isc.EH.DOUBLE_CLICK,

    //> @attr listGrid.editOnFocus (boolean : null : [IRWA])
    // Should we start editing when this widget receives focus (if this ListGrid supports
    // editing)?
    // <P>
    // Note that this property being set to true will cause editing to occur on a single
    // click, even if +link{editEvent} is <code>"doubleClick"</code>, because single clicking
    // the grid will place keyboard focus there automatically.
    // <P>
    // If this property is set together with +link{listEndEditAction} being set to "next",
    // users can create a new edit row in an empty grid by simply tabbing into the grid.
    //
    // @group editing
    // @visibility external
    //<
    // Note that +link{canFocusInEmptyGrid} must be set to <code>true</code> for this
    // to be supported.
    // Note - editOnFocus behavior is slightly more complicated than might be imagined. Actual
    // behavior:
    // - focus must go to the body (not the header) to start editing.
    // - if we are currently editing, getting focus will not trigger a new edit.
    // - if the focus is a result of clicking on the listGrid body, we will only start editing
    //   if the user clicked on an editable cell -- this is the same behavior as with
    //   editEvent:"click"
    // - otherwise when this widget receives focus, the first editable row will become editable.

    //> @attr listGrid.editOnF2Keypress (Boolean : true : [IRWA])
    // Should we start editing when the widget has focus and the user presses the "f2" key
    // (if this ListGrid supports editing)?
    // <P>
    // Note that if +link{listGrid.editEvent} is set to <code>"click"</code> or
    // <code>"doubleClick"</code>, the <code>Space</code> or <code>Enter</code> key may
    // also be used to start editing, depending on the value for +link{generateClickOnSpace},
    // +link{generateDoubleClickOnSpace}, +link{generateClickOnEnter} and
    // +link{generateDoubleClickOnEnter}.
    // <P>
    // If +link{listGrid.canEdit} is false, or +link{listGrid.editEvent} is set to "none" this
    // property has no effect.
    //
    // @group editing
    // @visibility external
    //<
    // Tested on:
    // FF 4.0.1 (Mac)
    // Chrome 11.0.696.77 (Mac)
    // Safari 5.0.5 (Mac)
    // IE 8 (XP)
    editOnF2Keypress:true,


    //> @attr listGrid.editOnKeyPress (boolean : null : [IRWA])
    // If set to true, when this grid has focus, if the user starts typing character keys
    // we'll start editing the focused cell.
    // @group editing
    // @visibility internal
    //<

    //> @attr listGrid.moveEditorOnArrow (boolean : null : [IRWA])
    // If +link{listGrid.editOnKeyPress, editOnKeyPress} is true, once the user starts editing
    // a cell by typing while focused in it, should left / right arrow keys cause the
    // edit cell to shift horizontally?
    // @group editing
    // @visibility internal
    //<


    //> @attr listGrid.selectOnEdit (Boolean : true : [IRWA])
    //  When the user starts editing a row, should the row also be selected?
    //  <P>
    //  Note that  when this attribute is set to <code>true</code>, other all other rows in the grid
    //  will be deselected when a record is selected due to editing.
    // @group editing
    // @visibility external
    //<
    selectOnEdit : true,

    //> @attr listGridField.canToggle (Boolean : varies : IRWA)
    // Allows a boolean or +link{valueMap,valueMapped} field to be edited without going into
    // edit mode. When this attribute is set, clicking on the field will change the value - for
    // boolean fields toggling between <code>true</code> and <code>false</code>, and for valueMapped
    // fields, advancing the value to the next option in the valueMap.
    // <P>
    // To enable this feature, +link{listGrid.canEdit} must be set to true.
    // For boolean type fields <code>canToggle</code> is true by default, meaning setting
    // <code>canEdit</code> to true implies the user can toggle the value via a single click
    // without going into edit mode. You can disable this by explicitly setting
    // <code>canToggle</code> to false for a boolean field.<br>
    // Note that you can enable toggling only (without allowing the user to edit other fields)
    // by just setting +link{listGrid.editEvent,grid.editEvent:"none"}.
    // <P>
    // If +link{listGrid.editEvent} is set to "click", when the user clicks on the field,
    // the value will be toggled, and inline editing will be triggered as usual.
    // Otherwise the toggled value will be saved immediately to the server, or if
    // +link{listGrid.autoSaveEdits} has been set to false, will be stored as an edit value
    // for the record.
    //
    // @group editing
    // @visibility external
    //<

    //> @attr listGrid.enumCriteriaAsInitialValues (Boolean : true : IR)
    // In a ListGrid that has a DataSource and has filter criteria that include values for
    // fields declared as +link{type:FieldType,type "enum"} in the DataSource, by default a newly
    // edited row will use those filter criteria as initial values.
    // <P>
    // For example, if a ListGrid is showing all Accounts that have status:"Active" and a new row
    // is created, the new row will default to status:"Active" unless this flag is set to false.
    //
    // @group editing
    // @visibility external
    //<
    enumCriteriaAsInitialValues:true,

    //> @attr listGrid.application (application : null : IRW)
    //        Application to use when saving edited values in a databound ListGrid
    //    @group    editing
    //<

    //> @type AutoComplete
    // Possible autoComplete modes.
    //
    // @value   "smart"    Enable SmartClient autoComplete feature.  Suppresses browser's
    //                     built-in autoComplete feature where applicable.
    // @value   "native"   Allow browser's built-in autoComplete feature to function
    // @value   "none"     Suppress brower's built-in autoComplete feature
    // @group autoComplete
    // @visibility autoComplete
    //<

    //>    @attr listGrid.autoComplete (AutoComplete : null : IRW)
    // Whether to do inline autoComplete in text fields during inline editing<br>
    // Overridden by +link{ListGridField.autoComplete} if specified.
    // If unset picks up the default from the appropriate editor class (subclass of FormItem).
    //
    // @see listGridField.autoComplete
    // @group autoComplete
    // @visibility autoComplete
    //<
    //autoComplete:null,

    //>    @attr listGrid.uniqueMatch (boolean : true : IRW)
    // When SmartClient autoComplete is enabled, whether to offer only unique matches to the
    // user.
    // <p>
    // Can be individually enabled per TextItem, or if set for the grid as a whole, can
    // be set differently for individual fields.
    //
    // @see listGridField.uniqueMatch
    // @group autoComplete
    // @visibility autoComplete
    //<

    // autoSelectEditors - if true when the user starts editing a cell always select the content
    // of the cell
    autoSelectEditors:true,
    // defaults for the form used for inline editing
    editFormDefaults: {

        canSelectText:true,
        autoDraw:false,

        // show error icons on the left by default
        errorOrientation:"left",
        showErrorText:false,
        showErrorStyle:false,

        itemKeyPress:function (item, keyName, characterValue) {
            return this.grid.editorKeyPress(item, keyName, characterValue);
        }
    },

    //> @attr listGrid.longTextEditorThreshold (int : 255 : IRW)
    // When the length of the field specified by +link{attr:dataSourceField.length} exceeds this
    // value, the ListGrid shows an edit field of type +link{attr:listGrid.longTextEditorType}
    // rather than the standard text field when the field enters inline edit mode.
    //
    // @group editing
    // @visibility external
    //<
    longTextEditorThreshold : 255,

    //> @attr listGrid.longTextEditorType (string : "PopUpTextAreaItem" : IRW)
    // When the length of the field specified by +link{attr:dataSourceField.length} exceeds
    // <code>this.longTextEditorThreshold</code> show an edit field of this type
    // rather than the standard text field when the field enters inline edit mode.
    //
    // @group editing
    // @visibility external
    //<
    longTextEditorType : "PopUpTextAreaItem",

    // functions installed into FormItems used for inline editing.
    // Note - these will be executed in the scope of the form item (not in the scope of the
    // ListGrid).
    //
    // Set up keyboard handling on form items to handle user navigation via "Enter", "Escape",
    // "Tab" keypresses, etc.
    //
    // NOTE: in Moz, if the user is holding down tab and we are cancelling the field change
    // *while logging to an open Log window*, we'll be stuck in the field indefinitely.  This
    // is probably a non-bug but can look like a freeze.
    //
    // row editing: Cancel tab to prevent focus cycling through the visible form items, since
    // for the first/last editable or visible item, we want to place focus in newly drawn
    // editors (on next/previous row, or just in newly drawn area that we scrolled into)
    editorKeyDown : function (item, keyName) {

        if (isc.Browser.isMoz && item && item.multiple &&
            isc.isA.NativeSelectItem(item) && keyName == "Enter")
        {
            item._selectedOnEnterKeydown = item.getValue();
        }
    },

    editorKeyPress : function (item, keyName, characterValue) {

        // We will return false to cancel native behavior on any key event for the keys
        // used for navigating around the edit cells (arrow keys, tab, etc.)
        var EH = isc.EH,
            returnValue,
            editEvent;

        var isMultiLineEditor =  //>PopUpTextAreaItem
                    isc.isA.PopUpTextAreaItem(item) ||     //<PopUpTextAreaItem
                    (isc.RichTextItem && isc.isA.RichTextItem(item)) ||
                    isc.isA.TextAreaItem(item);

        if (keyName == "Tab") {

            // Always cancel the native event that would take focus from this item.
            // This is appropriate as we will always focus in the next item programmatically,
            // even if we are currently showing the entire edit row.

            var shift = EH.shiftKeyDown();

            // If this is a container item, with sub items, or has a number of focusable
            // icons, we may be moving focus within the item, rather than going to another
            // edit cell.  This is handled by _moveFocusWithinItem().
            if (this.ns.isA.ContainerItem(item) ||
                (item.icons != null && item.icons.length > 0
                    // If the event occurred on a PopUpTextAreaItem, native focus is either
                    // on the icon, or within the TextArea in another form.
                    // In this case we always navigate to the next cell
                    && !this.ns.isA.PopUpTextAreaItem(item))
               )
            {
                if (!this._moveFocusWithinItem(item, shift)) {
                    return false;
                }
            }

            editEvent = shift ? isc.ListGrid.SHIFT_TAB_KEYPRESS
                                            : isc.ListGrid.TAB_KEYPRESS;
            returnValue = false;

        } else if (keyName == "Enter") {
            // If the event occurred over an icon, we don't want to interfere with it, as
            // enter will activate the link (for accessibility)
            if (item.getFocusIconIndex() != null) return;

            // allow enter to work normally for text areas.  Alt + Enter overrides.
            if (isMultiLineEditor && isc.EH.altKeyDown() == false) {
                return returnValue;
            }
            if (item._selectedOnEnterKeydown != null) {
                var oldVal = item._selectedOnEnterKeydown;
                delete item._selectedOnEnterKeydown;
                item.setValue(oldVal);
            }
            editEvent = isc.ListGrid.ENTER_KEYPRESS;
            returnValue = false;

        } else if (keyName == "Escape") {
            editEvent = isc.ListGrid.ESCAPE_KEYPRESS;
            returnValue = false;

        // By default move to a new row in response to
        //  - arrow keypress (up / down)
        //  - ctrl + arrow keypress (up / down), but not if the shift key is also down
        // Exceptions:
        //  - Text area use alt+arrowKeypress only
        //  - explicitly avoid row change on alt+arrow key in SelectItem, where this is used
        //    to show / hide the pickList
        //

        } else if (keyName == "Arrow_Up") {
            var textArea = isMultiLineEditor;
            if (textArea && !isc.EH.altKeyDown()) return returnValue;

            if (isc.isA.SpinnerItem(item) && !isc.EH.altKeyDown()) return returnValue;

            if (isc.isA.SelectItem(item) && !isc.EH.ctrlKeyDown()) return returnValue;

            if (isc.isA.PresetCriteriaItem(item) && !isc.EH.ctrlKeyDown()) return returnValue;

            if (isc.EH.ctrlKeyDown() && isc.EH.shiftKeyDown()) return returnValue;
            editEvent = isc.ListGrid.UP_ARROW_KEYPRESS;
            returnValue = false;

        } else if (keyName == "Arrow_Down") {

            var textArea =  isMultiLineEditor;
            if (textArea && !isc.EH.altKeyDown()) return returnValue;

            if (isc.isA.SpinnerItem(item) && !isc.EH.altKeyDown()) return returnValue;

            if (isc.isA.SelectItem(item) && !isc.EH.ctrlKeyDown()) return returnValue;

            if (isc.isA.PresetCriteriaItem(item) && !isc.EH.ctrlKeyDown()) return returnValue;

            if (isc.EH.ctrlKeyDown && isc.EH.shiftKeyDown()) return returnValue;
            editEvent = isc.ListGrid.DOWN_ARROW_KEYPRESS;
            returnValue = false;

        // if the user started editing via editOnKeyPress and the 'moveEditorOnArrow' flag is true
        // we shift cells on left/right arrow
        } else if (this.moveEditorOnArrow && this._editSessionFromKeyPress) {
            if (keyName == "Arrow_Left") {
                editEvent = isc.ListGrid.LEFT_ARROW_KEYPRESS;
                returnValue = false;
            } else if (keyName == "Arrow_Right") {
                editEvent = isc.ListGrid.RIGHT_ARROW_KEYPRESS;
                returnValue = false;
            }
        }

        if (editEvent != null) {

            if (isc.EH.clickMaskUp()) {
                isc.EH.setMaskedFocusCanvas(null, isc.EH.clickMaskRegistry.last());
            }

            // Fire cellEditEnd to handle saving out the value / moving to the next cell as
            // appropriate
            this.cellEditEnd(editEvent);
        }

        return returnValue;
    },

    // _moveFocusWithinItem() Helper method fired when the user hits tab / shift+tab while
    // focused in some edit item. This will move focus to the appropriate icon or sub item
    // if necessary.  A return value of false indicates focus was moved within the item, so
    // should not move to another edit cell.
    _moveFocusWithinItem : function (item, shift) {

        if (!item) return true;
        return (!item._moveFocusWithinItem(!shift));
    },

    // Override elementFocus on the form items:
    // If we're editing the whole row, and the user clicks in a new field to focus in it,
    // call 'editCellEnd' method to perform validation / saving on the previous
    // edit field (if required).

    _editFormItem_elementFocus : function (suppressHandlers) {
        var cell = this._setLGEditCellForFocus(suppressHandlers);
        this.Super("elementFocus", arguments);
        this._fireLGEditorEnter(cell[0], cell[1]);
    },
    _editFormItem_setLGEditCellForFocus : function (suppressHandlers) {



        var form = this.form,
            lg = form.grid;


        var rowNum, colNum, fieldName, fieldChanged;
        if (lg._editorShowing) {
            rowNum = lg._editRowNum;
            if (!lg.editByCell) {
                rowNum = lg._editRowNum;
                fieldName = this.getFieldName();
                colNum = lg.fields.findIndex(lg.fieldIdProperty, fieldName);

                if (!suppressHandlers) {
                    fieldChanged = (lg._editColNum != colNum);
                    // If the user has clicked in another field in the edit form,
                    // fire editField on the appropriate field
                    if (fieldChanged) {
                    // store the new edit cell
                        lg.setNewEditCell(rowNum, colNum);
                    // fire 'cellEditEnd' to save / validate before moving to the new cell
                        lg.cellEditEnd(isc.ListGrid.EDIT_FIELD_CHANGE);

                    // Note - if we could cancel the focus here, it might make sense, as the
                    // cellEditEnd callback method will re-focus in the new focus cell, but we can't
                    // cancel the focus by simply returning false from this method.
                    // Therefore allow the focus to proceed, and fall through to the super
                    // implementation of this method which will fire focus handlers, show any
                    // 'showOnFocus' icons, etc.
                    }
                }
            } else {
                colNum = lg._editColNum;
            }
        }
        return [rowNum, colNum];
    },
    _editFormItem_fireLGEditorEnter : function (rowNum, colNum) {
        var form = this.form,
            lg = form.grid;

        if (lg._editorShowing) {
            // If this is the current edit field, and hasn't yet fired its 'editorEnter' handlers
            // fire them now.
            var rowEnter = this._rowEnterOnFocus,
                cellEnter = this._cellEnterOnFocus;
            // Note: we're clearing out the flags before we fire the handlers. If the
            // handler trips a change of edit row, etc., we want editorExit to fire.
            delete this._rowEnterOnFocus;
            delete this._cellEnterOnFocus;


            var editVals = isc.addProperties(
                {},
                lg.getCellRecord(rowNum,colNum),
                lg._getEditValues(rowNum,colNum)
            );

            if (cellEnter) {
                var fieldName = lg.getFieldName(colNum);
                lg._handleEditorEnter(this, rowNum, colNum, editVals[fieldName]);
            }
            if (rowEnter) lg._handleRowEditorEnter(this, rowNum, editVals);

        } else {
            lg.logWarn("suppressing editorEnter handlers on focus as listGrid._editorShowing is null");
        }
    },


    // Header
    // ----------------------------------------------------------------------------------------

    //> @groupDef gridHeader
    // Properties and methods related to the ListGrid header. ListGrid headers are implemented
    // as a +link{class:Toolbar} of buttons shown at the top of the ListGrid
    // (one button per column).<br>
    // The toolbar header provides UI for interacting with the ListGrid fields directly (sorting,
    // resizing, reordering columns, etc).
    // @visibility external
    //<

    //> @attr listGrid.header (AutoChild Layout : null : R)
    // A Toolbar used to manager the headers shown for each column of the grid.
    // @group gridHeader
    // @visibility external
    //<

    //> @attr listGrid.headerContextMenu (AutoChild Canvas : null : R)
    // The context menu displayed for column headers.
    // @group gridHeader
    // @visibility external
    //<

    //> @attr listGrid.cellContextMenu (AutoChild Layout : null : R)
    // The menu displayed when a cell is right clicked on.
    // @visibility external
    //<

    //> @attr listGrid.spanContextMenu (AutoChild Layout : null : R)
    // The menu displayed when a cell is right clicked on.
    // @group gridHeader
    // @visibility external
    //<

    //>    @attr listGrid.canTabToHeader (boolean : null : IR)
    // Should the header be included in the tab-order for the page? If not explicitly specified,
    // the header will be included in the tab order for the page if +link{isc.setScreenReaderMode,isc.setScreenReaderMode()} is
    // called.
    // @group accessibility
    // @visibility external
    //<
    //canTabToHeader:null,

    //>    @attr listGrid.headerHeight (number : 22 : [IRW])
    //          The height of this listGrid's header, in pixels.
    //      @setter setHeaderHeight()
    //      @visibility external
    //      @group  gridHeader, sizing
    //<
    // Note: we treat a headerHeight of zero as an equivalent of showHeader:false
    headerHeight:22,

    //> @attr listGrid.minFieldWidth (int : 15 : IRW)
    // Minimum size, in pixels, for ListGrid headers.
    // @visibility external
    //<
    minFieldWidth:15,

    //>    @attr listGrid.showHeader (Boolean: true : [IRW])
    // Should we show the header for this ListGrid?
    // @group gridHeader, appearance
    // @visibility external
    //<
    showHeader:true,

    //>    @attr listGrid.headerBarStyle (CSSStyleName : null : IR)
    // Set the CSS style used for the header as a whole.
    // @group    gridHeader, appearance
    // @visibility external
    //<
    //headerBarStyle:null,

    //>    @attr listGrid.headerBackgroundColor (Color: "#CCCCCC" : IRW)
    // BackgroundColor for the header toolbar. Typically this is set to match the color
    // of the header buttons.
    //        @group    gridHeader, appearance
    // @visibility external
    //<
    headerBackgroundColor:"#CCCCCC",

    headerDefaults : {

        // immediately reposition headers during drag resizing, don't delay
        instantRelayout:true,
        // when the user resizes buttons, don't try to fit them into the available space -
        // allow the user to introduce hscrolling
        enforcePolicy:false,

        // when the header is clicked, have it call headerClick() on us
        itemClick : function (button, buttonNum) {
            this.Super("itemClick",arguments);
            this.grid._headerClick(buttonNum, this);
        },

        itemDoubleClick : function (button, buttonNum) {
            this.Super("itemDoubleClick", arguments);
            this.grid._headerDoubleClick(buttonNum, this);
        },

        showContextMenu : function () {
            return this.grid.headerBarContextClick(this);
        },

        backgroundRepeat:isc.Canvas.NO_REPEAT,

        // don't print the header, we handle this as part of the body instead, to ensure column
        // alignment
        shouldPrint:false
    },

    //> @attr listGrid.headerButtonConstructor (Class : null : IR)
    // Widget class for this ListGrid's header buttons. If unset, constructor will be
    // picked up directly from the standard +link{class:Toolbar} button constructor.
    // @group    gridHeader, appearance
    // @visibility external
    //<

    //>    @attr listGrid.headerBaseStyle (CSSStyleName : null : IR)
    // +link{Button.baseStyle} to apply to the buttons in the header, and the sorter, for
    // this ListGrid.
    // Note that, depending on the +link{listGrid.headerButtonConstructor, Class} of the header
    // buttons, you may also need to set +link{listGrid.headerTitleStyle}.
    // @group    gridHeader, appearance
    // @visibility external
    //<

    //> @attr listGrid.headerTitleStyle (CSSStyleName : null : IR)
    // +link{StretchImgButton.titleStyle} to apply to the buttons in the header, and the sorter,
    // for this ListGrid.
    // Note that this will typically only have an effect if
    // +link{listGrid.headerButtonConstructor} is set to +link{class:StretchImgButton} or a subclass
    // thereof.
    // @group    gridHeader, appearance
    // @visibility external
    //<

    //>    @attr listGrid.frozenHeaderBaseStyle (CSSStyleName : null : IR)
    // If this listGrid contains any frozen fields, this property can be used to apply a custom
    // headerBaseStyle to the frozen set of fields. If unset, the standard headerBaseStyle
    // will be used for both frozen and unfrozen cells.
    // @visibility external
    // @group gridHeader, appearance, frozenFields
    // @see listGrid.headerBaseStyle
    // @see listGridField.frozen
    //<

    //>    @attr listGrid.frozenHeaderTitleStyle (CSSStyleName : null : IR)
    // If this listGrid contains any frozen fields, this property can be used to apply a custom
    // headerTitleStyle to the frozen set of fields. If unset, the standard headerTitleStyle
    // will be used for both frozen and unfrozen cells.
    // @visibility external
    // @group gridHeader, appearance, frozenFields
    // @see listGrid.headerTitleStyle
    // @see listGridField.frozen
    //<


    //>    @attr listGrid.headerButtonDefaults (Button Properties: {...} : IRA)
    // Defaults to apply to all header buttons. To modify this object,
    // use +link{class.changeDefaults(), ListGrid.changeDefaults()}
    // rather than replacing with an entirely new object.
    // @group    gridHeader, appearance
    // @visibility external
    //<
    headerButtonDefaults:{
        clipTitle: true,

        // override getCurrentCursor to show default (non pointer) for canSort:false fields
        getCurrentCursor : function () {
            var grid = this.parentElement ? this.parentElement.grid : null;
            var currentCursor = isc.Canvas.DEFAULT;
            if (grid && this.masterIndex != null) {
                var field = grid.getField(this.masterIndex),
                    canSort = grid._canSort(field) != false;
                if (canSort) currentCursor = isc.Canvas.HAND;
            } else {
                if (this.isSorterButton) {
                    if (!grid && isc.isA.ListGrid(this.parentElement)) grid = this.parentElement;
                    if (grid) {
                        var canSort = grid._canSort(grid._getSortFieldNum()) != false;
                        if (canSort) currentCursor = isc.Canvas.HAND;
                    }
                } else {
                    currentCursor = this.getClass().getPrototype().cursor;
                }
            }
            this.cursor = currentCursor;
            return this.Super("getCurrentCursor", arguments);
        },

        dragScrollType:"parentsOnly",
        minWidth:20,
        hoverDelay:500
    },

    //>    @attr listGrid.headerButtonProperties (Button Properties: null : IRA)
    // Properties to apply to all header buttons.
    // Overrides defaults applied via  +link{ListGrid.headerButtonDefaults}.
    // @group    gridHeader, appearance
    // @visibility external
    //<

    //> @attr listGrid.clipHeaderTitles (Boolean : varies : IRA)
    // Whether the ListGrid should manage the clipping of titles of header buttons, showing
    // ellipses if the title is clipped, and potentially showing the full title
    // on +link{listGrid.showClippedHeaderTitlesOnHover,hover}.
    // <p>
    // In some cases this may be preferable to the button component's default clipping behavior
    // because if a +link{ListGrid.showSortArrow,sort arrow} or sort numeral is displayed for
    // a header, then the button's default clipping behavior may clip the sort arrow/numeral
    // whereas ListGrid-managed title clipping utilizes special HTML which keeps the sort
    // arrow/numeral visible.
    // <p>
    // This feature is automatically enabled if supported by the browser. The only supported
    // use of this attribute is to <em>disable</em> the feature by setting clipHeaderTitles
    // to false.
    // <P>
    // Note that this feature is incompatible with +link{listGridField.wrap}, and will
    // automatically be disabled for wrapping fields.
    //
    // @group gridHeader, appearance
    // @visibility external
    //<


    clipHeaderTitles: (!isc.Browser.isIE ||
                       (isc.Browser.version > 6 && isc.Browser.isStrict) ||
                       isc.Browser.version >= 10),

    //> @attr listGrid.wrapHeaderTitles (Boolean : null : IR)
    // If +link{listGridField.wrap} is not explicitly set, should fields wrap?
    //
    // @visibility external
    //<

//    wrapHeaderTitles:null,

    //> @attr listGrid.sorterConstructor (Class : Button : IR)
    // Widget class for the corner sort button, if showing.  This button displays the current
    // sort direction of the primary sort field (either the only sorted field or the first in a
    // +link{listGrid.canMultiSort, multi-sort} grid) and reverses the direction of that field
    // when clicked.  For consistent appearance, this
    // is usually set to match +link{listGrid.headerButtonConstructor}
    // @group gridHeader, appearance
    // @visibility external
    //<
    sorterConstructor:isc.Button,


    //> @attr listGrid.sorterButtonTitle (string : "corner menu" : IR)
    // The title for the corner sort button.  The title will only
    // +link{Class.changeDefaults(), ListGrid.changeDefaults()} rather than replacing with an
    // entirely new object.
    // @group i18nMessages, gridHeader, appearance
    // @visibility external
    //<
    sorterButtonTitle: "corner menu",

    //> @attr listGrid.sorterDefaults (object : {...} : IRA)
    // Defaults to apply to the corner sort button. To modify this object, use
    // +link{Class.changeDefaults(), ListGrid.changeDefaults()} rather than replacing with an
    // entirely new object.
    // @group gridHeader, appearance
    // @visibility external
    //<
    sorterDefaults:{
        _redrawWithParent:false,
        getTitle : function () {return this.creator.getSortArrowImage() },
        click : function () { return this.creator._sorterClick() },
        showContextMenu : function() { return this.creator._sorterContextClick() },
        isSorterButton: true,
        align: "center"
    },

    //>    @attr listGrid.sorterProperties (Button Properties: null : IRA)
    // Properties to apply to the sorter button. Overrides defaults applied via
    // +link{ListGrid.sorterDefaults}.
    // @group gridHeader, appearance
    // @visibility external
    //<

    // Sorting
    // --------------------------------------------------------------------------------------------

    //>    @attr listGrid.canSort (Boolean : true : [IRW])
    // Enables or disables interactive sorting behavior for this listGrid. Does not
    // affect sorting by direct calls to the +link{listGrid.sort, sort} or
    // +link{listGrid.setSort, setSort} methods.
    //
    // @group sorting
    // @visibility external
    //<
    canSort:true,

    //>    @attr listGrid.canUnsort (boolean : false : [IRW])
    // When set to true, the third click on a column header removes the sort indicator
    // from the field.
    //
    // @group sorting
    // @visibility internal
    //<
    canUnsort:false,

    //> @attr listGrid.invalidateCacheOnUnsort (boolean : false : [IRW])
    // If true, and +link{listGrid.canUnsort} is also true and the grid is databound to a
    // +link{ResultSet}, then unsort causes the grid to drop the current client-side
    // data cache and refetch the current range of records from the server.
    //
    // @group sorting
    // @visibility internal
    //<
    invalidateCacheOnUnsort: false,

    //> @attr listGrid.selectHeaderOnSort (Boolean : true : [IRW])
    // If true, show the field-header for the sorted field (or the first field in a
    // +link{listGrid.canMultiSort, multi-sort} grid) in the selected state.
    //
    // @group sorting
    // @visibility external
    //<
    selectHeaderOnSort: true,

    //>    @attr listGrid.sortFieldNum (number : null : [IRW])
    // Specifies the number of the field by which to sort this listGrid. Column numbers
    // start at 0 for the left-most column.
    // @group sorting
    // @example sort
    // @visibility external
    // @deprecated as of version 7.0 in favor of +link{listGrid.sortField}
    //<
    //sortFieldNum:null,

    //> @attr listGrid.sortField (String | int : null : IR)
    // Specifies the field by which this grid should be initially sorted. Can be set to either a
    // +link{listGridField.name,field name} or the index of the field in the fields Array.
    // <P>
    // ListGrids also support being initialized with multiple-field sort via
    // +link{listGrid.initialSort}. If initialSort is specified, it will be used in preference
    // to this property.
    // <P>
    // To sort the grid after it has been initialized, use +link{ListGrid.sort()} or
    // +link{listGrid.setSort()}. Details about the current sort of a live grid can be
    // retrieved by calling +link{listGrid.getSortField()} or +link{listGrid.getSort()}
    //
    // @group sorting
    // @example sort
    // @visibility external
    //<

    //>    @attr listGrid.keyboardClickField (string|number : null : [IRW])
    // When simulating click events listGrid rows as a result of keyboard events
    // (navigating using the arrow keys, space, enter for doubleClick), which column
    // should the event be generated upon?
    // Should be set to the name or index of the desired column.
    // If null, defaults to the first column where +link{listGridField.ignoreKeyboardClicks}
    // is not false.
    // @group  events
    //<

    //>    @attr listGrid.sortDirection (SortDirection : "ascending" : [IRW])
    // Sorting direction of this ListGrid. If specified when the ListGrid is initialized,
    // this property will be the default sorting direction for the +link{listGrid.sortField}.
    // May be overridden by specifying +link{ListGridField.sortDirection}.
    // <P>
    // After initialization, this property will be updated on +link{ListGrid.sort()} or
    // +link{ListGrid.setSort()} to reflect the current sort direction of the grid. When
    // this grid is sorted by multiple fields, the grid's sortDirection reflects the
    // sort direction of the primary sort field.
    // @group  sorting
    // @see type:SortDirection
    // @example sort
    // @setter sort()
    // @visibility external
    //<

    sortDirection:Array.ASCENDING,

    //>    @attr listGrid.showSortArrow (SortArrow : null : [IRW])
    // Indicates whether a sorting arrow should appear for the listGrid, and its
    // location. See +link{SortArrow} for details.
    // <P>
    // Clicking the sort arrow reverses the direction of sorting for the current sort
    // column (if any), or sorts the listGrid by its first sortable column. The arrow
    // image on the button indicates the current direction of sorting.
    // If undefined, the sort arrow will show up in the sorted field, and the
    // corner sort button will be displayed if a vertical scrollbar is being displayed
    //
    // @group sorting, appearance
    // @visibility external
    //<

    //showSortArrow:null,

    //> @attr listGrid.canPickFields (Boolean : true : [IRW])
    // Indicates whether the field picker item and submenu should be present in the header
    // context menu. This menu allows the user to hide visible fields and show hidden fields.
    // <p>
    // By default only fields explicitly included in the +link{listGrid.fields} array will
    // be available in this menu, unless +link{listGrid.canPickOmittedFields} is set to true for
    // a databound grid.
    // <p>
    // A specific field can be omitted from the column picker via +link{listGridField.canHide}.
    //
    // @visibility external
    //<
    canPickFields: true,

    //> @attr listGrid.canPickOmittedFields (Boolean : false : [IR])
    // If this grid has a specified +link{listGrid.dataSource}, and
    // +link{listGrid.useAllDataSourceFields} is false, setting this property to true
    // will cause all dataSource fields not included in the specified set of fields to
    // show up in the +link{listGrid.canPickFields,field picker menu item}.
    // <P>
    // Has no effect if +link{listGrid.fields} is unset (as in this case all dataSource fields
    // will be displayed by default), or if +link{listGrid.canPickFields} is false.
    // @visibility external
    //<
    canPickOmittedFields:false,

    // Frozen Fields (aka Frozen Columns)
    // ---------------------------------------------------------------------------------------

    //> @groupDef frozenFields
    // Frozen fields are fields that do not scroll horizontally with other fields, remaining on
    // the screen while other fields may be scrolled off.  This feature is typically used to
    // allow basic identifying information (like an "accountId") to remain on screen while the
    // user scrolls through a large number of related fields.
    // <P>
    // Fields can be programmatically frozen via setting
    // +link{listGridField.frozen,field.frozen} to true when the grid is created, or
    // dynamically frozen and unfrozen via +link{listGrid.freezeField,freezeField()} and
    // +link{listGrid.unfreezeField,unfreezeField()}.
    // The setting +link{listGrid.canFreezeFields,canFreezeFields} enables a user interface to
    // allow end users to dynamically freeze and unfreeze fields.
    // <P>
    // The frozen fields feature is not compatible with the following features:
    // <ul>
    // <li> variable height auto-sizing records
    //      (+link{listGrid.fixedRecordHeights,fixedRecordHeights:false})
    // <li> +link{autoFitData}:"horizontal", as well as headers that autoFit to titles
    //      (normally enabled via <code>field.overflow:"visible"</code>)
    // <li> the +link{CubeGrid} subclass of ListGrid
    // <li> nested grids
    // </ul>
    // The frozen fields feature <b>is</b> compatible with column resize and reorder, selection
    // and multi-selection, loading data on demand, inline editing, drag and drop and reorder
    // of records, the +link{TreeGrid} subclass of ListGrid, and all dynamic styling-related and
    // formatting-related features.
    //
    // @title Frozen Fields
    // @visibility external
    //<

    //> @attr listGrid.canFreezeFields (boolean : null : IRW)
    // Whether an interface should be shown to allow user is allowed to dynamically "freeze" or
    // "unfreeze" columns with respect to horizontally scrolling. If unset, this property defaults
    // to <code>true</code> unless:<ul>
    // <li>+link{listGrid.fixedRecordHeights,this.fixedRecordHeights} is <code>false</code></li>
    // <li>+link{listGrid.bodyOverflow,this.bodyOverflow} is <code>"visible"</code></li>
    // <li>+link{listGrid.autoFitData,this.autoFitData} is set to <code>"horizontal"</code> or
    // <code>"both"</code></li>
    // <li>Any field has overflow set to <code>"visible"</code></li></ul>
    // <P>
    // Note that the <code>canFreezeFields</code> setting enables or disables the user
    // interface for freezing and unfreezing fields only.  Fields can be programmatically
    // frozen via setting +link{listGridField.frozen,field.frozen} to true when the grid is
    // created, or dynamically frozen and unfrozen via +link{freezeField()} and
    // +link{unfreezeField()}.
    //
    // @group frozenFields
    // @visibility external
    //<
    // Note that fixedColumnWidths:false will also disable canFreezeFields but this
    // is not currently public.

    // -------------------------
    // Formula / summary fields (picked up from databoundcomponent)

    //>    @attr listGrid.badFormulaResultValue        (String : "." : IRW)
    // @include dataBoundComponent.badFormulaResultValue
    //<

    //>    @attr listGrid.missingSummaryFieldValue        (String : "-" : IRW)
    // @include dataBoundComponent.missingSummaryFieldValue
    //<

    //>    @attr listGrid.missingFormulaFieldValue (String : "-" : IRW)
    // @include dataBoundComponent.missingFormulaFieldValue
    //<

    //> @attr listGrid.canAddFormulaFields (boolean : false : IRW)
    // @include dataBoundComponent.canAddFormulaFields
    // @visibility external
    //<

    //> @method listGrid.getFormulaFieldValue()
    // @include dataBoundComponent.getFormulaFieldValue()
    // @param field (ListGridField) field that has a formula
    // @param record (Record) record to use to compute formula value
    // @return (Double or String) formula result if a valid number or
    // +link{dataBoundComponent.badFormulaResultValue} if invalid
    // @visibility external
    //<

    //> @attr listGrid.canAddSummaryFields (boolean : false : IRW)
    // @include dataBoundComponent.canAddSummaryFields
    // @visibility external
    //<

    //> @method listGrid.getSummaryFieldValue()
    // @include dataBoundComponent.getSummaryFieldValue()
    // @param field (ListGridField) field that has a summary format
    // @param record (Record) record to use to compute formula value
    // @return (String) formula result
    // @visibility external
    //<

    //> @attr listGrid.canEditHilites (boolean : false : IRW)
    // @include dataBoundComponent.canEditHilites
    // @visibility external
    //<

    // Context Menus
    // --------------------------------------------------------------------------------------------

    //>    @attr listGrid.showCellContextMenus (Boolean : false : [IRW])
    // Whether to show a context menu with standard items for all context clicks on rows in the
    // body.
    // @visibility external
    //<
    //showCellContextMenus:false,

    //> @attr listGrid.openRecordEditorContextMenuItemTitle (string : "Edit" : [IRW])
    // If +link{listGrid.canOpenRecordEditor} is true and +link{listGrid.showCellContextMenus}
    // is true, this property specifies the title for the context menu item shown allowing the
    // user to perfom editing on a row via an embedded form.
    // @group i18nMessages
    // @visibility nextedGrid
    //<
    openRecordEditorContextMenuItemTitle:"Edit",

    //> @attr listGrid.dismissEmbeddedComponentContextMenuItemTitle (string : "Dismiss" : IRW)
    // If +link{listGrid.showCellContextMenus} is true, and we are currently showing either
    // an embedded editor (see +link{listGrid.canOpenRecordEditor}) or an embedded
    // detail grid (see +link{listGrid.canOpenRecordDetailGrid}, this property
    // specifies the title for the context menu item shown allowing the user to dismiss the
    // embedded component.
    // @group i18nMessages
    // @visibility nextedGrid
    //<
    dismissEmbeddedComponentContextMenuItemTitle:"Dismiss",

    //> @attr listGrid.deleteRecordContextMenuItemTitle (string : "Delete" : IRW)
    // If +link{listGrid.showCellContextMenus} is true, this property
    // specifies the title for the context menu item shown allowing the user to delete the
    // record on which the contextMenu was shown.
    // @group i18nMessages
    // @visibility experimental
    //<
    deleteRecordContextMenuItemTitle:"Delete",


    //> @attr listGrid.canOpenRecordDetailGrid (boolean : true : [IRW])
    // Whether context menu items will be shown for viewing records from related DataSources in
    // grids embedded in the record.
    // <P>
    // Valid only when <code>showCellContextMenus</code> is true.
    // @visibility nestedGrid
    //<
    canOpenRecordDetailGrid:true,

    //> @attr listGrid.recordDetailGridProperties (Object : null : [IR])
    // Properties for detail grids shown embedded inside rows.
    // @visibility nestedGrid
    //<

    //> @attr listGrid.canOpenRecordEditor (boolean : true : [IRW])
    // Whether a context menu item will be shown for editing records with a form embedded in
    // the record.
    // <P>
    // Valid only when <code>showCellContextMenus</code> is true.
    // @visibility nestedGrid
    //<
    canOpenRecordEditor:true,

    //> @attr listGrid.recordEditorProperties (Object : null : [IR])
    // Properties for editor forms shown embedded inside rows.
    // @see listGrid.canOpenRecordEditor
    // @visibility nestedGrid
    //<

    //> @attr listGrid.recordEditorSaveButtonTitle (string : "Save" : [IRW])
    // Title for the Save button shown in the editor form embedded inside rows if
    // +link{listGrid.canOpenRecordEditor} is true.
    // @see listGrid.canOpenRecordEditor
    // @group i18nMessages
    // @visibility nestedGrid
    //<
    recordEditorSaveButtonTitle:"Save",

    //> @attr listGrid.recordEditorCancelButtonTitle (string : "Cancel" : [IRW])
    // Title for the Cancel button shown in the editor form embedded inside rows if
    // +link{listGrid.canOpenRecordEditor} is true.
    // @see listGrid.canOpenRecordEditor
    // @group i18nMessages
    // @visibility nestedGrid
    //<
    recordEditorCancelButtonTitle:"Cancel",


    //>!BackCompat 2007.02.02
    // showCornerContextMenu was never externally documented and we have no in-code comments
    // about having ever exposed this property, so it may be safe to get rid of this
    // back-compat

    //>    @attr listGrid.showCornerContextMenu (boolean : null : [IR])
    // Whether to allow a context menu on the sorter with standard items for showing and hiding
    // fields.
    // @deprecated as of 5.6 in favor of +link{attr:listGrid.showHeaderContextMenu}
    //<
    //<!BackCompat

    //>    @attr listGrid.showHeaderContextMenu (Boolean : true : [IR])
    // Whether to show a context menu on the header with standard items for showing and hiding
    // fields.  Not supported for +link{cubeGrid}.
    // @group gridHeader
    // @see method:listGrid.displayHeaderContextMenu()
    // @see method:listGrid.getHeaderContextMenuItems()
    // @visibility external
    //<
    // NOTE: avoid crashing if Menu class isn't loaded by defaulting to false.
    // when we load the Menu class, we override this default.
    //showHeaderContextMenu:false,

    // headerMenuButton
    // ----------------------------
    //>    @attr listGrid.showHeaderMenuButton (Boolean : true : [IR])
    // If set to true and +link{listGrid.showHeaderContextMenu,showHeaderContextMenu} is true, the
    // +link{listgrid.headerMenuButton} will be displayed when the user rolls
    // over the header buttons in this grid.  Not supported for +link{cubeGrid}.
    // @group headerMenuButton
    // @visibility external
    //<
    // As with showHeaderContextMenu, this default should not be set to true until we know
    // for sure that Menu has been loaded (see Menu.js)
    //showHeaderMenuButton:true,

    //> @attr listGrid.leaveHeaderMenuButtonSpace (boolean : null : [IWA])
    // If +link{listGrid.showHeaderMenuButton} is true, when auto-fitting fields to
    // the title width via +link{listGrid.autoFitFieldWidths} or +link{listGridField.autoFitWidth},
    // should the button be sized such that there is enough space for the header menu button to
    // show without covering the field title?
    // <P>
    // May be explicitly specified at the +link{listGridField.leaveHeaderMenuButtonSpace,field level}
    // or at the +link{listGrid.leaveHeaderMenuButtonSpace,grid level}. If not explicitly
    // specified space will be left for fields with
    // +link{listGridField.align} set to <code>"left"</code> or <code>"right"</code>, but not for
    // fields with align set to <code>"center"</code>.
    //
    // @group headerMenuButton
    // @visibility external
    //<
    leaveHeaderMenuButtonSpace:null,

    //> @attr listGridField.leaveHeaderMenuButtonSpace (boolean : null : [IWA])
    // If +link{listGrid.showHeaderMenuButton} is true, when auto-fitting fields to
    // the title width via +link{listGrid.autoFitFieldWidths} or +link{listGridField.autoFitWidth},
    // should the button be sized such that there is enough space for the heeader menu button to
    // show without covering the field title?
    // <P>
    // May be explicitly specified at the +link{listGridField.leaveHeaderMenuButtonSpace,field level}
    // or at the +link{listGrid.leaveHeaderMenuButtonSpace,grid level}. If not explicitly
    // specified space will be left for fields with
    // +link{listGridField.align} set to <code>"left"</code> or <code>"right"</code>, but not for
    // fields with align set to <code>"center"</code>.
    //
    // @group headerMenuButton
    // @visibility external
    //<

    //> @attr listGrid.headerMenuButtonConstructor (className : null : [IRA])
    // Constructor for the  +link{listGrid.headerMenuButton}. If unset a standard "Button" will
    // be rendered out. Note that this property may be overridden by different skins.
    // @group headerMenuButton
    // @visibility external
    //<
    //headerMenuButtonConstructor: "StretchImgButton",

    //> @attr listGrid.headerMenuButton (AutoChild StatefulCanvas : null : [RA])
    // If +link{showHeaderMenuButton} is true, when the user rolls over the header buttons in this
    // grid the headerMenuButton will be shown over the header button in question. When clicked
    // this button will display the standard header context menu (see
    // +link{listGrid.displayHeaderContextMenu}).
    // <P>
    // +link{group:headerMenuButton,Several properties} exist to customize the appearance of the
    // headerMenuButton. Also see the +link{type:AutoChild} documentation for information on how
    // to make freeform modifications to autoChild widgets
    // @group headerMenuButton
    // @visibility external
    //<

    //>    @attr listGrid.headerMenuButtonIcon (URL : "[SKIN]/ListGrid/headerMenuButton_icon.gif" : [IRA])
    // If +link{listGrid.showHeaderMenuButton} is true, this property governs the icon shown on the
    // auto-generated <code>headerMenuButton</code>
    // @group headerMenuButton
    // @visibility external
    //<
    headerMenuButtonIcon:"[SKIN]/ListGrid/headerMenuButton_icon.gif",

    //>    @attr listGrid.headerMenuButtonIconWidth (number : 7 : [IRA])
    // If +link{listGrid.showHeaderMenuButton} is true, this property governs the width of the icon
    // shown on the auto-generated <code>headerMenuButton</code>
    // @group headerMenuButton
    // @visibility external
    //<
    headerMenuButtonIconWidth:7,

    //>    @attr listGrid.headerMenuButtonIconHeight (number : 7 : [IRA])
    // If +link{listGrid.showHeaderMenuButton} is true, this property governs the height of the icon
    // shown on the auto-generated <code>headerMenuButton</code>
    // @group headerMenuButton
    // @visibility external
    //<
    headerMenuButtonIconHeight:7,

    //>    @attr listGrid.headerMenuButtonWidth (number : 16 : [IRA])
    // If +link{listGrid.showHeaderMenuButton} is true, this property governs the width of the
    // auto-generated <code>headerMenuButton</code>
    // @group headerMenuButton
    // @visibility external
    //<
    headerMenuButtonWidth:16,

    //>    @attr listGrid.headerMenuButtonHeight (measure : "100%" : [IRA])
    // If +link{listGrid.showHeaderMenuButton} is true, this property governs the height of the
    // auto-generated <code>headerMenuButton</code>
    // @group headerMenuButton
    // @visibility external
    //<
    headerMenuButtonHeight:"100%",


    // Drag Resize / Reorder / Drag and Drop
    // --------------------------------------------------------------------------------------------

    //>    @attr listGrid.canDragRecordsOut (Boolean : false : [IRW])
    // Indicates whether records can be dragged from this listGrid and dropped elsewhere.
    // @visibility external
    // @group  dragging
    // @see ListGridRecord.canDrag
    // @see ListGridRecord.canAcceptDrop
    // @example dragListMove
    // @example recordsAcrossWindows
    //<
    canDragRecordsOut:false,

    //>    @attr listGrid.canAcceptDroppedRecords (Boolean : false : [IRW])
    // Indicates whether records can be dropped into this listGrid.
    // @visibility external
    // @group  dragging
    // @see ListGridRecord.canDrag
    // @see ListGridRecord.canAcceptDrop
    // @example dragListMove
    //<
    //canAcceptDroppedRecords:false,

    //>    @attr listGrid.canReorderRecords (Boolean : false : [IRW])
    // Indicates whether records can be reordered by dragging within this listGrid.
    // @visibility external
    // @group  dragging
    // @see ListGridRecord.canDrag
    // @see ListGridRecord.canAcceptDrop
    // @example dragListMove
    // @example gridsDragReorder
    //<
    //canReorderRecords:false,

    //>    @attr listGrid.canReorderFields (Boolean : true : [IRW])
    // Indicates whether fields in this listGrid can be reordered by dragging and
    // dropping header fields.
    // @visibility external
    // @group  dragging
    // @example columnOrder
    //<
    canReorderFields:true,

    //>    @attr listGrid.canResizeFields (Boolean : true : [IRW])
    // Indicates whether fields in this listGrid can be resized by dragging header
    // fields.
    // @visibility external
    // @group  dragging
    // @example columnSize
    //<
    canResizeFields:true,

    // for dragging records out, use the drag tracker
    dragAppearance:isc.EH.TRACKER,

    // if you set canDragResize to true on the grid show an outline rather than the tracker!
    dragResizeAppearance:isc.EH.OUTLINE,

    //> @type DragTrackerMode
    // When records are being dragged from within a ListGrid, what sort of drag-tracker
    // should be displayed?
    // @value "none" Don't display a drag tracker at all
    // @value "icon" Display an icon to represent the record(s) being dragged. Icon src is
    //              derived from +link{ListGrid.getDragTrackerIcon()}
    // @value "title" Display a title for the record being dragged. Title derived from
    //              +link{ListGrid.getDragTrackerTitle()}
    // @value "record" Display the entire record being dragged
    // @group dragTracker
    // @visibility external
    //<

    //> @attr listGrid.dragTrackerMode (DragTrackerMode : "icon" : [IRA])
    // When records are being dragged from within a ListGrid, what sort of drag-tracker
    // should be displayed?<br>
    // Note that if multiple records are being dragged the displayed tracker will be
    // based on the first selected record.
    // @group dragTracker
    // @visibility external
    //<
    dragTrackerMode:"title",


    //> @attr listGrid.resizeFieldsInRealTime (boolean : : IRWA)
    // If <code>true</code>, the grid contents are redrawn in real time as fields are resized.
    // This can be slow with a large grid and/or on some platforms. By default, this is enabled
    // in modern desktop browsers. This is automatically switched off in mobile browsers.
    //
    // @group dragging
    // @visibility external
    //<
    resizeFieldsInRealTime: (isc.Browser.isIE && isc.Browser.isWin)
                            || (isc.Browser.isFirefox && isc.Browser.geckoVersion >= 20080529)
                            // Safari 3.0+, Google Chrome
                            || (isc.Browser.isSafari && isc.Browser.safariVersion >= 500),

    //>    @attr listGrid.dragDataAction
    // @include dataBoundComponent.dragDataAction
    //<

    // Embedded Components
    // --------------------------------------------------------------------------------------------
    embeddedComponentIndent: 25,

    // Nested Master-Detail
    // --------------------------------------------------------------------------------------------
    nestedGridDefaults : {
        height:150
    },

    // Skinning
    // --------------------------------------------------------------------------------------------
    //>    @attr listGrid.skinImgDir (URL : "images/ListGrid/" : IRWA)
    // Where do 'skin' images (those provided with the class) live?
    // @group appearance, images
    // @visibility external
    //<
    skinImgDir:"images/ListGrid/",

    //>    @attr listGrid.sortAscendingImage (ImgProperties : {...} : IRWA)
    // Image to show when sorting ascending. See +link{class:ImgProperties} for format.
    // @group appearance
    // @visibility external
    //<
    sortAscendingImage:{src:"[SKIN]sort_ascending.gif", width:7, height:7},

    //>    @attr listGrid.sortDescendingImage (ImgProperties : {...} : IRWA)
    // Image to show when sorting descending. See +link{class:ImgProperties} for format.
    // @group appearance
    // @visibility external
    //<
    sortDescendingImage:{src:"[SKIN]sort_descending.gif", width:7, height:7},

    //>    @attr listGrid.trackerImage (ImgProperties : {...} : IRWA)
    // Default image to use for the dragTracker when things are dragged within or out of this
    // list. See +link{class:ImgProperties} for format.
    //
    // @group dragTracker
    // @see listGrid.dragTrackerMode
    // @see listGrid.getDragTrackerIcon()
    // @visibility external
    //<
    trackerImage:{src:"[SKIN]tracker.gif", width:16, height:16},

    //> @attr listGrid.booleanBaseStyle (CSSStyleName : null : IRA)
    // An optional CSS style to apply to the checkbox image. If supplied, and the checkbox is
    // enabled, the base style is suffixed with "True", "False", or "Partial" if the checkbox
    // is selected, unselected, or partially selected; if the checkbox is disabled, the suffix
    // is "TrueDisabled", "FalseDisabled", or "PartialDisabled".
    // <p>
    // <b>NOTE:</b> This attribute is not supported by +link{TreeGrid}.
    // @group imageColumns
    // @see ListGrid.printBooleanBaseStyle
    // @visibility external
    //<

    //> @attr listGrid.printBooleanBaseStyle (CSSStyleName : null : IRA)
    // If set, the +link{ListGrid.booleanBaseStyle,booleanBaseStyle} to use when +link{groupDef:printing,printing}.
    // @group imageColumns
    // @group printing
    // @see ListGrid.booleanBaseStyle
    // @visibility external
    //<

    //> @attr listGrid.booleanTrueImage (SCImgURL : null : IRWA)
    // Image to display for a true value in a boolean field. The special value "blank" means
    // that no image will be shown.
    // <P>
    // To turn this off explicitly set +link{listGridField.suppressValueIcon} to true.
    // <P>
    // If this, +link{listGrid.booleanFalseImage} and +link{listGrid.booleanPartialImage}
    // are unset, this will be set to the default +link{CheckboxItem.checkedImage}.
    // @see ListGrid.booleanFalseImage
    // @see ListGrid.booleanPartialImage
    // @see ListGrid.printBooleanTrueImage
    // @group imageColumns
    // @visibility external
    //<
    booleanTrueImage:null,

    //> @attr listGrid.booleanFalseImage (SCImgURL : null : IRWA)
    // Image to display for a false value in a boolean field. Default <code>null</code> value
    // or the special value "blank" means no image will be displayed.
    // <P>
    // To turn this off explicitly set +link{listGridField.suppressValueIcon} to true
    // <P>
    // If this, +link{listGrid.booleanTrueImage} and +link{listGrid.booleanPartialImage}
    // are undefined, this will be set to +link{checkboxItem.uncheckedImage}.
    // @group imageColumns
    // @see ListGrid.booleanTrueImage
    // @see ListGrid.booleanPartialImage
    // @see ListGrid.printBooleanFalseImage
    // @visibility external
    //<
    booleanFalseImage:null,

    //> @attr listGrid.booleanPartialImage (SCImgURL : null : IRWA)
    // Image to display for a partially true value in a boolean field (typically selection).
    // The special value "blank" means that no image will be shown.
    // <P>
    // To turn this off explicitly set +link{listGridField.suppressValueIcon} to true.
    // <P>
    // If this, +link{listGrid.booleanTrueImage} and +link{listGrid.booleanFalseImage}
    // are unset, this will be set to the default +link{CheckboxItem.partialSelectedImage}.
    // @see ListGrid.booleanTrueImage
    // @see ListGrid.booleanFalseImage
    // @see ListGrid.printBooleanPartialImage
    // @group imageColumns
    // @visibility external
    //<
    booleanPartialImage:null,

    //> @attr listGrid.printBooleanTrueImage (SCImgURL : null : IRWA)
    // If set, the +link{ListGrid.booleanTrueImage} to use when +link{groupDef:printing,printing}.
    // @group imageColumns
    // @group printing
    // @see ListGrid.booleanTrueImage
    // @visibility external
    //<
    printBooleanTrueImage:null,

    //> @attr listGrid.printBooleanFalseImage (SCImgURL : null : IRWA)
    // If set, the +link{ListGrid.booleanFalseImage} to use when +link{groupDef:printing,printing}.
    // @group imageColumns
    // @group printing
    // @see ListGrid.booleanFalseImage
    // @visibility external
    //<
    printBooleanFalseImage:null,

    //> @attr listGrid.printBooleanPartialImage (SCImgURL : null : IRWA)
    // If set, the +link{ListGrid.booleanPartialImage} to use when +link{groupDef:printing,printing}.
    // @group imageColumns
    // @group printing
    // @see ListGrid.booleanPartialImage
    // @visibility external
    //<
    printBooleanPartialImage:null,

    //> @attr listGrid.booleanImageWidth (number : 16 : IRWA)
    // Width for the +link{listGrid.booleanTrueImage}, +link{listGrid.booleanFalseImage}
    // and +link{listGrid.booleanPartialImage}.
    // Note: If +link{listGrid.booleanTrueImage} is unset, the +link{checkboxItem.checkedImage}
    // will be used to indicate a true value in a boolean field. In this case this property is
    // ignored in favor of +link{checkboxItem.valueIconWidth}.
    // @group imageColumns
    // @visibility external
    //<

    booleanImageWidth:16,

    //> @attr listGrid.booleanImageHeight (number : 16 : IRWA)
    // Height for the +link{listGrid.booleanTrueImage}, +link{listGrid.booleanFalseImage}
    // and +link{listGrid.booleanPartialImage}.
    // Note: If +link{listGrid.booleanTrueImage} is unset, the +link{checkboxItem.checkedImage}
    // will be used to indicate a true value in a boolean field. In this case this property is
    // ignored in favor of +link{checkboxItem.valueIconHeight}.
    // @group imageColumns
    // @visibility external
    //<
    booleanImageHeight:16,


    //> @attr listGrid.mozBodyOutlineColor (string : "white" : IRWA)
    // If we're in Moz Firefox 1.5 or above, and showing a header, what color should the
    // dotted focus outline show around the body. Must be a color that contrasts with the
    // header of the ListGrid.
    // @visibility internal
    //<
    mozBodyOutlineColor:"white",
    //> @attr listGrid.mozBodyNoHeaderOutlineColor (string : "red" : IRWA)
    // If we're in Moz Firefox 1.5 or above, and we're not showing a header, what color
    // should the dotted focus outline show around the body. Must be a color that contrasts
    // with the header of the ListGrid.
    // @visibility internal
    //<
    mozBodyNoHeaderOutlineColor:"red",



// -----------------------------------------------------------------------------------------
// row numbers
//

    //> @attr listGrid.showRowNumbers (boolean : null : IRWA)
    // When set to true, shows an additional field at the beginning of the field-list
    // (respecting RTL) that displays the current rowNum for each record.
    // @group rowNumberField
    // @visibility external
    //<

    //> @attr listGrid.applyRowNumberStyle (boolean : true : IRWA)
    // If +link{showRowNumbers} is true, should we apply the +link{rowNumberStyle} to
    // the +link{listGrid.rowNumberField}
    // @group rowNumberField
    // @visibility external
    //<
    applyRowNumberStyle:true,

    //> @attr listGrid.rowNumberStyle (CSSStyleName : "specialCol" : IRWA)
    // The CSS Style name for the +link{listGrid.rowNumberField}.
    // @group rowNumberField
    // @visibility external
    //<
    rowNumberStyle: "specialCol",

    //> @attr listGrid.rowNumberStart (number : 1 : IRWA)
    // The number to start the row-count from - default value is 1.
    // @group rowNumberField
    // @visibility external
    //<
    rowNumberStart: 1,

    //> @attr listGrid.rowNumberField (AutoChild ListGridField : null : IRWA)
    // An automatically generated field that displays the current row number when
    // +link{listGrid.showRowNumbers, showRowNumbers} is true.
    // @group rowNumberField
    // @visibility external
    //<
    rowNumberFieldDefaults: {
        name: "_rowNumberField",
        excludeFromState:true,
        canEdit: false,
        canFilter:false,
        canGroupBy: false,
        canSort: false,
        canExport: false,
        canHide: false,
        canReorder: false,
        canDragResize: false,
        // make this special field canHilite false so we don't see it in HiliteEditors by default
        canHilite: false,
        canAutoFitWidth: false,
        showAlternateStyle: false,
        _isRowNumberField: true,
        showDefaultContextMenu: false,
        keyboardFiresRecordClick: false,
        showGroupSummary:false,
        showGridSummary:false,
        summaryValue: "&nbsp;",
        formatCellValue : function (value, record, rowNum, colNum, grid) {
            if (grid.isGrouped) {
                if (record == null || record._isGroup) return "&nbsp;";

                var groupedRowNum = grid.getGroupedRecordIndex(record);
                // skip any records we can't find in the group tree (EG summary rows)
                if (groupedRowNum == -1) return null;
                return (grid.rowNumberStart + groupedRowNum);
            } else {
                return this.rowNumberStart + rowNum;
            }
        },
        autoFreeze: true,
        // disable this from ever being assigned as the treeField
        treeField:false
    },

    // helper method to get index of the group in which a record exists
    getParentGroupIndex : function (record) {
        // bail if we're not grouped (return group 0)
        if (!this.isGrouped) return 0;

        // find out which group this record is in
        var tree = this.groupTree,
            parentNode = tree.getParent(record),
            rootChildren = tree.getChildren(tree.getParent(parentNode)),
            groupCount = 0;

        if (!isc.isA.ResultSet(rootChildren) || rootChildren.lengthIsKnown()) {
            for (var i = 0, length = rootChildren.getLength(); i < length; ++i) {
                var child = rootChildren.getCachedRow(i);
                if (child != null && child.groupValue == parentNode.groupValue) {
                    groupCount = i;
                    break;
                }
            }
        }

        return groupCount;
    },

    // helper method to get the true row-number for a grouped record, whether or not prior groups are closed
    getGroupedRecordIndex : function (record) {
        // bail if we're not grouped
        if (!this.isGrouped) return -1;
        // find the true index of this record in a grouped grid - indexOf doesn't cater for
        // closed groups
        var tree = this.groupTree,

            parentNode = tree == null ? null : tree.getParent(record);

        // Checking for parentNode == null allows us to skip group-summary nodes which
        // shouldn't be counted anyway.
        if (parentNode == null) return -1;


        if (tree.getLevel(record) > 1) {
            var recurseState = { currentIndex: 0, parentNode: parentNode, record: record };
            this.getGroupedRecordIndexRecursive(tree.root, recurseState);
            return recurseState.currentIndex;
        }

        var rootChildren = tree.getChildren(tree.getParent(parentNode)),
            groupCount = 0,
            trueIndex = 0;

        if (!isc.isA.ResultSet(rootChildren) || rootChildren.lengthIsKnown()) {
            for (var i = 0, rootChildrenLength = rootChildren.getLength(); i < rootChildrenLength; ++i) {
                var child = rootChildren.getCachedRow(i);
                if (child != null) {
                    if (child.groupValue == parentNode.groupValue) {
                        var siblings = tree.getChildren(child);
                        if (!isc.isA.ResultSet(siblings) || siblings.lengthIsKnown()) {
                            for (var j = 0, siblingsLength = siblings.getLength(); j < siblingsLength; ++j) {
                                var sibling = siblings.getCachedRow(j);
                                if (sibling != null && this.objectsAreEqual(sibling, record)) {
                                    return trueIndex + j;
                                }
                            }
                        }
                    }
                    var prevSiblings = tree.getChildren(child);
                    if (!isc.isA.ResultSet(prevSiblings) || prevSiblings.lengthIsKnown()) {
                        var length = prevSiblings.getLength();
                        // Don't count group summary rows - these show up at the end of the
                        // group [and we support an arbitrary number of them]
                        if (this.showGroupSummary && !this.showGroupSummaryInHeader) {
                            for (var ii = length - 1; ii >=0; --ii) {
                                var prevSibling = prevSiblings.getCachedRow(ii);
                                if (prevSibling != null) {
                                    if (prevSibling[this.groupSummaryRecordProperty]) --length;
                                    else break;
                                }
                            }
                        }
                        trueIndex += length;
                    }
                }
            }
        }

        return trueIndex;
    },

    getGroupedRecordIndexRecursive : function (startNode, state) {
        var tree = this.groupTree;
        if (!startNode) startNode = tree.root;

        // iterate through all the children of the node
        var children = tree.getChildren(startNode);
        if (!children) return true;
        if (isc.isA.ResultSet(children) && !children.lengthIsKnown()) return true;

        var parentNode = state.parentNode,
            record = state.record
        ;

        // for each child
        var length = children.getLength();
        for (var i = 0; i < length; ++i) {
            var child = children.getCachedRow(i);

            if (child == null) {
                // Do nothing.
            } else if (tree.isFolder(child)) {
                if (!this.getGroupedRecordIndexRecursive(child, state)) {
                    return false;
                }
            } else {
                if (startNode.groupName == parentNode.groupName &&
                    startNode.groupValue == parentNode.groupValue &&
                    this.objectsAreEqual(child, record))
                {
                    return false;
                }
                // Don't count group summary rows
                if (!child[this.groupSummaryRecordProperty]) {
                    state.currentIndex++;
                }
            }
        }
        return true;
    },

    // helper method to compare the properties on two objects
    objectsAreEqual : function (object1, object2) {
        for (var key in object1) {
            if (object1[key] != object2[key]) return false;
        }
        return true;
    },

    _rowNumberFieldWidth: 30,
    getRowNumberField : function () {
        var grid = this,
            rnField = {
                // default the width
                width:this._rowNumberFieldWidth,
                rowNumberStart: this.rowNumberStart,
                getAutoFreezePosition: function () { return grid.getRowNumberFieldPosition() }
            }
        ;
        if (this.applyRowNumberStyle) rnField.baseStyle = this.rowNumberStyle;
        isc.addProperties(rnField, this.rowNumberFieldDefaults, this.rowNumberFieldProperties);

        rnField.title = isc.nbsp;

        return rnField;
    },

    getCurrentRowNumberField : function () {
        var fields = this.completeFields || this.fields,
            rnFields = fields.find(this.fieldIdProperty, "_rowNumberField");
        return !rnFields ? null : isc.isAn.Array(rnFields) ? rnFields[0] : rnFields;
    },

    //> @method listGrid.isRowNumberField()
    // Identifies whether the passed-in field is the specially generated
    // +link{listGrid.rowNumberField, rowNumberField} used when +link{showRowNumbers} is
    // true.  Use this method in your custom event handlers to avoid inappropriately
    // performing actions when the rowNumberField is clicked on.
    //
    // @param field (ListGridField) field to test
    // @return (Boolean) whether the provided field is the rowNumberField
    // @group rowNumberField
    // @visibility external
    //<
    isRowNumberField : function (field) {
        if (!field || !field._isRowNumberField) return false;
        else return true;
    },

    // helper function to get the rowNumber field position
    // Appears at the far left of the grid, to the left of the other special fields which are
    // - group summary title field
    // - expansion component icon field
    // - checkbox selection field
    getRowNumberFieldPosition : function () {
        if (this.fieldSourceGrid) return this.fieldSourceGrid.getRowNumberFieldPosition();
        if (!this.shouldShowRowNumberField()) return -1;
        return 0;
    },

    shouldShowRowNumberField : function () {
        // fieldSourceGrid: for cases like the summaryRow / filterEditor where we
        // share field objects across grids (and don't necessarily percolate settings like
        // 'showRowNumbers').
        return this.fieldSourceGrid ? this.fieldSourceGrid.shouldShowRowNumberField()
                                    : (this.showRowNumbers == true);
    },

    //> @attr listGrid.exportRawValues (Boolean : null : IR)
    // Dictates whether the data in this grid should be exported raw by
    // +link{listGrid.exportClientData, exportClientData()}.  If set to true,
    // data will not be processed by field-formatters during exports.
    // Decreases the time taken for large exports.  This property can also be set at the
    // +link{listGridField.exportRawValues, field level}.
    //
    // @visibility external
    //<

// -----------------------------------------------------------------------------------------
// Expando Rows
//

    //> @type ExpansionComponentPoolingMode
    // The method of pooling to employ for +link{listGrid.canExpandRecords,expansionComponents}.
    // <P>
    // @value "destroy" auto-created, built-in components are destroyed when record are
    //        +link{listGrid.collapseRecord,collapsed}.
    // @value "none" all expansion components are deparented from the grid when a record is
    //        +link{listGrid.collapseRecord,collapsed} but are not destroyed.  It is the responsibility
    //        of the developer to handle component destruction
    // @visibility external
    //<

    //> @attr listGrid.expansionComponentPoolingMode (ExpansionComponentPoolingMode : "destroy" : IRWA)
    // The method of +link{type:RecordComponentPoolingMode, component-pooling} to employ for
    // +link{canExpandRecords,expansionComponents}.
    // <P>
    // The default mode is "destroy", which means that automatically created expansionComponents
    // are destroyed when rows are collapsed.
    //
    // @visibility external
    //<
    expansionComponentPoolingMode: "destroy",


    destroyCustomExpansionComponents: false,

    //> @attr listGrid.canExpandRecords (Boolean : false : IRWA)
    // When set to true, shows an additional field at the beginning of the field-list
    // (respecting RTL) to allow users to expand and collapse individual records.
    // See +link{listGrid.expandRecord()} and +link{listGrid.expansionMode} for details
    // on record expansion.
    // <P>
    // +link{listGrid.virtualScrolling} is automatically enabled when canExpandRecords is
    // set to true.
    // <P>
    // Note that expanded records are not currently supported in conjunction
    // with +link{listGridField.frozen,frozen fields}.
    //
    // @group expansionField
    // @visibility external
    //<

    //> @attr listGrid.canExpandMultipleRecords (Boolean : true : IRWA)
    // When +link{listGrid.canExpandRecords} is true, this property indicates whether multiple
    // records can be expanded simultaneously.  If set to false, expanding a record will
    // automatically collapse any record which is already expanded.  The default value is
    // <code>true</code>.
    //
    // @group expansionField
    // @visibility external
    //<
    canExpandMultipleRecords: true,

    //> @attr listGrid.maxExpandedRecords (integer : null : IRWA)
    // When +link{listGrid.canExpandRecords} and +link{listGrid.canExpandMultipleRecords} are
    // both true, this property dictates the number of
    // records which can be expanded simultaneously.  If the expanded record count hits the
    // value of this property, further attempts to expand records will result in a popup
    // warning (see +link{listGrid.maxExpandedRecordsPrompt}) and expansion will be cancelled.
    // <P>
    // The default value is null, meaning there is no limit on the number of expanded records.
    //
    // @group expansionField
    // @visibility external
    //<

    //> @attr listGrid.maxExpandedRecordsPrompt (HTMLString : "This grid is limited to ${count} simultaneously expanded records.  Please collapse some expanded records and retry." : IR)
    // This is a dynamic string - text within <code>&#36;{...}</code> will be evaluated as JS code
    // when the message is displayed. Note that the local variable <code>count</code> will be
    // available and set to this.maxExpandedRecords. The string will be executed in the scope of the
    // ListGrid so <code>this</code> may also be used to determine other information about this grid.
    // <P>
    // Default value returns <P>
    // <code>
    // <i>This grid is limited to <code>[+link{listGrid.maxExpandedRecords}]</code> simultaneously
    // expanded records.  Please collapse some expanded records and retry.</i>
    // </code>
    // @visibility external
    // @group i18nMessages
    //<
    maxExpandedRecordsPrompt: "This grid is limited to ${count} simultaneously expanded records.  Please collapse some expanded records and retry.",

    //> @type ExpansionMode
    // When +link{ListGrid.canExpandRecords, canExpandRecords} is true, ExpansionMode
    // dictates the type of UI to be displayed in the expanded portion of the row.
    // <P>
    // There are a number of builtin ExpansionModes and you can override
    // +link{listGrid.getExpansionComponent, getExpansionComponent()} to create your own
    // expansion behaviors.
    //
    //  @value  "detailField"  Show a single field's value in an +link{class:HtmlFlow}. Field
    //      to use is +link{listGrid.detailField}.
    //  @value  "details"   Show a +link{class:DetailViewer} displaying those fields from the
    //      record which are not already displayed in the grid.
    //  @value  "related"    Show a separate +link{class:ListGrid} containing related-records.
    //      See +link{ListGridRecord.detailDS} and +link{ListGrid.recordDetailDSProperty} for
    //      more information.
    //  @value  "editor"    Show a +link{class:DynamicForm} to edit those fields from the
    //      record which are not already present in the grid.  If the record is collapsed with
    //      unsaved changes and +link{listGrid.expansionEditorShowSaveDialog} is not set, Edits
    //      will be saved automatically, or stored as +link{group:editing,editValues} if
    //      +link{listGrid.autoSaveEdits} is false.  Otherwise, a confirmation dialog is
    //      displayed.  Can optionally show a
    //      +link{listGrid.showExpansionEditorSaveButton,save button} and
    //      +link{listGrid.expansionEditorCollapseOnSave,auto-collapse} when save is pressed.
    //      If a record fails validation on save and the field in question is not visible in
    //      the grid, the record is automatically expanded and validated to show the errors.
    //  @value  "detailRelated"    Show a +link{class:DetailViewer} displaying those fields
    //      from the record not already displayed in the grid, together with a separate
    //      +link{class:ListGrid} containing related-records.
    // @group expansionField
    // @visibility external
    //<

    //> @attr listGrid.detailField (String : null : IRWA)
    // The field whose contents to show in the expanded portion of a record when
    // +link{listGrid.canExpandRecords, canExpandRecords} is <code>true</code> and
    // +link{type:ExpansionMode, listGrid.expansionMode} is <code>detailField</code>.
    // @group expansionField
    // @visibility external
    //<

    //> @attr listGrid.expansionMode (ExpansionMode : null : IRWA)
    // The +link{ExpansionMode} for records in this grid.
    // Default <code>null</code> value means no expansion.
    // @group expansionField
    // @visibility external
    //<

    //> @attr listGrid.expansionField (AutoChild ListGridField : null : IRWA)
    // The field providing the facility to expand and collapse rows.
    // @group expansionField
    // @visibility external
    //<
    expansionFieldDefaults: {
        name: "_expansionField",
        canEdit: false,
        canFilter:false,
        canGroupBy: false,
        canSort: false,
        canExport: false,
        canHide: false,
        canReorder: false,
        canDragResize: false,
        // make this special field canHilite false so we don't see it in HiliteEditors
        canHilite: false,
        _isExpansionField: true,
        showDefaultContextMenu: false,
        keyboardFiresRecordClick: false,
        cellAlign: "center",
        summaryValue: "&nbsp;",
        recordClick: function (viewer, record, recordNum, field, fieldNum, value, rawValue) {
            if (!viewer.canExpandRecords || !field._isExpansionField) return;
            if (!viewer._canExpandRecord(record, recordNum)) return;
            if (viewer.isExpanded(record)) viewer.collapseRecord(record);
            else viewer.expandRecord(record);
        },
        formatCellValue : function (value, record, rowNum, colNum, grid) {
            // This ensures that if we're looking at an edit row for a new record we
            // don't show the expansion icon
            record = grid.getCellRecord(rowNum, colNum);
            if (record == null || record._isGroup) return null;
            if (!grid._canExpandRecord(record, rowNum)) return null;
            var src = grid.isExpanded(record) ? grid.expansionFieldTrueImage : grid.expansionFieldFalseImage
            if (grid.expansionFieldImageShowRTL && grid.isRTL()) {
                src = isc.Img.urlForState(src, null, null, null, null, "rtl");
            }
            return grid.getValueIconHTML(src, this);
        },
        autoFreeze: true,
        // disable this from ever being assigned as the treeField
        treeField:false
    },

    // Helper method - should this grid show the special expansion field when canExpandRecords is true
    shouldShowExpansionField : function () {
        return this.fieldSourceGrid ? this.fieldSourceGrid.shouldShowExpansionField()
                                    : this.canExpandRecords;
    },

    //> @attr listGrid.expansionFieldTrueImage (SCImgURL : null :IRWA)
    // If +link{listGrid.canExpandRecords} is set to <code>true</code>, this property
    // determines the image to display in the expansion field for expanded rows.
    // If unset, the +link{listGrid.booleanTrueImage} will be used.
    // @see listGrid.expansionFieldFalseImage
    // @see listGrid.expansionFieldImageWidth
    // @see listGrid.expansionFieldImageHeight
    // @group expansionField
    // @visibility external
    //<
    expansionFieldTrueImage: "[SKINIMG]/ListGrid/group_opened.gif",

    //> @attr listGrid.expansionFieldFalseImage (SCImgURL : null :IRWA)
    // If +link{listGrid.canExpandRecords} is set to <code>true</code>, this property
    // determines the image to display in the expansion field for collapsed rows.
    // If unset, the +link{listGrid.booleanFalseImage} will be used.
    // @see listGrid.expansionFieldTrueImage
    // @see listGrid.expansionFieldImageWidth
    // @see listGrid.expansionFieldImageHeight
    // @group expansionField
    // @visibility external
    //<
    expansionFieldFalseImage: "[SKINIMG]/ListGrid/group_opening.gif",

    //> @attr listGrid.expansionFieldImageShowRTL (boolean : false : IRA)
    // If this grid is in RTL mode, should an "_rtl" suffix be added to the +link{expansionFieldTrueImage,expansionFieldTrueImage}
    // and +link{expansionFieldFalseImage,expansionFieldFalseImage} image URLs? This should only
    // be enabled if RTL media for the true and false expansion field images are available.
    // @group RTL
    // @group expansionField
    // @visibility internal
    //<
    expansionFieldImageShowRTL: false,

    //> @attr listGrid.expansionFieldImageWidth (integer : null : IR)
    // If +link{listGrid.canExpandRecords} is set to <code>true</code>, this property
    // may be set to govern the width of the expansion image displayed to indicate whether a row
    // is expanded. If unset, the expansionField image will be sized to match the
    // +link{listGrid.booleanImageWidth} for this grid.
    // @group expansionField
    // @visibility external
    //<

    //> @attr listGrid.expansionFieldImageHeight (integer : null : IR)
    // If +link{listGrid.canExpandRecords} is set to <code>true</code>, this property
    // may be set to govern the height of the expansion image displayed to indicate whether a
    // row is expanded. If unset, the expansionField image will be sized to match the
    // +link{listGrid.booleanImageHeight} for this grid.
    // @group expansionField
    // @visibility external
    //<

    //> @method listGrid.getExpansionField()
    // Returns the specially generated expansion field used when +link{canExpandRecords} is
    // true.
    // <P>
    // Called during +link{setFields()}, this method can be overridden to add advanced dynamic
    // defaults to the expansion field (call Super, modify the default field returned by Super,
    // return the modified field).  Normal customization can be handled by just setting
    // +link{AutoChild} properties, as mentioned under the docs for +link{listGrid.expansionField}.
    //
    // @return (ListGridField)
    // @group expansionField
    // @visibility external
    //<
    // the amount to add to the icon width to get the expansion field width
    expansionFieldExtraWidth: 16,
    getExpansionField : function () {
        var grid = this,
            expField = {
                excludeFromState:true,
                // default the width to the width of the icon plus an arbitrary buffer
                width: this._getExpansionFieldImageWidth() + this.expansionFieldExtraWidth,
                getAutoFreezePosition: function () { return grid.getExpansionFieldPosition() }
            }
        ;

        // if expansionFieldImageWidth/Height are set on this grid, pass them through to the field
        expField.valueIconWidth = this._getExpansionFieldImageWidth();
        expField.valueIconHeight = this._getExpansionFieldImageHeight();

        // combine the properties for the field using the autoChild pattern
        isc.addProperties(expField, this.expansionFieldDefaults, this.expansionFieldProperties);

        expField.title = isc.nbsp;

        return expField;
    },

    getCurrentExpansionField : function () {
        var fields = this.completeFields || this.fields,
            expFields = fields.find(this.fieldIdProperty, "_expansionField");
        return !expFields ? null : isc.isAn.Array(expFields) ? expFields[0] : expFields;
    },

    _getExpansionFieldImageWidth : function () {
        return this.expansionFieldImageWidth || this.booleanImageWidth ||
                (isc.CheckboxItem ? isc.CheckboxItem.getInstanceProperty("valueIconWidth") : null);
    },
    _getExpansionFieldImageHeight : function () {
        return this.expansionFieldImageHeight || this.booleanImageHeight ||
                (isc.CheckboxItem ? isc.CheckboxItem.getInstanceProperty("valueIconHeight") : null);
    },

    //> @method listGrid.isExpansionField()
    // Identifies whether the passed-in field is the specially generated
    // +link{listGrid.expansionField,expansionField} used when +link{canExpandRecords} is
    // true.  Use this method in your custom event handlers to avoid inappropriately
    // performing actions when the expansionField is clicked on.
    //
    // @param field (ListGridField) field to test
    // @return (Boolean) whether the provided field is the expansion field
    // @group expansionField
    // @visibility external
    //<
    isExpansionField : function (field) {
        if (!field || !field._isExpansionField) return false;
        else return true;
    },

    // helper function to get the expansion field position
    // This is one of the fields that appears "stuck" to the left of the grid - these are
    // - row number field
    // - group summary title field
    // - expansion field
    // - checkbox selection field
    getExpansionFieldPosition : function () {
        if (this.fieldSourceGrid != null) return this.fieldSourceGrid.getExpansionFieldPosition();

        if (!this.shouldShowExpansionField()) return -1;

        var pos = 0;
        if (this.shouldShowRowNumberField()) pos++;
        if (this.shouldShowCheckboxField()) pos++;
        return pos;
    },

    _canExpandRecord:function (record,rowNum) {
        if (record == null) record = this.getRecord(rowNum);
        if (record == null) return false;
        return this.canExpandRecord(record,rowNum);
    },
    //> @method listGrid.canExpandRecord()
    // Indicates whether a given record or rowNum can be expanded.  The default implementation
    // checks the value of +link{listGrid.canExpandRecords} and
    // <code>record[+link{listGrid.canExpandRecordProperty}]</code>.
    // <P>
    // Override this method for more specific control over individual record expansion.
    // <P>
    // <b>Note:</b> Rows with no underlying record in the data array - for example newly
    // added edit rows that have not yet been saved - cannot be expanded.
    //
    // @param record (ListGridRecord) record to work with
    // @param rowNum (Number) rowNum of the record to work with
    // @return (boolean) true if the record can be expanded
    // @group expansionField
    // @visibility external
    //<
    canExpandRecord : function (record, rowNum) {
        return record[this.canExpandRecordProperty] == false ? false :
            true && (this.canExpandRecords != false);
    },

    //> @method listGrid.setCanExpandRecords()
    // Setter for +link{listGrid.canExpandRecords}
    // @param canExpand (boolean) new value for listGrid.canExpandRecords.
    // @visibility external
    //<
    setCanExpandRecords : function (canExpand) {
        if (this.canExpandRecords == canExpand) return;
        if (!canExpand) {
            var data = this.data;
            if (data) {
                var expandedRows = this._getExpandedRows();
                if (expandedRows != null) {
                    for (var i = 0; i < expandedRows.length; i++) {
                        this.collapseRecord(expandedRows[i]);
                    }
                }
            }
        }
        this.canExpandRecords = canExpand;
        this.refreshFields();
    },

    //> @method listGrid.expandRecord()
    // Expands a given +link{ListGridRecord, record} by creating a subcomponent and inserting it
    // in to the record's grid-row.  A number of built-in +link{ExpansionMode, expansionModes}
    // are supported by the default implementation of
    // +link{listGrid.getExpansionComponent, getExpansionComponent()} and you can override
    // that method to provide your own expansion behavior.
    // <P>
    // Once a record has been expanded, the currently visible expansion component may be
    // retrieved via +link{getCurrentExpansionComponent()}.
    //
    // @param record (ListGridRecord) record to expand
    // @group expansionField
    // @visibility external
    //<
    _expandedRecordCount: 0,
    expandRecord : function (record, shouldRedraw) {
        if (!this.body) {
            // called before draw - add the record to an internal array, this method
            // will be called again with this record after the grid has drawn
            if (!this._recordsToExpand) this._recordsToExpand = [];
            this._recordsToExpand.add(record);
            return;
        }

        var result = false,
            component
        ;

        if (!this.isExpanded(record)) {

            // notification method / cancellation point

            if (this.onExpandRecord != null && !this.onExpandRecord(record)) return false;

            if (!this.canExpandMultipleRecords) {
                // can only expand one record - if one's expanded already, collapse it now
                if (this._currentExpandedRecord)
                    this.collapseRecord(this._currentExpandedRecord);
                this._currentExpandedRecord = record;
            } else if (this.maxExpandedRecords != null) {
                // limited number of expanded records allowed - if we've hit that number, show the
                // maxExpandedRecordsPrompt and return
                if (this._expandedRecordCount >= this.maxExpandedRecords) {
                    var message = this.maxExpandedRecordsPrompt.evalDynamicString(this, {
                        count: this.maxExpandedRecords
                    });
                    isc.say(message);
                    return false;
                }
            }

            // create an appropriate subcomponent and bind it
            component = this.getExpansionComponent(record);
            var isRTL = this.isRTL(),
                layout = this.createAutoChild("expansionLayout",
                {
                    layoutLeftMargin: isRTL ? 0 : this.embeddedComponentIndent,
                    layoutRightMargin: isRTL ? this.embeddedComponentIndent : 0,
                    members: [ component ]
                }
            );
            layout.isExpansionComponent = true;
            layout.removeOnHideField = true;
            this.addEmbeddedComponent(layout, record, this.data.indexOf(record));
            this._setExpanded(record, true);
            this._setExpansionComponent(record, true);
            this._expandedRecordCount++;
            if (!this.canExpandMultipleRecords) this._currentExpandedRecord = record;
            // only return true if the record was actually expanded as a result of this call
            result = true;
        }

        if (shouldRedraw != false) {
            this.delayCall("markForRedraw", ["Expanded Record"]);
        }

        return result;
    },

    //> @method listGrid.expandRecords()
    // Expands the passed list of +link{ListGridRecord, records} by creating a subcomponent for
    // each record and inserting them it in to the record's grid-row.  Calls
    // +link{listGrid.expandRecord, expandRecord} for each passed record, but only marks the
    // grid for redraw once, after all expansions are complete.
    //
    // @param records (Array of ListGridRecord) records to expand
    // @group expansionField
    // @visibility external
    //<
    expandRecords : function (records) {
        if (!records || records.length == 0) return;
        var len = records.length;
        for (var i=0; i<len; i++) {
            var isLast = (i == len - 1);
            this.expandRecord(records[i], isLast);
        }
    },

    //> @method listGrid.collapseRecord()
    // Collapses a given +link{ListGridRecord, record} which has been previously expanded using
    // +link{listGrid.expandRecord}.
    // <P>
    // Depending on the +link{listGrid.expansionComponentPoolingMode, pooling mode},
    // this method may automatically destroy expansionComponents.  By default, components
    // created automatically by the ListGrid will be auto-destroyed.  This
    // behavior can be changed by setting a different pooling mode.
    // <P>
    // Note that components created via an override to +link{listGrid.getExpansionComponent}
    // will *not* be auto-destroyed - developers should override collapseRecord to take care of
    // clean-up for such components.
    //
    // @param record (ListGridRecord) record to collapse
    // @group expansionField
    // @visibility external
    //<
    collapseRecord : function (record, shouldRedraw) {
        var component =
                (record && this._hasEmbeddedComponents(record)) ?
                        this._getEmbeddedComponents(record).find("isExpansionComponent", true) : null
        ;

        // set this flag here because collapse can be interactive (and can be cancelled)
        this._redrawOnCollapseRecord = shouldRedraw;

        if (isc.isA.Layout(component)) {
            var member = component.getMember(0);
            if (member) {
                if (!isc.isA.DynamicForm(member)) member = member.formMember;
                if (isc.isA.DynamicForm(member) && member.valuesHaveChanged()) {
                    this.saveAndCollapseRecord(member, component, record, true, true);
                    return;
                }
            }
        }

        this._collapseRecord(record, component);
    },

    //> @method listGrid.collapseRecords()
    // Collapses the passed list of expanded +link{ListGridRecord, records}.  Calls
    // +link{listGrid.collapseRecord, collapseRecord} for each passed record, but only marks
    // the grid for redraw once, after all records have been collapsed.
    //
    // @param records (Array of ListGridRecord) records to collapse
    // @group expansionField
    // @visibility external
    //<
    collapseRecords : function (records) {
        if (!records || records.length == 0) return;
        var len = records.length;
        for (var i=0; i<len; i++) {
            var isLast = (i == len - 1);
            this.collapseRecord(records[i], isLast);
        }
    },

    saveAndCollapseRecord : function (member, component, record, shouldShowSaveDialog, shouldCollapse) {
        if (this.autoSaveEdits == true) {
            var _this = this;

            if (this.expansionEditorShowSaveDialog && shouldShowSaveDialog) {
                isc.confirm(this.expansionEditorSaveDialogPrompt,
                    function (yes) {
                        if (yes) {
                            _this.saveExpansionDetail(member, component, record, shouldCollapse);
                        } else {
                            if (shouldCollapse) _this._collapseRecord(record, component);
                        }
                    }
                );
            } else {
                this.saveExpansionDetail(member, component, record, shouldCollapse);
            }
            return;
        } else {
            var _values = member.getChangedValues(),
                rowNum = this.getRecordIndex(record),
                _this = this
            ;

            if (this.expansionEditorShowSaveDialog && shouldShowSaveDialog) {
                isc.confirm("You have unsaved changes - do you want to save them now?",
                    function (yes) {
                        if (yes) {
                            _this._saveExpansionEditorValues(rowNum, _values);
                        }
                        if (shouldCollapse) _this._collapseRecord(record, component);
                    }
                );
                return;
            } else {
                _this._saveExpansionEditorValues(rowNum, _values);
                if (shouldCollapse) this._collapseRecord(record, component);
                return;
            }
        }
    },


    _saveExpansionEditorValues : function (rowNum, values) {
        for (var key in values) {
            this.setEditValue(rowNum, key, values[key]);
        }
    },

    saveExpansionDetail : function (member, component, record, shouldCollapse) {
        var _this = this;
        member.saveData(
            function (dsResponse, data, dsRequest) {
                if (data) {
                    record = data;
                    if (shouldCollapse) _this._collapseRecord(record, component);
                    else member.editRecord(record);
                }
            }, { showPrompt: true, promptStyle: "cursor" }
        );
    },

    _collapseRecord : function (record, component) {
        component = component ||
            (record && this._hasEmbeddedComponents(record) ?
                    this._getEmbeddedComponents(record).find("isExpansionComponent", true) : null);

        if (this.isExpanded(record)) {
            // notification method / cancellation point

            if (this.onCollapseRecord != null && !this.onCollapseRecord(record)) return;

            if (this._currentExpandedRecord && this._currentExpandedRecord == record)
                delete this._currentExpandedRecord;

            this.removeEmbeddedComponent(record, component ? component : this.frozenFields ? this.frozenFields.length : 0);
            this._expandedRecordCount--;
        }
        this._setExpanded(record, false);

        if (this._redrawOnCollapseRecord != false) {
            // only markForRedraw if shouldRedraw:false wasn't passed to collapseRecord
            this.markForRedraw();
        }
    },



    _$expandedPrefix:"_expanded_",

    //> @method listGrid.isExpanded()
    // Whether a given +link{ListGridRecord, record} is expanded or collapsed.
    //
    // @param record (ListGridRecord) record in question
    // @group expansionField
    // @return  (Boolean)           true if the node is expanded
    // @visibility external
    //<
    isExpanded : function (record) {
        return (!record ? false : !!record[this._$expandedPrefix + this.ID]);
    },

    _setExpanded : function (record, value) {
        record[this._$expandedPrefix + this.ID] = value;
    },
    _getExpandedRows : function () {
        return this.data.findAll(this._$expandedPrefix + this.ID, true);
    },

    _$hasExpansionComponentPrefix:"_hasExpansionComponent_",
    _hasExpansionComponent : function (record) {
        return record[this._$hasExpansionComponentPrefix + this.ID];
    },
    _setExpansionComponent : function (record, value) {
        record[this._$hasExpansionComponentPrefix + this.ID] = value;
    },


    _$embeddedComponentsPrefix:"_embeddedComponents_",

    _hasEmbeddedComponents : function (record) {
        var ids = record[this._$embeddedComponentsPrefix + this.ID];
        return (ids != null && ids.length > 0);
    },
    _getEmbeddedComponents : function (record) {
        // Convert array of IDs into an array of component references
        var ids = record[this._$embeddedComponentsPrefix + this.ID],
            components = []
        ;
        if (!ids) return null;
        for (var i = 0; i < ids.length; i++) {
            components[i] = isc.Canvas.getById(ids[i]);
        }
        return components;
    },
    _setEmbeddedComponents : function (record, value) {
        record[this._$embeddedComponentsPrefix + this.ID] = value;
    },

    _addEmbeddedComponent : function (record, component) {
        if(!record[this._$embeddedComponentsPrefix + this.ID]) {
            record[this._$embeddedComponentsPrefix + this.ID] = [];
        }
        if (!record[this._$embeddedComponentsPrefix + this.ID].contains(component.getID())) {
            record[this._$embeddedComponentsPrefix + this.ID].add(component.getID());
        }
    },
    _removeEmbeddedComponent : function (record, component) {

        var ids = record[this._$embeddedComponentsPrefix + this.ID];
        if (ids == null) return;
        if (ids.length == 0) {
            record[this._$embeddedComponentsPrefix + this.ID] = null;
            return;
        }
        ids.remove(component.getID());
        if (ids.length == 0) {
            record[this._$embeddedComponentsPrefix + this.ID] = null;
        }
    },

    _deleteEmbeddedComponents : function (record, value) {
        delete record[this._$embeddedComponentsPrefix + this.ID];
    },


    _$recordComponentsPrefix:"_recordComponents_",
    _hasRecordComponents : function (record) {
        return (record && record[this._$recordComponentsPrefix + this.ID] != null);
    },
    _getRecordComponents : function (record) {
        if (!record) return null;

        // Convert array of IDs into an array of component references
        var ids = record[this._$recordComponentsPrefix + this.ID],
            components = {}
        ;
        if (ids) {
            for (var key in ids) {
                if (ids[key].isNullMarker) {
                    components[key] = ids[key];
                } else {
                    components[key] = isc.Canvas.getById(ids[key]);
                }
            }
        }
        return components;
    },
    _addRecordComponent : function (record, fieldName, component) {
        if(!record[this._$recordComponentsPrefix + this.ID]) {
            record[this._$recordComponentsPrefix + this.ID] = {};
        }
        if (component.isNullMarker) {
            record[this._$recordComponentsPrefix + this.ID][fieldName] = component;
        } else {
            record[this._$recordComponentsPrefix + this.ID][fieldName] = component.getID();
        }
    },
    _deleteRecordComponent : function (record, fieldName) {
        var ids = record[this._$recordComponentsPrefix + this.ID];
        if (ids == null) return;
        if (isc.isAn.emptyObject(ids)) {
            record[this._$recordComponentsPrefix + this.ID] = null;
            return;
        }
        // Not per-cell - just use the special "no field" fieldName
        if (fieldName == null) fieldName = this._$noFieldString;

        delete ids[fieldName];
        if (isc.isAn.emptyObject(ids)) {
            record[this._$recordComponentsPrefix + this.ID] = null;
        }
    },

    //> @attr listGrid.expansionDetailField (MultiAutoChild HTMLFlow : null : RA)
    // Automatically generated +link{class:HTMLFlow} for displaying the contents of
    // +link{listGrid.detailField, a specified field} in a record's expanded section when
    // +link{type:ExpansionMode, listGrid.expansionMode} is <code>detailField</code>.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>listGrid.expansionDetailFieldProperties</code> and
    // <code>listGrid.expansionDetailFieldDefaults</code>.
    // <P>
    // Note, however, that this is a multi-instance component (potentially one per record),
    // so it is created using +link{Class.createAutoChild, createAutoChild()} not
    // +link{Class.addAutoChild, addAutoChild()}, and no default single instance is created by
    // name on the grid.
    //
    // @group expansionField
    // @visibility external
    //<
    expansionDetailFieldDefaults: {
        _constructor: isc.HTMLFlow,
        autoDraw: false,
        width: "100%",
        height: "100%"
    },
    //> @attr listGrid.expansionDetails (MultiAutoChild DetailViewer : null : RA)
    // Automatically generated +link{class:DetailViewer} for displaying the details of a record
    // in its expanded section when
    // +link{type:ExpansionMode, listGrid.expansionMode} is <code>details</code>.  Note that
    // only those fields
    // which do not already appear in the grid are displayed in the expanded section.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>listGrid.expansionDetailsProperties</code> and
    // <code>listGrid.expansionDetailsDefaults</code>.
    // <P>
    // Note, however, that this is a multi-instance component (potentially one per record),
    // so it is created using +link{Class.createAutoChild, createAutoChild()} not
    // +link{Class.addAutoChild, addAutoChild()}, and no default single instance is created by
    // name on the grid.
    //
    // @group expansionField
    // @visibility external
    //<
    expansionDetailsDefaults: {
        _constructor: isc.DetailViewer,
        autoDraw: false,
        width: "100%"
    },
    //> @attr listGrid.expansionRelated (MultiAutoChild ListGrid : null : RA)
    // Automatically generated +link{class:ListGrid} for displaying data related to a record
    // in its expanded section when
    // +link{type:ExpansionMode, listGrid.expansionMode} is <code>related</code>.
    // The +link{class:DataSource} containing the related data is provided by
    // +link{listGrid.getRelatedDataSource, getRelatedDataSource()} which, by default,
    // returns the DataSource referred to in +link{listGridRecord.detailDS}.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>listGrid.expansionRelatedProperties</code> and
    // <code>listGrid.expansionRelatedDefaults</code>.
    // <P>
    // Note, however, that this is a multi-instance component (potentially one per record),
    // so it is created using +link{Class.createAutoChild, createAutoChild()} not
    // +link{Class.addAutoChild, addAutoChild()}, and no default single instance is created by
    // name on the grid.
    //
    // @group expansionField
    // @visibility external
    //<
    expansionRelatedDefaults: {
        _constructor: isc.ListGrid,
        autoDraw: false,
        width: "100%",
        autoFitData: "vertical",
        autoFitMaxRecords: 4
    },

    //> @attr listGrid.expansionEditor (MultiAutoChild DynamicForm : null : RA)
    // Automatically generated +link{class:DynamicForm} for editing the details of a record
    // in its expanded section when
    // +link{type:ExpansionMode, listGrid.expansionMode} is <code>editor</code>.  Note that only
    // those fields which do not already appear in the grid will appear in the expanded section.
    // <P>
    // According to the value of +link{showExpansionEditorSaveButton}, a save button is shown
    // beneath the editor.  You can save the values in the editor by clicking this button
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>listGrid.expansionEditorProperties</code> and
    // <code>listGrid.expansionEditorDefaults</code>.
    // <P>
    // Note, however, that this is a multi-instance component (potentially one per record),
    // so it is created using +link{Class.createAutoChild, createAutoChild()} not
    // +link{Class.addAutoChild, addAutoChild()}, and no default single instance is created by
    // name on the grid.
    //
    // @group expansionField
    // @visibility external
    //<
    expansionEditorDefaults: {
        _constructor: "DynamicForm",
        autoDraw: false,
        numCols: 4,
        colWidths: ["*", "*", "*", "*"],
        width: "100%",
        saveOperationType: "update"
    },

    //> @attr listGrid.expansionEditorShowSaveDialog (boolean : null : IR)
    // When +link{listGrid.canExpandRecords, canExpandRecords} is true and
    // +link{listGrid.expansionMode, expansionMode} is <i>editor</i>, whether a dialog should be
    // displayed when an expanded row is collapsed while it's nested editor has changed values.
    //
    // @group expansionField
    // @visibility external
    //<
    //expansionEditorShowSaveDialog: null,

    //> @attr listGrid.expansionEditorSaveDialogPrompt (string : "You have unsaved changes - do you want to save them now?" : IR)
    // When +link{listGrid.canExpandRecords, canExpandRecords} is true and
    // +link{listGrid.expansionMode, expansionMode} is <i>editor</i>, the prompt to display
    // in a dialog when an expanded row is collapsed while it's nested editor has changed values.
    //
    // @group expansionField,i18nMessages
    // @visibility external
    //<
    expansionEditorSaveDialogPrompt: "You have unsaved changes - do you want to save them now?",

    //> @attr listGrid.expansionEditorCollapseOnSave (Boolean : true : RW)
    // When +link{expansionMode} is <i>editor</i>, should the row be collapsed following a
    // save initiated by the expansion-component's +link{expansionEditorSaveButton, save button}.
    //
    // @group expansionField
    // @visibility external
    //<
    expansionEditorCollapseOnSave: true,

    //> @attr listGrid.showExpansionEditorSaveButton (Boolean : true : RW)
    // When +link{expansionMode} is <i>editor</i>, should a Save button be shown below the
    // the expanded editor?
    // <P>
    // Note that if an expanded-row containing an editor is collapsed while changes are
    // oustanding, changes will be either be automatically updated to the grid, or will first
    // show a confirmation dialog, according to the value of
    // +link{expansionEditorShowSaveDialog}.
    //
    // @group expansionField
    // @visibility external
    //<
    showExpansionEditorSaveButton: true,

    //> @attr listGrid.expansionEditorSaveButton (MultiAutoChild IButton : null : RA)
    // Automatically generated +link{class:IButton} for saving the values in the expanded
    // portion of a ListGrid row.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>listGrid.expansionEditorSaveButtonProperties</code> and
    // <code>listGrid.expansionEditorSaveButtonDefaults</code>.
    // <P>
    // Note, however, that this is a multi-instance component (potentially one per record),
    // so it is created using +link{Class.createAutoChild, createAutoChild()} not
    // +link{Class.addAutoChild, addAutoChild()}, and no default single instance is created by
    // name on the grid.
    //
    // @group expansionField
    // @visibility external
    //<
    expansionEditorSaveButtonDefaults: {
        _constructor: "IButton",
        autoFit: true,
        layoutAlign: "center",
        click : function () {
            if (!this.grid || !this.form) return;
            var grid = this.grid,
                component =
                (this.record && grid._hasEmbeddedComponents(this.record)) ?
                    grid._getEmbeddedComponents(this.record).find("isExpansionComponent", true) :
                    null
            ;

            this.grid.saveAndCollapseRecord(this.form, component, this.record, false,
                this.grid.expansionEditorCollapseOnSave
            );
        }
    },

    //> @attr listGrid.expansionEditorSaveButtonTitle (String : "Save" : RWA)
    // The title for the +link{expansionEditorSaveButton}.
    //
    // @group expansionField,i18nMessages
    // @visibility external
    //<
    expansionEditorSaveButtonTitle: "Save",

    //> @attr listGrid.expansionDetailRelated (MultiAutoChild HLayout : null : RA)
    // Automatically generated +link{class:HLayout} appearing in a record's expanded section
    // when +link{type:ExpansionMode, listGrid.expansionMode} is <code>detailRelated</code>.
    // This component contains two other autoChild components,
    // a +link{class:DetailViewer} for viewing fields from the record which are not already
    // present in the grid and a separate embedded +link{class:ListGrid} for displaying other
    // data related to this record via record.detailDS.  See +link{listGrid.expansionDetails}
    // and +link{listGrid.expansionRelated} for more information.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>listGrid.expansionDetailRelatedProperties</code> and
    // <code>listGrid.expansionDetailRelatedDefaults</code>.
    // <P>
    // Note, however, that this is a multi-instance component (potentially one per record),
    // so it is created using +link{Class.createAutoChild, createAutoChild()} not
    // +link{Class.addAutoChild, addAutoChild()}, and no default single instance is created by
    // name on the grid.
    //
    // @group expansionField
    // @visibility external
    //<
    expansionDetailRelatedDefaults: {
        _constructor: isc.HLayout,
        autoDraw: false,
        width: "100%",
        height: "100%"
    },

    //> @attr listGrid.expansionLayout (MultiAutoChild VLayout : null : RA)
    // Automatically generated +link{class:VLayout} which fills a record's expanded section
    // and contains other builtin +link{ExpansionMode, expansion-components}.  You can also
    // override +link{listGrid.getExpansionComponent, getExpansionComponent()} to provide
    // components of your own specification.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>listGrid.expansionLayoutProperties</code> and
    // <code>listGrid.expansionLayoutDefaults</code>.
    // <P>
    // Note, however, that this is a multi-instance component (potentially one per record),
    // so it is created using +link{Class.createAutoChild, createAutoChild()} not
    // +link{Class.addAutoChild, addAutoChild()}, and no default single instance is created by
    // name on the grid.
    //
    // @group expansionField
    // @visibility external
    //<
    expansionLayoutDefaults: {
        _constructor: isc.VLayout,
        autoDraw: false,
        width: "100%",
        height: 10,
        overflow: "visible"
    },

    //> @method listGrid.getCurrentExpansionComponent()
    // Returns the expansion component derived from +link{listGrid.getExpansionComponent()}
    // currently visible in some record, or null if the specified record is not showing
    // an expansion component.
    //
    // @param record (Integer | ListGridRecord) rowNum or record to get the expansionComponent for
    // @return (Canvas) the currently visible expansion component for the expanded row.
    // @group expansionField
    // @visibility external
    //<
    getCurrentExpansionComponent : function (record) {
        if (isc.isA.Number(record)) record = this.getRecord(record);

        if (!this._hasExpansionComponent(record)) return null;

        // we actually hang 'isExpansionComponent' onto the layout containing the generated
        // expansion component so return its (only) member.
        var component = this._hasEmbeddedComponents(record) ?
                    this._getEmbeddedComponents(record).find("isExpansionComponent", true) : null;
        if (component) {
            return component.members[0];
        }
        return null;
    },

    //> @method listGrid.getExpansionComponent()
    // Gets the embedded-component to show as a given record's expansionComponent.  This
    // component is then housed in +link{listGrid.expansionLayout, a VLayout} and embedded
    // into a record's row.
    // <P>
    // By default, this method returns one of a set of built-in components, according to the
    // value of +link{type:ExpansionMode, listGrid.expansionMode}.  You can override this method
    // to return any component you wish to provide as an expansionComponent.
    // <P>
    // As long as the record is expanded, this component may be retrieved via a call to
    // +link{getCurrentExpansionComponent()}.
    // <P>
    // When an expanded record is collapsed, the component is disassociated from the record and
    // may or may not be automatically destroyed.  By default, built-in components
    // will be destroyed on unembed.  Custom expansion components, created via an override of
    // getExpansionComponents(), may not be auto-destroyed, according to the
    // +link{ExpansionComponentPoolingMode, pooling mode} being used.
    //
    // @param record (ListGridRecord) record to get the expansionComponent for
    // @return (Canvas | Canvas Properties) the component to embed
    // @group expansionField
    // @visibility external
    //<
    getExpansionComponent : function (record) {
        return this._getStockEmbeddedComponent(record, true, false, this.getRecordIndex(record), 0);
    },

    //> @attr listGridRecord.backgroundComponent (Canvas : null : IR)
    // Has no effect unless +link{listGrid.showBackgroundComponents} is <code>true</code>.
    // <P>
    // Canvas created and embedded in the body behind a given record.   When set, either as
    // a Canvas or Canvas Properties, will be constructed if necessary, combined with the
    // autoChild properties specified for +link{listGrid.backgroundComponent} and displayed
    // behind this record in the page's z-order, meaning
    // it will only be visible if the cell styling is transparent.
    // @group rowEffects
    // @visibility external
    //<

// Expando Rows - Record implementation

    //> @attr listGrid.detailDS (String : null : IRWA)
    // If +link{canExpandRecords} is true and +link{type:ExpansionMode,listGrid.expansionMode}
    // is <code>"related"</code>, this property specifies the dataSource for the
    // related records grid to be shown embedded in expanded records.
    // <P>
    // This property may also be specified on a per-record basis - see
    // +link{recordDetailDSProperty}
    // @visibility external
    //<

    //> @attr listGrid.recordDetailDSProperty (String : "detailDS" : IRWA)
    // The name of the ListGridRecord property that specifies the DataSource to use when
    // +link{type:ExpansionMode, listGrid.expansionMode} is "related".  The default is
    // +link{ListGridRecord.detailDS}. Note that you can set the +link{detailDS} at the
    // grid level instead if the same dataSource is to be used for all records.
    //
    // @visibility external
    //<
    recordDetailDSProperty: "detailDS",

    //> @attr listGridRecord.detailDS (DataSource : null : IRWA)
    // The default value of +link{listGrid.recordDetailDSProperty}.
    //
    // @visibility external
    //<

    //>    @method    listGrid.getRelatedDataSource()
    // Returns the +link{class:DataSource} containing data related to the passed record.  Used
    // when +link{ListGrid.canExpandRecords} is true and +link{ExpansionMode} is "related". The
    // default implementation returns the DataSource specified in
    // +link{listGridRecord.detailDS} if set, otherwise +link{listGrid.detailDS}.
    //
    // @param   record  (ListGridRecord)    The record to get the Related dataSource for.
    // @return (DataSource) The related DataSource for the "record" param
    //
    // @visibility external
    //<
    getRelatedDataSource : function (record) {
        return isc.DS.getDataSource(record[this.recordDetailDSProperty]) ||
                    isc.DS.get(this.detailDS);
    }

    //> @attr listGrid.childExpansionMode (ExpansionMode : null : IRWA)
    // For +link{ExpansionMode, expansionModes} that show another grid or tree, what the
    // child's expansionMode should be.
    // <P>Default value <code>null</code> means no further expansion.
    //
    // @group expansionField
    // @visibility external
    //<

    //> @attr listGrid.expansionCanEdit (Boolean : null : IRWA)
    // For +link{ExpansionMode, expansionModes} that show another grid or tree, is that
    // component editable?
    // <P>The default value for this property is <code>false</code>.
    //
    // @group expansionField
    // @visibility external
    //<

    //> @attr listGrid.filterLocalData (boolean : null : IRW)
    // If this grid has its +link{listGrid.setData(),data property} set to a static array
    // of records, should +link{listGrid.filterData()} filter this array?
    // If this property is set to true, the local data array will be filtered according to
    // the specified criteria, and the results displayed. If false, a normal databound
    // fetch will occur, retriving records that match the specified criteria from this
    // component's +link{listGrid.dataSource}.
    // <P>
    // If this property is not explicitly set, default behavior will filter against the
    // dataSource unless the grid has a specified +link{DataPath, dataPath}, in which case
    // filtering will occur locally.
    // <P>
    // Note that a specified +link{listGrid.dataSource} is required for filtering to occur
    // even when filtering a local data array.
    // @visibility external
    //<

    //> @attr listGrid.recordDropAppearance (RecordDropAppearance : isc.ListGrid.BETWEEN : [IRW])
    // If +link{canAcceptDroppedRecords} is true for this listGrid, this property governs
    // whether the user can drop between, or over records within the grid.
    // This controls what +link{type:RecordDropPosition} is passed to the +link{recordDrop()}
    // event handler.
    //
    // @visibility external
    //<
    , recordDropAppearance: isc.ListGrid.BETWEEN
});



isc.ListGrid.addMethods({

// warnOnReusedFields -- will log a warning at init time if this.fields is set to an array of
// field objects which are already displayed as fields in another grid.
// Internal - we only disable this for a couple of special cases where we really do want to share
// field objects across grids, like the record editor
warnOnReusedFields:true,

//> @method ListGrid.initWidget() (A)
// Initialize the canvas and call listGrid.setData().
// @param [all arguments] (object) objects with properties to override from default
//<
initWidget : function () {


    this.Super("initWidget", arguments);

    // Disable `resizeFieldsInRealTime' in mobile browsers.
    if (isc.Browser.isMobile) this.resizeFieldsInRealTime = false;

    // call the setter on 'showRecordComponents()' - this disables the
    // drawAllMaxCells logic for the grid.
    if (this.showRecordComponents) {
        delete this.showRecordComponents;
        this.setShowRecordComponents(true);
    }

    // we want to avoid a fields array object getting re-used across multiple ListGrids.
    // This can happen if the developer does something like assigning a fields object to the
    // instance prototype.
    // To handle this we have another attribute 'defaultFields' which may be set on the instance
    // prototype and differs only from initializing with a fields block in that array will be
    // shallow cloned and assigned to this.fields rather than sharing the same object across
    // ListGrid instances

    if (this.fields == null && this.defaultFields != null) {
        this.fields = isc.shallowClone(this.defaultFields);
        //this.logWarn("fields picked up from this.defaultFields -- now set to:" + this.fields);
    } else if (this.warnOnReusedFields && this.fields != null) {
        if (this.fields._initializedFieldsArray) {
            this.logWarn("ListGrid initialized with this.fields attribute set to an array " +
                "which is already being displayed in another ListGrid instance. To reuse " +
                "standard field configuration across multiple ListGrids, use " +
                "listGrid.defaultFields rather than assigning directly to listGrid.fields.");

        // Also catch the case where the developer re-uses individual fields within separate
        // arrays across grids
        } else {

            var dupedFields;
            if (isc.isAn.Array(this.fields)) {
                dupedFields = this.fields.findIndex("_initializedFieldObject", true) != -1;
            } else if (isc.isAn.Object(this.fields)) {
                for (var fieldName in this.fields) {
                    var field = this.fields[fieldName];
                    if (field && field._initializedFieldObject) {
                        dupedFields = true;
                        break;
                    }
                }
            }
            if (dupedFields) {
                 this.logWarn("ListGrid initialized with this.fields attribute set to an array " +
                "containing fields which are already being displayed in another ListGrid " +
                "instance. To reuse standard field configuration across multiple ListGrids, use " +
                "listGrid.defaultFields rather than assigning directly to listGrid.fields.");
            }
        }

        // Set a flag on the array and the individual field objects - this allows us catch the case
        // where a developer reuses a standard fields array across multiple grids rather than using
        // the defaultFields attribute
        this.fields._initializedFieldsArray = true;
        if (isc.isAn.Array(this.fields)) {
            this.fields.setProperty("_initializedFieldObject", true);
        } else if (isc.isAn.Object(this.fields)) {
            for (var fieldName in this.fields) {
                var field = this.fields[fieldName];
                if (field) {
                    field._initializedFieldObject = true;
                }
            }
        }
    }

    if (this.canEditNew) this.listEndEditAction = this.rowEndEditAction = "next";

    if (this.alwaysShowEditors) {
        this.editByCell = false;

        // disable selection

        this.selectionType = "none";
        this.selectOnEdit = false;

        if (this.canGroup != false) {
            this.logInfo("grouping functionality is not supported when alwaysShowEditors is true." +
                    " Explicitly disabling this.canGroup", "inactiveEditorHTML");
            this.canGroup = false;
        }
        if (this.modalEditing) {
            this.logInfo("modalEditing is not supported when alwaysShowEditors is true." +
                " Explicitly setting this.modalEditing to fales", "inactiveEditorHTML");
            this.modalEditing = false;
        }

        // enforce editEvent:"click" - this means that if the user clicks on a cell which
        // isn't showing an inactive editor (for example where showEditorPlaceholder is true),
        // we still start editing that row/cell
        this.editEvent = "click";

        // If canEdit is unset, and we have no fields explicitly marked as canEdit:true,
        // flip the default to true.
        // This gives us the most intuitive behavior - if the developer specifies per-field
        // editablility we'll respect it, otherwise we'll default to canEdit true
        if (!this.isEditable()) {
            this.logInfo("alwaysShowEditors has been set for this grid but canEdit is unset and " +
              "no fields are explicitly marked as editable. Defaulting this.canEdit to true. " +
              "Note that to avoid this override developers can explicitly specify canEdit " +
              "at the grid or field level", "inactiveEditorHTML");
            this.canEdit = true;
        }

    }

    // if we have variable record heights and virtualScrolling is unset, switch it on

    if (this.canExpandRecords ||
        (this.fixedRecordHeights == false && this.virtualScrolling == null))
    {
        // the _specifiedFixedRecordHeights flag is used by shouldShowAllColumns - we know
        // that even though a row may exceed its cell height this isn't due to any cell's content
        // in the row, so we don't need to render out every column to get correctly sized rows.
        if (this.fixedRecordHeights) this._specifiedFixedRecordHeights = this.fixedRecordHeights;
        this.fixedRecordHeights = false;
        this.virtualScrolling = true;
    }

    // disable canAddFormulaField if the required component isn't present
    if (this.canAddFormulaFields && isc.FormulaBuilder == null) {
        this.logInfo("Required modules for adding formula fields not present - setting " +
                    "canAddFormulaFields to false.");
        this.canAddFormulaFields = false;
    }
    if (this.canAddSummaryFields && isc.SummaryBuilder == null) {
        this.logInfo("Required modules for adding summary fields not present - setting " +
                    "canAddSummaryFields to false.");
        this.canAddSummaryFields = false;
    }

    // default to canSelectCells mode if we're using rowSpan-sensitive styling
    if (this.useRowSpanStyling && this.canSelectCells == null) this.canSelectCells = true;

    // default cell-level roll overs to the value of canSelectCells
    if (this.useCellRollOvers == null) this.useCellRollOvers = this.canSelectCells;

    // force loading rows to contain at least &nbsp; otherwise row height may be reported as less
    // than the actual height.
    if (this.loadingMessage == null || this.loadingMessage == isc.emptyString)
        this.loadingMessage = "&nbsp;";

    // default our overflow to "visible" if autoFitData is set
    if (this.autoFitData != null) {
        this._specifiedOverflow = this.overflow;
        this.setOverflow("visible");
    }

    // default our groupStartOpen to "all" if canCollapseGroup is false
    if (this.canCollapseGroup == false) this.groupStartOpen = "all";

    // store off the initial sortDirection - we'll revert to this when unsorting
    this._baseSortDirection = this.sortDirection ? "ascending" : "descending";

    // initialize the data object, setting it to an empty array if it hasn't been defined
    this.setData(this.data ? null : this.getDefaultData());

    // set up selectionType dynamic default
    this.setSelectionAppearance(this.selectionAppearance, true);

    this._setUpDragProperties();

    if(!this.canResizeFields) this.canAutoFitFields=false;

    // For back-compat, if headerButtonProperties.wrap is set, and wrapHeaderTitles is
    // null, respect headerButtonProperties.wrap.
    if (this.wrapHeaderTitles == null) {
        var wrap = this.headerButtonProperties != null ? this.headerButtonProperties.wrap : null;
        if (wrap == null) {
            wrap = this.headerButtonDefaults != null ? this.headerButtonDefaults.wrap : null;
        }
        if (wrap == null) wrap = false;
        this.wrapHeaderTitles = wrap;
    }
},

getDefaultData : function () { return []; },

_storeDragProperties : function () {
    if (!this._initialDragProperties) {
        var iDP = this._initialDragProperties = {};
        iDP.canDrag = this.canDrag;
        iDP.canDrop = this.canDrop;
        iDP.canAcceptDrop = this.canAcceptDrop;
    }
},
_restoreDragProperties : function () {
    var iDP = this._initialDragProperties;
    if (iDP) {
        this.canDrag = iDP.canDrag;
        this.canDrop = iDP.canDrop;
        this.canAcceptDrop = iDP.canAcceptDrop;
    }
},
_setUpDragProperties : function () {
    if (!this._initialDragProperties) {
        // store the original dragProperties
        this._storeDragProperties();
    }
    // set up our specific drag-and-drop properties

    this.canDrag = !this.canDragSelectText &&
                    (this.canDrag || this.canDragRecordsOut || this.canReorderRecords ||
                    this.canDragSelect);
    this.canDrop = (this.canDrop || this.canDragRecordsOut || this.canReorderRecords);
    this.canAcceptDrop = (this.canAcceptDrop || this.canAcceptDroppedRecords ||
                    this.canReorderRecords);

},

//> @method listGrid.setCanReorderRecords()
// Setter for the +link{listGrid.canReorderRecords} attribute.
// @param canReorderRecords (boolean) new value for <code>this.canReorderRecords</code>
// @visibility external
//<
setCanReorderRecords : function (canReorderRecords) {
    if (canReorderRecords != this.canReorderRecords) {
        this.canReorderRecords = canReorderRecords;
        this._restoreDragProperties();
        this._setUpDragProperties();
        this.redraw();
    }
},

getEmptyMessage : function () {
    if (isc.ResultSet && isc.isA.ResultSet(this.data) && !this.data.lengthIsKnown()) {
        if (isc.Offline && isc.Offline.isOffline()) {
            return this.offlineMessage;
        }
        return this.loadingDataMessage == null ? "&nbsp;" :
            this.loadingDataMessage.evalDynamicString(this, {
                loadingImage: this.imgHTML(isc.Canvas.loadingImageSrc,
                                           isc.Canvas.loadingImageSize,
                                           isc.Canvas.loadingImageSize)
            });
    }
    if (this.isOffline()) {
        return this.offlineMessage;
    }
    return this.emptyMessage == null ? "&nbsp;" : this.emptyMessage.evalDynamicString(this, {
        loadingImage: this.imgHTML(isc.Canvas.loadingImageSrc,
                                   isc.Canvas.loadingImageSize,
                                   isc.Canvas.loadingImageSize)
    });
},

isEmpty : function () {
    if (!this.data) return true;

    // treat having no fields as being empty so we don't attempt to write out and manipulate
    // an empty table
    if (!this.fields || this.fields.length == 0) return true;


    if (isc.ResultSet && isc.isA.ResultSet(this.data)) {

        if (this.data.isPaged()) {
            if (!this.data.isEmpty()) return false;


            var editRows = this.getAllEditRows();
            if (editRows && editRows.length > 0) {
                for (var i = 0; i < editRows.length; i++) {
                    if (editRows[i] >= 0) return false;
                }
            }
            return true;
        } else {
            // If our length is not known we must be in the process of loading, so return the
            // loading message.
            return (!this.data.lengthIsKnown() || this.getTotalRows() == 0);
        }
    } else {
        return (this.getTotalRows() == 0);
    }
},

//>    @attr listGrid.preserveEditsOnSetData (boolean : null : IRWA)
// By default any edit values in an editable ListGrid are dropped when 'setData()' is called,
// as previous edits are almost certainly obsoleted by the new data-set.
// This flag allows the developer to suppress this default behavior.
// @visibility internal
// @group data
//<
// Leave this internal for now - no known use cases for it, but seems like something that
// could come up.
//preserveEditsOnSetData : null,

//>    @method    listGrid.setData()
// Provides a new data set to the ListGrid after the grid has been created or drawn.
// The ListGrid will redraw to show the new data automatically.
// @param newData (List of ListGridRecord) data to show in the list
// @group data
// @visibility external
//<
setData : function (newData) {
    // if the current data and the newData are the same, bail
    //    (this also handles the case that both are null)
    if (this.data == newData) return;

    if (!this.preserveEditsOnSetData) this.discardAllEdits();

    // drop "lastHiliteRow" -no sense in hanging onto it
    this.clearLastHilite();

    // if we are currently pointing to data, stop observing it
    if (this.data) {
        this._ignoreData(this.data);
        // if the data was autoCreated, destroy it to clean up RS<->DS links
        if (this.data._autoCreated && isc.isA.Function(this.data.destroy))
            this.data.destroy();
    }

    // if newData was passed in, remember it
    if (newData) this.data = newData;

    // if data is not set, bail
    if (!this.data) return;

    // create a new selection if we don't have one or if we receive new data

    if (!this._isGrouped && !this.isGrouped &&
        (!this.selection || (this.data != this.selection.data)))
    {
        this.createSelectionModel();
    }

    // observe the data so we will update automatically when it changes
    this._observeData(this.data);

    var sortSpecifiers;
    // if data is a resultSet, and it already has both data and sortSpecifiers, apply the
    // sortSpecifiers from the ResultSet to this grid, replacing those already on the grid
    if (isc.isA.ResultSet(this.data) && this.data.getSort() && this.data.lengthIsKnown()) {
        sortSpecifiers = this.data.getSort();
    } else {
        sortSpecifiers = this.getSort();
    }
    if (sortSpecifiers) this.setSort(sortSpecifiers);

    // Call this._remapEditRows() if we're hanging onto edit values
    // as we know they're now out of date.

    if (this.preserveEditsOnSetData) this._remapEditRows();

    // if we can regroup, do so.

    this.regroup(true);

    // Don't redraw / refresh grid summary - this happens explicitly later.
    this.calculateRecordSummaries(null, null, null, true, true);

    // if this.alwaysShowEditors is set, and we have data, and we're not currently showing
    // editors, show them now.

    var fetching = isc.ResultSet && isc.isA.ResultSet(this.data) && !this.data.lengthIsKnown();
    if (!fetching && this._alwaysShowEditors() && !this._editorShowing) {
        this.startEditing(null,null,true,null,true);
    }

    if (this.hilites) this.applyHilites();


    if (isc.ResultSet && isc.isA.ResultSet(this.data) &&
        !(this.data.allRows && this.data.neverDropCache) &&
        this.body && this.body.overflow == "visible")
    {
        this.body.showAllRows = false;
    }

    // clear out the current expanded row count, along with the current expanded record,
    // if there is one, since the expansionComponents have been dropped, along with the
    // records they were in
    this._expandedRecordCount=0;
    if (!this.canExpandMultipleRecords && this._currentExpandedRecord)
        delete this._currentExpandedRecord;

    if (this._lastStoredSelectedState) {
        this.setSelectedState(this._lastStoredSelectedState);
        delete this._lastStoredSelectedState;
    }

    // if any fields are marked as autoFitWidth, recalculate their sizes

    if (this.isEmpty() && ((isc.ResultSet && isc.isA.ResultSet(this.data)) ||
                    (isc.ResultTree && isc.isA.ResultTree(this.data))) )
    {
        this._updateFieldWidthsOnDataArrived = true;
    } else {
    this.updateFieldWidthsForAutoFitValue("setData called.");
    }

    // mark us as dirty so we'll be redrawn if necessary
    this._markBodyForRedraw("setData");

    this.updateBodyCanFocusForData();

},


// Determine which field to expand when autoFitFieldWidths is true and the
// fields don't take up the full set of space available.
// If +link{listGrid.autoFitExpandField} is explicitly set use it.
// Otherwise, basically we want to find the first field that will not look odd to expand
// Implementation:
// - if a field is not of type text, ignore (don't want to expand images, dates, booleans, numbers)
// - If field has showValueIconsOnly set, ignore (even though it has text content it shouldn't
//   expand beyond the value icons width)
// - field.length:
//      - if all fields have field.length specified, expand the largest one
//      - if no fields have this property set, just expand the first text field
//      - if *some* fields have field.length set - the length may indicate a small field
//        (say 2 characters) which we don't want to expand.
//        Expand fields with no specified length in preference to those with
//        a small specified length.
autoFitExpandLengthThreshold : 10,
getAutoFitExpandField : function () {
    if (!this.autoFitFieldsFillViewport) return null;

    if (this.autoFitExpandField != null) {
        var field = this.getField(this.autoFitExpandField);
        // We don't support auto-expanding a frozen field at this time

        if (field != null && this.fields && this.fields.contains(field) &&
            (!this.frozenFields || !this.frozenFields.contains(field)))
        {
            return field;
        }
    }
    var fields = [], lengthFields = [];

    if (this.fields) {
        for (var i = 0; i < this.fields.length; i++) {
            var field = this.fields[i];
            if (!field.showValueIconOnly &&
               (field.type == null ||
                (isc.SimpleType.inheritsFrom(field.type, "text") &&
                 !isc.SimpleType.inheritsFrom(field.type, "image"))
               ))
            {
                if (!this._suppressedFrozenFields && field.frozen) continue;
                fields.add(field);
                if (fields[i] != null && fields[i].length != null) {
                   lengthFields.add(fields[i]);
                }

            }
        }
    }

    if (lengthFields.length > 0) {
        lengthFields.sortByProperty("length", Array.DESCENDING);
        if (lengthFields.last().length >= this.autoFitExpandLengthThreshold ||
            lengthFields.length == fields.length)
        {
            return lengthFields[0];
        }
    }
    if (fields.length > 0) {
        var i = 0;
            field = fields[i]
        // Note: This conditional contains cases where we should *not* auto expand the field
        while (field != null &&
                // Respect explicitly specified pixel width
                ((field.width != null && field.width != "*") ||
                // Explicit length less than the min "expand" number of characters
                 (field.length != null &&
                    field.length < this.autoFitExpandLengthThreshold))
              )
        {
            i++;
            field = fields[i];
        }
        return field;
    }

    // Note that this could still return null - no text fields etc.
    return null;
},

// updateFieldWidthsForAutoFitValue()
// If this.autoFitWidthApproach means we size to
// column data and any fields are marked as autoFitWidth:true,
// this method will set the _fieldWidthsDirty flag on the gridRenderer, causing
// _updateFieldWidths() to be called on the next body redraw,
// Calling code should call this before causing a body redraw.
updateFieldWidthsForAutoFitValue : function (reason) {
    if (!this.body || this.body._fieldWidthsDirty) return;

    var fields = this.fields || [];
    for (var i = 0; i < fields.length; i++) {
        var shouldAutoFit = this.shouldAutoFitField(fields[i]);
        if (!shouldAutoFit) continue;
        var approach = this.getAutoFitWidthApproach(fields[i]);

        if (approach == "value" || approach == "both") {
            this.fields._appliedInitialAutoFitWidth = false;
            this.body._fieldWidthsDirty = "Updating field widths for field auto-fit" +
                                      (reason ? (":" + reason) : ".");
            break;
        }
    }
},

invalidateCache : function () {
    // make sure that header checkbox is unchecked after refreshing cache
    if (this.getCheckboxFieldPosition() != -1) {
        this.deselectAllRecords();
        this._setCheckboxHeaderState(false);
    }

    // if we have a stored drawArea, delete it now so that the
    // redraw recalculates it and reapplies recordComponents
    if (this.body && this.body._oldDrawArea) delete this.body._oldDrawArea;

    return this.Super("invalidateCache", arguments);
},

// use this rather than field.canSort to handle canSortClientOnly fields
_canSort : function (field) {
    if (!isc.isAn.Object(field)) {
        field = this.getSpecifiedField(field);
    }
    if (field == null) return false;

    var canSort = (field.canSort == false) ? false : this.canSort == false ? false : true;
    canSort = canSort && this._canSortData(field);

    return canSort;
},

_canSortData : function (fieldName) {

    var field = isc.isAn.Object(fieldName) ? fieldName : this.getSpecifiedField(fieldName);
    if (field == null) return false;

    if (isc.isAn.Array(this.data)) return true;
    if (field.canSortClientOnly == true) {
        if (isc.isA.ResultSet(this.data)) {
            if (!this.data.lengthIsKnown() || !this.data.canSortOnClient()) {
                return false;
            }
        }
    }

    return true;
},

// helper to determine whether this grid can be sorted on multiple fields - returns false if
// either canMultiSort or canSort are false, or if the data itself doesn't support multiSort
_canMultiSort : function () {
    var canMultiSort = (this.canMultiSort != false) && this.canSort && this._canMultiSortData();
    return canMultiSort;
},

// helper to determine whether this grid's data supports sorting on multiple fields
_canMultiSortData : function () {
    var canMultiSort = this.canMultiSort;

    // if data doesn't support setSort (resultSet and Array do), disable multiSort
    if (!this.data.setSort) {
        canMultiSort = false;
    } else if (this.getDataSource() && canMultiSort != false) {
        canMultiSort = this.getDataSource().canMultiSort && this.canSort;
    }

    return canMultiSort;
},

// helper method to get an appropriate sortDirection for a field
_getFieldSortDirection : function (field) {
    var field = isc.isAn.Object(field) ? field : this.getSpecifiedField(field),
        direction;

    var fieldDir = field ? field.sortDirection : null;
    if (fieldDir != null) {
        direction = Array.shouldSortAscending(fieldDir) ? "ascending" : "descending";
    } else if (this.sortDirection != null) {
        direction = Array.shouldSortAscending(this.sortDirection) ? "ascending" : "descending";
    } else {
        direction = Array.shouldSortAscending(this._baseSortDirection) ? "ascending" : "descending";
    }

    return direction;
},

// Override createSelectionModel, from DataBoundComponent, to set the body's selection object
// with our selection object. Our body GridRenderer will then observe selection.setSelected.
createSelectionModel : function () {
    this.invokeSuper(isc.ListGrid, "createSelectionModel", arguments);
    if (isc.isA.Canvas(this.body)) {
        this.body.setSelection(this.selection);
        if (this.frozenBody) {
            this.frozenBody.setSelection(this.selection);
            // create separate selection objects for body/frozenBody
            this._separateDependentSelections();
        }
    }
},

destroySelectionModel : function () {
    if (this.body) this.body.clearSelection();
    if (this.frozenBody) this.frozenBody.clearSelection();
    return this.Super("destroySelectionModel", arguments);
},

// Method tripped to reselect a selected record that was updated in the DataSource
performReselectOnUpdate : function (updatedRecord) {
    if (this.reselectOnUpdateNotifications == "none") {
        this.suppressSelectionChanged = true;
    }

    this.selection.select(updatedRecord);
    if (this.reselectOnUpdatedNotifications == "selectionUpdated") {
        this.fireSelectionUpdated();
    }
    delete this.suppressSelectionChanged;
},

//>    @method    listGrid.setSelectionType()    [A]
// Changes selectionType on the fly.
// @param    selectionType (SelectionStyle)    New selection style.
// @visibility external
//<
setSelectionType : function (selectionType, ignoreCheckbox) {
    // NOTE: this is sufficient because the Selection object dynamically inspects this property
    // on it's grid
    this.selectionType = selectionType;
    if (this.body) this.body.selectionType = selectionType;
},

//>    @method    listGrid.setSelectionAppearance()
// Changes selectionAppearance on the fly.
// @param    selectionAppearance (String)    new selection appearance
// @visibility external
//<
setSelectionAppearance : function (selectionAppearance, isInit) {
    if (this.selectionAppearance == selectionAppearance && !isInit) return;

    if (selectionAppearance == "checkbox" && this.canSelectCells) {
        this.logWarn("ignoring selectionAppearance:'checkbox' since canSelectCells is set");
        selectionAppearance = "rowStyle";
    }

    this.selectionAppearance = selectionAppearance;

    // at initialization time, if selectionType wasn't explicitly set, default selectionType to
    // "simple" for "checkbox" appearance, otherwise "multiple"
    if (isInit && this.selectionType == null && !this.canSelectCells) {
        this.selectionType = (selectionAppearance == "checkbox" ? isc.Selection.SIMPLE :
                                                                  isc.Selection.MULTIPLE);
    }

    // If this.completeFields is set (IE setFields has run already), we need to either add or
    // remove the checkboxField
    // Call setFields() with a duplicate of the current set of fields, less the
    // checkboxField (if there was one)
    // This will create a new checkboxField if necessary otherwise just clear the existing one
    if (this.completeFields != null) {
        var completeFields = [];
        for (var i = 0; i < this.completeFields.length; i++) {
            var field = this.completeFields[i];
            if (this.isCheckboxField(field)) continue;
            completeFields.add(field);
        }
        this.setFields(completeFields);
    }
},

//> @method listGrid.setBodyOverflow()  ([A])
// Update the +link{listGrid.bodyOverflow, bodyOverflow} for this listGrid.
// @param overflow (Overflow) new overflow setting for the body
// @visibility external
//<
setBodyOverflow : function (newOverflow) {
    this.bodyOverflow = newOverflow;
    if (this.body) this.body.setOverflow(this.bodyOverflow);
},

//> @method listGrid.setBodyStyleName()
// Update the +link{listGrid.bodyStyleName,bodyStyleName} for this listGrid.
// @param styleName (CSSStyleName) new body style name
// @visibility external
//<
setBodyStyleName : function (styleName) {
    this.bodyStyleName = styleName;
    if (this.body && (!this.alternateBodyStyleName || !this.alternateRecordStyles)) {
        this.body.setStyleName(styleName);
    }
},

//> @method listGrid.setAlternateBodyStyleName()
// Update the +link{listGrid.alternateBodyStyleName,alternateBodyStyleName} for this listGrid.
// @param styleName (CSSStyleName) new body style name when showing alternateRecordStyles
// @visibility external
//<
setAlternateBodyStyleName : function (styleName) {
    this.alternateBodyStyleName = styleName;
    if (this.body && this.alternateRecordStyles) {
        // if passed 'null', reset to this.bodyStyleName
        this.body.setStyleName(styleName || this.bodyStyleName);
    }
},

//> @method listGrid.setAlternateRecordStyles()
// Setter for +link{listGrid.alternateRecordStyles}
// @param alternateStyles (boolean) New value for <code>this.alternateRecordStyles</code>
// @visibility external
//<
setAlternateRecordStyles : function (alternateStyles) {
    if (this.alternateRecordStyles == alternateStyles) return;
    this.alternateRecordStyles = alternateStyles;

    if (this.body && (this.alternateBodyStyleName != null)) {
        if (alternateStyles) this.body.setStyleName(this.alternateBodyStyleName);
        else this.body.setStyleName(this.bodyStyleName);
    }

},

// Override hasInherentHeight / width: If we're autoFitting to our data, advertise inherent height
// This means that a layout will not expand us to fit the available space.

hasInherentHeight : function (a,b,c,d) {
    if (this.inherentHeight != null) return this.inherentHeight;
    if (this.autoFitData == isc.Canvas.VERTICAL || this.autoFitData == isc.Canvas.BOTH) {
        return true;
    }
    return this.invokeSuper(isc.ListGrid, "hasInherentHeight", a,b,c,d);
},

hasInherentWidth : function (a,b,c,d) {
    if (this.inherentWidth != null) return this.inherentWidth;
    if (this.autoFitData == isc.Canvas.HORIZONTAL || this.autoFitData == isc.Canvas.BOTH) {
        return true;
    }
    return this.invokeSuper(isc.ListGrid, "hasInherentWidth", a,b,c,d);
},

//> @method listGrid.setAutoFitData()
// Setter for +link{listGrid.autoFitData}.
// @param autoFitData (string) One of <code>"vertical"</code>, <code>"horizontal"</code>
//  or <code>"both"</code>. To disable auto fit behavior, pass in <code>null</code>.
// @group autoFitData
// @visibility external
//<
setAutoFitData : function (autoFitData) {
    this.autoFitData = autoFitData;

    if (this._autoDerivedCanFreeze && (autoFitData == "both" || autoFitData == "horizontal"

                                       || autoFitData == "vertical")) {
        delete this._autoDerivedCanFreeze;
        delete this.canFreezeFields;
    }
    if (autoFitData == null && this._specifiedOverflow) {
        this.setOverflow(this._specifiedOverflow);
    } else if (this.overflow != "visible") {
        this._specifiedOverflow = this.overflow;
        this.setOverflow("visible");
    }
    if (this.body) {
        this.body.autoFitData = this.autoFitData;
        this.body.adjustOverflow();
    }
},

//> @method listGrid.setAutoFitExtraRecords()
// Setter for +link{listGrid.autoFitExtraRecords}.
// @param extraRecords (integer) Number of extra rows beyond the data-size we'll expand to
// accommodate if +link{listGrid.autoFitData,auto fit} is enabled vertically.
// @group autoFitData
// @visibility external
//<
setAutoFitExtraRecords : function (extraRecords) {
    this.autoFitExtraRecords = extraRecords;
    if (this.body) {
        this.body.autoFitExtraRecords = extraRecords;
        this.body.adjustOverflow();
    }
},


//> @method listGrid.setAutoFitMaxRecords()
// Setter for +link{listGrid.autoFitMaxRecords}.
// @param maxRecords (integer) Maximum number of rows we'll expand to accommodate if
// +link{listGrid.autoFitData,auto fit} is enabled vertically.
// @group autoFitData
// @visibility external
//<
setAutoFitMaxRecords : function (maxRecords) {
    this.autoFitMaxRecords = maxRecords;
    if (this.body) {
        this.body.autoFitMaxRecords = maxRecords;
        this.body.adjustOverflow();
        if (this.frozenBody) {
            this.frozenBody.autoFitMaxRecords = maxRecords;
            this.frozenBody.redraw();
        }
    }
},

//> @method listGrid.setAutoFitMaxHeight()
// Setter for +link{listGrid.autoFitMaxHeight}.
// @param height (integer) Maximum height in px we'll expand to accommodate if
// +link{listGrid.autoFitData,auto fit} is enabled vertically.
// @group autoFitData
// @visibility external
//<
setAutoFitMaxHeight : function (height) {
    this.autoFitMaxHeight = height;
    if (this.body) {
        this.body.adjustOverflow();
        if (this.frozenBody) this.frozenBody.redraw();
    }
},
getAutoFitMaxBodyHeight : function () {
    if (this.autoFitMaxHeight == null) return null;
    var offset = this.getVBorderPad();
    if (this.showHeader) offset += this.getHeaderHeight();
    if (this.showFilterEditor) offset += this.filterEditorHeight;
    if (this.showGridSummary) offset += this.summaryRowHeight;
    return this.autoFitMaxHeight - offset;

},

// When auto-fitting vertically, specified height for the grid acts as a minimum
getAutoFitMinBodyHeight : function () {
    var minHeight = this.getHeight(),
        offset = this.getVBorderPad();
    if (this.showHeader) offset += this.getHeaderHeight();
    if (this.showFilterEditor) offset += this.filterEditorHeight;
    if (this.showGridSummary) offset += this.summaryRowHeight;

    return (minHeight - offset);
},

//> @method listGrid.setAutoFitMaxColumns()
// Setter for +link{listGrid.autoFitMaxColumns}.
// @param maxColumns (integer) Maximum number of fields we'll expand to accommodate if
// +link{listGrid.autoFitData,auto fit} is enabled horizontally.
// @group autoFitData
// @visibility external
//<
setAutoFitMaxColumns : function (maxColumns) {
    this.autoFitMaxColumns = maxColumns;
    if (this.body) {
        this.body.autoFitMaxColumns = maxColumns;
        this.body.adjustOverflow();
    }
},

//> @method listGrid.setAutoFitMaxWidth()
// Setter for +link{listGrid.autoFitMaxWidth}.
// @param width (integer) Width in px we'll expand to accommodate if
// +link{listGrid.autoFitData,auto fit} is enabled horizontally.
// @group autoFitData
// @visibility external
//<
setAutoFitMaxWidth : function (width) {
    this.autoFitMaxWidth = width;
    if (this.body) {
        this.body.autoFitMaxWidth = width;
        this.body.adjustOverflow();
    }
},

// --------------------------------
// AutoFitFields

//> @method listGrid.autoFitField()
// Programmatically cause a field to auto-fit horizontally to it's contents or title.
// <P>
// Does not establish permanent auto-fitting - use +link{listGrid.setAutoFitWidth()} to do so.
// @param fieldName (string)
// @return (int) new width in pixels
//
// @group autoFitFields
// @visibility external
//<
autoFitField : function (fieldName, scrollIntoView) {
    var field = this.getField(fieldName),
        fieldNum = this.getFieldNum(field);
    // avoid attempting to autofit hidden fields, or fields where
    // autoFitWidth is already set (should just happen dynamically!)
    if (field == null || fieldNum == -1) return;

    var widths = this.getFieldAutoFitWidths([field]);

    if (widths == null || widths[0] == null) return;

    // Set a flag noting that we're auto-fitting this field.
    // We'll use this to suppress normal reflow in reaction to the 'headerButtonResized' notification.
    // Treat this somewhat similarly to a drag-resize.
    this._autoFittingField = field;

    var width = widths[0];
    // resize the field (as if the user drag-resized to the auto-fit size)
    var dontStoreWidth = this.shouldAutoFitField(field);
    if (this._fieldWidths && width != this._fieldWidths[fieldNum]) {
        this.resizeField(fieldName, width, !dontStoreWidth);
    } else if (!dontStoreWidth) field.width = width;
    if (this._fieldWidths && dontStoreWidth) field._calculatedAutoFitWidth = this._fieldWidths[fieldNum];

    if (scrollIntoView) this.scrollToColumn(fieldNum, "left");

    if (this.headerMenuButton != null && this.headerMenuButton.masterIndex == fieldNum) {
        this.headerMenuButton.hide();
    }

    delete this._autoFittingField;

    return width;
},

//> @method listGrid.autoFitFields()
// Perform a one-time horizontal auto-fit of the fields passed. Fields will be sized
// to match their contents or title (as specified in +link{listGrid.autoFitWidthApproach})
// Does not establish permanent auto-fitting - use +link{listGrid.setAutoFitWidth()} to do so.
// @param [fields] (Array of ListGridField) Array of fields to auto fit. If this parameter
//  is not passed, autoFitting will occur on all visible fields.
//
// @group autoFitFields
// @visibility external
//<
autoFitFields : function (fields) {


    if (this.body) this.body._fieldWidthsDirty = true;
    this.autoSizeHeaderSpans = false;
    if (fields == null) fields = this.fields;

    this._autoFittingFields = true;
    var finalFields = [];
    for (var i = 0; i < fields.length; i++) {
        var field = this.getField(fields[i]);
        if (field != null) {
            delete fields[i].width;
            delete fields[i]._calculatedAutoFitWidth;
            finalFields[finalFields.length] = field;
        }
    }
    // we've removed any hidden fields here.
    fields = finalFields;

    var widths = this.getFieldAutoFitWidths(fields, this.autoFitFieldsFillViewport);

    var fieldNums = [], resizeWidths = [], storeWidths = [];
    for (var i = 0; i < fields.length; i++) {

        if (widths[i] == null) continue;
        var width = widths[i],
            field = fields[i],
            fieldNum = this.getFieldNum(fields[i]),
            fieldName = fields[i].name;
        // resize the field (as if the user drag-resized to the auto-fit size)
        var dontStoreWidth = this.shouldAutoFitField(field);
        if (this._fieldWidths && width != this._fieldWidths[fieldNum]) {

            fieldNums[fieldNums.length] = fieldNum;
            resizeWidths[resizeWidths.length] = width;
            storeWidths[storeWidths.length] = !dontStoreWidth;

        } else if (!dontStoreWidth) field.width = width;
    }
    this._resizeFields(fieldNums, resizeWidths, storeWidths);

    for (var i = 0; i < fieldNums.length; i ++) {
        if (!storeWidths[i]) {
            var fieldNum = fieldNums[i];
            this.getField(fieldNum)._calculatedAutoFitWidth = this._fieldWidths[fieldNum];
        }
    }

    this._autoFittingFields = false;

    // we suppressed placeEmbeddedComponents() during the field resizes, so
    // run it now to ensure any embedded components are correctly positioned and sized.
    if (this.frozenBody) this.frozenBody._placeEmbeddedComponents()
    if (this.body) this.body._placeEmbeddedComponents();
    // Normally we do this in resizeField() but we don't want a bunch of unnecessary
    // reflows so we delay until resize of fields is complete
    if (this.header && this.autoFitHeaderHeights) {
        this.dropCachedHeaderButtonHeights();
        this._updateHeaderHeight();
    }

    this.getFieldWidths();
    // This will adjust header heights if necessary to account for differently wrapped
    // content
    if (this.header) this.header._sizeSpans();
},


shouldAutoFitField : function (field) {
    if (field.autoFitWidth != null) return field.autoFitWidth;
    return this.autoFitFieldWidths;
},

// This method is called directly as part of resizeFields() and acts as a minimum, disallowing
// resizing below the min field width.

getMinFieldWidth : function (field, ignoreFieldWidth) {
    var minWidth = this.minFieldWidth || 1;
    if (!ignoreFieldWidth) {

        var fieldWidth = field.width;
        if (isc.isA.Number(fieldWidth)) {
            minWidth = Math.max(minWidth, field.width);
        }
    }
    var fieldName = field.name;
    // If we have embeddedComponents for the field we should treat that as a minimum
    if (!this.clipRecordComponents &&
            (this._columnComponentsMap && this._columnComponentsMap[fieldName]))
    {
        var componentMaxWidth = this._getFieldComponentMaxWidth(fieldName);
        if (componentMaxWidth > minWidth) {
            minWidth = componentMaxWidth;
        }
    }

    return minWidth;
},
clipRecordComponents: true,

_$within:"within",
_getFieldComponentMaxWidth : function (fieldName) {
    var field = this.getField(fieldName);
    if (field == null) return 0;

    if (field._maxComponentWidth != null) return field._maxComponentWidth;

    var components = this._columnComponentsMap[fieldName];
    var maxWidth = 0;
    for (var compID in components) {
        var component = window[compID];
//         this.logWarn("For field:" + fieldName + " contemplating:" + component +
//             " which has width:" + component.getVisibleWidth());
        // skip "expand" components - these are always sized to fit the field.
        if (component && (component.embeddedPosition == this._$within) &&
            (component.getVisibleWidth() > maxWidth) )
        {
            maxWidth = component.getVisibleWidth();
        }
    }
    field._maxComponentWidth = maxWidth;
    return maxWidth;
},

// This method fires when we have embedded components which may overflow the
// specified field width horizontally, so we need to expand to fit.
_fieldComponentWidthsChanged : function (fieldName, newWidth) {
    var field = this.getField(fieldName);
    if (field) {
        // If we happen to know we just increased the size, we can update
        // the max-width without having to query sizes of all drawn components.
        // Otherwise just clear the cached value and we'll lazily recalculate.
        if (newWidth) field._maxComponentWidth = newWidth;
        else delete field._maxComponentWidth;

        // Use "fireOnPause" to actually resize the field to fit the components on a delay

        if (this._staleComponentWidthFields == null) {
            this._staleComponentWidthFields = {};
        }
        this._staleComponentWidthFields[fieldName] = true;
        this.fireOnPause(
            "checkFieldComponentOverflow",
            {target:this,methodName:"_checkFieldComponentOverflow"}, 0
        );
    }

},
_checkFieldComponentOverflow : function () {
    if (this.fields == null) return;



    var currentWidths = this._fieldWidths || this.getFieldWidths(),
        overflowedFields = [],
        newWidths = [],
        storeWidths = [],
        overflowed = false;
    for (var i = 0; i < this.fields.length; i++) {
        var name = this.fields[i].name;
        if (!this._staleComponentWidthFields[name]) continue;
        this._staleComponentWidthFields[name] = null;

        var minWidth = this.getMinFieldWidth(this.fields[i]);
        if (minWidth > currentWidths[i]) {
            overflowedFields.add(i);
            newWidths.add(minWidth);
            // We don't want to store any of these widths. If autoFitFieldWidths
            // is true, if the component is hidden etc we want to be able to resize
            // smaller again if _updateFieldWidthsForAutoFitValue() gets re-run.
            storeWidths.add(false);
        }
    }
    // Embedded components in some cell overflowed the available width - resize
    // the field to accomodate it.

    if (overflowedFields.length > 0) {
        this._resizeFields(overflowedFields, newWidths, storeWidths);
    }
},

getFieldAutoFitWidths : function (fields, fillViewport) {
    if (this.body == null || fields == null) return;
    if (!isc.isA.Array(fields)) {
        fields = [fields]
    }
    var widths = [],
        colNums = [],
        headers = [],
        bodyFields = [];

    for (var i = 0; i < fields.length; i++) {
        var minWidth = this.getMinFieldWidth(fields[i], true),
            field = fields[i],
            colNum = this.getColNum(field),
            approach = this.getAutoFitWidthApproach(field, true),
            checkHeader = approach != "value",
            checkBody = approach != "title";
        // we'll use the colNums outside this loop when we pick up the
        // body col widths.
        colNums[i] = colNum;
        widths[i] = minWidth;

        if (checkHeader) {
            var header = this.getFieldHeaderButton(colNum);

            if (header != null) headers[i] = header;
        }

        if (checkBody) {
            bodyFields.add(field);
        }

    }

    if (headers.length > 0) {
        //var startTime = isc.timeStamp();
        var headerWidths = this.getAutoFitTitleWidths(headers);
        //this.logWarn("Time to get header title widths:" + (isc.timeStamp() - startTime));
        for (var i = 0; i < headerWidths.length; i++) {


            if (headerWidths[i] == null) continue;
            widths[i] = Math.max(widths[i],headerWidths[i]);
        }
    }

    // for efficiency, pass all the body fields to getAutoFitValueWidths() at once.

    if (bodyFields.length > 0) {
        var bodyColWidthArr = this.getAutoFitValueWidths(bodyFields);
        for (var i = 0; i < fields.length; i++) {
            if (colNums[i] != null) {
                var colNum = colNums[i],
                    bodyColWidth = bodyColWidthArr ? bodyColWidthArr[colNum] : null;
                if (bodyColWidth != null) {
                    if (widths[i] == null || bodyColWidth > widths[i]) {
                        widths[i] = bodyColWidth;
                    }
                }
            }
        }
    }

    // If passed the fillViewport flag, and there is extra space, expand the
    // appropriate field by the delta such that we do indeed fill the viewport.

    if (fillViewport) {
        var frozenSize = 0,
            unfrozenSize = 0,
            unfrozenSpace,
            expandField = this.getAutoFitExpandField(),
            expandFieldIndex = this.fields.indexOf(expandField),
            requiresExpansion = colNums.contains(expandFieldIndex);

        if (requiresExpansion) {

            for (var i = 0; i < this.fields.length; i++) {
                var colWidth,
                    index = colNums.indexOf(i);
                if (index >= 0) {
                    colWidth = widths[index];
                } else {
                    colWidth = this._fieldWidths[i];
                    if (colWidth == null) {

                        requiresExpansion = false;
                        break;
                    }
                }
                if (this.fields[i].frozen) {
                    frozenSize += colWidth;
                } else {
                    unfrozenSize += colWidth;
                }
            }

            if (requiresExpansion) {

                unfrozenSpace = this.getAvailableFieldWidth(true) - frozenSize;
                requiresExpansion = unfrozenSize < unfrozenSpace;
            }
            if (requiresExpansion) {
                var index = colNums.indexOf(expandFieldIndex);

                widths[index] += unfrozenSpace - unfrozenSize;
            }
        }
    }

    return widths;
},

getAutoFitTitleWidths : function (headers) {

    var testHTML = "",
        missingHeaders = {};
    for (var i = 0; i < headers.length; i ++) {

        if (headers[i] == null) {
            missingHeaders[i] = true;
            continue;
        }
        var header = headers[i],
            titleStyle = header.titleStyle;
        if (titleStyle == null) titleStyle = header.getStateName();


        testHTML += header._getSizeTestHTML(this.getHeaderButtonTitle(header, false));
    }
    if (this.logIsDebugEnabled("autoFitFieldWidths")) {
        this.logDebug("Auto Fit Title test HTML:\n" + testHTML, "autoFitFieldWidths");
    }

    if (isc.ListGrid.headerWidthsTester == null) {
        isc.ListGrid.headerWidthsTester = isc.Canvas.create({
            top:-100,
            autoDraw:true,
            overflow:"hidden",
            contents:testHTML,

            ariaState: {
                hidden: true
            }
        });

    } else {
        isc.ListGrid.headerWidthsTester.setContents(testHTML);
    }
    if (isc.ListGrid.headerWidthsTester.isDirty()) {
        isc.ListGrid.headerWidthsTester.redraw();
    }

    var handle = isc.ListGrid.headerWidthsTester.getHandle(),
        childNodes = handle.childNodes;

    var widths = [];
    for (var i = 0, j=0; i < headers.length; i ++) {
        if (missingHeaders[i]) {
            widths[i] = null;
            continue;
        }

        var titleDiv = childNodes[j];

        if (isc.Browser.isChrome || isc.Browser.isMoz || isc.Browser.isIE9) {
            var titleBCR = titleDiv.getBoundingClientRect();
            if (!isc.Browser.isIE || isc.Browser.isIE10) {
                widths[i] = Math.ceil(titleBCR.width);
            } else {
                widths[i] = (titleBCR.width + 1) << 0;
            }
        } else {
            widths[i] = Math.ceil(titleDiv.offsetWidth);
        }
        j++;
    }

    if (widths.length != headers.length) {
        this.logWarn("getAutoFitTitleWidths(): Mismatch between " +
            "headers passed in and calculated widths", "autoFitFieldWidths");
    }
    return widths;
},

//> @method listGrid.setAutoFitWidth()
// Setter for +link{listGridField.autoFitWidth}. Enables or disables dynamic autoFitWidth behavior
// on the specified field. Note if the field is currently autoFitWidth:true, and this method is
// disabling autoFit, the field will not be resized by default - if you wish to resize to an
// explicit width, use +link{listGrid.resizeField()}.
//
// @param fieldName (string) field to auto-fit
// @param autoFit (boolean) Should autoFitWidth be enabled or disabled?
// @group autoFitFields
// @visibility external
//<
setAutoFitWidth : function (fieldName, autoFit) {
    var field = this.getField(fieldName);
    if (field == null) return;
    if (field.autoFitWidth == autoFit) return;

    field.autoFitWidth = autoFit;

    if (autoFit) this.autoFitField(field);
    // If we're turning off auto-fit, don't bother to resize
},

//> @method listGrid.setAutoFitFieldWidths()
// Setter for +link{listGrid.autoFitFieldWidths}. Modifies the default auto-fit-width behavior
// for fields in this grid. Note that this may be overridden at the field level via
// +link{listGridField.autoFitWidth}.
// @param autoFit (boolean) New value for autoFitFieldWidths
// @param [dontResetWidths] (boolean) If autoFitFieldWidths was true, and is being set to false,
//  should fields be resized to their originally specified size?
//  Pass in this parameter to suppress this behavior.
// @visibility external
//<
setAutoFitFieldWidths : function (autoFit, dontResetWidths) {
    if (autoFit == this.autoFitFieldWidths) return;
    this.autoFitFieldWidths = autoFit
    if (autoFit) {
        this._updateFieldWidths("autoFitFieldWidths enabled");
    } else if (!dontResetWidths) {
        // If we're showing a header, we use it to handle converting specified
        // field widths into real sizes - running updateHeader will rebuild it and
        // perform this initial calculation for us.
        if (this.showHeader && this.headerHeight > 0) this.updateHeader();
        // Clear the flag indicating we've run through auto-fit logic and re-run
        // _updateFieldWidths() to revert the fields to specified sizes.
        this.fields._appliedInitialAutoFitWidth = false;
        this._updateFieldWidths("autoFitFieldWidths disabled");
    }
},

//> @method listGrid.setAutoFitWidthApproach()
// Setter for the +link{listGrid.autoFitWidthApproach}.
// @param approach (AutoFitWidthApproach) new AutoFitWidth approach
// @visibility external
//<
setAutoFitWidthApproach : function (approach) {
    if (this.autoFitWidthApproach == approach) return;
    this.autoFitWidthApproach = approach;

    // If we're showing a header, we use it to handle converting specified
    // field widths into real sizes - running updateHeader will rebuild it and
    // perform this initial calculation for us.

    if (this.showHeader && this.headerHeight > 0) this.updateHeader();
    // Clear the flag indicating we've run through auto-fit logic and re-run
    // _updateFieldWidths() to revert the fields to specified sizes.
    this.fields._appliedInitialAutoFitWidth = false;
    this._updateFieldWidths("autoFitFieldWidthApproach changed");
},

// mark the body for redraw, or if the body doesn't exist, the widget as a whole
_markBodyForRedraw : function (reason) {
    if (this.bodies) {
        this.bodies.map("markForRedraw", reason);
    } else if (this.body) {
        this.markForRedraw(reason);
    }
},

redraw : function (a, b, c, d) {
    if (this.body) {

        if (this.body._scrollbarChangeDuringAnimation) {
            this._updateFieldWidths("scrollbar change during animation");
            delete this.body._scrollbarChangeDuringAnimation;
        }
    }

    this.invokeSuper(isc.ListGrid, "redraw", a, b, c, d);

},

//>    @method listGrid._observeData() (A)
//        observe methods on the data so we redraw automatically when data changes
//        called automatically by setData
//    @param    data    (object)        new data to be observed
//<
_observeData : function (data) {
    // redraw if the data changed
    var isRS = isc.ResultSet && isc.isA.ResultSet(data);
    if (!this.isObserving(data, "dataChanged")) {
        if (isRS) {
            this.observe(data, "dataChanged", function (operationType, originalRecord, rowNum, updateData, filterChanged, dataFromCache) {
                this.dataChanged(operationType, originalRecord, rowNum, updateData, filterChanged, dataFromCache);
            });
        } else {
            this.observe(data, "dataChanged", function () {
                this.dataChanged();
            });
        }
    }

    if (!this.isObserving(data, "dataArrived")) {
        if (isRS) {
            this.observe(data, "dataArrived", function (startRow, endRow, dataFromCache) {
                this._dataArrived(startRow, endRow, dataFromCache);
            });
        } else if (isc.ResultTree && isc.isA.ResultTree(data)) {
            this.observe(data, "dataArrived", function (parentNode) {
                this._dataArrived(parentNode);
            });
        }
    }

    // Note - we must check for data being a tree as if it is not defined, the inherited
    // ListGrid.init() code will set it to an empty array, in which case this observation will
    // fail.
    if (isc.isA.Tree(data)) {
        // update view in response to folders opening / closing
        this.observe(data, "changeDataVisibility", function (node, newState) {
            this._folderToggleObservation(node, newState);
        });
    }
},
//> @method listGrid.groupTreeChanged()
// Callback fired from group tree +link{listGrid.groupTree} dataChanged().
// <p>
// Handles remapping edit rows and forcing a redraw if necessary.
// @group grouping
//<
// The groupTree may be a ResultTree or a Tree object.
// When a listGrid is grouped we still observe dataChanged on the underlying data object and react
// to it by updating or rebuilding the groupTree as required, and marking for redraw etc.
// Therefore if this method is fired from standard dataChanged() we typically need to take no
// action.
// Note that we explicitly disable databound cache-synch for the ResultTree and instead manage
// updating the ResultTree cache directly as part of ListGrid.dataChanged. This is appropriate since
// The ResultTree code for cache sync is organized around node ids and parent ids whereas the
// groupTree is a dynamic grouping based on records have the same values for a field.
//
// This will actually fire in response to listGrid sort or direct manipulation of the groupTree
//
// Note that this method is only fired when an existing groupTree changes - not when regroup()
// is run, creating a new groupTree.
_$dataChanged:"dataChanged",
groupTreeChanged : function () {
    // If the groupTree was updated from underlying data change, no need to
    // redraw etc (already handled in dataChanged())
    if (this._handlingDataChanged) return;

    if (!this._savingEdits && !this.suppressEditRowRemap) this._remapEditRows();
    var lastRow = this.getTotalRows()-1;
    if (this.body) {

        if (this.body.lastOverRow > lastRow) delete this.body.lastOverRow;
        if (this.body.lastMouseOverRow > lastRow) delete this.body.lastMouseOverRow;
        if (this.body._lastHiliteRow > lastRow) delete this.body._lastHiliteRow;
    }
    if (this._lastRecordClicked > lastRow) delete this._lastRecordClicked;

    if (this.hilites) this.applyHilites();
    if (!this._suppressRedrawOnDataChanged) this.redrawForDataChanged();

},
//>    @method listGrid._observeGroupData() (A)
//      observe methods on the group tree object, so that changes to the group layout
//      can be detected
//    @param    data    (object)        new group tree to be observed
//  @visibility internal
//<
_observeGroupData : function (data) {
    // redraw if the data changed
    this.observe(data, "dataChanged", function () {
        this.groupTreeChanged();
    });
    this.observe(data, "changeDataVisibility", function (node, newState) {
        this._folderToggleObservation(node, newState);
    });
},

// METHODS MOVED FROM TREEGRID
// The following methods were moved from treegrid to allow the listgrid to support the tree
// as a data model for grouping. They will continue to be doc'd on treegrid for now

// Helper method - fired when folders open/close within the tree
_folderToggleObservation : function (node, newState) {
    // If we're loaded, reapply hilites, but pass in the flag to suppress marking for redraw
    // We need to do this since we won't get a dataChanged notification from the
    // children loading.
    // No need to redraw here since we will redraw (below) in response to the folder toggling.
    if (node != null && this.hilites && newState) {
        var loadState = this.data.getLoadState(node);
        if (loadState == isc.Tree.LOADED || loadState == isc.Tree.LOADED_PARTIAL_CHILDREN) {
            this.applyHilites(true);
        }
    }
    //>Animation
    // During animated folder open/close we suppress redraw in response to the folder toggling

    if (this._suppressFolderToggleRedraw) {
        this._remapEditRows();

        // Re run auto fit logic expand our cols to fit the revealed content

        this.updateFieldWidthsForAutoFitValue("Folder Toggled");

        return;
    }

    // Cut short any currently running animated folder open / close
    // Just call finishAnimation - this will no op if no animation is running

    if (this.body) this.body.finishRowAnimation();
    //<Animation

    // Length changes so we need to remap edit rows.
    this._remapEditRows();

    // Re run auto fit logic expand our cols to fit the revealed content
    this.updateFieldWidthsForAutoFitValue("Folder Toggled");

    // redraw to display the updated folder
    this._markBodyForRedraw('folderToggled');
},

//> @method treeGrid.toggleFolder()   ([])
//          Opens the folder specified by node if it's closed, and closes it if it's open.
//          TreeGrid will redraw if there's a change in the folder's open/closed state.
//
//      @visibility external
//      @param  node        (TreeNode)      node to toggle
//<
toggleFolder : function (node) {
    if (this.data.isOpen(node)) {
        this.closeFolder(node);
    } else {
        this.openFolder(node);

        if (this.frozenBody) this.frozenBody.markForRedraw();
    }
},

//> @method treeGrid.openFolder() ([A])
// Opens a folder.
// <p>
// Executed when a folder node receives a 'doubleClick' event.
// <smartclient>If you override this method, the single parameter passed will be
// a reference to the relevant folder node in the tree's data.</smartclient>
// <p>
// See the ListGrid Widget Class for inherited recordClick and recordDoubleClick events.
//
// @param   node        (TreeNode)      node to open
// @see closeFolder()
// @see folderOpened()
// @see class:ListGrid
// @visibility external
//<
openFolder : function (node) {
    // CALLBACK API:  available variables:  "node"
    // Convert a string callback to a function
    if (this.folderOpened != null) {
        this.convertToMethod("folderOpened");
        if (this.folderOpened(node) == false) return false;
    }


    if (this.animateFolders) {
        this.animateOpen(node);
    } else {
        this.data.openFolder(node);
    }

},

//> @method treeGrid.animateOpen()
// Animates a folder opening to display its children (which grow into view).
// Automatically triggered from <code>treeGrid.folderOpen()</code> if
// <code>this.animateFolders</code> is true.
// @group animation
// @param folder (node) node to open
// @visibility animation_advanced
//<
animateOpen : function (folder) {
    var data = this.data;
    if (data.isOpen(folder)) return;

    // Open the data, but don't redraw with the new data visible (we'll handle redrawing
    // when the animation completes).
    this._suppressFolderToggleRedraw = true;
    data.openFolder(folder);
    delete this._suppressFolderToggleRedraw;

    // parent may be null if we're looking at the root node
    var parent = data.getParent(folder);
    if (parent && !data.isOpen(parent)) return;

    var loadState = data.getLoadState(folder);
    if (loadState != isc.Tree.LOADED && loadState != isc.Tree.LOADED_PARTIAL_CHILDREN) {
        //this.logWarn("animation for LOD folder");
        // wait for dataChanged() to fire
        this._pendingFolderAnim = folder;
        return;
    }

    this._startFolderAnimation(folder);
},

//> @method treeGrid.closeFolder()
// Closes a folder.
//
// @param   node        (TreeNode)      node to close
// @see openFolder()
// @see folderClosed()
// @visibility external
//<
closeFolder : function (node) {
    // CALLBACK API:  available variables:  "node"
    // Convert a string callback to a function
    if (this.folderClosed != null) {
        this.convertToMethod("folderClosed");
        if (this.folderClosed(node) == false) return false;
    }

    // cancel editing of any nodes under this one
    if (this.getEditRow() != null) {
        var editRecord = this.getRecord(this.getEditRow());
        if (this.data.isDescendantOf(editRecord, node)) this.endEditing();
    }
    // now tell the data to close the folder
    if (this.shouldAnimateFolder(node))
        this.animateClose(node);
    else
        this.data.closeFolder(node);
},

//> @method treeGrid.animateClose()
// Animates a folder closing to hide its children (which shrink out of view).
// Automatically triggered from <code>treeGrid.folderOpen()</code> if
// <code>this.animateFolders</code> is true.
// @param folder (node) node to open
// @group animation
// @visibility animation_advanced
//<
animateClose : function (folder) {
    if (!this.data.isOpen(folder)) return;

    var parent = this.data.getParent(folder);
    if (parent && !this.data.isOpen(parent)) {
        return this.closeFolder(folder);
    }

    var data = this.data,
        folderIndex = data.indexOf(folder),
        numChildren = data.getOpenList(folder).getLength()-1;



    this.startRowAnimation( false,
                            folderIndex+1,
                            folderIndex + numChildren + 1,
                            {target:this, methodName:"redraw", args:["close folder animation complete"]},
                            this.animateFolderSpeed,
                            this.animateFolderTime,
                            this.animateFolderEffect,
                            true
                          );
    var wasSuppressed = this._suppressFolderToggleRedraw;
    this._suppressFolderToggleRedraw = true;
    this.data.closeFolder(folder);
    this._suppressFolderToggleRedraw = wasSuppressed;

    if (this.body && this.body._delayedRowAnimation != null) {
        this.body._openFolder = folder;
    }
    if (this.frozenBody && this.frozenBody._delayedRowAnimation != null) {
        this.frozenBody._openFolder = folder;
    }

},

_startFolderAnimation : function (folder) {
    // At this point we know we have all the children for the folder loaded - verify
    // that we actually should animate the folder into view - if we have too many children
    // we may not want to -- in this case just redraw.
    if (!this.shouldAnimateFolder(folder)) {
        this.markForRedraw();
        return;
    }
    var data = this.data,
        folderIndex = data.indexOf(folder),
        numChildren = data.getOpenList(folder).getLength()-1;

    // don't try to animate empty folders
    if (folderIndex < 0 || numChildren <= 0) return;

    this.startRowAnimation( true,
                            folderIndex+1,
                            (folderIndex + numChildren+1),
                            {target:(this.bodyLayout || this.body), methodName:"redraw", args:["open folder animation complete"]},
                            this.animateFolderSpeed,
                            this.animateFolderTime,
                            this.animateFolderEffect,
                            true
                          );
},

// Used to store open folder state in the groupTree
// (Also used by TreeGrid.getOpenState())

_addNodeToOpenState : function (tree, node, openState, isGroupTree) {
    if (!tree.isOpen(node) || !tree.isLoaded(node)) return false;
    var children = tree.getFolders(node),
        hasOpenChildren = false;
    if (children != null) {
        for (var i = 0; i < children.length; i++) {
            hasOpenChildren = this._addNodeToOpenState(tree, children[i], openState, isGroupTree)
                              || hasOpenChildren;
        }
    }
    if (isGroupTree) {
        var folderInfo = {};
        folderInfo[node.groupName] = node.groupValue;
        openState.add(folderInfo);
    } else {
        openState[openState.length] = tree.getPath(node);
    }
    return true;
},

// END METHODS MOVED FROM TREEGRID

_getDataChangedRecord : function (originalRecord, rowNum) {
    if (rowNum == null) return null;

    var currData = this.data;
    if (this.data.isGroupedOutput && this.originalData) currData = this.originalData;

    // updatedRecord does not exist (was deleted)
    var newRecord =  currData.get(rowNum);
    if (newRecord == null) return null;

    if (originalRecord != null) {
        var pks = this.dataSource != null ? this.getDataSource().getPrimaryKeyFieldNames() : [];
        for (var i = 0; i < pks.length; ++i) {
            // if primary keys differ, the record was deleted via filtering
            if (originalRecord[pks[i]] != newRecord[pks[i]]) {
                return null;
            }
        }
    }
    return newRecord;
},


dataChanged : function (type, originalRecord, rowNum, updateData, filterChanged, dataFromCache) {
    if (isc._traceMarkers) arguments.__this = this;

    // set a flag so we know we're handling dataChanged
    // This prevents us from causing unnecessary additional redraws from dataChanged on the
    // groupTree if we're currently grouped by any field(s)
    this._handlingDataChanged = true;

    // DataChanged fires in some cases where we don't want to reset autoFieldWidths
    // For example, scrolling through a paged resultset where the columns resizing on scroll
    // would be ugly.
    // Use ResultSet parameters to test for cases to react to:
    // - crud operations which will affect the data displayed
    // - filter changing (including invalidateCache calls)
    // Note that we have equivalent logic inline in removeData, cellChanged to handle
    // non-databound grids' data being changed through standard grid APIs
    var resetAutoFitWidths = filterChanged || type == "add" || type == "remove";
    if (!resetAutoFitWidths && type == "replace") {
        if (originalRecord == null || rowNum == null) resetAutoFitWidths = true;
        else {
            var updatedRecord = this.getOriginalData().get(rowNum);
            if (updatedRecord == null) resetAutoFitWidths = true;
            else {
                for (var i = 0; i < this.fields.length; i++) {
                    if (this.shouldAutoFitField(this.fields[i])) {
                        var fieldName = this.getFieldName(this.fields[i]);
                        if (updatedRecord[fieldName] != updateData[fieldName]) {
                            resetAutoFitWidths = true;
                            break;
                        }
                    }
                }
            }
        }
    }

    var updatedGroupSummaries = false;

    // if a change was made to the groupBy field of a record, regroup
    var groupByFields = (this._groupByFields || this.getGroupByFields());
    if (groupByFields != null && !this._markForRegroup) {
        var markForRegroup = false;

        // fully regroup for add/remove, or for an update where dataChanged is not passed the
        // originalRecord to figure out if the groupField was changed
        if (type == "add" || type == "remove" || type == "replace" ||
            (type == "update" && (originalRecord == null || rowNum == null)))
        {
            markForRegroup = true;
        // otherwise for other updates see if we need to regroup
        } else if (type == "update") {

            var updatedRecord = this._getDataChangedRecord(originalRecord, rowNum, type);
            // regroup if the updatedRecord is not valid
            if (updatedRecord == null) markForRegroup = true;

            // regroup if group field values differ
            if (!markForRegroup) {
                this.calculateRecordSummaries(updatedRecord, null, false, false);


                var pks = this.getDataSource().getPrimaryKeyFieldNames(),
                    keyCriteria = {};
                for (var i = 0; i < pks.length; i++) {
                    keyCriteria[pks[i]] = updatedRecord[pks[i]];
                }

                // NOTE: In general, we explicitly do not support composite primaryKeys with
                // Trees and TreeGrids.  However, ListGrid grouping is implemented by use of
                // a Tree - "this.data" in the below call is a Tree, not the List or ResultSet
                // holding the grid's real data.  Fortunately, Tree supports a criteria search
                // of its nodes, so we make use of that with a criteria object that happens to
                // contain only primary keys
                var nodes = this.data.findAll(keyCriteria);

                var fieldNames = this.getDataSource().getFieldNames();
                fieldNames.addList(this.getCacheableFields().getProperty("name"));

                for (var i = 0; i < nodes.length; i ++) {
                    var node = nodes[i];

                    // if we're grouped by the pk field we are likely to have header nodes
                    // with the same value for this field - only apply updates to the
                    // actual data node we care about!
                    if (node._isGroup) continue;

                    for (var ii = 0; ii < groupByFields.length; ii++) {
                        var undef, fieldName = groupByFields[ii];
                        if (originalRecord[fieldName] === undef) continue;

                        var field = this.getUnderlyingField(fieldName);
                        if (!this.fieldValuesAreEqual(field, originalRecord[fieldName],
                                                              updatedRecord[fieldName]))
                        {
                            this._remapEditRows();

                            this._incrementalRegroup(updatedRecord, node, updateData);
                            updatedGroupSummaries = true;
                            this._ignoreRegroup = true;
                            break;
                        }
                    }

                    // apply all modified fields to the node.

                    if (node == updatedRecord) continue;

                    for (var ii = 0; ii < fieldNames.length; ii++) {
                        nodes[i][fieldNames[ii]] = updatedRecord[fieldNames[ii]];
                    }
                    nodes[i]["_cache_" + this.ID] = updatedRecord["_cache_" + this.ID];
                }

                // refresh the group(s) that contain the updated record
                if (!updatedGroupSummaries) {
                    updatedGroupSummaries = true;
                    this.refreshGroupSummary(node);
                }
            }
        } else if (filterChanged) {
            markForRegroup = true;
        }

        if (markForRegroup) {
            this._setMarkForRegroup(true, false, false, false, true, groupByFields);
        }
    }

    // if a change was made to a value of a sort field, the result set should update a partial
    // cache, and the result set has a partial cache (allMatchingRowsCached() is false), then
    // unsort the grid. This is done because when a result set has updatePartialCache:true and
    // a partial cache, "updated rows will remain in their current position.  No attempt will
    // be made to sort them into a new position even if the sort field was updated."
    var sortSpecifiers = this._sortSpecifiers;
    if (sortSpecifiers != null && sortSpecifiers.length > 0) {
        var origData = this.getOriginalData();
        if (isc.isA.ResultSet(origData) &&
            ((origData.shouldUpdatePartialCache() && !origData.allMatchingRowsCached()) ||
             !origData.canSortOnClient()) &&
            (type == "replace" || type == "update"))
        {
            var unsort = false;
            if (originalRecord == null || rowNum == null) {
                unsort = true;
            } else {
                var updatedRecord = this._getDataChangedRecord(originalRecord, rowNum, type);
                if (updatedRecord == null) unsort = true;
                else {
                    for (var i = 0, numSortSpecifiers = sortSpecifiers.length; i < numSortSpecifiers; ++i) {
                        var sortSpecifier = sortSpecifiers[i],
                            property = sortSpecifier.property;

                        if (originalRecord[property] !== undefined &&
                            !this.fieldValuesAreEqual(property,
                                                      originalRecord[property],
                                                      updatedRecord[property]))
                        {
                            unsort = true;
                        }
                    }
                }
            }

            if (unsort) this.unsort();
        }
    }


    if (this._markForRegroup && !this._savingEdits &&
        // Skip attempting to regroup / reset selection if our resultSet is in mid-fetch
        (!isc.isA.ResultSet(this.data) || this.data.lengthIsKnown()))
    {
        this._lastStoredSelectedState = this.getSelectedState(true);
        this.regroup();
    } else if (this._asyncRegroupInProgress) {
        delete this._asyncRegroupInProgress;
        isc.Timer.clear(this._regroupTimerEvent);
        delete this._regroupTimerEvent;
        isc.clearPrompt();
    }

    //>Animation
    // Call finishRowAnimation - will kill any show/hide row animations.
    // These animations assume the data remains constant for the duration
    // of the animation.
    // (No-ops if appropriate)
    if (this.body) this.body.finishRowAnimation();
    //<Animation

    // Call _remapEditRows() to ensure that editValues are associated with the (possibly
    // modified) rowNumbers using pointers between record primary key and edit values

    if (!this._savingEdits && !this.suppressEditRowRemap) this._remapEditRows();

    // re-associate embeddedComponents with records which were not previously present in the
    // cache but are now - set grid._shouldRetainEmbeddedComponents to false to have components
    // removed when their associated records are no longer in the cache.
    if (!isc.isA.ResultSet(this.data) || this.data.lengthIsKnown()) {
        this._remapEmbeddedComponents();
    }

    // if this.alwaysShowEditors is set, and we have data, and we're not currently showing
    // editors, show them now.
    // This handles us getting new data (from a fetch for example)
    if (this._alwaysShowEditors() && !this._editorShowing) {
        this.startEditing(null,null,true,null,true);
    }


    var lastRow = this.getTotalRows()-1;
    if (this.body) {
        if (this.body.lastOverRow > lastRow) delete this.body.lastOverRow;
        if (this.body.lastMouseOverRow > lastRow) delete this.body.lastMouseOverRow;
        if (this.body._lastHiliteRow > lastRow) delete this.body._lastHiliteRow;
    }
    if (this._lastRecordClicked > lastRow) delete this._lastRecordClicked;


    var changedRecords = null;
    if (type != "update" && type != "remove") {
        changedRecords = this._getDataChangedRecord(originalRecord, rowNum);
    }

    if (updatedGroupSummaries) {

        if (this.summaryRow && this.showGridSummary) this.summaryRow._recalculateSummaries();
    } else {
        this.calculateRecordSummaries(changedRecords, null, null, true, true, type == "remove",
                                      type != null && type != "update");
    }

    if (this.hilites) this.applyHilites();


    if (!this._suppressRedrawOnDataChanged) {
        // recalculate autoFitWidth field widths to fit the new data
        if (resetAutoFitWidths) this.updateFieldWidthsForAutoFitValue(this._$dataChanged);

        this.redrawForDataChanged();
    }

    // Note - a regroup may require a re-selection of the prior-to-regroup selection
    // range.
    // We rely on code in the _regroupFinish flow to handle resetting selection

    // clear the _handlingDataChanged flag
    delete this._handlingDataChanged;

    this.updateBodyCanFocusForData();

},


_setMarkForRegroup : function (
        markForRegroup,
        calledFromGroupBy, calledFromRegroup, calledFromClearGroupBy, calledFromDataChanged,
        fields, groupByFields)
{

    var markForRegroup0 = this._markForRegroup;

    if (calledFromGroupBy) {

        this._groupByCompleteFieldsBeforeRegroup = fields;
        this._isGrouped = true;
        this._groupByFields = groupByFields;
    } else if (calledFromRegroup) {
        if (markForRegroup0 && !markForRegroup) {

            this._groupByCompleteFieldsAfterRegroup = this._groupByCompleteFieldsBeforeRegroup;
            delete this._groupByCompleteFieldsBeforeRegroup;
            delete this._isGrouped;
            delete this._groupByFields;
        }
    } else if (calledFromClearGroupBy) {


        // clean up temporary grouping state

        delete this._isGrouped;
        delete this._groupByFields;

        delete this._groupByCompleteFieldsBeforeRegroup;
        delete this._groupByCompleteFieldsAfterRegroup;
    }



    this._markForRegroup = markForRegroup;
},

redrawForDataChanged : function () {
    if (this.frozenBody) this.frozenBody._suppressRecordComponentsUpdate = true;
    this._markBodyForRedraw(this._$dataChanged);
},



// Ensure that when body redraw runs, we do a full field-widths refresh, including
// calculating auto-fit size
_forceUpdateFieldWidths : function (reason) {
    if (this.body && this.fields) {
        this.body._fieldWidthsDirty = reason == null ? true : reason;
        // Ensure we actually calculate new field widths based on data values.
        delete this.fields._appliedInitialAutoFitWidth;
    }

},

// wrap the call out to the dataArrived override point and handle sorter according to
// canSortClientOnly value and current data-state
_dataArrived : function (startRow, endRow) {

    var sortField = this._getSortFieldNum();


    if (this._updateFieldWidthsOnDataArrived || startRow == 0) {
        this._updateFieldWidthsOnDataArrived = false;
        this._forceUpdateFieldWidths("New dataset loaded");
    }
    if (sortField != null && sortField != -1) {
        var fieldNum = this.getFieldNum(sortField),
            field = this.getField(fieldNum)
        ;

        if (field && field.canSortClientOnly && !this._canSortData(field)) {
            this._setSortFieldNum(null);

            // tell that toolbar button to unselect / get rid of sort arrow
            if (sortField != null && this.header && isc.isA.Toolbar(this.header)) {
                this.header.deselectButton(sortField);
                var button = this.header.getButton(sortField);
                if (button) button.setTitle(this.getHeaderButtonTitle(button));
            }

            // Get rid of the sort arrow in the sorter
            if (this.sorter && this.sorter.setTitle) this.sorter.setTitle(this.sorter.getTitle());
        }
    }

    if (!(this.canSelectAll == false) && this.getCurrentCheckboxField()) {
        var cbPos = this.getCheckboxFieldPosition(),
            field = this.getField(cbPos);
        // if we are showing a checkbox select-all header, and we don't have
        // a full cache, disable the checkbox header and show a hover prompt
        if (isc.ResultSet && isc.isA.ResultSet(this.data)
            && !this.data.allMatchingRowsCached())
        {
            var props = {
                disabled: true,
                showHover: true,
                prompt: this.selection.selectionRangeNotLoadedMessage,
                title: (this.selectionType == "single") ? isc.nbsp :
                        this._getCheckboxValueIconHTML(false, true, true, field)
            }
        // if we now have a full cache, enable the checkbox selectAll header
        } else {
            var props = {
                disabled: false,
                showHover: false,
                prompt: null,
                title: (this.selectionType == "single") ? isc.nbsp :
                        this._getCheckboxValueIconHTML(false, true, false, field)
            }
        }
        this.setFieldProperties(cbPos, props);
    }


    if (isc.screenReader && this.body != null) {
        if (isc.isA.Tree(this.data)) {
            // in this case we're passed a single param, the parent node.
            var node = startRow;
            if (this.data.isOpen(node) && this.data.hasChildren(node)) {
                var children = this.data.getChildren(node);
                if (children && !children.isEmpty()) node = children.first();
            }
            var rowNum = this.data.indexOf(node);
            // If we don't currently have focus, just remember the native focus row - this means
            // if we're showing a modal prompt / redrawing we should refocus on the right native
            // element...
            this.body._putNativeFocusInRow(rowNum, !this.hasFocus);
        }
    }

    // we only want to run auto-sizing code if the data has already arrived
    if (this._autoSizeOnDataArrived) {
        this._headerDoubleClick(this._autoSizeHeaderFieldNum, this._autoSizeHeader);
        this._autoSizeHeaderFieldNum = null;
        this._autoSizeHeader = null;
        this._autoSizeOnDataArrived = false;
    }

    this._markBodyForRedraw();

    this._fromDataArrived = true;
    this.dataArrived(startRow, endRow);
    delete this._fromDataArrived;

    this.updateBodyCanFocusForData();

},

updateBodyCanFocusForData : function () {
    if (this.body && !this.canFocusInEmptyGrid) {
        var isEmpty = this.isEmpty();
        if (isEmpty != this._wasEmptyForCanFocus) {
            this.body._updateCanFocus();
            if (this.frozenBody != null) this.frozenBody._updateCanFocus();
            this._wasEmptyForCanFocus = isEmpty;
        }
    }
},


// doc'd in registerStringMethods block
dataArrived : function (startRow, endRow) {},

//>    @method listGrid._ignoreData() (A)
//        stop observing methods on data when it goes out of scope
//        called automatically by setData
//    @param    data    (object)        old data to be ignored
//<
_ignoreData : function (data, destroying) {
    //>Animation
    // Call finishRowAnimation - will kill any show/hide row animations
    // These animations assume the data remains constant for the duration
    // of the animation.
    // (No-ops if appropriate)
    if (this.body) this.body.finishRowAnimation();
    //<Animation

    if (isc.isA.Tree(this.data)) this.ignore(data, "changeDataVisibility");

    this.ignore(data, "dataChanged");

    if (this.isObserving(data, "dataArrived")) {
        this.ignore(data, "dataArrived");
    }


    // If we're destroying, no need to call deslectAll, as we'll continue to
    // destroySelectionModel() anyway, and calling deselectAll can cause
    // selectionChanged notifications to fire which is likely to lead to
    // application level crashes since the grid is in an invalid state
    if (!destroying && this.selection) this.selection.deselectAll();
    // NOTE: we don't ignore this.selection.setSelected because
    //            we're re-using the same selection object

},

//>    @method    listGrid.applyFieldDefaults()
//        @group    data
//         Derive default field sizes and formatters where possible, based on schema information.
//<
_generatedFieldNameCount:0,
applyFieldDefaults : function (fields) {
    if (fields == null) return;

    // apply ListGrid-specific defaults, like using toShortDate() for Date fields
    for (var i = 0; i < fields.length; i++) {
        var field = fields[i];

        if (field == null) continue;

        // In general we can support un-named fields in dataBoundComponents if there is a dataPath
        if (!this.allowNamelessFields && field[this.fieldIdProperty] == null) {
            if (field.dataPath == null) {
                this.logWarn("unable to process field with no name / dataPath:" +
                             this.echo(field));
                continue;
            }
            // apply an arbitrary name - this gives us a straightforward way to map the
            // field object any generated editor item, etc.

            field[this.fieldIdProperty] = "field" + this._generatedFieldNameCount++;
        }
        // default the alignment of each field to left if not specified
        // In RTL mode default to right alignment (so text flows from start of value
        // outward as you'd expect). This means setting the property to "left" if
        // reverseRTLAlign is set, otherwise to "right".
        var defaultAlign = (this.isRTL() && !this.reverseRTLAlign) ? isc.Canvas.RIGHT
                                                                   : isc.Canvas.LEFT;

        // "type" is used for align and default formatting. If we have a display field
        // we're pulling display-values from, use the type specified there rather than
        // on the underlying data values the display values represent.
        var type = this.getFieldDisplayType(field);
        var baseType = (type != null ? isc.SimpleType.getBaseType(type) : null);


        // note: needs to be first, as "image" type technically inherits from text
        if (isc.SimpleType.inheritsFrom(type, "image")) {
            field._typeFormatter = this._formatImageCellValue;

        // Attempt to size columns to fit their content
        } else if (baseType == this._$text) {

            if (field.width == null && field.length != null) {
                if (field.length < 15 && !field.valueMap) {
                    // use minimal space for small text fields with no value map
                    field.width = field.length * 7;
                }
            }

        } else if (baseType == "integer" || baseType == "float") {
            // align numbers right by default to line up decimal places
            // Exception: If there's a valueMap attached we don't want to right-align
            // (Example - mapping error codes to display strings).

            var hasObjValueMap = field.valueMap != null && !isc.isAn.Array(field.valueMap),
                reverseAlign = (this.isRTL() && !this.reverseRTLAlign);
            defaultAlign = hasObjValueMap ?
                            (reverseAlign ? isc.Canvas.RIGHT : isc.Canvas.LEFT) :
                            (reverseAlign ? isc.Canvas.LEFT : isc.Canvas.RIGHT);
            field._typeFormatter = this._formatNumberCellValue;

        // by default size date columns fields to match the default shortDate format applied
        // to date fields

        } else if (baseType == "date") {
            var canEdit = (this.canEdit == true && field.canEdit != false) ||
                          (this.canEdit != false && field.canEdit == true);
            // If the field has unspecified size, size to accommodate formatted date
            // (or editor if the field is editable)
            field.width = field.width || (canEdit ? 100 : 80);
            // right alignment lines up years if day/month values are numeric and not padded
            defaultAlign = (this.isRTL() && !this.reverseRTLAlign)  ? isc.Canvas.LEFT
                                                                    : isc.Canvas.RIGHT;
            field._typeFormatter = this._formatDateCellValue;

        // by default size time columns fields to match the default format applied to time
        // fields
        } else if (baseType == "time") {
            field.width = field.width || 80;
            field._typeFormatter = this._formatTimeCellValue;
            defaultAlign = (this.isRTL() && !this.reverseRTLAlign)  ? isc.Canvas.LEFT
                                                                    : isc.Canvas.RIGHT;

        } else if (type == "binary" || type == "blob" || type == "upload" || type == "imageFile") {
            field._typeFormatter = this._formatBinaryCellValue;
        } else if (type == "link") {
            field._typeFormatter = this._formatLinkCellValue;
        } else if (type == "icon") {
            if (field.width == null && field.autoFitWidth == null) {
                if (this.autoFitIconFields != "none") {
                    field.autoFitWidth = true;
                    field.autoFitWidthApproach =
                        (this.autoFitIconFields == "title") ? "both" : "value";
                }
            }

            // check autoFitIconFields -- if set, set min width to accommodate the
            // icons and set autoFitWidth:true / autoFitWidthApproach such that
            // it'll expand to accommodate the title if appropriate
            if (field.width == null && field.autoFitWidth == null) {
                if (this.autoFitIconFields != "none") {
                    field.autoFitWidth = true;
                    field.autoFitWidthApproach =
                        (this.autoFitIconFields == "title") ? "both" : "value";
                    // set the default width (min width) to the
                    // calculated default width

                    field.width = this.getDefaultFieldWidth(field);
                }
            }
            field.align = field.align || "center";

            // install a formatter that will put button.icon into the cell
            field._typeFormatter = this._formatIconCellValue;

            // default title so that icon appears alone (otherwise would default to field name
            // if title was unset)
            field.title = field.title || isc.nbsp;

        // turn on 'canToggle' for all boolean fields.
        // If 'canEdit' is also set to true these fields will be editable via a single
        // click
        } else if (type == "boolean" || type== "checkbox") {
            if (field.canToggle == null) field.canToggle = true;
        }

        // For boolean fields we show checkbox images by default
        // this is handled via the valueIcon system - see getValueIcon(), getValueIconWidth() and
        // showValueIconOnly()


        // If formatCellValue was passed to us as a string, convert it to a method
        if (field.formatCellValue != null && !isc.isA.Function(field.formatCellValue))
            isc.Func.replaceWithMethod(field, "formatCellValue", "value,record,rowNum,colNum,grid");

        if (this.showValueIconOnly(field)) {
            defaultAlign = isc.Canvas.CENTER;


            field.iconSpacing = 0;

            // apply the "icon" field logic to fields that show valueIcons -
            // respect autoFitIconFields here too
            if (field.width == null && field.autoFitWidth == null) {
                if (this.autoFitIconFields != "none") {
                    field.autoFitWidth = true;
                    field.autoFitWidthApproach =
                        (this.autoFitIconFields == "title") ? "both" : "value";
                    // set the default width (min width) to the
                    // calculated default width
                    field.width = this.getDefaultFieldWidth(field);
                }
            }
        } else if (field.icon != null && field.showTitle == false) {
            field.iconSpacing = 0;
        }

        // TODO: numeric quantities with range validators could be given specific sizes
        if (!field.align) field.align = defaultAlign;

        // For fields marked as multiple:true, set the "validateEachItem" flag.
        // This ensures that when validators run in an editable grid we will
        // validate each selected value
        if (field.multiple && field.validateEachItem == null) field.validateEachItem = true;

        // If field is marked as initially hidden, use showIf:"false"
        if (field.hidden) field.showIf = "false";
    }
},

// If this field has a specified display field, pick up the underlying display field object
// (from the optionDataSource if appropriate, otherwise from the grid itself, or the
// underlying DS the grid is bound to).
getDisplayField : function (field) {
    var displayField = field.displayField;
    if (displayField != null) {
        var ods = field.optionDataSource ? isc.DataSource.get(field.optionDataSource) : null;
        if (ods != null) displayField = ods.getField(displayField);
        else {
            displayField = this.getField(displayField);
            if (displayField == null && this.dataSource != null) {
                displayField = this.getDataSource().getField(field.displayField);
            }
        }
    }
    return displayField;
},

// Returns the specified type of the field's displayField, if there is one, otherwise of the
// field itself.
getFieldDisplayType : function (field) {
    var displayField = this.getDisplayField(field),
        type;
    if (displayField != null) type = displayField.type;
    if (type == null) type = field.type;
    return type;
},

// Helper method called on boolean fields to determine whether we should use the
// booleanTrueImage/booleanFalseImage and related settings, or fall back to the general
// valueIcons system, which may show a combination of text and icons
_$boolean:"boolean",
_formatBooleanFieldAsImages : function (field) {
    // If booleanTrue/FalseImage have been set null, always back off to showing text / valueIcons
    if (this.booleanTrueImage == null && this.booleanFalseImage == null) return false;

    var type = this.getFieldDisplayType(field),
        baseType = (type != null ? isc.SimpleType.getBaseType(type) : null);
    if (baseType != this._$boolean) return false;

    // read as: user has not tried to set valueIcon-specific flags.
    // Also note: it's commonly necessary to set a valueMap with a boolean field in order to
    // allow stored values like YES/NO/null to be mapped to boolean true/false, so a valueMap
    // doesn't indicate an intent to use valueIcons.  If you have a valueMap and there are more
    // values than true/false/unset, you shouldn't declare the field boolean, it should be
    // enum.
    return (!field.suppressValueIcon && field.showValueIconOnly == null &&
             field.valueIcons == null && field.formatCellValue == null);
},


//>    @method    listGrid.setFieldProperties()
// Dynamically set properties for a particular field. This method will update the fields
// header-button without having to explicitly reset the fields in the grid.  <smartgwt>
// The passed-in +link{ListGridField} should contain just the minimal properties you want to
// change; do not take the original ListGridField, modify it, and just pass that to this
// function.</smartgwt>
// <P>
// NOTE: Where explicit setters exist for field properties (such as
// +link{resizeField()}, +link{setFieldTitle()}, +link{setFieldIcon()}, etc.) these should be
// used instead.
//
// @param    fieldNum (number | String) name of the field, or index.
// @param    properties (ListGridField Properties) properties to apply to the header
// @visibility external
//<
// NOTE: little testing has been done on which properties can actually be set this way
setFieldProperties : function (fieldNum, properties) {
    var field, allFields = this.getAllFields();
    var origField = fieldNum;
    if (isc.isA.Number(fieldNum)) {
        // if an index was passed, use the visible fields
        field = this.getField(fieldNum);
    } else {
        // if a key was passed, use the complete fields to handle hidden columns
        var globalFieldNum = isc.Class.getArrayItemIndex(
                                fieldNum, allFields, this.fieldIdProperty);
        field = allFields[globalFieldNum];
        // map back to the fieldNum within this.fields (not within this.completeFields)
        fieldNum = this.getFieldNum(field);
    }
    if (!field) return;
    isc.addProperties(field, properties);

    if (this.header != null && this.header.isDrawn()) {
        // getFieldHeader / getLocalFieldNum will account for frozen fields
        var header = this.getFieldHeader(fieldNum),
            headerButton = header.getMember(this.getLocalFieldNum(fieldNum));
        if (headerButton) headerButton.setProperties(properties);
    }
},

//> @method listGrid.setFieldTitle()
// Change the title of a field after the grid is created.
//
// @param fieldNum (int | String) name of the field, or index.
// @param title (String) new title
// @visibility external
//<
setFieldTitle : function (fieldNum, title) {
    this.setFieldProperties(fieldNum, {title:title});
},

//> @method listGrid.setFieldHeaderBaseStyle()
// Update the +link{listGridField.headerBaseStyle} for a field within the grid at runtime.
// @param name (String) name of the field.
// @param newStyle (CSSClass) new baseStyle for the field header
// @visibility external
//<
setFieldHeaderBaseStyle : function (name, baseStyle) {
    var field = this.getField(name);
    if (field == null) {
        this.logWarn("setFieldHeaderBaseStyle() unable to find field:" + name);
        return;
    }
    field.headerBaseStyle = baseStyle;
    var button = this.getFieldHeaderButton(this.getFieldNum(field));
    if (button != null) {

        // Treat being passed null as an attempt to revert to default base style.
        if (baseStyle == null) {
            var buttonProperties = this.getHeaderButtonProperties();
            if (field.frozen && buttonProperties.frozenBaseStyle) {
                baseStyle = buttonProperties.frozenBaseStyle;
            } else if (buttonProperties.baseStyle) {
                baseStyle = buttonProperties.baseStyle;
            }
        }
        if (baseStyle == null) {
            baseStyle = button.getClass().getPrototype().baseStyle;
        }
        button.setBaseStyle(baseStyle);
    }
},

//> @method listGrid.setFieldHeaderTitleStyle()
// Update the +link{listGridField.headerTitleStyle} for a field within the grid at runtime.
// @param name (String) name of the field.
// @param newStyle (CSSClass) new titleTyle for the field header
// @visibility external
//<
setFieldHeaderTitleStyle : function (name, titleStyle) {
    var field = this.getField(name);
    if (field == null) {
        this.logWarn("setFieldHeaderTitleStyle() unable to find field:" + name);
        return;
    }
    field.headerTitleStyle = titleStyle;
    var button = this.getFieldHeaderButton(this.getFieldNum(field));
    if (button != null) {
        if (titleStyle == null) {

            var buttonProperties = this.getHeaderButtonProperties();
            if (field.frozen && buttonProperties.frozenTitleStyle) {
                titleStyle = buttonProperties.frozenTitleStyle;
            } else if (buttonProperties.titleStyle) {
                titleStyle = buttonProperties.titleStyle;
            }
        }
        if (titleStyle == null) {
            titleStyle = button.getClass().getPrototype().titleStyle;
        }
        button.setTitleStyle(titleStyle);
    }
},

//> @method listGrid.setFieldIcon()
// Change the +link{listGridField.icon} for a field after the grid is created
// @param fieldName (String) field to update
// @param icon (SCImgURL) icon for the field
// @visibility external
//<
setFieldIcon : function (fieldName, icon) {
    var field = this.getField(fieldName);
    this.setFieldProperties(fieldName, {icon:icon});
    if (field && field.type == "icon" && field.cellIcon == null) {
        delete field._iconHTML
        this.body.markForRedraw("Field icon changed");
    }
},

//> @method listGrid.setFieldCellIcon()
// Change the +link{listGridField.cellIcon} for a field after the grid is created
// @param fieldName (String) field to update
// @param cellIcon (SCImgURL) new cellIcon for the field
// @visibility external
//<
setFieldCellIcon : function (fieldName, icon) {
    this.setFieldProperties(fieldName, {cellIcon:icon});
    var field = this.getField(fieldName);
    if (field && field.type == "icon") {
        delete field._iconHTML
        this.body.markForRedraw("Field cell icon changed");
    }
},


// AutoComplete
// --------------------------------------------------------------------------------------------

//> @method listGrid.setAutoComplete()
// Change the autoCompletion mode for the grid as a whole.
//
// @param   newSetting (AutoComplete)  new setting
// @group autoComplete
// @visibility autoComplete
//<
setAutoComplete : function (newSetting) {
    this.autoComplete = newSetting;
},

//> @method listGrid.setFieldAutoComplete()
// Change the autoCompletion mode for an individual field.
//
// @param   newSetting (AutoComplete)  new setting
// @group autoComplete
// @visibility autoComplete
//<
setFieldAutoComplete : function (field, newSetting) {
    field = this.getField(field);
    if (field) field.autoComplete = newSetting;
},

// --------------------------------------------------------------------------------------------

//>    @method    listGrid.showFields()
// Force an array of fields to be shown. This method does not add new fields to the grid,
// it simply changes field visibility. If a field.showIf expression exists, it will be
// destroyed.
// <P>
// Note: for showing multiple fields it is more efficient to call this method than to call
// +link{showField()} repeatedly.
//
// @param    field           (Array of String | Array of ListGridField)    Fields to show.
// @param   [suppressRelayout] (boolean) If passed, don't resize non-explicitly sized columns
//                                       to fill the available space.
// @visibility external
// @example columnOrder
//<
// Actually this is a synonym for showField() - separated for ease of documentation
showFields : function (fields, suppressRelayout) {
    return this.showField(fields,suppressRelayout);
},

//> @method listGrid.showField()
// Force a field to be shown. This method does not add new fields to the grid,
// it simply changes field visibility. If a field.showIf expression exists, it will be
// destroyed.
// <P>
// Note: for showing multiple fields it is more efficient to call +link{showFields()} than
// to call this method repeatedly.
//
// @param field (String | ListGridField) field to show
// @param [suppressRelayout] (boolean) If passed, don't resize non-explicitly sized columns
//                                       to fill the available space.
// @visibility external
// @example columnOrder
//<
showField : function (fields, suppressRelayout) {

    arguments.__this = this;

    if (!isc.isAn.Array(fields)) {
        fields = [fields];
    }

    var noFields = true,
        allVisible = true;

    var setFieldsCalled = this.completeFields != null,
        mustSetFields = (!setFieldsCalled || this.frozenFields || this._suppressedFrozenFields)
    ;

    for (var i = 0; i < fields.length; i++) {

        var field = fields[i],
            fieldObj = field;

        // Use getSpecifiedField() to retrieve the fieldObject from the fields / completeFields
        // array.
        // Note that this returns null for an invalid field object / ID
        fieldObj = this.getSpecifiedField(fieldObj);

        if (fieldObj == null) {
            fields[i] = null;
            this.logWarn("showField(): unable to find field object for field: " + field
                         + ". Taking no action. Call setFields() to add new fields.")
            continue;
        }

        noFields = false;

        // Update initialization property to match new field state
        fieldObj.hidden = false;

        // -- We always want to clear out any showIf property on the field, as even if the field is
        //    currently being shown, we want the field to continue to be shown from this point on
        //    regardless of any conditions in a showIf property

        if (fieldObj.detail) {
            fieldObj.showIf = "true";
        } else {
            if (fieldObj.showIf != null) fieldObj.showIf = null;
        }

        // need to call setFields if the field is frozen or if it was already visible at a
        // different masterIndex
        if (fieldObj.frozen) mustSetFields = true;

        // if this field is in a headerSpan, we need to call setFields() to rebuild it
        if (this.spanMap && this.spanMap[fieldObj.name] != null) mustSetFields = true;

        if (mustSetFields) continue;

        // If this.fields contains the object, we can assume it's already visible if we're drawn
        // and will show up when we get drawn otherwise.
        if (this.fields.contains(fieldObj)) {
            fields[i] = null;
            continue;
        }

        // At this point we know we have at least one field that needs to be added
        // to the fields array (was previously hidden)
        allVisible = false;
        // hang onto the "live" fieldObj in the array
        fields[i] = fieldObj;
    }

    if (mustSetFields) {
         // Frozen fields: with frozen fields, the partial rebuild attempted below doesn't work.
         // This hasn't been looked into in detail yet.
         this.setFields(this.completeFields || this.fields);
         this.handleFieldStateChanged();
         return;
    }

    if (noFields || allVisible) return;


    // update this.fields
    this.deriveVisibleFields();

    // Empty slots may be present due to already visible fields
    fields.removeEmpty();

    var shownFieldNums = [],
        foundFields = 0;

    // Determine the position of the newly shown fields in our fields array.
    for (var i = 0; i < this.fields.length; i++) {
        var index = fields.indexOf(this.fields[i]);
        if (index != -1) {
            shownFieldNums[index] = i;

            foundFields++;
            // stop when we've figured out the positon of all fields passed in.
            if (foundFields == fields.length) break;
        }
    }

    var header = this.header;
    // Update any UI to display the new field, if necessary


    // create the header button for the new column
    if (header != null) {
        if (!suppressRelayout) this.header.hPolicy = "fill";
        // undocumented feature - addButtons handles being pased an array of field indices
        // as well as an array of buttons

        this._suppressAutoFitToTitle = true;
        this.header.addButtons(fields.duplicate(), shownFieldNums);
        delete this._suppressAutoFitToTitle;
    }

    // tell the body about the new field
    if (this.body) {

        // If we're showing an editor in the new field we need to
        // - create the form items for it and slot it into the edit form
        // - shift the colNum on other items, and the _editColNum of the grid as a whole.
        if (this._editorShowing) {

            var editRowNum = this.getEditRow(),
                record = this.getRecord(editRowNum),


                editedRecord = this.getEditedRecord(editRowNum),

                adjustedEditColNum = false,
                items = this.getEditForm().items,
                liveItemIndex = items.length-1,
                liveItem = items[liveItemIndex],
                itemColNum = liveItem.colNum;

            // sort the shownFieldNums
            // this allows us to update the live edit item colNum values easily.
            // Note that it means the shownFieldNums will no longer match up to the fields
            // entries in the shown fields array.
            shownFieldNums.sort();
            for (var i = shownFieldNums.length-1; i >= 0; i--) {
                var offset = i+1,
                    fieldNum = shownFieldNums[i],
                    threshold = (fieldNum - i);

                if (!adjustedEditColNum && this._editColNum >= fieldNum) {
                    this._editColNum += offset;
                }

                // Update the edit form
                var fieldObj = this.fields[fieldNum],
                    width = this.getEditFormItemFieldWidths(record)[fieldNum],
                    item;
                while (liveItem != null && itemColNum >= threshold) {
                    liveItem.colNum += offset;

                    liveItemIndex --;
                    liveItem = (liveItemIndex >= 0) ? items[liveItemIndex] : null;
                    itemColNum = (liveItem != null) ? liveItem.colNum : null;
                }

                // at this point we've updated any items with a colNum >= ours
                // Create an item for the newly shown field and slot it into the edit form
                //
                // liveItemIndex will now be set to the slot before the one we're interested in
                // (index of the item that previously matched our colNum, minus 1)
                //
                // Note if we're editing by cell this won't apply since if we're showing an editor
                // the field its showing on must already be visible.
                var drawnRange = this.body.getDrawArea();
                if (!this.editByCell && fieldNum >= drawnRange[2] && fieldNum <= drawnRange[3]) {
                    item = this.getEditItem(fieldObj, record, editedRecord, editRowNum, fieldNum, width);
                }
                if (item != null) {
                    this._editRowForm.addItems([item], liveItemIndex+1);
                }
            }
        }

        this.body.fields = this.normalFields || this.fields;
        this.setBodyFieldWidths(this.getFieldWidths());

        this._remapEmbeddedComponentColumns(this.body);

        // instant redraw rather than markForRedraw because we have to avoid dropping
        // values
        if (this.body.isDrawn()) this.body.redraw("show field");
    }

    // If we're auto-fitting vertically, allow the header to shrink or grow vertically
    // as appropriate

    if (this.header && this.autoFitHeaderHeights) {
        this.dropCachedHeaderButtonHeights();
        this._updateHeaderHeight();
    }


    // reset the sortFieldNum - we may have added or repositioned the primary sort field
    if (this.sortField != null) {
        this.sortFieldNum = null;
        this.sortFieldNum = this._getSortFieldNum()
    }

    // If we have a filterEditor showing, update its fields too
    if (this.filterEditor != null) this.filterEditor.showField(fields, suppressRelayout);

    // update recordSummaries as well as grid/group ones
    this.recalculateSummaries(null, fields);
    if (this.summaryRow != null && this.showGridSummary) {
        this.summaryRow.showField(fields, suppressRelayout);
    }

    // reapply hilites - we don't store hilite information on hidden fields so if
    // hidden fields are displayed we'll need to reapply.
    if (this.hilites) this.applyHilites()

    this.markForRedraw("showField");

    this.handleFieldStateChanged();
    this._clearFieldDependencyTable();
},

//>    @method    listGrid.hideFields()
// Force an array of fields to be hidden.
// <P>
// NOTE: If a field.showIf expression exists, it will be destroyed.
// <P>
// When hiding multiple fields, this method should be called rather than
// calling +link{listGrid.hideField()} repeatedly for each field to hide.
//
// @param fields (Array of String | Array of ListGridField) fields to hide
// @param [suppressRelayout] (boolean) if passed, don't relayout non-explicit sized fields
//                                      to fit the available space
// @visibility external
//<
hideFields : function (fields, suppressRelayout) {
    return this.hideField(fields, suppressRelayout);
},

//> @attr listGrid.discardEditsOnHideField (boolean : true : IRW)
// If a user is editing a +link{listGrid.canEdit,canEdit:true} listGrid, and they hide
// a field while the editor is showing, should we discard any edits in the edit row for
// the field being hidden?
// <P>
// Default behavior is to discard the edits - set this flag to false to preserve edits
// <P>
// Note that the +link{dropCriteriaOnHideField} attribute allows developers to
// control behavior for filter-editor criteria when a field is hidden.
//
// @visibility external
//<
// The intention here is to avoid confusion. If the user is editing a field in an
// auto-save-edits:true grid, it is rare for there to be pending editor values outside the
// edit-form. Most commonly this will occur only for validation failure.
// In this case, if the edits a field and hides it and then a save fails due to a validation
// failure on the hidden field, there's no user-visible feedback indicating what happened.
// Developers who choose to do so can set this flag to false and handle this case via
// validation failure handling code at the grid level of course.
// We also disable this for the filterEditor where we want to retain filter criteria which
// are not reflected in the visible set of fields.
discardEditsOnHideField:true,

//> @attr listGrid.dropCriteriaOnHideField (boolean : true : IRW)
// For ListGrids with +link{showFilterEditor} set to true, if the user hides a field,
// should we discard the criteria for the hidden field when the user next performs a
// filter? This is an equivalent of using the AutoChild system to set
// +link{listGrid.discardEditsOnHideField} on the +link{listGrid.filterEditor}.
//
// @visibility external
//<
dropCriteriaOnHideField:true,

//> @method listGrid.hideField()
// Force a field to be hidden.<br><br>
//
// NOTE: If a field.showIf expression exists, it will be destroyed.
// <P>
// Note also that if multiple fields are to be hidden it is more efficient to
// call +link{hideFields()} passing in the array of fields to hide rather than to call
// this method repeatedly.
// <P>
// See +link{listGrid.discardEditsOnHideField} and +link{listGrid.dropCriteriaOnHideField}
// for behavior when hiding fields while +link{listGrid.canEdit,editing}.
//
//
// @param field (String | ListGridField) field to hide
// @param [suppressRelayout] (boolean) if passed, don't relayout non-explicit sized fields
//                                      to fit the available space
// @visibility external
// @example columnOrder
//<
hideField : function (fields, suppressRelayout) {
    arguments.__this = this;

    var noFields = true,
        allHidden = true;

    if (!isc.isAn.Array(fields)) {
        fields = [fields];
    }
    // we shuffle some stored colNums around on edit items - store out the index of each field
    // being hidden in an array to make this easier.
    var hiddenFieldNums = [];

    if (fields.length == this.getFields().length) {
        // can't hide the last bunch of fields
        this.logWarn("Attempt to hide all fields in one call - disallowed.");
        return;
    }

    // If we need to update the UI, it's more efficient to do it here directly than
    // just calling setFields().
    // However if setFields has never been called we will have to call it
    //
    // Frozen fields: with frozen fields, the partial rebuild attempted below doesn't work.
    // This hasn't been looked into in detail yet.


    var mustSetFields = (this.completeFields == null || this.frozenFields);
    // If we're showing an editor we need to make certain changes to get rid of the
    // form item, etc.
    var editorShowing = this._editorShowing,
        editRow = editorShowing ? this.getEditRow() : null,
        editCol = editorShowing ? this.getEditCol() : null,
        hidEditCell = false;
    for (var i = 0; i < fields.length; i++) {
        var field = fields[i],
            fieldObj = field;

        // Use getSpecifiedField() to pick up the field object from the completeFields array
        // (or if setFields has never been called, from the fields array).
        // Note - if we're passed an invalid field object, this method returns null.
        fieldObj = this.getSpecifiedField(fieldObj);
        if (fieldObj == null) {
            this.logWarn("hideField(): unable to find field object for field: " + field
                         + ". Taking no action. To add this field use the setFields() method.");
            fields[i] = null;
            continue;
        }
        noFields = false;

        // -- Set showIf to always evaluate to false.
        fieldObj.showIf = this._$false;

        // If the field is not currently present in this.fields, we can safely assume it's already
        // hidden. No need to proceed in this case
        if (!this.fields.contains(fieldObj)) {
            fields[i] = null;
            continue;
        }
        allHidden = false;

        // Update initialization property to match new field state
        fieldObj.hidden = true;

        // If we're going to call setFields, we're done - we'll just call that
        // method outside this for-loop to update the completeFields array, edit values, etc.
        if (mustSetFields) continue;

        var fieldNum = this.fields.indexOf(fieldObj),
            fieldName = this.getFieldName(fieldNum);

        hiddenFieldNums.add(fieldNum);

        if (editorShowing) {
            if (editCol == fieldNum) hidEditCell = true;

            // If the user has modified the edit value and not yet stored the change
            // (IE it isn't yet part of our 'pending edit values' for the row), discard
            // the unsaved edits.

            // By default we're going to clear the edit value from the cell being hidden
            // pass the additional 3rd parameter to avoid re-displaying the record's value
            // in the (about to be cleared) cell.
            var focusItem = this.getEditFormItem(fieldName);
            // (Item may not exist due to incremental rendering, non editable fields, editByCell)
            if (focusItem && focusItem.hasFocus) focusItem.blurItem();

            if (this.discardEditsOnHideField) {
            this.clearEditValue(editRow, fieldNum, true);
        }
    }
    }
    // If we were passed an empty array, or an array containing already hidden fields
    // we can bail here.
    if (noFields || allHidden) return;

    // if we have to call setFields, simply call it and allow that method to do the work
    // of updating the fields array etc.
    if (mustSetFields) {
        this.setFields(this.completeFields || this.fields);
        this.handleFieldStateChanged();
        return;
    }

    fields.removeEmpty();
    if (editorShowing) {
        if (hidEditCell) {
            // If we're editing by cell, and hiding the current edit cell just kill the edit.
            if (this.editByCell) {
                this.cancelEditing(isc.ListGrid.PROGRAMMATIC);
                editorShowing = false;
            } else {
                // If possible we want to shift edit focus to an adjacent field
                // findNextEditCell() is unaware of the cells we've just hidden so
                // reimplement the relevent part of this method to find the next editable and
                // still visible field. Check for closest field to the left first, then to the right
                var newEditCol = editCol-1,
                    foundEditCol = false;
                while (newEditCol >= 0) {
                    if (!hiddenFieldNums.contains(newEditCol) &&
                        this.canEditCell(editRow, newEditCol) &&
                        this._canFocusInEditor(editRow, newEditCol))
                    {
                        foundEditCol = true;
                        break;
                    }
                    newEditCol--;
                }
                if (!foundEditCol) {
                    newEditCol = editCol +1;
                    while (newEditCol < this.fields.length) {
                        if (!hiddenFieldNums.contains(newEditCol) &&
                            this.canEditCell(editRow, newEditCol) &&
                            this._canFocusInEditor(editRow, newEditCol))
                        {
                            foundEditCol = true;
                            break;
                        }
                        newEditCol++;
                    }
                }

                // If we don't have any other editable cells in the row, just cancel the edit
                if (!foundEditCol) {
                    this.cancelEditing(isc.ListGrid.PROGRAMMATIC);
                    editorShowing = false;
                } else {
                    // focus in the adjacent field.

                    this._startEditing(editRow, newEditCol,
                                            !this.getEditForm().hasFocus);
                }
            }
        }
    }
    // update this.fields
    this.deriveVisibleFields();
    // destroy the header button
    var header = this.header;
    if (header != null) {
        // Setting the hPolicy to "fill" will cause the header to relay it's buttons out to
        // fill the available space.
        if (!suppressRelayout) this.header.hPolicy = "fill";
        var buttons = [];
        for (var i = 0; i < hiddenFieldNums.length; i++) {
            var fieldNum = hiddenFieldNums[i];

            var button = this.header.getButton(fieldNum);
            buttons[buttons.length] = button;
            if (this.headerMenuButton && this.headerMenuButton.masterElement == button) {
                this.headerMenuButton.depeer();
            }
        }
        // removeButtons actually effects the display passed in, so duplicate it
        this.header.removeButtons(buttons.duplicate());
        buttons.map("destroy");

        // If we're auto-fitting vertically, allow the header to shrink or grow vertically
        // as appropriate
        if (this.autoFitHeaderHeights) {
            this.dropCachedHeaderButtonHeights();
            this._updateHeaderHeight();
        }
    }
    // If we're currently showing any edit form items for subsequent columns,
    // we must decrement their 'colNum' properties.
    // do this *before* we redraw the body, as the body redraw relies on these values being
    // accurate to create new items for fields that get shifted into view.
    // Also update the _editColNum if necessary.


    var itemsToClear = [];
    if (editorShowing) {
        hiddenFieldNums.sort();

        var form = this._editRowForm,
            items = form.getItems(),

            itemIndex = items.length-1,
            item = items[itemIndex],
            itemColNum = item.colNum,

            adjustedEditColNum = false;

        for (var i = hiddenFieldNums.length-1; i >= 0; i--) {

            var offset = i+1,
                threshold = hiddenFieldNums[i];

            if (!adjustedEditColNum && this._editColNum > threshold) {
                this._editColNum -= offset;
                adjustedEditColNum = true;
            }

            while (item != null && itemColNum >= threshold) {
                if (itemColNum == threshold) itemsToClear.add(item);
                else item.colNum -= offset;

                itemIndex --;
                item = (itemIndex >= 0) ? items[itemIndex] : null;
                itemColNum = (item != null) ? item.colNum : null;
            }
        }
    }
    // tell the body about the removed fields
    if (this.body) {
        this.body.fields = this.normalFields || this.fields;
        this.setBodyFieldWidths(this.getFieldWidths());

        this._remapEmbeddedComponentColumns(this.body);
        // instant redraw rather than markForRedraw because we have to avoid dropping
        // values
        if (this.body.isDrawn()) this.body.redraw("hide field");
    }

    if (editorShowing && itemsToClear.length > 0) {
        // If we're currently showing an edit form item for this field, remove it now (already
        // been cleared from the DOM), and had the edit values cleared.
        for (var i = 0; i < itemsToClear.length; i++) {
            // Hide the actual item if there is one.
            var item = itemsToClear[i];
            // Note that we may already have updated the items as part of redraw so
            // don't attempt to remove an already destroyed item from the form
            if (item.destroyed) continue;

            this._editRowForm.removeItems([item]);
        }
    }
    // reset the sortFieldNum - we may have removed or repositioned the primary sort field
    if (this.sortField != null) {
        this.sortFieldNum = null;
        this.sortFieldNum = this._getSortFieldNum()
    }

    // If we have a filterEditor showing, update its fields too
    if (this.filterEditor != null) this.filterEditor.hideField(fields, suppressRelayout);
    if (this.summaryRow && this.showGridSummary) {
        this.summaryRow._recalculateSummaries();
        this.summaryRow.hideField(fields, suppressRelayout);
    }

    //this._remapEmbeddedComponents();
    this.handleFieldStateChanged();
    this._clearFieldDependencyTable();
},

//>    @method    listGrid.fieldIsVisible()
// Check whether a field is currently visible
//
// @param    field (String | ListGridField)    field to be checked
// @return (boolean) true if the field is currently visible, false otherwise.
// @visibility external
//<
fieldIsVisible : function (field, optimize) {
    var fieldObj = field;
    // If passed a field ID, look for it in the completeFields array rather than the fieldsArray
    // as it is may be defined, but not visible
    if (!isc.isAn.Object(fieldObj)) fieldObj = this.getSpecifiedField(field);

    // avoid a linear lookup across visible fields
    if (optimize) {
        if      (fieldObj.showIf == "false") return false;
        else if (fieldObj.showIf ==  null)   return true;
    }

    return this.fields.contains(fieldObj);
},

// ----------------------------------------------------------------------------
// panelHeader related methods

showActionInPanel : function (action) {
    // specifically add the "editNew" action, which is not added by default
    if (action.name == "editNew") return true;
    return this.Super("showActionInPanel", arguments);
},

//> @method listGrid.getTitleField()
// @include dataBoundComponent.getTitleField()
//<

//>    @method    listGrid.setFields()  ([A])
// Sets the fields array and/or field widths to newFields and sizes, respectively.<br><br>
//
// If newFields is specified, it is assumed that the new fields may have nothing in common with
// the old fields, and the component is substantially rebuilt.  Furthermore, it's invalid to
// modify any of the existing +link{ListGridField}s after they've been passed to this function.
// Consider the following methods for more efficient, more incremental changes:
// +link{resizeField()}, +link{reorderField()}, +link{showField()}, +link{hideField()}, or
// +link{setFieldProperties()}.
//
// @visibility external
//
// @param    [newFields]        (Array of ListGridField)    array of fields to draw
//<
_$setFields:"set fields",
setFields : function (newFields) {
    if (isc._traceMarkers) arguments.__this = this;
    var recalculateSummaries = false;
    // If called with new fields (more than just an update of existing field state), reset
    // the flag indicating that we've used specified width as a minimum for autoFitWidth fields
    if (newFields != null && this.fields != null &&
        this.fields != newFields && this.completeFields != newFields)
    {
        delete this.fields._appliedInitialAutoFitWidth;
    }
    // Check for any userFormulae / summary fields - we may have to "recalculateSummaries"
    if (isc.isAn.Array(newFields)) {
        for (var i = 0; i < newFields.length; i++) {
            if (newFields[i] &&
                (newFields[i].userFormula || newFields[i][this._$summary]))
            {
                recalculateSummaries = true;
            }
        }
    }


    if (!newFields && this.getDataSource() && !this.getDataSource().hasFields()) {
        this.logWarn("ListGrid.setFields() : neither this ListGrid nor its dataSource have fields");
    }
    // set boolean images to default checkboxItem images if unset
    if (!this.booleanFalseImage && !this.booleanTrueImage && !this.booleanPartialImage) {
        this.booleanTrueImage = isc.CheckboxItem ?
                        isc.CheckboxItem.getInstanceProperty("checkedImage") : null;
        this.booleanFalseImage = isc.CheckboxItem ?
                        isc.CheckboxItem.getInstanceProperty("uncheckedImage") : null;
        this.booleanPartialImage = isc.CheckboxItem ?
                        isc.CheckboxItem.getInstanceProperty("partialSelectedImage") : null;
        // set imageWidth from checkboxItem.valueIconWidth to avoid images having different sizes
        // when editing and not editing

        this.booleanImageWidth = isc.CheckboxItem ?
                        isc.CheckboxItem.getInstanceProperty("valueIconWidth") : null;
        this.booleanImageHeight = isc.CheckboxItem ?
                        isc.CheckboxItem.getInstanceProperty("valueIconHeight") : null;

    }

    // if there is a sortFieldNum set, get a pointer to that field
    // we'll check later to reset the sort if we need to
    var sortFieldNum = this._getSortFieldNum(),
        oldSortField = (sortFieldNum != null && this.fields
                        ? this.fields[sortFieldNum] : null);

    // Interaction of setFields() with editing:
    // - If we have editValues for any row(s) [Pending unsaved edits], it is possible that
    //   some of the fields for which we have edit values will go away - in this case we need
    //   to drop those edit values, and any validation errors for those fields.
    // - If we are currently showing an editor for some row, we may also need to update the
    //   fields in the edit form.
    // * We'll drop the removed fields' edit values silently, [log at info level only] - this
    //   is expected / acceptable behavior when fields are removed.
    var editorShowing = this._editorShowing,
        editRowNum = this.getEditRow(),
        editColNum = this.getEditCol(),
        editForm = this._editRowForm,
        oldEditFieldName = this.fields ? this.getEditFieldName() : null
    ;

    // listGrid.completeFields and listGrid.fields are set to arrays which contain pointers
    // to the same set of "field" objects.
    // - Any fields with a showIf property that evaluates to false will be removed from
    //   this.fields, making it an array of only the list of *visible* fields.
    // - on a reorder or similar manipulation of the set of fields, you should manipulate
    //   this.completeFields and pass the result to setFields().  Otherwise, you will lose any
    //   fields that aren't visible at the time.




    // on the first setFields() ever, create a new array for the visible fields
    if (this.completeFields == null) this.fields = [];
    // bind the passed-in fields to the DataSource and store
    // canPickOmittedFields - if true we'll pick up all dataSource fields but have them
    // marked as showIf:false if useAllDataSourceFields is false
    this.completeFields = this.bindToDataSource(newFields, this.canPickOmittedFields);
    if (this.completeFields == null) this.completeFields = [];

    // rowNumberField displaying the current rowNum of each record
    if (this.shouldShowRowNumberField()) {
        var rnField = this.getCurrentRowNumberField(),
            rnPos = this.getRowNumberFieldPosition(),
            shouldAdd = !rnField
        ;

        // Exception: If the completeFields passed in (the new fields) contains the
        // special field already, no need to add it.

        if (shouldAdd &&
            newFields && newFields.find(this.fieldIdProperty, "_rowNumberField") != null)
        {
            shouldAdd = false;
        }

        if (shouldAdd) rnField = this.getRowNumberField();

        if (shouldAdd) this.completeFields.addAt(rnField, rnPos);
        else this.completeFields.slideList([rnField], rnPos);
    } else {
        var rnField = this.getCurrentRowNumberField();
        if (rnField) this.completeFields.remove(rnField);
    }

    // checkboxField for selection
    if (this.shouldShowCheckboxField()) {
        var cbField = this.getCurrentCheckboxField(),
            cbPos = this.getCheckboxFieldPosition(),
            shouldAdd = !cbField
        ;
        if (shouldAdd && newFields && newFields.find(this.fieldIdProperty, "_checkboxField")) {
            shouldAdd = false;
        }

        if (shouldAdd) cbField = this.getCheckboxField();
        // make checkboxField frozen if we have any other frozen fields - note autoFreeze: true does this now

        // avoid adding the checkbox field at position 1 when there's nothing at position 0
        // (eg, when showingGroupTitleColumn() returns true, but groupBy() hasn't yet been called)
        if (cbPos > this.completeFields.length) cbPos = this.completeFields.length;

        if (shouldAdd) this.completeFields.addAt(cbField, cbPos);
        else this.completeFields.slideList([cbField], cbPos);
    } else {
        var cbField = this.getCurrentCheckboxField();
        if (cbField) this.completeFields.remove(cbField);
    }

    // expansionField for selection
    if (this.shouldShowExpansionField()) {
        var expField = this.getCurrentExpansionField(),
            expPos = this.getExpansionFieldPosition(),
            shouldAdd = !expField
        ;
        if (shouldAdd) expField = this.getExpansionField();
        // make expansionField frozen if we have any frozen fields - note autoFreeze: true does this now

        if (shouldAdd) this.completeFields.addAt(expField, expPos);
        else this.completeFields.slideList([expField], expPos);
    } else {
        var expField = this.getCurrentExpansionField();
        if (expField) this.completeFields.remove(expField);
    }

    // Add / update the removeField if this.canRemoveRecords is true
    if (this.shouldShowRemoveField()) {
        var removeFieldNum = this.completeFields.findIndex("isRemoveField", true),
            removeField = (removeFieldNum >= 0) ? this.completeFields[removeFieldNum]
                                                : {excludeFromState:true, isRemoveField:true};

        if (removeFieldNum == -1 && newFields) {
            removeFieldNum = newFields.findIndex("isRemoveField", true);
        }
        if (!removeField._removeFieldInitialized) {

            isc.addProperties(removeField, this.removeFieldDefaults, this.removeFieldProperties);
            if (removeField.name == null) removeField.name = "_removeField";
            if (removeField.title == null) removeField.title = this.removeFieldTitle;

            // show removeIcon / unremoveIcon by default.
            // if cellIcon or formatCellValue are explicitly specified on the field
            // don't override those.
            if (removeField.cellIcon == null && removeField.formatCellValue == null) {
                removeField.formatCellValue = function (value, record, rowNum, colNum, grid) {
                    // If this record is explicitly marked to not allow removal show no icon
                    if (grid.recordCanRemoveProperty && record && record[grid.recordCanRemoveProperty] === false) return "&nbsp;";

                    if (!this.removeIconHTML) {
                         this.removeIconHTML =
                            isc.Canvas.imgHTML(grid.removeIcon, grid.removeIconSize);
                         this.unremoveIconHTML =
                            isc.Canvas.imgHTML(grid.unremoveIcon, grid.removeIconSize);
                    }
                    if (grid.recordMarkedAsRemoved(rowNum)) {
                        return this.unremoveIconHTML;
                    } else {
                        return this.removeIconHTML;
                    }
                }
                // Store out the iconSize - this makes autoFit more efficient
                removeField.iconSize = this.removeIconSize;
            }
            if (removeFieldNum == -1) {
                this.completeFields.add(removeField);
            }

            removeField._removeFieldInitialized = true;
        }
    }

    // If we have headerSpans ensure the order of fields works with the set of spans
    if (this.headerSpans) {

        // header spans can span fields that aren't next to each other in the fields array.
        // If this happens we'll need to render them next to each other for the spans to work of course
        this.reorderFieldsForHeaderSpans();

        this.spanMap = this.buildSpanMap();
    }



    // set field state if necessary
    var fieldStateAlreadySet = false;
    if (this.fieldState != null) {
        this.setFieldState();
        fieldStateAlreadySet = true;
    }

    // set group state if necessary
    var groupStateAlreadySet = false;
    if (this.groupState != null) {
        this.setGroupState(this.groupState);
        groupStateAlreadySet = true;
    }

    // determine which fields should be shown, and add them to the visible fields array
    // (this.fields)
    // Need to derive the visible fields array before calling applyFieldDefaults() because
    // some field defaults depend on whether a field is the last visible non-frozen field.
    this.deriveVisibleFields();

    // apply various type-based defaults
    this.applyFieldDefaults(this.completeFields);

    this._clearFieldDependencyTable();

    // sets things up to look up display maps when LG is drawn

    if (isc.DataSource) this._setOptionDataSources();

    // warn about all fields being hidden.  Note it's valid to have no fields if you've created
    // a grid that will be bound to a DataSource or provided fields later.
    if (this.fields.length == 0 && this.completeFields.length > 0) {
        this.logWarn("All specified fields for this component are hidden. Note that fields " +
                     "may be hidden via 'showIf' or 'detail' attribute values. " +
                     "In order to display data this grid must have some visible fields.");
    }

    // wipe out the cached fieldWidths, if any
    this._fieldWidths = null;

    var newEditColNum, editFieldStillPresent, hadFocus;

    if (editorShowing) {
        // make sure we get the latest value of the field being edited
        // (Not relevant if we're not drawn since the user can't have updated)
        this.storeUpdatedEditorValue();

        // assume we should continue editing at the field with the same id
        newEditColNum = this.fields.findIndex(this.fieldIdProperty, oldEditFieldName);
        if (newEditColNum != -1 && !this.canEditCell(editRowNum, newEditColNum))
            newEditColNum = -1;

        // if the field with same id isn't editable, find the next editable cell
        if (newEditColNum == -1) {
            var newEditCell;
            // extra param to suppress checking past this row
            if (!this.editByCell) newEditCell = this.findNextEditCell(editRowNum, 0, 1,
                                                                      true,true, false, true);
            if (newEditCell != null && newEditCell[0] == editRowNum) {
                newEditColNum = newEditCell[1];
            }

            // Kill the edit if we're editing by cell, or can't find another editable field
            // in the edit row.
            if (newEditColNum == -1) {
                this.cancelEditing(isc.ListGrid.PROGRAMMATIC);
                editorShowing = false;
            }
        } else {
            // field with the same name present and editable: blur and refocus after redraw
            var item = editForm.getItem(oldEditFieldName);
            if (item) {
                hadFocus = item.hasFocus;
                if (hadFocus) editForm._blurFocusItemWithoutHandler();
            }
            editFieldStillPresent = true;
        }

        // Hide the editor if still editing.
        // Note that this will fire a blur handler unless we have already blurred without
        // the handler

        if (editorShowing) this.hideInlineEditor(false, true);
    }

    var autoCanFreeze = (this.canFreezeFields == null || this._autoDerivedCanFreeze) &&
                        this.fixedRecordHeights != false && this.fixedFieldWidths != false &&
                        this.autoFitData != "horizontal" && this.autoFitData != "both" &&
                        this.bodyOverflow != "visible";
    if (autoCanFreeze) {
        if (this.completeFields.getProperty("overflow").contains("visible")) autoCanFreeze = false;
    }
    if (autoCanFreeze) {
        this._autoDerivedCanFreeze = true;
        this.canFreezeFields = true;
    } else if (this._autoDerivedCanFreeze) {
        delete this._autoDerivedCanFreeze;
        this.canFreezeFields = null;
    }
    // if we're working with a cellSelection, it needs to know how many columns are visible
    if (this.canSelectCells) this.selection.numCols = this.fields.length;

    // if there is an oldSortField, try to find it in the new fields array
    if (oldSortField) {
        var newSortField = this.fields.indexOf(oldSortField);
        if (newSortField == -1) newSortField = null;
        this._setSortFieldNum(newSortField);
    }

    // if we are showing the header, rebuild it with the new set of fields
    var showingHeader = this.showHeader && this.headerHeight > 0 && this.header != null;
    if (showingHeader) {
        // this method will actually update the header to match this.fields
        this.updateHeader();
    }

    // set sort state if necessary
    var sortStateAlreadySet = false;
    if (this.sortState != null) {
        this.setSortState(this.sortState);
        sortStateAlreadySet = true;
    } else {
        // if we've already been sorted and we *can* sort, resort now
        var sortFieldNum = this._getSortFieldNum();
        if (sortFieldNum != null && this._canSortData(sortFieldNum)) {

            this.setSort(this.getSort());
        }
    }

    if (this.summaryRow && this.showGridSummary) {
        this.summaryRow.setFields(this.completeFields.duplicate());
    }

    // If we added formula / summary fields, run the "recalculateSummaries" method to
    // apply calculated values to the records if necessary.
    this.calculateRecordSummaries(null, null, true, true, false, !recalculateSummaries);

    this.updateBody();

    // If we have a filterEditor, update its fields

    if (this.filterEditor) {
         // update the ds on the filterEditor (and the edit-form) - we need a ds to support some
        // expression-parsing features, like "matches other field" detecting fields by title as
        // well as name
        this.filterEditor.updateDataSource(this.getDataSource());
        var completeFields = this.completeFields || [];
        this.filterEditor.setFields(completeFields.duplicate());
    }

    // if we've rebuilt the header, this will place it and draw it.
    // this will also determine new field widths for the new fields (whether from the header or
    // otherwise)
    this.layoutChildren(this._$setFields);

    // sync scroll position of new header with current body scroll position
    if (showingHeader && this.body != null) {
        this.syncHeaderScrolling(this.body.getScrollLeft());
    }

    // Now the fields have been set, update the edit values field:
    // Update our editValues, and validation errors to account for any fields that have
    // gone from the list.
    if (this._editSessions != null) {
        var fieldsDropped = {};

        for (var i in this._editSessions) {
            if (this._editSessions[i] == null) continue;
            var vals = this._editSessions[i]._editValues,
                // We want to hang onto primary key values, even if they're not shown in the
                // ListGrid
                pks = (this.dataSource != null
                    ? this.getDataSource().getPrimaryKeyFieldNames()
                    : [])
            ;

            for (var currentFieldName in vals) {

                if (!this.fields.containsProperty(this.fieldIdProperty, currentFieldName) &&
                    !pks.contains(currentFieldName)) {
                    // track which fields were dropped so we can inform the user / developer
                    fieldsDropped[currentFieldName] = true;
                    // clearEditValue will clear the editValue and any validation errors for
                    // the field
                    // Pass the additional 3rd parameter to avoid refreshing the updated cells

                    this.clearEditValue(vals, currentFieldName, true);
                }
            }
        }

        fieldsDropped = isc.getKeys(fieldsDropped);
        if (fieldsDropped.length > 0) {
            this.logInfo("'setFields()' removed the following fields which had pending edit " +
                         "values for some row[s]: '" + fieldsDropped.join("', '") + "'.\n" +
                         "Edit values for these fields have been dropped.", "gridEdit");
        }
    }

    // re-show the editor, with the new set of fields
    if (editorShowing) {
        // if the previous edit field is still showing, just re-show the editor
        // and focus silently if appropriate.
        if (editFieldStillPresent) {
            // 2nd and 3rd parameters are both false - even though this field may be in a
            // new position, it's the same logical field, so we don't want to fire
            // editorEnter()
            this.showInlineEditor(editRowNum, newEditColNum, false, false, true);
            if (hadFocus) editForm._focusInItemWithoutHandler(newEditFieldName);
        } else {
            // If we've killed the previous edit field, but still want to be editing, use
            // _startEditing() to start editing the new cell to ensure editValues get set up
            // if necessary
            this._startEditing(editRowNum, newEditColNum, !hadFocus);
        }

        // Refocus without firing the handler if the old edit field is still around and had
        // focus before the setFields.
        if (hadFocus) {
            var newEditFieldName = this.getEditFieldName();
            if (newEditFieldName == oldEditFieldName) {
                editForm._focusInItemWithoutHandler(newEditFieldName);
            } else {
                editForm.focusInItem(newEditFieldName)
            }
        }
    }

    // Re-run the logic to apply hilites (that method stores information on record and
    // field objects).

    if (this.hilites != null) this.applyHilites();

    // Remember the fieldState when setFields is first called.
    // If "setFieldState()" is called with a partial fieldState that doesn't include all fields,
    // we'll reapply the default field state for
    // any fields that are not explicitly listed in the new state.
    // That is implemented in DBC.setFieldState
    // _suppressStoreFieldState flag used to ensure 'setFieldState()' doesn't also remember
    // that field state as the default

    if (!this._suppressStoreFieldState) this.defaultFieldState = this.getFieldState();

    // Set view state if necessary
    if (this.viewState) {
        var viewState = this.viewState;
        // Only do this the very first time through
        this.viewState = null;

        if (fieldStateAlreadySet) {
            // Don't overwrite fieldState set earlier
            var state = this.evalViewState(viewState, "viewState");
            if (state) {
                delete state.field;
            }
            viewState = "(" + isc.Comm.serialize(state,false) + ")";
        }
        if (sortStateAlreadySet) {
            // Don't overwrite sortState set earlier
            var state = this.evalViewState(viewState, "viewState");
            if (state) {
                delete state.sort;
            }
            viewState = "(" + isc.Comm.serialize(state,false) + ")";
        }
        if (groupStateAlreadySet) {
            // Don't overwrite groupState set earlier
            var state = this.evalViewState(viewState, "viewState");
            if (state) {
                delete state.group;
            }
            viewState = "(" + isc.Comm.serialize(state,false) + ")";
        }
        this.setViewState(viewState);
    }

},

// override add/removeField to account for us having 'this.completeFields' which
// dataBoundComponent doesn't expect
addField : function (field, index) {
    return this.Super("addField", [field, index, this.completeFields], arguments);
},
removeField : function (field) {
    return this.Super("removeField", [field, this.completeFields], arguments);
},

// Helper method - should this grid show the special checkbox field for
// selectionAppearance:"checkbox". TreeGrid's show the checkbox in the
// special tree field so we don't show this field.
_$checkbox:"checkbox",
shouldShowCheckboxField : function () {
    if (this.fieldSourceGrid) return this.fieldSourceGrid.shouldShowCheckboxField();
    return (this.selectionAppearance == this._$checkbox &&
        this.selectionType != this._$none &&
        !isc.isA.TreeGrid(this));
},

//> @method listGrid.focusInFilterEditor()
// If the filter editor (+link{listGrid.showFilterEditor}) is visible for this grid,
// this method will explicitly put focus into the specified field in the filter editor.
// @group filterEditor
// @visibility external
// @param [fieldName] (String) Name of the field to put focus into. If unspecified focus will go
//                             to the first field in the editor
//<
focusInFilterEditor : function (fieldName) {
    if (this.filterEditor == null) return;
    var fieldNum = fieldName != null ? this.getColNum(fieldName) : null;
    this.filterEditor.startEditing(0, fieldNum);
},

//> @method listGrid.filterByEditor()
// If the filter editor (+link{listGrid.showFilterEditor}) is visible for this grid,
// this method will perform a filter based on the current values in the editor.
// @group filterEditor
// @visibility external
//<
filterByEditor : function () {
    if (this.filterEditor != null) this.filterEditor.performAction();
},


// Override bindToDataSource. Unlike other data-bound widgets if this.showDetailFields is true
// we want to default our detailFields to be hidden (but accessable via the headerContextMenu)
// Note: We do this at init (setFields) time by setting the showIf property on the field,
// rather than overriding fieldShouldBeVisible() to return false for detail fields, so that
// when showField() is called on the field, that method can return true and allow the field
// to show.
bindToDataSource : function (fields, componentIsDetail, a,b,c,d) {
    var numericFieldName = false;

    var completeFields = this.invokeSuper(isc.ListGrid, "bindToDataSource",
                                          fields, componentIsDetail, a,b,c,d);
    if (this.showDetailFields && completeFields != null) {
        for (var i = 0; i < completeFields.length; i++) {
            var field = completeFields[i];
            if (field.showIf == null && field.detail == true) {
                field.showIf = this._$false;
            }

            if (this._isNumberOrArrayProp(field.name)) {
                numericFieldName = true;
            }
            if (this._isFieldObject(field)) {
                field._isFieldObject = true;
            }
        }
    }
    this._noNumericFields = !numericFieldName;
    return completeFields;
},

// should we apply the "_isFieldObject" marker to this field object?
// this is used by the column picker submenu to identify "true fields" as opposed to auto-generated
// columns such as the removeField

_isFieldObject : function (field) {
    if (field == null) return false;
    return (!field.isRemoveField && !field._isRowNumberField &&
            !field._isExpansionField && !field._isCheckboxField);

},

// Check whether a field name is a number
// *and* whether it's some property present on the fields array such as "size", etc
_$emptyArr:[],
_isNumberOrArrayProp : function (name) {
    if (this._$emptyArr[name] != null) return true;
    if (isc.isA.Number(parseInt(name)) &&
        parseInt(name).toString() == name) return true;
    return false;
},

// Field State
// --------------------------------------------------------------------------------------------
// The fieldState is an object capturing presentation information about the fields -
// expected to be used as a way for developers to save the current presentation (EG in cookies)
// and re-load that presentation when the page is reloaded.
// fieldState is an opaque format.



//> @type ListGridFieldState
// An object containing the stored presentation information for the fields of a listGrid.
// Information contained in a <code>ListGridFieldState</code> object includes the
// visibility and widths of the listGrid's fields.<br>
// Note that this object is a JavaScript string, and may be stored (for example) as a blob
// on the server for state persistence across sessions.
//
// @group viewState
// @visibility external
//<

//> @attr listGrid.fieldState (String : null : IRW)
// Initial +link{ListGridFieldState, field state} for the grid.
// <p>
// +link{viewState} can be used to initialize all view properties of the grid.
// When doing so, <code>fieldState</code> is not needed because <code>viewState</code>
// includes it as well. If both are provided, <code>fieldState</code> has priority for
// field state.
// <smartclient>
// <P>
// To retrieve current state call +link{ListGrid.getFieldState,getFieldState}.
// </smartclient>
//
// @group viewState
// @visibility external
//<

//>    @method    listGrid.getFieldState()
// Returns a snapshot of the current presentation of this listGrid's fields as
// a +link{type:ListGridFieldState} object.
// <P>
// This object can later be passed to +link{listGrid.setFieldState()} to reset this grid's
// fields to the current state.
// <P>
// Note that the information stored includes the current width and visibility of each of this
// grid's fields, as well as any +link{listGrid.canAddFormulaFields,formula} or
// +link{listGrid.canAddSummaryFields,summary fields} added by the user.
//
// @group viewState
// @see listGrid.setFieldState();
// @visibility external
// @return (ListGridFieldState) current state of this grid's fields.
//<
// LG.getFieldState() moved up to DBC

//>    @method    listGrid.setFieldState()
// Sets some presentation properties (visibility, width, userFormula and userSummary) of the
// listGrid fields based on the +link{type:ListGridFieldState} object passed in.<br>
// Used to restore previous state retrieved from the grid by a call to +link{listGrid.getFieldState()}.
//
// @group viewState
// @param fieldState (ListGridFieldState) state to apply to the listGrid's fields.
// @visibility external
// @see listGrid.getFieldState()
//<
setFieldState : function (fieldState) {
    //!OBFUSCATEOK
    if (this.completeFields == null) this.setFields(this.fields);

    if (fieldState == null && this.fieldState != null) {
        if (isc.isA.String(this.fieldState)) {
            fieldState = this.evalViewState(this.fieldState, "fieldState")
        } else {
            fieldState = this.fieldState;
        }
        this.completeFields = this._setFieldState(fieldState);
        // fieldState is init-only property, so null after use
        this.fieldState = null;
        return;
    }

    fieldState = this.evalViewState(fieldState, "fieldState")
    if (fieldState) {
        this.completeFields = this._setFieldState(fieldState);
        this.refreshFields();
    }
},


handleFieldStateChanged : function (preserveSelection) {

    this.fieldStateChanged();
    this.handleViewStateChanged();
    if (this.canSelectCells && !preserveSelection) {
        this.selection.deselectAll();
        if (this.fields && this.fields.length != this.selection.numCols) {
            this.selection.numCols = this.fields.length;
        }
    }
},

// fieldStateChanged implemented in DBC

// Override setHilites to fire the viewStateChanged notification
setHilites : function () {
    this.Super("setHilites", arguments);
    this.handleViewStateChanged();
},

// Override _handleHilitesChanged to fire the hilitesChanged notification.
_handleHilitesChanged : function (hilites) {
    this.Super("_handleHilitesChanged", arguments);
    if (isc.isA.Function(this.hilitesChanged)) this.hilitesChanged();
},

// Override redrawHilites to redraw the body rather than the entire grid.
redrawHilites : function () {
    this._markBodyForRedraw("redrawHilites");
},


// ---------------------------------------------------------------------------------------

//> @type SelectionAppearance
// How data selection should be presented to the user.
// @value "rowStyle" selected rows should be shown with different appearance - see
//                   +link{ListGrid.getCellStyle()} and optionally
//                   +link{ListGrid.selectionCanvas}.
// @value "checkbox" an extra, non-data column should be automatically added to the ListGrid,
//                   showing checkboxes that can be toggled to select rows.
//                   See +link{listGrid.getCheckboxField()}.
// @visibility external
//<

// defaults for checkbox field
checkboxFieldDefaults: {
    name: "_checkboxField",
    excludeFromState:true,
    canEdit: false,
    shouldPrint:false,
    // Note we could probably allow filtering by this field for client-side grids - which would
    // allow you to view all selected / unselected rows easily, (nice feature) but it wouldn't
    // work for databound grids
    canFilter:false,
    canGroupBy: false,
    canSort: false,
    canExport: false,
    canHide: false,
    canReorder: false,
    canDragResize: false,
    // make this special field canHilite false so we don't see it in HiliteEditors by default
    canHilite: false,
    _isCheckboxField: true,
    type:"boolean",
    showDefaultContextMenu: false,
    hoverHTML: "return null;", // supress hover at row level, only hovering for header
    autoFreeze: true,
    showGridSummary: false,
    showGroupSummary: false,
    summaryValue: "&nbsp;",
    title: isc.nbsp,
    // disable this from ever being assigned as the treeField
    treeField:false
},

//> @method listGrid.getCheckboxField()
// Returns the specially generated checkbox field used when +link{selectionAppearance} is
// "checkbox".
// <P>
// Called during +link{setFields()}, this method can be overridden to add advanced dynamic
// defaults to the checkbox field (call Super, modify the default field returned by Super,
// return the modified field).  Normal customization can be handled by just setting
// +link{AutoChild} properties, as mentioned under the docs for +link{listGrid.checkboxField}.
//
// @return (ListGridField)
// @group checkboxField
// @visibility external
//<
getCheckboxField : function () {
    var grid = this,
        cbField = {
        // default the width to the width of the icon plus an arbitrary buffer
            width:this.checkboxFieldHSpace + this._getCheckboxFieldImageWidth(),
            getAutoFreezePosition: function () { return grid.getCheckboxFieldPosition() }
        }
    ;
    isc.addProperties(cbField, this.checkboxFieldDefaults, this.checkboxFieldProperties);

    cbField.title = (this.canSelectAll == false || this.selectionType == "single" ? isc.nbsp :
        this._getCheckboxValueIconHTML(false, true, false, cbField));

    return cbField;
},

//> @attr listGrid.checkboxFieldHSpace (int : 15 : IR)
// How much horizontal space should the +link{getCheckboxField(),checkbox field} leave
// around the checkbox icon when +link{listGrid.selectionAppearance} is set to
// <code>"checkbox"</code>?
// <P>
// The automatically generated checkbox field will be sized to the width of the
// checkbox icon (specified via +link{listGrid.checkboxFieldImageWidth} or
// +link{listGrid.booleanImageWidth}) plus this value.
// @group checkboxField
// @visibility external
//<
checkboxFieldHSpace:15,

getCurrentCheckboxField : function () {
    var fields = this.completeFields || this.fields;
    if (!fields) return null;
    var cbFields = fields.find(this.fieldIdProperty, "_checkboxField");
    return !cbFields ? null : isc.isAn.Array(cbFields) ? cbFields[0] : cbFields;
},

_getCheckboxFieldImageWidth : function () {
    return this.checkboxFieldImageWidth || this.booleanImageWidth ||
            (isc.CheckboxItem ? isc.CheckboxItem.getInstanceProperty("valueIconWidth") : null);
},
_getCheckboxFieldImageHeight : function () {
    return this.checkboxFieldImageHeight || this.booleanImageHeight ||
            (isc.CheckboxItem ? isc.CheckboxItem.getInstanceProperty("valueIconWidth") : null);
},

//> @method listGrid.isCheckboxField()
// Identifies whether the passed-in field is the specially generated
// +link{listGrid.checkboxField,checkboxField} used when +link{selectionAppearance} is
// "checkbox".  Use this method in your custom event handlers to avoid inappropriately
// performing actions when the checkboxField is clicked on.
//
// @param field (ListGridField) field to test
// @return (Boolean) whether the provided field is the checkbox field
// @group checkboxField
// @visibility external
//<
isCheckboxField : function (field) {
    if (!field || !field._isCheckboxField) return false;
    else return true;
},

// helper function to get the checkbox field position
// This is a special field which appears "stuck to" the left edge of the grid.
// We have a few other special fields:
// - rowNumbers column
// - groupTitle column when showing group summaries in header
// - record expansion icon column
getCheckboxFieldPosition : function () {
    if (this.fieldSourceGrid) return this.fieldSourceGrid.getCheckboxFieldPosition();
    if (this.selectionAppearance != "checkbox" || isc.isA.TreeGrid(this)) return -1;

    var pos = 0;
    if (this.shouldShowRowNumberField()) pos++;
    return pos;
},

//> @type ListGridSelectedState
// An object containing the stored selection information for a listGrid.
// Note that this object is not intended to be interrogated directly, but may be stored
// (for example) as a blob on the server for state persistence across sessions.
//
// @group viewState
// @visibility external
//<
// ListGridSelectedState object is implemented as an array of primaryKeys indicating the
// selected set of records.

//>    @method    listGrid.getSelectedState()
// Returns a snapshot of the current selection within this listGrid as
// a +link{type:ListGridSelectedState} object.<br>
// This object can be passed to +link{listGrid.setSelectedState()} to reset this grid's current
// selection state (assuming the same data is present in the grid).<br>
// @group viewState
// @see listGrid.setSelectedState();
// @visibility external
// @return (ListGridSelectedState) current state of this grid's selection
//<
getSelectedState : function (supressWarnings, returnObject) {
    if (!this.selection) return null;
    if (!this.dataSource ||
        isc.isAn.emptyObject(this.getDataSource().getPrimaryKeyFields()))
    {
        if (!supressWarnings) {

            this.logWarn("can't getSelectedState without a DataSource " +
                         "with a primary key field set");
        }
        return null;
    }

    var selectedState = [],
        selection;

    if (this.canSelectCells) {
        selection = this.selection.getSelectedCells();

        // store primary keys and cell-range.  Works only with a DataSource
        for (var i = 0; i < selection.length; i++) {
            var cell = selection[i],
                PKs = this.getPrimaryKeys(cell[0]),
                entry = null
            ;
            for (var k=0; k<selectedState.length; k++) {
                if (this.objectsAreEqual(PKs, selectedState[k].PKs)) {
                    entry = selectedState[k];
                    break;
                }
            }
            if (!entry) {
                selectedState.add({ PKs: PKs, cells: [] });
                entry = selectedState[selectedState.length-1];
            }
            entry.cells.add(cell[1])
        }
    } else {

        selection = this.getSelection() || [];

        // store primary keys only.  Works only with a DataSource
        for (var i = 0; i < selection.length; i++) {
            selectedState[i] = this.getPrimaryKeys(selection[i]);
        }
    }

    // returnObject is undocumented and passed when called from getViewState() - return the
    // object to avoid double escaping
    if (returnObject) return selectedState;

    return isc.Comm.serialize(selectedState,false);
},


//> @method listGrid.getCellSelection()
// When +link{canSelectCells} is active, returns the +link{CellSelection} object that tracks
// and manages the current selection.  Returns null if +link{canSelectCells} is false.
// @return (CellSelection) current cellSelection
// @visibility external
//<
getCellSelection : function () {
    if (!this.canSelectCells) return null;
    return this.getSelectionObject();
},

//>    @method    listGrid.setSelectedState()
// Reset this grid's selection to match the +link{type:ListGridSelectedState} object passed in.<br>
// Used to restore previous state retrieved from the grid by a call to
// +link{listGrid.getSelectedState()}.
//
// @group viewState
// @param selectedState (ListGridSelectedState) Object describing the desired selection state of
//                                              the grid
// @see listGrid.getSelectedState()
// @visibility external
//<
setSelectedState : function (selectedState) {

    selectedState = this.evalViewState(selectedState, "selectedState")
    if (!selectedState || selectedState.length == 0) {
        if (this.selection) this.selection.deselectAll();
        return;
    }

    if (selectedState[0].PKs && !this.canSelectCells) {
        if (this.selection) this.selection.deselectAll();
        this.logWarn("Cannot restore CellSelection state to a " +
            "ListGrid with canSelectCells: false.");
        return;
    } else if (!selectedState[0].PKs && this.canSelectCells) {
        if (this.selection) this.selection.deselectAll();
        this.logWarn("Cannot restore Selection state to a " +
            "ListGrid with canSelectCells: true.");
        return;
    }

    var selection = this.selection,
        data = this.originalData || this.data;

    // ignore calls at an invalid time (data still loading or cache just invalidated)
    if (isc.ResultSet && isc.isA.ResultSet(data) && !data.lengthIsKnown()) return;

    if (data && selection) {
        selection.deselectAll();

        var records = [];

        for (var i = 0; i < selectedState.length; i++) {
            var item = selectedState[i]
            // resultSet.indexOf() looks up by matching PK values
            var PKs = selectedState[i];

            if (isc.isAn.Object(item) && item.cells != null) {
                // with cell-selection, we store an object with aPKs object an a cells array
                PKs = selectedState[i].PKs;
            }
            var index = data.findByKeys(PKs, this.getDataSource());
            // record may have been removed
            if (index != -1) {
                if (this.canSelectCells) {
                    // build an array of cell-reference arrays in the format [rowNum,colNum]
                    var cells = item.cells;
                    for (var k=0; k<cells.length; k++) {
                        records.add([index, cells[k]]);
                    }
                } else {
                    records.add(data.get(index));
                }
            }
        }
        if (this.canSelectCells) {
            this.selection.selectCellList(records);
        } else {
            this.selection.selectList(records);
        }
    }
},

//> @type ListGridSortState
// An object containing the stored sort information for a listGrid.
// Note that this object is not intended to be interrogated directly, but may be stored
// (for example) as a blob on the server for state persistence across sessions.
//
// @group viewState
// @visibility external
//<
// ListGridSortState object is implemented as a simple JS object containing fieldName and sortDir
// attributes - also now supports the multiSorting subsystem by storing the grid's list of
// sortSpecifiers

//> @attr listGrid.sortState (ListGridSortState: null : IRW)
// Initial sort state for the grid.
// <P>
// +link{listGrid.viewState} can be used to initialize all view properties of the grid.
// When doing so, <code>sortState</code> is not needed because <code>viewState</code>
// includes it as well. If both are provided, <code>sortState</code> has priority for
// sort state.
// <smartclient>
// <P>
// To retrieve current state call +link{ListGrid.getSortState,getSortState}.
// </smartclient>
//
// @group viewState
// @visibility external
//<

//>    @method    listGrid.getSortState()
// Returns a snapshot of the current sort state within this listGrid as
// a +link{type:ListGridSortState} object.<br>
// This object can be passed to +link{listGrid.setSortState()} to reset this grid's sort to
// the current state (assuming the same fields are present in the grid).<br>
// @group viewState
// @see listGrid.setSortState();
// @visibility external
// @return (ListGridSortState) current sort state for the grid.
//<
getSortState : function (returnObject) {

    if (this.logIsInfoEnabled("sorting")) {
        this.logInfo("\n"+
            "grid.sortFieldNum is: "+this.sortFieldNum+"\n"+
            "grid.sortField is: "+this.sortField+"\n"+
            "grid.getField(grid.sortFieldNum) is:\n"+
                isc.echoAll(this.getField(this.sortFieldNum))+"\n"+
            "-----------------------------------------\n"+
            "grid._getSortFieldNum() is: "+this._getSortFieldNum()+"\n"+
            "grid.getField(grid._getSortFieldNum()) is:\n"+
                isc.echoAll(this.getField(this._getSortFieldNum()))+"\n"+
            "", "sorting"
        )
    }

    var sortFieldNum = this._getSortFieldNum(),
        sortField = (sortFieldNum != null ? this.getField(sortFieldNum) : null),
        sortFieldName = sortField != null ? this.getFieldName(sortField) : null,
        sortDir = this._getFieldSortDirection(sortField),
        sortState = {fieldName:sortFieldName, sortDir:sortDir}
    ;

    if (this._sortSpecifiers && this._sortSpecifiers.length > 0) {
        var specifiers = isc.shallowClone(this._sortSpecifiers);
        // remove some props added during sorting - keeps the output neat and they'll be
        // re-applied on setSort()
        specifiers.clearProperty("primarySort");
        specifiers.clearProperty("sortIndex");
        specifiers.clearProperty("normalizer");
        specifiers.clearProperty("context");
        specifiers.clearProperty("_comparator");
        sortState.sortSpecifiers = specifiers;
    }

    // returnObject is undocumented and passed when called from getViewState() - return the
    // object to avoid double escaping
    if (returnObject) return sortState;

    // eval() of a string containing object literal text will js error - enclose in "(" ... ")" to
    // avoid this.
    return "(" + isc.Comm.serialize(sortState,false) + ")";
},


//>    @method    listGrid.setSortState()
// Reset this grid's sort state (sort field and direction or list of
// +link{sortSpecifier}s) to match the
// +link{type:ListGridSortState} object passed in.<br>
// Used to restore previous state retrieved from the grid by a call to
// +link{listGrid.getSortState()}.
//
// @param sortState (ListGridSortState) Object describing the desired sort state for the grid.
// @group viewState
// @see listGrid.getSortState()
// @visibility external
//<
setSortState : function (state) {
    state = this.evalViewState(state, "sortState")
    if (!state) {
        this.clearSort();
        return;
    }

    if (state.sortSpecifiers) {
        // multisort
        this.setSort(isc.shallowClone(state.sortSpecifiers));
    } else if (state.fieldName == null) {
        this.clearSort();
    } else {
        // will only get here now if the user has legacy state or creates their own state
        // object which doesn't include sortSpecifiers - a sortSpecifier will be created
        // anyway and further calls to getSortState() will include it
        var fieldNum = this.getFieldNum(state.fieldName)
        if (fieldNum != -1) this.sort(fieldNum, state.sortDir);
    }
},

//> @type ListGridViewState
// An object containing the "view state" information for a listGrid.<br>
// This object contains state information reflecting<br>
// - +link{type:ListGridFieldState}<br>
// - +link{type:ListGridSortState}<br>
// - +link{type:ListGridSelectedState}<br>
// for the grid.<br>
// Note that this object is a JavaScript string, and may be stored (for example) as a blob
// on the server for state persistence across sessions.
//
// @group viewState
// @visibility external
//<
// ListGridViewState object is implemented as a simple JS object containing the following
// fields:
// - selected [a ListGridSelectedState object]
// - field [a ListGridFieldState object]
// - sort [a ListGridSortState object]

//>    @method    listGrid.getViewState()
// Returns a snapshot of the current view state of this ListGrid.<br>
// This includes the field, sort, hilite, group, and selected state of the grid, returned
// as a string representation of a +link{type:ListGridViewState} object.<br>
// This string can be passed to +link{listGrid.setViewState()} to reset this grid's view state
// to the current state (assuming the same data / fields are present in the grid).<br>
// @group viewState
// @see type:ListGridViewState
// @see listGrid.setViewState();
// @visibility external
// @return (ListGridViewState) current view state for the grid.
//<
getViewState : function (returnObject) {


    var state = {
        selected: this.getSelectedState(true, true),
        field: this.getFieldState(true),
        sort: this.getSortState(true),
        hilite: this.getHiliteState(true),
        group: this.getGroupState(true)
    };

    // Available so TG can call Super() and get an object back
    if (returnObject) return state;
    return "(" + isc.Comm.serialize(state,false) + ")";
},

//> @type ListGridGroupState
// An object containing the stored grouping information for a listGrid.
// Note that this object is not intended to be interrogated directly, but may be stored
// (for example) as a blob on the server for state persistence across sessions.
//
// @group viewState
// @visibility external
//<
// ListGridGroupState object is implemented as a serialized array of objects specifying
// fieldName, groupingMode, granularity.
// We also support a simple comma separated string (the older format) for backcompat

//> @attr listGrid.groupState (ListGridGroupState: null : IRW)
// Initial group state for the grid.
// <P>
// +link{listGrid.viewState} can be used to initialize all view properties of the grid.
// When doing so, <code>groupState</code> is not needed because <code>viewState</code>
// includes it as well. If both are provided, <code>groupState</code> has priority for
// group state.
// <smartclient>
// <P>
// To retrieve current state call +link{ListGrid.getGroupState,getGroupState}.
// </smartclient>
//
// @group viewState
// @visibility external
//<

//>    @method    listGrid.getGroupState()
// Returns a snapshot of the current grouping state of this ListGrid.<br>
// This object can be passed to +link{listGrid.setGroupState()} to reset this grid's grouping
// to the current state (assuming the same data / fields are present in the grid).<br>
// @group viewState
// @see type:ListGridGroupState
// @see listGrid.setGroupState();
// @visibility external
// @return (ListGridGroupState) current view state for the grid.
//<
getGroupState : function (returnObject) {
    var i, field, states, groupFields = this.getGroupByFields();
    if (groupFields == null) {
       return "";
    } else {
        states = [];
        for (i = 0; i < groupFields.length; i++) {
            var allFields = this.completeFields || this.fields || [];
            field = allFields.find("name", groupFields[i]);
            if (field) {
                states.add({
                    fieldName : groupFields[i],
                    groupingMode : field.groupingMode,
                    groupGranularity : field.groupGranularity,
                    groupPrecision : field.groupPrecision
                });
            }
        }
    }

    // returnObject is undocumented and passed when called from getViewState() - return the
    // object to avoid double escaping
    if (returnObject) return states;

    // eval() of a string containing object literal text will js error - enclose in "(" ... ")"
    // to avoid this.
    return "(" + isc.Comm.serialize(states,false) + ")";

},

//>    @method    listGrid.setGroupState()
// Reset this grid's grouping to match the +link{type:ListGridGroupState} object passed in.<br>
// Used to restore previous state retrieved from the grid by a call to
// +link{listGrid.getGroupState()}.
//
// @group viewState
// @param groupState (ListGridGroupState) Object describing the desired grouping state of
//                                              the grid
// @see listGrid.getGroupState()
// @visibility external
//<
setGroupState : function (state) {
    var i, field, fields;

    if (state) {
        if (isc.isA.String(state)) {
            if (!state.startsWith("(")) {
                // Backcompat - handle the state being stored as a comma separated string of fieldNames
                this.groupBy(state.split(","));
                return;
            } else {
                state = this.evalViewState(state, "groupState")
            }
        }

        fields = [];
        for (i = 0; i < state.length; i++) {
            field = this.completeFields.find("name", state[i].fieldName);
            if (field) {
                fields.push(state[i].fieldName);
                field.groupingMode = state[i].groupingMode;
                field.groupGranularity = state[i].groupGranularity;
                field.groupPrecision = state[i].groupPrecision;
            }

        }

        this.groupBy(fields);
    } else {
        this.ungroup();
    }
},

//> @attr listGrid.viewState (String : null : IRW)
// Initial +link{ListGridViewState, view state} for the grid.
// <P>
// Since view state contains field state it is not necessary
// to set +link{fieldState,fieldState} when also setting <code>viewState</code>.
// If both are provided, <code>fieldState</code> has priority for field state.
// <smartclient>
// <P>
// To retrieve current state call +link{ListGrid.getViewState,getViewState}.
// </smartclient>
//
// @group viewState
// @visibility external
//<

//>    @method    listGrid.setViewState()
// Reset this grid's view state to match the +link{type:ListGridViewState} object passed in.<br>
// Used to restore previous state retrieved from the grid by a call to
// +link{listGrid.getViewState()}.
//
// @param viewState (ListGridViewState) Object describing the desired view state for the grid
// @group viewState
// @see listGrid.getViewState()
// @visibility external
//<
setViewState : function (state) {
    state = this.evalViewState(state, "viewState")
    if (!state) return;

    var fetchValueMap = this._fetchValueMap;

    // Order is somewhat important - for example show fields before potentially sorting
    // by them, etc
    if (state.field) this.setFieldState(state.field);
    this.setSortState(state.sort);
    this.setGroupState(state.group);
    this.setHiliteState(state.hilite);
    this.setSelectedState(state.selected);

    if (fetchValueMap && !this.isDrawn()) {

        this._fetchValueMap = true;
    }
},

// Group state changed. Fire both "groupStateChanged" and "viewStateChanged".
// These methods documented under stringMethods section.
handleGroupStateChanged : function () {
    this.groupStateChanged();
    this.handleViewStateChanged();
},
groupStateChanged : function () {},

// Fire the viewStateChanged notification. This is documented in registerStringMethods()

handleViewStateChanged : function () {
    this.fireOnPause("viewStateChangeNotification", {target:this,methodName:"viewStateChanged"}, 0);
},
getViewStateChangedFunction : function () {
    if (this._viewStateChangedFunction == null) {
        var grid = this;
        this._viewStateChangedFunction = function () {
            if (grid.destroyed) return;
            grid.viewStateChanged();
        };
    }
    return this._viewStateChangedFunction;
},
viewStateChanged : function() {
},

//> @method listGrid.setDataSource()
// @include dataBoundComponent.setDataSource()
// @visibility external
//<
// Override setDataSource() - we need to reset the stored filter criteria in case we are
// showing the filterEditor.
setDataSource : function (dataSource, fields) {
    // If we have a specified groupByField, ungroup when changing from one DataSource to another.
    // *Don't ungroup if we didn't have a dataSource before this method ran - this is likely to
    // happen on init when a developer can validly set groupBy before setting dataSource.

    var currentDS = this.getDataSource();
    if (currentDS != null && currentDS != dataSource && currentDS.ID != dataSource) {
        this.ungroup();
    }

    // discard edits as they don't apply to the new DataSource records


    this.discardAllEdits();
    // The edit row form has fields as defined in the previous datasource. Drop the form
    // so it will be recreated on the next edit attempt.
    if (this._editorShowing) this.hideInlineEditor();
    if (this._editRowForm) {
        this._editRowForm.destroy();
        delete this._editRowForm;
    }

    this.Super("setDataSource", arguments);
    this.clearFilterValues();
},


// determine which fields should be shown, and add them to the visible fields array.
// (Used as an internal helper - developers should call 'refreshFields' instead)
deriveVisibleFields : function () {
    // NOTE: we use setArray() so that this.fields remains the same array instance.
    this.fields.setArray(this.getVisibleFields(this.completeFields));
    delete this.fields._appliedInitialAutoFitWidth

    this.deriveFrozenFields();
    this.refreshMasterIndex();
},


//> @method ListGrid.refreshFields
// Re-evaluates +link{ListGridField.showIf} for each field, dynamically showing and
// hiding the appropriate set of fields
// @visibility external
//<
refreshFields : function () {
    // nothing to do if external code calls this method before setFields/draw
    if (!this.completeFields) return;
    // Just fall through to 'setFields()' for now

    this._suppressStoreFieldState = true;
    this.setFields(this.completeFields);
    delete this._suppressStoreFieldState;
},

getFieldWidths : function (reason) {
    // o appropriate time to get field widths:
    // LG w/ header:
    // - initially: right after we draw the header
    // - after setFields() header rebuilt: right after we rebuild the header
    // LG w/o header:
    // - initially: anytime before the body is drawn
    // - after setFields(): anytime before the body is redrawn

    var sizes = this._getCalculatedFieldWidths();
    // When autoFitFieldWidths is true, we may need to tweak these values to either
    // fill the available space, or clip certain fields if we're overflowing the
    // available space.
    // On drag-resize we set autoFitWidth to false on the field being dragged.
    // However we also want to suppress other fields' resizing (expanding to fill viewport
    // etc/ clipping to avoid h-scrollbars) or we'll get strange interactions with drag
    // similar to the problems if you re-evaluated "*" widths during drag
    if (this.autoFitFieldWidths && !this._calculatingAutoFitFieldWidths
        && !this._dragResizingField )
    {
        this._calculatingAutoFitFieldWidths = true;
        var unfrozenWidths = sizes.duplicate(),
            frozenWidths = null;

        if (this.frozenFields != null) {
            var left = this.freezeStart();
            if (left) {
                frozenWidths = unfrozenWidths.slice(0, this.frozenFields.length);
                unfrozenWidths = unfrozenWidths.slice(this.frozenFields.length);
            } else {
                frozenWidths = unfrozenWidths.slice(-this.frozenFields.length);
                unfrozenWidths = unfrozenWidths.slice(0, -this.frozenFields.length);
            }
        }

        var availableSpace = this.getAvailableFieldWidth(true),
            totalSize = unfrozenWidths.sum();

        var unfrozenSpace = availableSpace;
        if (frozenWidths != null) unfrozenSpace -= frozenWidths.sum();



        // Case 1: the fields don't fill the available space.
        // Expand the autoFitExpandField to fill the available space in the body.
        // Note: We don't auto-expand frozen fields - that would require resizing the
        // frozen body as well. The getAutoFitExpandField() method already handles not
        // returning frozen fields.
        // NOTE: If we're fitting to data, but data is currently loading, don't expand a field
        // now - wait until we redraw with loaded data. Otherwise we don't really know the
        // rendered sizes of all fields, so we won't know how much to expand the expansion field
        // by. Then when data arrives and the other fields all resize, we end up rendering the
        // expansion field potentially too wide since the other fields may now overflow available
        // space.

        var validData = true;
        if (this.autoFitWidthApproach != "title") {
            var dA = this.getDrawArea();
            // drawArea may be null if the body hasn't yet been created.
            if (!this.data || dA == null || Array.isLoading(this.data.get(dA[0]))) {
                validData = false;
            }
        }


        if (totalSize < unfrozenSpace && validData) {

            var expandField = this.getAutoFitExpandField();
            if (expandField) {
                // we want to update the sizes array (includes both frozen and
                // unfrozen fields) so get the global fieldNum for the expand field
                // and update that value.
                var expandFieldNum = this.getFieldNum(expandField);
                var diff = unfrozenSpace - totalSize;
                sizes[expandFieldNum] += diff;

                // If we're showing a header for the field we have to resize
                // the button too so it stays in synch with the sizes array we
                // return (which will be applied to the body)
                var button = this.getFieldHeaderButton(expandFieldNum);
                if (button && button.isDrawn()) {
                    button.setWidth(sizes[expandFieldNum]);
                }
            }
        // case 2: the auto-fit fields are overflowing the available space, clip them
        // if appropriate.
        // Note: we don't clip frozen fields - that would require resizing the actual bodies.
        } else if (totalSize > unfrozenSpace && this.autoFitClipFields != null) {
            // If any autoFitFields are marked as clippable, and we're now overflowing
            // horizontally, we want to re-run stretchResize logic ignoring
            // calculated autoFit for those fields -- just let them take up the available
            // space.
            // If we're not showing a header this is easy
            // If we are (and autoFitWidthApproach includes the header title), we need to
            // have the header re-run it's logic so it takes account of other (non clippable)
            // fields' title-widths
            // This means reset it's policy to fill, resize our fields to "*", then reflow,
            // reset the policy back to whatever it was before (probably "none")
            var policy;
            var clipFields = this.autoFitClipFields;
            for (var i = 0; i < clipFields.length; i++) {
                var field = this.getField(clipFields[i]),
                    fieldNum = this.getFieldNum(field);
                if (field == null || fieldNum < 0) continue;

                // Don't attempt to clip frozen fields since that would require resizing
               // the frozen body.
                if (!this._suppressedFrozenFields && field.frozen) {
                    this.logInfo("auto-fitting field:" + field.name +
                        " is present in the autoFitClipFields array for this grid, but is" +
                        " currently frozen. This is not supported - the field will not be clipped.",
                        "frozenFields");
                    continue;
                }


                // deleting the calculated autoFitWidth ensures that when
                // _getCalcualtedFieldWidths runs stretchResizePolicy will simply resize
                // the row to fit if possible, or if a header is showing, the header reflow
                //.will achieve the same result.
                delete field._calculatedAutoFitWidth;
                var header = this.getFieldHeader(fieldNum);
                if (header && header.isDrawn()) {
                    button = header.getMember(this.getLocalFieldNum(fieldNum));

                    button.setWidth(field.width || "*");

                    button.setOverflow("hidden");
                }
            }
            if (this.header && this.header.isDrawn()) {
                var reflowReason = this._$gettingFieldWidths;
                if (reason != null) reflowReason += reason;
                 policy = this.header.hPolicy;
                 this.header.hPolicy = "fill";
                this.header.reflowNow(reflowReason);
                this.header.hPolicy = policy;
                if (this.frozenHeader) {
                    this.frozenHeader.hPolicy = "fill";
                    this.frozenHeader.reflowNow(reflowReason);
                    this.frozenHeader.hPolicy = policy;
                }
            }
//            this.logWarn("after reflow..." + this.header.getMemberSizes());

            // rerun the method to get the stretch-resize calculated widths
            // (will run the appropriate logic based on whether or not we're showing
            // headers
            sizes = this._getCalculatedFieldWidths();
        }
        this._calculatingAutoFitFieldWidths = false;
    }

//     this.logWarn("getFieldWidths() ultimately gave sizes:" + sizes);

    return sizes;

},
_$gettingFieldWidths:"Getting listGrid fieldWidths. ",

// helper for getFieldWidths() - returns the stretch-resize calculated widths
// (based on the header if appropriate).
_getCalculatedFieldWidths : function () {




    var header = this.header;
    if (isc.isA.Layout(header) && header.isDrawn()) {

//          this.logWarn("using header-based field widths");

        // Handle the case where the header has been resized but the buttons are pending
        // a reflow

        if (header._layoutIsDirty) {
            header.reflowNow();
        }

        // Force an immediate redraw of any dirty buttons.
        // This is required to ensure sizes are correct -- if redrawOnResize is true for
        // the button or label with overflow:"visible", and setWidth() is called on it
        // the redraw isn't immediate - we need to force a redraw now if it hasn't occurred
        // so getVisibleWidth() returns the new size.
        var buttons = header.members;
        for (var i = 0; i < buttons.length; i++) {
            if (buttons[i].isDirty()) buttons[i].redraw();
            if (buttons[i].label != null && buttons[i].label.isDirty()) buttons[i].label.redraw();


        }

        // derive field widths from header sizes
        var sizes = header.getMemberSizes();
        if (sizes.length > 0) {
            if (this.allowMismatchedHeaderBodyBorder) {

                var leftHeaderBorder = header.getLeftBorderSize() + header.getLeftMargin(),
                    rightHeaderBorder = header.getRightBorderSize() + header.getRightMargin();
                if (leftHeaderBorder != 0) {
                    sizes[0] += leftHeaderBorder;
                }
            }

            var totalSize = sizes.sum(),
                availableSpace = header.getInnerWidth();

            if (this.allowMismatchedHeaderBodyBorder) {
                var overflowAmount = totalSize - availableSpace;
                if (overflowAmount > 0) {
                    sizes[sizes.length-1] += Math.min(overflowAmount, rightHeaderBorder);
                }
            }
        }
        if (this.frozenFields) {
            var frozenWidths = this.frozenHeader.getMemberSizes();
            sizes.addListAt(frozenWidths, this.freezeStart() ? 0 : sizes.length);
        }


    } else {
//         this.logWarn("using stretchResize-based field widths");
        var sizes = this.getStretchResizeWidths();

    }
    return sizes;

},

getStretchResizeWidths : function () {
    if (this.fields == null) return [];

    var widths = this.fields.getProperty("width"),
        autoFitWidths = this.fields.getProperty("_calculatedAutoFitWidth"),
        hasDynamicDefaults = [];

    for (var i = 0; i < widths.length; i++) {

        if (autoFitWidths[i] != null) {

            if (isc.isA.String(widths[i])) {
                hasDynamicDefaults.add(i);
            } else if ((widths[i] == null) || (widths[i] < autoFitWidths[i])) {
                widths[i] = autoFitWidths[i];
            }
        }
    }

    // no header: derive field widths via stretch resize policy on widths set in fields
    var innerWidth = (this.innerWidth != null ? this.innerWidth : this.getAvailableFieldWidth()),
        calculatedWidths = isc.Canvas.applyStretchResizePolicy(
                widths,
                innerWidth,
                this.minFieldSize
                );
    if (hasDynamicDefaults.length > 0) {
        var fieldOverflowed = false;
        for (var i = 0; i < hasDynamicDefaults.length; i++) {
            var j = hasDynamicDefaults[i];
            if (calculatedWidths[j] < autoFitWidths[j]) {
                fieldOverflowed = true;
                widths[j] = autoFitWidths[j];
            }
        }
        if (fieldOverflowed) {
            calculatedWidths = isc.Canvas.applyStretchResizePolicy(
                widths,
                innerWidth,
                this.minFieldSize
            );
        }
    }
    return calculatedWidths;
},

getAvailableFieldWidth : function (specifiedWidth) {

    if (specifiedWidth == null) {
        specifiedWidth = this.autoFitData != "both" && this.autoFitData != "horizontal";
    }

    var width = (!specifiedWidth
                    ? this.getVisibleWidth() : this.getWidth());

    return this._getInnerSpaceFromWidth(width);
},
_getInnerSpaceFromWidth : function (width) {
    width -= this.getHMarginBorderPad();
    // leave a gap for the scrollbar if vertical scrolling is on or if we're configured to
    // always leave a gap
    var leaveGap = this._shouldLeaveScrollbarGap();

    if (leaveGap) {
        width -= this.body ? this.body.getScrollbarSize() : this.getScrollbarSize();
    }
    return Math.max(1, width);
},



//> @method ListGrid.getFieldWidth()
// Returns a numeric value for the width of some field within this <code>ListGrid</code>.
// @param fieldNum (int | String) Index or name of the field for which the width is to be determined.
// @return (Integer) width of the field in px, or <code>null</code> if the width can't be determined.
// @visibility external
//<
getFieldWidth : function (fieldNum) {
    fieldNum = this.getFieldNum(fieldNum);
    if (fieldNum == -1 || !this.fields || fieldNum >= this.fields.length) return null;
    var width;
    if (this.body != null) width = this.getColumnWidth(fieldNum);
    if (width == null || isNaN(width)) width = this.getFieldWidths()[fieldNum];
    return width;
},

_adjustFieldSizesForBodyStyling : function (sizes, vertical) {
    if (vertical == null) vertical = false;

    if (sizes == null || sizes.length == 0) return sizes;


    if (!this.body) {

        return sizes;
    }

    // Adjust sizes of first and last field to account for styling on the body.

    sizes[sizes.length -1] =
        this._adjustLastFieldForBodyStyling(sizes[sizes.length-1], sizes.sum(), vertical);
    sizes[0] = this._adjustFirstFieldForBodyStyling(sizes[0], vertical);

    return sizes;
},

_adjustFirstFieldForBodyStyling : function (size, vertical) {
    if (!this.body) return size;

    // always knock the left (or top) border off the first field, otherwise everything starts
    // shifted too far right (or down)
    var bodyStartAdjust = (vertical ? this.body.getTopBorderSize() + this.body.getTopMargin()
                                    : this.body.getLeftBorderSize() + this.body.getLeftMargin());
    if (bodyStartAdjust != 0) size -= bodyStartAdjust;

    return Math.max(0, size);
},

_adjustLastFieldForBodyStyling : function (size, totalFieldsWidth, vertical) {
    if (!this.body) return size;

    // Figure out whether all the sizes will expand as far or further than the bottom or right
    // edge. If so we want to knock off the end border from the last col or row.
    var bodyEndAdjust = (vertical ? this.body.getBottomBorderSize() + this.body.getBottomMargin()
                                  : this.body.getRightBorderSize() + this.body.getRightMargin());

    if (bodyEndAdjust != 0) {

        var overflowAmount = totalFieldsWidth -
            (vertical ? this.body.getInnerHeight() : this.body.getInnerWidth());
        if (overflowAmount > 0) {
            size -= Math.min(overflowAmount, bodyEndAdjust);
        }
    }
    return Math.max(size,1);
},

setBodyFieldWidths : function (sizes) {


    // set the _fieldWidths array to the list passed in
    this._fieldWidths = sizes;

    // adjust the first / last column width for any left/right border or margin on the body.

    var origSizes = sizes;
    if (this.allowMismatchedHeaderBodyBorder) {
        if (isc.isAn.Array(sizes)) {
            sizes = this._adjustFieldSizesForBodyStyling(sizes.duplicate());
        }
    }

    // if we have frozenFields, separate out the sizes for those columns
    var frozenFields = this.frozenFields;

    if (frozenFields && this.frozenBody) {
        var frozenWidths = this.getFrozenSlots(sizes);
        sizes = this.getUnfrozenSlots(sizes);
        this.frozenBody.setColumnWidths(frozenWidths);

        var freezeWidth = frozenWidths.sum();
        // this will automatically cause the main body to size to fill remaining space
        this.frozenBody.setWidth(freezeWidth);

        this.frozenBody._userWidth = freezeWidth;
        if (this.frozenHeader) this.frozenHeader.setWidth(freezeWidth);
    }

    // give the GridRenderer new fieldWidths
    // will mark the body for redraw.
    if (this.body != null) this.body.setColumnWidths(sizes);
    // If we're showing the edit form, update the widths of the form items

    if (this._editorShowing) {
        var items = this._editRowForm.getItems(),
            record = this.getRecord(this.getEditRow()),

            // This method gives the widths of form items for every column - since
            // we only create a subset of form items, we won't use every one.

            completeFormFieldWidths = this.getEditFormItemFieldWidths(record);

        for (var i = 0; i < items.length; i++) {
            var colNum = items[i].colNum;

            if (items[i].width != completeFormFieldWidths[colNum])
                items[i].setWidth(completeFormFieldWidths[colNum]);
        }
    }
},

//>    @type ListGridComponent
// Standard component-type displayed within a ListGrid, as contained by +link{listGrid.gridComponents}.
// @group appearance
// @value "filterEditor" The standard filter-component displayed when +link{listGrid.showFilterEditor}
//   is true
// @value "header" The header-component displayed when +link{listGrid.showHeader} is true.
// @value "body" The body component for the grid.
// @value "summaryRow" The summary-row component displayed when +link{listGrid.showGridSummary} is
//   true.
// @visibility external
//<

//> @attr listGrid.gridComponents (Array of ListGridComponent | Array of any : (see below) : IR)
// Array of components that make up this grid. This array controls which standard and/or custom
// parts will be displayed within this ListGrid.
// <P>
// ListGrid is a subclass of +link{VLayout} and consists of a number of member components. The
// standard set of members are automatically generated by the grid, and include (for example)
// the header (a Toolbar of buttons for each field) and the body
// (a GridRenderer displaying the actual data contained in the grid).<br>
// The default value of <code>gridComponents</code> is an Array of +link{ListGridComponent}s listing
// the standard components in their default order:
// <smartclient>
// <pre>
//    gridComponents : ["filterEditor", "header",
//                      "body", "summaryRow"]
// </pre>
// </smartclient>
// <smartgwt>
// <pre>
//    [ListGridComponent.FILTER_EDITOR, ListGridComponent.HEADER, ListGridComponent.BODY,
//     ListGridComponent.SUMMARY_ROW]
// </pre>
// </smartgwt>
//
// You can override <code>gridComponents</code> to change the order of standard components.
// You can also omit standard components this way, although it more efficient to
// use the related "show" property if available (eg +link{showFilterEditor}). Note that
// this array must contain an entry for the <code>"body"</code> - listGrids with no body showing
// are unsupported.<br>
// <i>Advanced note:</i> The live components generated for each of these standard
// +link{ListGridComponent} types may differ across different listGrids. For example if this
// grid has any +link{listGridField.frozen,frozen fields}, the "body" entry will actually
// be created as an HLayout containing two GridRenderers (one for frozen fields,
// and one for unfrozen fields). This is really an implementation detail - the "body" entry
// in the gridComponents array simply specifies where the UI for the body should render within
// the ListGrid layout.
// <P>
// By embedding a Canvas directly in this list you can add arbitrary additional components to the
// listGrid as members, and have them be displayed alongside the standard automatically generated
// parts of the ListGrid.
// <P>
// Note that having added controls to gridComponents, you can still call APIs directly on
// those controls to change their appearance, and you can also show() and hide() them if
// they should not be shown in some circumstances.
// <P>
// Tip: custom controls need to set layoutAlign:"center" to appear vertically centered.
//
// @visibility external
//<

gridComponents:[
    "filterEditor",
    "header",
    "body",
    "summaryRow"
],

getGridMembers : function () {
    var gridComponents = this.gridComponents,
        members = [],
        bodyShown;

    for (var i = 0; i < gridComponents.length; i++) {
        var component = gridComponents[i],
            liveComponent = null;

        // allow arbitrary canvii to be shoehorned into the grid.
        if (isc.isA.Canvas(component)) {
            liveComponent = component;

        } else if (isc.isA.String(component)) {

            if (!this.shouldShowGridComponent(component)) continue;

            // This isn't quite as simple as auto-children -- for example if we have
            // frozen fields we use a Layout to show the 2 bodies - otherwise we simply
            // add the body directly to ourselves as a child.
            switch (component) {
                case "filterEditor" :
                    if (this.filterEditor == null) this.makeFilterEditor();
                    liveComponent = this.filterEditor;
                    break;

                case "header" :
                    if (this.header == null) {
                        this.makeHeader();
                    }
                    liveComponent = this.headerLayout || this.header;
                    break;

                case "body" :
                    bodyShown = true;

                    this.createBodies();
                    liveComponent = this.bodyLayout || this.body;
                    break;

                case "summaryRow" :
                    liveComponent = this.getSummaryRow();
                    break;

//                 default :
//                     this.logWarn("getGridComponents() - Hit default - unrecognized component " + component);
            }
        }
        // Handle being passed anything you could pass to "addChild" (EG "autoChild:foo") by
        // explicitly calling 'createCanvas'.
        if (component != null && liveComponent == null) {
            liveComponent = this.createCanvas(component);
        }
        members.add(liveComponent);
    }
    if (!bodyShown) {
        this.logWarn("ListGrid specified with gridComponents:" + gridComponents +
            ".  This does not include a \"body\" entry. ListGrids with no body are unsupported," +
            " displaying the body as the last member in the grid.");
        members[members.length] = this.createBodies();
    }
    return members;
},

showComponentPropertyMap:{
    header:"showHeader",
    filterEditor:"showFilterEditor",
    summaryRow:"showGridSummary"
},
_$body:"body", _$header:"header",
shouldShowGridComponent : function (component) {
    if (component == this._$body) return true;
    if (component == this._$header && this.headerHeight == 0) {
        return false;
    }

    var property = this.showComponentPropertyMap[component];
    if (property == null) {
        this.showComponentPropertyMap[component] = property =
                "show" + component.substring(0,1).toUpperCase() + component.substring(1);
    }
    return this[property];
},

// createChildren - builds (or retrieves) our standard set of grid components and adds them
// to self as members
// Called from prepareToDraw, also from other cases where we need to refresh UI (EG showing/hiding
// filterEditor)

createChildren : function () {

    this.updateGridComponents();


    if (isc.Browser.isMoz && isc.Browser.geckoVersion >= 20051111) {
        if (this.header) {
            this.body.mozOutlineOffset = "0px";
            if (this.body.mozOutlineColor == null)
                this.body.mozOutlineColor = this.mozBodyOutlineColor;
        } else {
            if (this.body.mozOutlineColor == null)
                this.body.mozOutlineColor = this.mozBodyNoHeaderOutlineColor;
            this.body.mozOutlineOffset = "-1px";
        }
    }


    this._originalUseNativeTabIndex = this._useNativeTabIndex;
    this._useNativeTabIndex = false;
},

// actually creates the standard 'gridComponent' widgets and adds them as members.
// If components are already created they're just retrieved (not clobbered)
// If components are already members, the Layout 'setMembers' code should no-op.

updateGridComponents : function () {
    this.setMembers(this.getGridMembers());
},

// layoutChildren at the Layout level handles positioning and sizing members.
// We have to do a certain amount of tweaking to this, mainly to ensure we calculate field
// widths correctly, and, if we have frozen fields, assign sizes to the frozen header and body
// correctly.

layoutChildren : function (reason,b,c) {
    // If we've created our children, go ahead and call updateFieldWidths() to figure out
    // column sizes, and perform various member-management tasks.

    if (this.body != null) {
        // Note - If the grid as a whole was resized, our fields will potentially reflow. This could
        // change the required header heights in autoFitHeaderHeights mode, requiring
        // us to resize the header bar.
        // We already run the _updateHeaderHeight logic within updateFieldWidths so no
        // need to explicitly do that again here.
        this._updateFieldWidths(reason, b,c);
    }


    isc.VLayout._instancePrototype.layoutChildren.call(this,reason,b,c);

    if (this.body != null) {
        // Every time we layout children (may be due to body resize or body scroll change), if
        // we're showing the frozen body, ensure it's the correct (explicit) height
        if (this.frozenBody) {
            var adjustForHScroll = this.shrinkForFreeze && this.body.hscrollOn;
            var height = this.bodyLayout.getInnerHeight();
            if (adjustForHScroll) height -= this.body.getScrollbarSize();
            this.frozenBody.setHeight(height);
            if (this.frozenBody._needRowRedraw()) this.frozenBody.markForRedraw("height changed");
            // this will avoid the layout from scrapping this specified height on resize etc
            this.frozenBody._userHeight = height;
        }
    }
},


// override updateMemberTabIndex. If an explicit T.I. is set on the grid just apply it to all
// members

updateMemberTabIndex : function (newMember) {
    // If a developer specifies an explicit tab-index for a Layout as a whole, how to handle this?
    // For now just assign all members the same tab index
    if (this.tabIndex != null && !this._autoTabIndex && this.tabIndex != -1 &&
        // Skip this if we have a CanvasItem




        (this.canvasItem == null) &&
        (this.tabIndex < isc.Canvas.TAB_INDEX_FLOOR))
    {
        newMember.setTabIndex(this.tabIndex);
        return;
    }
    return this.Super("updateMemberTabIndex", arguments);
},

// If we're auto-fitting to our fields, we may have specified or calculated
// field widths that exceed the specified width for the widget as a whole (availableFieldWidth)
// In this case, use the greater value to size the header.
_updateHeaderWidth : function (fieldWidths, headerWidth, headerLayout) {



    // if autoFitMaxColumns is set, then limit the sum to the initially visible
    // columns; this prevents an infinite resizing loop where we try to resize
    // larger and other logic forces us back to a narrower width
    var maxColumns = this.autoFitMaxColumns;
    if (maxColumns != null) {
        if (this.frozenFields == null || this.freezeStart()) {
            fieldWidths = fieldWidths.slice(0, maxColumns);
        } else {
            var nFrozenFields = this.frozenFields.length;
            if (maxColumns < nFrozenFields) {
                fieldWidths = fieldWidths.slice(-nFrozenFields, -nFrozenFields + maxColumns);
            } else {
                var unfrozenFields = fieldWidths.slice(0, maxColumns - nFrozenFields);
                fieldWidths = fieldWidths.slice(-nFrozenFields);
                fieldWidths.addList(unfrozenFields);
            }
        }
    }
    var sum = fieldWidths.sum();

    // if autoFitMaxWidth is set, clip the sum based on its value
    var maxWidth = this.autoFitMaxWidth;
    if (maxWidth != null) {
        maxWidth = this._getInnerSpaceFromWidth(maxWidth);
    }
    if (maxWidth != null && sum > maxWidth) {
        sum = maxWidth;
    }

    // if width has increased, update header width or resize header layout
    if (sum > headerWidth) {
        if (headerLayout) headerLayout.resizeTo(sum);
        else headerWidth = sum;
    }

    return headerWidth;
},


_updateFieldWidths : function (reason, b,c) {
    // don't do anything until we've created our children (eg resized before draw())
    if (this.body == null) return;

    // don't allow this method to fire recursively

    if (this._updatingFieldWidths) {
        return;
    }

    // getFieldWidths() will actually resize fields in order to handle the special cases
    // where we have autoFitWidths enabled plus expansion of an autoFitField to fill space
    // or clipping to avoid introducing hscrollbars.
    // Avoid running this method in response to a header-resized event from that resize

    if (this._calculatingAutoFitFieldWidths) {
        return;
    }
    // If autoFitting widths, allow calling code to handle specifying sizes rather than
    // reacting to resized events on our header buttons...
    if (this._autoFittingFields) {
        return;
    }
    this._updatingFieldWidths = true;



    // wipe out fieldWidths on resize so they'll be recalculated.

    this._fieldWidths = null;
    // If any fields has autoFitWidth set to true, calculate the auto-fit size for the
    // column apply it to the field
    // Note that we only care about the cases where we're fitting to the body content - if
    // we're fitting to the header this is handled by simply setting overflow to "visible"
    // on the header button.


    if (this.fields && !this.skipAutoFitWidths) {
        // The _appliedInitialAutoFitWidth flag allows us to avoid resizing
        // cols to fit content when this is not required. Should be cleared out
        // lazily by cases where we know content has changed and we should
        // reflow to accomodate it.
        if (!this.fields._appliedInitialAutoFitWidth) {
            var autoFitFieldWidths = this.getAutoFitValueWidths(null, true);
            if (autoFitFieldWidths == null) {
                this.fields.setProperty("_calculatedAutoFitWidth", null);
            } else {
                for (var i = 0; i < this.fields.length; i++) {
                    var field = this.fields[i];
                    if (autoFitFieldWidths[i] == null) {
                        field._calculatedAutoFitWidth = null;
                        continue;
                    }
                    var minWidth = this.getMinFieldWidth(field);
                    var headerButton = this.getFieldHeaderButton(i);
                    if (headerButton != null) {
                        var isBoth = this.getAutoFitWidthApproach(field) == "both",
                            originalOverflow = headerButton.overflow,
                            overflowNotVisible = isBoth && (originalOverflow != "visible");
                        if (isBoth && overflowNotVisible) {
                            headerButton.setOverflow("visible");
                        }

                        if (minWidth < autoFitFieldWidths[i]) {

                            // update the header if there is one
                            // Note: If autoFitWidthApproach is "both", the header title can
                            // still overflow this new specified size (giving us the desired behavior
                            // of fitting to the larger of the 2 drawn sizes)
                            headerButton.setWidth(autoFitFieldWidths[i]);
                            headerButton.parentElement.reflow();
                        } else {
                            headerButton.setWidth(minWidth);
                            headerButton.parentElement.reflow();
                        }

                        if (headerButton.isDirty()) headerButton.redraw();
                        if (headerButton.label && headerButton.label.isDirty()) {
                            headerButton.label.redraw();
                        }
                        // If the autoFitWidth approach is "both" the header button
                        // may render wider than the specified width (overflow:"visible")
                        // In this case
                        // - we want the calculated autoFitWidth to be wider
                        // - we need to apply that wider size actually to the header button
                        //   (specicified size is used directly in calculations
                        //   such as getFieldWidths()
                        if (isBoth) {
                            var headerDrawnWidth = headerButton.getVisibleWidth(),
                                expectedWidth = Math.max(minWidth, autoFitFieldWidths[i]);
                            if (headerDrawnWidth > expectedWidth) {
                                autoFitFieldWidths[i] = headerDrawnWidth;
                                headerButton.setWidth(headerDrawnWidth);
                            }

                            if (overflowNotVisible) {
                                headerButton.setOverflow(originalOverflow);
                            }
                        }

                    }

                    field._calculatedAutoFitWidth = autoFitFieldWidths[i];

                    if (minWidth < autoFitFieldWidths[i]) {

                        // update the header if there is one
                        // Note: If autoFitWidthApproach is "both", the header title can
                        // still overflow this new specified size (giving us the desired behavior
                        // of fitting to the larger of the 2 drawn sizes)
                        var headerButton = this.getFieldHeaderButton(i);

                        if (headerButton != null) {
                            headerButton.setWidth(autoFitFieldWidths[i]);
                            headerButton.parentElement.reflow();
                        }
                    } else {
                        var headerButton = this.getFieldHeaderButton(i);
                        if (headerButton != null) {
                            headerButton.setWidth(minWidth);
                            headerButton.parentElement.reflow();
                        }
                    }

                }
                // Hang a flag on the array to avoid re-calculating the width every time we
                // run stretchResizePolicy, etc
                this.fields._appliedInitialAutoFitWidth = true;

                // Remember how big the grid as a whole is - if it resizes we may need to
                // resize the field that expands to fill available space.
                this._initialAutoFitGridWidth = this.getWidth();

            }

        } else if (this._initialAutoFitGridWidth != this.getWidth()) {
            var expandField = this.getAutoFitExpandField();
            if (expandField) {
                var expandFieldNum = this.getFieldNum(expandField);

                delete expandField._calculatedAutoFitWidth;

                // Pick up the autoFitValueWidth for the field in question

                var autoFitFieldWidths = this.getAutoFitValueWidths(null, true);
                if (autoFitFieldWidths) {
                    var autoFitValueWidth = autoFitFieldWidths[expandFieldNum];
                    if (isc.isA.Number(autoFitValueWidth)) {

                        var minWidth = this.getMinFieldWidth(expandField);

                        expandField._calculatedAutoFitWidth = autoFitValueWidth;

                        // Note: If autoFitWidthApproach is "both", the header title can
                        // still overflow this new specified size (giving us the desired behavior
                        // of fitting to the larger of the 2 drawn sizes)
                        var headerButton = this.getFieldHeaderButton(expandFieldNum);
                        if (headerButton != null) {
                            headerButton.setWidth(Math.max(minWidth, autoFitValueWidth));
                            headerButton.parentElement.reflow();
                        }
                        this._initialAutoFitGridWidth = this.getWidth();
                    }
                }
            }
        }
    }

    var innerWidth = this.getAvailableFieldWidth(true),
        innerWidthChanged = (innerWidth != this.innerWidth);
    this.innerWidth = innerWidth;

//       this.logWarn("total columns width: " + innerWidth +
//                    (this.body ? ", vscrollon: " + this.body.vscrollOn : ""));

    var header = this.header,
        headerHeight = (this.showHeader ? this.getHeaderHeight() : 0);
    if (header != null) {

        // place the header
        // in RTL, sorter is on left
        var left = (this.isRTL() && this._showSortButton() ? this._getSorterWidth() : 0);


        header.hPolicy = "fill";
        if (this.frozenHeader) this.frozenHeader.hPolicy = "fill";
        var autoFitHorizontal = this.autoFitData == "horizontal" || this.autoFitData == "both",
            headerWidth = this.innerWidth,
            fieldWidths;


        if (autoFitHorizontal && this._dragResizingField) {
            fieldWidths = this.getFieldWidths(reason + " [sizing horizontal auto-fit header]");
            headerWidth = this._updateHeaderWidth(fieldWidths, headerWidth);
        }

        if (!this.leaveScrollbarGap && header.isDrawn() &&
            headerWidth != header.getWidth() && reason == "body scroll changed") {

            //this.logWarn("header changing size" + this.getStackTrace());
            if (this._settingBodyFieldWidths || !this.resizeFieldsForScrollbar) {
                header.hPolicy = "none";
            }
        }
        var headerLayout = this.headerLayout || header;

        headerLayout.resizeTo(headerWidth, headerHeight);

        headerLayout._userHeight = headerHeight;


        if (autoFitHorizontal && !this._dragResizingField) {
            fieldWidths = this.getFieldWidths(reason + " [sizing horizontal auto-fit header]");
            this._updateHeaderWidth(fieldWidths, headerWidth, headerLayout);
        }

        // if we're in the middle of the initial drawing process, draw the header now so we can
        // get fieldWidths from it to give to the body before it draws.  Also draw the header
        // if we're completely drawn and it's undrawn because it was just recreated.

        var isInitialDraw = (reason == "initial draw");
        if (!header.isDrawn() && (isInitialDraw || this.isDrawn())) {
            if (!this.frozenFields) {
                if (isInitialDraw) this._moveOffscreen(header);
                header.draw();
            } else {
                // running getFieldWidths before the header is drawn
                // to determine how much space will be required for the frozen header
                // (see _getCalculatedFieldWidths for explanation)
                if (fieldWidths == null) {
                    fieldWidths = this.getFieldWidths(reason + " [sizing frozen fields]");
                }
                var frozenWidths = this.getFrozenSlots(fieldWidths);
                var frozenWidth = frozenWidths.sum();
                this.frozenHeader.setWidth(frozenWidth);

                this.frozenHeader._userWidth = frozenWidth;
                if (isInitialDraw) this._moveOffscreen(this.headerLayout);
                this.headerLayout.draw()
            }
        }

        if (header.isDrawn()) header.hPolicy = "none";
        if (this.frozenHeader && this.frozenHeader.isDrawn()) this.frozenHeader.hPolicy = "none";

        // ensure the sorter is showing in the right place and visible / hidden as appropriate

        if (this.sorter) {
            this.updateSorter();
        }

        if (this.autoFitHeaderHeights) {
            var headerHeight = this.getHeaderHeight();
            this.dropCachedHeaderButtonHeights();
            var newHeaderHeight = this.getHeaderHeight();
            if (headerHeight != newHeaderHeight ||
                this.header.getHeight() != newHeaderHeight)
            {
                this.header.setHeight(newHeaderHeight);
                this._updateHeaderHeight();
            } else {
                if (this.headerSpans) {
                    this.header._adjustSpans(true);
                }
            }
        }

    }

    // if we haven't figure out field widths, this is the time to get them, since the header
    // has been drawn if we have one, and the body is yet to be drawn.
    // NOTE: if we are clear()d and then redrawn, and we have a header, we don't want to get
    // sizes from the header while it's undrawn, as it will not run the layout policy while
    // undrawn.
    if ((!this._fieldWidths || innerWidthChanged) &&
        ((header && header.isDrawn()) || headerHeight == 0))
    {
        var fieldWidths = this.getFieldWidths(reason);
        this.setBodyFieldWidths(fieldWidths);

        // update the filter editor field widths too [only need to worry about the body]
        if (this.filterEditor && this.filterEditor.body != null) {
            this.filterEditor.setBodyFieldWidths(this.filterEditor.getFieldWidths(reason));
        }

        // update the summaryRow (if visible)
        if (this.summaryRow && this.showGridSummary && this.summaryRow.body != null) {
            this.summaryRow.setBodyFieldWidths(fieldWidths.duplicate());
        }

           if (this.logIsDebugEnabled("layout")) {
            this.logDebug("new field widths: " + this._fieldWidths, "layout");
        }
    }

    // Normally we can rely on the standard layout behavior to size the filter editor.
    // however, if autoFitData is horizontal our content can exceed our specified size, and
    // the layout doesn't automatically handle expanding content to fit an overflowed breadth
    // Therefore in this case explicitly match the filter editor to the body width
    if (autoFitHorizontal && this.filterEditor) {
        this.filterEditor.setWidth(this.body.getVisibleWidth());
    }


    this._updatingFieldWidths = false;
},

//> @attr listGrid.autoFitHeaderHeights (boolean : null : IR)
// If this property is set to true, header buttons for either
// +link{listGrid.fields,fields} or +link{listGrid.headerSpans,header spans} will
// automatically expand to accomodate their titles vertically.
// This means if you have a "tall" title - typically a long string where
// +link{listGridField.wrap} is set to true such that you end up with several lines of
// text - the button will render large enough to accomodate it.
// If necessary this will cause the header for the grid as a whole to expand beyond the
// specified +link{listGrid.headerHeight}.
// @visibility external
//<



// get the "expected" height for field or span header-buttons based on
// this.headerHeight, span.height, etc.
getHeaderButtonDefaultHeight : function (span, recalculate) {
    if (recalculate || span._defaultHeight == null) {
        this.calculateDefaultSpanHeights();
    }
    return span._defaultHeight;
},

// Perform a one-time iteration through all header buttons determining
// "expected" heights based on header height etc, and cache these values.
calculateDefaultSpanHeights : function () {

    if (this.headerSpans == null) {
        // Shouldn't be called if headerSpans is null
        return;
    }

    if (this.headerSpans != null) {

        var fields = this.getFields(),
            fieldNames = fields.getProperty("name");
        for (var i = 0; i < this.headerSpans.length; i++) {
            var availableSpace = this.headerHeight,
                span = this.headerSpans[i];

            this._calculateDefaultSpanHeight(span, availableSpace, fieldNames);
        }
        for (var i = 0; i < fields.length; i++) {
            if (this.spanMap[fields[i].name] == null) {
                fields[i]._defaultHeight = this.headerHeight;
            }
        }
    }
},

// helper for calculateDefaultSpanHeights()
_calculateDefaultSpanHeight : function (span, availableSpace, fieldNames, isField) {
    if (!isField) {
        // Skip anything that isn't visible
        if (!this._headerSpanVisible(fieldNames, span)) return;

        var defaultHeight;
        if (!span._spanAutoSizeHeight) {
            defaultHeight = span.height || this.headerSpanHeight;
        }
        if (defaultHeight == null) {
            var spanDepth = this.getNestedSpanDepth(span);
            // add one to account for field at the bottom
            spanDepth += 1;
            defaultHeight = Math.floor(availableSpace / spanDepth);
        }
        span._defaultHeight = defaultHeight;
        availableSpace -= defaultHeight;

        if (span.spans) {
            for (var i = 0; i < span.spans.length; i++) {
                this._calculateDefaultSpanHeight(span.spans[i], availableSpace, fieldNames);
            }
        } else {
            for (var i = 0; i < span.fields.length; i++) {
                // skip hidden / missing fields
                var field = this.getField(span.fields[i]);
                if (field) {
                    this._calculateDefaultSpanHeight(field, availableSpace, fieldNames,
                        true);
                }
            }
        }
    } else {
        // not visible? Just bail
        if (!fieldNames.contains(span.name)) return;

        span._defaultHeight = availableSpace;
    }
},

getHeaderButtonMinHeight : function (field, recalculate) {
    if (!recalculate && field._calculatedMinHeight != null) {
        return field._calculatedMinHeight;
    }
    return this.getHeaderButtonMinHeights([field], recalculate)[0];
},

// Use an offscreen tester to figure out how much space is required for
// a field, or span header button based on its specified title.
// Caches the value for re-use
getHeaderButtonMinHeights :function (fields, recalculate) {
    var mustCalculate = [];
    var heights = [];
    var testHTML = "";
    for (var i = 0; i < fields.length; i++) {
        var field = fields[i];
        if (!recalculate && field._calculatedMinHeight != null) {
            heights[i] = field._calculatedMinHeight;
        } else {
            var isSpan = !this.fields.contains(field);
            var width;

            if (isSpan) {
                var spannedFields = this.getSpannedFields(field),
                    width = 0;
                //this.logWarn("fields:" + spannedFields);
                for (var ii = 0; ii < spannedFields.length; ii++) {
                    var spannedField = this.getField(spannedFields[ii]);
                    if (spannedField == null) {
                        continue;
                    }
                    width += this.getFieldWidth(spannedFields[ii]);
                }

            } else {
                width = this.getFieldWidth(field);
            }

            // XXX This basically matches framework behavior if we
            // don't see a useful numerical width. May want to revisit.
            if (isNaN(width)) {
                heights[i] = 1;
            } else {

                var baseStyle = field.headerBaseStyle,
                    buttonProperties = this.getHeaderButtonProperties();
                if (baseStyle == null) {
                    if (field.frozen && buttonProperties.frozenBaseStyle) {
                        baseStyle = buttonProperties.frozenBaseStyle;
                    } else if (buttonProperties.baseStyle) {
                        baseStyle = buttonProperties.baseStyle;
                    }
                }
                var titleStyle = field.headerTitleStyle;
                if (titleStyle == null) {
                    if (field.frozen && buttonProperties.frozenTitleStyle) {
                        titleStyle = buttonProperties.frozenTitleStyle;
                    } else if (buttonProperties.titleStyle) {
                        titleStyle = buttonProperties.titleStyle;
                    }
                }

                if (baseStyle == null || titleStyle == null) {
                    var buttonConstructor = this.headerButtonConstructor ||
                                this.header ? this.header.buttonConstructor
                                            : isc.Toolbar.getPrototype().buttonConstructor;
                    if (isc.isA.String(buttonConstructor)) {
                        buttonConstructor = isc[buttonConstructor];
                    }
                    if (baseStyle == null)  {
                        baseStyle = buttonConstructor.getPrototype().baseStyle;
                    }
                    if (titleStyle == null) {
                        titleStyle = buttonConstructor.getPrototype().titleStyle;
                    }
                }

                var title = isSpan ? field.headerTitle || field.title
                                 : this.getHeaderButtonTitle(field);
                var oldConfig = field._calculatedHeightConfig;

                if (oldConfig &&
                    oldConfig.baseStyle == baseStyle &&
                    oldConfig.titleStyle == titleStyle &&
                    oldConfig.width == width &&
                    oldConfig.wrap == field.wrap &&
                    oldConfig.title == title)
                {
                    field._calculatedMinHeight = oldConfig.height;
                    heights[i] = oldConfig.height;
                }
            }
        }
        if (heights[i] != null) continue;

        // At this point we want to actually measure the
        // height (and cache the result).

        var config = {};
        config.width = width;
        config.baseStyle = baseStyle;
        config.titleStyle = titleStyle;
        config.wrap = field.wrap;
        config.title = title

        mustCalculate.add({
            index:i,
            config:config
        });

        var buttonWrap = config.wrap;
        if (buttonWrap == null) buttonWrap = this.wrapHeaderTitles;
        testHTML += "<div style='position:absolute;width:" + width + "px;'><div class=" +
             (config.titleStyle || config.baseStyle) +
             (buttonWrap ? ">" : " style='white-space:nowrap;'>") +
             title +
             "</div></div>"
    }
    if (mustCalculate.length == 0) return heights;

    if (isc.ListGrid.headerHeightTester == null) {
        isc.ListGrid.headerHeightTester = isc.Canvas.create({
            overflow:"hidden",
            top:-1000,
            autoDraw:true,
            contents:testHTML
        });
    } else {
        isc.ListGrid.headerHeightTester.setContents(testHTML);
    }
    // immediate redraw (setContents does a delayed redraw)
    if (isc.ListGrid.headerHeightTester.isDirty()) isc.ListGrid.headerHeightTester.redraw();

    var handle = isc.ListGrid.headerHeightTester.getHandle(),

    childNodes = handle.childNodes;
    if (childNodes.length != mustCalculate.length) {

    }
    for (var i = 0; i < childNodes.length; i++) {
        var div = childNodes[i];

        var config = mustCalculate[i].config,
            index = mustCalculate[i].index,
            field = fields[index];

        config.height = div.offsetHeight;
        field._calculatedHeightConfig = config;
        field._calculatedMinHeight = config.height;

        heights[index] = config.height;
    }
    return heights;
},

// Drops the cached expected (default) and min-heights for all fields / header spans
dropCachedHeaderButtonHeights : function () {
    for (var i = 0; i < this.fields.length; i++) {
        this.fields[i]._defaultHeight = null;
        this.fields[i]._calculatedMinHeight = null;
        if (this.spanMap) {
            var span = this.spanMap[this.fields[i].name];
            while (span != null) {
                span._defaultHeight = null;
                span._calculatedMinHeight = null;
                span = span.parentSpan;
            }
        }
    }
},

// getHeaderHeight() - if autoFitting headers vertically, this method will calculate
// the required height for the header-bar as a whole and return it
// (Otherwise returns specified header height).
getHeaderHeight : function () {
    if (this.headerHeight == 0 || !this.showHeader) return 0;
    if (!this.autoFitHeaderHeights) return this.headerHeight;

    var minHeight = 0;
    if (this.headerSpans == null) {
        minHeight = this.headerHeight;
        var fields = this.getFields();

        var fieldHeights = this.getHeaderButtonMinHeights(fields);
        minHeight = Math.max(minHeight, fieldHeights.max());

    } else {

        var fields = this.getFields(),
            fieldColHeight = 0;

        // We've found that redraws are a significant bottleneck and that
        // redrawing the offscreen header height tester for every field and every
        // span is expensive.
        // Cut this down by passing all fields into getMinHeights... then all
        // spans (two redraws total rather than one per button)
        var fieldMinHeights = this.getHeaderButtonMinHeights(fields),
            spans = [];

        for (var i = 0; i < fields.length; i++) {
            if (this.spanMap[fields[i].name]) {
                var span = this.spanMap[fields[i].name];
                while (span) {
                    if (!spans.contains(span)) {
                        spans.add(span);
                    }
                    span = span.parentSpan;
                }
            }
        }


        var spanMinHeights = this.getHeaderButtonMinHeights(spans);
        for (var i = 0; i < fields.length; i++) {
            fieldColHeight = Math.max(
                                this.getHeaderButtonDefaultHeight(fields[i]),
                                this.getHeaderButtonMinHeight(fields[i])
                             );
            if (this.spanMap[fields[i].name]) {
                var span = this.spanMap[fields[i].name];
                while (span) {

                    fieldColHeight +=  Math.max(
                                        this.getHeaderButtonDefaultHeight(span),
                                        this.getHeaderButtonMinHeight(span)
                                       );
                    span = span.parentSpan;
                }
            }

            minHeight = Math.max(minHeight, fieldColHeight);
        }

    }
    return Math.max(minHeight, this.headerHeight);
},

// getAutoFitValueWidths: Calculates the auto-size width based on rendered field values
// Returns a sparse array of field sizes indexed by fieldNum in this.fields.
// By default expects no params and returns only auto-size width of the fields marked with
// autoFitWidth:true.
// If fields are passed in directly we'll calculate sizes for those fields only (regardless of
// any per-field autoFitWidth settings)
getAutoFitValueWidths : function (fields, checkApproach) {
    if (this.data == null) return;
    if (isc.isA.ResultSet(this.data)) {
        var drawArea = this.body.getDrawArea();
        if (!this.data.rangeIsLoaded(drawArea[0], drawArea[1])) {
            // If we're autofitting all fields, and data is loading, mark for resize
            // again - will occur on redraw from dataChanged/dataArrived
            if (fields == null) {
                this.updateFieldWidthsForAutoFitValue("Delayed resize pending data load");
            }
            return;
        }
    }

    var noAutoFit = (fields == null);
    if (fields == null) {
        fields = [];
        for (var i = 0; i < this.fields.length; i++) {
            var field = this.fields[i];

            if (this.shouldAutoFitField(field)) {
                // checkApproach - if passed don't collect valueWidths unless the field's
                // autoFitWidthApproach is 'value' or 'both'
                if (!checkApproach || this.getAutoFitWidthApproach(field) != "title") {
                    fields.add(field);
                    noAutoFit = false;
                }
            }
        }
    }
    if (noAutoFit) return [];

    // widths will be a sparse array containing just the calculated field widths
    var widths = [],
        checkAutoFitWidths = false,
        frozenAutoFitFields = [], frozenAutoFitColNums = [],
        autoFitFields = [], autoFitColNums = [];

    for (var i = 0; i < this.fields.length; i++) {
        if (fields.contains(this.fields[i])) {
            var field = this.fields[i];
            // call getDefaultFieldWidth per field with the special flag to
            // suppress checking autoFitWidths
            // If we get back any nulls (indicating we hit this) pass these to
            // the getColumnAutoSize() method directly.
            // It's more efficient to pass a number of cols to that method
            // at once, but we also want a custom "getDefaultFieldWidth" implementation
            // to behave as expected so it's a viable override point for customizing this
            // behavior.
            var defaultWidth = this.getDefaultFieldWidth(this.fields[i], true);
            if (defaultWidth == null) {
                checkAutoFitWidths = true;
                if (field.frozen) {
                    frozenAutoFitFields.add(this.getLocalFieldNum(i));
                    frozenAutoFitColNums.add(i);
                } else {

                    autoFitFields.add(this.getLocalFieldNum(i));
                    autoFitColNums.add(i);
                }
            } else {
                widths[i] = defaultWidth;
            }
        }
    }
    if (checkAutoFitWidths) {

        if (frozenAutoFitFields.length > 0) {
            var bodyWidths = this._getBodyColumnAutoSize(true, frozenAutoFitFields);


            if (bodyWidths != null) {
                for (var i = 0; i < bodyWidths.length; i++) {
                    widths[frozenAutoFitColNums[i]] = bodyWidths[i];
                }
            }
        }

        if (autoFitFields.length > 0) {
            var bodyWidths = this._getBodyColumnAutoSize(false, autoFitFields);
            if (bodyWidths != null) {
                for (var i = 0; i < bodyWidths.length; i++) {
                    widths[autoFitColNums[i]] = bodyWidths[i];
                }
            }
        }
    }
    return widths;
},

// Helper to call gridRenderer logic to get auto-fit widths for a number of fields.
_getBodyColumnAutoSize : function (frozen, localFieldNums) {
    var body = frozen ? this.frozenBody : this.body;
    if (body == null) return null;
    var bodyWidths = body.getColumnAutoSize(localFieldNums);
    if (this.includeGridSummaryInAutoFitWidth &&
        this.showGridSummary && this.summaryRow != null)
    {
        var summaryBody = (frozen ? this.summaryRow.frozenBody : this.summaryRow.body);
        if (summaryBody != null) {
            var summaryWidths = summaryBody.getColumnAutoSize(localFieldNums);
            if (summaryWidths != null) {
                if (bodyWidths == null) bodyWidths = [];
                for (var i = 0; i < summaryWidths.length; i ++) {
                    if (bodyWidths[i] == null ||
                        summaryWidths[i] > bodyWidths[i]) bodyWidths[i] = summaryWidths[i];
                }
            }
        }
    }
    // In setBodyFieldWidths, we automatically adjust body col sizes (shrink them)
    // to avoid the body styling from impacting the overall width of the body - required
    // to support mismatched header/border styling.
    // Preemptively expand the calculated "auto fit" width so doing this won't eat into
    // that space
    if (this.allowMismatchedHeaderBodyBorder && bodyWidths != null) {
        var body = frozen ? this.frozenBody : this.body,
            firstIndex = localFieldNums.indexOf(0),
            lastIndex = localFieldNums.indexOf(
                            (frozen ? this.frozenFields.length : this.fields.length)-1);
        if (firstIndex != -1) {
            bodyWidths[firstIndex] =  bodyWidths[firstIndex] +
                                        body.getLeftBorderSize() + body.getLeftMargin();
        }
        if (lastIndex != -1) {
            bodyWidths[lastIndex] =  bodyWidths[lastIndex] +
                                        body.getRightBorderSize() + body.getRightMargin();
        }
    }
    return bodyWidths;
},

//> @attr listGrid.iconPadding (integer : 2 : IR)
// When using +link{autoFitFieldWidths}, padding in pixels left on each side of fields that
// show images.
// @visibility external
//<

//> @method listGrid.getDefaultFieldWidth()
// Method to calculate and return the default width of a field. This method is called
// to calculate the size of each field's content as part of the
// +link{listGrid.autoFitFieldWidths,field auto fit} behavior.
// <P>
// The default implementation varies by +link{ListGridFieldType,field type}.
// For fields of type <code>"icon"</code>, or fields which show only a
// +link{ListGridField.valueIcons,valueIcon} as a value, and for boolean fields which
// show a checkbox value, the width will be calculated based on the icon size and
// +link{listGrid.iconPadding}. For other fields the width will be calculated
// based on the rendered width of content. Note that for
// <code>"image"</code> type fields, this method will rely on the +link{listGridField.imageWidth}
// being specified.
// <P>
// Note that this width is the default width of "content" - it does not take into account
// the rendered size of the field title.
//
// @param field (ListGridField) Field for which the size should be determined
// @return (int) default size required for the field's content.
//
// @visibility external
//<

iconPadding:2,
getDefaultFieldWidth : function (field, suppressAutoFitWidths) {

    var width;

    // special cases where we can avoid writing out and measuring content
    if (field.type == "icon" && (field.iconWidth != null || field.iconSize != null)) {
        return (field.iconWidth || field.iconSize) + 2*this.cellPadding + 2*this.iconPadding;
    } else if (this.showValueIconOnly(field)) {
        width = this.getValueIconWidth(field) + (2* this.cellPadding) +
                              this.getValueIconRightPadding(field) +
                              this.getValueIconLeftPadding(field);



        if (this.showFilterEditor && this.fields != null && this.fields.length > 0) {
            var fieldAlign = field.cellAlign || field.align;
            if (fieldAlign === this._$center && field === this.fields[this.fields.length - 1]) {
                width += 2 * this._getSorterWidth();
            }
        }

        return width;
    }

    if (suppressAutoFitWidths) return null;


    var fieldNum = this.getFieldNum(field),
        localFieldNum = this.getLocalFieldNum(fieldNum);
    var widths = this._getBodyColumnAutoSize(field.frozen, [localFieldNum]);
    width = widths ? widths[0] : null;
    if (width == null) width = field.width;

    return width;
},

getAutoFitWidthApproach : function (field) {
    if (field.autoFitWidthApproach != null) return field.autoFitWidthApproach;
    return this.autoFitWidthApproach;
},

draw : function (a,b,c,d) {
    if (isc._traceMarkers) arguments.__this = this;
    if (!this.readyToDraw()) return this;

    // create children and set up fields if not already set up
    this.prepareForDraw();

    // call the superclass draw routine to do the actual drawing
    this.invokeSuper(isc.ListGrid, "draw", a,b,c,d);

    // get the actual rendered sizes of the columns
    this.body.getColumnSizes();


    for (var i = 0; i < this.bodies.length; i++) {
        var body = this.bodies[i];
        if (body._embeddedComponents) this.markForRedraw();
    }

    // if the sortFieldNum is specified, tell the header about it
    var sortFieldNum = this._getSortFieldNum();
    if (this.header && sortFieldNum != null && this.selectHeaderOnSort) {
        var header = this.getFieldHeader(sortFieldNum),
            sortButton = this.getFieldHeaderButton(sortFieldNum);
        header.selectButton(sortButton);
        if (isc.Browser.isTouch && this.shouldShowHeaderMenuButton(sortButton, true)) {
            this._showHeaderMenuButton(sortButton);
        }
    }

    // scroll the header along with the list (necessary for textDirection == RTL)
    this.bodyScrolled(this.body.getScrollLeft(), this.body.getScrollTop());


    if (this.sorter) this.updateSorter();

    // If we have embedded components ensure that our initial auto-fit widths take
    // their size into account.
    if (this.body._embeddedComponents ||
        (this.frozenBody && this.frozenBody._embeddedComponents))
    {
        this.updateFieldWidthsForAutoFitValue("Initial draw with embeddedComponents");
    }

    this._wasEmptyForCanFocus = this.isEmpty();

    return this;
},

// should we show inactive editors for every record - returns true if we have any data and
// alwaysShowEditors is true at the grid or field level [can check specific fields]
_alwaysShowEditors : function (field, ignoreTotalRows) {
    // ignoreTotalRows -- useful to test whether we would ever show editors in every cell
    // as opposed to whether we currently do.

    // if we have no data don't show any edit cells
    if (!ignoreTotalRows && this.getTotalRows() == 0) return false;

    if (this.alwaysShowEditors) return true;
    var fields = field ? [field] : this.fields;
    if (fields) {
        for (var i = 0; i < fields.length; i++) {
            if (fields[i].alwaysShowEditors) {
                return true;
            }
        }
    }
    return false;
},

prepareForDraw : function () {


    // call setFields() for the first time, if it hasn't already been done
    if (this.completeFields == null) this.setFields(this.fields);

    if (this.shouldShowCheckboxField()) this.updateCheckboxHeaderState();

    // if alwaysShowEditors is true, ensure we are editing
    if (this.getEditRow() == null) {
        var forceEditing = this._alwaysShowEditors();
        if (forceEditing) this.startEditing(null,null,true,null,true);
    }

    // if grid is not grouped, and a grouping is already set, apply it with groupBy
    if (!this.isGrouped && this.groupByField) {
        var fields = this.getGroupByFields();
        this.groupByField = null;

        // if we have no fields don't call groupBy yet - should occur on setFields
        //if (this.fields != null)
        this.groupBy(fields);
    }

    // create the header and body.
    this.createChildren();
},

//> @method listGrid.getGroupByFields()
// Get the current grouping of this listGrid as an array of fieldNames.
// <P>
// This method returns an arry containing the names of the field(s) by which this
// grid is grouped (either from +link{listGrid.groupByField} having been explicitly set or
// from a call to +link{listGrid.groupBy()}). If this grid is not currently grouped, this method
// will return null.
// @return (Array of String) Current grouping for this grid. If grouped by a single field an array
//  with a single element will be returned.
// @visibility external
//<

getGroupByFields : function () {
    var fields = this.groupByField;
    if (fields != null && !isc.isAn.Array(fields)) {
        fields = [fields];
    }
    return fields;
},

prepareForDestroy : function (indirectDestroy) {
    if (this._dragLine) {
        this._dragLine.destroy();
        this._dragLine = null;
    }
    if (this._cornerMenu) this._cornerMenu.destroy();
    if (this.spanContextMenu) this.spanContextMenu.destroy();
    if (this.cellContextMenu) this.cellContextMenu.destroy();
    if (this._editRowForm) {
        this._editRowForm.destroy();
        delete this._editRowForm;
        delete this._editorShowing;
    }

    if (this.data){
        // if the data was autoCreated, destroy it to clean up RS<->DS links
        if (this.data._autoCreated && isc.isA.Function(this.data.destroy)) {
            this.data.destroy();
        } else {
            // ignore so we don't leak memory from the observation references
            this._ignoreData(this.data, true);
            delete this.data;
        }
    }
    // If we are grouped our ResultSet may be stored under this.originalData
    // Clean this up too (otherwise we can end up with the DataSource attempting to
    // notify a destroyed ListGrid via it's orphaned resultSet)
    if (this.originalData){
        if (this.originalData._autoCreated && isc.isA.Function(this.originalData.destroy)) {
            this.originalData.destroy();
        } else {
            this._ignoreData(this.originalData, true);
            delete this.originalData;
        }
    }

    if (this.selection) {
        this.destroySelectionModel();
    }
    if (this.selectionCanvas) this.selectionCanvas.destroy();
    if (this.selectionUnderCanvas) this.selectionUnderCanvas.destroy();
    if (this.currentRollOverCanvas) this.currentRollOverCanvas.destroy();
    if (this.currentRollUnderCanvas) this.currentRollUnderCanvas.destroy();

    this._dropODSData();

    // RecordComponents:
    // - if pooling mode is recycled, destroy any created recordComponents sitting in our pool.
    // - if pooling mode is data, destroy any created recordComponents that got scrolled out
    //   of view and never destroyed.

    var recordComponents = this.getRecordComponentPool();
    if (recordComponents) {
        for (var i = 0; i < recordComponents.length; i++) {
            var comp = recordComponents[i];
            if (comp && comp.destroy && !comp.destroyed) {
                if (!comp.dontAutoDestroy) comp.destroy();
                else comp.deparent();
            }
        }
    }
    if (this._orphanedRecordComponents != null) {
        for (var ID in this._orphanedRecordComponents) {
            var comp = this._orphanedRecordComponents[ID];
            if (!comp || comp.destroyed || comp.destroying
                || comp._pendingDestroy)
            {
                continue;
            }
            if (!comp.dontAutoDestroy) comp.destroy();
            else comp.deparent();
        }
    }

    this.Super("prepareForDestroy", arguments);
},

//>    @method    listGrid.redrawHeader()    (A)
//            redraw the header
//        @group    drawing
//<
redrawHeader : function () {
    if (this.header) this.header.markForRedraw();
},

//>    @method    listGrid.getBaseStyle()
// Return the base stylename for this cell.  Has the following implementation by default:
// <ul>
// <li>If +link{listGrid.editFailedBaseStyle, this.editFailedBaseStyle} is defined, and the
//     cell is displaying a validation error return this value.</li>
// <li>If +link{listGrid.editPendingBaseStyle, this.editFailedPendingStyle} is defined, and
//     the cell is displaying an edit value that has not yet been saved (see
//     +link{ListGrid.autoSaveEdits}) return this value.</li>
// <li>Otherwise return +link{ListGrid.recordBaseStyleProperty, record[listGrid.recordBaseStyleProperty]},
//     if defined, otherwise +link{ListGridField.baseStyle, field.baseStyle}.</li>
// </ul>
// If no custom style is found for the cell as described above, the default baseStyle will be
// returned. If +link{listGrid.baseStyle} is specified this will be used. Otherwise for
// grids showing fixed height rows which match +link{listGrid.normalCellHeight}
// +link{listGrid.normalBaseStyle} will be used. For grids with variable, or modified
// cell heights, +link{listGrid.tallBaseStyle} will be used.
// <P>
// Note also that
// enabling +link{listGrid.fastCellUpdates} will cause the <code>tallBaseStyle</code> to be
// used rather than +link{listGrid.normalBaseStyle}.
// <smartgwt><p>
// <b>Note: This is an override point.</b></smartgwt>
//
// @see getCellStyle()
//
// @param [record] (ListGridRecord) Record associated with this cell. May be <code>null</code>
//                                  for a new edit row at the end of this grid's data set.
// @param    rowNum    (number)    row number for the cell
// @param    colNum    (number)    column number of the cell
// @return    (CSSStyleName)    CSS class for this cell
// @visibility external
// @example replaceStyle
//<
getBaseStyle : function (record, rowNum, colNum) {


    if (this.canEdit == true && !this.isPrinting) {
        if (this.editFailedBaseStyle && this.cellHasErrors(rowNum, colNum))
            return this.editFailedBaseStyle;

        if (this.editPendingBaseStyle && this._cellHasChanges(rowNum, colNum, false, true))
            return this.editPendingBaseStyle;
    }

    if (this.originBaseStyle && this.selection._isCellSelectionOrigin(rowNum, colNum)) {
        return this.originBaseStyle;
    }

    if (record && this.recordBaseStyleProperty && record[this.recordBaseStyleProperty])
        return record[this.recordBaseStyleProperty];

    var field = this.getField(colNum);
    if (field && field.baseStyle) return field.baseStyle;

    if (field && field.type == "summary" && this.recordSummaryBaseStyle)
        return this.recordSummaryBaseStyle;

    if (field && field.frozen && !this._suppressedFrozenFields && this.frozenBaseStyle) {
        return this.frozenBaseStyle;
    }

    if (this.isPrinting && (this.printBaseStyle != null)) return this.printBaseStyle;

    var baseStyle = this.baseStyle;
    if (baseStyle == null) {
        if (this.cellHeight != this.normalCellHeight ||

            this.fastCellUpdates ||
            this.allowRowSpanning ||
            !this.shouldFixRowHeight(record, rowNum) ||

            (record != null && this._hasEmbeddedComponents(record)))
        {
            baseStyle = this.tallBaseStyle;
        } else {
            baseStyle = this.normalBaseStyle;
        }
    }
    return baseStyle;
},

getCellCSSText : function (record, rowNum, colNum) {
    var field = this.getField(colNum),
        cssText;

    // per-record cssText
    if (record) {

        if (!record._isGroup && this.fieldSourceGrid == null && field != null && this.showValueIconOnly(field)) {
            cssText = "line-height:0px;";
        }

        var recordCSSText = record[this.recordCSSTextProperty];
        if (recordCSSText != null) {
            return cssText != null ? cssText + recordCSSText : recordCSSText;
        }
    }

    var removedCSSText = this.removedCSSText;
    if (this.recordMarkedAsRemoved(rowNum) && removedCSSText) {
        cssText = cssText != null ? cssText + removedCSSText : removedCSSText;
    } else {

        if (this.isEditable()) {
            var editCSSText;
            if (this.editFailedBaseStyle == null && (editCSSText = this.editFailedCSSText) &&
                this.cellHasErrors(rowNum, colNum))
            {
                cssText = cssText != null ? cssText + editCSSText : editCSSText;
            } else if (this.editPendingBaseStyle == null && (editCSSText = this.editPendingCSSText) &&
                this._cellHasChanges(rowNum, colNum, false, true))
            {
                cssText = cssText != null ? cssText + editCSSText : editCSSText;
            }
        }
    }


    cssText = this.getRecordHiliteCSSText(record, cssText, this.getField(colNum), true);

    return cssText;

},

//>    @method    listGrid.getRawCellValue()
//        @group    data
//            return the raw data for one particular cell in the list
//
//        @param    record        (object)    a record in the data
//        @param    recordNum    (number)    number of that record (in case it's important for the output)
//        @param    fieldNum    (number)    number of the field to display
//
//        @return    (string)    raw value for this cell
//<

getRawCellValue : function (record, recordNum, fieldNum, isFieldName) {
    return this._getRawCellValue(
        this.emptyCellValue, this.groupSummaryRecordProperty, this.gridSummaryRecordProperty,
        null, null, this.data, this.applyFormulaAfterSummary, null, null, null, null,
        record, recordNum, fieldNum, isFieldName);
},
_getRawCellValue : function (
    emptyCellValue, groupSummaryRecordProperty, gridSummaryRecordProperty,
    fieldsHaveUserFormulas, fieldsHaveUserSummaries, data, applyFormulaAfterSummary,
    fieldHasUserFormula, fieldUserFormula, fieldHasUserSummary, fieldUserSummary,
    record, recordNum, fieldNum, isFieldName)
{
    // If `getRawCellValue()` is overridden in a subclass then use that.
    if (this.getRawCellValue != isc.ListGrid.getInstanceProperty("getRawCellValue")) {
        return this.getRawCellValue(record, recordNum, fieldNum, isFieldName);
    }

    var field,
        fieldName,
        dataPath;

    if (isFieldName) {
        fieldName = fieldNum
        // when fieldName specified, search completeFields if available
        field = this.completeFields ? isc.Class.getArrayItem(fieldName, this.completeFields, this.fieldIdProperty) : this.getField(fieldName);
    } else {
        field = this.fields[fieldNum];
        fieldName = field ? field[this.fieldIdProperty] : fieldName;
    }
    if (fieldHasUserFormula == null) {
        fieldHasUserFormula = (field && field.userFormula != null);
    }
    if (fieldHasUserFormula && fieldUserFormula == null) {
        fieldUserFormula = field.userFormula;
    }
    if (fieldHasUserSummary == null) {
        fieldHasUserSummary = (field && field.userSummary != null);
    }
    if (fieldHasUserSummary && fieldUserSummary == null) {
        fieldUserSummary = field.userSummary;
    }

    dataPath = field ? this.getTrimmedFieldDataPath(field) : null;

    if (fieldName == null && dataPath == null) return emptyCellValue;
    // Note even if fieldName is set, field is not guaranteed to be present - this could
    // be a displayField which is being rendered in a different field's cell

    var editValue, value, undef;
    if (this.rowEditNotComplete(recordNum)) {
        editValue = this._getEditValue(recordNum, fieldNum);
    }
//this.logWarn("GRCV is running - row/col:" + [recordNum, fieldNum], editVal:" + editValue);

    if (editValue !== undef) {
        // This ensures that the value for the cell as it is currently stored for local editing
        // is displayed, rather than the saved value of the cell.

        return editValue;
    // record can be null if there's no record for this cell
    } else if (record == null) {
        return emptyCellValue;

    // show the result of getRecordSummary for summary fields
    // Note that if shouldApplyRecordSummaryToRecord is true it's already stored on the record
    // object so we've already picked it up

    } else if (
        this._shouldShowRecordSummary(
            groupSummaryRecordProperty, gridSummaryRecordProperty, field,record) &&
        !this.shouldApplyRecordSummaryToRecord(field))
    {
        value = this._getRecordSummary(
            fieldsHaveUserFormulas, fieldsHaveUserSummaries, recordNum, field);

    // Default - just look at the record.
    } else {

        // let the dataset return a value if it supports doing so

        if (data.getFieldValue && field) {
            value = data.getFieldValue(record, dataPath, field, this, "getRawValue");

        } else if (record.ownerDocument && field) {
            // if working with XML, fetch values via XPath selectors if so configured.  (Needed
            // for viewing XML elements when not dataBound, but could be moved to be a built-in
            // feature of both List and native Array)

            value = isc.xml.getFieldValue(record, fieldName, field);
        } else {
            value = isc.Canvas._getFieldValue(dataPath, field, record, this, true, "getRawValue");
            if (value == null) value = record[fieldName];
        }
    }

    // if the field has a 'getRawCellValue' attribute

    if (field && field.getRawCellValue) {
        // CALLBACK API:  available variables:  "viewer,record,recordNum,field,fieldNum,value"
        // Convert a string callback to a function
        isc.Func.replaceWithMethod(field, "getRawCellValue", "viewer,record,recordNum,field,fieldNum,value");

        // call it as a function (returns string | null)
        value = field.getRawCellValue(this, record, recordNum, field, fieldNum, value);
    }

    // handle user-formula and user-summary fields

    if (field) {

        if (this._shouldApplyUserFormulaAfterSummary(
                applyFormulaAfterSummary, fieldHasUserFormula, field) &&
            this._shouldShowUserFormula(
                groupSummaryRecordProperty, applyFormulaAfterSummary, field, record))
        {
            var currentUserFormula = field.userFormula;
            field.userFormula = fieldUserFormula;
            value = this.getFormulaFieldValue(field, record);
            field.userFormula = currentUserFormula;
        }
        // note: actual value computed later (after formatters have been applied
        // to raw value) - see getSummaryFieldValue() call in getFormattedCellValue
        if (fieldHasUserSummary) {
            var currentUserSummary = field.userSummary;
            field.userSummary = fieldUserSummary;
            this.getSummaryFunction(field);
            field.userSummary = currentUserSummary;
        }
    }
    return value;
},

// shouldShowRecordSummary - returns true if this is a summary field unless
// this is a summary row and there's an explicit summaryFunction (which takes precedence over
// any record summary function).

_$summary:"summary",
shouldShowRecordSummary : function (field, record) {
    return this._shouldShowRecordSummary(
        this.groupSummaryRecordProperty, this.gridSummaryRecordProperty, field, record);
},
_shouldShowRecordSummary : function (
    groupSummaryRecordProperty, gridSummaryRecordProperty, field, record)
{
    if (field && field.type == this._$summary) {
        if (record[groupSummaryRecordProperty]) {
            return (field.summaryFunction == null && field.getGroupSummary == null);

        } else if (record[gridSummaryRecordProperty]) {
            return (field.summaryFunction == null && field.getGridSummary == null);
        }
        return true;
    }
    return false;
},

// shouldShowUserFormula - As with record summaries, if you have a formula field
// you likely do not want the field-level record-formula to show in a summary row
// - instead the grid or group summary should be displayed as the field value.
shouldShowUserFormula : function (field, record) {
    return this._shouldShowUserFormula(
        this.groupSummaryRecordProperty, this.applyFormulaAfterSummary, field, record);
},
_shouldShowUserFormula : function (
    groupSummaryRecordProperty, applyFormulaAfterSummary, field, record)
{
    var isSummary = this._isSummaryRow ||
                     (record && record[groupSummaryRecordProperty]);
    if (isSummary && !this.__applyFieldFormulaAfterSummary(applyFormulaAfterSummary, field)) {
        return false;
    }
    return true;
},

//> @attr listGrid.applyFormulaAfterSummary (Boolean : false : IRW)
// If +link{listGridField.userFormula} is set for some field, and this grid is showing
// +link{listGrid.showGroupSummary,group summaries} or a
// +link{listGrid.showGridSummary,grid summary}, this property determines what field value
// should be present in those summary rows. Should the field apply the user-formula to the
// calculated summary row, or should it apply a standard grid or group summary to the
// user-formula values displayed in the grid?
// <P>
// May be overridden at the field level via +link{listGridField.applyAfterSummary}
// @visibility external
//<

applyFormulaAfterSummary:false,

//> @attr listGrid.refreshDependentFieldsForSummaries (Boolean : true : IRW)
// When summaries are refreshed, and a list of fields is passed to limit what's refreshed,
// should the dependent fields automatically be added to ensure they're included?
//<
refreshDependentFieldsForSummaries: true,

// If we have a formulaField, how should the cell value in a summary row be calculated?
// if applyFormulaAfterSummary is true we'llcalculate summary row values and apply
// the field-formula to that calculated row. Otherwise we will display "normal" summary
// values for the field in the row (for example summing all the calculated values in the
// formula field).
_applyFieldFormulaAfterSummary : function (field) {
    return this.__applyFieldFormulaAfterSummary(this.applyFormulaAfterSummary, field);
},
__applyFieldFormulaAfterSummary : function (applyFormulaAfterSummary, field) {
    var applyAfterSummary = field && field.applyAfterSummary;
    if (applyAfterSummary == null) applyAfterSummary = applyFormulaAfterSummary;
    if (applyAfterSummary == null) applyAfterSummary = false;
    //this.logWarn("For field:" + field.name + ", applyFormulaAfterSummary:" + applyAfterSummary);
    return applyAfterSummary;
},

_$blank: "blank",
_getCheckboxValueIconHTML : function (isSel, canSelect, disabled, field) {
    var icon,
        iconStyle;
    if (!canSelect) {
        // record cannot be selected but we want the space allocated for the checkbox anyway.
        icon = this._$blank;
    } else {
        var isPrinting = this.isPrinting;

        // checked if selected, otherwise unchecked
        var booleanBaseStyle = (isPrinting
                                ? this.printBooleanBaseStyle || this.booleanBaseStyle
                                : this.booleanBaseStyle);
        if (isSel) {
            icon = (isPrinting
                    ? this.printCheckboxFieldTrueImage || this.checkboxFieldTrueImage ||
                      this.printBooleanTrueImage || this.booleanTrueImage
                    : this.checkboxFieldTrueImage || this.booleanTrueImage);
            iconStyle = booleanBaseStyle == null ? null : booleanBaseStyle + "True";
        } else {
            icon = (isPrinting
                    ? this.printCheckboxFieldFalseImage || this.checkboxFieldFalseImage ||
                      this.printBooleanFalseImage || this.booleanFalseImage
                    : this.checkboxFieldFalseImage || this.booleanFalseImage);
            iconStyle = booleanBaseStyle == null ? null : booleanBaseStyle + "False";
        }
        if (icon == null) icon = this._$blank;
    }
    // if the record is disabled, make the checkbox image disabled as well
    if (disabled) {
        if (icon != this._$blank) icon = isc.Img.urlForState(icon, false, false, isc.StatefulCanvas.STATE_DISABLED);
        if (iconStyle != null) iconStyle += isc.StatefulCanvas.STATE_DISABLED;
    }
    if (icon == this._$blank) icon = isc.Canvas._nullSrcPlaceholder;

    var extraExtraStuff;
    if (iconStyle != null) {
        extraExtraStuff = "class='" + iconStyle + this._$singleQuote;
    }

    return this.getValueIconHTML(icon, field, extraExtraStuff);
},

//>    @method    listGrid.getCellValue()   ([A])
//          Obtains the display value for a specific cell according to the given input
//          parameters.<br>
//          To format the value displayed in the cell, make use of the
//          +link{listGrid.formatCellValue(),formatting} methods rather than
//          overriding this method directly.
//      @visibility external
//        @group    data
//
//        @param    record        (object)    the current record object
//        @param    recordNum    (number)    number of the record in the current set of displayed
//                                      record (e.g. 0 for the first displayed record)
//        @param    fieldNum    (number)    number of the field in the listGrid.fields array
//
//      @see    method:ListGrid.formatCellValue
//        @return    (string)    display value for this cell
//<
_$HR:"<HR>",
getCellValue : function (record, recordNum, fieldNum, gridBody) {

    if (gridBody == null) gridBody = this.getFieldBody(fieldNum);

    // If we're handed an empty record, we may be looking at a new edit row - if so get
    // the edit values instead
    if (record == null) {
        if (this.showNewRecordRow && this._isNewRecordRow(recordNum)) {
            return this.getNewRecordRowCellValue();
        }
        record = this._getEditValues(recordNum, fieldNum);
    } else {
        // Special cases:
        // if it's a separator row, return a horizontal rule

        if (record[this.isSeparatorProperty]) return this._$HR;

        // group controller node - write out the groupNodeHTML
        if (record._isGroup && fieldNum != null) {

            var currentField = this.fields[fieldNum],
                groupTitleField = this.getGroupTitleField(),
                isGroupTitleField;
            if (groupTitleField == null) {
                isGroupTitleField = currentField._isGroupTitleColumn;
            } else {
                isGroupTitleField = currentField.name == groupTitleField;
            }

            // If we're showing multiple fields and we're not showing a summary in the
            // header, explicitly empty all fields except the groupTitleField
            if ((!gridBody._gettingAutoSizeHTML && this.singleCellGroupHeaders())
                 || isGroupTitleField)
            {
                return this.getGroupNodeHTML(record, gridBody);
            } else if (!this.showGroupSummaryInHeader || this.isCheckboxField(currentField)) {
                return isc.nbsp;
            }
        }
    }

    // get the actual data record
    var field = this.fields[fieldNum],
        value = null;

    // if field is null, we've shrunk the number of columns, so forget it
    if (field == null) return isc.emptyString;

    // if this is a summary row and the field has a summaryValue specified, return that - all
    // the special fields (rowNumber, checkbox, expansion, remove) set this value to "&nbsp;"
    if (this._isSummaryRow && field.summaryValue) {
        return field.summaryValue;
    }

    // If this is the checkboxField, write out the checkbox HTML

    if (this.isCheckboxField(field)) {
        return this._getCheckboxValueIconHTML(!!this.selection.isSelected(record),
                                              this.body.canSelectRecord(record),
                                              record && record[this.recordEnabledProperty] == false,
                                              field);
    }

    // Determine whether we should be showing JUST a valueIcon
    // Do this before we figure out other HTML to avoid calculating stuff we're not going to
    // actually use.
    var icon,
        iconStyle,
        iconOnly = this.showValueIconOnly(field),
        isEditCell;
    if (record != null) {


        if (record[this.singleCellValueProperty] != null) {
            return record[this.singleCellValueProperty];
        }

        if (Array.isLoading(record)) {

            if (!isc.Browser.isSafari || fieldNum == 0) {
                return this.loadingMessage;
            }
            return isc.nbsp;
        }

        // If we are currently editing there are three possibilities for what we should write
        // out for each cell:
        // - This cell is being edited -- write out the edit form's element HTML
        // - This cell has pending edit values, but is not currently being edited.
        //   (this can happen if we're editing another cell in this row, or if an edit was not
        //   successfully saved for this cell) -- display the edit values.
        // - This cell should display the value for the record (default behavior)
        // We catch the first case here, and write out the edit-form item into the cell if
        // appropriate.
        // The other two cases will be handled by 'getRawCellValue()', which will check for
        // the presence of editValues on a cell.


        // if rowSpanning is enabled, and the user starts editing a row that
        // is spanned by some cell, behavior is governed by rowSpanEditMode:
        // - if rowSpanEditMode is "first", canEditCell will return false for the
        //   logical cell so we won't show an editor in the cell.
        // - otherwise, we show an editor in the cell and populate it with values
        //   based on the edit row.
        //   This means the value displayed in the editor will come from the
        //   edit-row, whereas the value displayed while not in edit mode comes from
        //   the first spanned record (by default).
        //   In this case the edit-value displayed may be different to the static
        //   value displayed in the spanning cell.
        //   The developer might use this mode if they have some advanced behavior like
        //   custom formatters that render a value for every spanned record, etc
        var editRowNum = this._editRowNum,
            editStartRow = editRowNum;
        if (editStartRow != null && this.allowRowSpanning) {
            editStartRow = this.getCellStartRow(editStartRow, fieldNum);
        }
        isEditCell = (this._editorShowing && editStartRow == recordNum &&
                      (!this.editByCell || this._editColNum == fieldNum) &&

                      this.canEditCell(editRowNum, fieldNum));
        if (isEditCell) {
            var editRecord = record;
            if (editRowNum != recordNum) {
                editRecord = this.getCellRecord(editRowNum, fieldNum);
            }
            // Avoid writing out live form items if we're just getting HTML for the
            // GR auto-sizer canvas.

            if (gridBody._gettingAutoSizeHTML) {
                value =this.getInactiveEditorCellValue(editRecord, editRowNum, fieldNum);
            } else {
                value = this.getEditItemCellValue(editRecord, editRowNum, fieldNum);
            }
        // Checking for this._editorShowing would mean that when changing edit cell,
        // hideInlineEditor would render the row with no inactive editors, and they
        // wouldn't get shown again on showEditForm for the new cell without another
        // row-refresh
        } else if (this._showInactiveEditor(fieldNum) && this.canEditCell(recordNum,fieldNum)) {

            value = this.getInactiveEditorCellValue(record, recordNum, fieldNum);
            // set isEditCell -- this will suppress the standard 'valueIcon' stuff
            isEditCell = true;
        } else {
            // get the value according to the field specification
            var field = this.fields[fieldNum],
                displayFieldValue,
                pickUpDisplayFieldValue;
            // If a displayField was specified on the field, and no separate optionDataSource,
            // we should display the value for the record from the displayField rather than the
            // data field.
            if (field.displayField != null) {
                pickUpDisplayFieldValue = !field.valueMap &&
                                          !field.getCellValue &&
                                          // this method actually checks for field.valueMap
                                          // being non null
                                          this._useDisplayFieldValue(field);

                if (pickUpDisplayFieldValue) {
                    var displayField = field.displayField;
                    displayFieldValue = this.getRawCellValue(record, recordNum, displayField, true);
                }
                // Note: we still need to proceed through the rest of the logic, since the
                // underlying data value drives the valueIcon
            }
            value = this.getRawCellValue(record, recordNum, fieldNum);
             // if the field has a 'getCellValue' attribute
            // NOTE: this is deprecated - overriding 'getCellValue()' at the Grid level is very
            // advanced, so for simple formatting (which is likely to be done at either the Field
            // or Grid level) we provide 'formatCellValue()' methods instead.)
            // Leave this code in place for Back-compat only.

            //>!BackCompat 2005.1.12
            if (field.getCellValue) {
                // CALLBACK API:  available variables:  "viewer,record,recordNum,field,fieldNum,value"
                // Convert a string callback to a function
                isc.Func.replaceWithMethod(field, "getCellValue",
                                                 "viewer,record,recordNum,field,fieldNum,value");
                value = field.getCellValue(this, record, recordNum, field, fieldNum, value);
            }
            //<!BackCompat


            icon = this.getValueIcon(field, value, record, recordNum);
            iconStyle = this.getValueIconStyle(field, value, record, recordNum);

            // if we're ONLY showing an icon we don't really need to figure out the text from
            // the valueMap.
            if (!iconOnly) {

                // if the field has an 'valueMap' parameter, treat the value as a key in the map
                var valueMap = field.valueMap;
                if (valueMap) {
                    value = this.resolveValueMap(value, field.valueMap);
                }
            }

            if (pickUpDisplayFieldValue) value = displayFieldValue;
        }
    }

    if (!isEditCell) {

        // If this is a summary row, avoid calling formatCellValue at all if we're not showing
        // a summary for the field.
        if (record &&
            ((record[this.groupSummaryRecordProperty] && !this.shouldShowGroupSummary(field)) ||
             (this._isSummaryRow &&
                ((record[this.gridSummaryRecordProperty] && !this.shouldShowGridSummary(field))
                 || (field.showGridSummary == false))
             )
            )
           )
        {
            value = this.emptyCellValue;
        } else {

            // If this cell isn't showing an editor we may need to write out an icon

            var iconHTML = null;

            if (icon != null) {
                var iconExtraExtraStuff = iconStyle == null ? null : "class='" + iconStyle + this._$singleQuote;
                iconHTML = this.getValueIconHTML(icon, field, iconExtraExtraStuff);
            }

            if (iconOnly) {
                // If there's no icon write out the empty cell value. This avoids us having
                // un-styled cells.
                if (!iconHTML || isc.isAn.emptyString(iconHTML)) iconHTML = this.emptyCellValue;
                value = iconHTML;
            } else {

                // apply hilites to capture htmlBefore/after
                var hilites = this.getFieldHilites(record, field);
                // Note hilites can == 0, so if(hilites) doesn't work

                var hiliteAfterFormat = field.hiliteHTMLAfterFormat;
                if (hiliteAfterFormat == null) hiliteAfterFormat = this.hiliteHTMLAfterFormat;
                if (hiliteAfterFormat == null) hiliteAfterFormat = true;
                if (hiliteAfterFormat) {
                    value = this._formatCellValue(value, record, field, recordNum, fieldNum);
                    // Note that hilites can == 0, so if (hilites) doesn't work
                    if (hilites != null) value = this.applyHiliteHTML(hilites, value);
                } else {
                    if (hilites != null) value = this.applyHiliteHTML(hilites, value);
                    value = this._formatCellValue(value, record, field, recordNum, fieldNum);
                }
                if (iconHTML) {
                    if (field.valueIconOrientation != isc.Canvas.RIGHT)
                        value = iconHTML + value;
                    else
                        value = value + iconHTML;
                }
            }

            // Only show error icon HTML if we're not showing an editor for the cell - otherwise
            // we'd get doubled error icons.
            if (this.isEditable() && this.showErrorIcons && this.cellHasErrors(recordNum, fieldNum)) {
                value = this.getErrorIconHTML(record, recordNum, fieldNum) + value;
            }

            // Add the hiliteIcons last
            var field = this.fields[fieldNum];
            var hilites = this.getFieldHilites(record, field);
            // Note that hilites can == 0, so if(hilites) won't work
            if (hilites != null) {
                if (!record || (record && !record.isGroupSummary) || (this.showHilitesInGroupSummary && field.showHilitesInGroupSummary != false)) {
                    value = this.applyHiliteIcon(hilites, field, value);
                }
            }
        }
    }
    // if current field is the groupTitleField, add indentation to the cell value
    var gtf = this.isGrouped ? this.getGroupTitleField() : null;
    if (gtf && this.fields[fieldNum].name == gtf) {
        var groupIndent =
            isc.Canvas.spacerHTML(this.groupIndentSize + this.groupLeadingIndent, 1);
        value = groupIndent + value;
    }
    return value;
},

//> @method listGrid.cellValueIsClipped()
// @include gridRenderer.cellValueIsClipped()
//<
cellValueIsClipped : function (rowNum, colNum) {
    var body = this.getFieldBody(colNum);
    colNum = this.getLocalFieldNum(colNum);
    return body.cellValueIsClipped(rowNum, colNum);
},

resolveValueMap : function (value, valueMap) {
    // NOTE: this can be really expensive, since we may eval the function that
    // returns the value map for each record!!!
    if (isc.isA.String(valueMap)) valueMap = this.getGlobalReference(valueMap);
    // NOTE: don't look up displayed values in Array valueMaps, which just list legal
    // values, and don't provide stored->displayed value mappings.
    if (!isc.isAn.Array(valueMap)) {
        if (isc.isAn.Array(value)) {
            var displayValue = [];
            for (var i = 0; i < value.length; i++) {
                var dataVal = isc.getValueForKey(value[i], valueMap, value[i]);
                displayValue[displayValue.length] = dataVal;
            }
            value = displayValue;
        } else {
            value = isc.getValueForKey(value, valueMap);
        }
    }
    return value;
},

// Formula/summary -related overrides from DBC
getTitleFieldValue : function (record) {
    var titleField = this.getDataSource().getTitleField(),
        title = this.getCellValue(record, this.getRecordIndex(record),
            this.getFieldNum(titleField), this.body
    );

    if (!title || title == "") {
        title = this.getRawCellValue(record, this.getRecordIndex(record), titleField, true);
    }
    return title;
},

getRawValue : function (record, fieldName) {
    var recordIndex = this.getRecordIndex(record);

    return this.getRawCellValue(record, recordIndex, fieldName, true);
},

getFormattedValue : function (record, fieldName, value) {
    var recordIndex = this.getRecordIndex(record),
        field = this.getSpecifiedField(fieldName),
        fieldNum = this.getFieldNum(fieldName);

    if (this.fieldIsVisible(field) && value == null) {
        return this.getCellValue(record, recordIndex, fieldNum, this.body);
    }
    if (value == null) value = this.getRawValue(record, fieldName);
    // if the field has an 'valueMap' parameter, treat the value as a key in the map
    var valueMap = field ? field.valueMap : null;
    if (valueMap) {
        // NOTE: this can be really expensive, since we may eval the function that
        // returns the value map for each record!!!
        if (isc.isA.String(valueMap)) valueMap = this.getGlobalReference(valueMap);
        // NOTE: don't look up displayed values in Array valueMaps, which just list legal
        // values, and don't provide stored->displayed value mappings.
        if (!isc.isAn.Array(valueMap)) {
            if (isc.isAn.Array(value)) {
                var displayValue = [];
                for (var i = 0; i < value.length; i++) {
                    var dataVal = isc.getValueForKey(value[i], valueMap, value[i]);
                    displayValue[displayValue.length] = dataVal;
                }
                value = displayValue;
            } else {
                value = isc.getValueForKey(value, valueMap);
            }
        }
    }
    var ret = this._formatCellValue(value, record, field, recordIndex, fieldNum);
    return ret;

},


// get the width of the specified field or null
getSpecifiedFieldWidth : function (fieldName) {

    var field = this.getField(fieldName);
    if (field == null) {
        // field may just be hidden - if so check for an explicit width on the
        // field object in our completeFields array and return that
        if (isc.isAn.Object(fieldName)) fieldName = fieldName[this.fieldIdProperty];
        field = this.getSpecifiedField(fieldName);
    }
    if (field == null) return;

    var fieldName = field[this.fieldIdProperty],
        members = this.header ? this.header.members : null,
        frozenMembers = this.frozenFields && this.frozenHeader ? this.frozenHeader.members : null,
        width;

    if (members || frozenMembers) {
        var member;

        if (members) member = members.find(this.fieldIdProperty, fieldName);
        if (!member && frozenMembers) {
            member = frozenMembers.find(this.fieldIdProperty, fieldName);
        }
        if (member) {
            if (member._userWidth && isc.isA.Number(member._userWidth)) {
                width = member._userWidth;
            }
        // If we don't have a member for the field, look at the specified width and
        // use it if possible.
        // This can happen if a field is currently hidden
        } else {
            if (field.width != null && isc.isA.Number(field.width)) {
                width = field.width;
            }
        }
    }
    return width;
},

getValueIconHTML : function (icon, field, extraExtraStuff) {

    var prefix = field.imageURLPrefix || field.baseURL || field.imgDir,
        suffix = field.imageURLSuffix,
        width = this.getValueIconWidth(field),
        height = this.getValueIconHeight(field),
        leftPad = this.getValueIconLeftPadding(field),
        rightPad = this.getValueIconRightPadding(field);

    if (suffix != null) icon += suffix;

    var iconHTML = isc.Canvas._getValueIconHTML(icon, prefix, width, height,
                                      leftPad, rightPad,
                                      // no need for an ID
                                      null,
                                      // pass in the LG as an instance - required
                                      // for generating the path of the valueIcon src
                                      this,
                                      extraExtraStuff);
    return iconHTML;
},

//>    @method    listGrid.getCellAlign()
// Return the horizontal alignment for cell contents. Default implementation returns
// +link{listGridField.cellAlign} if specified, otherwise +link{listGridField.align}.
//
// @see getCellStyle()
//
// @param   record (listGridRecord) this cell's record
// @param    rowNum    (number)    row number for the cell
// @param    colNum    (number)    column number of the cell
// @return    (Alignment)     Horizontal alignment of cell contents: 'right', 'center', or 'left'
// @visibility external
//<
getCellAlign : function (record, rowNum, colNum) {
    var isRTL = this.isRTL();
    // single cells are aligned according to RTL - skip checkboxField (which needs center align)
    if (record && record[this.singleCellValueProperty] != null &&
        (!this.showSingleCellCheckboxField(record) ||
         !this.isCheckboxField(this.getField(colNum)))
       )
    {
        return isRTL ? isc.Canvas.RIGHT : isc.Canvas.LEFT;
    }

    return this.getFieldCellAlign(colNum, isRTL);
},

// helper for getHeaderButtonTitle() - not dependent on field.cellAlign
getFieldHeaderAlign : function (fieldNum, isRTL) {
    var field = this.getField(fieldNum);
    if (!field) return isRTL ? isc.Canvas.RIGHT : isc.Canvas.LEFT;

    // This is set up in applyFieldDefaults if not explicitly specified on the field config
    var align = field.align;

    // If 'reverseRTLAlign' is true, flip the explicitly specified alignment.
    // This is true by default and makes sense for common cases where you really want
    // specified 'left' / 'right' to have the meaning of 'start'/'end'
    if (isRTL && this.reverseRTLAlign) {
        if      (align == isc.Canvas.RIGHT) align = isc.Canvas.LEFT;
        else if (align == isc.Canvas.LEFT) align = isc.Canvas.RIGHT;
    }
    return align;
},

getFieldCellAlign : function (fieldNum, isRTL) {

    var field = this.getField(fieldNum);

    if (!field) return isRTL ? isc.Canvas.RIGHT : isc.Canvas.LEFT;


    if (field.cellAlign == null && field.userFormula) {
        return isRTL ? isc.Canvas.LEFT : isc.Canvas.RIGHT;
    }
    // This is set up in applyFieldDefaults if not explicitly specified on the field config
    var align = field.cellAlign || field.align;
    // If 'reverseRTLAlign' is true, flip the explicitly specified alignment.
    // This is true by default and makes sense for common cases where you really want
    // specified 'left' / 'right' to have the meaning of 'start'/'end'
    if (isRTL && this.reverseRTLAlign) {
        if (align == isc.Canvas.RIGHT) align = isc.Canvas.LEFT;
        else if (align == isc.Canvas.LEFT) align = isc.Canvas.RIGHT;
    }
    return align;
},

// Helper method
// If we're showing a singleCellValue for a record, should we show the checkboxField to the
// left of the single value
showSingleCellCheckboxField : function (record) {
    return (this.getCurrentCheckboxField() != null) && record &&
            !record._isGroup && !record[this.isSeparatorProperty];
},

//>    @method    listGrid.getCellVAlign()
// Return the vertical alignment for cell contents.
// Expected values are: 'top', 'center', or 'bottom'
//
// @see getCellStyle()
//
// @param   record (listgridRecord) this cell's record
// @param    rowNum    (number)    row number for the cell
// @param    colNum    (number)    column number of the cell
// @return    (Alignment)     Vertical alignment of cell contents: 'right', 'center', or 'left'
// @visibility external
//<
// Unset by default


// Helper method:
// If a displayField was specified for a field, should we pick up the 'displayField' value for
// this record and display it here?
// Basically this makes sense if no optionDataSource was specified, unless overridden by a valueMap
_useDisplayFieldValue : function (field) {
    // no display field, or we have an explicit valueMap
    if (!field || field.valueMap || field.displayField == null ||
        (field.displayField == field.name))
    {
        return false;
    }

    // displayField and no explicit optionDataSource, pick up the value from the record
    // unless explicitly suppressed by the developer
    if (field.optionDataSource == null) {
        if (field.displayValueFromRecord == false) return false;
        return true;
    }

    // field has an optionDataSource and autoFetchDisplayMap is true - in this case we'll
    // fetch against the ODS and build a valueMap from the data we get back - so even if we don't
    // have a valueMap yet, we soon will!
    var fetchDisplayMap = field.autoFetchDisplayMap;
    if (fetchDisplayMap == null) fetchDisplayMap = this.autoFetchDisplayMap;
    if (fetchDisplayMap) return false;

    // At this point, if optionDataSource and valueField matches we'll use the
    // value from the record - however allow the developer to override this to either
    // a) force the display field value to be picked up (as if there was no ODS specified - somewhat
    // probable if autoFetchDisplayMap has been disabled, since the ODS has really no other meaning
    // to the grid) or
    // b) suppress the display field value even if the dataSources do match (for example may want
    // to show display-field values in edit mode only.
    if (field.displayValueFromRecord  != null) return field.displayValueFromRecord;

    // same dataSource / same valueField - pick up the value from the record.
    return (field.valueField == null || field.valueField == field.name) &&
            (isc.DS.get(field.optionDataSource) == this.getDataSource());
},

// Row Spanning
// ---------------------------------------------------------------------------------------

//> @attr listGrid.allowRowSpanning (Boolean : false : IR)
// Should cells in this grid be allowed to span multiple rows?
// If set to <code>true</code>, the +link{listGrid.getRowSpan()} method will be
// called for every cell when rendering out the listGrid to determine how many
// rows the cell should span.
// <P>
// See +link{listGrid.getRowSpan()} for more details
// @visibility external
//<
allowRowSpanning:false,

//> @method listGrid.getRowSpan()
// Return how many rows this cell should span.  Default is 1.
// <P>
// This method will only be called if +link{listGrid.allowRowSpanning} is set to
// <code>true</code>
// <P>
// When using row spanning, consider setting +link{useRowSpanStyling} to enable
// row-span-sensitive styling behaviors.
// <P>
// Note that the standard implementation assumes that the number of rows spanned by cells
// decreases or stays the same, starting with the first (leftmost) column in the grid and
// moving rightwards.
// <P>
// When using row spanning:
// <ul>
// <li> APIs that allow modifying the contents of cells (such as +link{getCellStyle()} or
// +link{listGridField.formatCellValue()}) will be called only once per row-spanning cell
// <li> when using +link{listGrid.canSelectCells,cell-based selection}, only the
// spanning cell is considered selected, and not any of the cells spanned through.  For
// example, if the cell at row 2 column 0 spans 2 cells, +link{CellSelection,CellSelection.isSelected()}
// will be true for 2,0 but false for 3,0.
// <li> if using incremental rendering (either horizontal or vertical),
// <code>getRowSpan()</code> may be called for a rowNum <b>in the middle of a spanning
// cell</b>, and should return the remaining span from that rowNum onward.
// <li> cell-level events such as +link{listGrid.recordClick()} will report the logical
// rowNum for spanned cells. In other words if a cell spans two rows, a different
// rowNum parameter will be passed to the recordClick handler depending on whether the user
// clicks at the top of the spanning cell or the bottom. Developers can normalize
// this to the starting cell via the +link{listGrid.getCellStartRow()} API.
// <li> for cells that span multiple records, editing behavior may be controlled by the
// +link{rowSpanEditMode, rowSpanEditMode} attribute.
// <li> rowSpanning can be used in conjunction with
//  +link{showRecordComponents(),recordComponents}. If record
// component are enabled on a grid with row-spanning cells the behavior is as follows:
//   <ul>
//    <li>Having +link{recordComponentPosition} set to "expand" is not currently supported
//        for grids that render out spanning cells.
//    <li>The method to retrieve / create record components will not be run for cells that
//        are "spanned". In other words if the first row in a grid spans 2 rows for some
//        field, the second logical row is "spanned" for that field - that cell doesn't
//        render any content and won't attempt to create a recordComponent.
//    <li>If +link{showRecordComponentsByCell} is false, the method to create record
//        components will be called for every non-spanned cell in the first column
//        of the grid.
//    <li>Percentage sizing of record components spanning multiple cells will be
//        calculated relative to the set of spanned cells.
//   </ul>
// </ul>
// <P>
// More generally, the ListGrid has a data model of one +link{Record} per row, and spanning cells
// doesn't fit well with this model, meaning that many ListGrid features are incompatible
// with rowSpanning.
//
// @include gridRenderer.getRowSpan()
// @visibility external
//<
// Note the limitation of row spanning never increasing going left to right comes up
// in a couple of spots:
// - the default selection model basically assumes the number of rows spanned is reducing
//   and performs a selection of all spanned cells in the row, moving rightwards
// - the getRowHeight implementation makes this assumption to ensure frozen body row
//   heights are correct (we have an undocumented flag to remove this limitation, but
//   this introduces some extra calculation that would have to run, making this undesirable
//   for many-columned grids).

//> @method listGrid.getCellRowSpan()
// @include gridRenderer.getCellRowSpan()
// @visibility external
//<

//> @method listGrid.getCellStartRow()
// @include gridRenderer.getCellStartRow()
// @visibility external
//<

//> @attr listGrid.useRowSpanStyling (boolean : null : IR)
// Enables various styling behaviors that potentially make sense when +link{getRowSpan()} has
// been overriden to introduce spanning cells, and spanning is largest on the left and
// smaller as cells go to the right.  Specifically:
// <ul>
// <li> computes +link{alternateRecordStyles,banded styling} based on the span of the
//      cell in the left-most column
// <li> enables +link{listGrid.canSelectCells,cell-level selection}, including
//      +link{useCellRollOvers,cell-level rollover} styling
// <li> enables row-span-sensitive cell selection.  See also +link{rowSpanSelectionMode}
//      for available behaviors
// </ul>
// <P>
// Because this setting enables +link{canSelectCells}, it is incompatible with any APIs
// that expect a record-oriented data model.
// <P>
// Because this setting only makes sense when row spanning decreases from the first column
// to the last, it has unspecified behavior with +link{listGrid.canReorderFields}.
//
// @visibility external
//<

//> @type RowSpanSelectionMode
// Behavior of selection when row spanning is active.  See +link{listGrid.useRowSpanStyling}.
// @value "forward" when a cell is clicked on, select any cells in subsequent columns which
//                  are at least partially spanned by the clicked cell
// @value "both"    when a cell is clicked on, selects any cells in any other columns which
//                  are at least partially spanned by the clicked cell
// @value "outerSpan" behaves like "forward", except as though the cell in the first column
//                    was clicked instead.  If the largest row spans are in the first
//                    column and all cells in subsequent columns do not extend out of the
//                    first cell's span, this creates a row-like selection model where the
//                    span of the left-most cell defines the "row" of cells being selected.
// @visibility external
//<

//> @attr listGrid.rowSpanSelectionMode (RowSpanSelectionMode : "forward" : IR)
// Chooses the selection mode when +link{useRowSpanStyling} is enabled.  See
// +link{RowSpanSelectionMode}.
//
// @visibility external
//<
rowSpanSelectionMode:"forward",

//> @attr listGrid.rowSpanEditMode (RowSpanEditMode : "first" : IRWA)
// If +link{listGrid.allowRowSpanning} is enabled, this property may be used to specify
// editing behavior for cells that span multiple rows.
// @visibility external
//<
rowSpanEditMode:"first",

//> @type RowSpanEditMode
// When +link{listGrid.allowRowSpanning} is enabled, certain cells may span multiple
// rows. In this case, the cell displays the value from the record in the first row.
// If the grid is +link{listGrid.canEdit,editable} (and the
// +link{listGridField.canEdit,field is also editable}), these settings allow the user
// to specify what happens to the data when the user edits this cell.
// <P>
// Note that in this scenario, a user may begin an edit on the row-spanning cell directly
// (via double-click for example), or on a cell in another column in any of the rows
// spanned by the cell. The appropriate behavior with respect to user-experience and how
// the data is manipulated will depend on the application in question. Developers may
// of course entirely disable editing for the field via +link{listGridField.canEdit} or
// +link{listGrid.canEditCell()}.
// <P>
// See also: +link{listGrid.useRowSpanStyling}
//
// @value "first" This setting assumes that only the field-value for the first record
//  spanned by this cell is significant. In this case the editor will only show for this
//  cell if the user is editing the first spanned record. If the user initialized the edit
//  on another spanned row, the editor will not show for this field.
// @value "each" This setting assumes that each row's values are logically separate, so if
//  a cell spans multiple rows, and a user initializes an edit on some cell in the
//  second spanned row, the spanning cell will show an editor containing the value for
//  the second spanned row. This may differ from the value displayed when not
//  in edit mode (which is derived from the first spanned row by default). This setting may
//  be useful for developers who which to implement their own logic on how to handle
//  spanning cell display values and/or edit values (for example by using custom
//  +link{listGridField.formatCellValue(),formatting} and applying custom logic to handle
//  editing on +link{listGridField.editorEnter} and +link{listGridField.editorExit}).
//
// @visibility external
//<


// Value Icons
// ---------------------------------------------------------------------------------------

_valueIconStyleTemplate:[
    "style='margin-left:",  // [0]
    ,                       // [1] - icon padding
    "px;margin-right:",     // [2]
    ,                       // [3] - icon padding
    "px;'"
],

//> @method ListGrid.getValueIcon()
// Returns the appropriate valueIcon for a cell based on the field and the data value for the
// cell. Default implementation returns null if +link{ListGridField.suppressValueIcon} is true
// otherwise looks at +link{ListGridField.valueIcons}.
// @param field (ListGridField) field associated with the cell
// @param value (any) data value for the cell's record in this field.
// @param record (ListGridRecord) record associated with this cell
// @return (SCImgURL) url for the icon
// @group imageColumns
// @visibility external
//<
// We need the rowNum for checking whether the cell is editable
// calculate this from the record object via findRowNum(), but also support being passed an
// explicit rowNum for critical path code
getValueIcon : function (field, value, record, rowNum) {
    if (!field.valueIcons || field.suppressValueIcon) {
        if (this._formatBooleanFieldAsImages(field)) {
            var img;
            if (value) {
                img = (this.isPrinting
                       ? this.printBooleanTrueImage || this.booleanTrueImage
                       : this.booleanTrueImage);
            } else {
                img = (this.isPrinting
                       ? this.printBooleanFalseImage || this.booleanFalseImage
                       : this.booleanFalseImage);
            }
            // if the cell can't be edited and can be toggled, make sure it displays the
            // disabled checkbox icon
            rowNum = (rowNum != null) ? rowNum : this.findRowNum(record);
            var colNum = field.masterIndex;
            if (!this.canEditCell(rowNum, colNum) && field.canToggle && img != this._$blank) {
                img = isc.Img.urlForState(img, false, false, isc.StatefulCanvas.STATE_DISABLED);
            }
            if (img == this._$blank) {
                img = isc.Canvas._nullSrcPlaceholder;

            // If no image was specified, still write out a blank gif into the slot - this
            // allows us to recognize events over the (invisible) icon for canToggle behavior
            } else if (img == null) {
                img = isc.Canvas.getImgURL(isc.Canvas._blankImgURL);
            }
            return img;
        }
        return null;
    }
    var icon = field.valueIcons[value];
    return icon;
},

//> @method listGrid.getValueIconStyle() [A]
// Returns the CSS style for a cell based on the field and the data value for the cell.
// @param field (ListGridField) field associated with the cell
// @param value (any) data value for the cell's record in this field.
// @param record (ListGridRecord) record associated with this cell
// @return (CSSStyleName) CSS style to use, or null if no style should be used
//<
getValueIconStyle : function (field, value, record, rowNum) {
    var booleanBaseStyle = (this.isPrinting ? this.printBooleanBaseStyle || this.booleanBaseStyle : this.booleanBaseStyle);
    if (booleanBaseStyle && field && (!field.valueIcons || field.suppressValueIcon)) {
        if (this._formatBooleanFieldAsImages(field)) {
            var imgStyle = booleanBaseStyle + (value ? "True" : "False");
            // if the cell can't be edited and can be toggled, make sure it displays the
            // disabled checkbox style
            rowNum = (rowNum != null) ? rowNum : this.findRowNum(record);
            var colNum = field.masterIndex;
            if (!this.canEditCell(rowNum, colNum) && field.canToggle) {
                imgStyle += isc.StatefulCanvas.STATE_DISABLED;
            }
            return imgStyle;
        }
    }
    return null;
},

// Helpers to get padding on each side of a field's valueIcon
getValueIconLeftPadding : function (field) {
    return (field && field.valueIconLeftPadding != null ? field.valueIconLeftPadding
            : this.valueIconLeftPadding || 0);
},

getValueIconRightPadding : function (field) {
    return (field && field.valueIconRightPadding != null ? field.valueIconRightPadding
            : this.valueIconRightPadding || 0);
},

// showValueIconOnly - returns true if the valueIcon should be displayed without any
// text value for some field.
_$boolean:"boolean",
showValueIconOnly : function (field) {
    if (field.showValueIconOnly != null) return field.showValueIconOnly;

    // See discussion near showValueIconOnly docs.
    // If we have a valueIcons map, with no vmap, return text AND icon
    // if we have both valueIcons and a vmap, return just the icon
    // if we have no icon map, obviously return text (and icon, which is null)
    if (field.valueIcons != null && field.valueMap != null) return true;

    // If we are looking at a boolean field for which we want to show just the checkbox images
    // return true - otherwise false
    return this._formatBooleanFieldAsImages(field);

},

//> @method ListGrid.getValueIconWidth()
// If some field is showing valueIcons, this method returns the width those items should render
// Default implementation derives this from the first specified of
// +link{ListGridField.valueIconWidth}, +link{ListGridField.valueIconSize},
// +link{ListGrid.valueIconWidth}, or +link{ListGrid.valueIconSize}
// @param field (object) field for which we're retrieving the valueIcon width
// @return (number) width for the icon
// @group imageColumns
// @visibility internal
//<
getValueIconWidth : function (field) {

    if (this.isCheckboxField(field)) return this._getCheckboxFieldImageWidth();
    if (this._formatBooleanFieldAsImages(field)) return this.booleanImageWidth;

    return (field.valueIconWidth != null ? field.valueIconWidth  :
                (field.valueIconSize != null ? field.valueIconSize :
                    (this.valueIconWidth != null ? this.valueIconWidth : this.valueIconSize)));
},

//> @method ListGrid.getValueIconHeight()
// If some field is showing valueIcons, this method returns the height those items should render
// Default implementation derives this from the first specified of
// +link{ListGridField.valueIconHeight}, +link{ListGridField.valueIconSize},
// +link{ListGrid.valueIconHeight}, or +link{ListGrid.valueIconSize}
// @param field (object) field for which we're retrieving the valueIcon height
// @return (number) height for the icon
// @group imageColumns
// @visibility internal
//<
getValueIconHeight : function (field) {
    if (this.isCheckboxField(field)) return this._getCheckboxFieldImageHeight();
    if (this.isExpansionField(field)) return this._getExpansionFieldImageHeight();
    if (this._formatBooleanFieldAsImages(field)) return this.booleanImageHeight;

    return (field.valueIconHeight != null ? field.valueIconHeight  :
                (field.valueIconSize != null ? field.valueIconSize :
                    (this.valueIconHeight != null ? this.valueIconHeight : this.valueIconSize)));
},

// New record row: optional special row added to encourage new record entry
// ---------------------------------------------------------------------------------------

_isNewRecordRow : function (recordNum) {
    return this.showNewRecordRow && (recordNum == this.getTotalRows()-1);
},

//> @method ListGrid.getNewRecordCellValue()
//  Returns the contents to display in the new record row.
//  Note that this row displays a single cell spanning every column.
//  @return (string)    Value to display in new record row. Default
//                      implementation returns <code>"-- Add New Row --"</code>
//<
getNewRecordRowCellValue : function () {
    return '<div align="center">' + this.newRecordRowMessage + '</div>';
},

// ---------------------------------------------------------------------------------------
getErrorIconHTML : function (record, rowNum, colNum) {
    var errors = this.getCellErrors(rowNum, colNum);
    if (errors == null) return isc._emptyString;

    var HTML = this.imgHTML(this.errorIconSrc, this.errorIconWidth, this.errorIconHeight, null,
                            " isErrorIcon='true'");

    var isRTL = this.isRTL(),
        align = this.getCellAlign(record, rowNum, colNum),
        spaceOnLeft = ((isRTL && align === isc.Canvas.LEFT) ||
                       (!isRTL && align === isc.Canvas.RIGHT));

    // Since we're writing the icon out to the left of our content, write a spacer out to the
    // right of the image to give us some padding between the image and the cell content

    var spacerHTML;
    if (isc.Browser.isIE && this._editorShowing && this.getEditRow() == rowNum &&
        (!this.editByCell || this.getEditCol() == colNum))
    {
        spacerHTML = " ";
    } else {

        spacerHTML = isc.Canvas.spacerHTML(this.cellPadding, "auto");
    }

    if (spaceOnLeft) {
        HTML = spacerHTML + HTML;
    } else {
        HTML += spacerHTML;
    }

    return HTML;
},

// Handle events over the error icon by showing a hover containing the
// error text
_handleErrorIconOver : function (rowNum, colNum) {
    this._overErrorIcon = [rowNum,colNum];

    // This will fire the method on a delay unless the hover is already up
    // in which case it'll update it immediately.
    isc.Hover.setAction(this, this._handleErrorIconHover);


},
_handleErrorIconHover : function () {
    if (this._overErrorIcon == null) return;

    var rowNum = this._overErrorIcon[0],
        colNum = this._overErrorIcon[1];

    var errors = this.getCellErrors(rowNum, colNum);
    if (errors == null) return;

    var promptString = isc.FormItem.getErrorPromptString(errors);
    isc.Hover.show(promptString, this._getHoverProperties());
},

_handleErrorIconOut : function () {
    delete this._overErrorIcon;

    if (isc.Hover.isActive) {
        isc.Hover.clear();
    }
},

// _formatCellValue: Helper method to format the static cell value using developer defined
// formatCellValue() methods.
_$text:"text",
_formatCellValue : function (value, record, field, rowNum, colNum) {
    if (field && field.userSummary) {
        value = this.getSummaryFieldValue(field, record);

    } else {
        // If this is a summary row (group or grid level) we support custom formatting.
        // If custom formatters (formatGroupSummary / formatGridSummary) are present, apply them
        // and don't then run through additional formatting logic.
        var customFormatApplied = false;
        if (record && record[this.groupSummaryRecordProperty]) {
            if (field && field.formatGroupSummary) {
                if (!isc.isA.Function(field.formatGroupSummary)) {
                    field.formatGroupSummary =
                        isc.Func.expressionToFunction("value", field.formatGroupSummary);
                }
                if (isc.isA.Function(field.formatGroupSummary)) {
                    value = field.formatGroupSummary(value);
                    customFormatApplied = true;
                }
            }
        }


        if (record && this._isSummaryRow) {
            if (field && field.formatGridSummary) {
                if (!isc.isA.Function(field.formatGridSummary)) {
                    field.formatGridSummary =
                        isc.Func.expressionToFunction("value", field.formatGridSummary);
                }
                if (isc.isA.Function(field.formatGridSummary)) {
                    value =  field.formatGridSummary(value);
                    customFormatApplied = true;
                }
            }
        }

        // If we already ran through group/grid summary formatters, don't run
        // standard cellFormatters on top of them!
        if (!customFormatApplied) {

            if (field && field.formatCellValue != null) {
                value = field.formatCellValue(value,record,rowNum,colNum,this);
            } else if (field && field.cellValueTemplate) {
                // NOTE:
                // - probably don't need grid.cellValueTemplate, as this would be rare
                // - not exposed publicly yet
                // - might want XSLT option
                value = field.cellValueTemplate.evalDynamicString(this, {
                            value:value, record:record, field:field});
            // listGrid-wide formatter
            } else if (this.formatCellValue != null) {
                value = this.formatCellValue(value, record, rowNum, colNum);
            } else if (field && field.format && (isc.isA.Number(value) || isc.isA.Date(value))) {
                value = isc.isA.Number(value) ? isc.NumberUtil.format(value, field.format)
                                              : isc.DateUtil.format(value, field.format);


            } else if (field && !field._suppressTypeFormatting) {

                value = this.applyCellTypeFormatters(value, record, field, rowNum, colNum);
            }
        }
    }
    // formatValueAsString() performs final conversions such as catching the case
    // where the value is null or "" and mapping to "&nbsp;" in the cell
    // We already do this as part of "getDefaultFormattedValue()".
    // A custom formatCellValue implementation may call 'getDefaultFormattedValue()' directly
    // in this case 'formatValueAsString()' will actually run twice, but should have
    // no ill effects since since the string won't be further modified by a second pass
    // through this method.
    value = this.formatValueAsString(value, record, field, rowNum, colNum);

    // hook for final processing of the display value that is applied to the actual display
    // value as derived by the various other hooks

    if (this.formatDisplayValue) {
        value = this.formatDisplayValue(value, record, rowNum, colNum);
    }

    // support escapeHTML flag per field
    if (field && field.escapeHTML) {
        // don't escape "&nbsp;" or the emptyCellValue, though, since DataSourceField.emptyCellValue's
        // documented type is HTMLString
        if (isc.isA.String(value) && value != this._$nbsp &&
            (field.emptyCellValue == null || value != field.emptyCellValue))
        {
            value = value.asHTML();
        }
    }

    return value;
},

//> @method listGrid.getDefaultFormattedValue() [A]
// Get the value for some cell with default formatters applied.
// <P>
// This method is useful for cases where a developer wishes to
// conditionally customize a cell's formatting, but needs to see what
// the default formatted value would be.
// <P>
// For example - a developer might wish to apply a custom
// +link{ListGridField.formatCellValue(),formatter} to some
// <code>link</code> type field, and be able to return the default
// active link HTML in some cases. In this case a formatter could
// check for the conditions in which custom formatting should be applied
// and run appropriate custom logic to generate a value for display - otherwise
// return the result of this method to leave the standard formatted-value intact.
//
// @param record (Record) the cell's record object
// @param rowNum (int) rowNum for the cell
// @param colNum (int) colNum for the cell
// @return (string) Cell value with default formatters applied
// @see ListGridField.formatCellValue
// @visibility external
//<

getDefaultFormattedValue : function (record, rowNum, colNum) {
    var field = this.getField(colNum);
    var value = this.applyCellTypeFormatters(
                    this.getRawCellValue(record, rowNum, colNum),
                    record, field, rowNum, colNum
                );
    return this.formatValueAsString(value, record, field, rowNum, colNum);
},

//> @method listGrid.getDefaultFormattedFieldValue() [A]
// Get a field value for some record with default field formatters applied.
// <P>
// This method differs from +link{getDefaultFormattedValue()} in a couple of ways.
// Firstly, this method does not rely on the rowNum and colNum parameters to find the
// record and field in the grid.<br>
// Secondly, unlike +link{getDefaultFormattedValue()} this method <i>will</i> call
// any explicit cell formatter specified on the field passed in (though it will not
// call a +link{listGrid.formatCellValue(),component level formatter} if one exists).
// <P>
// This is useful for cases where a developer wishes to
// display a formatted value for some record and field combination which does not
// necessarily map to a cell displayed in the ListGrid.
// <P>
// If <code>rowNum</code> and <code>colNum</code> parameters are passed through to
// the field level cell formatter if one exists. If not explicitly provided these are
// defaulted to -1.
//
// @param record (Record) the record object
// @param field (ListGridField) the field object
// @param [rowNum] (int) rowNum (passed to any field level cell formatter)
// @param [colNum] (int) colNum (passed to any field level cell formatter)
// @return (string) Default formatted value
// @see ListGridField.formatCellValue
// @visibility external
//<

getDefaultFormattedFieldValue : function (record, field, rowNum, colNum) {
    var rawValue = isc.DataSource.getFieldValue(field, record);

    if (rowNum == null) rowNum = -1;
    if (colNum == null) colNum = -1;

    var value;
    if (field && field.formatCellValue != null) {
        value = field.formatCellValue(rawValue,record,rowNum,colNum,this);
    } else {
         value = this.applyCellTypeFormatters(rawValue, record, field, rowNum, colNum);
    }
    return this.formatValueAsString(value, record, field, rowNum, colNum);
},

// Apply type-formatters to a cell value.
applyCellTypeFormatters : function (value, record, field, rowNum, colNum, isMultipleElement) {

    if (!isMultipleElement && field && field.multiple && isc.isA.Array(value)) {
        var values = [];
        for (var i = 0; i < value.length; i++) {
            values[i] = this.applyCellTypeFormatters(value[i], record, field,
                                                    rowNum, colNum, true);
        }
        // multipleValueSeparator documented at the DataSource level
        return values.join(field.multipleValueSeparator || ", ");
    }

    // check for formatter defined on a SimpleType definition
    if (field && field._shortDisplayFormatter != null) {
        value = field._simpleType.shortDisplayFormatter(value, field, this, record, rowNum, colNum);
    // We apply some standard (default) formatters to fields with particular data types.
    // NOTE: these should be moved to the built-in SimpleType definitions
    } else if (field && field._typeFormatter != null) {
        value = field._typeFormatter(value, field, this, record, rowNum, colNum);
    }

    // Note: this method does *not* run if there is a custom formatter
    // applied to a field (including a custom grid/group formatter for grid/group summary rows).
    //
    // apply the plural title if the special flag was set on the record
    if (value != null && !isc.isA.emptyString(value) &&
        record && field && record[this.recordApplyPluralTitleProperty])
    {
        var fieldNames = record[this.recordApplyPluralTitleProperty];
        if (fieldNames.contains(field.name)) {
            var title = field.pluralTitle;
            if (title == null) title = field.title;
            if (title != null) value += " " + title;
        }
    }
    // For summary rows where the summary method returned null (indicating unable to calculate a
    // summary), return the invalidSummaryValue if specified.
    if (value == null &&
        record && field &&
        this.invalidSummaryValue != null &&
        ((record[this.groupSummaryRecordProperty] && this.shouldShowGroupSummary(field)) ||
         (record[this.gridSummaryRecordProperty] && this.shouldShowGridSummary(field)))
       )
    {
        value = this.invalidSummaryValue;
    }

    return value;
},

// formatValueAsString: This takes a value to be displayed in a cell and ensures it displays
// correctly.
// This is run after any custom formatters / type-based formatters have been applied to
// the value.
formatValueAsString : function (value, record, field, rowNum, colNum) {

    // For "empty" values, write out the emptyCellValue

    if (value == null || isc.is.emptyString(value)) {
        // if the field specifies a 'emptyCellValue' (or 'emptyDisplayValue')
        if (this._resolveEmptyDisplayValue(field) != this.emptyCellValue) {
            // return the field-specific value
            value = this._resolveEmptyDisplayValue(field);
        } else if (field.type == "summary") {
            value = this.invalidSummaryValue;

        } else {
            // otherwise return the emptyCellValue for the entire list
            value = this.emptyCellValue;
        }

    // In IE, an element containing only whitespace characters (space or enter) will not show css
    // styling properly.

    } else if (this._emptyCellValues[value] == true) {
        value = this._$nbsp;

    // convert the value to a string if it's not already
    } else if (!isc.isA.String(value)) {
        // For date type values we want to respect dateFormatter / timeFormatter specified at the
        // field or component level.
        // If the specified fieldType is "date", "datetime" or "time" this is already handled by the
        // _typeFormatter function (which calls formatDateCellValue / formatTimeCellValue for
        // these fields)

        if (isc.isA.Date(value)) {

            // We already know the "type" of this field is not "time" (as formatTimeCellValue)
            // would have run and converted it to a string already.
            // So we'll actually only format as time if the field has ane explicit timeFormatter
            // and no explicit dateFormatter.
            if (this._formatAsTime(field)) {
                var formatter = this._getTimeFormatter(field);
                var isLogicalTime = isc.SimpleType.inheritsFrom(field.type, "time");
                value = isc.Time.toTime(value, formatter, isLogicalTime);
            } else {

                var isDatetime = field && isc.SimpleType.inheritsFrom(field.type, "datetime"),
                    isLogicalDate = !isDatetime && field && isc.SimpleType.inheritsFrom(field.type, "date"),
                    formatter = this._getDateFormatter(field);
                if (isDatetime) value = value.toShortDateTime(formatter, true);
                else value = value.toShortDate(formatter, !isLogicalDate);
            }
        } else {
            value = isc.iscToLocaleString(value);
        }
    }
    return value;
},

// these Strings can be considered to be "empty" cells, causing bad styling.  Replace with
// '&nbsp;'

_emptyCellValues:{" ":true, "\n":true, "\r":true, "\r\n":true},
_$nbsp:"&nbsp;",

//>    @method listGrid.getEditItemCellValue()   ([IA])
//          Returns the HTML for a cell within a row that is being edited (as a result of a call
//          to 'editRow')<br>
//          Will <i>not</i> call 'updateEditRow()' to update the values displayed in the edit
//          row - this must be handled by calling methods, once we know the form element has
//          been written into the DOM.
//      @visibility internal
//        @group    editing
//
//      @param  record      (object)    the current record object
//      @param  rowNum      (number)    index of the record containing this cell
//      @param  colNum      (number)    index of the field containing this cell
//
//        @return    (string)    display value for this cell
//<
getEditItemCellValue : function (record, rowNum, colNum) {
    var itemName = this.getEditorName(rowNum, colNum);

    // If the editRowForm or item haven't yet been created, no op - implies this
    // is being called at an invalid time

    if (!this._editRowForm || !this._editRowForm.getItem(itemName)) {
        return "&nbsp;"
    }

    // Write a form item out into this cell.
    // We have already created the '_editRowForm' - a dynamic form with an appropriate set of
    // form items (see 'editRow').
    // Make use of the appropriate item's getStandaloneHTML() to write out the form element

    // If we have any errors for the field, set them on the form item too so the error icon
    // gets rendered out
    var errors = this.getCellErrors(rowNum, colNum);
    if (errors) {
        this._editRowForm.setFieldErrors(itemName, errors);
    }

    // get the HTML for the form item
    // Relies on the form item being present - this is fine as long as our logic to create
    // and remove edit form items for incremental rendering stays in synch with the set of
    // cells being written out.
    var item = this._editRowForm.getItem(itemName),
        HTML = item.getStandaloneItemHTML(item.getValue(), false, true);

    // once we've retrieve the HTML, clear the errors so if we re-render the form for another item
    // etc, we don't end up with errors hanging around
    if (errors) {
        this._editRowForm.setFieldErrors(itemName, null);
    }


    var body = item.containerWidget;
    if (!body._drawnEditItems) body._drawnEditItems = [];
    if (!body._drawnEditItems.contains(item)) {
        body._drawnEditItems.add(item);
    }

    return HTML;
},

// should we show inactive version of the edit form item for some cell?
_showInactiveEditor : function (fieldNum) {
    return this._alwaysShowEditors(fieldNum);
},


// getInactiveEditorCellValue()
// If this.alwaysShowEditors is true, we write inactive versions of our edit form items in every
// row (with appropriate value displayed for the record in question)
getInactiveEditorCellValue : function (record, rowNum, colNum) {
    var itemName = this.getEditorName(rowNum, colNum);

    // get the HTML for the form item
    // Relies on the form item being present - this is fine as long as our logic to create
    // and remove edit form items for incremental rendering stays in synch with the set of
    // cells being written out.
    var item = this._editRowForm.getItem(itemName);
    // Don't crash if the form doesn't actually contain an item for this field.

    if (item == null) {
        return this.getFormattedValue(record, rowNum, colNum);
    }
    var editorType = isc.DynamicForm.getEditorType(item,this._editRowForm),
        value = this.getRawCellValue(record, rowNum, colNum),
        HTML;

    if (this.shouldShowEditorPlaceholder(this.getField(colNum),editorType)) {
        HTML = this.getEditorPlaceholderHTML(editorType, value, record,rowNum,colNum);
    } else {

        var context = {grid:this.getID(), record:record, rowNum:rowNum, colNum:colNum}

        var errors = this.getCellErrors(rowNum, colNum);
        if (errors) {
            this._editRowForm.setFieldErrors(itemName, errors);
        }
        HTML = item.getInactiveEditorHTML(value, false, true, context);
        if (errors) {
            this._editRowForm.setFieldErrors(itemName, null);
        }

        // the inactiveEditorContext ID gets stored directly on the cell
        // When we refresh the cell we'll throw away this inactive context (and potentially create
        // another one)
        // We want to be able to map from row+colNum to contextID so we don't have
        // to iterate through all the inactiveContexts on all the form items

        if (!this._inactiveEditorContextMap) this._inactiveEditorContextMap = {};
        var row_col = "_" + rowNum + "_" + colNum;
        if (this._inactiveEditorContextMap[row_col]) {
            this.logWarn("creating new inactiveEditor context for a cell without having cleared " +
                "the previous one." + this.getStackTrace(), "inactiveEditorHTML");
        }
        this._inactiveEditorContextMap[row_col] = context;
    }


    return HTML;
},


// if we're showing editors for every record, if this method returns true, suppress showing
// inactive editorHTML and show simplified placeholder HTML instead for the field in question.
// This allows us to not have to ensure that every editorType supports inactiveHTML in an efficient
// manner.
shouldShowEditorPlaceholder : function (field, editorType) {

    if (field.showEditorPlaceholder != null) return field.showEditorPlaceholder;

    return !this._supportsInactiveEditorHTML[editorType];
},
// list of editorTypes that support inactiveEditorHTML for alwaysShowEditors:true

_supportsInactiveEditorHTML:{
    text:true, TextItem:true,
    select:true, SelectItem:true,
    combobox:true, ComboBoxItem:true, comboBox:true,
    checkbox:true, CheckboxItem:true,
    date:true, DateItem:true,
    spinner:true, SpinnerItem:true,
    popUpTextArea:true, PopUpTextAreaItem:true
},

// getEditorPlaceholderHTML()
// If this.alwaysShowEditors is true, return the placeholder HTML to show in place of
// inactiveEditorHTML for cells where shouldShowEditorPlaceholder returns true.
getEditorPlaceholderHTML : function (editorType, value, record, rowNum, colNum) {
    // for now just return the value.
    return value;
},

// _editItemsDrawingNotification - function to notify the edit row form items when they are
// drawn, cleared or redrawn.

_editItemsDrawingNotification : function (item, fireMoved, gr) {

    // Items with an optionDataSource may kick off a fetch request on draw
    // Set up a queue around the drawn notifications so we put any such requests into a queue.
    var shouldSendQueue = isc.RPCManager && !isc.RPCManager.startQueue();

    var items;
    if (item) items = [item];
    else {
        items = [];
        var allItems = this._editRowForm.getItems();
        for (var i = 0; i < allItems.length; i++) {
            if (allItems[i].containerWidget == gr) items.add(allItems[i]);
        }
    }
    var newlyDrawnItems = gr._drawnEditItems;

    for (var i = 0; i < items.length; i++) {
        var currentItem = items[i],
            wasDrawn = currentItem.isDrawn(),
            isDrawn = newlyDrawnItems ? newlyDrawnItems.contains(currentItem) : false,
            isCanvasItem = isc.CanvasItem && isc.isA.CanvasItem(currentItem);
        if (wasDrawn) {
            if (isDrawn) {
                currentItem.redrawn();
                // Redraw of the body will frequently result in repositioning the edit cells.
                // Fire the 'moved' handler on any visible form items that were present before
                // the redraw to notify them of being moved.

                if (fireMoved) currentItem.moved();
            } else {
                currentItem.cleared();
            }

        } else if (isDrawn) {
            // newly drawn
            if (isCanvasItem) currentItem.placeCanvas();
            currentItem.drawn();
        }
    }

    if (this._editRowForm) {
        this._editRowForm.destroyOrphanedItems("Grid edit-items removed");
    }
    if (shouldSendQueue) isc.RPCManager.sendQueue();



    // Get rid of the _drawnEditItems. Next time this method is run we only want to catch
    // items which have subsequently been drawn
    delete gr._drawnEditItems;
},

// clearingInactiveEditorHTML
// Called when we're about to clear the generated inactiveEditorHTML due to cell refresh
// (so clearing a single row), or redraw (clearing all).
// Use this to clean up the generated inactive context objects so we don't fill up lots of memory on
// every redraw etc.
_clearingInactiveEditorHTML : function (rowNum,colNum) {

    // Don't crash if we have no inactiveEditorContextMap set.

    if (this._inactiveEditorContextMap == null) return;

    // have cells be rowNum/colNum pairs allowing us to clear just a single row, or
    // if we find we need more intelligent incremental update handling, single col, etc
    if (!this._editRowForm ||
        !this._alwaysShowEditors((colNum != null ? this.getField(colNum) : null),true)) return;

    if (rowNum != null) {
        var row_col = "_" + rowNum + "_" + colNum,
            context = this._inactiveEditorContextMap[row_col];
        if (context) {
            context.formItem.clearInactiveEditorContext(context);
            delete this._inactiveEditorContextMap[row_col];
        }
    } else {
        var editForm = this.getEditForm(),
            items = editForm.getItems();

        for (var i = 0; i < items.length; i++) {
            items[i].clearAllInactiveEditorContexts();
        }

        delete this._inactiveEditorContextMap;
    }
},

// setRecordValues()
// Method to update client-side data in place
// This is called directly by DynamicForms when saving values if this is acting as the selection
// component for a form.
setRecordValues : function (pks, values) {
    if (!this.shouldSaveLocally()) {
        this.logWarn("setRecordValues() not supported for grids where saveLocally is false");
        return;

    }
    if (!this.data) return;

    var rowNum = this.data.indexOf(pks),
        record = this.data.get(rowNum);

    // Incoming values will be in a structured form matching the dataPath, which is no good to
    // us because the records inside this component only contain fields from a specific
    // sub-hierarchy.  For example, if this grid is bound to dataPath "/items" it will have
    // flat-named fields called "a" and "b" that are actually at dataPaths "/items/a" and
    // "/items/b".  New values will come into this method in that complete form, so unless
    // we do something, our record will up looking like this:
    // {
    //   a: "something",
    //   b: 10,
    //   items: {
    //     a: "the new value",
    //     b: 100
    //   }
    // }
    //
    // So, we just create a new values object by pulling out whatever is at this component's
    // dataPath in the passed-in values (it will be the same as the form's dataPath, because
    // the only way we get here is for selectionComponents, and a selectionComponent implicitly
    // shares a dataPath with the forms for which it is a selectionComponent)


    var newValues = isc.Canvas._getFieldValue(this.dataPath, null, values, this, true);


    if (!newValues) {
        newValues = values;
    }

    this.combineObjects(record, newValues);
    this.calculateRecordSummaries(record, null, true, true);

    if (this.useCellRecords) {
        rowNum = this.findRowNum(record);
        var colNum = this.findColNum(record);
        this.refreshCell(rowNum,colNum);
    } else {
        this.refreshRow(rowNum);
    }

    // Reapply sort and filter, in case this change affects them
    this.resort();
    this.filterByEditor();

    if (this.valuesManager != null) {
        // _updateMultipleMemberValue handles combining a data-index into a path
        // 2nd param unnecessary since we're modifying the whole record, not a field within it.
        this.valuesManager._updateMultipleMemberValue(rowNum, null, record, this);
    }

},



combineObjects : function (destination, source) {
    return isc.combineObjects(destination, source);
},

//>    @method    listGrid.setRawCellValue()
//        @group    data
//            Set the raw data for one particular cell in the list.
//
//        @param    record        (object)    record in question
//        @param    recordNum    (number)    number of that record
//        @param    fieldNum    (number)    number of the field to display
//        @param    newValue    (any)        new value
//
//<
// Overridden at the cubeGrid level to handle being passed an entire record rather than a single
// field value for the cell
setRawCellValue : function (record, recordNum, fieldNum, newValue) {

    var field = this.fields[fieldNum];
    // if record or field is null, we're dealing with an invalid column, so forget it
    if (!record || !field) return;
    // if the field has a 'setRawCellValue' attribute
    // We'll assume this DIRECTLY sets the value on the LG's data object, so we're basically done
    // at this point (other than firing dataChanged below to ensure everything gets updated
    // visually)
    if (field.setRawCellValue) {
        // CALLBACK API:  available variables:  "viewer,record,recordNum,field,fieldNum,value"
        // Convert a string callback to a function
        isc.Func.replaceWithMethod(field, "setRawCellValue", "viewer,record,recordNum,field,fieldNum,value");

        // call it as a function (returns string | null)
        field.setRawCellValue(this, record, recordNum, field, fieldNum, newValue);

    // otherwise just use the cells value in the normal way
    } else {
        isc.Canvas._saveFieldValue(null, field, newValue, record, this, true);
    }

    // HACK: fire dataChanged() by hand.  Really, we need an interface to update fields on
    // objects in the List so the List can recognize the change and fire dataChanged() itself
    this.getOriginalData().dataChanged();
},

//>    @method    listGrid.getCellBooleanProperty()    (A)
//  Given a property name, and a cell, check for the value of that property (assumed to be a
//  boolean) on the ListGrid, and the cell's field, returning false if the value is false at
//  either level.
//  If true at the ListGrid and Field level, check the value of the the second "recordProperty"
//  on the record object for the cell.
//  (If recordProperty is not passed, the record object will not be examined).
//
//    @param    property    (string)    Name of the property to look for.
//    @param    rowNum    (number)    Row number of the cell.
//    @param    colNum    (string)    Field number for the cell.
//  @param  [recordProperty]    (string)    Name of the equivalent property to check on the
//                                          record object
//
//    @return    (boolean)    true == at least one is true and none are false
//
//<

_$false:"false", _$true:"true",
getCellBooleanProperty : function (property, recordNum, fieldNum, recordProperty) {
    var trueFound = false,
        listValue = this[property]
    ;
    if (listValue == false || listValue == this._$false) return false;
    var fieldValue = this.fields[fieldNum][property];
    if (fieldValue == false || fieldValue == this._$false) return false;

    if (recordProperty != null) {

        var record = this.getRecord(recordNum, fieldNum),
            recordValue = (record != null ? record[recordProperty] : false)
        ;
        if (recordValue == false || recordValue == this._$false) return false;
        if (recordValue == true || recordValue == this._$true) return true;
    }

    // At this point we know none of the values was an explicit false - but we only want to
    // return true if the value was specified as true (rather than undefined) at some level.
    // We've already checked at the record level (if necessary)
    return (listValue == true) || (fieldValue == true) ||
            (listValue == this._$true) || (fieldValue == this._$true);
},


// ---------------------------------------------------------------------------------------

//> @method listGrid.setShowRecordComponents()
// Setter for the +link{listGrid.showRecordComponents} attribute
// @param showRecordComponents (boolean) new value for <code>this.showRecordComponents</code>
// @visibility external
//<
setShowRecordComponents : function (showRC) {
    if (this.showRecordComponents == showRC) return;


    if (showRC) {
        if (this.animateFolders) {
            this._animateFoldersWithoutRC = true
            this.animateFolders = false;
        }
    } else {
        if (this._animateFoldersWithoutRC) {
            this.animateFolders = true;
            delete this._animateFoldersWithoutRC;
        }
    }

    this.showRecordComponents = showRC;

    // Update virutalScrolling if necessary.
    // We'll also update virtual scrolling in createBodies() - this handles the case where
    // we're already showing recordComponents and a grid is frozen at runtime.
    this._updateVirtualScrollingForRecordComponents();

    // suppress 'drawAllMaxCells' type behavior - we don't want to render out potentially
    // hundreds of canvases.
    if (showRC) {
        this._oldDrawAllMaxCells = this.drawAllMaxCells;
        this.drawAllMaxCells = 0;
        if (this.body != null) this.body.drawAllMaxCells = 0;

    } else {
        if (this._oldDrawAllMaxCells != null) {
            this.drawAllMaxCells = this._oldDrawAllMaxCells;
            if (this.body != null) this.body.drawAllMaxCells = this._oldDrawAllMaxCells;
            delete this._oldDrawAllMaxCells;
        }

    }
    this.invalidateRecordComponents();

    // For "within" record components, if autoFitting to values, we may need to
    // resize smaller once the components have been removed.
    if (!showRC &&
        (this.recordComponentPosition == this._$within ||
         (this.recordComponentPosition == null && this.showRecordComponentsByCell)))
    {
        this.updateFieldWidthsForAutoFitValue("showRecordComponents changed");
        this._updateFieldWidths();
    }

},

_updateVirtualScrollingForRecordComponents : function () {
    if (!this.showRecordComponents) {
        if (this._rcVScroll) {
            delete this.virtualScrolling;
            delete this._rcVScroll;
        }

    // Virtual scrolling:
    // Embedded componenents can make row heights unpredictable
    // (may not show for every row, may be 'position:"expand"', or exceed this.cellHeight
    // etc)
    // Because of this we typically have to enable virtual scrolling for record components.
    // However we don't currently support virtual scrolling with frozen fields, so
    // don't enable it if we have frozen fields.
    // This *may* lead to unpredictable behavior. Cases where it's ok:
    // - if recordComonentPosition is 'within' and
    //   the recordComponentPosition < this.cellHeight
    // - if recordComponentHeight is set (and truly is not exceeded by embedded components)

    } else {
        if (this.virtualScrolling == null || this._rcVScroll) {
            if (this.frozenFields == null) {
                this.virtualScrolling = true;
                this._rcVScroll = true;

            } else {

                if (this.recordComponentHeight == null) {
                    this.logWarn("This grid has frozen fields and is showing " +
                        "recordComponents. This may lead to unpredictable row heights which " +
                        "are not supported with frozen fields. Setting " +
                        "listGrid.recordComponentHeight will avoid this issue.",
                        "recordComponents");
                }
                if (this._rcVScroll) {
                    delete this.virtualScrolling;
                    delete this._rcVScroll;
                }
            }
        }
    }

    if (this.body && this.virtualScrolling != this.body.virtualScrolling) {
        this.body.virtualScrolling = this.virtualScrolling;
        if (this.frozenBody) {
            this.frozenBody.virtualScrolling = this.virtualScrolling;
        }
    }
    // No need to call 'redraw' on the body -- calling code is expected to handle this.
},



//> @method listGrid.getDrawArea() (A)
// Returns the extents of the rows and columns currently visible in this grid's viewport.
// <P>
// Note: if there are any +link{listGridField.frozen,frozen fields}, they are not included
// in the draw area range returned by this method. Frozen fields are assumed to never be
// scrolled out of view.  The column coordinates returned by this method will only include
// unfrozen columns.
//
// @return    (Array of Integer)    The row/col co-ordinates currently visible in the viewport as
//    [startRow, endRow, startCol, endCol].
// @visibility external
//<

getDrawArea : function () {
    if (this.body) {
        var drawArea = this.body.getDrawArea();
        if (this.frozenFields && this.freezeStart()) {
            drawArea[2] += this.frozenFields.length;
            drawArea[3] += this.frozenFields.length;
        }
        return drawArea;
    }

    return null;
},

// _drawAreaChanged() - notification fired on GridRenderer.redraw() when the
// previous draw area doesn't match the new draw area

_drawAreaChanged : function (oldStartRow, oldEndRow, oldStartCol, oldEndCol, body) {
    if (this.frozenFields && this.freezeStart()) {
        oldStartCol += this.frozenFields.length;
        oldEndCol += this.frozenFields.length;
    }
    var oldDrawArea = [oldStartRow, oldEndRow, oldStartCol, oldEndCol];
    if (oldDrawArea.equals(this.getDrawArea())) return;
    this.drawAreaChanged(oldStartRow,oldEndRow,oldStartCol,oldEndCol);
},

// documented in registerStringMethods
drawAreaChanged:function () {},

// if any records were programatically expanded before the grid was drawn,
// expand them properly now
updateExpansionComponents : function () {
    if (!this._recordsToExpand || this._recordsToExpand.length == 0) return;
    this._updatingExpansionComponents = true;
    for (var i=this._recordsToExpand.length-1; i>=0; i--) {
        this.expandRecord(this._recordsToExpand[i]);
    }
    delete this._updatingExpansionComponents;
    delete this._recordsToExpand;
},

// updateRecordComponents() - fired from redraw on grid body (or frozen body).
// This method essentially iterates through our current draw area and ensures that if
// showRecordComponents is true, we're showing recordComponents for each row (or cell),
// calling 'createRecordComponent()' or 'updateRecordComponent()' as necessary to create
// new components, and discarding (or pooling) record components that we previously created
// which are no longer visible.
// This method should not need to be called by developers directly.
// To force an explicit invalidation and refresh of recordComponents, use
// invalidateRecordComponents()
// *Note: This method also handles updating backgroundComponents if specified

updateRecordComponents : function () {
    // Sanity check to avoid infinite loops if adding embedded components trips redraw of
    // body for example
    var debugLog = this.logIsDebugEnabled("recordComponents");
    if (this._updatingRecordComponents) {
        if (debugLog) {
            this.logDebug("updateRecordComponents called recursively - returning",
                "recordComponents");
        }
        return;
    }

     // If we're performing a show/hide row height animation, bail.
    // In this case the HTML in the body won't match the set of records in our data set
    // so we can't update / place embedded components properly
    var body = this.body,
        frozenBody = this.frozenBody;
    if (body == null) return;

    if (body._animatedShowStartRow !=  null) {
        return;
    }

    // This method is fired from various places including gridBody.draw.
    // In this case we may have one body drawn and the other not yet drawn. Wait for both
    // to be drawn before continuing - addEmbeddedComponent (used below) requires the
    // GR be drawn.
    // Also if _fieldWidths isn't set, we can't
    // yet size/position our embedded components. This can occur when we rebuildForFreeze.
    // Catch this case and return.
    if ((!body.isDrawn() || body._fieldWidths == null ||
        (body._fieldWidths.length == 0 && body.fields.length > 0)) ||
        (frozenBody &&
            (!frozenBody.isDrawn() || frozenBody._fieldWidths == null ||
            (frozenBody._fieldWidths.length == 0 && frozenBody.fields.length > 0))))
    {
        return;
    }

    this._updatingRecordComponents = true;

    // Implementation overview: The concept here is that if showRecordComponents is true,
    // we call a method 'createRecordComponent()' [or potentially 'updateRecordComponent']
    // for every visible row, or if showing by cell, every visible cell, lazily as its rendered
    // out.
    // When new cells are rendered out we want to
    // - call createRecordComponent() [or updateRC] for newly rendered cells
    // - *not* call createRC for cells that were visible and still are (regardless of whether
    //   createRecordComponent returned an actual component or just null)
    // - for cells that are no longer visible, clear up the created record components,
    //   clearing them, destroying them or adding them to our 'recordComponentPool' depending on
    //   the recordComponentPoolingMode.
    // Rather than trying to achieve this by tracking viewports (which has the major disadvantage
    // of being fragile on data change or field config change), we take this approach:
    // We store all generated record components in 2 places:
    // - on the records themselves, indexed by fieldName, under record._recordComponents
    // - on the ListGrid in both an array and an object mapping componentIDs to true
    // If the createRecordComponent method returned null for any cell, we store a special
    // nullMarker object on the record._recordComponents object as well.
    //
    // When this method runs we can then iterate through all visible records / fields
    // - determine if we have visible record components already present, or null markers,
    //   in which case we leave the component alone
    // - otherwise run the method to create a new record component / get one from the pool and
    //   apply it to the cell.
    // Once we've gone through all visible cells we iterate through all the recordComponents
    // we previously created and wipe out (either clear, destroy or recycle) any that weren't
    // noted as being attached to a visible cell in the previous step.

    // _liveRecordComponents / _liveRecordComponentsObj is the full set of recordComponents
    // generated last time this method was run.
    var oldRecordCompArr = this._liveRecordComponents || [],
        oldRecordCompObj = this._liveRecordComponentsObj || {};

    if (debugLog) {
        this.logDebug("updateRecordComponents - old record components before refreshing:" +
            this.echo(oldRecordCompArr),
            "recordComponents");
    }

    this._liveRecordComponentsObj = {};
    this._liveRecordComponents = [];

    // If showRecordComponents is false we can skip all logic to create
    // new recordComponents. If we had any previously created recordComponents we'll clear
    // them below. This will handle the showRecordComponents setting being changed dynamically.
    if (this.showRecordComponents || this.showBackgroundComponents) {

        // Determine what our current draw area is - set of drawn fields and rows.
        // This method is being called as part of redraw, before the render has occurred, so
        // we can't just look at body._firstDrawnRow / _lastDrawnRow etc - we need
        // to call the getDrawArea() APIs on the body to actually calculate the new values

        var drawArea = this.body.getDrawArea(),
            cellComponents = this.showRecordComponentsByCell,
            bodyID = this.body.getID(),
            frozenBodyID = this.frozenBody ? this.frozenBody.getID() : null
        ;

        if (this.recordComponentPoolingMode == "recycle") {
            for (var i = 0; i < oldRecordCompArr.length; i++) {
                // if the component's row is outside of the new draw area, clean the component
                // up now - this adds it to the pool and allows the loop below to actually
                // recycle them - otherwise, on the first scroll, we will call
                // createRecordComponent when we ought to be calling updateRecordComponent
                var comp = oldRecordCompArr[i];
                if (!comp) continue;
                var row = this.getRecordIndex(comp.currentRecord);
                if (row < drawArea[0] || row > drawArea[1]) {
                    this._cleanUpRecordComponent(comp);
                    oldRecordCompObj[comp.getID()] = null;
                }
            }
        }

        if (debugLog) {
            this.logDebug("updating to potentially show recordComponents for drawArea:"
                + drawArea, "recordComponents");
        }
        for (var rowNum = drawArea[0]; rowNum <= drawArea[1]; rowNum++) {
            var record = this.getRecord(rowNum);

            if (record == null || Array.isLoading(record)) continue;

            if (this.showRecordComponents) {

                // If we don't have cell components we will add components to the (unfrozen) body,
                // one per row.
                if (!cellComponents) {

                    var shouldShowRecordComponent = null;
                    // Skip spanned cells.

                    if (this.allowRowSpanning && this.getRowSpan) {
                        var isSpanned = this.getCellStartRow(rowNum, 0) != rowNum;
                        if (isSpanned) shouldShowRecordComponent = false;
                    }

                    if (shouldShowRecordComponent == null) {

                         shouldShowRecordComponent = this.shouldShowRecordComponent(record);
                    }
                    var liveComp = null;
                    if (shouldShowRecordComponent) {
                        // getLiveRecordComponent() will pick up the record component we've already
                        // applied to the record/field.
                        // NOTE: If createRecordComponent ran and returned null we store a special
                        // null-marker object which we'll get back here as well. This means we
                        // don't re-run createRecordComponent() unless we actually want to.
                        liveComp = this._getLiveRecordComponent(record, null, bodyID);
                        if (liveComp != null) {
                            if (liveComp.isNullMarker) {
                                liveComp = null;
                            } else {
                                var ID = liveComp.getID();
                                oldRecordCompObj[ID] = null;
                            }
                        } else {
                            liveComp = this._applyNewRecordComponent(record, null, this.body, rowNum);
                        }
                    }

                    // Store pointers to both the component and its ID

                    if (liveComp != null) {
                        var ID = liveComp.getID();
                        this._liveRecordComponentsObj[ID] = true;
                        this._liveRecordComponents[this._liveRecordComponents.length] = liveComp;
                    }

                // same logic as above, but applied per cell to both the frozen and
                // unfrozen body.
                } else {
                    if (this.frozenBody != null) {
                        for (var fieldNum = 0; fieldNum < this.frozenBody.fields.length; fieldNum++) {
                            var field = this.frozenBody.fields[fieldNum],
                                fieldName = field.name;
                            var shouldShowRecordComponent = null;
                            // Skip spanned cells.
                            if (this.allowRowSpanning && this.getRowSpan) {
                                var isSpanned = this.getCellStartRow(rowNum, field.masterIndex) != rowNum;
                                if (isSpanned) shouldShowRecordComponent = false;
                            }
                            if (shouldShowRecordComponent == null) {
                                shouldShowRecordComponent = this.shouldShowRecordComponent(record, field.masterIndex);
                            }
                            var liveComp = null;

                            if (shouldShowRecordComponent) {
                                liveComp = this._getLiveRecordComponent(record, fieldName, frozenBodyID);
                                if (liveComp != null) {
                                    if (!liveComp.isNullMarker) {
                                        var ID = liveComp.getID();
                                        oldRecordCompObj[ID] = null;
                                    } else {
                                        liveComp = null;
                                    }
                                } else {
                                    liveComp = this._applyNewRecordComponent(record, fieldName, this.frozenBody,
                                                    rowNum, fieldNum);
                                }
                            }
                            if (liveComp != null) {
                                var ID = liveComp.getID();
                                this._liveRecordComponentsObj[ID] = true;
                                this._liveRecordComponents[this._liveRecordComponents.length] = liveComp;
                            }
                        }
                    }
                    for (var bodyCol = drawArea[2]; bodyCol <= drawArea[3]; bodyCol++) {
                        var field = this.body.fields[bodyCol],
                            fieldName = field.name;
                        var shouldShowRecordComponent = null;
                        // Skip spanned cells.
                        if (this.allowRowSpanning && this.getRowSpan) {
                            var isSpanned = this.getCellStartRow(rowNum, field.masterIndex) != rowNum;
                            if (isSpanned) shouldShowRecordComponent = false;
                        }
                        if (shouldShowRecordComponent == null) {
                            shouldShowRecordComponent = this.shouldShowRecordComponent(record, field.masterIndex);
                        }
                        var liveComp = null;

                        if (shouldShowRecordComponent) {
                            var liveComp = this._getLiveRecordComponent(record, fieldName, bodyID);
                            if (liveComp != null) {
                                if (!liveComp.isNullMarker) {
                                     var ID = liveComp.getID();
                                     oldRecordCompObj[ID] = null;
                                } else {
                                    liveComp = null;
                                }
                            } else {
                                liveComp = this._applyNewRecordComponent(record, fieldName, this.body,
                                            rowNum, bodyCol);
                            }
                        }

                        if (liveComp != null) {
                            var ID = liveComp.getID();

                            this._liveRecordComponentsObj[ID] = true;
                            this._liveRecordComponents[this._liveRecordComponents.length] = liveComp;
                        }
                    }
                }
            }


            if (this.showBackgroundComponents) {
                if (record && record.backgroundComponent) {
                    var component = this._hasEmbeddedComponents(record) ?
                            this._getEmbeddedComponents(record).find("isBackgroundComponent", true) : null;

                    if (!component) {
                        // should be showing a backgroundComponent but it's not present yet - add it now
                        if (isc.isA.Canvas(record.backgroundComponent)) {
                            // backgroundComponent is specified as a canvas
                            var comp = record.backgroundComponent.addProperties(
                                this.backgroundComponentProperties,
                                { isBackgroundComponent: true }
                            );
                        } else {
                            // backgroundComponent is specified as properties
                            var props = isc.addProperties({ isBackgroundComponent: true },
                                this.backgroundComponentProperties,
                                record.backgroundComponent);
                            var comp = this.createAutoChild("backgroundComponent", props);
                        }

                        var tableIndex = body.getTableZIndex();
                        comp.setZIndex(tableIndex - 49);
                        comp.setWidth("100%");
                        comp.setHeight("100%");
                        comp.setOverflow("hidden");

                        comp.removeOnHideField = true;


                        this.addEmbeddedComponent(record.backgroundComponent, record, rowNum,
                            null, "within");

                        // This should stick with the record until it's wiped due to data change
                        // or similar (EG remapEmbeddedComponents)
                        // At that point, if this method runs again it'll be cleared
                    }
                }
            }
        }
    }

    if (this.logIsInfoEnabled("recordComponents")) {
        this.logInfo("updateRecordComponents - new recordComponents:" +
            this.echo(this._liveRecordComponentsObj) +
            ", old record components (will be cleaned up if value is 'true'):" +
            this.echo(oldRecordCompObj), "recordComponents");
    }

    // At this point we've iterated through our draw area (or showRecordComponents is false,
    // in which case we want to drop all pre existing Record Components).
    // Any pre-existing recordComponents that are still visible have been removed from
    // the 'oldRecordCompObj'.
    // Iterate through pre-existing record components that are left and clear them up
    // (remove from DOM if necessary, destroy / pool if necessary)
    for (var i = 0; i < oldRecordCompArr.length; i++) {
        // if it's been cleared from the oldRecordCompObj we know its still visible / being used
        var ID = oldRecordCompArr[i].getID();
        if (oldRecordCompObj[ID] != true) {
            continue;
        }
        if (debugLog) {
            this.logDebug("cleaning up RecordComponent:" + oldRecordCompArr[i], "recordComponents");
        }
        this._cleanUpRecordComponent(oldRecordCompArr[i]);
        oldRecordCompObj[ID] = null;
    }
    delete this._updatingRecordComponents;
},

// _applyNewRecordComponent()
// This method will run 'createRecordComponent()' or 'updateRecordComponent()' to
// get the recordComponent for some record or cell.
_applyNewRecordComponent : function (record, fieldName, body, rowNum, bodyCol) {

    if (this.logIsDebugEnabled("recordComponents")) {
        this.logDebug("getting record component for row/field:" + [rowNum,fieldName],
            "recordComponents");
    }

    var pool = this.recordComponentPoolingMode == "recycle",
        component,
        // same row variable - only used if we're picking up a pooled component
        sameRow,
        colNum = fieldName == null ? null : this.getColNum(fieldName)
    ;

    if (pool) {
        var compConfig = this.getFromRecordComponentPool(record, fieldName);
        component = compConfig ? compConfig[0] : null;
        sameRow = compConfig ? compConfig[1] : null;
    }

    if (!component) {
        if (this.createRecordComponent && isc.isA.Function(this.createRecordComponent)) {
            component = this.createRecordComponent(record, this.getColNum(fieldName));
            if (component != null) component.isRecordComponent = true;
            this.logDebug("created new record component:" + component, "recordComponents");
        }
    } else {
        if (this.updateRecordComponent && isc.isA.Function(this.updateRecordComponent)) {
            var origComponent = component;

            component = this.updateRecordComponent(record, colNum, component, !sameRow);

            // component may well be null - this is valid and allows the developer to
            // suppress components in cells even in pooling mode.
            if (component == null) {
                if (this.logIsInfoEnabled("recordComponents")) {
                    this.logInfo("showRecordComponents: updateRecordComponent() method " +
                        "failed to return an updated component.", "recordComponents");
                }
                // stick it back into the pool for future use
                this.addToRecordComponentPool(origComponent);
            }
            this.logDebug("updated record component from pool:" + component, "recordComponents");
        }
    }
    return this._finishApplyNewRecordComponent(record, fieldName, body, rowNum, bodyCol, component);
},

_finishApplyNewRecordComponent : function (record, fieldName, body, rowNum, bodyCol, component) {
    var pool = this.recordComponentPoolingMode == "recycle",
        addNullMarker = component == null;
    if (addNullMarker) {
        component = {
            isNullMarker:true,
            _embedBody:body.getID(),
            _recordComponentBatch:this._recordComponentSequence
        }
    }
    if (fieldName == null) fieldName = this._$noFieldString;

    this._addRecordComponent(record, fieldName, component);
    // We're applying a "currentFieldName" / "currentRecord" flag in addition
    // to the _currentFieldName applied by the embeddedComponents code. This is
    // intentional - we use these flags in pooling mode to pick up the component that
    // matched the previous record (if possible) and previous field if
    // poolComponentsPerColumn is true. Don't want to rely on the flags that are set up and
    // potentially cleared by the standard embeddedComponent subsystem.
    if (pool && !addNullMarker) {
        component.currentFieldName = fieldName;
        component.currentRecord = record;
    }
    if (!addNullMarker) {
        // have the recordComponent removed if the field it's in is hidden
        component.removeOnHideField = true;

        //this.logWarn("created component:" + component + ", adding to:" + [rowNum,fieldName]);
        return body.addEmbeddedComponent(component, record, rowNum, bodyCol, this.getRecordComponentPosition());
    }
},

//> @method listGrid.setDontAutoDestroyComponent()
// If +link{listGrid.showRecordComponents} is true, by default any created record components
// are destroyed once they are no longer in use (for example, if the ListGrid as a whole is
// destroyed).  This method may be used to suppress this behavior for some component. Typical
// usage might call this method as part of +link{listGrid.createRecordComponent()} to suppress
// this behavior.
// @param component (Canvas) component in question.
// @param dontAutoDestroy (boolean) If true, the component will not be destroyed automatically when
//   the grid is destroyed
// @visibility external
//<

setDontAutoDestroyComponent : function (component, dontAutoDestroy) {
    component.dontAutoDestroy = dontAutoDestroy;
},

// fired when a recordComponent's cell is no longer visible. Behavior depends on
// recordComponentPoolingMode.
_cleanUpRecordComponent : function (component, forceDestroy) {
    if (this.logIsDebugEnabled("recordComponents")) {
        this.logDebug("cleaning up recordComponent:" + component,
            "recordComponents");
    }

    var poolingMode = this.recordComponentPoolingMode;
    // If passed the forceDestroy parameter, behave in 'viewport' mode regardless of
    // the actual pooling mode - this means we'll destroy the component passed in.
    if (forceDestroy) poolingMode = "viewport";

    if (poolingMode == "data") {
        // No need to attempt to pool, etc here:
        // If the record is still around, placeEmbeddedComponent() will have cleared it and
        // it'll simply re-render when scrolled back into view, etc.
        //
        // If the record is no longer present in the data array, remapEmbeddedComponents()
        // will have already marked it for destruction, and updateRecordComponents() has
        // already rebuilt the array of _liveRecordComponents so we won't be hanging onto
        // a pointer to it anymore.
        //
        // However - when the listGrid as a whole gets destroyed, we should ensure the
        // recordComponent gets destroyed too.
        // Handle this by storing a separate reference to it which we can look at on
        // destroy(). Use "pointersToThis" to ensure that if the component gets destroyed separately
        // (while this component is still intact) we don't hang onto this reference.
        if (!component.destroyed && !component.destroying && !component._pendingDestroy) {
            if (this._orphanedRecordComponents == null) {
                this._orphanedRecordComponents = {};
            }
            var ID = component.ID;
            if (this._orphanedRecordComponents[ID] != component) {
                this._orphanedRecordComponents[ID] = component;
                // set up 'pointersToThis'
                if (component.pointersToThis == null) component.pointersToThis = [];
                component.pointersToThis.add({
                    object: this._orphanedRecordComponents, property: ID
                });
            }
        }

    } else {
        var ID = component.ID,
            body = isc.Canvas.getById(component._embedBody),
            record = component.embeddedRecord,
            fieldName = component._currentFieldName;

        // wipe the component from the components-cache on the record [IF it hasn't been updated
        // to point at a new component]


        if (this._hasRecordComponents(record)) {
            this._deleteRecordComponent(record, fieldName);
        }

        // this component may have already been removed from the body, for example by
        // _remapEmbeddedComponentColumns(). In this case _embedBody will have been null
        // so we can detect this by the body var being unset here.
        if (body != null) {
            body.removeEmbeddedComponent(record, component);
            // also remove the entry from record._embeddedComponents_{ID}
            this._removeEmbeddedComponent(record, component);
        }

        component._currentFieldName = null;

        if (poolingMode == "viewport") {
            if (!component.dontAutoDestroy) component.markForDestroy();
        } else {

            if (component.destroying || component.destroyed || component._pendingDestroy) return;
            this.addToRecordComponentPool(component);
        }
    }
},

// Helper method - look at a record and see if we currently have a recordComponent for it.

_$noFieldString:"_noField",
_recordComponentSequence:0,

getLiveRecordComponent : function (record, fieldName, bodyID) {
    if (!record) return null;
    if (isc.isA.Number(record)) record = this.getRecord(record);
    if (!bodyID) bodyID = this.body.getID();
    var comp = this._getLiveRecordComponent(record, fieldName, bodyID);
    return comp;
},
_getLiveRecordComponent : function (record, fieldName, bodyID) {
    if (fieldName == null) fieldName = this._$noFieldString;

    if (!this._hasRecordComponents(record)) return null;
    var recordComponents = this._getRecordComponents(record);

    if (recordComponents[fieldName] == null) return null;

    var component = recordComponents[fieldName];

    if (component._embedBody != bodyID) {
        return null;
    }
    if (component.isNullMarker && component._recordComponentBatch != this._recordComponentSequence) {
        return null;
    }

    // We should never see this but if a component gets destroyed without first being
    // cleared out of the record._recordComponents block, wipe it out now. This will force
    // creation of a new recordComponent in calling code.
    if (component.destroyed || component.destroying || component._pendingDestroy) {
        this.logWarn("Destroyed or Destroying record component:" + component +
            " present on record. Ignoring", "recordComponents");
        recordComponents[fieldName] = null;
        return null;
    }
    return component;
},

//> @method listGrid.invalidateRecordComponents()
// Invalidates the currently visible set of +link{listGrid.showRecordComponents,recordComponents}
// and gets fresh ones for the visible rows in the grid according to the
// +link{listGrid.recordComponentPoolingMode}
// <P>
// See also +link{listGrid.refreshRecordComponent()} which allows you to refresh a specific
// recordComponent
//
// @visibility external
//<
invalidateRecordComponents : function () {

    // force destruction of the visible recordComponents - otherwise this
    // method would have no visible effect in 'data' pooling mode.
    this.dropRecordComponents(true);

    if (this.showRecordComponents && this.isDrawn()) {
        this.updateRecordComponents();
    }
},

dropRecordComponents : function (forceDestroy) {

    // up the recordComponentSequence count. This is used to identify our special null markers
    // and essentially invalidates them, meaning we'll re-run the createRecordComponent logic
    // for records with null markers we've already set at this point.

    this._recordComponentSequence++;

    var oldRecordCompArr = this._liveRecordComponents || [];

    delete this._liveRecordComponents;
    delete this._liveRecordComponentsObj;

    for (var i = 0; i < oldRecordCompArr.length; i++) {
        this._cleanUpRecordComponent(oldRecordCompArr[i], forceDestroy);
    }
},

//> @method listGrid.refreshRecordComponent()
// Discards any currently visible set of +link{listGrid.showRecordComponents,recordComponent}
// and gets a fresh one for the specified record (or cell) according to the
// +link{listGrid.recordComponentPoolingMode}
// <P>
// See also +link{listGrid.invalidateRecordComponents()} which allows you to refresh all record
// components that are currently visible in the grid.
//
// @param rowNum (int) Row to refresh
// @param [colNum] (Integer) Column to refresh. This parameter should be passed
//      if +link{showRecordComponentsByCell} is true.
// @visibility external
//<
refreshRecordComponent : function (rowNum, colNum) {
    if (!this.showRecordComponents || rowNum == null || this.body == null) return;

    if (this.showRecordComponentsByCell && colNum == null) {
        this.logWarn("refreshRecordComponent() called with no colNum. This parameter is required when " +
            "showRecordComponentsByCell is true. Taking no action.");
        return;
    }

    var record = this.getRecord(rowNum);
    if (record == null || Array.isLoading(record)) return;

    var body = this.body,
        bodyColNum = null,
        fieldName = null;

    if (this.showRecordComponentsByCell) {
        body = this.getFieldBody(colNum);
        bodyColNum = this.getLocalFieldNum(colNum);
        fieldName = this.getFieldName(colNum);
    }

    var prevComp = this._getLiveRecordComponent(record, fieldName, body.getID());
    if (prevComp != null && prevComp.isNullMarker) prevComp = null;

    var shouldShowComponent = this.shouldShowRecordComponent(record, colNum);

    var liveComp;
    if (prevComp != null) {

        if (shouldShowComponent && isc.isA.Function(this.updateRecordComponent)) {
            liveComp = this.updateRecordComponent(record, colNum, prevComp, false);
            if (liveComp != prevComp) {
                this._cleanUpRecordComponent(prevComp, (this.recordComponentPoolingMode != "recycle"));
                this.addToRecordComponentPool(prevComp);

                liveComp = this._finishApplyNewRecordComponent(record, fieldName, body, rowNum, bodyColNum, liveComp);
                if (liveComp && liveComp.isNullMarker) liveComp = null;
            }
            shouldShowComponent = false;
        } else {
            // This will destroy the component, or add to the pool.
            this._cleanUpRecordComponent(prevComp, (this.recordComponentPoolingMode != "recycle"));
        }
    }

    if (shouldShowComponent) {
        liveComp = this._applyNewRecordComponent(record, fieldName, body, rowNum, bodyColNum);
        if (liveComp && liveComp.isNullMarker) liveComp = null;
    }

    // Update this._liveRecordComponents and this._liveRecordComponentsObj
    var index = this._liveRecordComponents.length;
    if (prevComp) {
        var ID = prevComp.getID();
        this._liveRecordComponentsObj[prevComp.getID()] = null;
        // Null the slot in the live recordComponents array and reuse it if possible for
        // efficiency.
        if (liveComp != null) {
            index = this._liveRecordComponents.indexOf(prevComp);
            this._liveRecordComponents[index] = null;
        } else {
            this._liveRecordComponents.remove(prevComp);
        }
    }

    if (liveComp != null) {
        var ID = liveComp.getID();
        this._liveRecordComponentsObj[ID] = true;
        this._liveRecordComponents[index] = liveComp;
    }
},

getRecordComponentPosition : function () {
    if (this.recordComponentPosition != null) return this.recordComponentPosition;
    return (this.showRecordComponentsByCell ? "within" : "expand");
},

getRecordComponentPool : function () {
    if (!this._recordComponentPool) this._recordComponentPool = [];
    return this._recordComponentPool;
},

// we want to indicate whether the record changed. Handle this by returning a 2 element array
// - the component and a boolean.
getFromRecordComponentPool : function (record, fieldName) {
    var components = this.getRecordComponentPool(),
        subList = [],
        fieldSpecific = this.showRecordComponentsByCell && this.poolComponentsPerColumn == true,
        component
    ;

    if (!components || components.length == 0) return null;

    if (fieldSpecific) {
        subList = components.findAll("currentFieldName", fieldName);
    } else {
        subList = components;
    }
    if (!subList || subList.length == 0) return null;

    for (var i = 0; i < subList.length; i++) {
        component = subList[i];
        var prevRecord = component.currentRecord;
        if (this.comparePrimaryKeys(prevRecord, record)) {
            components.remove(component);
            return [component,true];
        }
    }

    // we didn't find a component that previously sat in this record (and that had the correct
    // fieldName, if we're pooling by column) - just return the last component from the
    // applicable list (subList)

    components.remove(component);
    return [component,false];
},

addToRecordComponentPool : function (component) {
    var components = this.getRecordComponentPool();

    components.add(component);
},

// Should we show a recordComponent for this record/col?
// checks for various records we want to skip, like the separator rows, and
// fires the public 'showRecordComponent()' method to allow custom suppression of RCs for
// certain rows or cells.
shouldShowRecordComponent : function (record, colNum) {

    if (record == null || record._isGroup || record[this.isSeparatorProperty]
        || Array.isLoading(record))
    {
        return false
    }
    return this.showRecordComponent(record,colNum);
},

// Override point documented in registerStringMethods().

showRecordComponent : function () {
    return true;
},

// notification from each body when getInnerHTML is called.
bodyDrawing : function (body) {

    if (isc._traceMarkers) arguments.__this = this;

    // don't fetch valueMap in response to draw on both the frozen and liquid body - just
    // on the normal (liquid) one
    if (body != this.body) return;

    // when the primary body draws, kick of a queue of requests for the visible data and any
    // valueMaps we'll need for fields with optionDataSource set
    var startedQueue;
    if (this._fetchValueMap) {
        // fetch valueMaps for fields where optionDataSource is set
        startedQueue = !isc.RPCManager.startQueue();
        this._fetchValueMapData();
    }

    this.requestVisibleRows();

    if (startedQueue) isc.RPCManager.sendQueue();
    this._fetchValueMap = null;
},


//> @attr listGrid.recordComponentHeight (Integer : null : IRWA)
// If +link{listGrid.showRecordComponents} is true, this attribute may be used to
// specify a standard height for record components.
// If specified every row in the grid will be sized tall enough to accommodate a recordComponent
// of this size.
// <P>
// Note that if this property is unset, the grid will not be able to know row heights in
// advance, and +link{listGridField.frozen,freezing of columns} is not currently supported in
// this case.
// @visibility external
//<


//> @method listGrid.setRecordComponentHeight()
// Setter for the +link{listGrid.recordComponentHeight}
// @param height (integer) recordComponent height
// @visibility external
//<
setRecordComponentHeight : function (height) {
    this.recordComponentHeight = height;
    if (this.isDrawn()) this.markForRedraw();
},

// Override 'getAvgRowHeight()' - if recordComponentHeight is specified and we're showing
// recordComponents, make use of it.
// Used by the gridRenderer in virtual scrolling mode to figure out end spacer size /
// thumb size.
getAvgRowHeight : function (body) {

    if (this.showRecordComponents && this.recordComponentHeight != null) {
        var rcrh = this.getRecordComponentRowHeight();
        if (rcrh != null) return rcrh;
    }
    // standard behavior
    // For virtual-scrolling, rely on the "avgRowHeight" specified in the body
    // otherwise assume constant record heights for all rows.
    return !body.virtualScrolling ? body.cellHeight
                                 : Math.max(body.cellHeight,body.avgRowHeight);

},

_$expand:"expand",
getRecordComponentRowHeight : function () {
    if (this.recordComponentHeight == null) return null;
    var pos = this.getRecordComponentPosition();
    if (pos == this._$expand) return this.cellHeight + this.recordComponentHeight;
    else return Math.max(this.recordComponentHeight, this.cellHeight);
},

// ListGridField.optionDataSource handling
// ---------------------------------------------------------------------------------------

// This logic handles fields where an optionDataSource acts as a server-side valueMap
//
// For fields with a specified optionDataSource separate from this grid's dataSource, with
// displayField and valueField specified, we need to perform a fetch against the server to
// get a map from data values to display values

// _setOptionDataSources() (called from setFields())
// iterate through the list of fields and take note of optionDataSource property to look up the
// display map later when the LG is drawn or redrawn (see bodyDrawing)
_setOptionDataSources : function () {


    // _fetchValueMap - one time flag to kick off a fetch in bodyDrawing
    var pendingFetch = this._fetchValueMap;
    this._fetchValueMap = null;

    var oldODSs = this._optionDataSources;
    this._optionDataSources = [];

    var gridDS = this.getDataSource();

    for (var i = 0; i < this.completeFields.length; i++) {
        var field = this.completeFields[i];

        if (field == null) {
            this.logWarn("Fields array contains an empty entry");
            continue;
        }

        var displayField = field.displayField || field[this.fieldIdProperty],
            valueField = field.valueField || field[this.fieldIdProperty];

        // autoFetchDisplayMap can be set at the field or LG level
        if (field.optionDataSource == null ||
            // If the display field matches the valueField don't bother to do a fetch since
            // we don't need to map the static raw cell value to a display value.
            // If the field is editable this means that we'll rely on the editor to perform a fetch
            // based on the optionDataSource rather than being passed an explicit valueMap.
            field.displayField == null || (field.displayField == field.valueField) ||
            (field.valueField == null && (field.displayField == field[this.fieldIdProperty])) ||
            (field.autoFetchDisplayMap == false) ||
            (this.autoFetchDisplayMap == false && field.autoFetchDisplayMap == null))
        {
            continue;
        }

        var optionDS = isc.DS.get(field.optionDataSource);
        if (optionDS == null) {
            this.logWarn(field.optionDataSource + " dataSource not found, check value of " +
                         "optionDataSource property on the " + field[this.fieldIdProperty] +
                         " field");
            continue;


        }

        var optionDSID = optionDS.ID,
            fieldName = field[this.fieldIdProperty],
            optionCriteria = field.optionCriteria;

        // have we already set up an optionDataSource config object from our previous set of fields?
        // which matches this ODS / criteria?

        // If so re-use it - we may not even need to re-fetch data!
        var addedToConfig = false;
        // Never try to fold fields with explicitly specified optionFetchContext into
        // a single responses
        if (oldODSs && !field.optionFilterContext) {
            for (var ii = 0; ii < oldODSs.length; ii++) {
                var ODSConfig = oldODSs[ii];
                if (ODSConfig == null) continue;
                if (this._fieldMatchesODSConfig(field, ODSConfig)) {
                    ODSConfig._fields = [field];
                    this._optionDataSources.add(ODSConfig);
                    oldODSs[ii] = null;
                    addedToConfig = true;
                    /*
                    this.logWarn("setOptionDataSources() field:"+ field.name +
                        " bound to dataSource ID:"+ optionDSID +
                        ", [crit:"+ this.echo(optionCriteria) +
                        "] matched existing optionDataSource config block from previous fields " +
                        "so added field to config.");
                    */
                    break;
                }
            }
        }

        if (!addedToConfig && !field.optionFilterContext) {
            for (var ii = 0; ii < this._optionDataSources.length; ii++) {
                var ODSConfig = this._optionDataSources[ii];
                if (this._fieldMatchesODSConfig(field, ODSConfig)) {
                    ODSConfig._fields.add(field);
                    addedToConfig = true;
                    /*
                    this.logWarn("setOptionDataSources() field:"+ field.name +
                        " bound to dataSource ID:"+ optionDSID +
                        ", [crit:"+ this.echo(optionCriteria) +
                        "] matched existing optionDataSource config block so added to field.");
                    */
                    break;
                }
            }
        }

        if (!addedToConfig) {
            this._optionDataSources.add({
                _dsID:optionDSID,
                _fields:[field],
                _textMatchStyle:field.optionTextMatchStyle,
                _criteria:optionCriteria,
                _optionContext:field.optionFilterContext,
                _optionOperationId:field.optionOperationId
            });

            // -- is this the only case where we need to fetch?
            //    Assumption is that in other cases we either already kicked off a fetch
            //    or picked up the field from our 'old ds's', and so already have a resultSet

            // keep going for a bit...


            // in this case we need to kick off a fetch for this dataSource
            // Set the flag to handle this when the body gets redrawn to reflect the new set of
            // fields
            /*
            this.logWarn("setOptionDataSources() field:"+ field.name +
                " bound to dataSource ID:"+ optionDSID +
                ", [crit:"+ this.echo(optionCriteria) +
                "] doesn't match fetch conditions for any other field with an option dataSource, " +
                "so performing a new fetch for it.");
            */
            this._fetchValueMap = true;
        }
    }

    // If we added to existing config, and we've cleared the _fetchValueMap flag,
    // this implies we have a fetch config set up (which we've updated) but we haven't
    // kicked off the fetch. Re-set the fetchValueMap flag so we do fetch on body draw.
    if (addedToConfig && !this._fetchValueMap && pendingFetch) {
        this._fetchValueMap = true;
    }

    // update the valueMaps for new fields bound to optionDataSources for which we already have
    // a resultSet
    for (var i = 0; i < this._optionDataSources.length; i++) {
        if (this._optionDataSources[i]._data != null) {
            this._updateValueMapFromODS(this._optionDataSources[i]);
        }
    }

    // If we had any previous optionDataSource config type options which are no longer required,
    // clean them up now by calling 'detroy()' on the resultSet (data object) and letting everything
    // go out of scope at the end of the method.
    if (oldODSs != null) {
        for (var i = 0; i < oldODSs.length; i++) {
            if (oldODSs[i] && oldODSs[i]._data) oldODSs[i]._data.destroy();
        }
    }
},

// little helper method - does a field with an optionDataSource specified match an existing
// optionDataSource config object.

_fieldMatchesODSConfig : function (field, config) {

    return (
        // same dataSource
        config._dsID == field.optionDataSource &&
        // optionTextMatchStyle should match

        (field.optionTextMatchStyle == config._textMatchStyle) &&
        // if optionOperationId is specified on a field it must also match
        (field.optionOperationId == config._optionOperationId) &&
        // criteria unset or match
        (isc.DataSource.getDataSource(config._dsID).compareCriteria(
            field.optionCriteria || {}, config._criteria || {}) == 0)
    );

},

// Given a resultSet bound to a dataSource, update the valueMap for any fields bound to this
// optionDataSource.
_updateValueMapFromODS : function (ODSConfig) {
    var odsID = ODSConfig._dsID,
        fields = ODSConfig._fields;

    if (fields == null || fields.length == 0) {
        this.logWarn("_updateValueMapFromODS fired for dataSource:"+ odsID +
                    " which no longer applies to any fields in this ListGrid");
        return;
    }

    var resultSet = ODSConfig._data;

    // avoid trying to get a valueMap if we are called when the cache has just been invalidated
    // or before the ResultSet has been created
    if (resultSet == null ||
        // only bail if we have a resultSet with unknown length AND no initialData
        (isc.isA.ResultSet(resultSet) && !resultSet.lengthIsKnown() && !resultSet.initialData))
    {
            return;
    }

    if (ODSConfig._optionOperationId) {
        // if the ODSConfig has an optionOperationId, set that as the fetch op on the resultSet
        resultSet.fetchOperation = ODSConfig._optionOperationId;
    }

    var groupBy = this.getGroupByFields() || [],
        fieldNames = fields.getProperty(this.fieldIdProperty),
        needsRegroup = false
    ;

    if (!isc.isAn.Array(groupBy)) groupBy = [groupBy];

    for (var i = 0; i < fields.length; i++) {
        var field = fields[i],
            valueField = (field.valueField || field[this.fieldIdProperty]),
            map = resultSet.getValueMap(valueField, field.displayField)
        ;
        // If we haven't yet integrated the field into our fields array, store the
        // valueMap on it directly rather than calling this.setValueMap() -- this way it'll get
        // picked up when setFields() completes
        if (this.getField(field[this.fieldIdProperty]) == null) {
            field.valueMap = map;
        } else {
            this.setValueMap(field[this.fieldIdProperty], map);
        }

        if (groupBy.contains(field.valueField) || groupBy.contains(field.displayField)) {
            // if we've just loaded the ODS for one of the groupFields, we need to regroup to
            // re-evaluate the groupTitles
            this._needsRegroup = true;
        }

    }

    if (this._needsRegroup) {
        // we need a regroup - check to see if all ODSs have been loaded and only regroup if
        // they have
        var allConfigLoaded = true;
        for (var i = 0; i < this._optionDataSources.length; i++) {
            var ODSConfig = this._optionDataSources[i];
            if (!ODSConfig._data) {
                allConfigLoaded = false;
                break;
            }
        }

        if (allConfigLoaded) {
            this.regroup(true);
            delete this._needsRegroup;
        }
    }
},


// _fetchValueMapData()
// for fields with an optionDataSource, kick off a fetch to get a valueMap for the field
_fetchValueMapData : function () {

    var shouldSendQueue;

    // For each field with an optionDataSource, kick off a fetch if we have no data, otherwise
    // update the valueMap with the existing data
    for (var i = 0; i < this._optionDataSources.length; i++) {
        var ODSConfig = this._optionDataSources[i];
        if (ODSConfig._data == null) {
            if (shouldSendQueue == null) {
                shouldSendQueue = !isc.RPCManager.startQueue();
            }

            var optionDSID = ODSConfig._dsID;

            // add component context to request properties for rpc history tree in dev console
            // as a comma delimited list of fields bound to the ODS in question
            var compContext,
                optFields = ODSConfig._fields;
            if (optFields && optFields.length > 0) {
                compContext = "ODS:";
                for (var ii = 0; ii < optFields.length; ii++) {
                    compContext += optFields[ii].name;
                    if (ii < optFields.length - 1) compContext += ",";
                }
            }

            var context = ODSConfig._optionContext || {};
            isc.addProperties(
                context,
                {showPrompt:false, internalClientContext:{ODSConfig:ODSConfig},
                 componentId:this.getID(),
                 componentContext: compContext,
                 textMatchStyle:ODSConfig._textMatchStyle
                }
            );
            if (ODSConfig._optionOperationId != null) {
                context.operationId = ODSConfig._optionOperationId;
            }
            isc.DataSource.getDataSource(optionDSID).fetchData(
                ODSConfig._criteria,
                {target:this, methodName:"_fetchValueMapCallback"},
                context
            );
        }
    }

    if (shouldSendQueue) isc.RPCManager.sendQueue();
},


// _fetchValueMapCallback()
// Callback from request to get all data from some field's optionDS.
// Creates a valueMap so we show the displayField value in the field
_fetchValueMapCallback : function (dsResponse, data, dsRequest) {

    var optionDataSourceConfig = dsRequest.internalClientContext.ODSConfig;
    // if the fields were changed before the fetch returned
    // OR we kicked off 2 fetches against the same dataSource and have already
    // set up our resultSet data, bail
    if (!optionDataSourceConfig || (optionDataSourceConfig._data != null) ||
        !this._optionDataSources || !this._optionDataSources.contains(optionDataSourceConfig))
    {
        return;
    }

    optionDataSourceConfig._data = isc.ResultSet.create({
        dataSource:optionDataSourceConfig._dsID,
        ODSConfig:optionDataSourceConfig,
        targetGrid:this,
        dataChanged:"this.targetGrid._updateValueMapFromODS(this.ODSConfig)",
        initialData:data
    })

    // Force an update now
    this._updateValueMapFromODS(optionDataSourceConfig);

    return true;
},


// Helper to destroy() the resultSet(s) we create to handle valueMaps from the server
_dropODSData : function () {
    if (this._optionDataSources == null) return;
    for (var i = 0; i < this._optionDataSources.length; i++) {
        var data = this._optionDataSources[i]._data;
        if (data) data.destroy();
    }
    delete this._optionDataSources;
},

requestVisibleRows : function () {
    // ask for all the rows we're about to render.  This enables better predictive fetching
    // relative to asking for data one row at a time while we render.

    var isResultSet = (isc.ResultSet != null && isc.isA.ResultSet(this.data)),
        isPagedResultTree = (
            isc.ResultTree != null && isc.isA.ResultTree(this.data) && this.data.isPaged());

    if (isResultSet || isPagedResultTree) {

        if (this.body == null || this.body._reused) {

            var willBeGrouped = (isResultSet && (
                    this.isGrouped ||
                    (this.groupByField != null && this.getUnderlyingField(this.groupByField))));
            if (willBeGrouped) {
                var dataPageSize = Math.max(this.dataPageSize || 0, 1);
                return this.data.getRange(
                    0, Math.max(dataPageSize, this.groupByMaxRecords));
            } else {
                return this.data.getRange(0, this.dataPageSize);
            }
        }

        if (isResultSet && this.data.lengthIsKnown() && this.data.getLength() == 0) {
            return;
        }


        if ((isResultSet && !this.data.lengthIsKnown()) || isPagedResultTree) {
            this.body.showAllRows = false;
        } else {
            // NOTE: this check is necessary because the body itself forces showAllRows to true
            // on init if overflow:visible.  It would probably be more robust to pass a
            // one-time flag to getTableHTML() so that we don't clobber other changes to
            // showAllRows
            this.body.showAllRows = (this.body.overflow == isc.Canvas.VISIBLE ?
                                     true : this.showAllRecords);
        }

        var drawRect = this.body.getDrawArea();

        if (this._scrollCell && isc.isAn.Array(this._scrollCell)) {
            // if scrolling was applied before draw(), move the drawRect to the requested row
            var diff = drawRect[1]-drawRect[0];
            drawRect[0] = this._scrollCell[0];
            drawRect[1] = drawRect[0]+diff;
        }

        // force all rows to be grabbed if we're grouping. (We'll need them anyway.)
        if (isResultSet && this.isGrouped) {
            return this.data.getRange(0, this.groupByMaxRecords);
        } else {
            // getRange() is non-inclusive at the end, but getDrawArea() is inclusive
            // at the end so we need to increment drawRect[1] by 1.
            return this.data.getRange(drawRect[0], drawRect[1]+1);
        }

    } else if (this.body != null) {
        // If our data isn't a resultSet, ensure that showAllRows reflects this.showAllRecords
        // This is required for the case where we are databound and grouped so this.data
        // was a resultSet when this method last ran and is now a client-side tree with
        // complete results
        this.body.showAllRows = (this.body.overflow == isc.Canvas.VISIBLE ?
                                     true : this.showAllRecords);
    }
    return null;
},

// Printing
// --------------------------------------------------------------------------------------------

//>    @attr listGrid.printAutoFit (Boolean : true : IRW)
// Whether cell contents should wrap during printing.  Equivalent to +link{type:Autofit}, but
// specific to printed output.
// @group printing
// @visibility external
//<
printAutoFit:true,

//>    @attr listGrid.printWrapCells (Boolean : true : IRW)
// Whether cell contents should wrap during printing.  Equivalent to +link{wrapCells}, but
// specific to printed output.
// @group printing
// @visibility external
//<
printWrapCells:true,

//>    @attr listGrid.printHeaderStyle (CSSStyleName : "printHeader" : IRW)
// Style for header cells in printed output.  Defaults to +link{headerBaseStyle} if null.
// @group printing
// @visibility external
//<
printHeaderStyle:"printHeader",

//>    @attr listGrid.printBaseStyle (CSSStyleName : null : IRW)
// Style for non-header cells in printed output.  Defaults to +link{baseStyle} if null.
// @group printing
// @visibility external
//<

//> @attr ListGrid.printMaxRows (int : 100 : IRWA)
// Advanced property - when generating printHTML for a large ListGrid, rows are printed in
// batches in order to avoid triggering a native "script is running slowly" browser dialog.
// <P>
// For grids with exceptional numbers of columns or complex formatting logic, this number
// might need to be adjusted downward.
//
// @group printing
// @visibility external
//<
// Note that this means getPrintHTML() is frequently asynchronous for ListGrids
printMaxRows:100,




getPrintHTML : function (printProperties, callback) {

    var body = this.body;
    // we may have getPrintHTML called while we're undrawn - if so, we'll need to set up our
    // children here
    if (body == null) {
        this.createChildren();
        body = this.body;
    }

    // if the body is pending a redraw, force it now
    if (this.isDirty() || body.isDirty()) {
        this.redraw("updating HTML for printing");
    }

    var printProps = isc.addProperties({}, printProperties, this.printProperties);

    // with a partial cache, print only the contiguous cache around the currently visible rows

    var startRow, endRow;
    if (isc.isA.ResultSet(this.data) && !this.data.allMatchingRowsCached()) {
        var visRows = this.body.getVisibleRows(),
            firstVisibleRow = visRows ? visRows[0] : null,
            cachedRange = this.data.getCachedRange(firstVisibleRow);
        if (cachedRange != null) {
            startRow = cachedRange[0];
            endRow = cachedRange[1];
        }
    }


    var printWidths = isc.Canvas.applyStretchResizePolicy(this.fields.getProperty("width"),
                                                printProps.width || isc.Page.getWidth());

    // set up the HTML for the header / footer rows based on
    // gridComponents. This will actually be written out by the grid body.
    // Ultimately calls body.getTablePrintHTML() and returns the combined result
    var HTML = this.setupHeaderPrintHTML({startRow:startRow, endRow:endRow, callback:callback,
                                     printWidths:printWidths, printProps:printProps});

    // if a callback was passed, it will always fire, and this method should return nothing
    if (!callback) return HTML;
},

// This method will generate HTML for the header / footer rows, ultimately picked up by
// the override to gridBody.getPrintHeaders() / getPrintFooters()
// Note - potentially asynchronous, depending on what gridComponents are specified.
setupHeaderPrintHTML : function (gridPrintConfig, HTML, componentIndex) {
    var isAsync = componentIndex != null;
    if (HTML == null) HTML = "";
    if (componentIndex == null) componentIndex = 0;

    var body = this.body;

    var components = this.gridComponents;
    // Used by 'shouldPrintChild' and possibly printHTML of arbitrary children
    this.currentPrintProperties = gridPrintConfig.printProps;
    for (var i = componentIndex; i < components.length; i++) {
        var comp = components[i];

        // Once we hit the body, we've got all the "header" row HTML - store that
        // on the body (will be returned by customized 'getPrintHeaders()'), and
        // continue onto the footer HTML

        if (comp == "body") {
            body._printHeadersHTML = HTML;
            // reset "HTML" to start collecting "footers" HTML
            HTML = "";
            continue;
        }

        // This will skip the filterEditor, and anything that's not actually showing
        // by default.
        if (!this.shouldPrintGridComponent(comp)) {
            continue;
        }

        if (comp == "header") {
            HTML += this.getPrintHeaderRow();

        } else if (comp == "summaryRow") {
            HTML += this.getPrintSummaryRow();

        } else if (isc.isA.Canvas(comp)) {
            // this maybe asynchronous, so pass in a callback to continue
            // generating HTML

            // Use closure to hang onto context
            var _this = this,
                componentPrintCallback = function componentPrintCallback (compPrintHTML) {
                HTML += "<TR><TD colSpan='" + _this.fields.length + "'>"
                        + compPrintHTML + "</TD></TR>";
                _this.setupHeaderPrintHTML(gridPrintConfig, HTML, i+1);
            }
            var compHTML = comp.getPrintHTML(gridPrintConfig.printProps,
                                componentPrintCallback);

            // Component getPrintHTML method returned null - must be asynchronous, so
            // we'll continue this loop when that method completes.
            if (compHTML == null) {
                return null;
            }

            HTML += "<TR><TD colSpan='"+this.fields.length+ "'>" +
                    compHTML +
                    "</TD></TR>";
        } else {
            // Not an explicit widget, nor one of the standard strings - not clear
            // how to handle this.

            this.logWarn("getPrintHTML() - unable to get print HTML for gridComponent " +
                comp);
        }
    }
    // Used by 'shouldPrintChild' and possibly printHTML of arbitrary children
    delete this.currentPrintProperties;


    // at this point we've got the footer HTML for the body set up - store that off
    // so body.getPrintFooters() behaves correctly
    body._printFootersHTML = HTML;

    return this.setupHeaderPrintHTMLComplete(gridPrintConfig, isAsync);

},


setupHeaderPrintHTMLComplete : function (gridPrintConfig, isAsync) {
    // getTablePrintHTML() - implemented at the GridBody level.
    // If it goes asynchronous it'll fire the callback and return null - otherwise it'll
    // return print HTML
    var bodyHTML = this.body.getTablePrintHTML(gridPrintConfig);

    // isAsync flag - implies setupHeaderPrintHTML was asynchronous - in this case
    // rather than returning the generated HTML, or relying on the grid body to fire the
    // print callback, we have to fire the callback ourselves.
    // September 2015 - if a dev installed a callback, always fire it (as per the docs)
    var devCallback = gridPrintConfig.callback && gridPrintConfig.callback.printCallback;
    if (devCallback || (bodyHTML != null && isAsync)) {
        var callback = gridPrintConfig.callback;
        if (callback) {
            this.fireCallback(callback, "HTML,callback", [bodyHTML,callback]);
        }
    }

    return isAsync ? null : bodyHTML;
},

// Undocumented feature - suppress printing of gridComponents via "print<CompName>" flags
// being set to false
shouldPrintAttributeMap:{
    filterEditor:"printFilterEditor",
    header:"printHeader",
    summaryRow:"printSummaryRow"
},
// Suppress printing of filterEditor by default

printFilterEditor:false,

shouldPrintGridComponent : function (component) {
    if (component == null) return false;

    if (isc.isA.String(component)) {
        var shouldPrintAttr = this.shouldPrintAttributeMap[component];
        if (shouldPrintAttr != null && this[shouldPrintAttr] != null) {
            return this[shouldPrintAttr];
        }
        return this.shouldShowGridComponent(component);
    }
    return this.shouldPrintChild(component);
},


// Returns the print HTML (table row) for headers for this grid.
// This is a helper for "setupHeaderPrintHTML"
getPrintHeaderRow : function () {
    var fields = this.fields.duplicate();
    // suppress rendering out the shouldPrint fields
    fields.removeAll(fields.findAll("shouldPrint", false));

    var startCol = 0, endCol = fields.length;
    var defaultAlign = isc.Canvas.LEFT,
        printHeaderStyle = this.printHeaderStyle || this.headerBaseStyle,
        HTML;


    // We support arbitrarily nested, asymmetrical header-spans - these require
    // some slightly tricky logic so use a conditional to avoid this if not required.
    if (this.headerSpans) {

        // Step 1: We'll build an array of "logical columns" in this format:
        // [field1], [innerHeader1], [topHeader]
        // [field2], [innerHeader2], [topHeader]
        // [field3], [topHeader2]
        // Each array contains an entry for each row we'll write out (each header
        // span the field is embedded in, plus the field).
        // Note that the top row of HTML will be the last entry in each sub-array and
        // the bottom row will be the first entry (the field should appear below
        // all its headers).
        // Also note we have repeats in here - we'll handle this by applying colSpans
        // to the generated HTML - and that the column arrays will be different lengths
        // due to different depth of nesting of header spans - we'll handle this by
        // applying rowSpans.
        var logicalColumns = [],
            numRows = 1;

        var spanMap = this.buildSpanMap(null, null, true);
        for (var i = startCol; i < endCol; ++i) {
            var field = fields[i];
            logicalColumns[i] = [field];

            var span = spanMap[field.name];

            // build a logical column from the fieldName up to the top span
            // (Note that we will have the same span in multiple cols, which is ok)
            while (span != null) {
                logicalColumns[i].add(span);
                span = span.parentSpan;
            }
            // Remember how deep the deepest nested column is - this is required to
            // allow us to apply numRows.
            numRows = Math.max(logicalColumns[i].length, numRows);
        }


        // Step 2: Iterate through the column arrays starting at the last entry
        // (outermost header)
        HTML = [];

        for (var i = numRows-1; i >= 0; i--) {
            HTML[HTML.length] = "<TR>";

            var lastEntry = null,
                colSpanSlot = null;
            for (var ii = startCol; ii < endCol; ii++) {
                var rowSpan = 1, colSpan = 1;
                // When we reach the first entry in the array we'll be looking at a field
                var isField = (i == 0);

                var entry = logicalColumns[ii][i];


                // If we hit a spanned marker we've already written out a row and/or
                // colSpanning cell that covers this entry - short-circuit the logic
                // below in this case.
                if (entry == "spanned") {
                    continue;
                }
                // our logical column arrays will be different lengths as
                // we can have unequal depth header spans (including potentially
                // just fields).
                // To handle this we'll have to apply rowSpans.
                // Sub-headers can have unequal depths - for example a header
                // span could contain a child span with fields and another child span
                // with its own child-span before their are fields.
                // This means we cant "take up all the slack" on the outer headers, we
                // have to apply rowSpans at potentially several levels.
                // Strategy:
                // - if this is a null entry there are fewer rows in this column than
                //   the total, so we need a rowSpan > 1 somewhere.
                //   Reach up the column until we find an entry - this will be the
                //   header we'll write out, and remember how deep we had to go
                //   (that will be the rowSpan)
                // - The header may span multiple columns - repeat this process for
                //   each subsequent column until we find a header span that differs
                //   from the one we were looking at.
                // At this point we'll know how many cols the header spans, and how deep
                // we had to go to find it in each col.
                // ColSpan will be the number of cols spanned.
                // RowSpan will be the minimum depth we had to go.
                // Now modify the logical column arrays to ensure we don't write out
                // duplicate cells in the generated HTML:
                // - replace every cell we span with a "spanned" marker so we skip it
                // - if the entry we are using as the source for the header doesn't
                //   fall in this range, clear its entry from the logical column array.
                //   Then on the iteration for the row where we would have it it we'll
                //   hit another null entry, and that will cause us to look at the
                //   next entry down instead and render it out with appropriate rowSpan
                var minDepth = 0,
                    spanningColNum = ii,
                    spannedColOffsets = [];

                // set colSpan to zero. We'll increment in the loop below
                colSpan = 0;

                while (spanningColNum < endCol) {
                    var entryToTest = null,
                        foundMismatch = false;
                    for (var offset = 0; (i-offset) >= 0; offset++) {
                        entryToTest = logicalColumns[spanningColNum][i-offset];


                        if (entryToTest != null) {
                            // If we originally hit a null entry, pick up the first
                            // non null entry so we have something to actually write out.
                            if (entry == null) {
                                entry = entryToTest;
                                minDepth = offset;
                                if (i-offset == 0) {
                                    isField = true;
                                }
                            }
                            if (entry == entryToTest) {
                                spannedColOffsets[colSpan] = offset;
                                minDepth = Math.min(offset, minDepth);
                            } else {
                                foundMismatch = true;
                            }
                            break;
                        }
                    }
                    if (foundMismatch) {
                        break;
                    }
                    spanningColNum ++;

                    colSpan++;
                }

                // set rowSpan for the cell based on how deep we had to
                // go to find a real entry (shift from zero to 1-based)
                if (minDepth != null) {
                    rowSpan = minDepth+1;
                }



                // For each column this entry spans, add markers indicating that
                // we're handling this via TD with rowSpan and colSpan set (and
                // clear out duplicate entries).
                for (var spannedCols = 0; spannedCols < spannedColOffsets.length;
                    spannedCols++)
                {

                    var logicalColArray = logicalColumns[spannedCols + ii],
                        offset = spannedColOffsets[spannedCols];

                    for (var spannedRows = 0; spannedRows <= offset; spannedRows++) {

                        if (spannedCols == 0 && spannedRows == 0) {
                            logicalColArray[i-spannedRows] = entry;
                        } else if (spannedRows <= minDepth) {
                            logicalColArray[i - spannedRows] = "spanned";
                        } else {
                            logicalColArray[i - spannedRows] = null;
                        }
                    }
                }



                // We don't expect to ever end up with a null entry - not sure
                // how this could happen but log a warning
                if (entry == null) {
                    this.logWarn("Error in getPrintHeaders() - unable to generate " +
                        "print header HTML from this component's specified headerSpans");
                }

                var align = "center",
                    cellValue;

                if (isField) {
                    align = entry.align || defaultAlign;
                    cellValue = this.getHeaderButtonTitle(entry.masterIndex);
                } else {
                    cellValue = entry.title;
                }

                // Flip alignment if isRTL is true and reverseRTLAlign is set
                if (this.isRTL() && this.reverseRTLAlign) {
                    if (align == isc.Canvas.LEFT) align = isc.Canvas.RIGHT;
                    else if (align == isc.Canvas.RIGHT) align = isc.Canvas.LEFT;
                }

                var cellStart = HTML.length;

                HTML[HTML.length] = "<TD class='";
                HTML[HTML.length] = printHeaderStyle;
                HTML[HTML.length] = "' align='";
                HTML[HTML.length] = align;
                HTML[HTML.length] = "' rowSpan='";
                HTML[HTML.length] = rowSpan;
                HTML[HTML.length] = "' colSpan='";
                HTML[HTML.length] = colSpan;
                HTML[HTML.length] = "'>";
//                    HTML[HTML.length] = "' style='border:1px solid black'>";
                HTML[HTML.length] = cellValue;
                HTML[HTML.length] = "</TD>";



            }
            HTML[HTML.length] = "</TR>"
        }
//         this.logWarn("\n\nGenerated print header HTML (including spans):" + HTML.join(""));

    } else {
        var HTML = ["<TR>"];

        var cellStartHTML = ["<TD CLASS=", printHeaderStyle,
                             " ALIGN="].join("");

        // Just iterate through the fields once, then assemble the HTML and return it.
        for (var colNum = startCol; colNum < endCol; colNum++) {
            var field = fields[colNum];

            var align = field.align || defaultAlign;
            // Flip alignment if isRTL is true and reverseRTLAlign is set
            if (this.isRTL() && this.reverseRTLAlign) {
                if (align == isc.Canvas.LEFT) align = isc.Canvas.RIGHT;
                else if (align == isc.Canvas.RIGHT) align = isc.Canvas.LEFT;
            }

            HTML.addList([cellStartHTML, align, ">",
                                this.getHeaderButtonTitle(field.masterIndex), "</TD>"]);
        }

        // Output the standard header row
        HTML[HTML.length] = "</TR>";
    }
    return HTML.join(isc.emptyString);
},


getPrintSummaryRow : function () {
    if (!(this.summaryRow) || !(this.summaryRow.body) || !(this.showGridSummary)) {
        // return emty string if no data in the grid
        return "";
    }

    var chunkContext = {
        startRow:0,
        endRow:this.summaryRow.getTotalRows(),
        maxRows:this.summaryRow.printMaxRows,
        printProps:this.printProperties || {},
        html:[]
    }


    var printWidths = isc.Canvas.applyStretchResizePolicy(this.fields.getProperty("width"),
                                                          chunkContext.printProps.width || isc.Page.getWidth());
    chunkContext.printWidths = printWidths;

    this.summaryRow.body.printChunkOnly = true;

    var HTML = this.summaryRow.body.getPrintHTMLChunk(chunkContext, true);
    delete this.summaryRow.body.printChunkOnly;
    // this.logWarn("print summary row HTML:" + HTML);
    return HTML;
},

// -------

// This is run before getting the body tableHTML for printing
// If printing HTML in chunks it'll be run repeatedly for each chunk!
// Purpose is to set up the necessary attributes so the body generates print-formatted
// output
_prepareBodyForPrinting : function (printWidths, printProperties) {
    this.isPrinting = this.body.isPrinting = true;
    this.currentPrintProperties = printProperties;

    var body = this.body,
        // don't print editors?
        oldEditorShowing = this._editorShowing;

    // properties to store off and restore
    var origProps = isc.getProperties(body, ["autoFit", "wrapCells", "showAllRows",
                                "showAllColumns", "fixedRowHeights", "_fieldWidths", "fields"


                    ]);

    body.showAllRows = true;
    body.showAllColumns = true;

    this._editorShowing = false; // never show editors
    body.autoFit = this.printAutoFit;
    body.wrapCells = this.printWrapCells;
    body.fixedRowHeights = !this.printWrapCells;

    var fields = this.fields.duplicate();
    // suppress rendering out the shouldPrint fields
    fields.removeAll(fields.findAll("shouldPrint", false));
    body.fields = fields;

    // mark ourselves as unfrozen so we avoid logic to (for example) offset body col-num
    // with frozen col num)
    var oldFrozenFields = this.frozenFields;
    delete this.frozenFields;
    // Temporarily copy all components embedded in the frozenBody into the _embeddedComponents
    // list of the body.
    var oldEmbeddedComponents = this._oldEmbeddedComponents = body._embeddedComponents,
        numFrozenComponents = (
            this.frozenBody != null && this.frozenBody._embeddedComponents != null ?
                this.frozenBody._embeddedComponents.length : 0);
    if (numFrozenComponents > 0) {
        var numNonFrozenComponents = (
                oldEmbeddedComponents != null ? oldEmbeddedComponents.length : 0),
            numComponents = numFrozenComponents + numNonFrozenComponents,
            newComponents = body._embeddedComponents = new Array(numComponents);

        for (var i = numNonFrozenComponents; i--; ) {
            newComponents[i] = oldEmbeddedComponents[i];
        }
        for (var i = numFrozenComponents; i--; ) {
            var ec = this.frozenBody._embeddedComponents[i];
            ec._wasFrozen = true;
            newComponents[numNonFrozenComponents + i] = ec;
        }
    }

    this._embeddedComponentColNumOffset = 0;
    this._frozenEmbeddedComponentColNumOffset = 0;
    if (this.freezeStart()) {
        if (this.frozenBody &&
            this.frozenBody._lastDrawnCol != null &&
            this.frozenBody._firstDrawnCol != null)
        {
            this._embeddedComponentColNumOffset = (
                this.frozenBody._lastDrawnCol - this.frozenBody._firstDrawnCol + 1);
        }
    } else {
        if (this.body &&
            this.body._lastDrawnCol != null &&
            this.body._firstDrawnCol != null)
        {
            this._frozenEmbeddedComponentColNumOffset = (
                this.body._lastDrawnCol - this.body._firstDrawnCol + 1);
        }
    }

    body._fieldWidths = printWidths;
    return {oldEditorShowing:oldEditorShowing, oldFrozenFields:oldFrozenFields,
            origProps:origProps};

},

_bodyDonePrinting : function (context) {
    var body = this.body,
        origProps = context.origProps,
        oldEditorShowing = context.oldEditorShowing;

    isc.addProperties(body, origProps);
    if (origProps.showAllRows == null) body.showAllRows = null;
    this._editorShowing = oldEditorShowing;
    this.frozenFields = context.oldFrozenFields;
    body._embeddedComponents = this._oldEmbeddedComponents;
    delete this._oldEmbeddedComponents;
    if (this.frozenBody != null && this.frozenBody._embeddedComponents != null) {
        for (var i = this.frozenBody._embeddedComponents.length; i--; ) {
            var ec = this.frozenBody._embeddedComponents[i];
            delete ec._wasFrozen;
        }
    }
    delete this._embeddedComponentColNumOffset;
    delete this._frozenEmbeddedComponentColNumOffset;

    delete this.currentPrintProperties;

    this.isPrinting = this.body.isPrinting = false;

    this.body.markForRedraw("finished printing");
},

// Event Handling
// --------------------------------------------------------------------------------------------

//>    @method    listGrid.rowClick()    (A)
//
// Event handler for when rows in the body are clicked upon. The default implementation handles
// firing +link{ListGrid.startEditing()} if appropriate, and fires
// +link{ListGridField.recordClick()} and/or +link{ListGrid.recordClick()} if set. Developers
// should typically implement recordClick rather than overriding this method.
// <P>
// Note that this method fires in addition to any specified +link{listGrid.cellClick}
// handler (even if that method cancels the event as a whole by returning <code>false</code>).
//
//      @param  record      (ListGridRecord)    record object returned from getCellRecord()
//        @param    recordNum   (int)    index of the row where the click occurred
//        @param    fieldNum    (int)    index of the col where the click occurred
//      @param  [keyboardGenerated]   (boolean) indicates whether this was a synthesized record
//                                              click in response to a keyboard event
//        @group    events
//      @see    recordClick()
//
//      @group  events
//        @return    (Boolean)
//      @visibility external
//<
rowClick : function (record, recordNum, fieldNum, keyboardGenerated) {
    // record the last record clicked (used for keyboard navigation)
    this._lastRecordClicked = recordNum;

    var record = this.getCellRecord(recordNum, fieldNum),
        field = this.fields[fieldNum];

    // don't fire recordClick on loading rows
    if (Array.isLoading(record)) return;

    // if the field has a 'recordClick' method, call that
    var value = this.getCellValue(record, recordNum, fieldNum);
    var rawValue = this.getRawCellValue(record, recordNum, fieldNum);

    // if the record is a group header, expand/collapse the group
    if (record != null && record._isGroup) {
        if (this.canCollapseGroup == false) return;
        // if the user navigates through the grid using arrow up / down keys, don't toggle
        // the group when the user hits the group header (Except on explicit space keypress which is
        // more of a deliberate 'pseudo-click' type interaction).

        if (keyboardGenerated) {
            var key = isc.EH.getKey();
            if (key != "Space" && key != "Enter") return;
        }


        var mythis = this, myrecord=record;
        if (this.getEditRow() != null) {
            if (this.autoSaveEdits) {
                var saveAttempted = this.saveAllEdits(null, function () {
                    mythis.toggleFolder(myrecord);
                });
                // we'll toggle folder in the callback so no need to carry on here.
                if (saveAttempted) return;
            } else {
                this.storeUpdatedEditorValue();
            }
        }
        this.toggleFolder(record);
        return;
    }

    if (field.recordClick && !(keyboardGenerated && field.keyboardFiresRecordClick == false)) {
        // CALLBACK API:  available variables:  "viewer,record,recordNum,field,fieldNum,value,rawValue"
        // Convert a string callback to a function
        isc.Func.replaceWithMethod(field, "recordClick",
                                         "viewer,record,recordNum,field,fieldNum,value,rawValue");
        var returnVal = field.recordClick(this, record, recordNum, field, fieldNum, value, rawValue);
        if (returnVal == false) return false;
    }

    // Note - there is also a (legacy) recordClick handler on the GridRenderer class, with a
    // different signature.
    // 'recordClick()' is not one of the GridAPIs - so won't be fired automatically from the
    // GridRenderer click handling code.  If the method was defined on both the ListGrid and
    // the Body, both methods would be fired.
    // CALLBACK API:  available variables:  "viewer,record,recordNum,field,fieldNum,value,rawValue"
    if (this.recordClick) {
        if (this.onRecordClick(this,record,recordNum,field,fieldNum,value,rawValue) == false)
            return false;
        if (this.recordClick(this,record,recordNum,field,fieldNum,value,rawValue) == false)
            return false;
    }

    // if the cell is editable on click, edit it now
    // We also start editing if editOnFocus is true.
    var editOnClick = this.isEditable() && (this.editEvent == isc.EH.CLICK || this.editOnFocus);

    // one-click toggling of boolean/valueMapped fields.
    // Note: also allows entering of editing if editEvent is click.
    if (field.canToggle && this.canEditCell(recordNum, fieldNum) &&
        this.shouldToggle(field, keyboardGenerated))
    {
        var valueMap = this.getEditorValueMap(field, this.getEditedRecord(recordNum,fieldNum));
        // autocreate a valueMap for boolean
        if (valueMap == null && isc.SimpleType.getBaseType(field.type) == this._$boolean) {
            valueMap = [true,false];
        }
        if (valueMap != null) {
            if (!isc.isAn.Array(valueMap)) valueMap = isc.getKeys(valueMap);
            if (valueMap.length > 1) {

                var fieldName = this.getFieldName(fieldNum),
                    editValue = this.getEditedCell(recordNum, fieldNum),
                    index = valueMap.indexOf(editValue);
                index += 1;
                if (index >= valueMap.length) index = 0;
                var oldValue = editValue;

                editValue = valueMap[index];

                var cancelChange;

                if (field.change != null) {
                    this.logInfo("canToggle firing specified field.change() event directly", "gridEdit");
                    cancelChange = this.fireCallback(
                            field.change,
                            "form,item,value,oldValue",
                            [null,null,editValue,oldValue]
                    ) == false;
                }

                if (!cancelChange) {

                    // autoSaveEdits: Usually if canToggle is true we instantly commit
                    // however if we're also jumping into edit mode it makes more sense to
                    // just toggle the edit value and save when the user dismisses the editor
                    // as usual.
                    if (!editOnClick && this.autoSaveEdits) {
                        this.setEditValue(recordNum, fieldNum, editValue, true, false);

                    } else {
                        this.setEditValue(recordNum, fieldNum, editValue);
                    }
                    if (field.changed != null) {
                        this.logInfo("canToggle firing specified field.changed() event directly", "gridEdit");
                        this.fireCallback(
                            field.changed,
                            "form,item,value",
                            [null,null,editValue]
                        );
                    }

                    if (this.autoSaveEdits) this.saveEdits(null, null, recordNum, fieldNum);
                }
            }
        }
    }

    if (editOnClick) {
        if (this.handleEditCellEvent(recordNum, fieldNum, isc.ListGrid.CLICK) == true) {
            return true;
        }
        // If this was a keyboard event, and the keyboard click field is not editable, iterate
        // through the other fields, and edit the first editable one we find
        if (keyboardGenerated) {
            for (var i = 0; i< this.fields.length; i++) {
                if (i == fieldNum) continue;
                if (this.handleEditCellEvent(recordNum, i, isc.ListGrid.CLICK) == true) {
                    return true;
                }
            }
        }
    }
},

// By default we only toggle if the event occurred over a field's value-icon
// Exception: If _formatBooleanFieldAsImage returns false we didn't write out an icon for
// the cell
shouldToggle : function (field, keyboardGenerated) {
    if (keyboardGenerated) {
        var key = isc.EH.getKey();
        if (key == this._$Space || key == this._$Enter) return true;
    }
    // Note: no need to check 'canToggle' - this method is only called for fields where
    // canToggle is true.
    if (!this._formatBooleanFieldAsImages(field)) return true;
    var part = this.getEventPart();
    return (part && part.part == "valueicon");
},

//>    @method    listGrid.rowDoubleClick()    (A)
// Event handler for when a body record is double-clicked.
// <P>
// Default implementation fires 'editCell' if appropriate, and handles firing
// 'recordDoubleClick' stringMethod if defined at the field or LG level (That method has a
// different signature from this one)
//
//      @param  record      (ListGridRecord)    record object returned from getCellRecord()
//        @param    recordNum   (number)    index of the row where the click occurred
//        @param    fieldNum    (number)    index of the col where the click occurred
//      @param  [keyboardGenerated]   (boolean) indicates whether this was a synthesized record
//                                              doubleclick in response to a keyboard event
//
//      @see    recordDoubleClick()
//        @group    events
//        @return    (boolean)    false if first click not on same record; true otherwise
//      @visibility external
//<
rowDoubleClick : function (record, recordNum, fieldNum, keyboardGenerated) {

    var field = this.fields[fieldNum],
        value = this.getCellValue(record, recordNum, fieldNum),
        rawValue = this.getRawCellValue(record, recordNum, fieldNum);
    // suppress user-defined handlers on the group header node
    if (record != null && record._isGroup) return;

    if (field.recordDoubleClick) {
        // CALLBACK API:  available variables:  "viewer,record,recordNum,field,fieldNum,value,rawValue"
        // Convert a string callback to a function
        isc.Func.replaceWithMethod(field, "recordDoubleClick",
                                         "viewer,record,recordNum,field,fieldNum,value,rawValue");
        var returnVal = field.recordDoubleClick(this, record, recordNum, field, fieldNum, value, rawValue);
        if (returnVal == false) return returnVal;
    }
    if (this.recordDoubleClick != null) {
        // CALLBACK API:  available variables: "viewer,record,recordNum,field,fieldNum,value,rawValue"
        var returnVal = this.recordDoubleClick(this, record, recordNum, field, fieldNum, value, rawValue);
        if (returnVal == false) return returnVal;
    }

    // if the cell is editable, edit it now
    //    (editCell will return true if we've brought up the cell editor)
    if (this.isEditable() && this.editEvent == isc.EH.DOUBLE_CLICK) {
        if (this.handleEditCellEvent(recordNum, fieldNum, isc.ListGrid.DOUBLE_CLICK) == true) return true;
        // If this was a keyboard event, and the keyboard click field is not editable, iterate
        // through the other fields, and edit the first editable one we find
        if (keyboardGenerated) {
            for (var i = 0; i< this.fields.length; i++) {
                if (i == fieldNum) continue;
                if (this.handleEditCellEvent(recordNum, i, isc.ListGrid.DOUBLE_CLICK) == true) return true;
            }
        }
    }

},

// Body Context Menu
// --------------------------------------------------------------------------------------------
// This will fire 'ListGrid.cellContextClick' if defined.
// Otherwise implements default behavior of showing context menu for the cell.
// enhancement: check for contextMenu,cellContextClick on cell, row, field?
_cellContextClick : function (record, rowNum, colNum) {

    // clear any previous context menu items
    this.cellContextItems = null;

    // Call handler if defined; return false to cancel context menu if handler returns false.  The
    // handler can call lv.makeCellContextItems(record,recordNum,fieldNum), modify the items, and
    // set lv.cellContextItems to customize the context menu.
    if (this.cellContextClick) {
        var record = this.getCellRecord(rowNum, colNum);
        if (this.cellContextClick(record, rowNum, colNum) == false) return false;
    }

    // show cell context menus, or generic context menu?
    if (this.showCellContextMenus) {

        // create the cellContextMenu if necessary
        if (!this.cellContextMenu) {
            this.cellContextMenu = this.createAutoChild("cellContextMenu", this.contextMenuProperties , this.getMenuConstructor());
        }

        // get standard menu items if the handler above did not set custom items
        if (!this.cellContextItems) {
            this.cellContextItems = this.makeCellContextItems(
                this.getCellRecord(rowNum, colNum), rowNum, colNum);
        }
        // if there are any menu items, set and show the menu
        if (isc.isAn.Array(this.cellContextItems) && this.cellContextItems.length > 0) {
            this.cellContextMenu.setData(this.cellContextItems);
            this.cellContextMenu.showContextMenu(this);
        }

        // return false to kill the standard context menu
        return false;

    } else {
        // do normal Canvas context menu handling. Will fall through to this.showContextMenu.
        return true;
    }
},

//> @method ListGrid.getShowChildDataSourceContextMenuItemTitle() [A]
// If +link{ListGrid.canOpenRecordDetailGrid} is true and +link{ListGrid.showCellContextMenus}
// is true, we will show menu items to drill into the child datasources in this grid's context
// menu. This method returns the title for that menu item. Override for localization.
// @param ds (DataSource) child datasource to be drilled into
// @return (string) By default returns <code>"Show " + ds.getPluralTitle()</code>
// @group i18nMessages
// @visibility nestedGrid
//<
getShowChildDataSourceContextMenuItemTitle : function (ds) {
    return "Show " + ds.getPluralTitle();
},

// generate standard cell context menu items
makeCellContextItems : function (record, rowNum, colNum) {
    if (this.dataSource != null) {
        var menuItems = [];

        // menu items to drill into a child DataSource via a nested grid
        if (this.canOpenRecordDetailGrid) {
            var recordDS = isc.DS.get(this.getRecordDataSource(record)),
                childDataSources = recordDS.getChildDataSources();
            if (childDataSources != null) {
                for (var i = 0; i < childDataSources.length; i++) {
                    var ds = childDataSources[i];
                    menuItems.add({
                        title : this.getShowChildDataSourceContextMenuItemTitle(ds),
                        record : record,
                        dataSource : ds,
                        click : "target.openRecordDetailGrid(item.record, item.dataSource)"
                    });
                }
            }
        }

        // menu item to edit with an embedded form
        if (this.canOpenRecordEditor) {

            menuItems.add({
                title : this.openRecordEditorContextMenuItemTitle,
                record : record,
                click : "target.endEditing();target.openRecordEditor(item.record)"
            });
        }

        // if we are currently showing anything inside the row offer to dismiss it
        if (record != null && this._openRecord == record) {
            menuItems.add({
                title : this.dismissEmbeddedComponentContextMenuItemTitle,
                click : "target.closeRecord()"
            });
        }

        // menu item to delete a record
        menuItems.add({
            title : this.deleteRecordContextMenuItemTitle,
            click : "target.removeSelectedData()"
        });
        return (menuItems.length > 0 ? menuItems : null);
    }
    return null;
},

// Hover
// ---------------------------------------------------------------------------------------

// override getCanHover.
// If this.canHover is explicitly set to true or false, respect it,
// Otherwise - if any fields are going to show hovers, return true

getCanHover : function () {
    if (this.canHover != null) return this.canHover;
    var showClippedValuesOnHover = this._getShowClippedValuesOnHover();
    if (showClippedValuesOnHover) return showClippedValuesOnHover;
    var fields = this.getFields();
    if (fields != null) {
        for (var i = 0; i < fields.length; i++) {
            if (fields[i].showHover) return true;
        }
    }
    // Either null or false
    return this.canHover;
},

_$hoverHTML: "hoverHTML",
_$fieldHoverHTMLArgNames: "record,value,rowNum,colNum,grid",
_getFieldHoverHTMLCallback : function (rowNum, colNum) {
    var field = this.getField(colNum);
    if (field == null) return null;
    if (field.showHover == false) return null;
    if (field.showHover == null && !this.canHover) return null;
    if (field.hoverHTML) {
        isc.Func.replaceWithMethod(field, this._$hoverHTML, this._$fieldHoverHTMLArgNames);
        return {
            target: field,
            methodName: this._$hoverHTML
        };
    }
    return null;
},

_isCellHoverSuppressed : function (rowNum, colNum) {
    // If we're showing an editor in the cell suppress the standard cell hover.
    if (this.isEditingCell(rowNum, colNum)) {
        return true;
    }
    // Don't attempt to write inactive editor HTML into the hover prompt. This would be
    // odd user-experience and can lead to warnings about inactiveEditorContexts getting
    // unexpectedly dropped.

    if (this._showInactiveEditor(colNum) && this.canEditCell(rowNum, colNum)) {
        return true;
    }
    return false;
},

getCellHoverDelay : function (record, rowNum, colNum) {
    var fieldDelay = this.getField(colNum).hoverDelay;
    if (fieldDelay != null) return fieldDelay;
    return this.hoverDelay;
},

// NOTE: JSDoc imported from GR
cellHoverHTML : function (record, rowNum, colNum) {
    var fieldHoverHTMLCallback = this._getFieldHoverHTMLCallback(rowNum, colNum),
        value = this.getCellValue(record, rowNum, colNum);
    if (fieldHoverHTMLCallback) {
        return isc.Class.fireCallback(fieldHoverHTMLCallback,
                                      this._$fieldHoverHTMLArgNames,
                                      [ record, value, rowNum, colNum, this ]);
    }

    if (value != null && !isc.isAn.emptyString(value) && value != this.emptyCellValue) {
        return value;
    }
},

cellValueHoverHTML : function (record, rowNum, colNum, defaultHTML) {
    var field = this.getField(colNum);
    if (field.showHover == false) return null;
    if (field.showHover == null && this.canHover == false) return null;

    return defaultHTML;
},

//> @method listGridField.hoverHTML()
// StringMethod override point for returning HTML to be shown in hovers over cells in the
// column described by this field.
// <P>
// Called only when +link{listGrid.canHover,canHover} and +link{listGrid.showHover,showHover}
// are both true.
// <P>
// The value of "this" within the method will by the +link{ListGridField,field definition}.
//
// @param record (ListGridRecord) record being hovered over
// @param value  (any) value of the cell being hovered over
// @param rowNum (number) row number where hover occurred
// @param colNum (number) column number where hover occurred
// @param grid   (ListGrid) ListGrid this field is a part of
// @return (HTML) HTML to show in the hover
// @group hovers
// @visibility external
// @example valueHoverTips
//<

//> @attr listGridField.hoverDelay (integer : null : IRW)
// Delay in ms for hovers shown for cells in this field. If unset, the grid's
// hoverDelay will be used.
// <p>
// This property also governes the hoverDelay for the header button. Developers wishing
// to have a different delay for hovers on the header can use +link{listGridField.headerHoverDelay}
// in addition to this property.
//
// @visibility external
//<


//> @attr listGridField.headerHoverDelay (integer : null : IRW)
// Delay in ms for hovers shown over this field's header.
// <P>
// If unset, any +link{listGridField.hoverDelay} will be used for both header hovers
// and hovers shown over cells in the grid's body.
//
// @visibility external
//<

//> @attr listGrid.showHoverComponents (Boolean : false : IRWA)
// When set to true and canHover is also true, shows a widget hovering at the mouse point.
// <P>
// A number of builtin modes are provided - see +link{type:HoverMode}.
// @group hoverComponents
// @visibility external
//<


//> @type HoverMode
// When +link{ListGrid.canHover, canHover} and
// +link{ListGrid.showHoverComponents, showHoverComponents} are both true, HoverMode
// dictates the type of UI to be displayed when a user hovers over a row or cell.
// <P>
// There are a number of builtin HoverModes and you can override
// +link{listGrid.getCellHoverComponent, getCellHoverComponent()} to create your own
// hover behaviors.
//
//  @value  "detailField"  Show a single field's value in an +link{class:HtmlFlow}. Field
//      to use is +link{listGrid.detailField}.
//  @value  "details"   Show a +link{class:DetailViewer} displaying those fields from the
//      record which are not already displayed in the grid.
//  @value  "related"    Show a separate +link{class:ListGrid} containing related-records.
//      See +link{ListGridRecord.detailDS} and +link{ListGrid.recordDetailDSProperty} for
//      more information.
//  @value  "detailRelated"    Show a +link{class:DetailViewer} displaying those fields
//      from the record not already displayed in the grid, together with a separate
//      +link{class:ListGrid} containing related-records.
// @group hoverComponents
// @visibility external
//<

//> @attr listGrid.hoverMode (HoverMode : null : IRWA)
// When +link{ListGrid.showHoverComponents, showHoverComponents} is true, the builtin mode
// to use when automatically creating a hover component for rows in this grid.
// <P>
// A number of builtin modes are provided - see +link{type:HoverMode}.  You can also override
// +link{ListGrid.getCellHoverComponent, getCellHoverComponent()} to provide a custom hover
// widget - in that case, this attribute is ignored.
// @group hoverComponents
// @visibility external
//<

_getCellHoverComponent : function (record, rowNum, colNum) {
    if (!this.showHoverComponents) return null;
    // If we're showing an editor in the cell suppress the standard cell hover.
    if (this._editorShowing && this.getEditRow() == rowNum &&
        (!this.editByCell || this.getEditCol() == colNum)) return null;
    var field = this.getField(colNum);
    if (field.showHover == false) return null;
    if (field.showHover == null && !this.canHover) return null;

    if (this.getCellHoverComponent && isc.isA.Function(this.getCellHoverComponent)) {
        return this.getCellHoverComponent(record, rowNum, colNum);
    } else return null;
},

//> @method listGrid.getCellHoverComponent()
// When +link{showHoverComponents} is set, this method is called to get the component to show
// as a hover for the current cell.
// <P>
// By default, this method returns one of a set of builtin components, according to the
// value of +link{type:HoverMode, listGrid.hoverMode}.  You can override this method
// to return any component you wish to provide as a hoverComponent, or invoke the superclass
// method to have the default hover component generated, then further customize it.
// <P>
// By default, components returned by <code>getCellHoverComponent()</code> will be
// automatically destroyed when the hover is hidden.  To prevent this, set
// +link{canvas.hoverAutoDestroy} to false on the returned component.
//
// @param record (Record) record to get the hoverComponent for
// @param rowNum (Integer) row number for the cell
// @param colNum (Integer) column number of the cell
// @return (Canvas | Canvas Properties) the component to show as a hover
// @group hoverComponents
// @visibility external
//<
defaultCellHoverComponentWidth: 100,
defaultCellHoverComponentHeight: 1,
getCellHoverComponent : function (record, rowNum, colNum) {
    return this._getStockEmbeddedComponent(record, false, true, rowNum, colNum);
},

_getStockEmbeddedComponent : function (record, isExpansion, isHover, rowNum, colNum) {
    var gridFields = this.getFields(),
        dsFields = this.dataSource ? isc.getValues(this.getDataSource().getFields()) :
            gridFields,
        defWidth = (!isHover ? null :
            this.hoverWidth || this.hoverSize || this.defaultCellHoverComponentWidth),
        defHeight = (!isHover ? null :
            this.hoverHeight || this.hoverSize || this.defaultCellHoverComponentHeight),
        remainingFields = [],
        component,
        fieldEscapeHTML = false
    ;

    for (var i=0; i<dsFields.length; i++) {
        var field = dsFields.get(i);

        if (this.detailField && field.name == this.detailField && field.escapeHTML) {
            fieldEscapeHTML = true;
        }

        if (this.dataSource) {
            if (!gridFields.find("name", field.name)) {
                remainingFields.add(field);
            }
        } else {
            // show all fields when not databound
            if (!this.isExpansionField(field)) {
                remainingFields.add(field);
            }
        }
    }

    var mode = (isHover ? this.hoverMode : (isExpansion ? this.expansionMode : null));

    var props;

    // create an appropriate subcomponent and bind it
    if (mode == "detailField") {
        if (!isExpansion && !record[this.detailField]) return null;
        var value = record[this.detailField];
        if (fieldEscapeHTML && isc.isA.String(value) && value != this._$nbsp) {
            value = value.asHTML();
        }
        component = this.createAutoChild("expansionDetailField", {
            styleName: this.hoverStyle,
            contents: value
        });
        props = {
            width: (isHover ? defWidth : "100%"),
            height: (isHover ? defHeight : "100%"),
            members: [component]
        };

        if (isHover) {
            props = isc.addProperties(props, {
                hoverAutoDestroy: this.hoverAutoDestroy
            });
        }

        component = isc.VLayout.create(props);
    } else if (mode == "details") {

        remainingFields = remainingFields.map(function (field) {
            if (field.showIf == null) return field;
            return isc.addProperties({}, field, {showIf: "true"});
        });
        props = { dataSource: this.dataSource, fields: remainingFields };

        if (isHover) {
            props = isc.addProperties(props, {
                width: defWidth,
                height: defHeight,
                hoverAutoDestroy: this.hoverAutoDestroy
            });
        }
        component = this.createAutoChild("expansionDetails", props);

        if (isc.isA.ResultSet(record)) {
            component.setData(record);
        } else {
            // create ResultSet wrapper on the record, it is possible to observe changes in the datasource
            var recordResultSet = isc.ResultSet.create({
                dataSource : this.dataSource,
                initialData : [record]
            });
            component.setData(recordResultSet);
        }
    } else if (mode == "related") {
        props = { dataSource: this.getRelatedDataSource(record) };

        if (isHover) {
            props = isc.addProperties(props, {
                canEdit: false,
                width: defWidth,
                height: defHeight,
                dataProperties: { context: { showPrompt: false } },
                hoverAutoDestroy: this.hoverAutoDestroy
            });
        }

        if (isExpansion) {
            // only propagate to autochild if childExpansionMode has been set
            if (this.childExpansionMode != null) props = isc.addProperties(props, {
                canExpandRecords: this.childExpansionMode ? true : false,
                expansionMode: this.childExpansionMode
            });
            // only propagate to autochild if expansionCanEdit has been set
            if (this.expansionCanEdit != null) props = isc.addProperties(props, {
                canEdit: this.expansionCanEdit
            });
        }

        component = this.createAutoChild("expansionRelated", props);

        // if editing is allowed in the sub-grid, set autoSaveEdits: true
        if (this.expansionCanEdit) component.autoSaveEdits = true;
        component.delayCall("fetchRelatedData", [record, this.dataSource,
            function (dsResponse, data) {
                // for hoverMode: "related", fix ListGrid shown when no related records are
                // present; grid's GridBody is squished; apply overflow: visible to fix it
                if (data == null || data.length == 0) component.setBodyOverflow("visible");
        }]);

    } else if (mode == "detailRelated") {
        props = { dataSource: this.dataSource, fields: remainingFields };

        if (isHover) {
            props = isc.addProperties(props, {
                dataProperties: { context: { showPrompt: false } },
                hoverAutoDestroy: this.hoverAutoDestroy
            });
        }

        var detail = this.createAutoChild("expansionDetails", props)
        detail.setData(record);

        props = { dataSource: this.getRelatedDataSource(record), height: "100%" };

        if (isHover) {
            props = isc.addProperties(props, {
                dataProperties: { context: { showPrompt: false } },
                hoverAutoDestroy: this.hoverAutoDestroy,
                canEdit: false
            });
        }
        if (isExpansion) {
            // only propagate to autochild if childExpansionMode has been set
            if (this.childExpansionMode != null) props = isc.addProperties(props, {
                canExpandRecords: this.childExpansionMode ? true : false,
                expansionMode: this.childExpansionMode
            });
            // only propagate to autochild if expansionCanEdit has been set
            if (this.expansionCanEdit != null) props = isc.addProperties(props, {
                canEdit: this.expansionCanEdit
            });
        }

        var related = this.createAutoChild("expansionRelated", props);

        props = { members:[detail, related] };
        if (isHover) {
            props = isc.addProperties(props, {
                width: defWidth,
                height: defHeight,
                hoverAutoDestroy: this.hoverAutoDestroy
            });
        }

        component = this.createAutoChild("expansionDetailRelated", props);

    } else if (mode == "editor") {
        component = isc.VLayout.create({
            autoDraw: false,
            width: "100%",
            height: "100%",
            creator: this
        });

        // create an editor form, edit the record in question and validate it - this causes an
        // editor to show validation errors when autoSaveEdits is false and saveAllEdits is
        // called.
        var editorForm = this.createAutoChild("expansionEditor", {
            dataSource: this.dataSource,
            fields: remainingFields,
            extraSpace: 5,
            itemChanged : function () {
                this.saveButton.setDisabled(false);
            }
        });
        editorForm.editRecord(this.getEditedRecord(rowNum) || record);
        editorForm.validate();

        // create a save button for this expansionEditor - add refs to the editor-form and the
        // record being edited for later use.
        var saveButton = this.createAutoChild("expansionEditorSaveButton", {
            title: this.expansionEditorSaveButtonTitle,
            grid: this,
            form: editorForm,
            record: record,
            disabled: true,
            extraSpace: 5
        });

        // add a ref to the save-button onto the editor-form, so we can enable/disable the
        // button according to whether changes have been made
        editorForm.saveButton = saveButton;

        // add a ref to the editorForm to the actual expansion-component - this is used when
        // getting hold of the editor-form to check for changes when a record is collapsed
        component.formMember = editorForm;
        component.addMembers([editorForm, saveButton]);
    }

    if (component) {
        // mark as a stock (auto-generated) component so we can auto-destroy it later
        component.isStockComponent = true;
        component.expandedRecord = record;
    }

    return component;
},

//> @attr listGridField.showAlternateStyle (boolean : null : IRWA)
// When set to false, don't apply alternate-row styling to this field.
// @visibility external
//<

// Selection
// --------------------------------------------------------------------------------------------

// Simple helper methods to avoid having to refer directly to this.selection
// Genericized up to DataBoundComponent, July 2008


//> @method listGrid.selectRecord()
// Select/deselect a +link{Record} passed in explicitly, or by index.
// <P>
// Note that this method selects records unconditionally, allowing multiple selected
// records, even when +link{listGrid.selectionType} is "single".  To enforce mutually-exclusive
// record-selection, use +link{listGrid.selectSingleRecord}.
// @include dataBoundComponent.selectRecord()
//<

//> @method listGrid.deselectRecord()
// @include dataBoundComponent.deselectRecord()
//<

//> @method listGrid.selectRecords()
// @include dataBoundComponent.selectRecords()
//<

//> @method listGrid.deselectRecords()
// @include dataBoundComponent.deselectRecords()
//<

//> @method listGrid.selectAllRecords()
// Select all records.
// <P>
// Note that this method will select records even if +link{listGrid.canSelectRecord()} returns
// false for the record in question. See also +link{listGrid.userSelectAllRecords()}
// @visibility external
//<
selectAllRecords : function () {
    this._dontRefreshSelection = true;
    this.selection.selectAll();
    this._dontRefreshSelection = null;
    this._markBodyForRedraw("select all");
    if (this.getCurrentCheckboxField() != null) {
        // don't check the "all records selected header box if we can't select all records
        if (!isc.ResultSet || !isc.isA.ResultSet(this.data) ||
                this.data.allMatchingRowsCached())
        {
            this._setCheckboxHeaderState(true);
        }
    }
    this.fireSelectionUpdated();
},

//> @method listGrid.deselectAllRecords()
// @include dataBoundComponent.deselectAllRecords()
//<
deselectAllRecords : function () {
    this._dontRefreshSelection = true;
    this.selection.deselectAll();
    this._dontRefreshSelection = null;
    this._markBodyForRedraw("deselect all");
    if (this.getCurrentCheckboxField() != null) this._setCheckboxHeaderState(false);
    this.fireSelectionUpdated();
},

//> @method listGrid.selectRange()
// @include dataBoundComponent.selectRange()
//<

//> @method listGrid.deselectRange()
// @include dataBoundComponent.deselectRange()
//<

//> @method listGrid.userSelectAllRecords()
// Selects every user-selectable record in the grid. Unlike +link{listGrid.selectAllRecords()},
// if a record is +link{listGrid.canSelectRecord(),unselectable}, this method will not
// attempt to select it.
// @visibility external
//<
userSelectAllRecords : function () {
    var data = this.getOriginalData(),
        total = data.getLength();
    if (isc.ResultSet && isc.isA.ResultSet(data) && !data.rangeIsLoaded(0, total)) {
        this.selection.warnSelectionRangeNotLoaded();
        return;
    }
    var records = (this.isGrouped ? this.getAllRecordsFromGroupTree()
                                : data.getRange(0, total)),
        finalRecords = [];
    for (var i = 0; i < records.length; i++) {
        if (!this.canSelectRecord(records[i])) continue;
        finalRecords[finalRecords.length] = records[i];
    }
    this.selection.selectList(finalRecords);
    this.fireSelectionUpdated();
},

getAllRecordsFromGroupTree : function () {
    var data;
    if (this.isGrouped) data = this.data;
    if (!data || !isc.isA.Tree(data)) return;

    // getDescendantLeaves will skip over the group-header nodes.
    return this.data.getDescendantLeaves(this.data.getRoot());
},

//> @method listGrid.anySelected()
// @include dataBoundComponent.anySelected()
//<

//> @method listGrid.selectSingleRecord()
// @include dataBoundComponent.selectSingleRecord
// @visibility external
//<

//> @method listGrid.canSelectCell() (A)
// If +link{listGrid.canSelectCells} is set to <code>true</code> then, whenever an end-user or
// programmatic cell-selection is attempted, this method is called for
// each cell in the selection. If it returns false, the cell will not be selected.
//
// @param rowNum (int) rowNum being selected
// @param colNum (int) colNum being selected
// @return (boolean) return false to disallow selection
//
// @visibility external
//<
canSelectCell : function(rowNum, colNum) {
    return true;
},

//> @method listGrid.canSelectRecord()
// If +link{listGrid.selectionType} is not set to <code>"none"</code>,
// This method will be called for each record the user attempts to select. If it returns false, the
// record will not be selected.
// <P>
// Default implementation will return true for any records where
// +link{listGrid.recordCanSelectProperty} is not explicitly set to false.
// <P>
// Note this method will not be called at all if +link{canSelectCells} is true.
//
// @param record (ListGridRecord) record being selected
// @return (boolean) return false to disallow selection
//
// @visibility external
//<
canSelectRecord : function(record) {
    return (record != null && record[this.recordCanSelectProperty] !== false);
},
// Keyboard Navigation
// --------------------------------------------------------------------------------------------

//> @method listGrid.keyPress()
// Handle a keyPress event on the ListGrid as a whole.
// <P>
// Note that the majority of keyboard handling for a ListGrid is performed by
// +link{bodyKeyPress()} and most overrides are better performed there.
//
// @return (boolean) return false to cancel
// @visibility external
//<

//> @method listGrid.bodyKeyPress()
// Handle a keyPress event on the body.
// <P>
// Default implementation handles navigating between records with arrow keys, and activating
// records with space and enter.
//
// @return (boolean) return false to cancel
//
// @visibility external
//<

_$ArrowUp:"Arrow_Up", _$ArrowDown:"Arrow_Down",
_$ArrowLeft:"Arrow_Left", _$ArrowRight:"Arrow_Right",
_$Space:"Space", _$Enter:"Enter",
_$f2:"f2",
_$Tab:"Tab",
bodyKeyPress : function (event, eventInfo) {

    // Easy to wrap entryPoint for SGWT.
    if (this.onBodyKeyPress(event, eventInfo) == false) return false;


    if (this._editorShowing) {
        var target = event.keyTarget,
            canvasItem;
        while (canvasItem == null && target != this && target != null) {
            canvasItem = target.canvasItem;
            target = target.parentElement;
        }
        if (canvasItem != null && canvasItem.form == this.getEditForm()) {
            var returnVal = this.editorKeyPress(canvasItem, isc.EH.getKey(),
                                isc.EH.getKeyEventCharacterValue());
            return (returnVal == null ? isc.EH.STOP_BUBBLING : returnVal);
        }
    }
    if (this.data.getLength() > 0) {

        // if we start editing on keypress, return false to kill the event (avoiding
        // page navigation on backspace keypress, etc)
        var EH = isc.EventHandler,
            keyName = event.keyName;

        // for arrow keys, navigate to the appropriate record
        var editOnKeyPress = this.editOnKeyPress && this.isEditable();
        if (editOnKeyPress && this._editOnKeyPress(event, eventInfo)) return false;

        var focusCell = this.getFocusCell(),
            record = this.getCellRecord(focusCell[0], focusCell[1]);

        if (this.canExpandRecords && this._canExpandRecord(record, focusCell[0])) {
            var expanded = this.isExpanded(record);
            if (expanded && keyName == this._$ArrowLeft) {
                this.collapseRecord(record);
                return;
            }
            if (!expanded && keyName == this._$ArrowRight) {
                this.expandRecord(record);
                return;
            }
        }

        switch (keyName) {
            case this._$ArrowUp:    return this._navigateToNextCell(-1,  0);
            case this._$ArrowDown:  return this._navigateToNextCell( 1,  0);
            case this._$ArrowLeft:  return this._navigateToNextCell( 0, -1);
            case this._$ArrowRight: return this._navigateToNextCell( 0,  1);
        }

        if (this.shouldNavigateOnTab() && keyName == this._$Tab) {
            if (isc.EH.shiftKeyDown()) return this._navigateToNextCell(0, -1, true);
            else return this._navigateToNextCell(0,1, true);
        }

        // Generate a click on the current focus record when the user hits Space
        if (keyName == this._$Space) {

            // generateFocusRecordClick return values
            // - false implies the actual rowClick handler returned false
            //   In this case suppress the doubleClick (if necessary) and return
            //   false immediately to stop propogation
            // - true implies we found a real cell and executed the rowClick handler
            //   allow doubleclick to fire if necessary, then return false to
            //   cancel propogation of the keypress event.
            //   [we don't for example want a parent to do some odd scroll etc
            //   responding to the space keypress]
            // - null implies we didn't find a target cell. Just allow normal
            //   event propogation.
            var propogate = true;
            if (this.generateClickOnSpace) {
                var rowClickReturnValue = this._generateFocusRecordClick();
                if (rowClickReturnValue == false) return false;
                else if (rowClickReturnValue == true) propogate = false;
            }

            if (this.generateDoubleClickOnSpace) {
                var rowDCReturnValue = this._generateFocusRecordDoubleClick();
                if (rowDCReturnValue != null) propogate = false;
            }
            if (!propogate) return false;

        // Generate a doubleClick on the current focus record when the user hits Enter
        } else if (keyName == this._$Enter) {
            // See "Space" keypress handling for overview of
            // the generateFocusRecordXXX() return values
            var propogate = true;
            if (this.generateClickOnEnter) {
                var rowClickReturnValue = this._generateFocusRecordClick();
                if (rowClickReturnValue == false) return false;
                else if (rowClickReturnValue == true) propogate = false;
            }

            if (this.generateDoubleClickOnEnter) {
                var rowDCReturnValue = this._generateFocusRecordDoubleClick();
                if (rowDCReturnValue != null) propogate = false;
            }
            if (!propogate) return false;

        // Invoke one of the copy/paste shortcuts controlled by useCopyPasteShortcuts
        } else if (EH.modifierKeyDown() &&
                   (keyName == "D" || keyName == "R" || keyName == "C" || keyName == "V")) {
            return this._invokeKeyboardCopyPasteShortcut(keyName);

        // support selecting all cells using Ctrl-A shortcut
        } else if (EH.modifierKeyDown() && keyName == "A" &&
                   this.canSelectCells && this.canSelectAll) {
            this.selection.selectAll();
            return false;

        // Start editing on f2 keypress if editOnF2Keypress is true.
        } else if (keyName == this._$f2 && this.editOnF2Keypress &&
                    this.isEditable() && this.editEvent != "none")
        {
            var rowNum, colNum;
            if (this.canSelectCells && this.editByCell) {
                var cell = this.getFocusCell();
                if (cell[0] >= 0) rowNum = cell[0];
                if (cell[1] >= 0) colNum = cell[1];
            } else {
                rowNum = this.getFocusRow();
                if (rowNum < 0) rowNum = null;
            }
            this.startEditing(rowNum, colNum);
            return false;
        }

    // If the grid is empty, do nothing.

    }
    return true;
},

//> @method ListGrid.onBodyKeyPress()
// @include ListGrid.bodyKeyPress
// @return (boolean) return false to cancel default drop handling
// @visibility sgwt
//<

onBodyKeyPress : function () {
},


_navigateToNextCell : function (rowStep, colStep, isTabNavigation, checkFirst, navStyle) {
    if (!this.canSelectCells) return this._navigateToNextRecord(rowStep, checkFirst, navStyle);
    else if (this.selectionType == isc.Selection.NONE) return true;

    if (navStyle == null) {
        navStyle = isTabNavigation ? this.tabKeyAction : this.getArrowKeyAction();
    }
    if (navStyle == this._$none) return true;

    // TabNavigation is slightly different

    var shiftedSelection = isc.EH.shiftKeyDown();

    // To match Excel, re-establish origin if it has been lost
    if (navStyle == this._$select && shiftedSelection) {
        this._ensureValidSelectionOrigin();
    }

    var hiliteCell = this.getFocusCell(shiftedSelection);

//this.logWarn("Get HiliteCell returned this:" + hiliteCell);
    var newRow = hiliteCell[0], newCol = hiliteCell[1];

    if (newRow != null && newCol != null) {
        if (!checkFirst) {
            newRow += rowStep;
            newCol += colStep;
        }
    } else {
        newRow = newCol = 0;
    }
    // getCellRecord ignores the colNum for 1 record / row data models, so
    // with cellSelection enabled, we can go off the end horizontally and
    // still appear to have a valid (enabled) record.
    // Catch this case explicitly
    var totalRows = this.getTotalRows(), totalCols = this.getTotalCols();
    var rowEndAction = "done";
    if (isTabNavigation) rowEndAction = this.rowEndEditAction || "next";

    if (newCol < 0 || newCol >= totalCols) {
        if (rowEndAction == "same") {
            if (colStep > 0) newCol = 0;
            else newCol = totalRows -1;
        } else if (rowEndAction == "next") {
            if (colStep > 0) {
                if (newRow < totalRows-1) {
                    newRow++;
                    newCol = 0;
                } else return true;
            } else {
                if (newRow > 0) {
                    newRow--;
                    newCol = totalCols-1;
                } else return true;
            }
        } else {
            if (rowEndAction == "stop") return false; // suppress normal tab behavior
            // rowEndAction "done" or "none" - return true allowing the
            // user to tab out of the widget.
            return true;
        }
    }

    // For Ctrl+Shift+Arrow Key, We don't want to end up on a disabled cell, so we
    // still need to use the skipping logic below, but reverse the search direction
    var selectToEnd = navStyle == this._$select && isc.EH.modifierKeyDown();
    if (selectToEnd) {
        if      (rowStep > 0) newRow = this.getTotalRows() -1;
        else if (rowStep < 0) newRow = 0;
        if      (colStep > 0) newCol = this.getTotalCols() -1;
        else if (colStep < 0) newCol = 0;
        rowStep = -rowStep;
        colStep = -colStep;
    }
    // At this point we have a pointer to the cell we'd like to hilite / click.
    // If the cell is not enabled, find the next one that is
    while (!this.recordIsEnabled(this.getCellRecord(newRow, newCol), newRow, newCol)) {
        if (rowStep != 0) newRow += rowStep;
        if (colStep != 0) newCol += colStep;
        // bail if reverse search returns to original cell
        if (newRow == hiliteCell[0] && newCol == hiliteCell[1]) {
            return true;
        }
        // moving off the end of the row in either direction - respect the
        // rowEndAction (set up for tab keypresses only)
        if (newCol < 0 || newCol >= totalCols) {
            if (rowEndAction == "same") {
                if (colStep > 0) newCol = 0;
                else newCol = totalRows -1;
            } else if (rowEndAction == "next") {
                if (colStep > 0) {
                    if (newRow < totalRows-1) {
                        newRow++;
                        newCol = 0;
                    } else return true;
                } else {
                    if (newRow > 0) {
                        newRow--;
                        newCol = totalCols-1;
                    } else return true;
                }
            } else {
                if (rowEndAction == "stop") return false; // suppress normal tab behavior
                // rowEndAction "done" or "none" - return true allowing the
                // user to tab out of the widget.
                return true;
            }
        } else if (newRow < 0 || newRow >= totalRows) {
            return true;
        }
    }

    // Note - we already returned if the navigation style is null, so it is fair to assume that
    // we're either going to simulate a click on a cell, or hilite one.
    //
    // Clear out the last hilite. This will both clear out the keyboard specific properties
    // (_lastKeyboardHiliteRow and col), and reset the css style of the last over cell
    this.clearLastHilite();

    // Remember which row and col we're interested in
    this._lastKeyboardHiliteRow = newRow;
    this._lastKeyboardHiliteCol = newCol;
    this._lastKeyboardHiliteBody = this.getFieldBody(newCol);

    if (navStyle == this._$select) {
        if (isTabNavigation)this.selection.deselectAll()
        this._generateCellClick(newRow, newCol);
    }
    if (navStyle == this._$focus)  this._hiliteCell(newRow, newCol);

    this._handlingKeyboardNavigation = true;
    this.scrollToCell(newRow, newCol);
    this._handlingKeyboardNavigation = false;

    return false; // stop event propagation
},

//> @method listGrid.focusInCell()
// Puts keyboard focus into the specified cell, showing a highlighted (roll-over style)
// appearance, and ensuring that arrow-key navigation will start from the specified cell.
// <P>
// Only applies where +link{listGrid.canSelectCells} is true.
// @param row (Integer) Index of target row
// @param col (Integer) Index of target col
// @see listGrid.focusInRow()
// @visibility external
//<
focusInCell : function (row, col) {
    this._lastKeyboardHiliteRow = row;
    this._lastKeyboardHiliteCol = col;
    var body = this.getFieldBody(col);
    this._lastKeyboardHiliteBody = body;

    this._hiliteCell(row, col);

    if (!body.hasFocus()) body.focus();

},

//> @method listGrid.focusInRow()
// Puts keyboard focus into the specified row, showing a highlighted (roll-over style)
// appearance, and ensuring that arrow-key navigation will start from the specified row.
// <P>
// Only applies where +link{listGrid.canSelectCells} is false.
// @param row (Integer) Index of target row
// @see listGrid.focusInCell()
// @visibility external
//<
focusInRow : function (row) {
    this._hiliteRecord(row);
    if (this.body && !this.body.hasFocus) this.body.focus();
},

_hiliteCell : function (row, col) {
    // if passed in a cell, resolve it to row / col
    if (isc.isAn.Object(row)) {
        col = row._colNum;
        row = row._rowNum;
    }

    // bail if the coordinates passed in don't match a valid row
    if (row == null || col == null ||
        row < 0     || col < 0     ||
        row >= this.getTotalRows() || col >= this.getTotalCols()) return;

    var body = this._lastKeyboardHiliteBody,
        colOffset = body.selection.firstCol || 0;

    body.lastOverRow = row;
    body.lastOverCol = col - colOffset;
    body.setRowStyle(row, null, col - colOffset);
},

_getLastMouseOverBody : function () {
    if (!this.frozenBody) return this.body;
    if (this.frozenBody.lastMouseOverRow == null &&
        this.frozenBody.lastMouseOverCol == null) return this.body;
    return this.frozenBody;
},

_ensureValidSelectionOrigin : function () {
     if (!this.selection._validateSelectionOrigin(this)) {
         var lastBody = this._getLastMouseOverBody(),
             colOffset = lastBody.selection.firstCol || 0;
         var row = lastBody.lastMouseOverRow || 0,
             col = lastBody.lastMouseOverCol || 0,
             record = this.getCellRecord(row, col + colOffset);
         if (this.recordIsEnabled(record, row, col + colOffset)) {
             lastBody.selectOnMouseDown(record, row, col);
         }
     }
 },

// editOnKeyPress behavior
// - modelled on spreadsheet style application editing
// - normal record selection and navigation occurs on click / arrow keypress when not currently
//   editing the grid
// - on character keypress, start editing (respecting the character typed)

_$f2:"f2",
_$Escape:"Escape",
_$Backspace:"Backspace",
_$Delete:"Delete",
_$keyPress:"keyPress",
_editOnKeyPress : function (event, eventInfo) {
    var keyName = eventInfo.keyName,
        charVal = isc.EH.getKeyEventCharacter(event);
    // We don't want to start editing if the user hit a non character key, such as a function key
    // or escape, etc

    if (keyName != this._$f2 && keyName != this._$Delete && keyName != this._$Backspace &&
        (keyName == this._$Escape ||
            isc.EH._nonCharacterKeyMap[keyName] || charVal == null || charVal == isc.emptyString))
    {
        return false;
    }

    var cell = this.getFocusCell(),
        row = cell[0] || 0,
        col = cell[1] || 0;
    // If we're already showing an editor just bail
    if (this._editorShowing || row < 0 || col < 0) return false;

    // on Enter / f2 keypress don't modify the value in the cell
    var undef;
    if (keyName == this._$Enter || keyName == this._$f2) charVal = null;

    var editVal;
    if (charVal != null) {
        if (keyName == this._$Delete || keyName == this._$Backspace) {
            editVal = null;
        } else if (this.autoSelectEditors) {
            editVal = charVal;
        } else {
            editVal = this.getEditedCell(row,col) + charVal;
        }
        // this flag ensures that when we focus in the item we put selection at the end, rather
        // than selecting the entire value.
        this._editorCursorAtEnd = true;
    }
    return this.handleEditCellEvent(cell[0], cell[1], this._$keyPress, editVal);

},

// getArrowKeyAction() - used by _navigateToNextRecord() to determine how the record
// should be hilighted.

// Strings used in navigation styles
_$none:"none", _$focus:"focus", _$select:"select", _$activate:"activate",
getArrowKeyAction : function () {

    var action = this.arrowKeyAction;

    // No action at all trumps everything
    if (action == this._$none) return this._$none;

    // if ctrl key is down but not shift, always just hilite / focus
    var doNotFocus = this.canSelectCells && isc.EH.shiftKeyDown();
    if (isc.EH.modifierKeyDown() && !doNotFocus) return this._$focus;

    // if an explicit arrowKeyAction has been set, respect it
    if (action != null) return action;

    // default to FOCUS if selectionAppearance is "checkbox"; SELECT otherwise
    return this.selectionAppearance == "checkbox" ? this._$focus : this._$select;
},


// In screenReader mode when putting focus onto an actual row element in the body,
// should we show normal keyboard hilite styling etc?
hiliteOnNativeRowFocus:true,

// _navigateToNextRecord()
// Called from this.bodyKeyPress() on arrow keys, to handle navigating around the listGrid.
// If step == +1, we want to navigate to the next record in the list, if step is -1, we want to
// navigate to the previous record.
// Determines which record to navigate to, and falls through to _generateCellClick() or
// _hiliteRecord() depending on the result of this.getArrowKeyAction()
_navigateToNextRecord : function (step, checkFirst, navStyle, suppressScroll) {
    // Are we going to simulate a click on the next record or just hilight it?
    var navStyle = navStyle == null ? this.getArrowKeyAction() : navStyle;

    // If keyboard navigation is disabled return true to allow processing to continue.
    if (navStyle == this._$none || step === 0) return true;

    // Note: we are either going forward or backward one record - assume jumping over multiple
    // records is not supported
    //
    // Default to selecting the next record
    if (step == null) step = 1;

    // Determine which record was last hilighted or clicked
    var newSelectionIndex;

    // By default we want the last row that had keyboard focus
    newSelectionIndex = this.getFocusRow(step > 0);

    // Otherwise, get the last record clicked
    if (newSelectionIndex == null) newSelectionIndex = this._lastRecordClicked;

    var originalSelection = newSelectionIndex,
        normalizedOrigSelection;
    if (!isc.isA.Number(newSelectionIndex)) {
        newSelectionIndex = this.body ? this.body._getViewportFillRows()[0] : 0;
        normalizedOrigSelection = newSelectionIndex;
    } else {
        newSelectionIndex = newSelectionIndex+(checkFirst ? 0 : step);
        normalizedOrigSelection = originalSelection;
    }

    var lastRow = this.getTotalRows() -1;
    // if we are trying to navigate past the ends just ensure the focus row is selected
    if (newSelectionIndex < 0 || newSelectionIndex > lastRow) {
        // bail if there were no records
        if (lastRow < 0) return true;

        // Ensure the original record is selected / focused

        newSelectionIndex = normalizedOrigSelection;

    }
    // At this point we are sure that newSelectionIndex is a number.
    // If the number is beyond the end of the list in either direction, or
    // the record is not enabled, recordIsEnabled() will return false.
    // Try the next record in the step direction, and so on until we find an enabled record or
    // hit the end of the list.

    // If the record is disabled, find the first non-disabled record (in the appropriate
    // direction)
    while (!this.recordIsEnabled(this.getCellRecord(newSelectionIndex, 0), newSelectionIndex, 0)) {
        newSelectionIndex += step;
        // if we are trying to navigate past the ends of the list, bail
        if (newSelectionIndex < 0 || newSelectionIndex > lastRow) {
            newSelectionIndex = normalizedOrigSelection;
            break;
        }
    }
    // move native focus to the selected row so that screen readers will read it
    if (isc.screenReader) {
        this.body._putNativeFocusInRow(newSelectionIndex);
    }

    //this.logWarn("navStyle: " + navStyle + ", target index: " + newSelectionIndex);

    if (navStyle == this._$focus) this._hiliteRecord(newSelectionIndex);
    else {
        // Explicitly hilite the record.
        // This provides a visual cue (roll over styling) to indicate the record has
        // keyboard focus even if it is unselectable.
        this._hiliteRecord(newSelectionIndex);

        // if the user hit up arrow on the first row or down arrow on the last row, don't
        // actually force a click handler to fire on the row.
        // This leads to odd interactions with ListGrid editing on click as arrow down on the
        // last row will hide the editor, then arrow down again will show it on the same
        // row but the first editable col.
        // If the user does want to force a click on the current row via the keyboard
        // they can always hit space or enter.

        if (newSelectionIndex == originalSelection) {
            var colNum = this._getKeyboardClickNum();
            if (colNum >= 0) {
                if (this.body.selectionEnabled() &&
                    this.recordIsEnabled(
                        this.getCellRecord(newSelectionIndex, colNum),
                        newSelectionIndex,
                        colNum))
                {
                    this.selection.selectOnMouseDown(this, newSelectionIndex, colNum);
                    this.selection.selectOnMouseUp(this, newSelectionIndex, colNum);

                    // Explicitly fire the selectionUpdated notification
                    this.fireSelectionUpdated();
                }
            }
        }
        else if (navStyle == this._$select) this._generateCellClick(newSelectionIndex);
        else if (navStyle == this._$activate) this._generateRecordDoubleClick(newSelectionIndex);
    }
    if (!suppressScroll) {
        this._handlingKeyboardNavigation = true;
        this.scrollRecordIntoView(newSelectionIndex)
        this._handlingKeyboardNavigation = false;
    }
    // Don't allow the keypress event handling to continue here.
    return false;
},

_getKeyboardClickNum : function () {

    // If this.keyboardClickField was specified, return the appropriate colNum
    // Note - can be specified as a field number or field name...
    var kcf = this.keyboardClickField;
    if (kcf != null) {

        var kcCol;
        if (isc.isA.Number(kcf) && kcf > 0 && kcf < this.fields.length) {
            kcCol = this.fields[kcf];
        } else {
            kcCol = this.fields.find(this.fieldIdProperty, kcf);
        }
        if (kcCol && kcCol.ignoreKeyboardClicks) {
            this.logWarn("Explicitly specified keyboardClickField:" + this.keyboardClickField +
                " refers to a field which disallows keyboard click events.");
            kcCol = null;
        }
        if (kcCol != null) {
            return this.fields.indexOf(kcCol);
        }
    }
    if (this.getCurrentCheckboxField() != null) {
        return this.getCheckboxFieldPosition();
    }

    for (var i = 0; i < this.fields.length; i++) {
        if (this.fields[i].ignoreKeyboardClicks != true) return i;
    }
    // Unable to find a valid field - return -1. Calling code will have to handle this
    return -1;
},

_generateCellClick : function (rowNum, colNum, focus) {
    // Ensure we're not showing a rollOver hilite on a different row.
    if (!this.canSelectCells && this.body.lastOverRow != null &&
        this.body.lastOverRow != rowNum)
    {
        this.clearLastHilite();
    }

    // if passed a record, resolve it to an index!
    if (isc.isAn.Object(rowNum)) rowNum = this.getRecordIndex(rowNum);

    // Make sure we're not trying to select a record beyond the ends of the list.
    if (!isc.isA.Number(rowNum) || rowNum < 0) rowNum = 0;
    if (rowNum >= this.data.getLength()) rowNum = this.data.getLength() -1;

    var body;
    if (this.canSelectCells) {
        if (colNum == null || colNum < 0 || colNum >= this.getTotalCols()) return;
        body = focus ? this.getFieldBody(colNum) : this._lastKeyboardHiliteBody;
    } else {
        body = this.body;
        // remember we artificially selected this record from a keyboard event
        body._lastHiliteRow = rowNum;

        colNum = this._getKeyboardClickNum();
        if (colNum == -1) return;
    }
    var colOffset = body.selection.firstCol || 0;

    // Trigger the methods to perform the selection (selection.selectOnMouseDown AND
    // selection.selectOnMouseUp)

    var record = this.getCellRecord(rowNum, colNum),
        performSelection = this.selectOnGeneratedCellClick(record, rowNum, colNum, body);

    if (performSelection) body.selectOnMouseDown(record, rowNum, colNum - colOffset);
    if (performSelection) body.selectOnMouseUp(record, rowNum, colNum - colOffset);

    // explicitly fire this 'rowClick' method, passing in the additional method flagging this
    // as a keyboard generated click
    return this.rowClick(this.getCellRecord(rowNum, colNum), rowNum, colNum, true);

},

selectOnGeneratedCellClick : function (record, rowNum, colNum, body) {
    return (body.selectionEnabled() && this.recordIsEnabled(record, rowNum, colNum));
},

//> @method listGrid.getFocusRow() [A]
// Get the row that currently has keyboard focus.  Arrow key navigation moves relative to this
// row.
//
// @return (Integer) rowNum of the current focus row
// @visibility external
//<
// @param last (boolean) if multiple rows are selected, should we return the last row in the
//  selection (rather than the first?
getFocusRow : function (last) {

    // We want the last record hilighted by the keyboard.
    // Note: If the last keyboard hilite type event was a generated record click, the
    // lastHiliteRow will match the lastRecordclicked property for this widget.
    // If the last keyboard hilite type event was a hilite (rollover style) event, the
    // lastHiliteRow will match the lastOverRow for the body.
    // If neither of these are true, we can assume a subsequent mouse event has occurred over
    // a different row, effectively invalidating the _lastHiliteRow property, so should be
    // ignored and deleted
    if (this.body._lastHiliteRow != null &&
        ((this.body._lastHiliteRow == this.body.lastOverRow) ||
         (this.body._lastHiliteRow == this._lastRecordClicked)) )
    {
        return this.body._lastHiliteRow;
    }

    delete this.body._lastHiliteRow;
    // If there is no valid keyboard hilite row, return a record from the end of the selection
    // We use 'direction' param to indicate whether it's more appropriate to return the
    // first or last selected record of a multiple selection
    var selection = this.getSelection();
    if (selection.length == 0) return null;
    selection = selection[(last ? selection.length -1 : 0)]
    return this.getRecordIndex(selection);

},

// Helper method to get the last keyboard hilite cell position.
getFocusCell : function (querySelectionObject) {

    if (!this.canSelectCells) {
        return [this.getFocusRow(), this._getKeyboardClickNum()];
    }

    var row, col;

    // this._lastKeyboardHiliteRow and col are only valid if they match the last row clicked, or
    // the last row hilited - otherwise some non-keyboard manipulation has occurred since the values
    // were set.

//this.logWarn("row/col:" + this._lastKeyboardHiliteRow + ", " + this._lastKeyboardHiliteCol);
    if (this._lastKeyboardHiliteRow != null && this._lastKeyboardHiliteCol != null) {
        var body = this._lastKeyboardHiliteBody;
        if (body != null) {
            var colOffset = body.selection.firstCol || 0;
            if ((this._lastKeyboardHiliteRow == body.lastOverRow &&
                 this._lastKeyboardHiliteCol == body.lastOverCol + colOffset) ||
                (this._lastSelectedBody      == body &&
                 this._lastKeyboardHiliteRow == body._lastSelectedRow &&
                 this._lastKeyboardHiliteCol == body._lastSelectedCol + colOffset))
            {
                row = this._lastKeyboardHiliteRow;
                col = this._lastKeyboardHiliteCol;
            }
        }
    }

    // If we didn't find a keyboard cell, grab the last selected cell
    if (row == null || col == null) {
        if (querySelectionObject) {
            row = this.selection.lastRow;
            col = this.selection.lastCol;
        }
        var body = this._lastSelectedBody;
        if (body) {
            var colOffset = body.selection.firstCol || 0;
            if (row == null || col == null) {
                row = body._lastSelectedRow;
                col = body._lastSelectedCol + colOffset;
            }
        }
        if (row == null || col == null) {
            if (this.getSelection().getLength > 0) {
                var selection = this.getSelection();
                row = selection[0]._rowNum;
                col = selection[0]._colNum;
            }
        }
    }
    return [row, col];
},

// Called from grid body keyboard event handling.
// Return values as follows
// - return false if actual rowClick etc returned false.
// - return true if a valid cell was found and the rowClick executed
// - return null if no valid cell was found
_generateFocusRecordClick : function () {
    var cell = this.getFocusCell(),
        row = cell[0],
        col = cell[1];

    if (row != null && col != null) {
        var rv = this._generateCellClick(row, col, true);
        if (rv == null) rv = true;
        return rv;
    }
    return null;
},

_generateRecordDoubleClick : function (rowNum, colNum) {
    if (colNum == null) colNum = this._getKeyboardClickNum();
    if (colNum == null || colNum == -1) return;

    // generate a double click, on the appropriate record (and field), passing in the
    // parameter flagging this as a keyboard synthesized click event.
    //this._handleRecordDoubleClick(currentRecord, colNum);
    return this.rowDoubleClick(this.getCellRecord(rowNum, colNum), rowNum, colNum, true);
},

_generateFocusRecordDoubleClick : function () {
    var cell = this.getFocusCell(),
        row = cell[0],
        col = cell[1];
    if (row != null && col != null && col >= 0) {
        var rv = this._generateRecordDoubleClick(row, col);
        if (rv == null) rv = true;
        return rv;
    }
    return null;
},

// Scrolling
// --------------------------------------------------------------------------------------------

//> @attr listGrid.scrollToCellXPosition (Alignment : "center" : IRW)
// When scrollToCell is called, this is used as defaults if xPosition weren't explicitly passed
// into the method.
// @visibility external
//<
scrollToCellXPosition: "center",

//> @attr listGrid.scrollToCellYPosition (VerticalAlignment : "center" : IRW)
// When scrollToCell is called, this is used as defaults if yPosition weren't explicitly passed
// into the method.
// @visibility external
//<
scrollToCellYPosition: "center",

//>    @method    listGrid.scrollRecordToTop()    (A)
// Scroll the listGrid body such that the specified row is visible at the top of the viewport.
//        @group    scrolling
//        @param    rowNum  (number)    Index of the row to scroll into view
//<
scrollRecordToTop : function (rowNum) { return this.scrollRecordIntoView(rowNum, "top"); },

//>    @method    listGrid.scrollRecordIntoView()    (A)
// Scroll the listGrid body such that the specified row is visible close to the
// center of the viewport.
//        @group    scrolling
//        @param    rowNum  (number)    Index of the row to scroll into view
//      @param  [yPosition] (VerticalAlignment) Vertical position of scrolled row (optional)
//<
scrollRecordIntoView : function (rowNum, yPosition) {
    return this.scrollToCell(rowNum, null, null, yPosition);
},

//>    @method    listGrid.scrollToColumn()
// Scroll the grid to specified column such that the row appears near the center of the
// viewport.
// <P>
// See +link{listGrid.scrollToCell()} for a full description of how
// this method interacts with incremental loading and rendering of data.
// @group    scrolling
// @param    colNum  (number)    Index of the column to scroll into view
// @param   [xPosition] (Alignment) Horizontal position of scrolled column (optional)
// @visibility external
//<
scrollToColumn : function (colNum, xPosition) {
    return this.scrollToCell(null, colNum, xPosition);
},

//> @method listGrid.scrollToRow()
// Scroll the grid to specified row such that the row appears near the center of the
// viewport, loading data if necessary.
// <P>
// See +link{listGrid.scrollToCell()} for a full description of how
// this method interacts with incremental loading and rendering of data.
//
// @group scrolling
// @param    rowNum  (number)    Row index of the cell to scroll into view
// @param   [yPosition] (VerticalAlignment) Vertical position of scrolled row (optional)
// @visibility external
//<
scrollToRow : function (rowNum, yPosition) {
    this.scrollToCell(rowNum, 0, null, yPosition);
    // allow for chaining other function calls after scrollToRow()
    return this;
},

// helper callback to weed out calls to scrollToRow that should be ignored.
// we only want to perform the last call to scrollToRow() that occurs before data arrives.
scrollCellCallback : function (rowNum, colNum, xPosition, yPosition, alwaysScroll, stamp) {
    if (stamp == this._currentScrollCall) {
        // reset counter
        this._currentScrollCall = null;
        this.scrollToCell(rowNum, colNum, xPosition, yPosition, alwaysScroll);
    }
},

//>    @method    listGrid.scrollToCell()    (A)
// Will scroll the listGrid body such that the specified cell is visible close to the
// centre of the viewport.
// <P>
// This method has no effect if the cell is already visible in the viewport.
// <P>
// When scrolling vertically, this will cause data to be automatically loaded
// if +link{dataFetchMode,paging is active} and you scroll into an area of
// the data that isn't loaded.  Only rows around the target row will be
// loaded, not all intervening rows.  See also +link{ResultSet}.
// <P>
// Scrolling into an undrawn area will cause the body area of the grid to
// redraw, but this won't happen synchronously unless you explicitly call
// redraw().  Scrolling into an area of the data that is not yet loaded
// will never synchronously draw new rows, even if you call redraw() -
// wait for +link{dataArrived} to be notified when new rows have been
// loaded.
// <P>
// Calling this method with a row index larger than the
// current dataset will clamp to the end of the dataset (similarly horizontal
// scrolling will clamp to the last column).
// <P>
// If a call to this method is made while data is still loading, such
// that the last row of the dataset is not yet known the grid will attempt to compensate
// by scrolling the record into view when data arrives, if it is valid.
// For better control over scrolling, developers should consider
// calling <code>scrollToRow()</code> or <code>scrollToCell</code>
// from +link{dataArrived()} if data is still loading.
// <P>
// With mixed-height rows it will only reliably work if virtualScrolling
// is enabled.
//
// @group    scrolling
// @param    rowNum  (int)    Row index of the cell to scroll into view
// @param    colNum  (int)    Column index of the cell to scroll into view
// @param   [xPosition] (Alignment) Horizontal position of scrolled cell (optional)
// @param   [yPosition] (VerticalAlignment) Vertical position of scrolled cell (optional)
// @visibility external
//<
scrollToCell : function(rowNum, colNum, xPosition, yPosition, alwaysScroll) {
    return this.scrollCellIntoView(rowNum, colNum, xPosition, yPosition, alwaysScroll);
},

// alwaysScroll: scroll even if the cell is already in view (position it
//               according to "xPosition" and "yPosition" parameters)
scrollCellIntoView : function (rowNum, colNum, xPosition, yPosition, alwaysScroll) {
    // method arguments has been changed from (rowNum, colNum, center, alwaysCenter) so if
    // we could determine old arguments we should log warning message and convert passed values
    // to new arguments
    if (isc.isA.Boolean(xPosition) && (typeof alwaysScroll == 'undefined')) {
        isc.logWarn("Center argument had been passed to a scroll method(scrollToCell, " +
            "scrollCellIntoView, scrollToColumn or scrollRecordIntoView). This is deprecated. " +
            "See documentation for detailed information about passed arguments.");
        alwaysScroll = yPosition;
        yPosition = xPosition? "center": "top";
        xPosition = xPosition? "center": "left";
    }

    if ((isc.isAn.Array(this.data) && this.data.length == 0 && this.dataSource)
        || (isc.ResultSet && isc.isA.ResultSet(this.data) && !this.data.lengthIsKnown())) {
        // keep track of conscective calls to scrollToRow() so we can only perform the most
        // recent one, before data arrives.
        if (!this._currentScrollCall) this._currentScrollCall = 1;
        else this._currentScrollCall += 1;
        var stamp = this._currentScrollCall;
        isc.Page.waitFor(this, "dataArrived",
            {method: this.scrollCellCallback, args: [rowNum, colNum, xPosition, yPosition, alwaysScroll, stamp],
            target:this}
        );

        return;
    }

    // if the body isn't drawn, we can't scroll the cell into view - set a flag to scroll the
    // body when it gets drawn
    if (!this.body || !this.body.isDrawn()) {
        this.logInfo("scrollCellIntoView() called before the body has been drawn.  Cell " +
                     rowNum + "," + colNum + " will scrolled into view on draw().");
        this._scrollCell = [rowNum, colNum, xPosition, yPosition];
        return;

    }

    // Force an immediate adjustOverflow before we calculate row / viewport size etc

    if (this.body._deferredOverflow || this.body._overflowQueued) {
        this.body.adjustOverflow("ScrollCellIntoView requesting size");
    }

    // Use default values scrollToCellXPosition and scrollToCellYPosition if null or incorrect
    // value has been passed
    if (xPosition != "left" && xPosition != "center" && xPosition != "right") {
        xPosition = this.scrollToCellXPosition;
    }
    if (yPosition != "top" && yPosition != "center" && yPosition != "bottom") {
        yPosition = this.scrollToCellYPosition;
    }

    var x, y, width, height, body = this.body;
    if (rowNum != null) {
        if (!body._isVirtualScrolling) {
            y = body.getRowTop(rowNum);
            height = body.getRowSize(rowNum);
        } else {
            // If the row is already in the viewport, don't call scrollToTargetRow()
            // as this will shift it about which can be confusing if the user is
            // navigating through rows with the keyboard.
            var undrawn = body._firstDrawnRow == null || rowNum < body._firstDrawnRow ||
                rowNum > body._lastDrawnRow,
                inViewport = !undrawn;
            if (inViewport) {
                var rowTop = body.getRowTop(rowNum),
                    rowHeight = body.getRowHeight ?
                                    body.getRowHeight(this.getCellRecord(rowNum), rowNum, 0) :
                                    body.cellHeight,
                    scrollTop = body.getScrollTop();
                if (scrollTop > rowTop ||
                    ((body.getViewportHeight() + scrollTop) < (rowTop + rowHeight)))
                {
                    inViewport = false;
                }
            }
            if (!inViewport) {
                // scrolling to a particular coordinate would be meaningless with unknown row
                // heights
                body._targetRow = rowNum;
                if (yPosition == "center") {
                    var rowHeight = body.getRowHeight ?
                            body.getRowHeight(this.getCellRecord(rowNum), rowNum, 0) :
                            body.cellHeight;
                    body._rowOffset = -1 * (body.getViewportHeight() - rowHeight) / 2;
                } else if (yPosition == "bottom") {
                    var rowHeight = body.getRowHeight ?
                            body.getRowHeight(this.getCellRecord(rowNum), rowNum, 0) :
                            body.cellHeight;
                    body._rowOffset = rowHeight - body.getViewportHeight();
                } else {
                    body._rowOffset = 0;
                }

                if (colNum != null) {
                    var cellOffset = null;
                    if (xPosition == "left") {
                        cellOffset = this.getColumnLeft(colNum);
                    } else if (xPosition == "right") {
                        cellOffset = this.getColumnLeft(colNum) +
                            this.getColumnWidth(colNum) - body.getViewportWidth();
                    } else {
                        cellOffset = this.getColumnLeft(colNum) +
                            (this.getColumnWidth(colNum) - body.getViewportWidth()) / 2;

                    }
                    // Scroll horizontally.
                    // no "virtual scrolling" on the h-axis so this is just a simple number.
                    // The scrollToTargetRow call below will handle scrolling vertically
                    body.scrollTo(cellOffset);

                }
                body._scrollToTargetRow();
            }
            return;
        }
    }
    if (colNum != null) {
        if (this.frozenFields != null && colNum < this.frozenFields.length) {
            colNum = null;
        } else {
            x = this.getColumnLeft(colNum);
            width = this.getColumnWidth(colNum);
        }
    }

    // Catch the case where we're dirty, and the row being scrolled into view is not yet present
    // in our HTML (so we can't scroll into view until the redraw occurs)
    if (this.isDirty() || this.body.isDirty()) {
        var mustRedraw;
        if (rowNum != null) {
            var scrollHeight = body.getScrollHeight();
            if (y+height > scrollHeight) mustRedraw = true;
        }
        if (!mustRedraw && colNum != null) {
            var scrollWidth = body.getScrollWidth();
            if (x+width > scrollWidth) mustRedraw = true;
        }
        if (mustRedraw) {
            // stash target edit cell so the correct row can be edited
            // after redraw
            if (this.alwaysShowEditors && !this._editorShowing) {
                this._editCellAfterRedraw = [rowNum, colNum];
            }
            this.redraw("scrollIntoView");
        }
    }

    //this.logWarn("ScrollIntoView passed: " + [rowNum, colNum] +
    //             ", calculated target cell position:" + [x,y] + ", size:" + [width,height]);

    body.scrollIntoView(x,y,width,height, xPosition, yPosition,
                        null, null, alwaysScroll)

},

// Header/Body Scroll Sync
// --------------------------------------------------------------------------------------------
// Note - we keep the body / header's horizontal scroll position in sync by firing
// syncHeaderScrolling when the body is scrolled, and syncBodyScrolling where the header is
// scrolled.
// We have to have these no-op if the header / body are already at the same place to avoid an
// infinite loop.
bodyScrolled : function (left, top, isFrozen) {

    if (left == null) left = isFrozen ? this.frozenBody.getScrollLeft() : this.body.getScrollLeft();

    // Assertion
    // the frozen body should never be clipping horizontally so if this was a
    // scroll from the frozen body, just sync up the vertical scroll position of the
    // unfrozen body and we're done.
    // NOTE: There's no obvious way for the user to scroll just the frozen body but this
    // could probably happen from interactions like keyboard events


    if (isFrozen) {
        this.body._noScrollObservation = true;
        var frozenBody = this.frozenBody;
        if (frozenBody._literalScroll) {

            this.body._targetRow = frozenBody._targetRow;
            this.body._rowOffset = frozenBody._rowOffset;
            this.body._scrollRatio = frozenBody._scrollRatio;
            this.body._scrollToTargetRow();
        } else {
            this.body.scrollTo(null, top, "scrollSync");
        }
        delete this.body._noScrollObservation

        return;
    }


    if (this.frozenBody != null) {
        this.frozenBody._noScrollObservation = true;
        var body = this.body,
            frozenBody = this.frozenBody;

        // virtual scrolling: The frozen body's virtual scrolling logic
        // (draw area etc) is all driven of the unfrozen body since that can detect
        // cases like "quick drag scrolling" of the scrollbars.
        // If the body is currently doing a "scrollToTargetRow", pick up the
        // stored targetRow info from the body and scrollToTargetRow ourselves

        if (body._literalScroll) {

            frozenBody._targetRow = body._targetRow;
            frozenBody._rowOffset = body._rowOffset;
            frozenBody._scrollRatio = body._scrollRatio;
            frozenBody._scrollToTargetRow();
        } else {
            frozenBody.scrollTo(null, top, "bodyScrollSync");
        }
        delete this.frozenBody._noScrollObservation;
    }

    // Don't attempt to sync scrolling while drag-resizing.

    if (!this._dragResizingField) {
        this.syncHeaderScrolling(left, top);
        this.syncFilterEditorScrolling(left, top);
        this.syncSummaryRowScrolling(left,top);
    }

    // If we took focus from the edit form as part of a redraw and haven't restored it yet
    // restore it now

    if (this._editorShowing && this._editorSelection) {
        var editForm = this.getEditForm(),
            editRow = this.getEditRow(),
            editColNum = this.getEditCol(),
            editItem = editForm.getItem(this.getEditorName(editRow, editColNum));

        if (editItem) {
            if (!editItem.hasFocus &&
                (editForm.hasFocus || isc.EH.getFocusCanvas() == null))
            {
                this._restoreFocusAfterRedraw(editColNum);
            } else {
                delete this._editorSelection;
            }
        }
    }

},


syncHeaderScrolling : function (left, top) {
    if (left != null && this.header) {
        if (!this.isRTL()) {
            if (left != this.header.getScrollLeft()) {
                this.header.scrollTo(left, null, "headerScrollSync");
            }
        } else {

            var header = this.header,
                body = this.body,
                headerMaxScroll = header.getScrollWidth() - header.getViewportWidth(),
                headerScrollPos = headerMaxScroll - header.getScrollLeft(),
                bodyMaxScroll = body.getScrollWidth() - body.getViewportWidth(),
                bodyScrollPos = bodyMaxScroll - left;
            /*
            this.logWarn("scroll sync: body new left: " + left +
                         ", body max: " + bodyMaxScroll +
                         ", body pos: " + bodyScrollPos +
                         ", header current left: " + header.getScrollLeft() +
                         ", header max: " + headerMaxScroll +
                         ", header pos: " + headerScrollPos +
                         ", will scroll header to: " + (headerMaxScroll - bodyScrollPos));
            */

            if (bodyScrollPos != headerScrollPos) {
                header.scrollTo(headerMaxScroll - bodyScrollPos, null, "scrollSync");
            }
        }
    }
},

// when the header is scrolled, keep the body scrolled in synch with it!
headerScrolled : function () {

    if (!this._delayingBodyScrolling) {
        this._delayingBodyScrolling = this.delayCall("syncBodyScrolling");
    }
},


syncBodyScrolling : function () {
    delete this._delayingBodyScrolling;
    var left = this.header.getScrollLeft();
    if (this.body) {
        if (!this.isRTL()) {
            if (left != this.body.getScrollLeft()) this.body.scrollTo(left, null, "scrollSync");

        } else {

            var header = this.header,
                body = this.body,
                headerMaxScroll = header.getScrollWidth() - header.getViewportWidth(),
                headerScrollPos = headerMaxScroll - header.getScrollLeft(),
                bodyMaxScroll = body.getScrollWidth() - body.getViewportWidth(),
                bodyScrollPos = bodyMaxScroll - left;

            if (bodyScrollPos != headerScrollPos) {
                body.scrollTo(Math.max(0, bodyMaxScroll - headerScrollPos), null, "scrollSync");
            }
        }
    }
},

// if we are showing a filter editor we must keep that horizontally scrolled to the same
// position as the body
syncFilterEditorScrolling : function (left, top) {
    if (left == null) return;
    if (this.filterEditor != null && this.filterEditor.body != null) {
        // RTL mode - account for the fact that scrolled to zero (IE hard left) on the main grid
        // body != scrolled to zero (hard left) on the filter editor since their left coords don't
        // align.
        if (this.isRTL()) {
            var offset = this.body.getViewportWidth() - this.filterEditor.body.getViewportWidth()
            left += offset;
        }

        // No op if they are already in synch to avoid an infinite loop
        if (this.filterEditor.body.getScrollLeft() != left)
        {
            this.filterEditor.body.scrollTo(left, null, "scrollSync");
        }
    }
},

syncSummaryRowScrolling : function (left,top) {
    if (left == null) return;
    if (this.summaryRow != null && this.showGridSummary && this.summaryRow.body != null &&
        this.summaryRow.body.getScrollLeft() != left)
    {
        this.summaryRow.body.scrollTo(left, null, "scrollSync");
    }
},

// RollOver
// --------------------------------------------------------------------------------------------


_hiliteRecord : function (recordNum) {

    if (!isc.isA.Number(recordNum)) {
        recordNum = this.getRecordIndex(recordNum);
    }

    // Make sure we're not trying to select a record beyond the ends of the list.
    if (!isc.isA.Number(recordNum) || recordNum < 0) recordNum = 0;

    // clamp to getTotalRows(), not data.getLength() - caters for unsaved rows
    if (recordNum >= this.getTotalRows()) {
        recordNum = this.getTotalRows() - 1;
    }

    this.clearLastHilite();

    // note the row number hilighted by keyboard navigation
    this.body._lastHiliteRow = recordNum;

    // set this.body.lastOverRow, so the recordStyle will be updated to the mouseOver style

    this.body.lastOverRow = recordNum;
    this.body.lastOverCol = 0;  // required to make the GR believe the mouse was over a real cell

    // no need to calculate the style - setRowStyle will achieve that
    this.bodies.map("setRowStyle", recordNum);

},

//>    @method    listGrid.clearLastHilite()    (A)
// Unhilites the last hilited item.
//        @group    events, hiliting
//<
clearLastHilite : function (frozen) {
    var body = frozen ? this.frozenBody : this.body;
    if (!body) return;

    // the obvious merge of the two cases of this.canSelectCells doesn't work because
    // body.lastOverCol can be != null for the case of !this.canSelectCells
    var rowToClear = body.lastOverRow,
        colToClear;

    if (rowToClear < 0) return;

    // clear the pointer to the last row hilited via keyboard navigation
    var hasKeyboardHilites = false;
    if (!this.canSelectCells) {
        hasKeyboardHilites = (body._lastHiliteRow != null);
        body._lastHiliteRow = null;
        if (rowToClear == null) return;
    } else {
        // make sure to clear frozen body hilites
        hasKeyboardHilites = (this._lastKeyboardHiliteRow != null && this._lastKeyboardHiliteCol != null);
        if (!frozen) this.clearLastHilite(true);
        this._lastKeyboardHiliteRow = null;
        this._lastKeyboardHiliteCol = null;
        this._lastKeyboardHiliteBody = null;
        colToClear = body.lastOverCol;
        if (rowToClear == null || colToClear == null) return;
        delete body.lastOverCol;
    }
    delete body.lastOverRow;

    // no need to calculate new styleName here - let setRowStyle determine that
    if (this.showRollOver || hasKeyboardHilites) body.updateRollOver(rowToClear, colToClear);
},

// Note that we basically use the body like a focusProxy - when focus() is called, focus
// will go to the body.
// o Set _useFocusProxy to false - we don't want the grid to ever have native focus
// o Set _useNativeTabIndex to false - this is done AFTER creating the body so the body can
//   pick up the '_useNativeTabIndex' as explicitly specified on the ListGrid before that
//   property gets overridden.
_useFocusProxy:false,

// Override setAccessKey to set the accessKey on the body rather than on the listGrid
setAccessKey : function (accessKey) {
    // call Super - will remember this.accessKey, (though it won't actually set it on the LV handle)
    this.Super("setAccessKey", arguments)
    if (this.body != null) this.body.setAccessKey(accessKey);
},

// Override setFocus to focus on the body rather than the ListGrid
setFocus : function (newfocus) {
    if (this.body != null) {
        this.body.setFocus(newfocus);
    }
},

// If "focusAtEnd" is called, always just pass focus to the "next tab element".
// The grid is marked as canFocus:true, but doesn't actually ever takes focus: Always
// delegates to its children.
// The grid's tab-index will precede that of its children.
// When synthetically tabbing through widgets, this method can be called:
// When the user is tabbing forward and encounters the grid
// - just put focus into the next tab widget (should be the appropriate header toolbar or
//   body)
// When the user is tabbing backwards and has already gone through the grid members
// - just put focus into the prev-tab-widget (will be whatever precedes the LG as a whole
//   in the page's tab order)
focusAtEnd : function (start) {
    this._focusInNextTabElement(start);
},

// Override _canFocus() - we are focusable if the body is focusable
// Note that the body already picks up the 'canFocus' attribute from the ListGrid, if specified
// _canFocus is also overridden in gridBody to disable focus when empty, if appropriate
_canFocus : function () {
    if (this.body) return this.body._canFocus();
    return false;
},

//> @attr listGrid.canFocusInEmptyGrid (boolean : true : IRA)
// If the listGrid is empty, should the user be able to put focus into the grid body by tabbing
// to it?
// <P>
// Note that if +link{editOnFocus} is true for this grid and +link{listEndEditAction} is set to next,
// having this property set to true will allow users to automatically create a new edit row by
// simply tabbing into the grid.
//
// @visibility external
//<
canFocusInEmptyGrid:true,

// disable the focus indicator in Chrome/Safari since it's clipped at the bottom/right and looks
// odd
showFocusOutline:!isc.Browser.isSafari,

// Body Clicks
// --------------------------------------------------------------------------------------------

//>    @method    listGrid.recordClick()
// Executed when the listGrid receives a 'click' event on an enabled, non-separator
// record. The default implementation does nothing -- override to perform some action
// when any record or field is clicked.<br>
// A record event handler can be specified either as
// a function to execute, or as a string of script to evaluate. If the handler is defined
// as a string of script, all the parameters below will be available as variables for use
// in the script.<br>
// To do something specific if a particular field is clicked, add a recordClick
// method or string of script to that field (same parameters) when you're setting up
// the list.<br>
// <b>Notes:</b><ul>
// <li>This will not be called if the click is below the last item of the list.</li>
// <li>This method is called from the default implementation of
// +link{method:listGrid.rowClick}, so if that method is overridden
// this method may not be fired.</li></ul>
//
// @param viewer (ListGrid) the listGrid that contains the click event
// @param record (ListGridRecord) the record that was clicked on
// @param recordNum (number) number of the record clicked on in the current set of
//                                  displayed records (starts with 0)
// @param field (ListGridField) the field that was clicked on (field definition)
// @param fieldNum (number) number of the field clicked on in the listGrid.fields
//                                  array
// @param value (any) value of the cell (after valueMap, etc. applied)
// @param rawValue (any) raw value of the cell (before valueMap, etc applied)
// @return (boolean) return false to cancel default behavior
//
// @see rowClick()
// @group events
// @visibility external
//<
// NOTE: params not needed for default no-op implementation
recordClick : function () {
    return true;
},

//>    @method    listGrid.onRecordClick()
// Executed when the listGrid receives a 'click' event on an enabled, non-separator
// record. The default implementation does nothing -- override to perform some action
// when any record or field is clicked.<br>
// A record event handler can be specified either as
// a function to execute, or as a string of script to evaluate. If the handler is defined
// as a string of script, all the parameters below will be available as variables for use
// in the script.<br>
// To do something specific if a particular field is clicked, add a recordClick
// method or string of script to that field (same parameters) when you're setting up
// the list.<br>
// <b>Notes:</b><ul>
// <li>This will not be called if the click is below the last item of the list.</li>
// <li>This method is called from the default implementation of
// +link{method:listGrid.rowClick}, so if that method is overridden
// this method may not be fired.</li></ul>
//
// @param viewer (ListGrid) the listGrid that contains the click event
// @param record (ListGridRecord) the record that was clicked on
// @param recordNum    (number) number of the record clicked on in the current set of
//                                  displayed records (starts with 0)
// @param field (ListGridField) the field that was clicked on (field definition)
// @param fieldNum (number) number of the field clicked on in the listGrid.fields
//                                  array
// @param value (object) value of the cell (after valueMap, etc. applied)
// @param rawValue (object)    raw value of the cell (before valueMap, etc applied)
// @return (boolean) return false to cancel default behavior
//
// @see rowClick()
//
// @group events
// @visibility sgwt
//<
onRecordClick : function (viewer, record, recordNum, field, fieldNum, value, rawValue) {
    return true;
},

//>    @method    listGrid.recordDoubleClick()
// Executed when the listGrid receives a 'doubleClick' event on an enabled, non-separator
// record. The default implementation does nothing -- override to perform
// some action when any record or field is double clicked.<br>
// A record event handler can be specified either as a function to execute, or as a string
// of script to evaluate. If the handler is defined as a string of script, all the
// parameters below will be available as variables for use in the script.<br>
// To do something specific if a particular field is double clicked, add a
// recordDoubleClick method or string of script to that field (same parameters) when you're
// setting up the list.<br>
// <b>Notes:</b><ul>
// <li>This will not be called if the click is below the last item of the list.</li>
// <li>This method is called from the default implementation of +link{method:listGrid.rowDoubleClick},
// so if that method is overridden this method may not be fired.</li></ul>
//        @group    events
//
// @param    viewer        (listGrid)    the listGrid that contains the doubleclick event
// @param    record        (ListGridRecord)    the record that was double-clicked
// @param    recordNum    (number)    number of the record clicked on in the current set of
//                                  displayed records (starts with 0)
// @param    field        (ListGridField)    the field that was clicked on (field definition)
// @param    fieldNum    (number)    number of the field clicked on in the listGrid.fields
//                                  array
// @param    value       (object)    value of the cell (after valueMap, etc. applied)
// @param    rawValue    (object)    raw value of the cell (before valueMap, etc applied)
// @return (boolean)    return false to cancel event bubbling
//
// @see    rowDoubleClick()
//
// @visibility external
//<
// NOTE: params not needed for default no-op implementation
recordDoubleClick : function () {},

// --------------------------------------------------------------------------------------------
// Summary row
// --------------------------------------------------------------------------------------------

// If grid.showGridSummary is true, generate a 'summaryRow' auto child to show summaries at
// the bottom of the list grid.
// Default behavior is to show totals for numeric fields

//> @attr listGrid.showGridSummary (Boolean : false : IRW)
// Should this ListGrid show a summary row beneath the last record of the grid. This summary
// row will contain per-field summary information. See +link{listGridField.showGridSummary} and
// +link{listGrid.getGridSummaryFunction()} for details on how the summary value to be displayed
// for each column will be calculated.
// <P>
// Note that the +link{listGrid.summaryRow,summaryRow autoChild} will be created to actually
// display the summary row.
// @visibility external
//<
showGridSummary:false,

//> @attr listGrid.invalidSummaryValue (string : "&nbsp;" : IRWA)
// Value to display to the user if showing summary values (through +link{listGrid.showGridSummary},
// +link{listGrid.showGroupSummary} or +link{listGridFieldType,listGridFieldType:"summary"}), and
// the summary function returns <code>"null"</code> (implying it was unable to calculate a
// valid summary value). This property will only be used in the default formatting behavior. If
// an explicit formatter has been specified - via +link{listGrid.formatCellValue} or
// +link{listGridField.formatGridSummary(),formatGridSummary()}, for example - this property has
// no effect.
// @visibility external
//<
invalidSummaryValue:"&nbsp;",

//> @attr listGrid.includeInSummaryProperty (string : "includeInSummary" : IRW)
// Property name on a record that will be checked to determine whether a record should
// be included when calculating totals for the +link{listGrid.showGridSummary,grid summary}.
// @visibility external
//<
includeInSummaryProperty:"includeInSummary",

//> @attr listGridRecord.includeInSummary (boolean : null : IRW)
// If specified as false this record should be ignored when calculating summary totals
// to be shown in the +link{listGrid.showGridSummary,summary row} for this grid.
// <P>
// Note that <code>includeInSummary</code> is the default property name for this attribute,
// but it may be modified via +link{listGrid.includeInSummaryProperty}.
// @visibility external
//<


//> @attr listGrid.gridSummaryRecordProperty (string : "isGridSummary" : IRW)
// If +link{listGrid.showGridSummary} is true, this attribute will be set to true on the
// record object representing the grid summary row.
// @visibility external
//<
gridSummaryRecordProperty:"isGridSummary",

//> @attr listGridRecord.isGridSummary (Boolean : null : IRW)
// This attribute will automatically be set to true for the record representing the
// grid-level summary row shown if +link{listGrid.showGridSummary} is true.
// <P>
// Note that <code>isGridSummary</code> is the default property name for this attribute but
// it may be modified by setting +link{listGrid.gridSummaryRecordProperty}
// @visibility external
//<

//> @attr listGrid.groupSummaryRecordProperty (string : "isGroupSummary" : IRW)
// If +link{listGrid.showGroupSummary} is true, this attribute will be set to true on each
// record object representing a group-level summary row.
// @visibility external
//<
groupSummaryRecordProperty:"isGroupSummary",

//> @attr listGridRecord.isGroupSummary (Boolean : null : IRW)
// This attribute will automatically be set to true for records representing
// group-level summary rows shown if +link{listGrid.showGroupSummary} is true.
// <P>
// Note that <code>isGroupSummary</code> is the default property name for this attribute but
// it may be modified by setting +link{listGrid.groupSummaryRecordProperty}
// @visibility external
//<

// recordApplyPluralTitleProperty
// This property may be set to an array of field-names on a record.
// If set, for each field listed, default formatting logic will apply a space, plus the
// plural title for the field (if present, otherwise the standard title).
//
// Has no effect if there is a custom formatter at the grid or field level.
//
// Used to allow us to append the plural title to field values derived from the
// "count" summary function.

recordApplyPluralTitleProperty: "applyPluralTitle",

//> @method listGrid.setShowGridSummary()
// Setter for the +link{listGrid.showGridSummary} attribute
// @param showGridSummary (boolean) new value for this.showGridSummary
// @visibility external
//<
setShowGridSummary : function (showGridSummary) {
    if (this.showGridSummary == showGridSummary) return;
    this.showGridSummary = showGridSummary;
    if (this.showGridSummary) {
        this.showSummaryRow();
    } else {
        this.clearSummaryRow();
    }
},

_getFieldDependencyTable : function () {

    if (this._fieldDependencyTable == null) {
        this.Super("_getFieldDependencyTable");

        var fields = this.fields,
            dependencyTable = this._fieldDependencyTable,
            summaryFields = fields.filter(function (field) { return field.type == "summary";});

        for (var i = 0; i < fields.length; i++) {
            var field = fields[i],
                include = field.includeInRecordSummary;
            if (include == true || include != false &&
                (field.type == "integer" || field.type == "float"))
            {
                if (dependencyTable[field.name] == null) {
                    dependencyTable[field.name] = {};
                }
                var localSummaryFields = field.includeInRecordSummaryFields || summaryFields;
                for (var j = 0; j < localSummaryFields.length; j++) {
                    var summaryField = localSummaryFields[j];
                    if (isc.isA.String(summaryField)) {
                        summaryField = this.getField(summaryField);
                    }
                    dependencyTable[field.name][summaryField.name] = summaryField;
                }
            }
        }
    }
    return this._fieldDependencyTable;
},

//> @method listGrid.recalculateSummaries()
// Recalculates values for fields with
// +link{listGridField.recordSummaryFunction, summary-functions} or
// +link{listGridField.userFormula, user formulae} defined and for values
// displayed in the +link{listGrid.showGridSummary,grid summary} and
// +link{listGrid.showGroupSummary,group summary rows}.
// @param [records] (Array of Record) Optional array of records to recalculate summaries for,
//                  or null for all records
// @param [fields] (Array of ListGridField) Optional array of fields to recalculate summaries
//                 for, or null for all fields
// <P>
// Note that the records should be from +link{listGrid.data}; thus, if the grid is grouped,
//  the records should be from the grouped data rather than +link{listGrid.originalData}.
// @visibility external
//<
recalculateSummaries : function (records, fields, suppressRedraw) {
    suppressRedraw = suppressRedraw != null ? suppressRedraw : true;
    this.calculateRecordSummaries(records, fields, true, true, suppressRedraw);
},

_recalculateSummaries : function (records, fields, calculateGroupRows, calculateSummaryRows,
                                  redrawSummaryRow)
{
    if (this.isGrouped          && this.showGroupSummary  && calculateGroupRows != false) {
        this.refreshGroupSummary(records, fields);
    }
    if (this.summaryRow != null && this.showGridSummary && calculateSummaryRows != false) {
        if (redrawSummaryRow && !this.isDirty()) this.markForRedraw();
        this.summaryRow._recalculateSummaries(fields);
    }
},

//> @method listGrid.recalculateGridSummary()
// Refresh the +link{listGrid.showGridSummary,grid summary}, by either re-calculating from
// already-loaded data or doing a new fetch from the +link{summaryRowDataSource}.
// <P>
// Note unlike +link{listGrid.recalculateSummaries()}, this method will not force a refresh of
// field-level summaries (see +link{listGridField.recordSummaryFunction}) or group level
// summaries (see +link{listGrid.showGroupSummary}).
// @visibility external
//<
recalculateGridSummary : function () {
    if (this.showGridSummary && this.summaryRow != null) {
        this.summaryRow._recalculateSummaries();
    }
},

// shouldShowGridSummary() - determines whether a field should show a grid summary
// If field.showGridSummary is specified, respect it, otherwise check for
// the presence of a field level summaryFunction, or a default summaryFunction for the field type
// NOTE: If a developer overrides getGridSummaryData(), this method does not reflect desired
// behavior - we actually want to show the value for all fields for which there are values,
// unless "showGridSummary" was explicitly set to false at the field level.
// (We don't want to check for the presence of a gridSummaryFunction for the field).

shouldShowGridSummary : function (field) {
    if (field.showGridSummary != null) return field.showGridSummary;
    return (field.getGridSummary != null || this.getGridSummaryFunction(field) != null)
},

//> @method listGrid.getGridSummaryFunction() [A]
// Determines the +link{type:SummaryFunction} to use when calculating per-field summary values
// describing multiple records in this grid.
// Used to determine the summary function to use for both +link{listGrid.showGridSummary} and
// +link{listGrid.showGroupSummary}.
// <P>
// Default implementation picks up +link{listGridField.summaryFunction} if explicitly specified,
// otherwise checks for a default summary function based on field type (see
// +link{SimpleType.setDefaultSummaryFunction()})
// @param field (listGridField) field to check for summary function
// @return (SummaryFunction) summary function for the field in question
// @visibility external
//<

getGridSummaryFunction : function (field) {
    if (!field) return;
    // If an explicit summaryFunction is specified, pick it up.
    // Otherwise pick up the default based on the field type.
    var summaryFunction = field.summaryFunction ||
                           isc.SimpleType.getDefaultSummaryFunction(field.type);

    return summaryFunction;
},

//> @method listGrid.getGridSummary() [A]
// When +link{listGrid.showGridSummary} is <code>true</code> this method is called for each field
// which will show a grid summary value (as described in +link{ListGridField.showGridSummary})
// to get the summary value to display below the relevant column.
// <P>
// The default implementation is as follows:
// <ul><li>If this is a databound grid and not all data is loaded, returns null for every field</li>
//     <li>Otherwise if +link{ListGridField.getGridSummary()} is defined, calls that method passing
//         in the current data set for the grid</li>
//     <li>If +link{ListGridField.getGridSummary()} is undefined, makes use of the
//          +link{listGrid.getGridSummaryFunction(),standard summary function} for the field to
//          calculate the summary based on the current data set</li>
// </ul>
// This method may return an array of values. This implies that the grid summary should show
// multiple rows. Note that if a field has more than one summaryFunction specified, this method
// will pick up values from each summary function and return them in an array, meaning
// these summaries will show up on multiple rows in the grid.
// <P>
// This method may be overridden to completely customize the summary value displayed for
// columns in this grid. An example use case would be when summary information is available on
// the client and does not need to be calculated directly from the data.
// <P>
// <b>Note:</b> this method will not be called if +link{listGrid.summaryRowDataSource} is
// specified.
//
// @param field (ListGridField) field for which the summary value should be returned
// @return (any) summary value to display for the specified field.
// @visibility external
//<
getGridSummary : function (field) {

    if (!field || !this.data || (isc.isA.ResultSet(this.data) && !this.data.lengthIsKnown()))
        return;
    var data = this.getOriginalData(),
        isRS = isc.ResultSet && isc.isA.ResultSet(data),
        isTree = isc.isA.Tree(data);

    if (isTree) {
        data = data.getDescendants(data.getRoot());
        isRS = isc.ResultSet && isc.isA.ResultSet(data);
    }

    if (isRS && !data.allMatchingRowsCached()) {
        this.logWarn("Unable to show summary values - dataset not completely loaded");
        return;
    }

    var localData = isRS ? data.getRange(0, data.getLength()) : data;

    // If we have outstanding / unsaved edits, we want to use those in our calculations
    var editRows = this.getAllEditRows();
    if (editRows != null && editRows.length > 0) {
        // ensure we don't stomp on our live data array!
        localData = localData.duplicate();

        for (var i = 0; i < localData.length; i++) {
            var record = localData[i];
            var rowNum = this.getEditSessionRowNum(record);
            if (rowNum != null) localData[i] = this.getEditedRecord(rowNum, null, true);
        }
    }

    // getGridSummary() is an override point on a listGridField which allows the developer to
    // calculate a grid summary based on the group summaries already calculated for the grid.
    // It takes an additional param -- an array of 'groupSummary' objects.
    // groupSummary contains
    // - groupName and groupValue to identify which group it belongs to
    // - for each field in the grid, the summary value for the records in the group.

    if (field.getGridSummary) {
        var groupSummaries;
        if (this.isGrouped && this.showGroupSummary && (this.groupTree != null)) {
            groupSummaries = this.assembleGroupSummaries();
        }
        return field.getGridSummary(localData, field, groupSummaries);
    }

    return this.getSummaryValue(localData, field);
},

//> @attr listGrid.summaryRowDataSource (DataSource : null : IRA)
// If +link{listGrid.showGridSummary} is true, by default summary values are calculated on the
// client based on the current data-set for the grid (see +link{ListGrid.getGridSummary()} and
// +link{ListGrid.getGridSummaryFunction()}).
// <P>
// In some cases however it may make sense to calculate summary values on the server and retrieve
// them via a dataSource fetch. If set, this property specifies a dataSource to fetch against for
// the summary row. The dataSource should return a single record with summary data for each
// field for which summary data should be shown. Note that specifying this property completely
// bypasses the standard client-side grid summary calculation logic.
// <P>
// The fetch may be further customized via +link{listGrid.summaryRowCriteria} and
// +link{listGrid.summaryRowFetchRequestProperties}
// @visibility external
//<
// summaryRowDataSource:null,
getSummaryRowDataSource : function () {
    return this.summaryRowDataSource;
},

//> @attr listGrid.summaryRowCriteria (Criteria : null : IRWA)
// If +link{listGrid.showGridSummary} is true, and a +link{listGrid.summaryRowDataSource} is specified
// this property may be used to specify fetch criteria to apply when retrieving summary data
// to show in the summary row. If unset, and any filter criteria have been specified for the
// grid, they will be used.
// @visibility external
//<
// summaryRowCriteria:null,
getSummaryRowCriteria : function () {
    if (this.summaryRowCriteria != null) return this.summaryRowCriteria;
    var data = this.getOriginalData();
    if (isc.ResultSet && isc.isA.ResultSet(data)) {
        return this.data.getCriteria();
    }
    return this.getInitialCriteria();
},

//> @attr listGrid.summaryRowFetchRequestProperties (DSRequest Properties : null : IRWA)
// If +link{listGrid.showGridSummary} is true, and a +link{listGrid.summaryRowDataSource} is specified
// this property may be used to customize the fetch request used when retrieving summary data
// to show in the summary row. An example use case might be specifying a
// +link{DSRequest.operationId} to perform a custom fetch operation which retrieved only summary
// values based on criteria.
//
// @visibility external
//<
//summaryRowFetchRequestProperties:null,
getSummaryRowFetchRequestConfig : function () {
    return isc.addProperties(
                    {textMatchStyle:this.autoFetchTextMatchStyle,
                     showPrompt:false,
                     startRow:0, endRow:1},
                    this.summaryRowFetchRequestDefaults,
                    this.summaryRowFetchRequestProperties);
},

summaryRowFetchComplete : function (response, data, request) {

    this.updateFieldWidthsForAutoFitValue("gridSummary fetch complete");

    this._updateFieldWidths();

},

//> @method listGrid.getGridSummaryData()
// This method returns the data <smartclient>to be</smartclient> displayed in the
// +link{summaryRow} when +link{showGridSummary} is true.
// <P>
// By default this will call +link{getGridSummary} for each field and generate an array of records
// containing the resulting values.
// <P>
// This method <smartclient>may be overridden for custom grid-summary display, and</smartclient>
// may return multiple records if more than one summary row is desired.
//
// @return (Array of ListGridRecord) summary record(s)
// @visibility external
//<
getGridSummaryData : function (recalculate, fields) {

    if (this._gridSummaryData && !recalculate) return this._gridSummaryData;


    var data;
    if (this._gridSummaryData && fields != null) {
        data = this._gridSummaryData;
    } else {
        fields = this.completeFields || this.fields;
        data = [];
        if (fields == null) return data;
    }

    for (var i = 0; i < fields.length; i++) {
        var field = fields[i],
            fieldName = fields[i].name;
        if (!this.shouldShowGridSummary(field)) {
             continue;
         }

        var values = this.getGridSummary(field),
            summaryFunc = this.getGridSummaryFunction(field);
        if (!isc.isAn.Array(summaryFunc)) summaryFunc = [summaryFunc];
        // getGridSummary returns either an atomic value or an array. If an array is returned
        // we map this to multiple records - this is how multiple summary functions give us
        // a multi-line result in the grid-summary.

        if (!isc.isAn.Array(values)) {
            values = [values]
        }
        for (var ii = 0; ii < values.length; ii++) {
            if (data[ii] == null) {
                data[ii] = {};
                // This property is used to allow summary fields to suspend the standard
                // summary from data in this record [so a standard 'grid summary' type sum etc can
                // be performed against the live grid data].
                data[ii][this.gridSummaryRecordProperty] = true;
                // this prevents the checkbox select column from showing a checkbox
                // for the summary row

                data[ii][this.recordCanSelectProperty] = false;

                // If this record came from a "count" summary function, set the
                // recordApplyPluralTitleProperty flag on the record.

                if (summaryFunc[ii] == "count") {
                    var array = data[ii][this.recordApplyPluralTitleProperty] || [];
                    array.add(fieldName);
                    data[ii][this.recordApplyPluralTitleProperty] = array;
                }

            }
            data[ii][fieldName] = values[ii];
        }
    }
    this._gridSummaryData = data;
    return data;
},

//> @method listGrid.getGroupSummaryData()
// If this grid is +link{listGrid.groupByField,grouped}, and +link{showGroupSummary} is true,
// this method will <smartclient>be called for each group to</smartclient> return the
//  group summary data <smartclient>to display</smartclient><smartgwt>displayed</smartgwt>
//  at the end of the group.
// <P><smartclient>
// By default this will call +link{listGridField.getGroupSummary} if defined for each field and
// generate an array of records containing the resulting values. If no explicit per-field
// getGroupSummary method is present, this method will fall back to calling the appropriate
// +link{listGridField.summaryFunction}.
// <P></smartclient>
// This method may <smartclient>be overridden for custom group-summary display, and
// may</smartclient> return multiple records if more than one summary row per group is desired.
//
// @param records (Array of Record) the records in the group, for which the summary values
//                                  are being calculated
// @param groupNode (Record) object with specified groupValue and groupName for this group
//
// @return (Array of ListGridRecord) summary record(s)
// @visibility external
//<

getGroupSummaryData : function (records, groupNode, fields) {
    return this._getGroupSummaryData(
        this.includeInSummaryProperty, this.applyFormulaAfterSummary,
        this.groupSummaryRecordProperty, null, records, groupNode, fields);
},
_getGroupSummaryData : function (
    includeInSummaryProperty, applyFormulaAfterSummary, groupSummaryRecordProperty,
    fieldsHaveUserFormulas, records, groupNode, fields)
{
    // If `getGroupSummaryData()` is overridden by the user then we have to call it.
    if (this.getGroupSummaryData != isc.ListGrid.getInstanceProperty("getGroupSummaryData")) {
        return this.getGroupSummaryData(records, groupNode, fields);
    }

    var summaryData;
    if (groupNode && groupNode._groupSummaryData && fields != null) {
        summaryData = groupNode._groupSummaryData;
    } else {
        fields = this.fields;
        summaryData = [];
    }

    for (var i = 0; i < fields.getLength(); i++) {
        var field = fields[i],
            fieldName = field.name,
            summaryValue;
        if (this.shouldShowGroupSummary(field)) {
            var fieldHasUserFormula = (
                    fieldsHaveUserFormulas == null ?
                        (field && field.userFormula != null) :
                        fieldsHaveUserFormulas[i]);
            summaryValue = this._getGroupSummary(
                includeInSummaryProperty, applyFormulaAfterSummary, fieldHasUserFormula,
                records, field, groupNode);

            // handle being passed an atomic value or an array. If an array
            // is returned we show multiple summary rows.
            if (!isc.isAn.Array(summaryValue)) summaryValue = [summaryValue];

            var summaryFunc = this.getGridSummaryFunction(field);
            if (!isc.isAn.Array(summaryFunc)) summaryFunc = [summaryFunc];

            for (var ii = 0; ii < summaryValue.length; ii++) {
                var summaryRecord = summaryData[ii];
                if (summaryRecord == null) {
                    summaryRecord = summaryData[ii] = {};

                    summaryRecord.customStyle = this.groupSummaryStyle;
                    // mark as disabled so we don't respond to clicks / attempts to select etc.
                    // should have no effect on styling since customStyle is static
                    summaryRecord[this.recordEnabledProperty] = false;
                    // don't include this record in the overall grid summary row!
                    summaryRecord[includeInSummaryProperty] = false;

                    // flag the record as a groupSummary record
                    // This allows custom record summary functions to treat it
                    // differently if necessary
                    summaryRecord[groupSummaryRecordProperty] = true;

                    // If this record came from a "count" summary function, set the
                    // recordApplyPluralTitleProperty flag on the record.

                    if (summaryFunc[ii] == "count") {
                        var array = summaryRecord[this.recordApplyPluralTitleProperty] || [];
                        array.add(fieldName);
                        summaryRecord[this.recordApplyPluralTitleProperty] = array;
                    }
                }
                var currentVal = summaryValue[ii];
                summaryRecord[fieldName] = currentVal;
            }
        }
    }
    if (groupNode) groupNode._groupSummaryData = summaryData;
    return summaryData;
},

//> @object GroupSummary
// Group Summary.
//
// @treeLocation Client Reference/Grids/ListGrid
//@visibility external
//<

//>    @attr groupSummary.groupName        (string : null : IRW)
//<
//>    @attr groupSummary.groupValue        (string : null : IRW)
//<
//>    @attr groupSummary.customStyle        (string : null : IRW)
//<

// This method is called to put together the group level summary values and pass them
// to the getGridSummary method, so developers can look at the summaries, rather than
// necessarily calculating grid summaries based on every data record.
assembleGroupSummaries : function (node, summaries) {
    var tree = this.groupTree;
    if (!node) node = tree.getRoot();
    if (!summaries) summaries = [];

    var folders = tree.getFolders(node);
    for (var i = 0; i < folders.length; i++) {
        var headerNode = folders[i];

        if (this.groupByFieldSummaries == null ||
            this.groupByFieldSummaries.contains(headerNode.groupName))
        {

            var summaryRecords = tree.combineWithEditVals(tree.getRecordsInGroup(headerNode));
            var summaryData = this.getGroupSummaryData(summaryRecords, headerNode);

            for (var ii = 0; ii < summaryData.length; ii++) {
                var groupSummary = isc.addProperties({},summaryData[ii]);
                delete groupSummary.customStyle;
                delete groupSummary[this.recordEnabledProperty];
                delete groupSummary[this.includeInSummaryProperty];
                groupSummary.groupName = headerNode.groupName;
                groupSummary.groupValue = headerNode.groupValue;

                summaries.add(groupSummary);
            }
        }

        // make this recursive
        this.assembleGroupSummaries(folders[i], summaries);
    }
    return summaries;

},

// getSummaryValue() - generic handler to take a bunch of records and a field definition and
// return the summary value from them. Used for both grid level summaries and group-level summaries
getSummaryValue : function (records, field) {
    return this._getSummaryValue(
        this.includeInSummaryProperty, this.applyFormulaAfterSummary,
        (field && field.userFormula != null), records, field);
},
_getSummaryValue : function (
    includeInSummaryProperty, applyFormulaAfterSummary, fieldHasUserFormula, records, field)
{
    if (records == null) return null;

    // pull out any records where includeInSummary is false
    var includedRecords = [];
    for (var i = 0; i < records.length; i++) {
        var record = records[i];
        if (!record || (record[includeInSummaryProperty] == false)) continue;
        includedRecords[includedRecords.length] = record;
    }

    var summaryFunction = this.getGridSummaryFunction(field);
    if (summaryFunction == null) return null;


    if (this._shouldApplyUserFormulaAfterSummary(
            applyFormulaAfterSummary, fieldHasUserFormula, field))
    {
        return [];
    }

    if (!isc.isAn.Array(summaryFunction)) {
        summaryFunction = [summaryFunction]
    }
    var results = [];
    for (var i = 0; i < summaryFunction.length; i++) {

        var currentFunction = summaryFunction[i];
        if (currentFunction != null) {
            results[i] = isc.SimpleType.applySummaryFunction(includedRecords,
                                            // Pass the component into 'applySummaryFunction' as
                                            // an extra, undocumented param.
                                            // We use this to handle resolving "absolute" dataPaths
                                            // on fields
                                            field, currentFunction, this);
        }
    }
    return results;
},


shouldShowGroupSummary : function (field) {
    if (field.showGroupSummary != null) return field.showGroupSummary;
    return (field.getGroupSummary != null || this.getGridSummaryFunction(field) != null)
},


getGroupSummary : function (records, field, groupNode) {
    return this._getGroupSummary(
        this.includeInSummaryProperty, this.applyFormulaAfterSummary,
        (field && field.userFormula != null), records, field, groupNode);
},
_getGroupSummary : function (
    includeInSummaryProperty, applyFormulaAfterSummary, fieldHasUserFormula, records, field, groupNode)
{
    var summaryValue;
    if (field.getGroupSummary != null) {
        summaryValue = field.getGroupSummary(records, field, groupNode);
    } else {
        summaryValue = this._getSummaryValue(
            includeInSummaryProperty, applyFormulaAfterSummary, fieldHasUserFormula,
            records, field);
    }
    return summaryValue;
},

// field level summaries
getRecordSummary : function (recordNum, summaryField) {
    return this._getRecordSummary(null, null, recordNum, summaryField);
},
_getRecordSummary : function (fieldsHaveUserFormulas, fieldsHaveUserSummaries, recordNum, summaryField) {
    // we want to use edit vals when calculating totals
    var record = isc.isAn.Object(recordNum) ? recordNum : this.getEditedRecord(recordNum);

    // override point for a completely custom method based on the record without looking at other
    // fields, etc
    if (summaryField.getRecordSummary != null) {
        return summaryField.getRecordSummary(record, summaryField);
    }

    var fieldsToInclude = [],
        fields = this.fields;

    for (var i = 0; i < fields.length; i++) {
        var field = fields[i];

        // should a field be included in this summary calculation?

        // Never include a field in its own calculation!
        if (field.name == summaryField.name) {
            // partial summary - only include fields up to this one (index < this field's index)
            if (summaryField.partialSummary) break;
            continue;
        }

        var shouldInclude = field.includeInRecordSummary;

        // If 'includeInRecordSummary' is unset, default to including numeric non summary fields

        var hasUserFormula = (
                fieldsHaveUserFormulas == null ?
                    (field && field.userFormula != null) : fieldsHaveUserFormulas[i]),
            hasUserSummary = (
                fieldsHaveUserSummaries == null ?
                    (field && field.userSummary != null) : fieldsHaveUserSummaries[i]);
        if (shouldInclude == null && (field.type == "integer" || field.type == "float")

            && (!hasUserFormula && !hasUserSummary))
        {
            shouldInclude = true;
        }

        // if 'includeInRecordSummaryFields' is explicitly set, respect it as well
        if (shouldInclude && field.includeInRecordSummaryFields != null) {
            if ((isc.isA.String(field.includeInRecordSummaryFields) &&
                     field.includeInRecordSummaryFields != summaryField.name) ||
                (isc.isAn.Array(field.includeInRecordSummaryFields) &&
                     !field.includeInRecordSummaryFields.contains(summaryField.name)))
            {
                shouldInclude = false;
            }
        }
        if (shouldInclude) {
            fieldsToInclude.add(field);
        }
    }
    var summaryFunction = summaryField.recordSummaryFunction || "sum";
    var value = isc.DataSource.applyRecordSummaryFunction(summaryFunction, record,
                                            fieldsToInclude, summaryField);
    return value;
},

// Summary fields are tricky: If we have a summary field which is also showing
// a group or grid level summary, it needs the summary value available on each
// record passed in so it can do calculations like "sum"
// Recalculate these summaries and hang them on the objects now

_$summary:"summary",
shouldApplyRecordSummaryToRecord : function (field) {
    return field && (field.type == this._$summary) &&
            (field.summaryFunction != null || field.getGroupSummary != null);
},

// Handle formula fields the same way we handle summary fields with the added wrinkle
// that the developer can allow formula functions to apply across summary rows
shouldApplyUserFormulaBeforeSummary : function (field) {
    return field && field.userFormula != null && !this._applyFieldFormulaAfterSummary(field);
},
shouldApplyUserFormulaAfterSummary : function (field) {
    return this._shouldApplyUserFormulaAfterSummary(
        this.applyFormulaAfterSummary, (field && field.userFormula != null), field);
},
_shouldApplyUserFormulaAfterSummary : function (applyFormulaAfterSummary, fieldHasUserFormula, field) {
    return (
        fieldHasUserFormula &&
        this.__applyFieldFormulaAfterSummary(applyFormulaAfterSummary, field));
},


calculateRecordSummaries : function (records, fields, updateGroupSummaries,
    updateGridSummaries, suppressDisplay, suppressApplyToRecords, keepUserCache)
{
    var fieldsToUpdate = this.fields;

    if (fields != null) {
        var grid = this;
        if (!isc.isAn.Array(fields)) fields = [fields];

        // translate field names to fields
        fields = fields.map(function (field) {
            return isc.isA.String(field) ? grid.getField(field) : field;
        });
        // expand to all dependent fields if configured
        if (this.refreshDependentFieldsForSummaries) {
            fields = this._addDependentUserFields(fields);
        }
        fieldsToUpdate = fields;
    }

    // clear cache for specified records/fields to ensure recalculation
    if (!keepUserCache) this.invalidateUserCache(records, fields);

    // nothing to do if there are no fields to update
    if (!fieldsToUpdate) return;

    if (records != null && !isc.isAn.Array(records)) records = [records];
    var recordsToUpdate = records == null ? this.getOriginalData() : records;
    if (recordsToUpdate == null ||
        isc.isA.ResultSet(recordsToUpdate) && !recordsToUpdate.lengthIsKnown())
    {
        return;
    }

    var valuesChanged = false;

    if (!suppressApplyToRecords) {
        var summaryFields     = [],
            userFormulaFields = [];

        for (var i = 0; i < fieldsToUpdate.length; i++) {
            var field = fieldsToUpdate[i];
            if        (this.shouldApplyRecordSummaryToRecord   (field)) {
                summaryFields.add(field);
            } else if (this.shouldApplyUserFormulaBeforeSummary(field)) {
                userFormulaFields.add(field);
            }
        }

        if (summaryFields.length > 0 || userFormulaFields.length > 0) {
            for (var i = 0; i < recordsToUpdate.getLength(); i++) {

                var record = isc.isA.ResultSet(recordsToUpdate) ?
                                               recordsToUpdate.getCachedRow(i) :
                                               recordsToUpdate.get(i);
                if (record == null) continue;

                var editVals = this.getEditValues(record),
                    hasEditVals = !isc.isAn.emptyObject(editVals),
                    editedRecord = hasEditVals ? isc.addProperties({_noCache: true}, record,
                                                                   editVals) : record;
                if (record == null) continue;

                for (var ii = 0; ii < summaryFields.length; ii++) {

                    var field = summaryFields[ii];

                    // Verify that we actually want to show a record summary value in
                    // this cell.
                    // This method will return false for the case where this is a summary row
                    // and we want to (for example) sum the calculated record summary shown in
                    // this column for all rows in the grid, rather than trying to perform
                    // record-summary arithmetic on the summary row itself.
                    if (!this.shouldShowRecordSummary(field, record)) {
                        continue;
                    }

                    var oldValue = record[field.name];

                    var summaryVal = this.getRecordSummary(editedRecord,field);
                    record[field.name] = summaryVal;

                    if (!this.fieldValuesAreEqual(field, oldValue, summaryVal)) {
                        valuesChanged = true;
                    }
                }
                for (var ii = 0; ii < userFormulaFields.length; ii++) {

                    var field = userFormulaFields[ii];
                    // Skip records where we suppress the formula (EG group summary rows)
                    if (!this.shouldShowUserFormula(field,record)) {
                        continue;
                    }

                    var oldValue = record[field.name],
                        newValue = this.getFormulaFieldValue(field, editedRecord);

                    if (!this.fieldValuesAreEqual(field, oldValue, newValue)) {

                        if (hasEditVals) {
                            this.invalidateUserCache(record, [field]);
                            record[field.name] = newValue;
                        }
                        valuesChanged = true;
                    }
                }
            }
        }
    }

    // if we're showing neither group nor grid summaries we can bail here since the
    // remaining code below is strictly dealing with group and grid summary rows.
    if ((!this.isGrouped         || !this.showGroupSummary) &&
        (this.summaryRow == null || !this.showGridSummary))
    {
        return;
    }


    this._recalculateSummaries(records, fields,
        updateGroupSummaries != null ? updateGroupSummaries : valuesChanged,
        updateGridSummaries  != null ? updateGridSummaries  : valuesChanged, !suppressDisplay);
},


// Standard field level summary functions.
// These allow a developer to specify a field of type "summary" with summaryFunction "sum" rather
// than re-implementing the sum method on their field.
// It's basically the same list as the SimpleType registered summary functions but it
// takes different parameters - A single record and multiple fields rather than vice versa


//> @attr listGrid.recordSummaryBaseStyle (CSSStyleName : "recordSummaryCell" : IRWA)
// If showing any record summary fields (IE: fields of +link{listGridFieldType,type:"summary"}),
// this attribute specifies a custom base style to apply to cells in the summary field
// @visibility external
//<
recordSummaryBaseStyle:"recordSummaryCell",

//> @attr listGrid.summaryRow (AutoChild ListGrid : null : RA)
// Automatically generated ListGrid for displaying grid summary information (see
// +link{listGrid.showGridSummary}).
// <P>
// This component is an +link{type:AutoChild} and as such may be customized via
// <code>listGrid.summaryRowProperties</code> and <code>listGrid.summaryRowDefaults</code>
// @visibility external
//<
summaryRowConstructor:"ListGrid",

summaryRowDefaults:{
    // disable rollOver styling
    showRollOver:false

},


//> @attr listGrid.summaryRowHeight (int : 20 : IR)
// Default height for the +link{listGrid.summaryRow,summary row autoChild}. Note that this
// height is a minumum - the summary row has +link{listGrid.autoFitData} set to "vertical" so
// if multiple rows are visible in the grid summary, the summaryRow component
// will expand to accomodate them.
//
// @visibility external
//<
summaryRowHeight:20,

//> @attr listGrid.summaryRowStyle (CSSStyleName : "gridSummaryCell" : IRWA)
// +link{listGrid.baseStyle} for the +link{listGrid.summaryRow}
// @visibility external
//<
summaryRowStyle:"gridSummaryCell",

// creates (or updates) and returns the summaryRow autoChild.
// not called directly -- call 'setShowGridSummary' instead
getSummaryRow : function () {

    if (!this.summaryRow) {

        var dataSource = this.getSummaryRowDataSource();

        var initialFields;
        if (this.completeFields) initialFields = this.completeFields.duplicate();
        else if (this.fields) initialFields = this.fields.duplicate();
        // We respond to setFields(), setFieldWidths(), showField(), hideField() etc explicitly
        // in those methods so don't worry if initialFields is null - it'll get set when required

        var height = this.summaryRowHeight;
        this.summaryRow = this.createAutoChild("summaryRow", {

            showEmptyMessage:false,

            warnOnReusedFields:false,
            autoDraw:false,

            width:"100%",
            height:height,

            autoFitData:"vertical",
            // autoFitMaxRows etc can be specified in summaryRowProperties if desired.

            // setting bodyOverflow to hidden.
            // If we don't do this and field widths exceed grid size, we end up with
            // hscrollbars on both the listgrid and the summary row.


            bodyOverflow:"hidden",

            showHeader:false,

            getBaseStyle:function() {return this.creator.summaryRowStyle},
            alternateRecordStyles:false,

            disabled:this.disabled,
            applyFormulaAfterSummary: this.applyFormulaAfterSummary,

            // Make fetchValueMapData into a no-op. We copy our fields from
            // our creator so any optionDataSources will match up meaning we can rely on
            // the creator's fetchValueMapData / 'setValueMap()' to update our valueMap
            // rather than having to do a second fetch against optionDataSources
            _fetchValueMapData : function () {
            },

            // The default summary functions all return number or string values, even if field.type
            // is boolean. If this method returns true we'll always display a checkbox rather than
            // the expected summary value.

            _formatBooleanFieldAsImages : function (field) {
                return false;
            },

            // support for databinding of summary row
            dataSource:dataSource,
            // pick up data from the gridSummaryData method
            data:dataSource == null ? this.getGridSummaryData() : null,

            _recalculateSummaries : function (fields) {

                var grid = this.creator,
                    data = grid.data
                ;
                // prevent the summaryRow from fetching before the parent grid - it will fetch
                // again when the parent is ready
                if (!grid.fields || (isc.isA.ResultSet(data) && !data.lengthIsKnown())) return;

                // if we are getting data from a dataSource, simply re-fetch
                if (this.dataSource != null) {
                    var criteria = grid.getSummaryRowCriteria(),
                        forceRefresh = isc.ResultSet && isc.isA.ResultSet(this.data) &&
                                       !this.data.willFetchData(criteria);

                    // Note - if we're dropping cache, do this before the fetchData call so our
                    // callback fires!
                    if (forceRefresh) this.invalidateCache();
                    this.fetchData(
                        criteria,
                        {target:grid, methodName:"summaryRowFetchComplete"},
                        grid.getSummaryRowFetchRequestConfig()
                    );
                } else {
                    this.setData(grid.getGridSummaryData(true, fields));
                }
            },

            fieldSourceGrid:this,
            fields:initialFields,

            // Copy formatting related properties across so _formatCellValue
            // behaves correctly

            recordApplyPluralTitleProperty:this.recordApplyPluralTitleProperty,
            gridSummaryRecordProperty:this.gridSummaryRecordProperty,
            invalidSummaryValue:this.invalidSummaryValue,
            emptyCellValue:this.emptyCellValue,


            // We size fields according to the main grid field-widths.

            skipAutoFitWidths:true,
            getFieldWidths : function () {
                return this.creator.getFieldWidths();
            },

            // We have our fields assigned from the source grid - If 'showIf' is set, evaluate it
            // on the source grid, not on this grid.
            // This ensures that the "list" param passed to that method refers to the correct grid

            fieldShouldBeVisible : function (field, fieldNum) {
                return this.fieldSourceGrid.fieldShouldBeVisible(field, fieldNum);
            },

            canEditCell:function () {
                return false;
            },

            // mark the summary row so we can have special handling for like checkboxField, etc
            _isSummaryRow: true

        });

    } else {
        this.summaryRow.setDataSource(this.getSummaryRowDataSource(), this.completeFields.duplicate());
        // recalculateSummaries handles moving and resizing the summary row
        // (It has to, since the number of summary rows displayed may change, changing the
        // height of the summary row grid).
        this.summaryRow._recalculateSummaries();
    }
    return this.summaryRow;
},


showSummaryRow : function () {
    // Refuse to show if the gridComponents array doesn't include the s.row
    var componentIndex = this.gridComponents.indexOf("summaryRow");
    if (componentIndex == -1) {
        this.logWarn("showGridSummary set to true, but gridComponents array does not include an " +
            "entry for the summary row - not showing.");
        return;
    }
    // This will handle creating, or updating the summary row and adding it as a member
    this.updateGridComponents()
    this.syncSummaryRowScrolling(this.body.getScrollLeft(), this.body.getScrollTop);

},

// clearSummaryRow() - clears (but doesn't destroy()) the summaryRow component.
// Developers will call 'setShowGridSummary' rather than calling this method directly
clearSummaryRow : function () {
    if (this.summaryRow && this.summaryRow.parentElement == this) {
        this.removeMember(this.summaryRow);
    }
},

_isSummaryRecord : function (record) {
    return record[this.gridSummaryRecordProperty] || record[this.groupSummaryRecordProperty];
},

// --------------------------------------------------------------------------------------------
// Filter editor row
// --------------------------------------------------------------------------------------------

// When 'showFilterEditor' is true on a listGrid, it will be drawn with a RecordEditor used to
// filter the List's data.

//> @method listGrid.setShowFilterEditor()
// Setter for the +link{ListGrid.showFilterEditor} property. Allows the filter editor to be
// shown or hidden at runtime.
// @param value (boolean) true if the filter editor should be shown, false if it should be hidden
// @group filterEditor
// @visibility external
//<
setShowFilterEditor : function (value) {
    // No op is required to avoid potentially setting up duplicate FEs etc.
    if (this.showFilterEditor == value) return;
    this.showFilterEditor = value;

    if (value) {
        var componentIndex = this.gridComponents.indexOf("filterEditor");
        if (componentIndex == -1) {
            value = false;
            this.logWarn("setShowFilterEditor(true) called, but gridComponents array does not " +
                "include the filterEditor. FilterEditor will not be shown.");
        }
    }

    if (value) {
        // if we're already drawn re-run createChildren to build the filterEditor and plug it in
        if (this.isDrawn()) {
            // store the viewState and restore it after updating
            var state = this.getViewState();
            this.updateGridComponents();
            this.setViewState(state);
        }

    // filterEditor may have never been defined if this widget has not yet been drawn
    } else if (this.filterEditor) {
        this.filterEditor.destroy();
        this.filterEditor = null;
    }

    // This will ensure field widths, sizes etc are all correct.
    this.layoutChildren();
},

// makeFilterEditor()
// Create a RecordEditor instance as this.filterEditor.
makeFilterEditor : function () {
    var filterEditorProps = isc.addProperties({
        autoDraw:false,
        warnOnReusedFields:false,

        reverseRTLAlign:this.reverseRTLAlign,


        dataPath:this.dataPath,
        setDataPath : function (dataPath) {
            this.dataPath = dataPath;
        },

        // Disable fetchValueMapData on the filterEditor entirely.
        // If we're showing an edit item for the field it'll have an optionDataSource
        // set on it, meaning the selectItem / comboBox etc will be responsible for issuing
        // any fetch request against the ODS if necessary.
        _fetchValueMapData:function () {
        },

        height : this.filterEditorHeight,
        // NOTE: filterEditors handle width sizing themselves

        sourceWidget:this,
        ID:this.getID() + "filterEditor",
        // We built this thing ourselves so mark it as generated. Allows the AutoTest subsystem to
        // generate a locator that keeps going up the chain, potentially
        _generated:true,
        locatorParent: this,

        // We have our fields assigned from the source grid - If 'showIf' is set, evaluate it
        // on the source grid, not on this grid.
        // This ensures that the "list" param passed to that method refers to the correct grid

        fieldShouldBeVisible : function (field, fieldNum) {
            return this.sourceWidget.fieldShouldBeVisible(field, fieldNum);
        },

        // we share field objects with the main grid.
        // This 'fieldSourceGrid' property is used to ensure we do the right thing about
        // generated fields like the selection-checkbox field etc.
        fieldSourceGrid:this,

        actionType:"filter",
        actionButtonPrompt:this.filterButtonPrompt,
        actionButtonProperties:this.filterButtonProperties,

        fetchDelay: this.fetchDelay,

        allowFilterExpressions: this.allowFilterExpressions,
        expressionDataSource: this.getDataSource(),

        // When the user hides a field, remember the user-entered criteria for that field
        discardEditsOnHideField: this.dropCriteriaOnHideField


    }, this.filterEditorDefaults, this.filterEditorProperties);

    if (filterEditorProps.bodyDefaults == null) filterEditorProps.bodyDefaults = {};
    // Ensure if the user tabs into the body we shift focus into the edit form
    // Exception: If this came from a click, we'll already start editing the clicked cell,
    // and a call to startEditing would mess that up and jump focus to the first
    // edit item unexpectedly.
    filterEditorProps.bodyDefaults.focusChanged = function (hasFocus) {
        if (hasFocus && isc.EH.isKeyEvent()) {
            if(this.parentElement.isA("RecordEditor")) {
                this.parentElement.startEditing();
            } else {
                // maybe we are using frozen fields, in that case the RecordEditor is the grandparent
                  if(this.parentElement.parentElement.isA("RecordEditor")) {
                    this.parentElement.parentElement.startEditing();
                }
            }
        }
    }

    this.filterEditor = isc.RecordEditor.create(filterEditorProps);

},



//>    @method listGrid.getFilterEditorValueMap()  ([A])
//
//  If we're showing the filter (query-by-example) row for this ListGrid, this method is
//  used to determine the valueMap to display in the filter row for this field.
//  Default implementation will return the field.filterEditorValueMap if specified, or
//  field.valueMap.
//
//  @group  filterEditor
//
//  @param  field   (ListGridField)    field definition field for which we need a valueMap
//  @return         (ValueMap)  ValueMap for the edit field (or null if no valueMap required)
//  @visibility external
//<
getFilterEditorValueMap : function (field) {
    return  field.filterEditorValueMap || field.valueMap ||
        (field.filterEditorProperties && field.filterEditorProperties.valueMap);
},

//>    @method listGrid.getFilterEditorType()  ([A])
// If we're showing the filter (query-by-example) row for this ListGrid, this method is
// used to determine the type of form item to display in the filter edit row for this field.
// Default implementation will return the field.filterEditorType if specified, or
// the result of +link{DynamicForm.getEditorType, form.getEditorType()} otherwise.
//
// @group filterEditor
//
// @param field (ListGridField) field definition to get the editorType for
// @return (String) the editorType to use in the filterEditor for the passed field
// @visibility external
//<
_$binary:"binary", _$file:"file", _$imageFile:"imageFile",
getFilterEditorType : function (field) {

    // Simple case: support explicit filterEditorType on the field
    if (field.filterEditorType != null) return field.filterEditorType;

    // TODO: re-implement this once RecordEditor correctly returns AdvancedCriteria
    if (isc.SimpleType.inheritsFrom(field.type, "date") && this.getDataSource() &&
        this.getDataSource().supportsAdvancedCriteria())
    {
        return "MiniDateRangeItem";
    }

    var type = field.type;
    var isFileType = (type == this._$binary || type == this._$file ||
                        type == this._$imageFile);
    if (isFileType && field.editorType == null) {
        return "StaticTextItem";
    }

    // filter editor config is basically picked up from field defaults and explicit
    // field.filterEditorProperties.
    // If a a field specifies an explicit filterEditorType or a filterEditorProperties block with
    // an explicit editor type, respect it.
    // Otherwise if a field specifies an explicit editorType, respect that
    // Otherwise generate the editor type based on data type in the normal way
    // A couple of exceptions:
    // - override canEdit with canFilter, so we don't get a staticTextItem in the field

    // - clear out field.length: we don't want to show the long editor type (text area) in our
    //   filter editor
    var filterEditorConfig = isc.addProperties ({}, field,
                                                 {canEdit:field.canFilter !== false,
                                                  length:null});

    // the _constructor property can come from XML -> JS conversion, and matches the
    // XML tag name for the field element.
    // Don't attempt to use this to determine DynamicForm editor type - it's likely to be
    // ListGridField or similar which shouldn't effect the generated form item type.
    if (filterEditorConfig._constructor != null) delete filterEditorConfig._constructor;
    if (field.filterEditorType != null) filterEditorConfig.editorType = field.filterEditorType;
    isc.addProperties(filterEditorConfig, field.filterEditorProperties);
    var type = isc.DynamicForm.getEditorType(filterEditorConfig, this);
    return type;

},

// getFieldFilterEditorProperties - returns a block of properties to apply to the form item displayed
// in the filter row for some field.
getFieldFilterEditorProperties : function (field) {
    var result = field.filterEditorProperties || {};
    if (field.filterOperator) result.operator = field.filterOperator;

    return result;
},

// Unexposed but publicly accessible method to get a pointer to the filter editor grid

getFilterEditor : function () {
    return this.filterEditor;
},

//> @method listGrid.setFilterEditorCriteria()
// If +link{listGrid.showFilterEditor} is true, this method will update the criteria shown
// in the <code>filterEditor</code> without performing a filter.
// @param criteria (Criteria or AdvancedCriteria) New criteria to show
// @visibility external
//<
setFilterEditorCriteria : function (criteria) {
    if (this.filterEditor) {
        // Normally we either want setFilterValues to drop all criteria and replace with new
        // ones (programmatic 'filterData()'), or retain all hidden criteria and just
        // sparsely apply specified criteria to visible fields (user interactions with the
        // filter editor).
        // However if 'setFilterEditorCriteria' is called directly we want to clear
        // criteria on fields that have been explicitly hidden by the user, but are
        // present in the grid 'completeFields' definition,
        // but still retain criteria applied to fields which aren't
        // actually present in the grid at all.

        var hasHiddenFields = false,
            hiddenFields = [];
        var editForm = this.filterEditor.getEditForm();
        for (var i = 0; i < this.completeFields.length; i++) {
            var field = this.completeFields[i];
            if (this.fieldIsVisible(field,true)) {
                // incremental rendering may cause the field not to be present in our
                // filter editor edit-form
                // Catch the case where we have the form, but not the item for some
                // (logically) visible field.
                if (!editForm || editForm.getItem(field.name) != null) {
                    continue;
                }
            }

            hasHiddenFields = true;
            hiddenFields.add(field.name);
        }
        this.setFilterValues(criteria, hasHiddenFields, hiddenFields);
    }
},

//> @method listGrid.getFilterEditorCriteria()
// If +link{listGrid.showFilterEditor} is true, this method will return the criteria currently
// displayed in the <code>filterEditor</code>. Note that these values may differ from the
// criteria returned by +link{listGrid.getCriteria()} if the filter editor values have been modified
// without performing an actual filter.
// @param [omitHiddenFields] (Boolean) By default this method will include criteria applied to
//   fields, including criteria that are not actually visible/editable in the filterEditor for the
//   grid. Pass in this parameter to get only values for visible fields returned.
// @return (Criteria or AdvancedCriteria) criteria currently displayed in the filterEditor
// @visibility external
//<
// Note: we rely on the filterEditor edit form to handle combining specified criteria with criteria
// from live items - as such calling getValuesAsCriteria() on that form will return all specified
// criteria, even for fields not visible in the grid.
// This is not always desired - it can be useful to apply additional criteria outside those
// editable by the user, and in order to repeatedly do this, we need to be able to get back a
// clean copy of *only* the criteria the user has modified. The includeHiddenFields parameter
// allows us to do this.
// - See the DynamicReporting sample for an example of this use-case:
//      isomorphic/system/reference/SmartClient_Explorer.html#dynamicReporting

getFilterEditorCriteria : function (omitHiddenFields) {
    if (this.filterEditor) {

        if (!omitHiddenFields || !this.filterEditor.getEditForm()) {
            return this.filterEditor.getValuesAsCriteria(this.autoFetchTextMatchStyle);

        } else {
            var items = this.filterEditor.getEditForm().getItems(),
                simple = true,
                criteria = {},
                advancedCriteria = {_constructor: "AdvancedCriteria",
                    operator:"and", criteria:[]}
            ;
            for (var i = 0; i < items.length; i++) {
                if (items[i].hasAdvancedCriteria()) {
                    var crit = items[i].getCriterion();
                    if (crit != null) {
                        simple = false;
                        advancedCriteria.criteria.add(crit);
                    }
                } else {
                    var value = items[i].getValue();
                    if (value != null) {
                        criteria[items[i].getCriteriaFieldName()] = value;
                    }
                }
            }
            if (!simple) {
                criteria = isc.DataSource.combineCriteria(criteria, advancedCriteria);
            }
            return criteria;
        }
    }
},

// Default setCriteria implementation will no-op if we have no data (and won't update the
// filterEditor even if we do have data) - override to ensure the FE reflects the new criteria
setCriteria : function (criteria) {
    if (this.filterEditor != null) {
        this.setFilterValues(criteria, true);
    }
    // If grouped we need to apply criteria to underlying ResultSet
    var data = this.isGrouped ? this.getOriginalData() : this.data;
    if (data && data.setCriteria) data.setCriteria(criteria);
    // if there is no data yet, set initial criteria to parameter criteria
    else this.initialCriteria = criteria;

},

// setFilterValues() - helper method called when this widgets filter criteria change.
// Will store the criteria locally and call the method to update the filter editor values.
// dropExtraCriteria is passed on to filterEditor.setValuesAsCriteria() and causes it to
// drop any advanced criteria it had kept hold of because it couldn't be edited
// The explicit fields passed in as dropCriteriaFields allows us to retain
// values for fields which aren't defined in the grid at all, but still clear values for
// fields which are defined but hidden (so aren't represented in the edit form)
// This matches the use case of a user editing a filter value, hiding a field and then
// attempting to clear filter editor values via the clearFilter menu item.
setFilterValues : function (criteria, dropExtraCriteria, dropCriteriaFields) {

    // store this in a local var - this allows us to show and hide the filterEditor independently
    // and know what the current criteria are.
    this._filterValues = isc.addProperties({}, criteria);

    // Update initialization property to match current criteria
    this.initialCriteria = this._filterValues;

    this.updateFilterEditor(dropExtraCriteria, dropCriteriaFields);
},


// update the filter editor's values to match the current filter criteria
// Needs to happen whenever the filter criteria change or the filter editor is shown
updateFilterEditor : function (dropExtraCriteria, dropCriteriaFields) {
    var editor = this.filterEditor;
    if (!editor) return;

    var values = this._getFilterEditorValues();
    this.filterEditor.setValuesAsCriteria(values, true, dropExtraCriteria, dropCriteriaFields);
},


// _getFilterEditorValues returns the values to be shown in our filter editor (doesn't get the
// values FROM our filter editor - see getFilterEditorCriteria() for that).
// Called when the filter editor is first created, and used by updateFilterEditorValues to update
// the filterEditor when it is already showing.
// Note that developers can customize the display value via the 'updateFilterEditorValues()'
// stringMethod
_getFilterEditorValues : function () {
    var currentCriteria = isc.addProperties({}, this._filterValues);

    // Allow for a completely custom display of filterEditorValues by the developer
    if (this.updateFilterEditorValues != null) {

        currentCriteria = this.updateFilterEditorValues(currentCriteria, this.autoFetchTextMatchStyle);
    }



    // If we've never performed a filter, use our default filter values. Note that if we
    // explicitly filter with null or empty criteria we do NOT want to reset to defaults -
    // defaults are only used if we've never filtered this datasource. Handled by the fact
    // that setFilterValues() will always store an object, never null.
    if (currentCriteria == null) {
        currentCriteria = {};
        for (var i = 0; i < this.completeFields.length; i++) {
            currentCriteria[this.completeFields[i].name] = this.completeFields[i].defaultFilterValue;
        }
    }

    return currentCriteria;
},


// re-set the filterEditor's values to display the defaultFilterValues.
// This will be called if the defaultFilterValues change (can happen from setFields()) [and
// no filter has yet been performed], or if we change datasources so the current filter gets
// invalidated.

clearFilterValues : function () {
    this._filterValues = null;
    this.updateFilterEditor(true);
},

handleFilterEditorSubmit : function (criteria, context, callback) {
    // notification method fired when the user modifies the criteria in the filter editor
    // and hits the filter button / enter key.
    if (this.filterEditorSubmit != null && this.filterEditorSubmit(criteria) == false) return;
    this.filterData(criteria, callback, context);
},


// If we're doing an auto-fetch, apply each field's defaultFilterValue to the initialCriteria object
getInitialCriteria : function () {
    var initialCriteria = {},
        hasInitialFieldValue,
        fields = this.getFields(),
        undefined;
    for (var i = 0; i < fields.length; i++) {
        if (fields[i].defaultFilterValue !== undefined) {
            hasInitialFieldValue = true;
            var fieldName = this.getFieldName(fields[i]);
            initialCriteria[fieldName] = fields[i].defaultFilterValue;
        }
    }

    // allow explicitly specified initialCriteria to override the field level defaultFilterValues
    if (!hasInitialFieldValue) {
        initialCriteria = this.initialCriteria || this.getCriteria();
    } else {
        isc.addProperties(initialCriteria, this.initialCriteria || this.getCriteria());
    }

    return initialCriteria;

},


// Treat the filterEditor as a "special" peer -- keep it next to us in the page's z-order
_adjustSpecialPeers : function (newIndex) {
    if (this.filterEditor != null) this.filterEditor.setZIndex(newIndex-1);
    return this.Super("_adjustSpecialPeers", arguments);
},

//> @attr listGrid.allowFilterExpressions (boolean : null : IR)
// For use with +link{showFilterEditor}:true, allows simple search expressions to be entered
// into filter fields, as though +link{dynamicForm.allowExpressions} were true.
// <P>
// Can also be enabled or disabled on a field-by-field basis via
// +link{listGridField.filterEditorProperties}.
//
// @group advancedFilter
// @visibility external
//<


// --------------------------------------------------------------------------------------------
// Inline Editing
// --------------------------------------------------------------------------------------------



// Editing kickoff
// --------------------------------------------------------------------------------------------

//> @attr listGrid.canEditFieldAttribute (string : "canEdit" : IRA)
// @include dataBoundComponent.canEditFieldAttribute
// @visibility external
//<

//>    @method listGrid.canEditCell() (A)
// Can this cell be edited?
// <P>
// The default implementation of <code>canEditCell()</code> respects the various property
// settings affecting editability:
// <ul><li>+link{listGridField.canEdit,field.canEdit} can be set to disable editing
//          for a field</li>
//     <li>If the grid is bound to a dataSource, the +link{listGrid.canEditFieldAttribute} value on the
//         dataSource field may enable / disable editing</li>
//     <li>a record with the +link{listGrid.recordEditProperty,recordEditProperty} set to
//         false is not editable</li>
//     <li>disabled records are not editable</li>
// </ul>
// You can override this method to control editability on a cell-by-cell basis.  For example,
// if you had a grid that allows editing of "orders", and you had a field "shipDate" that is
// normally editable, but should not be editable if the order is already "complete", you might
// implement <code>canEditCell()</code> as follows:
// <P>
// <pre>
//   isc.ListGrid.create({
//       ...
//       canEditCell : function (rowNum, colNum) {
//           var record = this.getRecord(rowNum),
//               fieldName = this.getFieldName(colNum);
//           if (fieldName == "shipDate" &&
//               record.orderStatus == "complete")
//           {
//               return false;
//           }
//           // use default rules for all other fields
//           return this.Super("canEditCell", arguments);
//       }
//   });
// </pre>
// <P>
// Notes on providing custom implementations:
// <ul>
// <li> In order to allow complete control over editing, <code>canEditCell()</code> is called
// very frequently.  If you see delays on row to row navigation, check that your implementation
// is efficient
// <li> If you change the editability of a cell on the fly, for example, during
// +link{editorExit()} on another cell, call refreshCell() to show or hide the editor
// <li> If this ListGrid allows new records to be created, <code>canEditCell()</code> may be
// called when there is no record available, in which case getRecord() will return null.  The
// values input so far by the user are available via +link{getEditValues()}.
// </ul>
//
// @param rowNum (number) row number for the cell
// @param colNum (number) column number of the cell
// @return (boolean) Whether to allow editing this cell
//
// @group editing
// @visibility external
//<
canEditCell : function (rowNum, colNum) {
    // just return if passed bad colNum
    if (colNum < 0 || colNum >= this.fields.length) return false;

    if (this.allowRowSpanning && this.rowSpanEditMode == "first") {
        // Disallow edit of the cell if it's a spanning cell and the user is
        // attempting to edit something other than the first row in the cell
        var startRow = this.getCellStartRow(rowNum, colNum);
        if (startRow != rowNum) {
            return false;
        }
    }

    var cellRecord = this.getCellRecord(rowNum, colNum);
    // Note - we may have no cellRecord - this will occur if we're editing a new row
    if (cellRecord != null) {
        if (!this.recordIsEnabled(cellRecord, rowNum, colNum)) return false;

        // Suppress editing if we're showing an embedded editor or detailGrid for this row
        if (this._openRecord == cellRecord) return false;
    }

    // If the field is explicitly marked as disabled, disallow editing

    var field = this.getField(colNum);
    if (field && field.disabled) return false;

    if (field && field.type == "summary") return false;

    // otherwise check the cascaded canEdit property
    if (
        // Note: the isEditable() check is required as 'getCellBooleanProperty' will not return
        // false if the record's "_canEdit" property is set to 'true', but canEdit is unset at
        // the field and listGrid level, and we don't allow editing cells when canEdit is
        // not set at both the listGrid and field level.
        !this.isEditable() ||
        this.getCellBooleanProperty(
                "canEdit",
                rowNum,
                colNum,
                (cellRecord != null ? this.recordEditProperty : null)
        ) == false
    ) {
        return false;
    }

    // Disallow editing of formula fields by default
    if ((field.userFormula || field.userSummary) && field.canEdit == null) return false;
    return true;
},

//> @method listGrid.isEditable()   (A)
//  Looks at the 'canEdit' property on both the listGrid and the set of fields to
//  determine whether any cells in this listGrid can be edited.
//  @return (boolean)   true if any fields in this listGrid can be edited.
//<
_$true:"true",
isEditable : function () {
    // A field can be edited if:
    // listGrid.canEdit is true, and field.canEdit is not set to false
    // listGrid.canEdit is unset, and field.canEdit is set to true
    // No field can be edited if listGrid.canEdit is set to false.

    if (this.canEdit == false) return false;

    if (this.canEdit == true || this.canEdit == this._$true) {
        var fields = this.getFields() || [];
        for (var i = 0; i < fields.length; i++) {
            if (fields[i].canEdit != false) return true;
        }
        return false;
    } else {
        // this.canEdit is null
        var fields = this.getFields() || [];
        for (var i = 0; i < fields.length; i++) {
            if (fields[i].canEdit == true) return true;
        }
        return false;
    }
},

//> @method listGrid.setCanEdit()
// Updates the canEdit property for this listGrid at runtime.
// <P>
// If setting canEdit to false, any current editing is cancelled by calling
// +link{cancelEditing}.
//
// @param (boolean) desired value of canEdit for this grid
//<
setCanEdit : function (canEdit) {
    if (canEdit == false) {
        if (this.getEditRow() != null) this.cancelEditing(isc.ListGrid.PROGRAMMATIC);
        this.canEdit = false;
    } else {
        this.canEdit = canEdit;
    }
},

//> @method listGrid.setFieldCanEdit()
//  Updates the canEdit property for some field at runtime.
//  @param  (boolean)   desired value of canEdit for this field
//<
setFieldCanEdit : function (field, canEdit) {

    if (isc.isA.String(field)) field = this.getField(field);
    if (field == null || !this.completeFields.contains(field) || field.canEdit == canEdit) return;

    field.canEdit = canEdit;
    if (this._editorShowing) {
        var editRow = this.getEditRow(),
            fieldName = field[this.fieldIdProperty],
            colNum = this.getColNum(field);

        // If we're editing by cell and making the current edit field non editable, kill the
        // current edit
        if (this.editByCell) {
            if (!canEdit && colNum == this.getEditCol()) {
                this.cancelEditing(isc.ListGrid.PROGRAMMATIC);
            }

        // If we're showing editors for the entire row, we need to refresh the appropriate
        // field in the edit row to display / hide the editor
        } else if (colNum >= 0) {

            // If we're hiding the current edit field, we want to shift focus to the nearest
            // edit field instead.
            // If there isn't one, just cancel the edit.
            if (!canEdit && colNum == this.getEditCol()) {

                var currentEditItem = this._editRowForm.getItem(fieldName),
                    fieldHasFocus = currentEditItem.hasFocus;

                // Try to put focus in an adjacent field - say the previous one
                // (try going backwards first, then forwards)
                var newEditCell = this.findNextEditCell(editRow, colNum, -1,
                                                        true, false, false, true);

                if (newEditCell == null || newEditCell[0] != editRow)
                    newEditCell = this.findNextEditCell(editRow, colNum, 1,
                                                        true, false, false, true);

                // If there isn't another editable cell in this row, just cancel the edit
                if (newEditCell == null || newEditCell[0] != editRow) {
                    this.cancelEditing(isc.ListGrid.PROGRAMMATIC);
                    return;
                }
                // startEditing the new cell. This will fire the editorExit handler on the
                // previous cell and save out the value if appropriate.
                // Note: don't focus in the new cell unless focus was already in the cell being
                // made un-editable.
                this.startEditing(newEditCell[0], newEditCell[1], !fieldHasFocus);
            }
            // Refresh the cell to actually display / hide the edit form item.
            this.refreshCell(this.getEditRow(), colNum);
        }
    }
},



//>    @method listGrid.handleEditCellEvent()    (A)
// Handle an 'editCell' event - typically a click or double click on an editable ListGrid.
// Verifies that the cell passed in is a valid edit candidate before falling through to
// startEditing().
//
//    @group    editing
//
//    @param    rowNum      (number)    Row number of the cell to edit.
//    @param    colNum      (number)    Column number of the cell to edit.
//  @param  event       (editCompletionEvent)   How was this edit event triggered. If
//                                      we shift focus to a new edit cell this event will be
//                                      passed to the editorExit handler(s) of the previous row.
//                                      Expected vals are "click", "doubleClick" or "focus"
//                                      (or null).
//  @param [newValue] (any) optional new edit value for the cell
//    @return    (boolean)    true if we are editing the cell, false if not editing for some reason
//<
handleEditCellEvent : function (rowNum, colNum, event, newValue) {
    // set a flag to notify us that we started this 'edit session' on keyPress
    // This is required for 'moveEditorOnArrow' behavior, which only applies to edit sessions
    // started on keypress
    // We can't use the _editorCursorAtEnd flag as that won't get set unless the value was
    // modified, and we want the moveEditorOnArrow behavior even if the user started editing
    // from an f2 keypress...
    // This flag will be cleared
    // - here if the user double clicks (etc) another cell
    // - by cellEditEnd unless the event is keyboard navigation to another cell
    if (event == this._$keyPress) this._editSessionFromKeyPress = true;
    else delete this._editSessionFromKeyPress

    // if they're trying to edit an invalid cell, return false
    if (rowNum < 0 || colNum < 0) return false;

    // if passed a logical cell that is part of a rowSpan, adjust the rowNum so we edit
    // the first spanned row
    if (this.allowRowSpanning) {
        rowNum = this.getCellStartRow(rowNum, colNum);
    }

    // can we actually edit that cell?  If not, bail.
    if (this.editByCell) {
         if (!this.canEditCell(rowNum, colNum)) return false;
    } else {
        // If the user double-clicks on a non-editable field and editByCell is
        // false assume they want to start editing the row and put focus into the closest
        // editable cell in that row.
        var editCell = this.findNextEditCell(rowNum, colNum, -1, true, true, false, true);
        if (editCell == null || editCell[0] != rowNum)
            editCell = this.findNextEditCell(rowNum, colNum, 1, true, false, false, true);

        if (editCell == null || editCell[0] != rowNum) return false;
        // Update the colNum to reflect the closest editable cell to the one clicked if the
        // row is editable.
        colNum = editCell[1];
    }
    var undef;
    if (newValue !== undef) {
        this.setEditValue(rowNum,colNum, newValue);
    }

    // startEditing will save out the value in the previous edit cell if necessary...
    return this.startEditing(rowNum, colNum, null, event);
},

// Show/Hide Inline Editor
// --------------------------------------------------------------------------------------------

//>    @method listGrid.startEditing()    (A)
// Start inline editing at the provided coordinates.
// <p>
// Invoked when a cell is editable and the <code>editEvent</code> occurs on that cell.  Can
// also be invoked explicitly.
// <P>
// If this method is called while editing is already in progress, the value from the current
// editCell will either be stored locally as a temporary edit value, or saved via 'saveEdits()'
// depending on <code>this.saveByCell</code>, and the position of the new edit cell.<br>
// Will update the UI to show the editor for the new cell, and put focus in it unless
// explicitly suppressed by the optional <code>suppressFocus</code> parameter.
//
//    @group    editing
//
//    @param    [rowNum]      (Integer)    Row number of the cell to edit.  Defaults to first
//                                  editable row
//    @param    [colNum]      (Integer)    Column number of the cell to edit.  Defaults to first
//                                  editable column
//  @param  [suppressFocus] (Boolean)   If passed this parameter suppresses the default
//                                  behavior of focusing in the edit form item when
//                                  the editor is shown.
//    @return    (Boolean)    true if we are editing the cell, false if not editing for some reason
//
// @see canEditCell()
// @see editEvent
// @visibility external
//<

startEditing : function (rowNum, colNum, suppressFocus, eCe, suppressWarning) {


    if (this._keyboardEventsDisabled) {
        this.logInfo("call to startEditing() while keyboard events disabled (may be due to " +
                     "a component-mask blocking this grid). Ignoring", "gridEdit");
        return false;
    }

    // force editing on if it's not configured for any field, but a programmatic call is made
    if (!this.canEdit && !(this.completeFields || this.fields).getProperty("canEdit").or()) {
        this.canEdit = true;
    }

    // if setFields() has never been called, call it now.
    if (this.completeFields == null) this.setFields(this.fields);

    // Possibilities:
    // - This is an entirely new editing flow
    // - We are in an uncompleted editing flow
    // - We are currently showing an editor for another cell

    // What needs to happen?
    // - Showing a previous editor:
    //   *Save the new value from the edit cell locally into the temp set of editValues (for
    //     the appropriate record)
    //   *Hide the inline editor (unless it's another field in the same row and we're editing
    //     the whole row)
    // - Update edit info for this edit:
    //   *If we have no editFlowID, set up a new one
    //   *If there are no editValues stored for this record, store those values
    //   *Update the current editRowNum and editColNum, so we know which field is being edited

    // - show the editor:
    //   *Call 'showInlineEditor' to show the editor.  This will draw a new editor if required,
    //       or just focus in the appropriate field of the existing editor.

    // default to editable row / col
    var noRow = (rowNum == null),
        noCol = (colNum == null);
    if (noRow || noCol) {
        var testRow = (noRow ? 0 : rowNum),
            testCol = (noCol ? 0 : colNum);
        var newCell = this.findNextEditCell(testRow, testCol, 1, noCol, true);
        if (newCell == null) {
            this.logInfo("startEditing() passed bad cell coordinates:" + [rowNum, colNum],
                "gridEdit");
        } else {
            this.logInfo("startEditing() using derived coordinates:"+ newCell, "gridEdit");
            rowNum = newCell[0];
            colNum = newCell[1];
        }
    }

    // Legal coordinates are
    // - any rowNum within the dataSet
    // - any rowNum for which we already have editValues (even if the record has not been saved)
    // - one rowNum past our last row (== this.getTotalRows()). In this case we'll be creating
    //   a new row on the end of the list.
    // Bail if the coordinates are not legal.
    if (rowNum == null || rowNum < 0 || rowNum > this.getTotalRows()) {
        //>DEBUG
        // SuppressWarning param passed in when alwaysShowEditors is true and we blindly call
        // 'startEditing()' with no coordinates.
        // In this case we expect to fail to start the edit if there's no data or no visible,
        // editable cells.
        if (!suppressWarning) {
            this.logWarn("startEditing() passed bad cell coordinates:" + [rowNum, colNum] +
                        ", can't edit" + this.getStackTrace(), "gridEdit");
        }
        //<DEBUG
        return false;
    }

    if (!this.canEditCell(rowNum, colNum)) {
        //>DEBUG
        this.logInfo("startEditing(): cell " + [rowNum, colNum] +
                     " is non editable. Returning.", "gridEdit");
        //<DEBUG
        return false;
    }

    // at this point we have a valid cell to start editing.
    // If we're showing an editor, allow 'changeEditCell()' to handle saving out the
    // previous cell value, etc. - otherwise just call _startEditing() to start the edit
    // process
    if (this._editorShowing) {
        this._changeEditCell((eCe || isc.ListGrid.PROGRAMMATIC),
                             this.getEditRow(), this.getEditCol(), rowNum, colNum);
    } else {

        // Punt it over to _startEditing to handle the actual editing
        this._startEditing(rowNum, colNum, suppressFocus);
    }

    // return true to indicate editing has begun

    return true;
},

isEditingCell : function (rowNum, colNum) {
    return this._editorShowing && this.getEditRow() == rowNum && this.canEditCell(rowNum, colNum) &&
           (!this.editByCell || this.getEditCol() == colNum);
},

// _changeEditCell()
// Internal method used by both 'startEditing' and 'cellEditEnd' to complete editing one cell
// and start editing another.
// This method will fire user event / change handlers, and save out the edit values if
// appropriate.
// Falls through to _startEditing() to handle updating the display (hides this editor and shows
// the new one), and setting up the editValues for the new edit row.
_changeEditCell : function (editCompletionEvent, currentRowNum,
                            currentColNum, newRowNum, newColNum)
{

    // Note that the getEditValue() method will automatically pick up the current value
    // of the form item and store it in the edit-values
    // Note - this will fire the 'editorChange()' handler if the value has changed.
    var newValue = this.getEditValue(currentRowNum, currentColNum);

    // if we never fired 'editorEnter' avoid firing editor exit.

    var fieldName = this.getFieldName(currentColNum),
        editForm = this._editRowForm,
        editItem = editForm ? editForm.getItem(fieldName) : null,
        shouldFireEditorExit = editItem ? !editItem._cellEnterOnFocus : true
    ;
    if (editItem) {
        // If the edit item never got focused, the 'enterOnFocus' tags will still be present
        // just clean these up since we'll reset them if necessary (if focus goes back to the row)
        delete editItem._cellEnterOnFocus;
        delete editItem._rowEnterOnFocus;


        if (this._shouldParkFocus(editItem, currentRowNum, newRowNum, newColNum)) {
            this._parkFocus(editItem, currentColNum);
        }
    }

    // determine whether we need to validate or save on this cell transition
    // Use _getEditValues() - we already updated the editValue if necessary
    var leavingRow = (newRowNum != currentRowNum),
        newValues = this._getEditValues(currentRowNum, currentColNum);

    // Fire any developer defined handlers to fire when the user attempts to exit the edit cell
    // Stop if 'editorExit' handlers returned false.
    if (shouldFireEditorExit) {
        var editKilled = !this._handleEditorExit(editCompletionEvent,
                                                 currentRowNum, currentColNum, newValue);
        if (leavingRow && !editKilled) {
            editKilled = !this._handleRowEditorExit(editCompletionEvent, currentRowNum, newValues);
        }

        if (editKilled) {
            // If the editorExit handlers didn't already cancel this edit, or start a new one, force
            // focus back into the current edit field.
            // This is required as focus may not be in this form field

            if (editCompletionEvent == isc.ListGrid.EDIT_FIELD_CHANGE) {
                var newFieldName = this.getFieldName(newColNum);
                if (editItem && editForm.getItem(newFieldName).hasFocus) {
                    editItem.focusInItem();
                }
            }
            return false;
        }
    }

    var saveNow = (this.autoSaveEdits &&
                    ((leavingRow &&
                     this.shouldSaveOnRowExit(currentRowNum, currentColNum, editCompletionEvent)) ||
                     this.shouldSaveOnCellExit(currentRowNum, currentColNum, editCompletionEvent)));

    // The 'neverValidate' property effectively disables validation for form items.
    // Otherwise:
    // - If we're saving, we avoid validating here, since saving will auto-validate for us.
    // - otherwise perform cell or row validation based on this.validateByCell / this.autoValidate

    if (!saveNow && this._validationEnabled()) {
        var validationFailed,
            validateRow = leavingRow &&
                          this.shouldValidateByRow(currentRowNum, currentColNum, editCompletionEvent);
        // Note that if we're working with cellRecords we call validateCell() directly
        // if we should validate the row
        if (validateRow && !this.usingCellRecords) {
            validationFailed = !this.validateRow(currentRowNum);
        // If we're not validating the entire row, determine whether we should validate the
        // cell individually
        } else {
            if (validateRow ||
                this.shouldValidateByCell(currentRowNum, currentColNum, editCompletionEvent))
            {
                validationFailed = !this.validateCell(currentRowNum, currentColNum);
            }
        }

        // Suppress navigation if validation failed. No need to show error to user - already
        // handled by the validation methods.
        if (this.stopOnErrors && validationFailed) return false;
    }

    // At this point the old editor is still showing, but values have been updated, and
    // handlers fired.  Fall through to _startEditing to handle hiding this editor and showing
    // the new one.

    if (saveNow) {
        return this._saveAndStartEditing(newRowNum, newColNum, editCompletionEvent);
    } else {
        // proceed immediately to next cell
        this._startEditing(newRowNum, newColNum);
    }

    // If the user changed the grouped value of an edit row, regroup.
    // Exception - if we're saving, locally, we will have already saved at this point, which
    // already handles regrouping.
    if (leavingRow && this.isGrouped && (!saveNow || !this.shouldSaveLocally())) {
        this._updateGroupForEditValueChange(currentRowNum);
    }
},

// If the user edits a grouped tree, and changes a value within the groupBy field we
// may need to regroup.
// Determine this from rowNum by looking at the current editValues / record values for the
// groupBy field[s] and the current position in the group tree.
//
// We call this when the user is done editing a row, so from
// - user moving from one edit row to another - _changeEditCell()
// - editor being dismissed - saveAndHideEditor()
//   [NB: that method doesn't actually save if autoSaveEdits is false]
// We do not call this if either of these actions tripped a local save since that
// occurs synchronously and handles regrouping automatically.


_updateGroupForEditValueChange : function (rowNum) {
    if (!this.isGrouped || !isc.isA.Tree(this.data)) return;

    var node = this.data.get(rowNum),
        groupNode = node,
        shouldRegroup = false;
    // passed a bad rowNum
    if (node == null) return;

    var groupFields = this.getGroupByFields();

    if (!isc.isAn.Array(groupFields)) groupFields = [groupFields];
    var curVals = this.getEditedRecord(rowNum);

    for (var i = groupFields.length-1; i >=0; i--) {
         var fieldName = groupFields[i],
            groupNode = this.data.getParent(groupNode);

        if (groupNode == null || groupNode.groupName != fieldName) {
            this.logWarn("error updating group for edit value change - unexpected group " +
                "tree structure. Regrouping.");
            shouldRegroup = true;
            break;
        }

        var field = this.getUnderlyingField(fieldName);
        if (!this.fieldValuesAreEqual(field, node[fieldName], curVals[fieldName])) {
            shouldRegroup = true;
            break;
        }
    }

    if (shouldRegroup) {
        this._incrementalRegroup(null, node, curVals);
        this._remapEditRows();
        this.markForRedraw();
    }
    return shouldRegroup;
},


_shouldParkFocus : function (editItem, currentRowNum, newRowNum, newColNum) {
    if (!isc.Browser.isIE) return false;

    // If current focus is in a non-text-based item we don't need to park focus - selection
    // will update as appropriate.
    var textBasedFocusItem = (isc.isA.PopUpTextAreaItem(editItem) &&
                              // in a difficult-to-reproduce case, editItem._popupForm can end up
                              // undefined when hiding columns while editing.  This check
                              // prevents a JS error, but may still cause focus to skip
                              editItem._popupForm && editItem._popUpForm.hasFocus) ||
                             (editItem.hasFocus && isc.FormItem._textBasedItem(editItem));
    //if (!textBasedFocusItem) return false;

    // If we will not be removing the current item from the DOM, we don't need to park focus.

    if (!this.editByCell && newRowNum == currentRowNum) return false;

    // If the new item is text-based - no need to park focus
    var newEditorType =
        this.getEditorType(this.getField(newColNum), this.getCellRecord(newRowNum, newColNum));
    return !(newEditorType == null || isc.FormItem._textBasedItem(newEditorType, true));

},

// Internal method to unconditionally start editing a rowNum / colNum.
// This method will
//  - hide the current editor (if appropriate)
//  - set up edit values for the new cell
//  - show the editor for the new cell (and focus unless suppressFocus param passed)
// o Does not check for validity of rowNum / colNum
// o Does not save / modify pending edit values for some other cell/row
_startEditing : function (rowNum, colNum, suppressFocus) {


    if (rowNum == "delayed") {
        // we're firing from a timer for a delayed edit

        // another call to startEditing happened while we were waiting for the timer to fire,
        // and its params took precedence
        var params = this._delayedStartEditing;
        if (params == null) return;

        rowNum = params[0];
        colNum = params[1];
        suppressFocus = params[2];



    } else if (this.isDrawn() &&
                (!this.body.readyToRedraw() ||
                 (this.frozenBody && !this.frozenBody.readyToRedraw())))
    {
        // set a timer if we have not already set one, otherwise, just update the parameters
        // for the delayed edit
        if (!this._delayedStartEditing) {
            this.delayCall("_startEditing", ['delayed'],0);
        }
        this._delayedStartEditing = [rowNum, colNum, suppressFocus];

        return;
    }

    // we're starting editing now, we don't need the delayed edit params (the current
    // startEditing invocation overrides them)
    delete this._delayedStartEditing;

    // Remember which cell we're currently editing as the last "keyboard hilite" cell.
    // This ensures that if the user hides the editor, then re-displays it, editing
    // will recommence at the same spot.
    if (!suppressFocus) {
        this._lastKeyboardHiliteRow = rowNum;
        this._lastKeyboardHiliteCol = colNum;
        this._lastKeyboardHiliteBody = this.getFieldBody(colNum);
        // If we're selecting on edit, also update the "lastSelected" cell.
        // This can also be used in determining the "focusCell"

        if (this.selectOnEdit && this._lastKeyboardHiliteBody != null) {
            this._lastSelectedBody = this._lastKeyboardHiliteBody;
            this._lastSelectedBody._lastSelectedRow = rowNum;
            this._lastSelectedBody._lastSelectedCol = colNum;
        }
    }


    var changingRow = this.getEditRow() != rowNum;

    // On a call to startEditing the current editRow / col, just put focus into the
    // field editor in question

    if (!changingRow && !suppressFocus && (this.getEditCol() == colNum)) {
        this.getEditForm().focusInItem(this.getEditorName(rowNum, colNum));
        return;
    }

    //>DEBUG
    this.logInfo("Starting editing at row " + rowNum + ", colNum " + colNum, "gridEdit");
    //<DEBUG

    // if we currently have an embedded editor showing, dismiss it
    if (this._openRecord != null) this.closeRecord();
    if (this._editorShowing) {
        // if we're changing rows, or only editing one cell at a time, hide the current editor,
        // as it has no overlap with the old editor
        if (this.editByCell || changingRow) {

            // hide the editor (but don't focus back in the body), and don't hide the CM as
            // we're about to show another editor.

            this.hideInlineEditor(false, true);
        }
    }



    // We're in a new edit flow if
    // - this._editingFlowID is null
    // - saveByCell is false
    // - we're editing a cell in a different row.
    // Otherwise this method is just showing the editor for a different cell

    var newEditFlow = (this._editingFlowID == null)  ||
                      changingRow ||
                      this.saveByCell;

    if (newEditFlow) {
        this._editingFlowID = this._getNextEditFlowID();
    }

    //this.logWarn("about to start editing, editValues for row: " + rowNum + " are now: " +
    //             this.echo(this.getEditValues(rowNum)));
    var record = this.getCellRecord(rowNum, colNum);
    if (record == Array.LOADING) {
        // DO not edit temporary 'loading' records
        return true;
    }

    // Set up initial empty edit vals

    var displayNewValues = this._editorShowing && (this.getEditRow() == rowNum)
    this.initializeEditValues(rowNum, colNum, displayNewValues, true);

    // At this point - we are definitely going to start editing the new row, and the editValues
    // have been set up.
    // If 'selectOnEdit' is true, select the row.

    if (this.selectOnEdit && record != null) this.selectRecordForEdit(record, colNum);


    // ModalEditing (and edit event 'click') - in this case we show a click mask so won't
    // update rollovers when the user moves over other rows in the grid.
    // If they then click another record we'll start editing there, but never have cleared the
    // current 'over' row.
    // This can lead to the over styling getting left around until editing is complete.
    // Resolve this by explicitly clearing the hilite here:
    if (this.modalEditing) this.clearLastHilite();

    // If this is a new record, and 'addNewBeforeEditing' is true, we want to create the new
    // edit record BEFORE we start editing it - we do this via the standard 'saveEdits()'
    // method - this will save out the newly created editValues.
    // The save, which may be asynchronous, may modify the values of the saved record, setting
    // up default field values, etc.  We want to reflect these changes in the edit data for
    // the record.
    // Handle this by having a method _updateNewEditRowValues() update the edit values for the
    // row with the values taken from the record.
    // We continue to show the editor on the newly created row, either now, or if wait for save
    // is true, after the save occurs.
    if (record == null && this.addNewBeforeEditing) {
        // Hang onto the editRow / col, so we know what cell we're currently editing
        // This is usually done in 'showInlineEditor' but we need this for the saveEdits call
        // in this case
        this._editRowNum = rowNum;
        this._editColNum = colNum;
        var waitForSave = this.shouldWaitForSave();


        var callback = "this._updateNewEditRowValues(" + waitForSave + "," + suppressFocus + ")" ;
        this.saveEdits(isc.ListGrid.PROGRAMMATIC, callback);
        if (waitForSave) return;
        else {
            // The 'saveEdits()' call above may have changed the edit row's position.
            rowNum = this._editRowNum;
            colNum = this._editColNum;
        }
    }
    // - show the editor:
    //      - if the editor is showing for the same row, this will just focus
    //      - if another row, or editByCell, this will update the edit form, and show it in
    //        the right place
    // Note that 3rd param is always true - we'll be calling showInlineEditor to show the
    // same cell in this method.
    this.showInlineEditor(rowNum, colNum, true, changingRow, suppressFocus);

    return true;

},

// Select the record about to be edited
selectRecordForEdit : function (record, colNum) {
    // perf: avoid updating the row we're about to draw editors into anyway

    if (!this.editByCell) record._ignoreStyleUpdates = true;

    if (this.canSelectCells) {
        var cell = this.getRecordCellIndex(record, colNum);
        this.selection.selectSingleCell(cell[0],cell[1]);
        this.fireSelectionUpdated();
    } else if (this.selection != null && (!this.selection.isSelected(record) ||
                                           this.selection.multipleSelected()))
    {
        // we want a selection similar to select on mouse down: If we're using simple selection
        // select the record in addition to whatever else is selected - otherwise do a
        // single selection
        if (this.selectionType == isc.Selection.NONE) {
            // shouldn't see this as we disable selectOnEdit when selectionType is none
            this.logInfo("selectOnEdit is true, but this.selectionType is set to 'none'." +
                " Unable to perform a selection on edit.", "gridEdit");

        } else if (this.selectionType == isc.Selection.SIMPLE) this.selection.select(record);
        else this.selection.selectSingle(record);
        this.fireSelectionUpdated();
    }
    delete record._ignoreStyleUpdates;

},


// For text-based editors, update selection on focus
// The behavior we want is:
// - if the user started editing via editOnKeyPress (they started typing in the cell), put focus
//   at the end of the value so they don't wipe out what they already typed
// - otherwise respect this.autoSelectEditors
// We achieve this via a temporary flag set when we start editing a cell via 'editOnKeyPress'
_updateEditorSelection : function (item) {
    // applies only to text items (and subclasses)
    if (!isc.isA.TextItem(item) && !isc.isA.TextAreaItem(item) &&
        !(isc.isA.DateItem(item) && item.useTextField)) return;

    // set up when form item.refocusAfterRedraw() fires.
    if (item._suppressGridTextSelection) return;

    var element = isc.isA.DateItem(item) ? item.dateTextField.getDataElement() : item.getDataElement();
    if (!element) return;
    if (this._editorCursorAtEnd || !this.autoSelectEditors) {
        var val = item.getDataElement().value || "";
        item.setSelectionRange(val.length, val.length);
        // Assertion: we only want this special behavior of putting the cursor at the end after
        // the first 'focus()' on the item the user typed in, so clear out the flag here
        delete this._editorCursorAtEnd;
    } else {
        item.selectValue();
    }
},

// Helper method to update the edit row values for the special case where we have saved out a
// new record before editing it due to 'addNewBeforeEditing'

_updateNewEditRowValues : function (showEditor, suppressFocus) {

    var rowNum = this._editRowNum,
        colNum = this._editColNum,
        record = this.getCellRecord(rowNum, colNum);

    if (record != null && record != "loading") {

        var values = this.getEditValues(rowNum, colNum);
        for (var i in record) {

            if (record[i] != null && values[i] == null) {
                values[i] = record[i];
            }
        }
    }

    if (showEditor) this.showInlineEditor(rowNum, colNum, true, true, suppressFocus);
    else this.updateEditRow(rowNum);
    // Note - no need to explicitly focus in the editor here - this will be handled by
    // showInlineEditor if we're showing a new editor - and if we're already showing the
    // editor we don't need to modify it's focus
},

//>    @method listGrid.editField() (A)
//
//  Start editing a specific field.  This will save the current edit if appropriate.
//
//    @group    editing
//
//    @param    fieldName   (string)    Field to start editing
//    @param    [rowNum]  (number)    Optional row to start editing - if null defaults to the current
//                              edit row.
//    @return    (boolean)    true if we are editing the cell, false if not editing for some reason
//
// @see canEditCell()
// @see editEvent
//<
editField : function (fieldName, rowNum) {
    // If this grid has not yet been drawn, this.fields may not have been set up yet
    if (this.completeFields == null) this.setFields(this.fields);

    var colNum;
    if (isc.isA.Number(fieldName)) colNum = fieldName;
    else colNum = this.fields.findIndex(this.fieldIdProperty, fieldName);
    if (rowNum == null) {
        rowNum = this.getEditRow();
        // It's legal to pass in no rowNum param, but if we're not already editing a row,
        // bail with a warning.
        if (rowNum == null) {
            this.logWarn("editField(): unable to determine which row to edit - returning.",
                        "gridEdit");
            return;
        }
    }
    // Fall through to startEditing() to handle performing the edit.
    return this.startEditing(rowNum, colNum);
},

// create or update the editForm to reflect the editable and visible fields around the given
// coordinate, and show editors.
// Focuses in the target cell unless suppressed.
// internal: assumes we are logically set up to edit this row (editValues created)
// Application developers would call 'startEditing()' instead of this method

// this method is also responsible for firing editorEnter handlers when focus goes to the
// new edit cell - handled by setting up a flag to be checked by the form item's focus()
// handler.
showInlineEditor : function (rowNum, colNum, newCell, newRow, suppressFocus) {

    // This method is called in the following circumstances:
    // - we need to re-set focus to the current edit cell (example: setFields, redraw of body, etc)
    // - we need to move focus to a new cell that is already showing (called from startEditing())
    // - The editor is currently hidden and we need to show it (and put focus into the
    //   appropriate cell).
    // It is not expected to be called when the editor is currently showing for another
    // row (or for editByCell grids, another cell) - this method should not have to handle
    // hiding the edit form just showing it (if necessary) and assiging focus.
    //
    // NOTE: if we're doing full-row editing and the editForm is already showing, we assume the
    // current edit field's value has already been updated / saved by 'startEditing' call
    // whenever appropriate.
    if (this._editorShowing) {
        // Catch the case where we're showing the edit form for another row
        if (rowNum != this.getEditRow() || (this.editByCell && colNum != this.getEditCol())) {
            this.logWarn("Unexpected call to 'showInlineEditor' during another edit " +
                         "- cancelling previous edit", "gridEdit");
            this.cancelEditing();
            this.startEditing(rowNum, colNum);
            return;
        }
    }
    // Set a flag to note that we're in the process of setting up the editor.
    // This will avoid infinite loops if showInlineEditor is called directly from
    // anything called by this method - EG: the redraw triggered by scrolling the
    // cell into view

    if (this._settingUpEditor && this._settingUpEditor[rowNum] == rowNum
        && this._settingUpEditor[colNum] == colNum)
    {
        return;
    }
    this._settingUpEditor = [rowNum,colNum];

    var suppressScroll = this.suppressEditScrollIntoView || suppressFocus;

    this.logDebug("showing inline editor at: " + [rowNum, colNum] +
                  ", will focus: " + !suppressFocus, "gridEdit");


    var scrollBeforeShowing = this.body && (!this.body.shouldShowAllColumns() || !this.body.showAllRows);
    if (scrollBeforeShowing && !suppressScroll) {
        // set scrollRedrawDelay to zero before scrolling into view. This ensures that the body will
        // be marked dirty instantly if a redraw is required, which in turn makes sure we set up the
        // correct set of form items
        var srd = this.body.scrollRedrawDelay;
        this.body.scrollRedrawDelay = 0;
        this.scrollToCell(rowNum, colNum, "left", "top");
        this.body.scrollRedrawDelay = srd;
    }

    // If we're showing the rollOver canvases, clear them now
    var refRollOverCanvas = this.currentRollOverCanvas || this.currentRollUnderCanvas;
    if (refRollOverCanvas != null && refRollOverCanvas._currentRowNum == rowNum) {
        this.updateRollOverCanvas(refRollOverCanvas._currentRowNum,
                                  refRollOverCanvas._currentColNum, true);
    }

    var forceRedraw;
    if (!this._editorShowing) {
        // create or update the editForm used to display editors for the fields.
        var rebuiltForm = this.makeEditForm(rowNum, colNum);
        if (this._alwaysShowEditors() && rebuiltForm) {
            // if the edit form items actually changed (rather than just having
            // new properties applied to them in place), we'll need a full redraw
            // since the inactiveHTML currently written into all other rows will
            // not be associated with the new set of live items
            this.logInfo("Edit Form rebuilt with alwaysShowEditors:true, requires full redraw",
                         "inactiveEditorHTML");
            forceRedraw = true;
        }
    }

    // Update the remembered editColNum
    this._editRowNum = rowNum;
    this._editColNum = colNum;

    // Also update the stored "lastRecordClicked".
    // This is used for keyboard navigation when the editor isn't showing.
    // Setting this will ensure that we don't mysteriously jump to whatever was previously
    // recorded as the last record clicked once the editor is dismissed and the user hits
    // the up or down arrow key

    this._lastRecordClicked = rowNum;

    // write the editor form items into the DOM
    this._showEditForm(rowNum, colNum, forceRedraw);

    // Clear the settingUpEditor flag here

    this._settingUpEditor = null;


    // Ensure the edit cell is visible in the viewport

    if (this.body && !suppressScroll) {
        var mustScroll;
        if (!scrollBeforeShowing) mustScroll = true;
        else {
            var body = this.body,
                rowTop = body.getRowTop(rowNum),
                rowHeight = body.getRowSize(rowNum),
                scrollTop = body.getScrollTop(),
                portHeight = body.getViewportHeight();
            mustScroll = (rowTop < scrollTop) || (rowTop + rowHeight > scrollTop + portHeight);
        }
        if (mustScroll){
            this.scrollToCell(rowNum, colNum, "left", "top");
        }
    }

    // Don't show the click mask, or focus in the form item if we're not drawn.
    if (!this.isDrawn()) {
        return;
    }
    this._showEditClickMask();
    var focusItemName = this.getEditorName(rowNum, colNum),
        focusItem = this._editRowForm.getItem(focusItemName);
    // focusItem should be present since we've just scrolled it into view, but perform
    // check for safety anyway
    if (focusItem == null) {
        this.logWarn("ListGrid showing inline editor. Unable to get a pointer to the edit " +
                        "form item for field:"+ focusItemName);
    } else {

        // If this is a shift to a new cell, we'll want to fire editorEnter.
        // If a new row, we'll want to fire rowEditorEnter.
        // [Otherwise this method could be a refresh / refocus of current edit cell]



        if (newCell) focusItem._cellEnterOnFocus = true;
        if (newRow) focusItem._rowEnterOnFocus = true;
        // focus in the field being edited if appropriate
        if (!suppressFocus) {

            if (isc.Browser.isMoz) {

                var handle = this.body.getClipHandle(),
                    beforeFocus = handle.scrollTop;
            }


            var lastEvent = isc.EH.lastEvent;
            var delayFocus = false;
            if (isc.Browser.isIE && lastEvent.eventType == isc.EH.MOUSE_DOWN) {
                var target = lastEvent.target;
                if (target != this._editRowForm) {
                    delayFocus = true;
                    var ciCanvii = this._editRowForm.getCanvasItemCanvii();
                    for (var i = 0; i < ciCanvii.length; i++) {
                        if (ciCanvii[i] == target || ciCanvii[i].contains(target)) {
                            delayFocus = false;
                            break;
                        }
                    }
                }
            }

            if (isc.Browser.isIE) {
                var focusParkForm = isc.ListGrid._focusParkForm;
                if (focusParkForm &&
                    (focusParkForm.hasFocus || focusParkForm.itemHasFocus())) delayFocus = true;
            }


            if (isc.Browser.isSafari && this.alwaysShowEditors) {
                delayFocus = true;
            }

            if (delayFocus) {
                this._delayedFocusEvent = isc.Timer.setTimeout(
                    this._editRowForm.getID() + ".focusInItem('" + focusItemName + "');",
                    0
                );

            } else {

                if (isc.Browser.isMoz && this.body.overflow == isc.Canvas.VISIBLE) {
                    this.adjustOverflow();
                }
                this._editRowForm.focusInItem(focusItemName);
            }
        }
    }
},

// Write the editor form items into the cells in the ListGrid body, by redraw or refresh
// Re-evaluates canEditCell(), so may hide editors or show new editors.
// Internal: purely a helper to showInlineEditor
_showEditForm : function (rowNum, colNum, forceRedraw) {
    var editorWasShowing = this._editorShowing;
    this._editorShowing = true;
    if (!this.isDrawn() || !this.body) return;

    // The edit form has been created, and we need to show its items in the DOM.
    // 3 cases to catch:
    //  - Some edit form items are already showing for the edit row:
    //     If editByCell is false, it's possible that the 'canEditCell' criteria have changed
    //     for some currently visible edit cells.
    //     Need to refresh any cells where this is the case to show / hide form items.
    //  - No edit form items are showing
    //    - if the edit row is beyond the end of the list, we need to redraw the body to
    //      display the new edit row.
    //    - Otherwise for each cell where canEditCell is true, we must refreshCell() to write
    //      the form item into the DOM.

    // If we're showing the editor for a new edit row at the end of the list that's not currently
    // in the DOM redraw the body
    // Note: if this._editorShowing was true at the start of this method, we know we're
    // editing a new cell in the current edit row (_startEditing will hide the inline editor
    // if we're editing a different row). In this case we only need to redraw the body if it's
    // already been marked as dirty.


    var newRow = this.isEmpty() ||
                 (!editorWasShowing &&
                    (rowNum >= this.data.getLength()) &&
                    (this.showNewRecordRow ||

                     (this.body.getTableElement(rowNum) == null)));

    var showInactiveEditors = this._alwaysShowEditors();

    // If we're showing embedded component(s) for the row force a redraw
    // This'll place them properly

    var record = this.getCellRecord(rowNum,colNum);
    if (record && this._hasEmbeddedComponents(record)) {

        //forceRedraw = true;
    }

    if (forceRedraw || newRow || this.body.isDirty() ||
        (this.frozenBody && this.frozenBody.isDirty()) ) {

        var body = this.bodyLayout ? this.bodyLayout : this.body;
        body.redraw("Showing editor");
        return;
    }

    // Drawing of edit items can trip a server fetch to pick up optionDataSource data
    // use a queue to minimize client-server transactions where possible

    var wasQueuing = isc.RPCManager && isc.RPCManager.startQueue();

    // otherwise, editor is showing for the same row
    if (this.editByCell) {
        // We need to refresh the entire row if
        // - frozenFields is non null -- this ensures the height of the row in both
        //   bodies matches
        // - we're using explicit tall vs normal base style -- ensure we switch to tall base
        //   style while the editor is showing
        var refreshRow = (this.frozenFields != null) || (this.baseStyle == null);
        // editing by cell - just refresh the edit cell to show the editor.
        if (refreshRow) this.refreshRow(rowNum);
        else this.refreshCell(rowNum, colNum);

    } else {
        // editing whole row - draw editors into the currently visible cells



        // use getDrawnFields() to retrieve the set of drawn fields (takes care of frozen fields and
        // incremental rendering)
        var fields = this.getDrawnFields();
        // Check the 'canEdit' status of each cell in the edit row and refresh to show (or hide)
        // editors as appropraite.

        for (var i = 0; i < fields.length; i++) {
            if (this.isCheckboxField(fields[i])) continue;

            var field = fields[i],
                fieldName = fields[i][this.fieldIdProperty],
                formItem = this._editRowForm.getItem(fieldName),
                colNum = formItem.colNum,

                formItemVisible = !!(editorWasShowing && formItem.isDrawn()),

                canEditCell = this.canEditCell(rowNum, colNum);
            if (formItemVisible != canEditCell) this.refreshCell(rowNum, colNum);
            // For cells we are not redrawing, we need to update the cellStyle if:
            // - This is the last over row - need to clear the current 'over' style since we
            //   will not be showing  rollovers for the edit row.
            // - ensure the row shows up in the 'selected' state if selectOnEdit is true
            else if (this.selectOnEdit || this.lastOverRow) {
                var body = (field.frozen && !this._suppressedFrozenFields) ? this.frozenBody : this.body;
                body._updateCellStyle(this.getCellRecord(rowNum, colNum), rowNum, colNum);
            }
        }
    }

    if (!wasQueuing) isc.RPCManager.sendQueue();

},

// show the clickmask to catch clicks outside the editors, which means we're done editing
// the row (and should save) if this.modalEditing is true.

_showEditClickMask : function () {

    //!DONTCOMBINE
    // only show the C.M. if this.modalEditing is true.
    if (!this.modalEditing) {
        // Note - if a hover is showing, or pending on the body, clear it now.

        if (this.canHover) this.stopHover();
        return;
    }

    // if the editRowForm CM is already up, no need to show it.
    if (!this._editRowForm.clickMaskUp()) {
        if (!this._editClickMaskFunction)
            this._editClickMaskFunction =
                isc._makeFunction(this.getID() + "._handleClickOutsideEditor()");

        var unmasked = [this._editRowForm];
        if (isc.ListGrid._focusParkForm != null) {
            unmasked.add(isc.ListGrid._focusParkForm);
        }

        this._editRowForm.showClickMask(this._editClickMaskFunction,
                                    // If this.stopOnErrors is true, always cancel the
                                    // 'mouseDown' that dismisses the clickMask.
                                    // [technically this should only be required if there
                                    //  actually *are* errors, but since errors may come
                                    //  back from an asynch save it's appropriate to always
                                    //  cancel here].
                                    (this.stopOnErrors ? isc.EH.SOFT_CANCEL : isc.EH.SOFT),
                                    // pass in the editForm to ensure that the form items
                                    // are not masked
                                    unmasked);
    }
},

//> @method ListGrid.stopHover()
// Notification that the user is no longer hovering over some cell. Hides the current hover canvas
// if one is showing.
// @visibility external
//<
// Implemented at the Canvas level. Gets called on mouseOut.

stopHover : function () {
    if (this._editorShowing && isc.EH.getTarget() == this.getEditForm()) return;
    return this.Super("stopHover", arguments);

},

// shouldWaitForSave()
// Should we block user interactions during save, or allow the user to keep editing.
// If this function returns true, we wait for a save to return successfully before moving to
// a new edit cell, or hiding the current editor.
// This depends on this.waitForSave, and this.stopOnErrors.
// Note that we can't really support stopOnErrors = true / waitForSave = false, as we will not
// know if we have validation errors from server logic until a save has completed (performing
// a server round trip), and stopOnErrors implies we want to leave the editor up (as well as
// alerting the error messages).
shouldWaitForSave : function () {
    if (this.stopOnErrors && !this.waitForSave) {
        var message =
            "Note: ListGrid initialized with 'waitForSave' false, and 'stopOnErrors' true." +
            " In this case user input will be be blocked during save, to allow server side " +
            " errors to be determined before the editor is hidden. Setting 'waitForSave' to true.";

        // Log this at the info level rather than the warning level unless waitForSave has
        // explicitly been set to 'false', as it is very likely to occur
        if (this.waitForSave == false) this.logWarn(message, "gridEdit");
        else this.logInfo(message, "gridEdit");

        // actually update 'waitForSave' -- this will avoid us showing this log repeatedly
        // when this method is run.
        // Note - if we want to support modifying these values on the fly
        // ("setStopOnErrors" / "setWaitForSave") we will have to take this modification into
        // account too.
        this.waitForSave = true;
    }


    return !!(this.waitForSave || this.stopOnErrors);
},

// Hide the editor(s) for the current edit row.  Internal

hideInlineEditor : function (focusInBody, suppressCMHide, shiftFocus) {
    // focusInBody is intended to restore focus to the body after hiding the (focused) editor
    // Therefore check for whether the body or the edit form currently has focus and only
    // refocus if so
    if (focusInBody) {
        if (!this.hasFocus && !(this.body && this.body.hasFocus) &&
            !(isc.ListGrid._focusParkForm && isc.ListGrid._focusParkForm.hasFocus))
        {
            var editForm = this.getEditForm();
            if (editForm == null) {
                focusInBody = false;
            } else if (!editForm.hasFocus) {
                // focus may be in a CanavsItem canvas [or descendant thereof]
                // force focus in body if the current focus target is a child of our
                // body.

                var focusCanvas = isc.EH.getFocusCanvas();
                if (!focusCanvas || !this.body || !this.body.contains(focusCanvas)) {
                    focusInBody = false;
                }
            }
        }
    }


    if (isc.Browser.isFirefox) {
        var editForm = this.getEditForm();
        if (editForm != null) {
            var item = editForm.getFocusItem();
            if (item != null && item.hasFocus) {
                var element = item._getCurrentFocusElement();
                if (element && element.blur) element.blur();
            }
        }
    }

    // - clear out the flags marking the editor as being visible
    // - If we're showing the editor for a new temporary edit row, who's values have been
    //   cleared, but we're still showing the row in the DOM, redraw the body to clear the row
    //   from the DOM
    // - otherwise call 'refreshCell()' for each cell currently showing a form item to clear
    //   it from the DOM.

    // If we aren't currently editing a row, no-op
    if (!this._editorShowing) return false;


    this._editorShowing = null;

    // clear _editorSelection flag - ensures we don't inappropriately refocus in the edit form
    // after redraw if we subsequently re show the editor.
    this._editorSelection = null;

    var editRow = this._editRowNum,
        editField = this._editColNum;
    this._editRowNum = this._editColNum = null;



    // ensure that the clickmask gets taken down after edit
    // We suppress this step when we're hiding an editor, then reshowing - for example on
    // ListGrid redraw / cell navigation.

    if (!suppressCMHide) this._editRowForm.hideClickMask();

    // At this point, if we have edit values for the row, but they match the underlying
    // data values, just drop them.

    if (this.getEditValues(editRow, editField) != null &&
        (!this._savingEdits || !this._savingEdits[this.getEditValuesID(editRow, editField)]) &&
        !this.recordHasChanges(editRow, editField, false) &&
        // recordHasChanges will return false for a row that has been submitted
        // (save not yet completed), without further edits. In this case we of course want
        // to retain the edit vals
        !this.hasSubmittedEditValues(editRow, editField) &&
        // clearing edit values drops removed flag. Unlikely to really hit this
        // case on a removed record since it's non interactive so won't show an editor, but
        // sanity check that the record isn't marked for removal before clearing edit vals
        !this.recordMarkedAsRemoved(editRow))
    {
        this.logInfo("hideInlineEditor for row with no edits - dropping edit values", "gridEdit");
        // Don't hang onto the empty edit values for the row
        this._clearEditValues(editRow, editField);
    }

    if (!this.body) return true;

    // update the visible cells.  Now that editRow/Field is unset, they'll revert to normal
    // display.  NOTE: don't bother updating if we're already slated to redraw the body

    var editForm = this._editRowForm;

    if (editForm.hasFocus) {
        editForm.blur();
    }
    if (isc.Browser.isIE) {
        var focusItem = editForm.getFocusSubItem(),
            unconfirmedBlur = isc.EH._unconfirmedBlur;
        if (editForm.hasFocus ||
            (unconfirmedBlur &&
                ((unconfirmedBlur == editForm) || (isc.EH._unconfirmedBlur.form == editForm)) ) )
        {

            focusItem.elementBlur();

            // Another artefact of IE's asynchronous focus handling behavior is that
            // if 'blur()' is called on a text item, then the item is cleared from the DOM
            // before onblur fires, and 'focus()' is called on another item in the DOM, when the
            // user hits a key, focus will be pulled from the new focus item for no good reason.
            // We work around this here by putting focus in the 'focus park form'.

            this._parkFocus(focusItem, editField);
        }
    }



    // If we're showing embedded component(s) for the row force a redraw
    // This'll place them properly

    var record = this.getCellRecord(editRow,editField),
        forceRedraw = false;
    if (record && this._hasEmbeddedComponents(record)) {
        forceRedraw = true;
    }

    if (!this.body.isDirty() && (!this.bodyLayout || !this.bodyLayout.isDirty()) &&
        !this.isDirty())
    {
        if (forceRedraw || editRow >= this.getTotalRows()) {
            var widget = this.bodyLayout || this.body;
            widget.markForRedraw("Editor Hidden");
        } else {
            if (this.editByCell) {
                var refreshRow = (this.frozenFields != null) || (this.baseStyle == null);

                if (refreshRow) this.refreshRow(editRow);
                else this.refreshCell(editRow, editField);

            }
            else this.refreshRow(editRow);

            // force a refresh of the group-summary row if one is showing.

            this.refreshGroupSummary(editRow);
        }
    }
    if (focusInBody) {

        if (shiftFocus != null) {
            // shiftFocus: This would come from a Tab/Shift+Tab keypress -- in this case move focus forward
            // to the next focusable widget.
            shiftFocus == 1 ? this.body.focusInNextTabElement() : this.body.focusInPreviousTabElement();
        } else {
            // return focus to the body
            // -- ensure we don't editOnFocus via the 'suppressEditOnFocus' flag
            //    (will get cleared out by _focusChanged on the body)

            this._suppressEditOnFocus = true;

            this.body.focus();
        }
    }
    return true;
},

_parkFocus : function (focusItem, editField) {


    if (isc.isA.TextItem(focusItem) || isc.isA.TextAreaItem(focusItem) ||
        isc.isA.PopUpTextAreaItem(focusItem))
    {
        var focusParkForm = isc.ListGrid._focusParkForm;

        // If the hidden text item we use for managing focus doesn't exist, create it
        // here.
        if (!focusParkForm) {
            focusParkForm = isc.ListGrid._focusParkForm = isc.DynamicForm.create({

                // Ensure that if this gets destroy'd we also clear up the pointer to it
                pointersToThis:[{object:isc.ListGrid, property:"_focusParkForm"}],
                getFocusParkItem : function () {
                    return this.getItem(0);
                },
                autoDraw:false,
                _redrawWithParent:false,
                ID:"_ListGrid_focusParkForm",
                _generated:true,
                selectOnFocus:true,
                tabIndex:-1,
                numCols:1,
                items:[
                    {name:"focusPark", type:"text", title:isc.nbsp,
                        // Suppress all key event handling - this will avoid the user from
                        // being able to tab out of this focus parking form (temporarily
                        // putting focus somewhere else on the page)
                        handleKeyPress:function(){return false;}
                    }
                ],
                width:1, height:1, overflow:isc.Canvas.HIDDEN,

                itemHasFocus : function () {
                    var item = this.getFocusParkItem();
                    if (item.hasFocus) return true;
                    if (isc.Browser.isIE && this.isDrawn() && this.isVisible() &&
                        (this.getActiveElement() == this.getFocusParkItem().getFocusElement()))
                            return true;
                    return false;
                },
                redraw : function () {
                    var forceRefocus = false;
                    //this.logWarn("redrawing fpf");
                    if (this.itemHasFocus()) forceRefocus = true;
                    this.Super("redraw", arguments);
                    if (forceRefocus) this.focusInItem(this.getFocusParkItem());
                },
                // If the clickMask gets shown / hidden while the focus is parked
                // don't manipulate focus / store as the current "masked focus target"
                // If we don't do this, we can get bugs whereby we 'park' focus while
                // the clickMask is up, then when the clickMask hides, focus gets put
                // back in the focus-park form rather than being left alone

                _ignoreClickMaskFocus:true
            })
        }

        // We'll position the form under the target cell
        // Avoids any unexpected native scrolling issues with focus change.
        if (focusParkForm.isVisible()) focusParkForm.hide();

        focusParkForm.moveTo(
            Math.max(this.body.getPageLeft(), this.getColumnPageLeft(editField)),
            this.getPageTop() + Math.min(
                ((this.showHeader ? this.getHeaderHeight() : 0) + this.getRowTop(this.getEditRow())
                        - this.body.getScrollTop()),
                this.getScrollHeight()-1
            )
        );

        focusParkForm.sendToBack();
        // focusParkForm must be visible to receive focus
        focusParkForm.show();

        // If we're showing the edit clickMask, unmask the focusParkForm wrt that so we don't
        // dismiss the editor when it receives focus

        if (this._editRowForm.clickMaskUp()) focusParkForm.unmask(this._editRowForm.getID());

        // Put focus into the text item.
        focusParkForm.getFocusParkItem().focusInItem();

        // Ensure the editRowForm is aware it doesn't have focus any more

        focusItem.form.hasFocus = false;
    }
},


// Create inline editing form and items
// --------------------------------------------------------------------------------------------

// create a DynamicForm for inline editing.  This form manages FormItems embedded in GR cells,
// but does not actually draw them.
// It's up to the calling function to handle displaying these edit form items in the DOM.
updateEditorItemsInPlace:true,
makeEditForm : function (rowNum, colNum) {

    var record = this.getCellRecord(rowNum, colNum),
        // get the values for the form

        values = this.getEditDisplayValues(rowNum, colNum),
        updateItemsInPlace
    ;
    // If we're already showing an edit form and the set of editable fields
    // hasn't changed we may be able to update the items in place (changing just
    // rowNum, colNum, record, valueMap, etc) rather than performing a full
    // setItems(...) with new items on the DynamicForm
    // If 'alwaysShowEditors' is true this is going to mean we can avoid a full
    // redraw, since the currently drawn inactiveEditorHTML for other records in
    // the grid will already point to the existing form items and have appropriate
    // 'inactiveEditorContext' set allowing us to respond to events properly
    if (this.updateEditorItemsInPlace && this._editRowForm != null) {
        updateItemsInPlace = true;

        var editFields = this.editByCell ? [this.getField(colNum)] : this.getDrawnFields();
        if (editFields) {
            if (editFields.length != this._editRowForm.getItems().length) {
                updateItemsInPlace = false;
            } else {
                for (var i = 0; i < editFields.length; i++) {
                    var editField = editFields[i],
                        fieldName = this.getEditorName(rowNum, editField),
                        liveItem = this._editRowForm.getItem(fieldName);

                    if (liveItem == null ||
                        (this.getEditorType(editField, values) !=
                         this._editRowForm.getEditorType(liveItem)))
                    {
                        updateItemsInPlace = false;
                        break;
                    }
                }
            }
        }
        if (updateItemsInPlace && editFields) {
            var widths = this.getDrawnFieldWidths(record, editFields);

            for (var i = 0; i < editFields.length; i++) {
                var editField = editFields[i],
                    editColNum = this.getColNum(editField),
                    fieldName = this.getEditorName(rowNum, editField),
                    liveItem = this._editRowForm.getItem(fieldName),
                    record = this.getRecord(rowNum),
                    editedRecord = this.getEditedRecord(rowNum),
                    props = this.getEditItem(editField, record, editedRecord,
                                            rowNum, editColNum, widths[i], true);
                liveItem.setProperties(props);
                liveItem._size = null;
                if (editField.frozen) {
                    liveItem.containerWidget = this.frozenBody;
                } else {
                    liveItem.containerWidget = this.body;
                }
            }

        } else {
            // get currently visible items
            var items = this.getEditRowItems(record, rowNum, colNum, this.editByCell);
            // just update the items array and current values if the form already exists
            //this.logWarn("rebuilding editRowForm");// + this.getStackTrace());
            this._editRowForm.setItems(items);
        }
        this._editRowForm.setValues(values);

    } else {
        var items = this.getEditRowItems(record, rowNum, colNum, this.editByCell);
        //this.logWarn("creating editRowForm..." + this.getStackTrace());
        // create the editForm.  Done once only per grid lifetime
        var properties = isc.addProperties({},
            this.editFormDefaults, {

            // keep track of the listGrid
            grid:this,

            dataPath:this.dataPath,
            // for AutoTest apis
            locatorParent:this,

            showErrorIcons:this.showErrorIcons,

            // Give the form the same tabIndex - this is not relevant in most ListGrids,
            // since the clickMask / keypress handling will prevent tabbing in and out of the
            // edit form, but we rely on this behavior in the RecordEditor class where the user
            // must be able to tab in and out of the edit row.
            tabIndex:this.getTabIndex(),

            // pass it this widget's datasource too
            dataSource:this.dataSource,

            autoComplete:this.autoComplete,
            uniqueMatch:this.uniqueMatch,
            // Avoid autoFocus - we explicitly focus and blur when appropriate.
            autoFocus:false,

            items:items,
            values:values

        }, this.editFormProperties);

        if (this.dateFormatter != null && properties.dateFormatter == null) {
            properties.dateFormatter = this.dateFormatter;
        }
        if (this.datetimeFormatter != null && properties.datetimeFormatter == null) {
            properties.datetimeFormatter = this.datetimeFormatter;
        }
        if (this.timeFormatter != null && properties.timeFormatter == null) {
            properties.timeFormatter = this.timeFormatter;
        }
        this._editRowForm = isc.DynamicForm.create(properties);

    }

    if (this.logIsDebugEnabled()) {
        this.logDebug("editRowForm created with values: " +
                      this.echo(this._editRowForm.getValues()), "gridEdit");
    }

    // The return value will indicate whether we actually rebuilt the form
    // (as opposed to just modifying the existing items in place)
    // We use this to determine whether a full redraw is required in the
    // alwaysShowEditors case
    return !updateItemsInPlace;
},

//> @method listGrid.getEditForm() [A]
// Method to retrieve the live edit form for an +link{listGrid.canEdit,editable} ListGrid.
// This is the automatically generated DynamicForm used to manage the per field edit items.
// <P>
// Note that this is an advanced method and developers should be aware of the following issues:
// <ul><li>The edit form is only present while the user is actually editing a record and this
// method will return null if no editor is currently showing.</li>
// <li>Live edit items may be retrieved by calling <code>getItem(<i>someFieldName</i>);</code>
// but items are only created for fields that are visible and being written out into the grid.
// This means with +link{showAllColumns} set to false an item may not be created until the
// user scrolls the column in question into view</li>
// <li>The editForm's values are managed by the ListGrid through the edit-values subsystem.
// If you want to change an edit value for a field, call +link{listGrid.setEditValue()} and the
// grid will handle updating the value in the live item if necessary. You should not need to call
// <code>setValue();</code> directly on the form or item and doing so will not always update the
// edit value for the grid.
// </li></ul>
// In general - bear in mind that this is an advanced usage and if there is an equivalent API
// available on the ListGrid it is always preferable to use that.
// @return (DynamicForm) the live edit form, or null if the grid is not currently showing any editors.
// @visibility internal
//<
// currently unexposed - getEditFormItem() is much more likely to be needed.
getEditForm : function () {
    return this._editorShowing ? this._editRowForm : null;
},


// These helpers are required to allow rowNum / colNum based edit values management
// rather than fieldName based valuesManagement.


//> @method listGrid.getEditFormItem() [A]
// Method to retrieve a live edit form item for an +link{listGrid.canEdit,editable} ListGrid.
// This is the automatically generated editor displayed in a cell while editing the grid.
// <P>
// Note that this is an advanced method and developers should be aware of the following issues:
// <ul><li>Edit form items are only present while a user is actually editing a cell. This method
// will return null if the user is not editing the grid or the field in question is not editable
// or not visible. Note that due to +link{showAllColumns,incremental rendering} columns which are
// not currently scrolled into view may be unrendered, in which case they may have no associated
// edit item until the user scrolls them into view.</li>
// <li>The items' values are managed by the ListGrid through the edit-values subsystem.
// If you want to change an edit value for a field, call +link{listGrid.setEditValue()} and the
// grid will handle updating the value in the live item if necessary. You should not need to call
// <code>setValue();</code> directly on the item and doing so will not always update the
// edit value for the grid.
// </li></ul>
// In general - bear in mind that this is an advanced usage and if there is an equivalent API
// available on the ListGrid it is always preferable to use that.
// @param field (String or Integer) fieldName or colNum to get the edit item for.
// @return (FormItem) the live edit item for the current edit row and specified field, or null if the grid is not currently showing any editors.
// @visibility external
//<
// NOTE: this differs from getEditItem() - it's retrieving an existent item in the edit form
// for a cell -- not retrieving the properties to create a form item object
getEditFormItem : function (colNum) {
    var editForm = this.getEditForm();
    if (!editForm) return null;
    // getEditorName() already handles being pased a fieldName or colNum
    var fieldName = this.getEditorName(this.getEditRow(), colNum),
        item = editForm.getItem(fieldName);
    // sanity check - if item.colNum != the colNum passed in assume the editorName is reused for
    // multiple fields
    if (!isc.isA.Number(colNum) || (item && item.colNum == colNum)) return item;
},

getEditFormValue : function (colNum) {
    var item = this.getEditFormItem(colNum);
    return (item ? item.getValue() : null);
},

// helper: sets edit form items to latest editValues (including reverting to original record
// values if editValues have been cleared)
// Called from setEditValues()
_updateEditItemValues : function () {
    if (!this._editRowForm) return;

    var rowNum = this.getEditRow(), colNum = this.getEditCol(),
        values = this.getEditDisplayValues(rowNum, colNum);
    this._editRowForm.setValues(values);
},


// Helper method to get all the values for the edit form at once
getEditDisplayValues : function (rowNum, colNum) {

    var editValues = this.getEditValues(rowNum, colNum),
        record = this.getCellRecord(rowNum, colNum),
        values = {};

    for (var fieldName in record) {
        values[fieldName] = record[fieldName];
    }
    for (var fieldName in editValues) {
        values[fieldName] = editValues[fieldName];
    }

    for (var fieldName in values) {
        var fieldNum = this.getFieldNum(fieldName);
        // call the edit formatter (if defined) for all visible cells
        if (fieldNum >=0 && this._formatEditorValue != null) {
            values[fieldName] = this._formatEditorValue(values[fieldName], record, rowNum, fieldNum);
        }
    }

    return values;
},

// Method to get the horizontal space available for the form items for each column
// (Note this is not a 1:1 mapping to form items, as not every field is editable, and we
// incrementally create form items for columns as they are rendered out)
// Overridden by TreeGrid to account for indentation in tree field.
getEditFormItemFieldWidths : function (record) {
    var widths = [];
    for (var i =0; i<this.fields.length; i++) {
        var colNum = this.getLocalFieldNum(i),
            field = this.fields[i],
            body = (!this._suppressedFrozenFields && field.frozen) ? this.frozenBody : this.body;
        widths[i] = body.getInnerColumnWidth(colNum);
    }
    return widths;
},

//>    @method listGrid.getEditorValueMap()  ([A])
//
//  Returns the valueMap to display for a field when it is displayed in the editor while
//  editing some record.<br>
//  Called when a user starts to edit a field, or whenever the field valueMap is updated via
//  a call to +link{listGrid.setValueMap()} or +link{listGrid.setEditorValueMap()}.
//  Default implementation will return the <code>field.editorValueMap</code> if specified, otherwise
//  <code>field.valueMap</code> - can be overridden to provide a
//  different specific valueMap for some field based on the record/field data.
//
//  @group  editing
//
//  @param  field   (ListGridField)    field definition field for which we need a valueMap
//  @param  values  (object)    Field values for record being edited. Note that this will include
//                              the current edit values for fields that have not yet been saved.
//                              May be null, if editing a new record.
//  @return         (ValueMap)  ValueMap for the edit field (or null if no valueMap required)
//  @visibility external
//<

//> @method listGridField.getEditorValueMap()
// Optional stringMethod to get a +link{listGridField.valueMap} for a specific field.
// If present this method will be called from +link{listGrid.getEditorValueMap()} and the resulting
// valueMap will be used instead of any static specified valueMap for the field.
// @param values (object) Field values for record being edited. Note that this will include
//                        the current edit values for fields that have not yet been saved.
//                        May be null, if editing a new record.
// @param field (ListGridField) pointer to the listGridField
// @param grid (ListGrid) pointer back to this ListGrid instance.
// @return (valueMap) ValueMap for the field (or null if no valueMap required)
// @visibility external
//<

getEditorValueMap : function (field, values) {
    if (field.getEditorValueMap != null) {

        isc.Func.replaceWithMethod(field, "getEditorValueMap", "values,field,grid");
        return field.getEditorValueMap(values,field,this)
    }
    if (field.editorValueMap != null) {
        return field.editorValueMap;
    }
    if (field.editorProperties && field.editorProperties.valueMap) {
        return field.editorProperties.valueMap;
    }
    return field.valueMap;
},


//> @method listGrid.getEditorValueIcons()
// Returns the valueIcons for a field when it is displayed in the editor while editing some
// record. Default implementation will return +link{ListGridField.editorValueIcons} if specified
// otherwise +link{ListGridField.valueIcons}
// @param field (object) field definition
// @param values (object) current edit values for the record
// @return (object) valueIcons for the editor
// @visibility external
// @group imageColumns
//<
getEditorValueIcons : function (field, values) {
    return  field.editorValueIcons || field.valueIcons;
},

//> @method listGrid.getEditorValueIconWidth()
// Returns the width for any valueIcon for a field when it is displayed in the editor
// while editing some record.<br>
// Returns +link{listGridField.editorValueIconWidth} if specified - otherwise
// +link{listGridField.valueIconWidth} or +link{listGridField.valueIconSize}
// @param field (object) field definition
// @return (number) width for the value icon to show in the editor
// @visibility internal
// @group imageColumns
//<
// Don't return listGrid.valueiconWidth / size - this is just a default and likely to break
// form items with standard valueIconSizes, such as checkboxes.
getEditorValueIconWidth : function (field) {
    if (field.editorValueIconWidth != null) return field.editorValueIconWidth;
    return (field.valueIconWidth != null ? field.valueIconWidth : field.valueIconSize);
},

//> @method listGrid.getEditorValueIconHeight()
// Returns the height for any valueIcon for a field when it is displayed in the editor
// while editing some record.<br>
// Returns +link{listGridField.editorValueIconHeight} if specified - otherwise
// +link{ListGridField.valueIconHeight} or +link{listGridField.valueIconSize}
// @param field (object) field definition
// @return (number) height for the value icon to show in the editor
// @visibility internal
// @group imageColumns
//<
getEditorValueIconHeight : function (field) {
    if (field.editorValueIconHeight != null) return field.editorValueIconHeight;
    return field.valueIconHeight != null ? field.valueIconHeight : field.valueIconSize;
},

//> @method listGrid.setEditorValueMap() ([A])
//
// Set a valueMap to display for this field while editing.<br>
// This method sets the +link{ListGridField.editorValueMap, field.editorValueMap} property -
// note that if  +link{ListGrid.getEditorValueMap()} has been overridden it may not make use
// of this property.
// @group editing
// @param fieldID   (object | number | field name)  Field object or identifier
// @param   map     (object)    ValueMap to apply to the field
// @visibility external
//<
setEditorValueMap : function (fieldID, map) {
    var fieldNum = this.getColNum(fieldID),
        field = this.getField(fieldID),
        fieldName = field[this.fieldIdProperty];
    field.editorValueMap = map;
    if (this._editorShowing) {
        var rowNum = this.getEditRow(),
            editRecord = this.getEditedRecord(rowNum, fieldNum);
        // Apply the valueMap to the edit form field - note that we retrieve it via the
        // getter method in case it has been overridden.
        this._editRowForm.setValueMap(fieldName, this.getEditorValueMap(field, editRecord));
    }

},

//>    @method listGrid.getEditorType()  ([A])
//
//  Returns the form item type (Class Name) to display for a field when it is displayed in the
//  editor while editing some record.<br>
//  Default implementation will return field.editorType if specified.
//  If not specified, the default form item for the appropriate data type will be displayed
//  - can be overridden to provide a different specific form item type for some field based on
//  the record/field data.
//
//  @group  editing
//
//  @param  field   (ListGridField)    field definition field for which we need a valueMap
//  @param  values  (object)    current edit values for the record (may be null, if editing a
//                              new record)
//  @return         (string)  form item type for the edit field
//  @visibility external
//<
getEditorType : function (field, values) {

    // determining type: editorProperties, being most specific, wins.  Otherwise
    // field.editorType, otherwise, you get the default editor picked
    // for field.type (which is the field's *data* type, not editor type).
    // NOTE: editorProps.type will always refer to the form item type, not the data type.
    // NOTE: "formItemType" is a legacy synonym of "editorType"
    var editorProperties = isc.addProperties({},field,field.editorProperties);

    // Use the static method on DynamicForm to get the editorType for this field.
    // Pass this ListGrid in as a parameter so the method can examine
    // this.longTextEditorThreshold and this.longTextEditorType.
    return isc.DynamicForm.getEditorType(editorProperties, this);
},

// getEditorProperties() - return a block of default properties for editor form items displayed
// while editing some field
// Overridden in RecordEditor.

getEditorProperties : function (editField, editedRecord, rowNum) {
    return isc.addProperties({}, this.editorProperties, editField.editorProperties);
},

//>    @method listGrid.getEditRowItems()  (IA)
//
//      Given a record to edit, return an appropriate array of dynamicForm item init blocks
//
//  @group  editing
//
//  @param  record  (object)    Record to be edited
//  @param  rowNum  (number)    index of the row being edited
//  @param  fieldNum (number)   index of the field on which the 'startEditing' occurred (typically
//                              used as a focus field)
//  @return         (array)     Array of DynamicForm item object instantiation blocks
// @visibility internal
//<
// Note: if editByCell is false, and we're incrementally rendering, we only create form items
// for the visible set of fields.

getEditRowItems : function (record, rowNum, fieldNum, singleCell) {
    var body = this.body;
    // if we haven't created the body yet, don't create any form items - they'll get set up
    // at draw, and updated at redraw.
    if (body == null) return [];

    // The set of fields for the form is basically this.fields, with some custom properties, such
    // as 'editorType'
    var firstEditable, lastEditable,
        widths = [],
        items = [];

    var editedRecord = this.getEditedRecord(rowNum, fieldNum);

    // create an array of the fields we want to work with
    var editFields;
    if (singleCell) editFields = [this.getField(fieldNum)]
    else editFields = this.getDrawnFields();

    widths = this.getDrawnFieldWidths(record, editFields);
    if (editFields != null) {
        for (var i = 0; i < editFields.length; i++) {

            // (Set up each item as a property-value map)


            var colNum = this.getColNum(editFields[i]);
            var item = this.getEditItem(editFields[i], record, editedRecord, rowNum, colNum, widths[i])
            if (item == null) continue;

            items[items.length] = item;
        }
    }
    return items;

},

getDrawnFields : function () {

    if (!this.body) return null;

    var drawnFields = [], body = this.body;
    var ff = this.frozenFields, fLeft = this.freezeStart();

    if (ff && fLeft) {
        // this assumes that we are not dealing with incremental column rendering of frozen
        // fields (should be true as not scrollable)
        drawnFields.addList(ff);
    }

    var firstVisible, lastVisible;

    if ((body._firstDrawnCol == null) || !body.isDrawn() || body.isDirty() || body._redrawing) {
        var drawnRange = body.getDrawArea();
        firstVisible = drawnRange[2];
        lastVisible = drawnRange[3];
    } else {
        firstVisible = body._firstDrawnCol;
        lastVisible = body._lastDrawnCol;
    }
    if (ff && fLeft) {
        firstVisible += ff.length;
        lastVisible += ff.length;
    }
    for (var i = firstVisible; i <= lastVisible; i++) {
        drawnFields.add(this.fields[i]);
    }

    if (ff && !fLeft) {
        drawnFields.addList(ff);
    }
    return drawnFields
},

getDrawnFieldWidths : function (record, fields) {
    // Allow the developer to pass in the fields to return widths for
    if (!fields) fields = this.getDrawnFields();

    if (!fields) return null;
    // apply the width from the fieldWidths array
    var completeWidths = this.getEditFormItemFieldWidths(record),
        widths = [];
    for (var i = 0; i < fields.length; i++) {
        widths[i] = completeWidths[fields[i].masterIndex];
    }
    return widths;
},

// getEditItem()
// returns an individual property block for edit-form form item.

// Helper methods to be applied to pop up text area items to specially process keydown and
// keypress events.  Fired in the scope of the pop up text area

_popUpTextAreaItemKeyPress : function (item, keyName, characterValue) {
    return this.grid.editorKeyPress(this, keyName, characterValue);
},
_popUpTextAreaItemKeyDown : function (item, keyName, characterValue) {
    return this.grid.editorKeyDown(this, keyName, characterValue);
},

// On focus in a pop up text area item, ensure that we have updated the current edit cell info.
// This handles the case where we got no elementFocus on the PUTA directly - happens if the
// PUTA has no focusable element itself.

_popUpTextAreaItemFocus : function () {
    var form = this.form, lg = this.grid,
        rowNum, colNum, fieldName, fieldChanged;
    if (lg._editorShowing) {
        rowNum = lg._editRowNum;
        // don't worry about editByCell case- for the pop up textareaitem to have got focus we
        // must have already shown it, meaning we must already know which field is being edited
        if (!lg.editByCell) {
            rowNum = lg._editRowNum;
            fieldName = this.getFieldName(),
            colNum = lg.fields.findIndex(lg.fieldIdProperty, fieldName);

            fieldChanged = (lg._editColNum != colNum);
            // If the user has clicked in another field in the edit form, fire editField on
            // the appropriate field
            if (fieldChanged) {
                // store the new edit cell
                lg.setNewEditCell(rowNum, colNum);
                // fire 'cellEditEnd' to save / validate before moving to the new cell
                lg.cellEditEnd(isc.ListGrid.EDIT_FIELD_CHANGE);

            }
        }
    }
},

_getPopUpTextAreaTop : function () {
    var grid = this.grid,
        fieldName = this.getFieldName(),
        rowNum = grid.getEditRow(),
        style = grid.getCellStyle(grid.getRecord(rowNum), rowNum, grid.getColNum(fieldName)),
        rowTop = grid.getRowPageTop(rowNum) +
                 isc.Element._getTopBorderSize(style) + isc.Element._getTopPadding(style);
    return rowTop;
},

_checkboxClick : function (a,b,c,d) {
    if (!this.hasFocus) {
        var lg = this.grid;
        lg.setNewEditCell(this.rowNum, this.colNum);
        lg.cellEditEnd(isc.ListGrid.EDIT_FIELD_CHANGE);
        // and force cellEnterOnFocus, so when the (delayed) focus
        // handler fires, we get a cellEnter
        this._cellEnterOnFocus = true;
    }

    return this.invokeSuper("CheckboxItem", "handleClick", a,b,c,d);
},



// handleEditorChanged()
// Fired when the changed() handler fires for any of our edit items.
// Allows us to perform validation on change
handleEditorChanged : function (item) {

    if (this._drawingItem == item) return;
    var fieldName = item.getFieldName(),
        field = this.getField(fieldName),
        validateOnChange;

    if (field && field.validateOnChange != null) validateOnChange = field.validateOnChange;
    else validateOnChange = this.validateOnChange;


    var rowNum = this.getEditRow();
    if (rowNum == null || rowNum != item.rowNum) return;

    if (validateOnChange) {

        // Note: we don't always update the editValue for the cell when the change handler
        // fires on the edit item - instead we usually update when the user moves to a new cell.
        // However, validateCell(), like most other APIs that interact with the editValues,
        // will update the edit value for the cell if it's stale before performing validateion.
        this.validateCell(rowNum, fieldName, false, true);

    // We support form items changing their value after they've lost focus - EG:
    // delayed "Tab" autoComplete for databound comboBoxItems.
    // In this case if we're validating or saving by cell we need to perform an update now
    // (as we won't get an editor exit event to react to).
    } else if (!item.hasFocus && !this.editByCell) {

        if (item.isDrawn() && item.isVisible()) {
            this.storeUpdatedEditorValue(false, item.colNum);

            if (this.saveByCell && this.autoSaveEdits) {
                // Essentially we're saving for the same reason as if the user had
                // taken focus from the field.
                var editCompletionEvent = isc.ListGrid.EDIT_FIELD_CHANGE;
                this.saveEdits(editCompletionEvent, null, rowNum, item.colNum);
            } else if (this.validateByCell) this.validateCell(rowNum, fieldName);

        }
    }

},

// Override for the (internal) handleChanged method so we can be notified when the
// user changes an edit cell's form item value.
_editorHandleChangedOverride : function (a,b,c,d) {

    this.invokeSuper(this.getClassName(), "handleChanged", a,b,c,d);

    if (!this.destroyed) this.grid.handleEditorChanged(this);

},

_editorGetAutoComplete : function () {
    var grid = this.grid;
    // This would imply something like a destroyed form item - so just ignore it.
    if (!grid) return null;

    var field = grid.getField(this.getFieldName());
    if (field.autoComplete != null) return field.autoComplete;
    if (grid.autoComplete != null) return grid.autoComplete;
    return this.Super("_getAutoCompleteSetting", arguments);
},


_timeEditorTypes:{time:true, TimeItem:true},
_$time:"time",
_dateEditorTypes:{date:true, DateItem:true},
_datetimeEditorTypes:{datetime:true, dateTime:true, DatetimeItem:true, DateTimeItem:true},
_$date:"date",
_$datetime:"datetime",

popUpTextAreaEditorTypes:{popUpTextArea:true, PopUpTextAreaItem:true},
_checkboxEditorTypes:{checkbox:true, CheckboxItem:true},
_$boolean:"boolean",
_$checkbox:"checkbox",
_$CycleItem:"CycleItem",
_selectEditorTypes:{select:true, SelectItem:true},


_commonEditorStringMethodsFromField:["change", "changed", "defaultDynamicValue"],
_commonEditorStringMethods:["change", "changed", "defaultDynamicValue",
                            "keyPress", "click",
                            // Not sure if showIf / enableIf would be set on editors - more
                            // likely to set canEdit:false for the cell
                            "showIf", "enableIf"],
// getEditItem()
// returns a config block for an item to be displayed in the edit form.
// 'updateOnly' parameter implies we're going to take the returned properties and
// apply them to an existing form item, so no need to include standard handlers
// or properties that wouldn't be applied to a live item such as editorType

getEditItem : function (editField, record, editedRecord, rowNum, colNum, width, updateOnly) {

    var item = {};

    item.width = width;
    // Hang some properties onto the form item so keypress handers (etc.) written onto
    // the form item can readily access details about the edit:
    item.record = record;
    item.rowNum = rowNum;

    item.colNum = colNum;

    item.getGlobalTabIndex = function () {
        if (!this.containerWidget) return this.Super("getGlobalTabIndex", arguments);
        return this.containerWidget.getTabIndex();
    };

    // the error icons will be written directly into the form item (so no need to
    // adjust the size of the edit item to account for them).

    // Set the "name" of the item
    var fieldName = this.getEditorName(rowNum, editField);
    item[this.fieldIdProperty] = fieldName;

    // Also copy the dataPath across if present
    var dataPath = this.getEditorName(rowNum, editField, true);
    if (dataPath && dataPath != fieldName) {
        item.dataPath = dataPath;
    }

    // copy the title (won't be visible but needed for ARIA)
    if (editField.title != null) item.title = editField.title;
    // Use the accessor function to get the valueMap for the item
    // This allows override of visible options on a per cell basis
    // Note that we pass in the edit values, rather than the record's saved values - we want
    // the valueMap to update as edits are performed

    item.valueMap = this.getEditorValueMap(editField, editedRecord);
    // if "multiple" is set on the field, apply it directly to the item.
    // Of course if multiple were set on the DataSource field it'd get picked up by the item
    // automatically.
    if (editField.multiple != null) item.multiple = editField.multiple;

    // If field.canEdit is explicitly set at the component level, set it on the item.
    // This is required for cases where we default canEdit to false at the DS level
    // (fields derived from other DSs etc) but we have custom logic to allow editing
    // so should override this setting at the component level...
    if (editField.canEdit != null) item.canEdit = this.canEditCell(rowNum, colNum);

    if (editField.disabled != null) item.disabled = editField.disabled;

    // If the field has a specified optionDataSource, pass that through to the editor too,
    // along with the valueField / displayField properties


    if (editField.valueField != null) item.valueField = editField.valueField
    if (editField.displayField != null) item.displayField = editField.displayField
    if (editField.optionDataSource) item.optionDataSource = editField.optionDataSource
    if (editField.optionFilterContext) item.optionFilterContext = editField.optionFilterContext
    if (editField.optionCriteria) item.optionCriteria = editField.optionCriteria
    if (editField.optionOperationId != null) item.optionOperationId = editField.optionOperationId;

    // apply valueIcons and related properties to the editor
    // Note that we allow different value icons in the editor from the icons displayed in the
    // static cell
    item.valueIcons = this.getEditorValueIcons(editField, editedRecord);

    var valueIconWidth = this.getEditorValueIconWidth(editField),
        valueIconHeight = this.getEditorValueIconHeight(editField);
    if (valueIconWidth) item.valueIconWidth = valueIconWidth;
    if (valueIconHeight) item.valueIconHeight = valueIconHeight;

    item.imageURLPrefix = (editField.editorImageURLPrefix || editField.imageURLPrefix);
    item.imageURLSuffix = (editField.editorimageURLSuffix || editField.imageURLSuffix);
    // back compat only:
    item.baseURL = editField.baseURL;
    item.imgDir = editField.imgDir;
    // Pick up icon-related properties from the edit field (if any set)
    // (most of these will be undef, so pick up standard defaults from item type)
    var undef;
    if (editField.icons !== undef) item.icons = editField.icons;
    if (editField.showPickerIcon !== undef) item.showPickerIcon = editField.showPickerIcon;
    if (editField.pickerIconSrc !== undef) item.pickerIconSrc = editField.pickerIconSrc;
    if (editField.pickerIconWidth !== undef) item.pickerIconWidth = editField.pickerIconWidth;
    if (editField.pickerIconHeight !== undef) item.pickerIconHeight = editField.pickerIconHeight;

    // Generic icon properties are unlikely to be set on a per-field basis, but handle them anyway
    if (editField.defaultIconSrc !== undef) item.defaultIconSrc = editField.defaultIconSrc;
    var iconHeight = (editField.editorIconHeight || editField.iconHeight);
    if (iconHeight !== undef) item.iconHeight = iconHeight
    var iconWidth = (editField.editorIconWidth || editField.iconWidth);
    if (iconWidth !== undef) item.iconWidth = iconWidth;
    if (editField.iconPrompt !== undef) item.iconPrompt = editField.iconPrompt;
    if (editField.iconHSpace !== undef) item.iconHSpace = editField.iconHSpace;
    if (editField.iconVAlign !== undef) item.iconVAlign = editField.iconVAlign;

    // ValueIcons properties:
    // Setting showValueIconOnly before getting editor type ensures we get back a cycle item
    // if appropriate rather than a select.
    if (this.showValueIconOnly(editField)) {
        if (editField.editorProperties == null) editField.editorProperties = {};
        editField.editorProperties.showValueIconOnly = true;
    }

    // If pickListWidth / fields is defined on the field object, pass it through to the item:
    if (editField.pickListWidth != null) item.pickListWidth = editField.pickListWidth;
    if (editField.pickListFields != null) item.pickListFields = editField.pickListFields;


    // Set textAlign to match field alignment (required so text within text items etc reflects
    // horizontal alignment even though the item will be sized to take up all the space in the
    // cell).
    item.textAlign = this.getFieldCellAlign(editField, this.isRTL())


    if (editField.editorProperties != null) {
        for (var i = 0; i < this._commonEditorStringMethods.length; i++) {

            var prop = this._commonEditorStringMethods[i],
                value = editField.editorProperties[prop];
            // convert both "action" objects and strings
            if (value != null && !isc.isA.Function(value)) {
                var stringMethodReg = isc.FormItem._stringMethodRegistry;
                editField.editorProperties[prop] =
                    value = isc.Func.expressionToFunction(stringMethodReg[prop], value);
            }
            if (value != null) editField.editorProperties[prop] = value;
        }
    }

    // This block applies field.change, field.changed, and field.defaultDynamicValue
    for (var i = 0; i < this._commonEditorStringMethodsFromField.length; i++) {

        var prop = this._commonEditorStringMethodsFromField[i];
        if (editField.editorProperties && editField.editorProperties[prop] != null) {
            continue;
        }
        var value= editField[prop];
        if (value != null) {
            if (editField.editorProperties == null) editField.editorProperties = {};
            if (!isc.isA.Function(value)) {
                var stringMethodReg = isc.FormItem._stringMethodRegistry;

                value = isc.Func.expressionToFunction(stringMethodReg[prop], value);
            }
            editField.editorProperties[prop] = value;
        }

    }

    item.valueIconLeftPadding = this.getValueIconLeftPadding(editField);
    item.valueIconRightPadding = this.getValueIconRightPadding(editField);

    // if we're updating an existing item in place we don't need to reapply standard handlers,
    // or any properties which can't be updated on the fly (like editorType)
    if (!updateOnly) {
        // Pick up autoCompletion settings from the grid in preference to any default settings
        // on the form item
        item._getAutoCompleteSetting = this._editorGetAutoComplete;
        item.autoCompleteCandidates = editField.autoCompleteCandidates;
        item.uniqueMatch = editField.uniqueMatch;

        // containerWidget should point to the ListGrid body (or frozen body if appropriate)
        // this ensures that 'formItem.isVisible()' tests the visibility of this widget, rather than
        // the DynamicForm managing the form's values.
        item.containerWidget = (!this._suppressedFrozenFields && editField.frozen)
                                ? this.frozenBody : this.body;

        item.grid = this;

        // validateOnChange: validation of edits is performed by the grid, not the editForm.
        // Override the internal 'handleChanged()' method to notify us if the edit item value
        // is changed by the user. The grid then checks for validateOnChange, and if appropriate
        // performs validation of the cell.
        item.handleChanged = this._editorHandleChangedOverride;

        // Apply a keyDown handler to all items.
        // Allows us to kill native tab navigation in Safari
        item.keyDown = function (item, form, keyName) {
            // check if form is still valid. It could happen that the startEditing() has moved
            // the editor to a new row, but the browser is sending the "keyPress" notification
            // after this has happened
            if (this.form == null) return;

            return this.form.grid.editorKeyDown(item, keyName);
        }

        // Apply an inactiveEditorHTML handler for mouseDown so we can switch edit rows on mouseDown
        // on inactive editor HTML.
        // This handles switching edit rows when alwaysShowEditors is true and the user mouseDowns
        // on a row other than the current edit row.

        item.inactiveEditorMouseDown = function (inactiveContext, itemInfo) {
            if (inactiveContext && (inactiveContext.grid == this.form.grid.getID()) &&
                inactiveContext.rowNum != null && inactiveContext.colNum != null)
            {
                // check first if form is still valid. It could happen that the startEditing() has moved
                // the editor to a new row, but the browser is sending the notification
                // after this has happened
                if (this.form == null) return;

                this.form.grid.startEditing(inactiveContext.rowNum, inactiveContext.colNum);
            }
        }


        item.handleMouseDown = function () {
            var rv = this.Super("handleMouseDown", arguments);

            var cell = this._setLGEditCellForFocus();
            this._fireLGEditorEnter(cell[0], cell[1]);
            return rv;
        }

        item.type = editField.type;
        // pick a form item type appropriate for embedded editing

        // explicit specification
        item.editorType = this.getEditorType(editField, record);
        var eT = item.editorType;

        // Pass specified "displayFormat", "dateFormatter" and "timeFormatter" through to the
        // edit item verbatim - FormItem knows how to handle these.
        if (editField.dateFormatter != null) item.dateFormatter = editField.dateFormatter;
        if (editField.timeFormatter != null) item.timeFormatter = editField.timeFormatter;
        if (editField.displayFormat != null) item.displayFormat = editField.displayFormat;

        var inputFormat = this._getDateInputFormat(editField);
        if (inputFormat) item.inputFormat = inputFormat;

        // for date items, use the text field rather than the 3 selects
        if (this._dateEditorTypes[eT] == true || this._datetimeEditorTypes[eT] == true ||
            (editField.type == this._$date && eT == null))
        {
            item.editorType = (this._datetimeEditorTypes[eT] == true? this._$datetime : this._$date);
            item.useTextField = true;
            // This improves the appearance for this item type
            item.cellPadding = 0;
            // Don't apply a style to the sub items' cells - we don't want padding, etc.
            item.itemCellStyle = null;
            // have the picker icon butt up against the text box.
            item.pickerIconHSpace = 0;

        }

        if (this._timeEditorTypes[eT] == true) {
            item.editorType = this._$time;
            var ds = this.getDataSource();
            if (ds && ds.allowAdvancedCriteria == false) item.allowExpressions = false;
        }

        //>PopUpTextAreaItem
        // For pop-up textArea type editors, apply the keyPress handling code to the textArea<b></b>
        if (this.popUpTextAreaEditorTypes[eT] == true) {
            // PopUpTextAreaItems are a subclass of staticTextItems. Override the default textBoxStyle
            // to match the hack - suppress "over" styling when getting the cell style since we
            // always suppress it on the edit row once the editor is showing
            item.getTextBoxStyle = function () {
                var grid = this.grid,
                    record = grid.getCellRecord(this.rowNum,this.colNum);
                this.textBoxStyle = grid.getCellStyle(record, this.rowNum,this.colNum);
                return this.Super("getTextBoxStyle", arguments);
            }

            // notification when the grid's cell style changed so we can update our textBoxStyle
            item.gridCellStyleChanged = function (record, rowNum, colNum, newStyle) {
                var textBox = this._getTextBoxElement();
                if (textBox) {
                    textBox.className = this.textBoxStyle = newStyle;
                }
            }

            // supppress doubled borders etc

            item.textBoxCellCSS = isc.Canvas._$noStyleDoublingCSS

            // Apply the custom keydown & keypress handlers to the pop up text area's textArea
            item.textAreaKeyDown = this._popUpTextAreaItemKeyDown;
            item.textAreaKeyPress = this._popUpTextAreaItemKeyPress;
            // Override getTextAreaTop() - rather than sticking to the top of the form item (which
            // may not line up with other items), stick to the top of the cell.
            item.getTextAreaTop = this._getPopUpTextAreaTop;
            // default popUpOnEnter to true.
            if (item.popUpOnEnter == null) item.popUpOnEnter = true;

            // react to text area focus as we would to item focus for other items
            // so we know what cell the user is editing
            item.textAreaFocus = this._popUpTextAreaItemFocus;
        }
        //<PopUpTextAreaItem

        // Don't show label for checkboxes by default.
        if (this._checkboxEditorTypes[eT] == true || (editField.type == this._$boolean && eT == null)) {
            if (item.showLabel == null) item.showLabel = false;
            // Also verify that the item has focus on click.
            // Required for IE where focus is asynchronous and would occur after the click changed
            // the value

            if (item.handleClick == null) {
                item.handleClick = this._checkboxClick;
            }
        }

        // Default to overflow:"auto" for richTextItem editors. Otherwise we find they typically
        // overflow into the next column
        if (eT == "RichTextItem" || eT == "richText") {
            if (item.overflow == null) item.overflow = "auto";
        }

        item.elementFocus = this._editFormItem_elementFocus;
        item._setLGEditCellForFocus = this._editFormItem_setLGEditCellForFocus;
        item._fireLGEditorEnter = this._editFormItem_fireLGEditorEnter;

        // By default set 'canTabToIcons' to false for edit fields
        // can be overridden on a per-field basis
        item.canTabToIcons = false;

        // override 'focusInItem' to manage selection
        // - we need more complicated behavior than the standard 'selectOnFocus'.
        //   If we started editing via editOnKeypress we will have updated the value of the item
        //   so should set the cursor at the end of the item value. Otherwise, just select on focus.
        // refocusAfterRedraw override prevents this from clobbering selection when its a straight
        // redraw of the item and it already has a meaningful selection set.
        item.focusInItem = this._editFormItem_focusInItem;
        item._refocusAfterRedraw = this._editFormItem_refocusAfterRedraw;
    }

    // Allow for developer specified defaults / properties for this field

    var propertyDefaults = this.getEditorProperties(editField, record, rowNum);
    isc.addProperties(item, propertyDefaults);
    // if this grid is databound, any other properties specified in the DS will be picked
    // up by the form during databinding
    return item;
},


// focusInItem override for edit form items.
_editFormItem_focusInItem : function () {
    this.Super("focusInItem", arguments);
    this.grid._updateEditorSelection(this);
},

// Override _refocusAfterRedraw()
// This handles setting the selection on the item to whatever it was before redraw().
// Don't allow this to get clobbered by _updateEditorSelection
_editFormItem_refocusAfterRedraw : function () {
    this._suppressGridTextSelection = true;
    this.Super("_refocusAfterRedraw", arguments);
    this._suppressGridTextSelection = false;
},

// helper to return the editItem name for some cell
getEditorName : function (rowNum, editField, returnDataPath) {
    // accept a colNum or a field object
    editField = this.getField(editField);
    if (!editField) return null;
    if (returnDataPath && editField.dataPath) {
        return editField.dataPath;
    }
    return editField[this.fieldIdProperty];
},

getTrimmedFieldDataPath : function (field) {
    // convert whatever we were passed (object, string, index) to a field object
    var fieldObj = this.getField(field);
    // If the conversion failed assume we were passed a string we can just use.
    if (fieldObj == null) return field;
    if (fieldObj.dataPath) {
        return this._trimDataPath(fieldObj.dataPath);
    } else {
        return field.name;
    }
},


_editItemStringMethodCache:{},

//>    @method    listGrid.refreshCell()
//  @include    gridRenderer.refreshCell()
//  @example calculatedCellValue
//<
// override refreshCell just to pass the additional params through to refreshCellValue()
refreshCell : function (rowNum, colNum, refreshingRow, allowEditCellRefresh) {
    if (rowNum == null) {

        this.logInfo("ListGrid.refreshCell(): first parameter rowNum not present, returning");


        return;
    }
    if (!this.isDrawn() || !this.body) return;

    var body = this.getFieldBody(colNum);

    // If the body is already marked for redraw, allow that to handle updating the cell
    if (body.isDirty()) {
        this.logDebug("refresh cell redrawing body", "gridEdit");
        body.redraw("refresh cell");
        // return false to indicate that we did not update the cell in place, but forced a
        // redraw (used to make 'refreshRow' more efficient.
        return false;
    }

    if (this._alwaysShowEditors(this.getField(colNum),true)) {
        this._clearingInactiveEditorHTML(rowNum,colNum);
    }

    var bodyColNum = this.getLocalFieldNum(colNum);
    body.refreshCellStyle(rowNum, bodyColNum);
    // refresh the value too unless it's already been refreshed as part of styling
    if (!body.shouldRefreshCellHTML()) {
        this.refreshCellValue(rowNum, colNum, refreshingRow, allowEditCellRefresh);
    }
},


_pendingCellRefreshDelay: 100,
_pendingCellRedrawThreshold: 0.2,
markCellForRefresh : function (rowNum, colNum) {
    if (rowNum == null) {

        this.logInfo("ListGrid.refreshCell(): first parameter rowNum not present, returning");


        return;
    }
    if (!this.isDrawn() || !this.body) return;

    // no need for incremental strategy if body is already dirty
    var body = this.getFieldBody(colNum);
    if (body.isDirty()) return;

    var grid = this;


    if (!this._pendingCellsToRefresh) this._pendingCellsToRefresh = [];
    var pendingCells = this._pendingCellsToRefresh;

    // add the new cell to our pending list of cells to refresh
    var foundCell = false;
    for (var i = 0; i < pendingCells.length; i++) {
        var cell = pendingCells[i];
        if (cell[0] == rowNum && cell[1] == colNum) {
            foundCell = true;
            break;
        }
    }
    if (!foundCell) pendingCells.add([rowNum, colNum]);

    // if a callback is already set up, we're done; otherwise set it up
    if (this._pendingCellRefreshTimer) return;
    this._pendingCellRefreshTimer = isc.Timer.setTimeout(function () {
        // clear state up front; use closure variables below
        delete this._pendingCellRefreshTimer;
        delete this._pendingCellsToRefresh;

        if (body.isDirty()) return;

        // if a lot of cells must be refreshed, just redraw the entire GridBody instead
        var drawArea = body.getDrawArea();
        if (isc.isAn.Array(drawArea)) {
            var nTotalCells = (drawArea[1] - drawArea[0] + 1) *
                              (drawArea[3] - drawArea[2] + 1);
            if (pendingCells.length > nTotalCells * grid._pendingCellRedrawThreshold) {
                return body.redraw("markCellForRefresh");
            }
        }

        for (var i = 0; i < pendingCells.length; i++) {
            var cell = pendingCells[i];
            grid.refreshCell(cell[0], cell[1]);
        }

    }, this._pendingCellRefreshDelay);
},

cancelPendingCellRefresh : function () {
    isc.Timer.clear(this._pendingCellRefreshTimer);
    delete this._pendingCellRefreshTimer;
    delete this._pendingCellsToRefresh;
},

// refreshCellValue overridden to handle refreshing cells within the edit row
// If we are showing an edit form item for this cell:
// - if the form item is present in the DOM, and has focus:
//   - we must save out it's element value (may be dirty)
//   - blur the item without firing the handler
// - rewrite the HTML for the cell (including the form item)
// - inform the form item that it has been written out / redrawn in the DOM
// - reset the element value
// - if the item had focus, re-set focus (and selection).

refreshCellValue : function (rowNum, colNum, refreshingRow, allowEditCellRefresh) {
    if (!this.isDrawn() || !this.body) return;

    var body = this.getFieldBody(colNum),
        bodyColNum = this.getLocalFieldNum(colNum);

    // If we need to delay the refresh, fire again after a delay
    if (!body._readyToRefreshCell(rowNum, bodyColNum)) {
        this.delayCall("refreshCellValue", [rowNum, colNum, refreshingRow, allowEditCellRefresh]);
        return;
    }
    // Handle the case of showing an edit form field for this cell.

    var editFieldName = this.getEditorName(rowNum, colNum),
        fieldName = this.getFieldName(colNum),
        form = this._editRowForm,
        editItem, cellHasFocus = false,
        cellShowingEditor, cellWillShowEditor;
    if (form) {
        var editItem = form.getItem(editFieldName),
            rowHasEditor = (this._editorShowing && rowNum == this.getEditRow());
        // sanity check if the colNum on the edit item doesn't match the colNum of the
        // cell we're refreshing, it doesn't relate to this cell.
        // This occurs in CubeGrids where we have one record per cell
        if (editItem && editItem.colNum != colNum) editItem = null;

        if (rowHasEditor) {
            // whether there is currently an editor in the cell
            cellShowingEditor = (editItem && editItem.isDrawn());
            // whether there will be an editor in the cell after refresh
            cellWillShowEditor = this.canEditCell(rowNum, colNum);
            if (editItem != null && form.hasFocus) {
                var formFocusItem = form.getFocusSubItem();
                cellHasFocus = (formFocusItem == editItem ||
                               (editItem.items && editItem.items.contains(formFocusItem)));
            }
        // catch the case where we're clearing out a drawn item

        } else if (editItem && editItem.rowNum == rowNum) {
            cellWillShowEditor = false;
            cellShowingEditor = editItem.isDrawn();
        }
    }


    if (!allowEditCellRefresh && (cellHasFocus && cellShowingEditor && cellWillShowEditor))
    {
        return;
    }
    // If there is a visible editor in this cell, update it's value and blur before redrawing
    if (cellShowingEditor) {

        this.getUpdatedEditorValue();
        if (editItem != null) {
            if (cellHasFocus) {
                // Note - if the item will be visible after this method, silently blur and
                // refocus. Otherwise allow the blur handler to fire, since we won't be
                // restoring focus.
                if (cellWillShowEditor) {
                     form._blurFocusItemWithoutHandler();
                }
                else editItem.blurItem();
            }
            // If this method will redraw a form item, notify it now
            if (cellWillShowEditor) editItem.redrawing();
        }
    }

    delete body._drawnEditItems;
    // Call the body's method to update the HTML of the cell:
    body.refreshCellValue(rowNum, bodyColNum);
    if (editItem && (cellShowingEditor || cellWillShowEditor)) {
        // Call our method to fire the appropriate 'drawn()' / 'redrawn()' / 'cleared()'
        // notification on the edit item.


        if (cellWillShowEditor) this._drawingItem = editItem;

        this._editItemsDrawingNotification(editItem, null, body);
        if (cellWillShowEditor) {
            // restore the element value (since it's not written out with the element)

            editItem.setValue(this.getEditDisplayValue(rowNum, colNum));
            delete this._drawingItem;
            // restore focus if it had focus

            if (cellHasFocus) {
                editItem._suppressGridTextSelection = true;
                form._focusInItemWithoutHandler(editItem);
                editItem._suppressGridTextSelection = false;
            }
        }
    }
},


//>    @method    listGrid.refreshRow()
// @include gridRenderer.refreshRow()
//<
refreshRow : function (rowNum) {
    if (!this.body || !this.isDrawn()) return;

    // If the body is already dirty, allow the redraw to handle updating the row.
    var frozenFields = this.frozenFields && this.frozenFields.length > 0;
    // If deriveVisibleFields is called, and then this method fires before the grid has redrawn
    // we won't  have created the frozen body. Treat this case like frozenFields is false;
    if (frozenFields && !this.frozenBody) frozenFields = false;


    //var dirty = this.body.isDirty() ||
    //            frozenFields ? (this.frozenBody.isDirty() || this.bodyLayout.isDirty()) : false;
    var dirty = false;
    if (this.body.isDirty()) dirty = true;
    if (frozenFields && !dirty) {
        if (this.frozenBody.isDirty() || this.bodyLayout.isDirty()) dirty = true;
    }

    if (dirty) {
        var bodyWidget = frozenFields ? this.bodyLayout : this.body;
        return bodyWidget.redraw("refresh row");
    }

    if (this._editorShowing && this._editRowForm != null) {
        this.logInfo("refresh row: " + rowNum, "gridEdit");
    }

    // just call 'refreshCell' on all drawn cells
    if (frozenFields) {
        for (var i = 0; i < this.frozenFields.length; i++) {
            this.refreshCell(rowNum, this.getFieldNum(this.frozenFields[i]), true);
        }
    }

    var firstVisible = this.body._firstDrawnCol,
        lastVisible = this.body._lastDrawnCol;

    for (var i = firstVisible; i <= lastVisible; i++) {
        var colNum = this.getFieldNumFromLocal(i, this.body);
        this.refreshCell(rowNum, colNum, true);
    }
},

//>    @method listGrid.startEditingNew() (A)
//
// Start editing a new row, after the last pre-existing record in the current set of data.
// <P>
// This new row will be saved via the "add" +link{group:dataSourceOperations,DataSource
// operation}.
// <P>
// See the +link{group:editing,Grid Editing overview} and also the
// +link{group:unsavedRecords,Editing Unsaved Records overview} for context about how unsaved
// records behave.
// <P>
// You can optionally pass <code>newValues</code> which are the initial values for the newly
// added record.  See also +link{listGridField.defaultValue} as a means of setting default
// values every time the user begins editing a new record, for instance, by pressing downArrow
// on the last normal record in the grid when +link{listEndEditAction} is "next".
// <P>
// If editing is already underway elsewhere in the grid, startEditingNew() behaves just like
// +link{startEditing()}.
//
// @group  editing
//
// @param  [newValues] (Map | Record)  Optional initial set of properties for the new record
// @param  [suppressFocus] (Boolean) Whether to suppress the default behavior of moving focus
//                                   to the newly shown editor.
// @see    startEditing()
// @visibility external
//<
startEditingNew : function (newValues, suppressFocus) {

    // force editing on if it's not configured for any field, but a programmatic call is made
    if (!this.canEdit && !(this.completeFields || this.fields).getProperty("canEdit").or()) {
        this.canEdit = true;
    }


    if (isc.isAn.Array(this.data) && this.data.length == 0 &&
        this.dataSource && !this.shouldSaveLocally())
    {
        this.fetchData(null, null, {_suppressFetch:true});
        this.data.setFullLength(0);
    }

    // The new row will be added to the end of the current set of rows


    var newRowNum = this.body ? this.body.getTotalRows() : this.getTotalRows();

    // If we're showing the 'edit new record' row, ensure we insert the record over that row,
    // rather than inserting after that row.
    if (this.showNewRecordRow) newRowNum -= 1;

    if (!this.useCellRecords && newValues != null) {
        // if this grid is using rowRecords (ListGrid), set the edit values before calling
        // findNextEditCell - rowRecords only need the rowNum to store editValues and doing
        // that first for LG means the new values are made available to getEditedRecord()
        // immediately (eg, before findNextEditCell(), below, starts calling canEditCell())
        this.setEditValues(newRowNum, isc.addProperties({}, newValues), true);
    }

    var newEditCell = this.findNextEditCell(newRowNum, 0, 1, true, true, true);

    // newEditCell can be null if canEditCell returned false for all fields!
    if (newEditCell == null) {
        this.logInfo("startEditingNew() failed to find any editable fields in this grid.",
                     "gridEdit");
        if (!this.useCellRecords && newValues != null) {
            // setEditValues() was called above, but there were no fields to edit - clear
            // the editSession
            this._clearEditValues(newRowNum, null, false);
        }
        return;
    }

    if (this.useCellRecords && newValues != null) {
        // Suppress displaying the new edit values - handled by 'startEditing', which will show
        // the edit form for the row.
        this.setEditValues(newEditCell, isc.addProperties({}, newValues), true);
    }

    // fall through to 'startEditing()' -- handles any current edit in another cell.
    this.startEditing(newEditCell[0], newEditCell[1], suppressFocus);
},

//>    @method listGrid.updateEditRow()  (IA)
//
// Internal method to update the set of form fields written into the ListGrid body's
// currently editable row (after a call to editRow()).
// - Updates the values of the form items
// - Focuses in the appropriate item
//
//  @group  editing
//
//  @param  rowNum      (number)    Row number to update.
//
//  @visibility internal
//<
updateEditRow : function (rowNum) {
    // if updateEditRow is called on a delay, by the time this update occurs, we may have moved
    // on to another row.
    if (this._editRowNum != rowNum || !this._editRowForm) {
        //this.logWarn("updateEditRow bailing, update was for row: " + rowNum +
        //             " current editRow is " + this._editRowNum);
        return;
    }

    // update the item values of the edit form.

    this._editRowForm.setItemValues();
    // Clear out the _setValuesPending flag
    delete this._editRowForm._setValuesPending;
    this._editRowForm._waitingOnUpdate = false;
},

// shouldFixRowHeight()
// Internal method allowing 'fixedRecordHeights' to be overridden for individual rows in the
// GridRenderer.
// Currently only used for row-level editing.
// When row-level editing is enabled, we want to allow the row showing the embedded editor to
// expand to accommodate it's contents regardless of this.fixedRecordHeights
//

shouldFixRowHeight : function (record, rowNum) {

    // if this row is being edited, don't vertically clip
    // (Note check for != false rather than == true, as if lg.canEdit is unset, we support
    // editing on fields where canEdit is explicitly set to true)
    if (this.canEdit != false && this._editorShowing && rowNum == this._editRowNum) {
       return false;
    }
    return this.fixedRecordHeights;
},

//--- helpers for edit flow methods

// Provide unique identifiers to be used to identify the edit flow.

_getNextEditFlowID : function () {
    if (this.__lastEditFlowID == null) this.__lastEditFlowID = isc.timeStamp();
    return this.__lastEditFlowID ++;
},

// internal helper method for click outside editor - avoids us having to determine the appropriate
// cell's value in a click-mask event handler type stringMethod.
_handleClickOutsideEditor : function () {
    // If we're modal editing and the user clicked on the grid body, remember the
    // cell coordinates of the click.
    // This ensures that if the row-height changes when we hide the editor, we still
    // treat this as a click on the appropriate target cell (which may have jumped to a new position)
    if (this.modalEditing) {
        var target = isc.EH.getTarget();
        if (target == this.body || target == this.frozenBody) {
            var rowNum = target.getEventRow(),
                colNum = target.getEventColumn();
            target._maskedMouseDownCell = [rowNum, colNum];
        }
    }
    this.cellEditEnd(isc.ListGrid.CLICK_OUTSIDE);
},

// Retrieving / updating editValues.
// We store copies of edited values locally for multiple records in the _editSessions
// object.  This allows us to hang onto edit values for more than one edited record at a time
// - required for records that have been edited but not yet saved, either because a save is
// in process, but hasn't returned from the server, and 'waitForSave' is false, or because a
// save failed with validation errors, and stopOnErrors is false.



// value to display in a cell for which there may be edit values present.

// Note: optional 'record' parameter passed by 'getEditDisplayValues' only.
getEditDisplayValue : function (rowNum, colNum, record) {
    var undef;
    // If 'record' is not passed, determine it from rowNum, colNum now.
    if (record === undef) record = this.getCellRecord(rowNum, colNum);

    // use the edit value for the cell if present
    var value = this._getEditValue(rowNum, colNum);
    if (value === undef && record != null) {

        value = this.getRawCellValue(record, rowNum, colNum);
    }
    // If a formatter is defined for the editor values, apply it now
    value = this._formatEditorValue(value, record, rowNum, colNum);
    return value;
},

// Internal method to fire developer defined 'formatEditorValue' methods at either the Grid or
// Field level.

_formatEditorValue : function (value, record, rowNum, colNum) {
    // If this is a new row, the record passed in will be null -
    // In this case pass the edit values to the formatter instead

    if (record == null) record = this._getEditValues(rowNum, colNum);

    // If a field-level formatter is defined, apply it.
    var field = this.fields[colNum];

    if (field && field.formatEditorValue != null) {

        isc.Func.replaceWithMethod(field, "formatEditorValue",
                                            "value,record,rowNum,colNum,grid");
        value = field.formatEditorValue(value, record, rowNum, colNum, this);
    // Only apply a Grid-level formatter if no formatter exists at the field level.
    } else if (this.formatEditorValue != null) {
        value = this.formatEditorValue(value, record, rowNum, colNum);
    }
    return value;
},

//>    @method listGrid.getEditValuesID()
//
//  Given either a rowNum, a set of primary key values,
//  returns a unique identifier for the set of temporary locally stored edit values for some
//  record being edited.
//  If passed the editValuesID, it will just be returned.
//
//  @group  editing
//  @visibility advancedInlineEdit
//  @param  ID (number | object | string)    Identifier for editValues for which we need to
//                                           return the unique editValuesID.
//  @return (string)   Unique identifier for the set of editValues.
//<

getEditValuesID : function (ID) {
    if (ID == null || this._editSessions == null) return null;

    if (isc.isA.String(ID) && this._editSessions[ID] != null) return ID;

    // rowNum (common case)
    if (isc.isA.Number(ID)) {
        return this._editRowMap[ID];
    }

    // handle primary keys object OR the edit data object itself
    for (var i in this._editSessions) {
        var data = this._editSessions[i];
        if (data == ID) return i;
        var pk = data._primaryKeys;
        if (pk && this.comparePrimaryKeys(pk, ID)) {
            return i;
        }
    }

    return null;
},

// getEditSession - returns the edit data object for some row.
// This contains the editValues, the rowNum (if known), any validation errors, and primary
// keys for the edited record.
getEditSession : function (editDataID, colNum) {
    // editDataID can be a rowNum, a primary key or an editValuesID string.
    if (this._editSessions == null) return null;

    if (!isc.isA.String(editDataID)) editDataID = this.getEditValuesID(editDataID, colNum);
    return this._editSessions[editDataID];
},

// getEditSessionRowNum   - given an edit data object, or an ID for an edit data object, returns
// the rowNum of the record associated with the data.
// May be null if we're editing a new row on the end of the list, or with a paged result-set
// if we don't yet know the rowNum for the record.
getEditSessionRowNum : function (editDataID) {
    editDataID = this.getEditSession(editDataID);
    return (editDataID != null ? editDataID._rowNum : null)
},

getEditSessionColNum : function (editDataID) {
    editDataID = this.getEditSession(editDataID);
    // Note that the _colNum will be unset if we have 1 record per row
    return (editDataID != null ? editDataID._colNum : null)
},

//>    @method listGrid.getAllEditRows()
// Returns an array of every rowNum for which we have pending (unsubmitted) edits.
// This will return records that have been marked as removed (see +link{listGrid.markRecordRemoved()}
// as well as records with unsaved changes to field values.
//  @group  editing
//  @visibility external
//  @return (Array of int) Array of rowNums for rows with edit values pending submission.
//<

getAllEditRows : function (getIds) {
    return this.getAllEditCells(getIds, true);
},


getAllEditCells : function (getIds, rowsOnly) {
    var registry = this._editSessions,
        cells = [];
    if (!registry) return cells;

    if (getIds) return isc.getKeys(this._editSessions);


    for (var i in registry) {
        var rowNum = registry[i]._rowNum;
        // Convert "1" to 1, etc.

        rowNum = parseInt(rowNum);
        if (rowNum == null || rowNum < 0 || isNaN(rowNum)) continue;
        if (rowsOnly) cells[cells.length] = rowNum
        else {
            var colNum = registry[i]._colNum;
            // If rowNum only was stored on the cell, use the special getRowEditColNum() to
            // figure out the colNum in question
            if (colNum == null) {
                colNum = this._editorShowing && (rowNum == this.getEditRow())
                                    ? this.getEditCol() : this.getRowEditColNum(rowNum);
            }
            if (colNum != null && !isc.isA.Number(colNum)) colNum = parseInt(colNum);
            cells[cells.length] = [rowNum, colNum];
        }
    }
    return cells;
},

//>    @method listGrid.getEditValues()
// Returns the current set of unsaved edits for a given row being edited.
//
//  @param  valuesID (number | Object)  rowNum of the record being edited, or an Object
//                                      containing values for all the record's primary keys
//  @return (object)   Current editValues object for the row.  This contains the current
//                     edit values in {fieldName1:value1, fieldName2:value2} format.
//  @group  editing
//  @visibility external
//<
//  @param [colNum] (number) colNum of the record being edited. Only required if valuesID
//                           is passed in as a rowNum, and we're displaying one record per cell
//                          as in a +link{CubeGrid}

getEditValues : function (valuesID, colNum) {
    if (valuesID == null) {
        return this.logWarn("getEditValues() called with no valuesID. "
                + (this.logIsDebugEnabled("gridEdit") ? this.getStackTrace() : ""));
    }



    if (this._initializingEditValues) return {};

    // handle being passed a 2 element array [rowNum,colNum] since this is the format we
    // accept for setEditValues()
    if (colNum == null && isc.isA.Array(valuesID)) {
        colNum = valuesID[1];
        valuesID = valuesID[0];
    }

    // If we're showing an editor for this row, ensure that the current value in the
    // edit field is present in the editValues we return.

    var rowNum = (isc.isA.Number(valuesID) ? valuesID : this.getEditSessionRowNum(valuesID));
    if (this._editorShowing && (this.getEditRow() == rowNum)) {
        this.storeUpdatedEditorValue();
    }


    var values = isc.addProperties({},this._getEditValues(valuesID, colNum));
    if (values != null) {
        delete values[this.recordRemovedProperty];
    }
    return values;
},

// Retrieve the stored edit values for some row (or null)
// Unlike the public method, this will not first update the editvals with the current value
// from the editForm (if showing)
_getEditValues : function (valuesID, colNum) {
    // we may be passed the editValues object, in which case we're passing it back again,
    var editSession = this.getEditSession(valuesID, colNum);
    return editSession != null ? editSession._editValues : null;
},

//> @method listGrid.getEditedRecord()
// Returns the combination of unsaved edits (if any) and original values (if any) for a given
// row being edited.
// <P>
// The returned value is never null, and can be freely modified.
//
// @param  valuesID (number | Object)  rowNum of the record being edited, or an Object
//                                      containing values for all the record's primary keys
// @return (Object) A copy of the record with unsaved edits included
// @group  editing
// @visibility external
//<

getEditedRecord : function (rowNum, colNum, suppressUpdate) {
    if (rowNum == null) return this.logWarn("getEditedRecord() called with no valuesID");

    // the valuesID allows for rowNum independent data storage - if passed a valuesID
    // determine resolve to rowNum/colNum here so we can get a pointer to the record object
    if (!isc.isA.Number(rowNum)) {
        rowNum = this.getEditSessionRowNum(rowNum);
        colNum = this.getEditSessionColNum(rowNum);
    }

    var record = this.getCellRecord(rowNum, colNum),
        // respect the parameter to avoid checking the edit form for updates
        editValues = suppressUpdate ? this._getEditValues(rowNum, colNum)
                                    : this.getEditValues(rowNum, colNum);

    // don't hang the "removed" flag onto the record.
    if (editValues != null) {

        //delete editValues[this.recordRemovedProperty];
    }


    return isc.addProperties({}, record, editValues, {__ref: null});
},

//> @method listGrid.getEditedCell()
// Returns the current value of a cell. If the cell has an outstanding edit value, this will
// be returned, otherwise the underlying value of the record will be returned.
//
// @param  record (number | Object)  rowNum of the record being edited, or an Object
//                                      containing values for all the record's primary keys
// @param field (number | string) colNum or fieldName of the cell
// @return (any) Current edit value, or underlying value for the cell
// @group  editing
// @visibility external
//<
getEditedCell : function (record, field) {
    if (record == null || field == null)
        return this.logWarn("getEditedCell() called with no record / field parameter");

    var editValues = this.getEditValues(record, field),
        rowNum = isc.isA.Number(record) ? record : this.getEditSessionRowNum(record),
        colNum = isc.isA.Number(field) ? field : this.getFieldNum(field),
        record = this.getCellRecord(rowNum, colNum);

    var editFieldName = this.getEditorName(rowNum, this.getField(colNum))
    var undef;

    if (editValues && editValues[editFieldName] !== undef) return editValues[editFieldName];
    return record ? record[editFieldName] : null;
},

// When we attempt to save an edit remember the edit values
// We use this to perform intelligent change detection while pending a save on the server
rememberSubmittedEditValues : function (valuesID, colNum) {

    var editSession = this.getEditSession(valuesID);
    if (editSession != null) {
        editSession._submittedValues = isc.addProperties({}, editSession._editValues);
    }
},

// retrieve the last set of editValues submitted to the server.
getSubmittedEditValues : function (valuesID, colNum) {
    var editSession = this.getEditSession(valuesID, colNum);
    return editSession != null ? editSession._submittedValues : null;
},

hasSubmittedEditValues : function (valuesID, colNum) {
    var vals = this.getSubmittedEditValues(valuesID, colNum);
    return vals != null && !isc.isA.emptyObject(vals);
},

// Clear the stored "submitted edit values"
// called from editFailedCallback method - oldValues object is required so we don't clear
// the submitted editValues if they have been modified since the (failed) save was committed
// a case we can only hit if 2 overlapping saves have been kicked off.
clearSubmittedEditValues : function (valuesID, oldValues) {
    var editSession = this.getEditSession(valuesID);
    if (editSession == null) return;
    var submittedVals = editSession._submittedValues;
    if (!submittedVals) return;

    for (var field in oldValues) {
        if (submittedVals[field] == oldValues[field]) delete submittedVals[field];
    }
    if (isc.isA.emptyObject(submittedVals)) editSession._submittedValues = null;
},



//>    @method listGrid.createEditValues()
//
// This method creates a new set of editValues for a row at the end of the list, and returns
// the editValuesID which can subsequently be passed to any of the following methods as a
// unique identifier (in place of the 'rowNum' parameter where appropriate):
// 'setEditValue()', 'getEditValues()', 'getEditValue()', 'getEditValues()', 'clearEditValue()'
// and 'clearEditValues()'.<br>
// The new edit values will be displayed at the end of the list.
//
//  @param  values       (any)       New values for the row
//  @visibility advancedInlineEdit
//<

createEditValues : function (values) {
    var rowNum = this.body.getTotalRows();
    // set up the edit values, and display them in the listGrid (don't pass the
    // suppressDisplay param)

    this.setEditValues(rowNum, values);
    return this.getEditValuesID(rowNum);
},



// initializeEditValues() - helper method to set up empty edit vals for some
// record
initializeEditValues : function (rowNum, colNum, displayNewValues, recalculateSummaries) {
    // If we don't have edit values for this record, set them up.
    // (we can use the internal method - we know we don't have outstnding edits in the
    // edit form at this point, as the editor should have been hidden)
    if (this._getEditValues(rowNum, colNum) == null) {
        this._initializingEditValues = true;
        this.setEditValues([rowNum, colNum], {}, !displayNewValues, !recalculateSummaries);
        delete this._initializingEditValues;
        //this.logWarn("editValues for row: " + rowNum + " are now: " + this.echo(this.getEditValues(rowNum)));
    }
},


//>    @method listGrid.setEditValues()
//
// This method sets up a set of editValues for some row / cell.  It differs from
// 'setEditValue()' in that:<br>
// &nbsp;- it takes values for multiple fields<br>
// &nbsp;- it clears out any previous edit values for the record<br>
//
//  @param rowNum (number) Row number for the record being edited
//  @param values (Object) New values for the row
//
// @visibility external
//<
// @param   suppressDisplay (boolean)   Additional internal parameter to suppress updating the
//                                      affected row to display the new edit values.
// Note that the first param may be a 2 element array of rowNum, colNum for one record-per-cell
// data models (documented in CubeGrid)

setEditValues : function (rowNum, editValues, suppressDisplay, suppressSummaryRecalc) {
    var colNum;
    if (isc.isAn.Array(rowNum)) {
        colNum = rowNum[1];
        rowNum = rowNum[0];
    }

    if (!isc.isA.Number(rowNum)) {
        this.logWarn("setEditValues() called with bad rowNum: " + this.echo(rowNum));
        return;
    }

    // Default to an empty set of values - if the user wants to entirely clear an editValues
    // object, they should use 'clearEditValue(s)' instead.
    if (editValues == null) editValues = {};

    var oldEditValues, changedFields, addedRow = true;
    if (!suppressDisplay) {

        var record = this.getCellRecord(rowNum, colNum);
        if (record == null) record = {};
        else addedRow = false;

        oldEditValues = this.getEditValues(rowNum, colNum);
        if (oldEditValues != null) addedRow = false;

        changedFields = isc.addProperties({}, oldEditValues);
        for (var i in changedFields) {
            changedFields[i] = record[i];
        }

        isc.addProperties(changedFields, editValues);

        // At this point changedFields will be a mapping of the new edit display values for
        // each field that has been changed.
    }

    if (this.logIsInfoEnabled("gridEdit")) {
        oldEditValues = oldEditValues || this.getEditValues(rowNum, colNum);
        if (!oldEditValues) {
            this.logInfo("establishing new edit session at row: " + rowNum +
                         (colNum != null ? ", col:" + colNum : "") +
                         (this.logIsDebugEnabled("gridEdit") ?
                         " with values: " + this.echo(editValues) :
                         ""), "gridEdit");
        }

    }
    // If we were marked as removed afore this method ran, continue to keep us marked as removed
    // If the user wants to drop that they can explicitly call 'discardEditValues()' or
    // 'unmarkRecordRemoved'.
    var markedAsRemoved = this.recordMarkedAsRemoved(rowNum);
    if (markedAsRemoved) editValues[this.recordRemovedProperty] = true;

    // store the new edit value
    this._storeEditValues(rowNum, colNum, editValues);

    var hasModifiedValues = changedFields != null && !isc.isAn.emptyObject(changedFields);

    if (suppressDisplay || !this.isDrawn() || !this.body) {
        if (hasModifiedValues && !suppressSummaryRecalc) {
            // unless explicitly prohibited, if suppress display is passed we do still want to
            // redraw the summary row if there is one since the calling code never handles that
            if (this.summaryRow && this.showGridSummary) this.summaryRow._recalculateSummaries()
        }
        return;
    }

    if (hasModifiedValues && rowNum < this.data.getLength()) {
        // don't refresh display when recacluating summaries - we'll refresh the row and
        // the grid summary if necessary.
        var modifiedRecord = this.data.get(rowNum);
        this.calculateRecordSummaries(modifiedRecord, null, true, true, true);
    }

    // values shown in the cells for this row are now stale (whether each cell is being edited
    // or not), so refresh.

    // if totalRows changed, it indicates that that a new edit row is being created at the end
    // of the body, hence we need to redraw to add the row to the DOM
    var shouldRedrawBody =  (addedRow || this.body.isDirty());

    if (shouldRedrawBody) {
        var editorShowing = this.isEditingRecord(rowNum, colNum) && this._editRowForm != null


        if (editorShowing) this._updateEditItemValues();

        this.body.markForRedraw(

        );
        if (hasModifiedValues && this.summaryRow && this.showGridSummary) {
            this.summaryRow.markForRedraw();
        }

    } else {
        this._displayNewEditValues(rowNum, colNum, changedFields);
    }
},

_displayNewEditValues : function (rowNum, colNum, changedFields, errors) {

    if (!changedFields || isc.isAn.emptyObject(changedFields)) {
        return;
    }

    var editorShowing = this.isEditingRecord(rowNum, colNum) && this._editRowForm != null
    // update each cell effected by the change, either by setting the form item value or
    // refreshing the cell

    var hasVisibleChanges = false;
    for (var fieldName in changedFields) {
        var fieldColNum = this.getColNum(fieldName);

        // If we're showing an edit item for the cell, set it's value rather than
        // refreshing the cell (and rewriting the form item HTML completely)
        var editItemDrawn;
        if (editorShowing) {
            this._editRowForm.setValue(fieldName, changedFields[fieldName]);

            var editItem = this._editRowForm.getItem(fieldName);
            editItemDrawn = (fieldColNum >=0 && editItem && this.canEditCell(rowNum, fieldColNum));
        }
        // undrawn cell - just continue to the next field
        // [note do this after modifying edit form values]
        if (fieldColNum == -1) continue;
        hasVisibleChanges = true;

        if (!editItemDrawn) {
            this.refreshCell(rowNum, fieldColNum);
        } else if (errors && errors[fieldName]) {
            this.showCellErrors(rowNum, fieldColNum);
        }
    }
    if (hasVisibleChanges && this.summaryRow && this.showGridSummary) {
        this.summaryRow._recalculateSummaries();
        // refresh the group summary!
        this.refreshGroupSummary(rowNum);
    }

},


// Internal helper method for 'setEditValues' - actually stores the editValues object for the
// row
// Call 'setEditValues()' rather than calling this method directly.
// NOTE: editValuesId is a *highly internal* param that allows creating of a new editSession
// with a predictable ID

_storeEditValues : function (rowNum, colNum, editValues, editValuesId) {
    // Get the record for this row.  Note that this might be null as we may be adding a new
    // edit row to the end of the list.
    var record = this.getCellRecord(rowNum, colNum);

    // Assertion - if we don't have the edit data for this rowNum, we don't have the
    // edit data for this record, since 'getRecord()' will always associate the edit data
    // for some record with the appropriate rowNum, if required.
    var editSession = this.getEditSession(rowNum, colNum) ||
            this.createEditSession(rowNum, colNum, record, editValuesId);
    // Always add the primary keys to edit values for databound lists, so that they are
    // available to identify the record in saveEditedValues().  Non-databound lists are
    // expected to use object identity.
    if (this.dataSource != null) {
        // set the 'newRecord' marker on loading rows to indicate that there's no associated
        // record (and we don't have PKs for the row
        if (record == "loading") {
            editSession._newRecord = true;
        } else if (record != null) {
            var ds = this.getDataSource(),
                pkArray = ds.getPrimaryKeyFieldNames();

            for (var i = 0; i < pkArray.length; i++) {
                editValues[pkArray[i]] = record[pkArray[i]];
            }
        }
    }

    // set the _newRecord flag for each row without an associated record
    if (record == null) {
        editSession._newRecord = true;

        // if we have any fields with a specified default value, pick it up as a default
        // edit value.
        // (true even if the field is hidden)

        // If we are showing any 'enum' type fields, and
        var fields = this.completeFields || this.fields || [],
            undef;

        for (var i = 0; i < fields.length; i++) {
            var field = fields[i],
                fieldName = field[this.fieldIdProperty];
            if (editValues[fieldName] === undef) {
                var defaultValue = this.getDefaultEditValue(fieldName, field);
                if (defaultValue != null) {
                    editValues[fieldName] = defaultValue;
                }
            }
        }
    }

    // clear out any stored editValues and copy the passed editValues into place, preserving
    // the same instance.
    for (var i in editSession._editValues) {
        delete editSession._editValues[i];
    }
    for (var i in editValues) {
        editSession._editValues[i] = editValues[i];
    }

    // Cache the last edit row - used by 'getTotalRows()'.  This value will also be
    // updated by 'clearEditValues()'

    if (this._lastEditRow == null || rowNum >= this._lastEditRow) {
        this._lastEditRow = rowNum
    }

},

// Helper method - gets the default value to show in an editor for a field (if no explicit value
// was specified).
getDefaultEditValue : function (fieldName, field) {
    var defaultValue = field.defaultValue;
    if (defaultValue == null && this.enumCriteriaAsInitialValues &&
        field.type == "enum" && this._filterValues != null&&
        this._filterValues[fieldName] != null)
    {
        // We could check the value against this.getEditorValueMap(fieldName, null)?
        defaultValue = this._filterValues[fieldName];
    }
    return defaultValue;
},

//>    @method listGrid.createEditSession() (I)
//
//  Internal method to initially set up internal, temporary edit values (and old, pre edit values)
//  for a record.
//  This method doesn't store these editValues - use 'setEditValues()' for that
//  These get updated as the record is edited, and cleared out when the edit is complete.
//
//  @group  editing
//  @visibility internal
//<

createEditSession : function (rowNum, colNum, record, editValuesID) {

    var editSession = {};
    if (record != null && record != Array.LOADING)
        editSession._primaryKeys = this.getPrimaryKeys(record);

    // A null primary key will imply we're editing a new record.
    // Assertion: when initializing edit data we will always be passed a rowNum

    editSession._rowNum = rowNum;
    if (this.useCellRecords) {
        editSession._colNum = colNum;

        if (this.getCellFacetValues) editSession._facetValues = this.getCellFacetValues(rowNum, colNum);
    }

    editSession._editValues = {};

    if (this._editSessions == null) this._editSessions = {};

    // generate an editValuesId if not passed one
    if (editValuesID == null) {
        if (this._currentEditValuesID == null) this._currentEditValuesID = 0;
        editValuesID = "_" + this._currentEditValuesID++;
    }

    this._editSessions[editValuesID] = editSession;

    // editRowMap used for retrieving edit sessions / row
    if (this._editRowMap == null) this._editRowMap = {};
    this._editRowMap[rowNum] = editValuesID;
    return editSession;
},


//>    @method listGrid.setEditValue()     ([A])
//
//  Modifies a field value being tracked as an unsaved user edit.<P>
//  Use for suggested or reformatted values for edits that remain unsaved.
//
//  @group  editing
//  @visibility external
//
//  @param  rowNum      (number)    Row number (or edit values ID)
//  @param  colNum      (number | string)    Column number of cell, or name of field
//                                           having editValue updated
//  @param  value       (any)       New value for the appropriate field.
//<


setEditValue : function (rowNum, colNum, newValue, suppressDisplay, suppressChange,
                         suppressSummaryRecalc, atomicValue)
{
    if (isc.isA.String(rowNum)) {
        this.logWarn("Warning: first parameter rowNum is a string, expecting a number");
        // bail if we have a bad rowNum
        return false;
    }

    var fieldName = isc.isA.String(colNum) ? colNum : this.getEditorName(rowNum, colNum, true);
    fieldName = this._trimDataPath(fieldName);
    if (isc.isA.String(colNum)) colNum = this.getFieldNum(colNum);

    // store the new edit value

    var changed = this._storeEditValue(rowNum, colNum, fieldName, newValue,
                           isc.CubeGrid && isc.isA.CubeGrid(this) ? true : suppressDisplay,
                                       suppressChange, suppressSummaryRecalc, atomicValue);

    // only proceed if there was a change
    if (!changed) return false;
    // If we were passed an atomic value, pick up the "opaque" equivalent now if necessary.
    if (atomicValue) newValue = this._getEditValue(rowNum, colNum, true);
    // If this is an edit value for a field with a specified displayField, and no
    // optionDataSource (so we're picking up the field's display value from the 'displayField' value
    // of the record), also update the edit value of the displayField on the record.


    var field = this.getField(fieldName);
    if (field && this._useDisplayFieldValue(field)) {
        var hasDisplayValue = false,
            displayValue;
        // Pick up the display value associated with this data-value from the edit-item if
        // possible.
        var editForm = this.getEditForm(),
            editItem = editForm ? editForm.getItem(fieldName) : null;

        if (editItem && this.getEditRow() == rowNum &&
            this.fieldValuesAreEqual(field, editForm.getValue(fieldName), newValue))
        {
            hasDisplayValue = true;
            displayValue = editItem.mapValueToDisplay(newValue);
        }
        // Otherwise, if we're looking at data from our own dataSource, reach into our
        // data array and see if we can find a record to lazily perform a mapping with
        // no fetch.
        if (!hasDisplayValue &&
            (field.valueField == null || field.valueField == field.name) &&
            (!field.optionDataSource ||
                isc.DS.get(field.optionDataSource) == this.getDataSource()) )
        {

            var data = this.data;
            if (isc.ResultSet && isc.isA.ResultSet(data)) data = data.localData;
            if (data) {
                var record = data.find(fieldName, newValue);
                if (record) {
                    hasDisplayValue = true;
                    displayValue = record[field.displayField];
                }
            }
        }
        if (hasDisplayValue) {
            // if displayValue was set to {formItem}.emptyDisplayValue, this means that data
            // value is empty (i.e. is null). {formItem} here stands for items that support
            // emptyDisplayValue, like SelectItem/ComboBoxItem/etc, for other items this check
            // would be always false, since emptyDisplayValue would be undefined.
            // So, we do not want to save emptydisplayValue (which can be set to some string as
            // part of UI) as displayField value if data value is empty (null), therefore we save
            // null as value for displayField as well.
            if (editItem && displayValue == editItem.emptyDisplayValue) displayValue = null;

            this.setEditValue(rowNum, field.displayField, displayValue, suppressDisplay, true);
        // Couldn't get a display value back. Log a warning so developers understand why the
        // displayValue doesn't show up.
        } else {
            if (this.warnOnUnmappedValueFieldChange) {
                this.logWarn("Edit value updated for field:" + fieldName
                    + ". This field has 'displayField' attribute specified as '"
                    + field.displayField
                    + ((field.optionDataSource == null ||
                        isc.DataSource.get(field.optionDataSource) == this.getDataSource())
                         ? "', and no unique optionDataSource, "
                         : "', ")
                    + "so display value is derived from the "
                    + "current record. In order to ensure the display value is updated to "
                    + "reflect the new edit-value for this field, developers can explicitly update the "
                    + "edit value for the display-field on this record. To avoid seeing this method "
                    + "set listGrid.warnOnUnmappedValueFieldChange to false.");
            }
        }
    }
    // If we're not supposed to update the display we're done.

    if (suppressDisplay) {
        if (!suppressSummaryRecalc) {
            // unless explicitly prohibited, if suppressDisplay is passed we do still want to
            // recalculate the summaries if present since the calling code never handles that
            this.calculateRecordSummaries(this.data.get(rowNum), null, true, true, true);
        }
        return true;
    }
    // Remember which field was most recently modified - this will be passed to callbacks as
    // the 'colNum' param
    this.setRowEditFieldName(rowNum, fieldName);

    if (!isc.isA.Number(rowNum)) {
        colNum = this.getEditSessionColNum(rowNum);
        rowNum = this.getEditSessionRowNum(rowNum);

    } else if (!isc.isA.Number(colNum)) {

        colNum = this.getFieldNum(colNum);
    }
    var vals = {};
    vals[fieldName] = newValue;
    this._displayNewEditValues(rowNum, colNum, vals);
    return true;
},

// store an edit value, firing editorChange() notification if there was a change.
// internal helper: call setEditValue() instead


_storeEditValue : function (rowNum, colNum, fieldName, newValue, suppressDisplay,
                            suppressChange, suppressSummaryRecalc, atomicValue)
{

    var changed = true, saveEqual = false,
        editSession, editValues, oldValue,
        undef;

    var dataPath = fieldName;
    // If we're working with a datapath, trim it (handles absolute dataPath being applied to field)

    if (dataPath.contains(isc.Canvas._$slash)) {
        dataPath = this._trimDataPath(dataPath);
    }

    editSession = this.getEditSession(rowNum, colNum)
    if (editSession != null) {
        editValues = editSession._editValues;
        // using getFieldValue() will allow us to use dataPath
        oldValue = isc.Canvas._getFieldValue(dataPath, null,
                                editValues, this, true, "edit");
    } else {
        // create a new set of edit values as necessary
        this.logInfo("creating new edit values for row: " + rowNum, "gridEdit");
        this.initializeEditValues(rowNum, colNum, !suppressDisplay, !suppressSummaryRecalc);
        editSession = this.getEditSession(rowNum, colNum);
        editValues = editSession._editValues;
    }

    // if there's no previous editValue, the old value is the original value from the
    // dataset
    if (oldValue === undef) {
        var record = this.getCellRecord(rowNum, colNum);
        oldValue = record ? isc.Canvas._getFieldValue(dataPath, null, record, this, true, "edit")
                          : null;

    } else saveEqual = true;



    var oldAtomicValue = oldValue, newAtomicValue = newValue;

    var field = this.getField(fieldName);
    if (field && field.type) {
        var simpleType = isc.SimpleType.getType(field.type);
        if (simpleType && simpleType.getAtomicValue != null) {
            oldAtomicValue = simpleType.getAtomicValue(oldValue, "compare");
            if (!atomicValue) newAtomicValue = simpleType.getAtomicValue(newValue, "compare");
        }
    }
    if (this.fieldValuesAreEqual(field, oldAtomicValue, newAtomicValue)) changed = false; // indicate no change

    // store the changed value
    // Note: If newValue was not passed in, clear the field value instead

    if (newValue === undef) {
        isc.Canvas._clearFieldValue(fieldName, editValues, this, true);
    } else if (saveEqual || changed) {
        // Don't pass the field in - we don't want to run any conversion based on type - the
        // value passed in is the actual value we want to save.
        isc.Canvas._saveFieldValue(dataPath, (atomicValue ? field : null),
                                              newValue, editValues, this, true);

        // If we were passed an atomic value, pick up the raw value we actually stored
        // before firing the change handler

        if (atomicValue) {

            newValue = isc.Canvas._getFieldValue(dataPath, null, record, this, true);
        }
    }

//     this.logWarn("edit value changed: oldValue: " + this.echo(oldValue) +
//                   ", new value: " + this.echo(newValue) +
//                   ", save equal?:" + saveEqual +
//                   " updated edit vals:" + this.echo(editValues), "gridEdit");
    // fire the change notification.
    if (changed && !suppressChange){
        this._editorChange(rowNum, colNum, newValue, oldValue);
    }

    return changed; // indicate change
},

// setRowEditFieldName() - used to track which field was last being edited for some set of edit values
setRowEditFieldName : function (rowNum, fieldName) {

    var editSession = this.getEditSession(rowNum);
    // Set up empty edit values if necessary
    if (!editSession) {
        var colNum = this.getColNum(fieldName);
        this.setEditValues([rowNum, colNum], null, true);
        editSession = this.getEditSession(rowNum);
    }

    if (isc.isA.Number(fieldName)) fieldName = this.getFieldName(fieldName);
    editSession._lastField = fieldName;
},

// getEditField() - given an edited row / valuesID, return the name of the last field being
// edited for that row
getRowEditFieldName : function (editValuesID) {
    var editSession = this.getEditSession(editValuesID);
    return (editSession ? editSession._lastField : null);
},

// getRowEditColNum - returns the colNum for the last edited field within some edit row
getRowEditColNum : function (editValuesID) {
    var fieldName = this.getRowEditFieldName(editValuesID);
    return fieldName ? this.getColNum(fieldName) : null;
},


//>    @method listGrid.getEditValue()
//
// Returns the current temporary locally stored edit value for some field within a record
// being edited.
//
// @param  rowNum  (number)    index of the row for which the editValue should be returned
// @param  colNum (number | string) index of the field, or fieldName, for which value should be
//                              returned
// @return (any)   edit value for the field in question
// @group  editing
// @visibility external
//<

getEditValue : function (rowNum, colNum) {

    var colID = colNum
    if (isc.isA.String(colNum)) colNum = this.getColNum(colNum);
    if (this._editorShowing &&
        (this.getEditRow() == rowNum) && (this.getEditCol() == colNum))
    {
        this.storeUpdatedEditorValue();
    }
    // Note pass the original column ID (colNum or fieldName) on to the _getEditValue() method
    // since there may be no column associated with the fieldName passed in
    return this._getEditValue(rowNum, colID, true);
},

// Retrieve the stored edit value for some row / field
// (unlike the public method, this will NOT first check for the value present in the editor
// if showing)
// returnOpaqueValue parameter: If the field for which we want the edit value is of a
// simpleType with a 'getAtomicValue()' method, should we call that and extract the atomic
// value or return the raw complex object.
// Most internal usage cares about the atomic value so by default we return that, but if
// this param is passed we'll return the complex object.
_getEditValue : function (rowNum, colNum, returnOpaqueValue) {
    // Note: Edit values are a case where the distinction between a value being unset and
    // being set to null is important... One case implies the user has not edited a cell,
    // the other implies the user has cleared out an edit value from a cell...
    var vals = this._getEditValues(rowNum, colNum);
    // Return undefined rather than explicit null if the edit row doesn't have any stored
    // values
    if (vals == null) return;

    var field = this.getField(colNum);
    // pass the field to _getFieldValue() so that method can apply the 'getAtomicValue' method
    // if necessary
    // Note: regardless of the "returnOpaqueValue" flag, we should give the underlying
    // SimpleType a chance to filter the value through getAtomicValue()
    if (field && !returnOpaqueValue) {
        return isc.Canvas._getFieldValue(null, field, vals, this, true, "edit");
    } else {

        // Convert the colNum to a fieldName to get the value
        var fieldID = this.getTrimmedFieldDataPath(field || colNum);
        return isc.Canvas._getFieldValue(fieldID, null, vals, this, true, "edit");
    }
},



//>    @method listGrid.clearEditValue()   ([A])
//
//  Clear a field value being tracked as an unsaved user edit.<P>
//  The saved record value will be displayed in the the appropriate cell instead.
//  Will also discard any validation errors for the specified field / row.
//
//  @group  editing
//  @visibility external
//
//  @param  editValuesID (number | object)    Row number, primary keys object for the record,
//                                              or editValues object
//  @param  colNum (number | string)    Column number, or Name of field for which
//                                      the value is to be cleared
//<
// @param   suppressDisplay (boolean)   Optional internal parameter to avoid updating the
//                                      affected cell to display the field value for the record
//                                      rather than the cleared out edit value.
// Note that in LGs the second parameter can be a fieldName
// If we're dealing with a 1 record / cell data model (EG CubeGrid) we need to
// be passed either a true editValuesId as the first parameter OR a colNum and a rowNum
clearEditValue : function (editValuesID, colNum, suppressDisplay, dontDropAll) {
    var rowNum = (isc.isA.Number(editValuesID) ? editValuesID
                                               : this.getEditSessionRowNum(editValuesID));

    var fieldName = colNum;
    if (isc.isA.Number(fieldName)) fieldName = this.getEditorName(rowNum, fieldName);
    else colNum = this.getColNum(fieldName);

    // If the user is currently editing this field, ensure the current value in the edit form
    // item is stored in this.editValues before clearing it, (so that the focused field is
    // successfully cleared!)
    if (this._editorShowing) {
        if (this.getEditRow() == rowNum && this.getEditFieldName() == fieldName) {
            // Suppress change - we're about to clear the value anyway!
            this.storeUpdatedEditorValue(true);
        }
    }

    //
    // This method will remove the edit value from the appropraite editValues object
    // If that was the last editValue in the object, fall through to clearEditValues to remove
    // the entire object
    // Otherwise, if suppressDisplay is not passed:
    // - if the field had validation errors, re-style the affected cell to hide the validation
    //   style (by default a red outline)
    // - if there is an editRowForm item for the effected cell, update it's value to reflect
    //   the value of the record
    // - otherwise call refresh cell to update the static innerHTML for the cell

    var editSession = this.getEditSession(editValuesID, colNum);
    //this.logWarn("clearEditValue got editSession: " + this.echo(editSession));

    if (editSession == null) return;

    var editValues = editSession._editValues,
        hasChanges = false,
        submittedValues = editSession._submittedValues,
        errors = editSession._validationErrors,
        hadErrors = errors && errors[fieldName],
        rowNum = editSession._rowNum;

    if (editValues != null) {
        var record = this.getRecord(rowNum);
        hasChanges = isc.propertyDefined(editValues, fieldName) &&
                     ((rowNum == null || record == null) ||
                         !this.fieldValuesAreEqual(this.getField(fieldName),
                                                   editValues[fieldName], record[fieldName]));

        delete editValues[fieldName];
        if (submittedValues) delete submittedValues[fieldName];
        if (errors != null) delete errors[fieldName];

        // If there's nothing in the editValues, clear the vals for the whole row

        if (!dontDropAll && isc.isAn.emptyObject(editValues)) {
            this.logDebug("no edit values left for row, discarding editSession", "gridEdit");
            return this._clearEditValues(editValuesID, colNum, suppressDisplay);
        }
    }

    // If this field has a specified 'display' field, we track edit values on that field that
    // match the record with the editValue specified for this field.
    // Drop the value on the display field as well.

    var field = this.getField(fieldName);
    if (field && this._useDisplayFieldValue(field) && (field.displayField != fieldName)) {
        this.clearEditValue(editValuesID, field.displayField, suppressDisplay, dontDropAll)
    }

    // If the edit val isn't displayed in a cell in this grid, we're done
    // otherwise continue to update the value if it changed, and clear any errors from the
    // cell.

    if (suppressDisplay || rowNum == null) return;

    // Update the display:
    // - If the edit form is showing for the row always set its value for the field
    // - If the cell is visible, but we're not showing an edit form item for it, refresh the
    //   cell to display the value of the record
    // Otherwise if we had errors, refresh the cell to hide the error icons
    if (hasChanges) {
        var editItemDrawn;
        if (this._editorShowing && this.getEditRow() == rowNum) {
            var newVal = record ? record[fieldName] : null;
            this._editRowForm.setValue(fieldName, newVal);
            editItemDrawn = colNum >= 0 && this.canEditCell(rowNum, colNum) &&
                            this._editRowForm.getItem(fieldName);
        }

        if (colNum >= 0 && !editItemDrawn) this.refreshCell(rowNum, colNum);

    // If we had validation errors for the cell, call 'refreshCellStyle' to clear the
    // error hilight from the cell (only necessary if we didn't refresh the entire cell)
    } else if (hadErrors && colNum >= 0) {
        if (hadErrors) this.showCellErrors(rowNum, colNum);
    }
},

//>    @method listGrid._clearEditValues()
//
//  Clear a whole row of values being tracked as an unsaved user edit.<P>
//  This should not be used for clearing out the edit values for some row after a save
//  has completed.  When a save has been successfully confirmed for some set of cell values,
//  the stored values should be cleared cell by cell using 'clearEditValue()', rather than
//  this method.
//
//  @group  editing
//  @visibility internal
//
//  @param  editValuesID      (number | object)    Identifier for the editValues to clear.
//                                                  This can be a rowNum, or a set of
//                                                  editValues (or a primary key)
//<
//  @param [colNum] (number) colNum of the record being edited. Only required if valuesID
//                           is passed in as a rowNum, and we're displaying one record per cell
//                          as in a +link{CubeGrid}
// @param   dontRefresh (boolean)   Optional internal parameter. If passed, don't update any
//                                  affected cells to display the removed edit values

_clearEditValues : function (editValuesID, colNum, dontRefresh) {
    // This method must
    // - update the central editSessions object to remove the specified set of edit values
    // - update the editRowMap if these values were mapped to a row.
    // - if this was a row beyond the end of the data for the List, shuffle subsequent
    //   temporary edit values down a rowNum
    // if refreshing to display the change:
    //   - if this was a row beyond the end of the list, it must be removed from the list:
    //      - if the editRowForm is showing for the row, it must be hidden
    //      - the body must be redrawn.
    //   - otherwise for each cell for which there was an edit value before this method must
    //     be updated.
    //      - if an edit form item is showing for the field
    //          - its value should be updated to show the record's value
    //          - if there were any validation errors for the row, the style of the relevant
    //            cells should be updated
    //      - otherwise the cell should be refreshed to show the record's value as static HTML.

    if (editValuesID == null) {

        return;
    }


    // If we're passed a rowNum, or primary keys, resolve to an editValuesID
    if (!isc.isA.String(editValuesID)) editValuesID = this.getEditValuesID(editValuesID, colNum);
    var editSession = this.getEditSession(editValuesID);


    // Bail if we can't get the object (not necessarily an error - may have already been
    // cleared)
    if (editSession == null) return;

    var rowNum = editSession._rowNum;
    if (rowNum != null) delete this._editRowMap[rowNum];
    // for cellRecords, ensure we have a valid rowNum and colNum
    if (colNum == null) colNum = editSession._colNum;

    var valuesObj = this._editSessions[editValuesID];
    // clear the object from the array
    delete this._editSessions[editValuesID];

    // Clear out the cached last edit row. Will be recalc'd lazily when required

    var lastRow = this._getLastEditRow();
    delete this._lastEditRow;
    // If this is a new record (not yet saved in the dataset), and we have any subsequent new
    // edit rows, we have to shuffle them down to fill the gap left by this record.
    if (rowNum != null && editSession._newRecord) {
        var editRow = this.getEditRow();

        for (var i = rowNum+1; i <= lastRow; i++) {
            var newRowObject = this.getEditSession(i);
            var oldRow = newRowObject._rowNum;
            newRowObject._rowNum = i-1;

            //this.logWarn("clearEditValues reordering temp rows." +
            //             " editSession: " + this.echo(newRowObject) +
            //             " was at row: " + oldRow);

            // No danger of putting this over another edit record, since we have already moved
            // every one.
            this._editRowMap[i-1] = this._editRowMap[i];
            delete this._editRowMap[i];
        }
        // If we're currently showing an editor for a subsequent temp-row, shift it up one row
        if (editRow != null && editRow > rowNum) this._editRowNum -= 1;

        //this.logWarn("clearEditValues: editRow was at: " + editRow +
        //             " now at: " + this._editRowNum);

        // We're clearing out edit values for an additional row beyond the end of the grid's data
        // refresh the UI if necessary
        if (!dontRefresh) {
            if (this._editorShowing) {
                    // if we're showing the inline editor for this row hide it now.
                    if (rowNum == editRow) this.hideInlineEditor();
                    // Otherwise if the edit row was AFTER this row, shuffle it up by 1 row to
                    // account for this row going away
                    else {
                        if (rowNum < editRow) this._moveEditor(editRow-1, null, "Earlier temp edit row removed");
                    }
            }
            // Redraw the body to hide the additional row.
            if (this.body) {
                var bodyContainer = this.frozenBody != null ? this.bodyLayout : this.body;
                bodyContainer.markForRedraw("clear edit values, remove row");
            }
        }

    } else if (rowNum != null && !dontRefresh) {
        // _displayNewEditValues takes an object describing the new edit values to display
        // Explicitly pass in the vals from the underlying record object so we update the
        // edit-form items
        var changedVals = {},
            record = this.data.get(rowNum);
        for (var fieldName in valuesObj._editValues) {
            changedVals[fieldName] = record ? record[fieldName] : null;
        }
        this._displayNewEditValues(rowNum, colNum, changedVals,
                                   valuesObj._validationErrors);
   }
},






//> @method listGrid.newRecordForEditValues()
// Internal method to associate a set of pending edit values with a new record in the
// dataset.<br>
// This method is required to handle the saving of a new edit row on the end of a list.
// When the save returns we need to associate the edit values with the record in the dataSet
// (which may also change the rowNum of the record).
// With background saving (waitForSave:false), after the attempted save was submitted to the
// server, further editing may have taken place.  This method will make sure any further edits
// get associated with the new record.
//
// @param editValuesId (ID)     id for the editValues which have been saved as a new record
// @param rowNum       (number) index of the newly saved record
// @visibility internal
//<


newRecordForEditValues : function (editValuesId, record) {


    var oldEditSession = this.getEditSession(editValuesId),
        editValues = oldEditSession._editValues;
    oldEditSession._primaryKeys = this.getPrimaryKeys(record);
    var pkFields = this.getDataSource().getPrimaryKeyFieldNames();
    for (var i = 0; i<pkFields.length; i++) {
        var field = pkFields[i];
        editValues[field] = record[field];
    }
    delete oldEditSession._newRecord;
},




// Re-associate all editValues with the appropriate rowNums for the records.
// Fired in response to dataChanged().
// This will be fired when the data is re-sorted, re-filtered, modified asynchronously from
// the server, or scrolled such that the cache is dropped when we have a partial cache.
// In any of these cases, the rowNums for each record can change (and in some cases rows can
// be entirely removed from the data cache)
// This method will attempt to match the editValues to the cached records after dataChanged()
// and update the rowNums as appropriate.  It will also take any editValues that represent
// newly created records and renumber them so they are past the new end of the dataset.
// If a record can't be found for some editValues object, there are a couple of possibilities:
// - If we have a complete cache (or local data), we know the record has gone from the dataSet
//   so we warn the user and clear out the editValues for that record
// - If we have a partial cache, the record may have gone from the resultSet (filter change,
//   for example), or it may be available in the resultSet, but not loaded in the client-side
//   cache (scrolled out of view).
//   In this case we keep the editValues around and set the _hasUnmatchedEdits flag on the
//   ListGrid.  getCellRecord() will then compare each record with the unmatched edits whenever
//   it is called, so we will reassociate the editValues with the appropriate rowNum as soon as
//   it is scrolled into view.
//   Note that editing will be dismissed if the record that was being edited vanishes in this
//   way.



// This method returns a boolean - true if any edit sessions were left intact and shifted to
// new rows, false otherwise.
_remapEditRows : function (dontMoveEditor) {

    // clear out the _lastEditRow and _hasUnmatchedEdits flags
    // - lastEditRow can get recalc'd lazily when required.
    // - we'll update _hasUnmatched edits below if required.
    delete this._lastEditRow;
    delete this._hasUnmatchedEdits;
    if (this._editSessions == null) return false;

    var remappedSession = false;

    var oldEditRow = this.getEditRow(),
        oldEditCol = this.getEditCol(),
        foundOldRecord = false;

    var lastRowNum = this.data.getLength(),
        editRowMap = {};

    // getKeys() first to avoid possible problems with key deletion during for..in (never
    // observed)
    var sessionList = isc.getKeys(this._editSessions);

    for (var i = 0; i < sessionList.length; i++) {


        var editValuesId = sessionList[i],
            editSession = this._editSessions[editValuesId];
        var oldRowNum = editSession._rowNum;

        var newCell = this._calculateEditCell(editSession, lastRowNum);
        // increment the lastRowNum if we added a row

        if (editSession._primaryKeys == null) lastRowNum ++;

        // newCell == null -> notification that we totally dropped an empty edit session
        if (newCell == null) continue;

        var newRowNum = newCell[0],
            newColNum = newCell[1];

        if (newRowNum != null && newRowNum >= 0) {

            if (newRowNum != oldRowNum) remappedSession = true;

            // we found the corresponding record in the modified dataset (or it was an unsaved,
            // newly created record)

            // Is this the current edit record?
            if (oldEditRow != null && oldEditRow == editSession._rowNum &&
                (!this.useCellRecords || (oldEditCol == editSession._colNum)))
            {
                foundOldRecord = true;
                // moveEditor will shift the edit form if necessary
                if (!dontMoveEditor) this._moveEditor(newRowNum, newColNum, "remapEditRows")
            }

            editRowMap[newRowNum] = editValuesId;

            editSession._rowNum = newRowNum;
            if (this.useCellRecords) editSession._colNum = newColNum;

        } else {

            if (oldRowNum != null && oldRowNum >= 0) remappedSession = true;

            var liveData = this.getOriginalData();
            var localSave = !this.dataSource || this.shouldSaveLocally() ||
                (isc.ResultSet && isc.isA.ResultSet(liveData) && liveData.allRowsCached());

            // drop the edit if the record being edited was clearly deleted
            // Don't blindly drop the record if we're grouped or data is a tree,
            // it may have just been hidden (folder closed) - in which case the rowNum
            // would be -1 -- use 'find' to see if the node is present in the tree
            // at all.
            if (localSave && !isc.isA.Tree(this.data) &&
                    (!this.isGrouped || !this.data.find(editSession._primaryKeys))
                )
            {
                    this.logWarn("Record:" + this.echo(editSession._primaryKeys) +
                       ", is no longer present in this List." +
                       "<br>Clearing edit values for this record.", "gridEdit");

                    delete this._editSessions[this.getEditValuesID(editValuesId)];
            } else {
                // the record could be either truly gone, or just not in the cache.  Hold onto
                // the edits hoping the record reappears (in which case we'll reassociate the
                // edit values with the record).
                // Note: If we're working with a paged result set, the data will typically have
                // disappeared because it has been scrolled out of view (due to data resort,
                // etc.) In this case it will reappear with scrolling.
                // However the data may have disappeared as it no longer matches the current
                // filter criteria.
                // We could also add a check for 'data.allMatchingRowsCached()' and handle the
                // case where we have loaded all the records for the current filter criteria
                // differently. We're much more likely to encounter the case of having loaded
                // all rows that match some criteria than caching every row in the data set.
                this.logInfo("Record:" + this.echo(editSession._primaryKeys) +
                    (localSave ? ", hidden in grouped tree data. " :
                                ", lost from local cache in paged result set. ") +
                    "Pending edits for this record will be maintained.", "gridEdit");
                delete editSession._rowNum;
                delete editSession._colNum;
                this._hasUnmatchedEdits = true;
            }
        }
    }
    // store the new editSession locations
    this._editRowMap = editRowMap;


    if (oldEditRow != null && !foundOldRecord && !dontMoveEditor) {
        this.hideInlineEditor(true);
    }
    return remappedSession;
},

// helper method called from dataChanged() to re-associate embeddedComponents stored in the
// grid body with the appropriate records
_remapEmbeddedComponents : function () {

    if (!this.body || this.body._embeddedComponents == null) return;

    if (this.showRecordComponents && this.recordComponentPoolingMode != "data") {
        this._remapVisibleEmbeddedComponents();
        return;
    }

    var components = this.body._embeddedComponents,
        componentCount = components ? components.length : 0,
        removeThese = [];

    // reset the _expandedRecordCount - we'll increment it if a currentRowNum is detected below,
    // and component.isExpansionComponent:true, rather than decrementing it if
    // the row is NOT present.  This handles the case of records eliminated from cache by
    // filtering.
    this._expandedRecordCount=0;
    for (var i = 0; i < componentCount; i++) {
        // get the keys for the record associated with the embeddedComponent
        var component = components[i],
            embeddedRecord = component.embeddedRecord,
            recordKeys = this.getPrimaryKeys(embeddedRecord),
            currentRowNum = component._currentRowNum,
            currentRecord = currentRowNum != null ? this.data.get(currentRowNum) : null,
            getRecord = false
        ;

        if (!(currentRecord === embeddedRecord)) {
            // only call findRowNum if the record has moved or changed
            currentRowNum = this.findRowNum(recordKeys);
            getRecord = true;
        }

        if (currentRowNum >= 0) {
            // only call data.get() for a second time if findRowNum was also called - otherwise
            // we already have the correct currentRecord
            if (getRecord) currentRecord = this.data.get(currentRowNum);
            // the record is present in the cache, rebuild the association between the record
            // and the embeddedComponent
            if (currentRecord && !this._hasEmbeddedComponents(currentRecord) ||
                    !this._getEmbeddedComponents(currentRecord).contains(component))
            {
                component._currentRowNum = currentRowNum;
                component.embeddedRecord = currentRecord;
                this._addEmbeddedComponent(currentRecord, component);

                if (component.isExpansionComponent) {
                    if (this.maxExpandedRecords == null ||
                        (this._expandedRecordCount < this.maxExpandedRecords))
                    {
                        this._expandedRecordCount++;
                        this._setExpanded(currentRecord, true);
                        this._setExpansionComponent(currentRecord, true);
                        if (!this.canExpandMultipleRecords) this._currentExpandedRecord = currentRecord;
                    } else {
                        // remove the 'expanded' marker from the row, and
                        // fire 'removeEmbeddedComponent' to clear up "embeddedComponent"
                        // markers on the record, etc.
                        // This will also clear the component - we still may want to
                        // destroy it below so also add to the "removeThese" list.
                        this._setExpanded(currentRecord, false);
                        this._setExpansionComponent(currentRecord, false);

                        this.removeEmbeddedComponent(currentRecord, component, true);
                        removeThese.add(component);
                        continue;
                    }
                } else {
                    // re-add to the "_recordComponents_" array on the record
                    this._addRecordComponent(currentRecord, component._currentFieldName, component);
                }
            }
        } else {
            // Note - even if indexOf returned -1,
            // the embeddedRecord may actually be non null here - either having been
            // dropped from this.data, or being a node in a closed parent in a tree.
            // Clear up the flags on the record object in this case as well.

            if (isc.isA.Tree(this.data)) {
                var pkVals;
                if (isc.ResultTree && isc.isA.ResultTree(this.data)) {
                    var pks = this.data.getDataSource().getPrimaryKeyFieldNames();
                    pkVals = isc.applyMask(embeddedRecord, pks);
                } else {
                    pkVals = embeddedRecord;
                }
                currentRecord = this.data.find(pkVals);
            }
            if (currentRecord == null) currentRecord = embeddedRecord;

            if (this._shouldRetainEmbeddedComponents == false ||
                    (this.showRecordComponents && this.recordComponentPoolingMode == "data") ||

                    (component.isExpansionComponent)
            ) {
                if (currentRecord != null) {
                    // This is essentially a dup of code form _removeEmbeddedComponent:
                    var ids = currentRecord[this._$embeddedComponentsPrefix + this.ID];
                    if (ids != null && ids.length > 0) {
                        ids.remove(component.getID());
                    }
                    if (ids && ids.length == 0) {
                        currentRecord[this._$embeddedComponentsPrefix + this.ID] = null;
                    }
                    // remove the 'expanded' marker from the row
                    if (component.isExpansionComponent) {
                        this._setExpanded(currentRecord, false);
                        this._setExpansionComponent(currentRecord, false);
                    }
                }
                // remove the embeddedComponent from the grid body and rehash the properties
                // that track expandedRecords
                removeThese.add(component);
            }
        }

    }
    if (removeThese.length>0) {
        // get rid of any embeddedComponents that no longer map to a cached row and update the
        // associated props that track expandedRecords
        for (var i = 0; i < removeThese.length; i++) {
            var item = removeThese[i];

            // only affect expandedRecord values if we're using expansionComponents - this loop
            // now also runs for recordComponents
            if (this.canExpandRecords) {
                if (this._currentExpandedRecord && this._currentExpandedRecord == item.embeddedRecord)
                    delete this._currentExpandedRecord;
            }
            this.body._embeddedComponents.remove(item);
            if (this.shouldDestroyOnUnembed(item, this._$dataChanged)) {
                item.markForDestroy();
            } else {
                item.deparent();
            }
        }
    }
},

// helper method called from remapEmbeddedComponents() when recordComponentPoolingMode is not "data"
_remapVisibleEmbeddedComponents : function () {

    if (!this.body || this.body._embeddedComponents == null) return;

    var allComponents = this.body._embeddedComponents,
        removeThese = [],
        drawArea = this.getDrawArea(),
        rowRange = [ drawArea[0], drawArea[1] ]
    ;

    if (rowRange[0] == null && rowRange[1] == null) return;

    // reset the _expandedRecordCount - we'll increment it if a currentRowNum is detected below,
    // and component.isExpansionComponent:true, rather than decrementing it if
    // the row is NOT present.  This handles the case of records eliminated from cache by
    // filtering.
    this._expandedRecordCount=0;

    var remappedCount = 0,
        inPlaceCount = 0
    ;

    for (var i=rowRange[0]; i<=rowRange[1]; i++) {
        var record = this.data.get(i),
            recordEntries = record ? this._getRecordComponents(record) : null,
            components = recordEntries ? isc.getValues(recordEntries) : null
        ;

        if (components) {
            for (var j=0; j<components.length; j++) {
                var component = components[j];
                if (!component || component.isNullMarker) continue;
                if (component._currentRowNum == i) {
                    // record is still in the same place - just ignore it
                    component._detectedInLoop = true;
                    inPlaceCount++;
                    continue;
                }

                component._currentRowNum = i;
                this._addEmbeddedComponent(record, component);
                component._detectedInLoop = true;
                remappedCount++;

                if (component.isExpansionComponent) {
                    if (this.maxExpandedRecords == null ||
                        (this._expandedRecordCount < this.maxExpandedRecords))
                    {
                        this._expandedRecordCount++;
                        this._setExpanded(record, true);
                        this._setExpansionComponent(record, true);
                        if (!this.canExpandMultipleRecords) this._currentExpandedRecord = record;
                    } else {
                        // remove the 'expanded' marker from the row, and
                        // fire 'removeEmbeddedComponent' to clear up "embeddedComponent"
                        // markers on the record, etc.
                        // This will also clear the component - we still may want to
                        // destroy it below so also add to the "removeThese" list.
                        this._setExpanded(record, false);
                        this._setExpansionComponent(record, false);

                        this.removeEmbeddedComponent(record, component, true);
                        removeThese.add(component);
                        continue;
                    }
                }
            }
        }

    }

    if (this.recordComponentPoolingMode != "data") {
        for (var i = 0; i < allComponents.length; i++) {
            var c = allComponents[i];
            if (c._detectedInLoop) delete c._detectedInLoop;
            else {
                // don't remove rollOver/UnderCanvas
                if (c == this.currentRollOverCanvas || c == this.currentRollUnderCanvas) continue;
                if (c.isBackgroundComponent || c.isExpansionComponent) {
                    // if this is an expansion component, only remove the component if the
                    // stored record is no longer in the grid - prevents us from removing
                    // components because an expanded record was scrolled out of view
                    var r = this.getRecordIndex(c.embeddedRecord);
                    if (r >= 0) {
                        if (c.isExpansionComponent) this._expandedRecordCount++;
                    } else {
                        if (c.isExpansionComponent && c.embeddedRecord) {
                            // the component is from a record which is no longer present -
                            // remove expansionComponent references
                            this._setExpanded(c.embeddedRecord, false);
                            this._setExpansionComponent(c.embeddedRecord, false);
                        }
                        removeThese.add(c);
                    }
                } else if (c.isRecordComponent) {
                    removeThese.add(c);
                }
            }
        }
    }

    if (removeThese.length>0) {
        // get rid of any embeddedComponents that no longer map to a cached row and update the
        // associated props that track expandedRecords
        for (var i = 0; i < removeThese.length; i++) {
            var item = removeThese[i];

            // only affect expandedRecord values if we're using expansionComponents - this loop
            // now also runs for recordComponents
            if (this.canExpandRecords) {
                if (this._currentExpandedRecord && this._currentExpandedRecord == item.embeddedRecord)
                    delete this._currentExpandedRecord;
            }
            this.body._embeddedComponents.remove(item);
            if (this.shouldDestroyOnUnembed(item, this._$dataChanged)) {
                item.markForDestroy();
            } else {
                item.deparent();
            }
        }
    }

    //this.logWarn("remappedCount = " + remappedCount + "\ninPlaceCount = " + inPlaceCount);
},

// Helper method to update all per-cell embedded components' "_currentColNum" based on the stored
// field name for the component
// Called per body when we reorder fields, show/hide fields etc.
// Note: this method updates all embedded components. It doesn't re run any specific
// recordComponent logic to re-create recordComponents on column reorder, or to re-pool components
// that are no longer visible. This is all handled by updateRecordComponents() which fires on
// redraw.
_$columnRemap:"column remap",
_remapEmbeddedComponentColumns : function (body) {

    if (body._embeddedComponents == null) return;
    var components = body._embeddedComponents,
        fieldMap = {},
        changes = false;
    for (var i = 0; i < body.fields.length; i++) {
        fieldMap[body.fields[i].name] = i;
    }
    // clear embedded components that are no longer visible once we've iterated through
    // this loop to avoid the array length being effected.
    var componentsToClear = [];
    for (var i = 0; i < components.length; i++) {
        var fieldName = components[i]._currentFieldName;
        // this is expected - it's not a per-cell component.
        if (fieldName == null) {
            continue;
        }
        var colNum = fieldMap[fieldName];


        // If the field has been hidden just remove the embedded component.

        if (colNum == null) {
            componentsToClear[componentsToClear.length] = components[i];
        } else {
            if (components[i]._currentColNum != colNum) {
                changes = true;
                components[i]._currentColNum = colNum;
            }
        }
    }
    if (componentsToClear.length > 0) {
        for (var i = 0; i < componentsToClear.length; i++) {
            var comp = componentsToClear[i];
            if (comp.removeOnHideField) {
                body.removeEmbeddedComponent(comp.embeddedRecord, comp);
                if (this.shouldDestroyOnUnembed(comp, this._$columnRemap)) {
                    comp.markForDestroy();
                }
                // already cleared by removeEmbeddedComponent() so no need to clear() or
                // deparent() here if we didn't mark for destroy
            } else {

                changes = true;
            }
        }
    }

    if (changes) {
        body._placeEmbeddedComponents();
    }
},

// calculateCell - helper for remapEditRows - given an editSession determine which cell it
// currently belongs to so we can update editSession._rowNum/colNum etc
_calculateEditCell : function (editSession, lastRowNum) {

    var newRowNum, newColNum,
        pk = editSession._primaryKeys;

    // in the case of a new edit row, we want to just make sure the rowNums are off the
    // end of the data...
    if (pk == null) {
        newRowNum = lastRowNum;

    } else {

        // if the editValues consist of just primary keys, drop them unless this is the
        // editSession for the current editRow

        var editValues = editSession._editValues,
            hasEdits = false,
            undef;
        if (editSession._validationErrors != null &&
            !isc.isA.emptyObject(editSession._validationErrors_))
        {
            hasEdits = true;
        } else {
            for (var val in editValues) {
                if (editValues[val] != pk[val] || pk[val] === undef) {
                    hasEdits = true;
                    break;
                }
            }
        }

        if (!hasEdits && editSession._rowNum != null &&
            !this.isEditingRecord(editSession._rowNum, editSession._colNum))
        {
            this.logInfo("dropping empty editSession", "gridEdit");
            delete this._editSessions[this.getEditValuesID(editSession)];
            return null;
        }

        // look for a row with matching primary keys
        newRowNum = this.findRowNum(pk, editSession);
        // colNum will only apply when the data model shows 1 record/cell (EG CubeGrid);
        newColNum = this.findColNum(pk, editSession);
    }

    return [newRowNum,newColNum];
},

// Override _filter
// - if we have pending edits, changing the filter criteria means that the
//   edited rows may not show up in the returned set of values.
//   For windowed result sets this is true even if we're broadening our criteria, as the edited
//   row may not be in our viewport.
//   In this case if confirmDiscardEdits is true, show a dialog giving the user a chance to
//   save or discard unsaved edits.
// - if we are showing a filterEditor, update the filterEditor values to show the new criteria
_filter : function (type, criteria, callback, requestProperties, doneSaving) {
    if (!doneSaving && this.confirmDiscardEdits && this.dataSource != null) {
        var data = this.getOriginalData();

        if (this.hasChanges() &&
            // If the criteria is unchanged, the filter will be a no-op. In this case
            // no need to drop edit values

            !(isc.ResultSet && isc.isA.ResultSet(data) &&
                  data.compareCriteria(criteria, data.getCriteria()) == 0) )
        {
            this.showLostEditsConfirmation({target:this, methodName:"_continueFilter"},
                                           {target:this, methodName:"_cancelFilter"});
            this._filterArgs = {
                type:type, criteria:criteria, callback:callback,
                requestProperties:requestProperties
            }
            return;
        }
    }
    // Apply criteria to filterEditor.

    // If passed a filter component, resolve to actual filter values!
    var criteriaValues = criteria
    if (isc.isA.Class(criteriaValues)) {
        criteriaValues = isc.DynamicForm.getFilterCriteria(criteriaValues);
    }

    this.setFilterValues(criteriaValues, true);

    return this.Super("_filter", [type,criteria,callback,requestProperties], arguments);
},

// Completes a filter after the user has been prompted to save or discard pending edits.
_continueFilter : function () {
    var args = this._filterArgs,
        type = args.type, criteria = args.criteria, callback = args.callback,
        requestProperties = args.requestProperties;
    delete this._filterArgs;
    this._filter(type, criteria, callback, requestProperties, true);
},

// Fired when a user cancelled a filter to resolve pending edits.
_cancelFilter : function () {
    delete this._filterArgs;
},

// showLostEditsConfirmation() - this method is fired from sort / filter if there are
// outstanding unsaved edits that may be otherwise lost by the sort/filter action.
// Shows the user a dialog with options to save, discard, or cancel.
showLostEditsConfirmation : function (continueCallback, cancelCallback) {
    // these callbacks will be fired, then cleared up when the callback fries from the
    // global warn dialog.
    this._continueCallback = continueCallback;
    this._cancelCallback = cancelCallback;

    isc.confirm(this.confirmDiscardEditsMessage,
                "if(window[this.targetGridID])window[this.targetGridID].lostEditsCallback(value, this);",
                {   targetGridID:this.getID(),


                    // Show "OK", "Save" and "Cancel" buttons
                    buttons:[isc.Dialog.OK,
                             {title:this.discardEditsSaveButtonTitle, width:75,
                              click:"this.hide();this.topElement.returnValue('save');"},
                             isc.Dialog.CANCEL]
                });
},

// Fired as a callback from the user selecting ok/save/cancel from the dialog displayed in
// showLostEditsConfirmation().
// Will save or discard edits as appropriate, and call any callback (used to fire the action
// after the values have been saved / discarded).
lostEditsCallback : function (value, dialog) {
    var continueCallback = this._continueCallback,
        cancelCallback = this._cancelCallback;
    delete this._continueCallback;
    delete this._cancelCallback;

    // CancelClick
    if (value == null) {
        this.fireCallback(cancelCallback);

    // Ok click
    } else if (value == true) {
        this.discardAllEdits();
        this.fireCallback(continueCallback);

    // save click
    } else if (value == "save") {
        // Pass continueCallback into saveAllEdits - we want to allow the
        // original action to continue after the save completes
        this.saveAllEdits(null, continueCallback);
    }

    // clear out the targetGrid pointer from the global warn dialog
    dialog.targetGrid = null;
},

// Given the primary keys for some record, return the rowNum.

findRowNum : function (primaryKeys) {
    // If a DataSource is present and the data object is an Array, use findByKeys on the
    // DataSource to compute index; otherwise, call indexOf on the data using primaryKeys.
    // Checking whether the data is an Array avoids both ResultSet and ResultTree cases.

    var dataSource = this.getDataSource(),
        isAnArray = isc.isA.Array(this.data);

    // If the record is not cached, this will return -1
    if (dataSource && isAnArray) return dataSource.findByKeys(primaryKeys, this.data);
    else                         return this.data.indexOf(primaryKeys);
},

// Given the primary keys for some record return the colNum of the cell displaying the
// record
// Required to support 1 record / cell data model (EG cubeGrid)
findColNum : function (primaryKeys) {
    // ListGrids never show 1 record/cell so just return -1
    return -1;
},




_moveEditor : function (rowNum, colNum, reason) {

    if (!this._editorShowing || rowNum == this._editRowNum) {
        return;
    }

    var previousRow = this._editRowNum;

    this.logInfo(reason + ": editSession: " + this.getEditValuesID(rowNum) +
                 " with values: " + this.echo(this._getEditValues(rowNum, colNum)) +
                 " was being edited at row: " + previousRow +
                 ", will now edit at row: " + rowNum, "gridEdit");
    this._editRowNum = rowNum;

    var editForm = this._editRowForm,
        items = editForm ? editForm.getItems() : null;
    if (items) {
        for (var i = 0; i < items.length; i++) {
            items[i].rowNum = rowNum;
        }
    }

},

//> @method listGrid.rowEditNotComplete()
//  Given a rowNum, return true if we have locally stored editValues for the row.  This implies
//  that it is the current edit row (an editor is showing in that row), or that it has not been
//  saved since it was edited - likely due to save / validation errors.
//  @group  editing
//  @visibility internal
//  @param  rowNum (number) index of row for which we want to determine 'edit status'
//  @return (boolean)   true if we still have edit values for the row
//<
rowEditNotComplete : function (rowNum) {
    return (this._editRowMap != null && this._editRowMap[rowNum] != null);
},

// ---------------------------------------------------------------------------------------

_editorChange : function (rowNum, colNum, newValue, oldValue) {

    var record = this.getCellRecord(rowNum, colNum);
    if (this.editorChange != null) this.editorChange(record,newValue,oldValue,rowNum,colNum);
    //>!BackCompat 2004.12.10
    var fieldName = this.getFieldName(colNum);
    if (this.editValueChanged != null)
        this.editValueChanged(rowNum, fieldName, newValue, oldValue);
    //<!BackCompat


    // If we are providing dynamic edit item value maps for any fields, they may return a
    // different value since their value may be dependant on the edited field.
    // Therefore we go through and determine whether we need to update the valueMap for any
    // form items visible in the rowEditForm.

    if (this._editorShowing && this.isEditingRecord(rowNum, colNum) && this.fields != null) {
        record = isc.addProperties({}, this._getEditValues(rowNum, colNum), record);
        var fields = !this.editByCell ? this.fields : [this.getEditField()];


        for (var i =0; i < fields.length; i++) {
            var field = fields[i],
                formItem = this._editRowForm.getItem(field[this.fieldIdProperty]);

            // We don't create form items for unrendered fields if we're incrementally rendering
            if (formItem == null) continue;

            var valueMap = this.getEditorValueMap(field, this.getEditedRecord(rowNum, colNum, true));

            if (formItem.valueMap != valueMap) {
                var changed = true;
                if (isc.isAn.Array(valueMap) && isc.isAn.Array(formItem.valueMap)) {
                    changed = !valueMap.equals(formItem.valueMap);

                } else if (isc.isAn.Object(valueMap) && isc.isAn.Object(formItem.valueMap)) {
                    if (isc.getKeys(valueMap).equals(isc.getKeys(formItem.valueMap))) {
                        changed = false;
                        for (var key in valueMap) {
                            if (valueMap[key] != formItem.valueMap[key]) {
                                changed = true;
                                break;
                            }
                        }
                    }
                }
                // we're rerunning getEditorValueMap on every change of the item.
                // Its very likely that the vm will be a different object but meaningfully unchanged
                // - check for this since we don't want to call setValueMap() if unnecessary.
                // This causes a side-effect where if a multiple:true pickList is showing it'd
                // appear to auto-dismiss on every selection change.
                if (changed) formItem.setValueMap(valueMap);
            }
        }
    }
},


//>    @method listGrid.getEditRow()
// Returns the index of the row being edited or null if there is no current edit row.
//
// @return (int) Index of the current edit row
// @group  editing
// @visibility external
//<
getEditRow : function () {
    return this._editRowNum;
},

//>    @method listGrid.getEditCol()
// Returns the index of the column being edited or null if there is no edit col.
//
// @return (int) index of the current edit column
// @group  editing
// @visibility external
//<
getEditCol : function () {
    return this._editColNum;
},

//>    @method listGrid.getEditField()
//
//  Returns the field object associated with cell currently being edited
//
//  @group  editing
//  @visibility external
//
//  @return (object)    Field object definition
//<
getEditField : function () {
    return this.getField(this.getEditCol());
},

//>    @method listGrid.getEditFieldName()
//
//  Returns the field name for the current edit field
//
//  @group  editing
//  @visibility internal
//
//  @return (string) Field name
//<
getEditFieldName : function () {
    return this.getFieldName(this.getEditCol());
},


//>    @method listGrid.getEditRecord()
//
//  Returns the record object currently being edited
//
//  @group  editing
//  @visibility internal
//
//  @return (object)    Current edit record object
//<
getEditRecord : function () {
    return this.getCellRecord(this.getEditRow(), this.getEditCol());
},

//>    @method listGrid.cancelEditing()
//
//  Cancel the current edit without saving.
//
//  @group  editing
//  @visibility external
//<
// Called directly from cellEditEnd with escape keypress, etc.
// Exposed so may also be called programmatically by developers
cancelEditing : function (editCompletionEvent) {
    if (!this._editorShowing) return;

    // fall through to 'discardEdits' to drop the edit values and hide the editor
    // Note: that method also catches the case where alwaysShowEditors is true and avoid hiding
    // the editor.
    this.discardEdits(this.getEditRow(), this.getEditCol(), false, editCompletionEvent);

},

//>    @method listGrid.cellEditEnd()
//
//  Method called on when editing ends for a cell as a result of a user interaction.
//  <P>
//  Editing may continue in another cell or row or end entirely, depending on the
//  editCompletionEvent, which represents what action the user took to end editing.
//  <P>
//  If this.shouldCancelEdit(rowNum, colNum, editCompletionEvent) returns true, the edit is
//  cancelled.
//  <P>
//  Otherwise the edit will be saved, and we'll proceed to a new cell to edit based on
//  getNextEditCell(rowNum, colNum, editCompletionEvent).
//  <P>
//  Return false from this method to notify the caller that the intended behavior failed -
//  typically this cancels the cell edit end (restoring focus to the appropriate edit-field).
//
//  @group  editing
//  @visibility internal
//
//  @param  editCompletionEvent (EditCompletionEvent)  Edit completion event constant
//                                                          indicating how edit was
//                                                          completed
//  @param  [newValue]    (any)   New Value for the edited cell. If not passed will be derived
//                                from current editor value for the edited cell.
//  @return (boolean)    False if the user should still be editing the current cell.
//<
cellEditEnd : function (editCompletionEvent, newValue) {


    // delete the _editSessionFromKeypress flag when editing completes unless the user is
    // simply using the keyboard to shift to another cell
    if (this._editSessionFromKeyPress &&
        editCompletionEvent != isc.ListGrid.TAB_KEYPRESS &&
        editCompletionEvent != isc.ListGrid.SHIFT_TAB_KEYPRESS &&
        editCompletionEvent != isc.ListGrid.UP_ARROW_KEYPRESS &&
        editCompletionEvent != isc.ListGrid.DOWN_ARROW_KEYPRESS &&
        editCompletionEvent != isc.ListGrid.LEFT_ARROW_KEYPRESS &&
        editCompletionEvent != isc.ListGrid.RIGHT_ARROW_KEYPRESS) delete this._editSessionFromKeyPress

    var rowNum = this.getEditRow(),
        colNum = this.getEditCol();
    if (rowNum == null && colNum == null) return;

    // If we were passed a new value, store it as an editValue
    // (Otherwise the next call to getEditValue() / getEditValues()
    //  [changeEditCell / saveAndHideEditor] will pick up the current edit value
    //  from the form item)
    if (arguments.length >= 2) {
        this.setEditValue(rowNum, colNum, newValue);
    }

    //this.logInfo("cellEditEnd: editValues: " + this.echo(this.getEditValues(rowNum)) +
    //             ", old values: " + this.echo(this.getRecord(rowNum)), "gridEdit");


    var fieldName = this.getFieldName(colNum),
        shouldCancelEdit = this.shouldCancelEdit(rowNum, colNum, editCompletionEvent);
    if (shouldCancelEdit) return this.cancelEditing(editCompletionEvent);



    // get the next editCell (if null, we're done with the edit)
    var nextEditCell = this.getNextEditCell(rowNum, colNum, editCompletionEvent);
    // The 'newEditCell' is used by getNextEditCell.  Clear it out now, since we've determined
    // which cell we'll be editing.
    this.clearNewEditCell();

    // if we've no next cell to go to, we're done editing
    if (nextEditCell == null) {
        //>DEBUG
        this.logInfo("cellEditEnd: ending editing, completion event: " + editCompletionEvent,
                     "gridEdit"); //<DEBUG
        this._saveAndHideEditor(editCompletionEvent);
        return;
    }

    // prevent editing of the loading marker.  Can happen with rapid tabbing / shift-tabbing
    // with non-blocking row loading
    if (Array.isLoading(this.getCellRecord(nextEditCell[0], nextEditCell[1]))) {
        //this.logWarn("prevented edit of loading row at: " + nextEditCell);
        return false;
    }
    this._changeEditCell(editCompletionEvent, rowNum, colNum, nextEditCell[0], nextEditCell[1]);
},


// getUpdatedEditorValue()
// Retrieves the value from the current edit cell editor item
// NOTE: returns the parsed version [can be saved as a raw value to the record]
getUpdatedEditorValue : function () {
    // If we're not showing an editor return undefined - there is no edit value to save.
    if (!this._editorShowing) return;

    var editItem = this._editRowForm.getItem(this.getEditFieldName()),
        editField = this.getEditField(),
        value;
    if (editItem) {

        if (editItem._itemValueIsDirty()) editItem.updateValue();
        value = editItem.getValue();
        return this._parseEditorValue(value, editField,this.getEditRow(), this.getEditCol());

    } else {
        // no edit item, just return the saved value
        return this.getEditDisplayValue(this.getEditRow(), this.getEditCol());
    }
},

// storeUpdatedEditorValue()
// Retrieves the value from the current edit cell editor item and stores it as an edit value
// for the appropriate row.
storeUpdatedEditorValue : function (suppressChange, editCol) {
    if (!this.isDrawn() || !this._editorShowing) return;


    var editRow = this.getEditRow();

    if (editCol == null) editCol = this.getEditCol();


    var editField = this.getField(editCol),
        editForm = this._editRowForm,
        editItem = this.getEditFormItem(editCol),
        value;
    if (editItem) {
        if (editItem._itemValueIsDirty()) editItem.updateValue();

        var itemValue = editItem.getValue();
        value = this._parseEditorValue(itemValue,
                                         editField, editRow, editCol);
        // Convert undefined to explicit null so we actually clear out the value
        // for the field if appropriate.

        var undefined;
        if (value === undefined) value = null;


        var oldVal = this._getEditValue(editRow, editCol),
            undef;
        if (oldVal === undef || !this.fieldValuesAreEqual(editField, oldVal, value)) {
            var type = isc.SimpleType.getType(editField.type);
            if (type && type.getAtomicValue != null) {
                // calling form.getValue(fieldName) rather than item.getValue() will give
                // us the opaque value - what we actually want to store.
                var opaqueValue = editForm.getValue(editItem.name);
                // If parseEditorValue changed it, we'll have to apply the change to this
                // opaqueValue of course.

                if (itemValue != value && type.updateAtomicValue != null) {
                    type.updateAtomicValue(value, opaqueValue);
                }
                value = opaqueValue;
            }
            this.setEditValue(editRow, editCol, value, true, suppressChange);
        }
    }
},

// Internal handler to fire user-defined editorExit handler functions
_handleEditorExit : function (editCompletionEvent, rowNum, colNum, newValue) {
    var record = this.getCellRecord(rowNum, colNum),
        field = this.getField(colNum),
        editFieldName = this.getEditorName(rowNum, field),
        returnVal = true;

    var item = this._editRowForm.getItem(editFieldName);
    //>PopUpTextAreaItem         If this is a pop-up text area, ensure the T.A. is hidden.
    if (isc.isA.PopUpTextAreaItem(item)) item.hidePopUp();  //<PopUpTextAreaItem

    // clear any selection if we're in a CubeGrid
    if (isc.CubeGrid && isc.isA.CubeGrid(this) && this.selection &&
            this.selection.anySelected()) this.selection.deselectAll();

    // NOTE: field can be null if hiding focused field

    if (field && field.editorExit != null) {
        isc.Func.replaceWithMethod(field, "editorExit",
                                     "editCompletionEvent,record,newValue,rowNum,colNum,grid");
        returnVal =
            (field.editorExit(editCompletionEvent, record, newValue, rowNum, colNum, this)
             != false);
    }
    if (returnVal && this.editorExit != null)
        returnVal = this.editorExit(editCompletionEvent, record, newValue, rowNum, colNum) != false;

    return returnVal;
},

// fired when the user leaves a row
_handleRowEditorExit : function (editCompletionEvent, rowNum, newValues) {

    var record = this.getRecord(rowNum);
    if (this.rowEditorExit != null)
        return (this.rowEditorExit(editCompletionEvent, record, newValues, rowNum) != false);
    // In this case there was no rowEditorExit handler defined.
    return true;

},

// Internal handler to fire user-defined editorEnter handler functions
_handleEditorEnter : function (editorItem, rowNum, colNum, value) {

    var record = this.getCellRecord(rowNum, colNum),
        fieldName = this.getFieldName(colNum),
        field = this.getField(colNum),
        returnVal = true;


    //>PopUpTextAreaItem
    // If this is a pop-up textArea editor, show the pop-up if appropriate

    if (isc.isA.PopUpTextAreaItem(editorItem) && editorItem.popUpOnEnter) {
        // pass in the param to put focus into the pop-up
        editorItem.showPopUp(true);
    }   //<PopUpTextAreaItem


    if (field && field.editorEnter != null) {
        isc.Func.replaceWithMethod(field, "editorEnter", "record,value,rowNum,colNum,grid");
        returnVal = field.editorEnter(record, value, rowNum, colNum, this) != false;
    }
    if (returnVal && this.editorEnter != null)
        returnVal = this.editorEnter(record, value, rowNum, colNum) != false;
    return returnVal;
},

_handleRowEditorEnter : function (editorItem, rowNum, editValues) {
    var record = this.getRecord(rowNum);
    if (this.rowEditorEnter != null)
        return this.rowEditorEnter(record, editValues, rowNum) != false;
    return true;
},

_validationEnabled : function () {
    if (!isc.DS) return false;
    var ds = isc.DS.get(this.dataSource);
    return !this.neverValidate &&

                      !(ds && ds.useLocalValidators != null &&
                        ds.useLocalValidators == false);
},

// These methods determine what action should be taken on cell edit completion

//>    @method listGrid.shouldSaveOnCellExit()
//  When a user exits a field within the current row being edited, but does not exit the edit
//  row, should a save be performed?<br>
//  Default behavior is <br>
//  - Never saveCellOnExit if this is a new edit row (has no corresponding record)<br>
//  - return this.saveByCell if defined, otherwise return true if the user is editing
//    individual cells, false if the entire row is editable (set via the editByCell property).
//
//  @group  editing
//  @visibility internal
//
//  @param  rowNum      (number)    Index of the row being edited
//  @param  colNum      (number)    Index of the column being edited
//  @return (boolean)    true if the value should be saved when moving to a different field within
//                       the edit row.
//  @see saveByCell
//  @see editByCell
//<
shouldSaveOnCellExit : function (rowNum, colNum) {
    if (this.getCellRecord(rowNum, colNum) == null) return false;
    return (this.saveByCell != null ? this.saveByCell : this.editByCell);
},

//>    @method listGrid.shouldSaveOnRowExit()
//
//  When a user exits the current edit row, should a save be performed.
//  Always returns true.
//
//  @group  editing
//  @visibility internal
//
//  @param  rowNum      (number)    Index of the row being edited
//  @param  colNum      (number)    Index of the column being edited
//  @param  editCompletionEvent (EditCompletionEvent) what caused the edit completion
//  @return (boolean)    true if the value should be saved when moving to a different field within
//                       the edit row.
//<
shouldSaveOnRowExit : function (rowNum, colNum, editCompletionEvent) {
    return true;
},

//>    @method listGrid.shouldValidateByCell()
//
//  When a user exits a field within the current row being edited, but does not exit the edit
//  row, should validation be performed.<br>
//  Default behavior is to return this.validateByCell if defined, otherwise return true if
//  the user is editing indivdual cells, false if the entire row is editable (set via the
//  editByCell property).<br>
//  Note that all edit-validation will be suppressed if listGrid.neverValidate is set to
//  true.
//
//  @group gridValidation
//  @visibility internal
//
//  @param  rowNum      (number)    Index of the row being edited
//  @param  colNum      (number)    Index of the column being edited
//  @return (boolean)    true if validation should be performed when moving to a different field
//                       within the edit row.
//  @see listGrid.validateByCell
//  @see listGrid.autoValidate
//<
shouldValidateByCell : function (rowNum, colNum, editCompletionEvent) {
    var field = this.getField(colNum);
    if (field && field.validateByCell != null) return field.validateByCell;

    return (this.validateByCell != null ? this.validateByCell : this.editByCell);
},


//>    @method listGrid.shouldValidateByRow()
// Should we perform validation on the current edit row when the user navigates away from it,
// even if the row is not being saved (due to +link{listGrid.autoSaveEdits} being false)?
// Default implementation returns +link{listGrid.autoValidate}.
//  @param  rowNum      (number)    Index of the row being edited
//  @param  colNum      (number)    Index of the column being edited
//  @return (boolean)    true if validation should be performed when moving to a different field
// @group gridValidation
// @see listGrid.autoValidate
// @see listGrid.autoSaveEdits
//<
shouldValidateByRow : function (rowNum, colNum, editCompletionEvent) {
    // Note: Method will only be called if _validationEnabled() returned true, so we
    // don't need to check this.neverValidate.
    return this.autoValidate;
},

//>    @method listGrid.shouldCancelEdit()
//
//  On completion of an edit via some editCompletionEvent, should the edited values be discarded,
//  rather than being saved.
//  Default implementation returns true if the editCompletionEvent was a keypress on the escape
//  key and the escapeKeyEditAction was set to cancel.
//  Override for different cancellation behavior.
//
//  @group  editing
//  @visibility internal
//
//  @param  rowNum      (number)    Index of the row being edited
//  @param  colNum      (number)    Index of the column being edited
//  @param  editCompletionEvent (EditCompletionEvent)  Event that completed the edit
//  @return (boolean)    true if the save should be cancelled
//  @see getNextEditCell()
//<
shouldCancelEdit : function (rowNum, colNum, editCompletionEvent) {
    if (editCompletionEvent == isc.ListGrid.ESCAPE_KEYPRESS) {
        var field = this.getField(colNum),
            action = field.escapeKeyEditAction || this.escapeKeyEditAction;
        if (action == "cancel") return true;
    }
    return false;
},

// Also see 'getNextEditCell()'


// --  Methods to perform the mechanics of saving / validation / etc.

//>    @method listGrid._killEdit()
//
//  Complete editing by hiding the cell editor, and clearing out any edit information.
//  Public API for this is 'cancelEditing()'
//
//  @group  editing
//  @visibility internal
//
//  @param  editingFlowID   (number)    Unique ID for the edit flow to quit.  We take this parameter
//                                      to avoid this method from attempting to quit an edit-flow
//                                      that has already ended (likely to cause a valid new edit-flow
//                                      to close.
//  @param  editCompoletionEvent (EditCompletionEvent) Event that caused the cancellation
//                                      of the edit.
//  @param  [confirmed]   (boolean)     If confirmCancelEditing is true, when the user
//                                      dismisses the confirmation dialog we call this method
//                                      back, with this additional parameter indicating
//                                      yes or no click.
//  @see cancelEditing()
//<
_killEdit : function (editingFlowID, editCompletionEvent, confirmed) {
    if (this._editingFlowID != editingFlowID) return;

    if (this.confirmCancelEditing) {
        if (confirmed == null) {
            var callback =
                    this.getID() + "._killEdit('" + editingFlowID + "','"
                                                 + editCompletionEvent + "',value);";

            isc.ask(this.cancelEditingConfirmationMessage, callback);

            var yesButton = isc.Dialog.Warn.toolbar.getButton(0);
            if (yesButton.isDrawn()) yesButton.focus()
            else isc.Timer.setTimeout({target:yesButton, methodName:"focus"}, 0);
            return;
        } else if (confirmed == false) return;
    }

    // If editorExit / rowEditorExit returns false, don't allow the cancellation
    var rowNum = this.getEditRow(), colNum = this.getEditCol(),
        newVal = this.getUpdatedEditorValue(),
        newVals = this.getEditValues(rowNum, colNum);
    if (!this._handleEditorExit(editCompletionEvent, rowNum, colNum, newVal) ||
        !this._handleRowEditorExit(editCompletionEvent, rowNum, newVals))
    {
        return;
    }

    // We're going to kill the temp-edit value, but we don't want to update any visible form
    // items' values since we're then going to hide the form items anyway.
    // Therefore track any cells that have no form item showing, but DO have edit values, so
    // we can refresh them individually.
    var mustRedraw,
        cellsToRedraw = [],
        editRowNum = this._editRowNum,
        editValues = this.getEditValues(editRowNum, colNum),
        // if undrawn set start/end col such that we don't attempt to redraw any cells
        drawnArea = this.body ? this.body.getDrawArea() : [null,null,1,0],
        startCol = drawnArea[2], endCol = drawnArea[3],
        undef;
    if (editValues != null) {
        // if the user is editing a new record (no underlying data value) we have to redraw
        // the whole body as the total length will change by discarding the edit
        if (this.isNewEditRecord(editRowNum, colNum)) {
            if (this.isDrawn()) this.body.markForRedraw("clearing extra edit row");
            mustRedraw = true;
        } else {
            for (var i = startCol; i <= endCol; i++) {
                if ( editValues[this.getFieldName(i)] !== undef &&
                     ((this.editByCell && this._editColNum != i) ||
                      !this.canEditCell(editRowNum, i)) )
                {
                    cellsToRedraw.add(i);
                }
            }
        }
        // Kill the temporary edit values for the row (passing in the additional parameter to avoid
        // refreshing the effected cells)
        this._clearEditValues(this._editRowNum, colNum, true);

        // Grouping: If we just cleared an edit value that'd need a regroup we need to
        // handle it
        // (This automatically requests a redraw)
        if (this.isGrouped && this._updateGroupForEditValueChange(editRowNum)) {
            mustRedraw = true;
            // no need to markForRedraw - already handled by updateGroupForEditValueChange(...)
        }

    }

    if (this.body) {
        // hide the editor and put focus back in the body
        this.hideInlineEditor(true);
        if (this.isDrawn()) {
            if (!mustRedraw) {
                for (var i = 0 ; i < cellsToRedraw.length; i++) {
                    this.refreshCell(editRowNum, cellsToRedraw[i]);
                }
            }
        }
    }
},

isNewEditRecord : function (rowNum) {
    var editData = this.getEditSession(rowNum);
    return editData && editData._newRecord
},

//>    @method listGrid.endEditing()
//
// Complete the current edit by storing the value and hiding the inline editor. Note that
// if +link{ListGrid.autoSaveEdits} is true, the value will be saved to the server.
//
//  @group  editing
//  @visibility external
//  @see startEditing()
//<
endEditing : function () {
    if (this.getEditRow() != null) this._saveAndHideEditor(isc.ListGrid.PROGRAMMATIC);
},

// Called from cellEditEnd when the user completes and edit with an enter keypress / click
// outside saves the values locally and hides the editor. If autoSaveEdits is true, it will
// also actually save the outstanding set of edits.

_saveAndHideEditor : function (editCompletionEvent) {
    // If we're always showing editors, just call 'saveEdits' and refuse to hide

    if (this._alwaysShowEditors()) {
        this.saveEdits(editCompletionEvent);
        return;
    }

    var shiftFocus = (editCompletionEvent == isc.ListGrid.TAB_KEYPRESS) ? 1 :
                        (editCompletionEvent == isc.ListGrid.SHIFT_TAB_KEYPRESS) ? -1 : null;

    var rowNum = this.getEditRow(),
        colNum = this.getEditCol(),
        // Note getEditValue() will pick up the updated value from the form (and store in
        // this.editValues)
        newValue = this.getEditValue(rowNum, colNum),
        newValues = this.getEditValues(rowNum, colNum);


    // If editorExit or rowEditorExit returns false, don't save and quit.
    if (!this._handleEditorExit(editCompletionEvent, rowNum, colNum, newValue) ||
        !this._handleRowEditorExit(editCompletionEvent, rowNum, newValues)) return;

    // Closing the editor: If waitForSave is false we will kick off the save and then close
    // the editor without waiting to see if the save completed successfully.
    // If waitForSave is true - we will pass a callback into the saveEdits method that will
    // close the editor when the save returns.
    // We'll use the "success" parameter to determine whether a validation error occurred
    // on the attempted save. If so, we'll look at this.stopOnErrors to determine whether
    // the navigation should continue
    var waitForSave = (this.autoSaveEdits && this.shouldWaitForSave()),
        stopOnErrors = (this.autoSaveEdits && this.stopOnErrors),
        saveCallback;

    // It is possible that the current edit flow will have been completed, and another begun when
    // the callback function fires for this save (true for any saveEdits call).
    // If this is the case we want to no-op, rather than close the new edit flow.
    // Note - we fire this in the scope of the ListGrid via 'apply'
    if (waitForSave) saveCallback = "if((success||!this.stopOnErrors)&&" +
                                        "(this._editingFlowID==" + this._editingFlowID + "))" +
                                            "this.hideInlineEditor(true, null, " + shiftFocus + ");";


    var cancelling = (editCompletionEvent == isc.ListGrid.ESCAPE_KEYPRESS &&
                        this.escapeKeyEditAction == "exit"),
        saving = this.autoSaveEdits && !cancelling;

    if (saving) {
        this.saveEdits(editCompletionEvent, saveCallback);
    } else {
        var fieldName = this.getFieldName(colNum);

        // Note: no need to call 'storeUpdatedEditorValue()'here
        // this has already been handled by getEditValue() above
        var validateNow = !cancelling && this._validationEnabled() &&
                          (this.shouldValidateByCell(rowNum, colNum, editCompletionEvent) ||
                           this.shouldValidateByRow(rowNum, colNum, editCompletionEvent));
        if (validateNow) {
            var validationFailed;
            if (this.useCellRecords) {
                validationFailed = !this.validateCell(rowNum, colNum);
            } else {
                validationFailed = !this.validateRow(rowNum);
            }
            if (this.stopOnErrors && validationFailed) return false;
        }

        // drop empty edit sessions, normally accomplished by saving


        if (!this.recordHasChanges(rowNum, colNum, false) &&
            !this.hasSubmittedEditValues(rowNum, colNum)
           )
        {
            this.logInfo("editor hiding at " + [rowNum,colNum] +
                         ", no actual changes, dropping editSession", "gridEdit");
            // Don't hang onto the empty edit values for the row
            this._clearEditValues(rowNum, colNum);
        }
    }

    // If we not blocking interaction during save, we want to hide the inline editor here.
    // However, we _killEdit to clear our editValues only after saving, since if there are
    // validation errors, etc. we may yet require the editValues
    if (!waitForSave) {
        this.hideInlineEditor(true, null, shiftFocus);
    }

    // If we're currently grouped, and the value of the grouping field changed, should we
    // regroup here?
    //
    // If we're actually saving:
    // - if the save is local we'll already have regrouped. In this case definitely don't
    //    attempt to regroup again since the rowNum for the data may have changed, etc.
    // - otherwise, we will regroup when the save successfully completes, but regrouping here
    //   will essentially just update the tree to show the edit values in the new position
    //   while the save is occurring (desirable).
    //
    // If we're not saving, (autoSaveEdits is false, or the user hit "escape"), we do need
    // to regroup to put the edit values in their new position.
    if (this.isGrouped && (!saving || !this.shouldSaveLocally())) {
        this._updateGroupForEditValueChange(rowNum);
    }
},

//>    @method listGrid.saveAndEditNewCell()
//
//  Save the current edit, and start editing another cell (may be in another row).
//
//  @group  editing
//  @visibility internal
//
//  @param  newRowNum   (number)    Index of new row to edit.
//  @param  newColNum   (number)    Index of new column to edit.
//  @param  editCompletionEvent (EditCompletionEvent)  How was the edit completed?
//  @see editNewCell()
//<

saveAndEditNewCell : function (newRowNum, newColNum, editCompletionEvent) {
    if (editCompletionEvent == null) editCompletionEvent = isc.ListGrid.PROGRAMMATIC;

    this._saveAndStartEditing(newRowNum, newColNum, editCompletionEvent);

},

// _saveAndStartEditing.  Saves the current edit, and starts editing the new cell passed in
_saveAndStartEditing : function (newRowNum, newColNum, editCompletionEvent) {

    var waitForSave = this.shouldWaitForSave(),
        saveCallback;
    if (waitForSave) {
        saveCallback = "if((success||!this.stopOnErrors)&&" +
                         "(this._editingFlowID==" + this._editingFlowID + "))" +
                          "this._startEditing(" + newRowNum + "," + newColNum + ");";
    }

    this.saveEdits(editCompletionEvent, saveCallback);
    if (!waitForSave) {
        this._startEditing(newRowNum, newColNum);
    }

},


// Internal helper method to save the current cell and move to the next one in some direction.
_saveAndEditNextCell : function (direction, stepThroughCells) {
    var rowNum = this.getEditRow(), colNum = this.getEditCol(),
        editCompletionEvent = isc.ListGrid.PROGRAMMATIC,

        nextEditCell = this.findNextEditCell(rowNum, colNum, direction, stepThroughCells, false);

    this._saveAndStartEditing(nextEditCell[0], nextEditCell[1], editCompletionEvent);
},

//>    @method listGrid.saveAndEditNextCell()
//
//  Save the current edit, and edit the next editable cell, found by stepping through each field
//  in the current row (and possibly subsequent rows, depending on this.rowEndEditAction).
//
//  @group  editing
//  @visibility internal
//<
saveAndEditNextCell : function () {
    this._saveAndEditNextCell(1, true);
},

//>    @method listGrid.saveAndEditPreviousCell()
//
//  Save the current edit, and edit the previous editable cell, found by stepping through each field
//  in the current row (and possibly subsequent rows, depending on this.rowEndEditAction).
//
//  @group  editing
//  @visibility internal
//<
saveAndEditPreviousCell : function () {
    this._saveAndEditNextCell(-1, true);
},

//>    @method listGrid.saveAndEditNextRow()
//
//  Save the current edit, and edit the same field in the next editable row.
//
//  @group  editing
//  @visibility internal
//<
saveAndEditNextRow : function () {
    this._saveAndEditNextCell(1, false);
},

//>    @method listGrid.saveAndEditPreviousRow()
//
//  Save the current edit, and edit the same field in the previous editable row.
//
//  @group  editing
//  @visibility internal
//<
saveAndEditPreviousRow : function () {
    this._saveAndEditNextCell(-1, false);
},

// Editing Navigation: finding next cell or record
// --------------------------------------------------------------------------------------------

//>    @method listGrid.getNextEditCell()
//
//  Given a current edit cell, and an edit completion event, return the next cell to be edited,
//  or null if there is no next cell to edit.<br>
//  Default implementation:<ul>
//  <li>return null (editing complete), if the edit completion event was an enter keypress,
//      a click outside the editor, or an escape keypress</li>
//  <li>return the result of 'getNewEditCell()' if the edit completion event was
//      ListGrid.EDIT_FIELD_CHANGE or PROGRAMMATIC.  This relies on 'setNewEditCell()' having
//      been called earlier to specify the next cell to edit.</li>
//  <li>return the next editable cell in the same column moving up or down rows if the edit
//      completion event was an up or down arrow keypress<li>
//  <li>return the next editable cell in the same row, or if there is none, and
//      this.rowEndEditAction is "next", in subsequent rows, if the edit was
//      completed with a Tab or Shift+Tab keypress.</li>
//  </ul>
//
//  @group  editing
//
//  @param  rowNum      (number)    Index of record edited
//  @param  colNum      (number)    Index of field edited
//  @param  editCompletionEvent (EditCompletionEvent)    Edit completine event constant
//  @return (Array)  2 element array specifying next editable rowNum, colNum.  Will
//                   return null if the edit should be completed without navigating to
//                   another cell.
//  @visibility internal
//<

getNextEditCell : function (rowNum, colNum, editCompletionEvent) {

    switch (editCompletionEvent) {
        case isc.ListGrid.CLICK_OUTSIDE :
        // unlikely to see this case, since this is a 'cancellation' by default, but treat as
        // not attempting to navigate elsewhere
        case isc.ListGrid.ESCAPE_KEYPRESS :
            return null;

        case isc.ListGrid.ENTER_KEYPRESS :
            var field = this.getField(colNum),
                navStyle = field.enterKeyEditAction || this.enterKeyEditAction;
            if (navStyle == "done") return null;
            else if (navStyle == "nextCell")
                return this.findNextEditCell(rowNum, colNum, 1, true, false);
            else if (navStyle == "nextRow")
                return this.findNextEditCell(rowNum, colNum, 1, false, false);
            else if (navStyle == "nextRowStart") {
                var listEndAction = this.listEndEditAction || "done";
                if (rowNum+1 < this.getTotalRows() || listEndAction == "next") {
                    // either there are more record or we're allowed to create new records by
                    // navigating off the end of the list, proceed
                    return this.findNextEditCell(rowNum +1, 0, 1, true, true);
                } else {
                    // we've hit Enter on the last row, and we're not allowed to create new
                    // rows.
                    // if we're supposed to "stop", we should return the current coordinates,
                    // so we can't call findNextEditCell with rowNum+1 since it wouldn't know
                    // the current coordinates.
                    if (listEndAction == "stop") return [rowNum, colNum];
                    return null; // "done"
                }
            }

        case isc.ListGrid.EDIT_FIELD_CHANGE :
        case isc.ListGrid.PROGRAMMATIC :
        // In this case we have an explicit cell to move to - should have been saved as
        // 'newEditCell'
            return this.getNewEditCell();

        case isc.ListGrid.UP_ARROW_KEYPRESS :
            return this.findNextEditCell(rowNum, colNum, -1, false, false);

        case isc.ListGrid.DOWN_ARROW_KEYPRESS :
            return this.findNextEditCell(rowNum, colNum, 1, false, false);

        case isc.ListGrid.TAB_KEYPRESS :
        case isc.ListGrid.RIGHT_ARROW_KEYPRESS :
            var field = this.getField(colNum);
            if (field.nextTabColNum != null) {
                return this.findNextEditCell(rowNum +1, field.nextTabColNum, 1, true, true);
            } else {
                return this.findNextEditCell(rowNum, colNum, 1, true, false);
            }
        case isc.ListGrid.SHIFT_TAB_KEYPRESS :
        case isc.ListGrid.LEFT_ARROW_KEYPRESS :
            var field = this.getField(colNum)
            if (field.previousTabColNum != null) {
                return this.findNextEditCell(rowNum -1, field.previousTabColNum, -1, true, true);
            } else
                return this.findNextEditCell(rowNum, colNum, -1, true, false);
    }

    this.logWarn("getNextEditCell(): Passed unrecognized editCompletionEvent type:" +
                editCompletionEvent + ", returning null");
    return null;
},

//>    @method listGrid.findNextEditCell()
//
//  Method to find the next editable cell given a starting row/col, and a direction,
//  either iterating through fields within each row, or checking the same field in each row.//
//  @group  editing
//
//  @param  rowNum      (number)    Index of record edited
//  @param  colNum      (number)    Index of field edited
//  @param  direction   (number)    +1 if searching forward for the next edit cell, -1 if
//                                  searching backwards
//  @param  stepThroughFields   (boolean)   true if we should check every field in each row -
//                                          false if we should check the same field in each
//                                          row.
//  @param  [checkStartingCell] (boolean)   Should we check whether the cell passed in is
//                                          editable? Default behavior WILL check that cell -
//                                          pass false to suppress checking that cell.
//  @return (Array)  2 element array specifying next editable rowNum, colNum.  Will
//                   return null if this method fails to find an editable cell.
//  @visibility internal
//<
// @param [checkPastListEnd]    Optional, internal parameter forces this method to check beyond
// the end of the list if necessary, even if this.listEndEditAction is not "next"
// @param [dontCheckPastRowEnd] Optional, internal parameter suppresses checking beyond the end
// of the row. We use this in handleEditEvent(click) to avoid iterating through the entire grid
// if there's no editable cell in a row!
// @param [ignoreFocus] optional, internal parameter - if we can't focus in an item

findNextEditCell : function (rowNum, colNum, direction, stepThroughFields, checkStartingCell,
                                checkPastListEnd, dontCheckPastRowEnd, ignoreFocus) {

    var newRow = rowNum,
        newCol = colNum,
        lastRow,
        totalCols = this.getFields().length;
    // no columns --> no way to get a valid cell
    if (totalCols == 0) return;

    // We want to look at every row up until the last drawn row.
    // IF listEditAction is "next", we will also check one row beyond the last drawn record
    // - this allows the user to tab beyond the end of the list and create a new edit row.
    // If we are passed a rowNum greater than the last drawn row, we will check every field
    // within that row (even if it's after tha current last drawn row).  This allows the
    // programmatic editing of rows beyond the end of the list via 'startEditingNew()'.
    // In cases where there is no record associated with a row, canEditCell() checks
    // editability based on the field definitions.

    lastRow = (dontCheckPastRowEnd ? newRow :
                (this.listEndEditAction == "next" || checkPastListEnd) ? this.getTotalRows()
                                                                       : this.getTotalRows() -1);
    if (newRow > this.getTotalRows()) lastRow = newRow;

    // Note: If we have an editable field with a non focusable editor, always skip it.
    // This method is typically called when the user hits Tab to shift focus to the next
    // editable cell, etc.
    // If editing by row, skipping non-focusable fields is intuitive - the focus jumps over
    // the non editable field. If editing by cell it means that editor actually won't show up unless
    // the user double-clicks the cell (which seems acceptable since if you tabbed into it you'd
    // be unable to tab out again anyway).
    // ignoreFocus param disables this check - used whenever we need to find the next editable
    // cell and for some reason don't care about focus. Example use case: Finding the first
    // edit cell with a RecordEditor.

    // check whether we can edit at the starting coordinates, unless that is being
    // explicitly suppressed
    // Note: The cell passed in may be off the end of the list or past the end of the fields,
    //       since we blindly add 1 to a newRow / colNum in some cases.
    if (  (checkStartingCell != false || direction == 0) &&
          newRow <= lastRow && newRow >=0 && newCol < totalCols && newCol >=0 &&
          this.canEditCell(newRow, newCol) &&
          (ignoreFocus || this._canFocusInEditor(newRow, newCol))
        )
    {
        return [newRow, newCol];
    }

    // direction will be either -1 or 1.
    // check for other values is unnecessary so long as this method remains internal, but
    // check for zero anyway, since that would cause real problems!
    if (direction == 0) return null;

    var canEditStartingCell = this.canEditCell(newRow, newCol) &&
                              this._canFocusInEditor(newRow, newCol),
        firstRow = dontCheckPastRowEnd ? newRow : 0;

    if (stepThroughFields) {
        var style = this.rowEndEditAction || "next";
        while (newRow < (lastRow+1) && newRow >= firstRow) {

            // special case - break if we hit a loading row.
            // Exception: for newly added, as yet unsaved rows, rowIsLoaded
            // will return false, so accept these -- we won't go past "lastRow"
            // anyway.

            if (isc.ResultSet && isc.isA.ResultSet(this.data)) {
                if (newRow < this.data.getLength() && !this.data.rowIsLoaded(newRow)) return null;
            }
            newCol += direction;

            // If we've looped through the editable cells for this row, and are back at the
            // initial col return the starting row if it's editable.
            if (style == "same" && colNum == newCol) {
                return canEditStartingCell ? [rowNum, colNum] : null;
            }
            // if we hit the end of the row, check the rowEndEditAction to determine whether
            // to move to the next record, etc.
            if (newCol < 0 || newCol >= totalCols) {
                if (style == "done" || isc.isA.RecordEditor(this)) return null;
                if (style == "stop") return (canEditStartingCell ? [rowNum, colNum] : null)
                // if we're looping through the same row, set the newCol to be off the end
                // since we add / subtract one before checking for canEdit()
                else {
                    newCol = (direction > 0 ? -1 : totalCols);
                    if (style == "next") newRow += direction;
                }
            } else if (this.canEditCell(newRow,newCol) &&
                        (ignoreFocus || this._canFocusInEditor(newRow, newCol)))
            {
                return [newRow, newCol];
            }
        }
        // reached the end of the list without finding an editable cell.
        // If the end action is stop, or we're trying to move past the start of the list and
        // the end action is 'next', start editing the same cell again
        // - otherwise return null to stop editing
        if (this.listEndEditAction == "stop" ||
            (this.listEndEditAction == "next" && newRow < firstRow))
        {
            if (canEditStartingCell) return [rowNum, colNum];
            else return null;
        // default behavior - rowEndEditAction is "done"
        } else {
            return null;
        }

     } else {
        newRow += direction
        while (newRow >= firstRow && newRow <= lastRow) {

            if (this.canEditCell(newRow, newCol) &&
                (ignoreFocus || this._canFocusInEditor(newRow, newCol))) {
                return [newRow, newCol];
            }
            newRow += direction;
        }
        // reached the end of the list without finding an editable cell.
        // If listEndEditAction is stop, or "next" - and we're trying to navigate past the
        // beginning of the list, return the cell passed in - otherwise return null to stop
        // editing
        if (this.listEndEditAction == "stop" ||
            (this.listEndEditAction == "next" && newRow < firstRow))
        {
            return canEditStartingCell ? [rowNum, colNum] : null;
        }
        return null;
     }
},

_canFocusInEditor : function (rowNum, colNum) {

    var fieldName = this.getFieldName(colNum),

        editForm = this.getEditForm();

    var fieldID = fieldName || colNum;

    if (editForm == null) {
        this.makeEditForm(rowNum, colNum);
        editForm = this._editRowForm;
    }

    var editItem = editForm ? editForm.getItem(fieldID) : null;
    // editItem can still be null since we may be editing by cell in which case
    // we only populate with a single item.
    if (editItem) return editItem._canFocus();
    var field = this.getField(fieldID);
    if (field.canFocus != null) return field.canFocus;
    var editorType = this.getEditorType(field, this.getEditedRecord(rowNum));

    // call getItemClassName on FormItemFactory to actually get a SmartClient class from
    // the editor type (required to get SelectItem from SelectOtherItem, etc)
    editorType = isc.FormItemFactory.getItemClassName(field, editorType, null, true);

    // Resolve to an actual class
    var editorClass = editorType = isc.FormItemFactory.getItemClass(editorType);


    // This could potentially be broken by an override to _canFocus() which required looking
    // at a live instance.


    if (editorClass) {
        var _prototype = editorClass.getPrototype(),
            canFocus = _prototype.canFocus;
        if (canFocus != null) return canFocus;

        // Special case: If this is a CanvasItem, assume it's a custom implementation
        // and editable.
        if (isc.isA.CanvasItem(_prototype)) {
            return true;
        }
        return _prototype._canFocus();
    }
    return false;
},

//>    @method listGrid.setNewEditCell() (I)
//
//  Internal method to record an explicit 'new edit cell'.  This can be retrieved (as an array)
//  via getNewEditCell();
//  Currently used when focus is put into a new cell by clicking in a field in a visible
//  (row-style) edit form, and by editfield.
//
//  @group  editing
//  @visibility internal
//
//  @param  rowNum      (number)    Index of the new row to edit
//  @param  colNum      (number)    Index of new field to edit
//<
setNewEditCell : function (rowNum, colNum) {
    this._newEditCell = [rowNum, colNum];
},

//>    @method listGrid.getNewEditCell() (I)
//
//  Internal method to retrieve the 'new edit cell', previously stored by setNewEditCell.
//
//  @group  editing
//  @visibility internal
//
//  @return (array)    2-element array indicating the stored rowNum and coNum
//<
getNewEditCell : function () {
    if (this.canEditCell(this._newEditCell[0], this._newEditCell[1])) return this._newEditCell;
    return null;
},

//>    @method listGrid.clearNewEditCell() (I)
//
//  Internal method to clear the 'new edit cell' stored by setNewEditCell.
//
//  @group  editing
//  @visibility internal
//<
clearNewEditCell : function () {
    this._newEditCell = null;
},


//>    @method listGrid.discardAllEdits() (A)
//
// Cancel outstanding edits, discarding edit values, and hiding editors for the record[s] passed
// in if appropriate.
// <P>
// If no rows are passed in, all outstanding edit values will be dropped.  This will <b>not</b>
// automatically end editing; call +link{endEditing()} <b>before</b> calling discardAllEdits()
// if you also want to end editing.
// <P>
// Note that this also clears the +link{listGrid.markRecordRemoved,removed} state of any records
// that have been marked as removed.
//
// @param  [rows] (Array of Number) allows you to specify which row(s) to drop edits for
// @param   [dontHideEditor]    (boolean)   By default this method will hide the editor if
//                              it is currently showing for any row in the grid. Passing in
//                              this parameter will leave the editor visible (and just reset
//                              the edit values underneath the editor).
// @visibility external
// @group editing
//<
discardAllEdits : function (rows, dontHideEditor) {

    if (rows == null) rows = this.getAllEditRows(true);
    else {
        // Convert numeric rows to edit-values IDs
        for (var i = 0; i < rows.length; i++) {
            if (isc.isA.Array(rows[i])) rows[i] = this.getEditValuesID(rows[i][0], rows[i][1]);
            if (isc.isA.Number(rows[i])) rows[i] = this.getEditValuesID(rows[i]);
        }
    }
    if (rows == null) return;

    // call discardEdits()
    // Note that we're passing in an editValuesID rather than a rowNum / colNum here
    for (var i = 0; i < rows.length; i++) this.discardEdits(rows[i], null, dontHideEditor);
},

//>    @method listGrid.discardEdits() (A)
//
// Cancel outstanding edits for the specified rows, discarding edit values, and hiding editors
// if appropriate.
// <P>
// Note that if this method is called on a new edit row (created via
// +link{listGrid.startEditingNew()} for example), which has not yet been saved, this method
// will remove the row entirely.
// <P>
// Also note that this method will clear the +link{listGrid.markRecordRemoved,removed} state
// of records that have been marked as removed.
//
// @param  rowNum (int) Row to cancel
// @param colNum (int) Column to cancel. Note that this parameter is ignored in ListGrids but
//                        may be required in subclasses of ListGrid where each cell represents
//                        one record in the data set (EG CubeGrid)
// @param   [dontHideEditor]    (Boolean)   By default this method will hide the editor if
//                              it is currently showing for the row in question. Passing in
//                              this parameter will leave the editor visible (and just reset
//                              the edit values underneath the editor).
// @group  editing
// @visibility external
//<
discardEdits : function (rowNum, colNum, dontHideEditor, editCompletionEvent) {
    if (rowNum == null) return;
    // if passed an ID rather than a rowNum, figure out the rowNum, so we can
    // easily see if its the current edit row.
    var ID;
    if (isc.isA.Number(rowNum)) {
        ID = this.getEditValuesID(rowNum, colNum);
    } else {
        ID = rowNum;
        rowNum = this.getEditSessionRowNum(ID, colNum);
    }
    var markedAsRemoved = this.recordMarkedAsRemoved(rowNum);

    // If alwaysShowEditors is true, suppress hiding the editor
    // unless we have no data and are showing a single new edit row.
    if (!dontHideEditor && this._alwaysShowEditors() &&
        (!(this.getTotalRows() == 1 && rowNum == 0)))
    {
        this.logInfo("Refusing to hide editor on 'discardEdits' [editCompletionEvent:" +
                editCompletionEvent + "] due to alwaysShowEditors settings", "inactiveEditorHTML");
        dontHideEditor = true;
    }

    if (!dontHideEditor && this._editorShowing && this.getEditRow() == rowNum) {

        if (editCompletionEvent == null) editCompletionEvent = isc.ListGrid.PROGRAMMATIC;
        return this._killEdit(this._editingFlowID, editCompletionEvent);

    } else {
        // Ensure the stored edit value matches the value in the edit form item before we clear it
        // This is required to refresh the edit item value

        if (this._editorShowing && this.getEditRow() == rowNum) {
            this.storeUpdatedEditorValue(true);
        }
        this._clearEditValues(ID, colNum);
    }
    // If marked as removed, explicitly refresh the row so we reset to normal styling
    if (markedAsRemoved) this.refreshRow(rowNum);
},

// Saving Inline Edits
// --------------------------------------------------------------------------------------------


//>    @method listGrid.saveEdits() (A)
//
// Validates and saves edits within the row currently being edited (or another row with unsaved
// edits, if indicated).
// <P>
// This method can be called to manually trigger saves if the default mechanisms of
// +link{saveByCell,cell by cell} or row by row saving are not suitable.
// <P>
// The 'callback' parameter provides a notification when the save attempt completes, which is
// likely to be asynchronous for databound grids.  Cases under which the callback will fire are:
// <ul><li>Save completed successfully</li>
// <li>No changes to the edited row, so save not required</li>
// <li>Validation failure occurred on the client or on the server</li></ul>
// Note that if this method was unable to determine the row to be saved, the callback will NOT
// fire - in this case, the method is a no-op.
// <P>
// Other, standard callbacks such as +link{editComplete()}, +link{editFailed()} and
// +link{cellChanged()} will fire normally.
// <P>
// Note this method does not hide the inline editors if they are showing - to explicitly save
// and end editing, use the method 'endEditing()'
// <P>
// If this method is called for a row which has been marked for deletion
// (see +link{listGrid.markRecordRemoved}) it will cause the record to be removed from the
// data-set.
//
//  @group  editing
//  @visibility external
//
//  @param  [editCompletionEvent] (EditCompletionEvent) Event used to complete cell editing.
//          Optional, and defaults to <code>"programmatic"</code>.  Can be used by the
//          <code>callback</code> method to perform custom actions such as navigation when the
//          save completes.
//  @param  [callback]    (Callback)  Callback to fire on completion of the saving process.
//                                    If no edits were made or client-side validation fails
//                                    the callback will be fired synchronously at the end of this
//                                    method.<br>
//                                    Takes the following parameters:
//                                    <br>- rowNum <i>(Number) edited row number</i>
//                                    <br>- colNum <i>(Number) edited column number</i>
//                                    <br>- editCompletionEvent <i>(EditCompletionEvent) event
//                                      passed in (defaults to <code>"programmatic"</code>)</i>
//                                    <br>- success <i>(boolean) false if the save was unable to
//                                      complete due to a validation failure or server-side
//                                      error.</i>
//  @param  [rowNum] (number) Which row should be saved. If unspecified the current edit row
//                            is saved by default. Note that if there is no current edit row
//                            this method will no op.
//  @see    endEditing()
//<
// parameter 'validateOnly' is used internally to avoid actually saving if validation
// succeeds - just return true to indicate success


// parameter 'skipValidation' used internally to suppress client side validation and allow the
// save to proceed. This is helpful for the case where we've validated the record in the same
// thread and want to avoid re-running validators unnecessarily
saveEdits : function (editCompletionEvent, callback, rowNum, colNum, validateOnly, skipValidation) {
    // Since this is being exposed as a public method (allowing saving while leaving
    // the editor visible), default to a "PROGRAMMATIC" editCompletionEvent.
    if (editCompletionEvent == null) editCompletionEvent = isc.ListGrid.PROGRAMMATIC;
    if (rowNum == null) {
        rowNum = this.getEditRow()
        colNum = this.getEditCol();
    } else if (colNum == null) {
        // this will not apply to cubeGrid editing

        colNum = (rowNum == this.getEditRow() ? this.getEditCol() : this.getRowEditColNum(rowNum));
    }
    // clicking rapidly from one row to another when the editEvent is 'CLICK', and editOnFocus
    // is true can sometimes cause this method to get fired before the editRow / col has been
    // set up.  Just bail if this happens.
    if (rowNum == null) return false;

    // If this is a removal, we want to remove rather than update.
    if (this.recordMarkedAsRemoved(rowNum)) {

        if (validateOnly) {
            return true;
        }

        var newRow = this.isNewEditRecord(rowNum, colNum);
        // new row - by dropping edit values we discard the row entirely.

        if (newRow) {
            this.discardEdits(rowNum);
            this._fireSaveCallback(callback, rowNum, colNum, editCompletionEvent, true);
            return;
        } else {
            // wrap the callback passed in in a ds callback,
            // using closure to access the various params we want
            var _this = this;
            var dsCallback = function (response,data,request) {
                _this.fireCallback(callback, "rowNum,colNum,editCompletionEvent,success",
                        [rowNum,colNum,editCompletionEvent,true]);
            };
            this.removeData(this.getCellRecord(rowNum, colNum), dsCallback, null, true);
            return;
        }
    }

    // If we're actually showing the editor, getEditValues() will ensure any changes are
    // saved in the editValues object for the row.
    var newValues = this.getEditValues(rowNum, colNum),
        record = this.getCellRecord(rowNum, colNum),
        editValuesID = this.getEditValuesID(rowNum, colNum);

    // don't pass the actual record as oldValues; allows downstream code to modify it freely
    var oldValues;
    if (record != null && !Array.isLoading(record)) {
        if (isc.isA.Tree(this.data)) {

            oldValues = this.data.getCleanNodeData(record, false);
        } else {
            oldValues = isc.addProperties({}, record);
        }

        if (oldValues && this.selection) delete oldValues[this.selection.selectionProperty];
    }
    // Assemble the edit information into a single object to be passed to callback
    // methods.
    // We use this object if we have to call editCompleteCallback or editFailedCallback
    // directly from this method - otherwise we'll re-create this object in
    // saveEditedValues() / updateRecordsReply()
    var editInfo = {
            // Note: rowNum / colNum are basically available for callbacks, and for special
            // handling if a user overrides this method
            rowNum:rowNum,
            colNum:colNum,
            oldValues:oldValues,
            editValuesID:editValuesID,
            values:newValues,
            editCompletionEvent:editCompletionEvent,
            newRecord:(rowNum > this.data.getLength())
    };
    //>DEBUG
    if (this.logIsDebugEnabled("gridEdit")) {
        this.logDebug("change detection: newValues: " + this.echo(newValues) +
                      ", oldValues: " + this.echo(oldValues), "gridEdit");
    } //<DEBUG


    var haveChanges = this.recordHasChanges(rowNum, colNum, false);
    // perform validation on the edited row before saving
    // NOTE: we always save, and validate the whole row, even if saveByCell is true
    // as saveByCell really governs when the save is kicked off rather than what is
    // to be saved.

    if (haveChanges && !skipValidation && this._validationEnabled()) {

        this._skipServerValidation = (validateOnly || haveChanges);

        var validationFailed;
        if (this.useCellRecords) {
            validationFailed = !this.validateCell(rowNum, colNum);
        } else {
            validationFailed = !this.validateRow(rowNum);
        }
        this._skipServerValidation = null;
        if (validationFailed) {
            if (!validateOnly) this._editFailedCallback(editInfo, callback);
            return false;
        }
    }
    // If we're only validating return true here to indicate that validation passed.
    if (validateOnly) return true;
    if (!haveChanges) {
        this.logInfo("saveEdits: no actual change, not saving", "gridEdit");
        // haveChanges will return false if we're pending a save and further edits haven't
        // been made.
        // In this case, retain the edit values.
        // Otherwise they are basically empty / match the record values, so remove 'em
        if (!this.hasSubmittedEditValues(editValuesID, colNum)) {
            this._clearEditValues(editValuesID, colNum);
        }
        // fire the saveCallback to finish flow (eg move to next cell, end editing..)

        this._fireSaveCallback(callback, rowNum, colNum, editCompletionEvent, true);
        return;
    }

    // get refreshed editValues since validation converts values to declared types, and we need
    // to store with correct types
    newValues = this._getEditValues(rowNum, colNum);

    //>DEBUG
    this.logInfo("Saving newValues '" + isc.echoAll(newValues) + "'", "gridEdit");
    //<DEBUG

    // remember this set of editValues as the "last submitted" for this row - this is used
    // for change detection while any save is pending.

    this.rememberSubmittedEditValues(rowNum, colNum);

    // NOTE: dupe the new values because they can be the live "editValues" for a row, and the
    // save implementation might integrate them directly into the data set with no copy
    newValues = isc.addProperties({}, newValues);

    // call the 'saveEditedValues' method to actually perform the save.
    var attempted = this.saveEditedValues(rowNum, colNum, newValues, oldValues,
                          editValuesID, editCompletionEvent, callback);

    // If saveEditedValues() returns explicit false, we abandoned the save attempt because
    // we're offline at the moment
    if (attempted === false) {
        this._editFailedCallback(editInfo, callback);
        return false;
    }

    // return true to indicate we're (attempting to) save the value
    // (If we're saving to the server, this does not indicate success, the server could still
    // give validation errors back to the asynchronous callback function - don't fire
    // _editCompleteCallback until we know the edit either went through or failed)
    return true;
},

//> @method listGrid.rowHasChanges()
// If this listGrid can be edited, this method will return true if the row passed in has been
// edited, but the edits have not yet been saved to the ListGrid's data object.
// <P>
// Note this method will not return true if a record has been marked as
// +link{markRecordRemoved,removed}, but has no other changes. Developers can use
// +link{listGrid.recordMarkedAsRemoved()} to check for this case.
//
// @param rowNum (int) index of row to check for changes
// @return (Boolean) true if the row has changes.
// @visibility external
// @group editing
//<


rowHasChanges : function (rowNum, checkEditor) {
    // call recordHasChanges
    // Ignore the 2nd parameter - this method is not supported in 1 record/cell editing
    // pattern (IE CubeGrid)
    return this.recordHasChanges(rowNum, null, checkEditor);
},

// this one is documented at the CG level where it should be used instead of rowHasChanges
recordHasChanges : function (rowNum, colNum, checkEditor) {

    if (checkEditor == null) checkEditor = true;

    var changes = false,
        newValues = (checkEditor ? this.getEditValues(rowNum, colNum)
                                 : this._getEditValues(rowNum, colNum)),
        oldValues = this.getCellRecord(rowNum, colNum);
    // If there is no record for this row, we're editing a new row.
    // In this case we always want to save the values out.
    if (!oldValues) return true;

    // determine whether any changes have occurred by looking at the oldValues.
    // If the record is pending a save, compare the last submitted edit values to the new
    // values rather than looking at the underlying record values.

    var testValues = isc.addProperties({}, oldValues,
                                       this.getSubmittedEditValues(rowNum, colNum));
    for (var fieldName in newValues) {
        if (fieldName == this.removeRecordProperty) continue;
        var oldFieldValue = testValues[fieldName],
            newFieldValue = newValues[fieldName];

        // Use custom comparator to catch things like Dates where '==' check is not sufficient
        if (!this.fieldValuesAreEqual(this.getField(fieldName), oldFieldValue,newFieldValue)) {
            //this.logWarn("field: " + fieldName +
            //             " changed from '" + this.echo(oldFieldValue) +
            //             "' to '" + this.echo(newFieldValue) + "'");
            changes = true;
            break;
        }
    }
//     this.logWarn("newValues:" + this.echoAll(newValues) +
//         ", compared against oldValues:" + this.echo(testValues) +
//         " and .. combined with submitted:" + this.echo(testValues));
    return changes;
},

//> @method listGrid.hasChanges()
// Whether the grid as a whole has any unsaved edits, in any row. Note that this method will return
// true if any rows are +link{markRecordRemoved,marked as removed} in addition to any rows that have
// pending unsaved edits.
// @return (Boolean) returns true of any unsaved edits are present
// @group editing
// @visibility external
//<

hasChanges : function (checkEditor) {
    var editRows = this.getAllEditRows();
    if (editRows != null) {
        for (var i = 0; i < editRows.length; i++) {
            if (this.recordMarkedAsRemoved(editRows[i])) return true;
            if (this.rowHasChanges(editRows[i], checkEditor)) return true;
        }
    }
    return false;
},

//> @method listGrid.cellHasChanges()
// If this listGrid can be edited, this method will return true if the cell passed in has been
// edited, but the edits have not yet been saved to the ListGrid's data object.
// @param rowNum (int) index of row to check for changes
// @param colNum (int) index of the col to check for changes
// @return (Boolean) returns true if the cell has unsaved edits
// @group editing
// @visibility external
//<
// Additional checkEditor param: Should we check for changes in the editor which have not yet been
// stored in our editValues?
// If unset, assumed to be true.
// Useful as when we use this method internally we're typically in the process of writing out
// CSS text, etc for the cell, so if the editor is showing we know the user hasn't had a
// chance to modify it.
cellHasChanges : function (rowNum, colNum, checkEditor) {
    return this._cellHasChanges(rowNum, colNum, checkEditor, false);
},
_cellHasChanges : function (rowNum, colNum, checkEditor, ignoreSubmittedVals) {

    if (rowNum == null || colNum == null) return false;
    var newValues = (checkEditor ? this.getEditValues(rowNum, colNum)
                                 : this._getEditValues(rowNum, colNum));
    // No new edit values - therefore no changes
    if (!newValues) return false;

    var savedValues = this.getCellRecord(rowNum, colNum);
    // No record, therefore this is a new row so every cell has a new value
    if (!savedValues) return true;

    var field = this.getField(colNum);
    // presumably field being null implies a bad colNum
    if (field == null) return false;


    var dataPath = !field.dataPath && isc.CubeGrid && isc.isA.CubeGrid(this) ?
        this.getValueProperty(rowNum, colNum) : null;

    // Pass the field directly into _getFieldValue()

    var newValue = isc.Canvas._getFieldValue(dataPath, field, newValues, this, true, "compare");

    var undef;
    // Edit values are sparse, so if undef, assume no edit value for the cell
    if (newValue === undef) return false;

    // If we have issued a save, don't report the cell as having changes
    // unless the field was re-edited since the save was kicked off
    // This matches recordHasChanges behavior

    var submittedValues = ignoreSubmittedVals ? null : this.getSubmittedEditValues(rowNum, colNum),
        oldValue;
    if (submittedValues) {
        oldValue = isc.Canvas._getFieldValue(dataPath, field, submittedValues, this, true, "compare");
    }
    if (oldValue == null) {
        oldValue = isc.Canvas._getFieldValue(dataPath, field, savedValues, this, true, "compare");
    }
    if (oldValue == null) {
        oldValue = isc.Canvas._getFieldValue(dataPath, field, submittedValues, this, true, "compare");
    }
    return !this.fieldValuesAreEqual(field, oldValue, newValue);
},

//> @method ListGrid.saveAllEdits()
// Save a number of outstanding edits for this ListGrid. If no rows are specified, all
// outstanding edits will be saved.
// <P>
// Note that if +link{listGrid.saveRequestProperties} is specified and the grid is performing
// a databound save, these properties will be applied to each generated DSRequest.
//
// @param [rows] (Array of Number) optionally specify which rows to save
// @param [saveCallback] (callback) If specified this callback will be fired on a successful save
//  of the specified rows. Note that if there are no pending edits to be saved this callback will
//  not fire - you can check for this condition using +link{ListGrid.hasChanges()} or
//  +link{ListGrid.rowHasChanges()}.  Use +link{editFailed} to find out about failures
//  encountered during saving (on a per-row basis).
// @return (boolean) true if a save has been initiated (at least one row had changes, passed
//                   client-side validation, and a save has been attempted).  False otherwise
// @group editing
// @visibility external
//<

_saveAllEditsFlow:0,
saveAllEdits : function (rows, saveCallback) {
    this._saveAllEditsPendingRows = this._saveAllEditsPendingRows || {};
    this._saveAllEditsRemoveRows = this._saveAllEditsRemoveRows || {};
    this._saveAllEditRowsCallback = this._saveAllEditRowsCallback || {};

    if (rows == null) rows = this.getAllEditCells();
    if (rows == null) return false;

    // handle being passed just a rowNum
    if (!isc.isAn.Array(rows)) rows = [rows];

    // We're going to iterate through our set of edited rows twice - the first time we'll
    // just perform validation. This allows us to bail if stopOnErrors is true.
    // The second time we iterate through the rows that didn't fail validation only, performing
    // the actual saves.
    if (this._validationEnabled()) {
        for (var i = 0; i < rows.length; i++) {

            // for the cube, each entry in "rows" can be a two-element array of rowNum, colNum
            var rowNum = isc.isAn.Array(rows[i]) ? rows[i][0] : rows[i],
                colNum = isc.isAn.Array(rows[i]) ? rows[i][1] : null;

            // call saveEdits with a flag telling it just to perform client-side validation.
            // Note this also prevents editFailed() from being called (it would normally be
            // called for a client-side validation failure)
            // Suppress this for records marked as removed.

            var validated = this.recordMarkedAsRemoved(rowNum) ||
                            this.saveEdits(null, null, rowNum, colNum, true);
            if (!validated) {
                if (this.stopOnErrors) return false;
                // don't save rows that failed client validation
                // Also suppress the callback from firing (just clear it) as firing would indicate
                // success
                else {
                    rows[i] = null; //
                    saveCallback = null;
                }
            }
        }
    }

    var shouldQueue = (this.dataSource != null && !this.shouldSaveLocally()),
        startedQueue = false;
    if (shouldQueue) startedQueue = !isc.RPCManager.startQueue();

    // If a callback was provided, we want to fire if we know every row saved successfully.
    // We handle this by tracking the set of rows committed with each 'saveAllEdits()' call and
    // only firing the callback once we've seen a callback tripped from every row committed.
    // The 'flowNum' logic is required to allow more than one 'saveAllEdits' call to be kicked
    // off before the response from the server without us clobbering info
    // (and therefore failing to fire the callback) from earlier calls to the method.

    var flowNum = this._saveAllEditsFlow ++,
        savingRows = this._saveAllEditsPendingRows[flowNum] = [],
        removingRows = this._saveAllEditsRemoveRows[flowNum] = [];

    this._saveAllEditRowsCallback[flowNum] = saveCallback;

    var saveAttempted = false;

    // We only want to fire the 'saveCallback' when we know every row committed has been
    // successfully saved. This is handled by _saveAllEditsRowCallback()
    var callback = ["this._saveAllEditsRowCallback('",,"',rowNum," + flowNum +
                                                        ",colNum,editCompletionEvent,success)"];

    var removedRowNums = [];
    for (var i = 0; i < rows.length; i++) {

        // any rows that failed validation have been removed from the array.
        if (rows[i] == null) continue;

        var rowNum = rows[i],
            colNum;
        if (isc.isAn.Array(rowNum)) {
            colNum = rowNum[1];
            rowNum = rowNum[0];
        }

        // Adjust the rowNum to account for previously removed rows.

        var offset = 0;
        if (removedRowNums && removedRowNums.length > 0) {
            for (var k = removedRowNums.length; k--; ) {
                if (rowNum > removedRowNums[k]) {
                    ++offset;
                }
            }
        }
        rowNum -= offset;
        var newRow = this.isNewEditRecord(rowNum, colNum);
        var removing = this.recordMarkedAsRemoved(rowNum);
        if ((!shouldQueue || newRow) && removing) {
            removedRowNums.push(rowNum);
        }

        // Don't call saveEdits() for an unchanged row, as it will synchronously call the
        // callback and hose tracking of committed changes.
        if (!this.recordHasChanges(rowNum,colNum) && !removing) {
            continue;
        }

        saveAttempted = true;
        var editValuesID = this.getEditValuesID(rowNum);
        callback[1] = editValuesID;
        if (removing) {
            removingRows[removingRows.length] = editValuesID;
        } else {
            savingRows[savingRows.length] = editValuesID;
        }
        // pass in the skipValidation param - we already validated earlier in this thread.
        this.saveEdits(null, callback.join(""), rowNum, colNum, false, true);
    }

    // If we're queuing, send the queue now

    if (startedQueue) {
        isc.RPCManager.sendQueue(null,null,null,true);
    }
    // return true to indicate we're (attempting to) save the value unless every save attempt
    // gave a validation error
    // (If we're saving to the server, this does not indicate success, the server could still
    // give validation errors back to the asynchronous callback function - don't fire
    // _editCompleteCallback until we know the edit either went through or failed)
    return saveAttempted;
},

_saveAllEditsRowCallback : function (editValuesID, rowNum, flowNum, colNum, editCompletionEvent, success) {
    if (!success) {
        // Clean up the stored array of saving rows and the stored callback method.
        // _saveAllEditsRowCallback will successfully recognize if this has occurred and have
        // other (successful) callbacks from this flow no-op.
        delete this._saveAllEditRowsCallback[flowNum];
        delete this._saveAllEditsPendingRows[flowNum];
        delete this._saveAllEditsRemoveRows[flowNum];
        return;
    }

    // look up the set of rows that were committed to together
    var savingRows = this._saveAllEditsPendingRows[flowNum],
        removingRows = this._saveAllEditsRemoveRows[flowNum];

    // If we have no stored 'saving rows' one of the saves must have failed so we will
    // never fire our save callback. Just bail in this case.
    if (savingRows == null && removingRows == null) return;

    // remove the row that we have just successfully saved
    var index, found, removing;
    for (index = 0; index < savingRows.length; index++) {
        var testID = savingRows[index];
        if (testID == editValuesID) {
            found = true;
            break;
        }
    }

    if (!found) {
        for (index = 0; index < removingRows.length; index++) {
            var testID = removingRows[index];
            if (testID == editValuesID) {
                found = true;
                break;
            }
        }
        if (found) removing = true;
    }



    if (removing) {
        removingRows.removeAt(index);
    } else {
        savingRows.removeAt(index);
    }
    // if all rows have been successfully saved
    if (savingRows.length == 0 && removingRows.length == 0) {
        // fire the callback passed into saveAllEdits()

        this._fireSaveCallback(this._saveAllEditRowsCallback[flowNum],
                                rowNum, colNum, editCompletionEvent);
        // clean up:
        delete this._saveAllEditRowsCallback[flowNum];
        delete this._saveAllEditsPendingRows[flowNum];
        delete this._saveAllEditsRemoveRows[flowNum];
    }
},

// _parseEditorValue - helper method to convert the value currently displayed in an editor into
// the appropriate raw value for saving.
// calls 'parseEditorValue' at the field / grid levels if defined.

_parseEditorValue : function (value, field, rowNum, colNum, record) {
    // allow the record to be passed in as an optional parameter if it happens to be available
    // for the calling function.
    var undef;
    if (record === undef) record = this.getCellRecord(rowNum, colNum);

    if (field && field.parseEditorValue != null) {
        isc.Func.replaceWithMethod(field, "parseEditorValue",
                                            "value,record,rowNum,colNum,grid");
        value = field.parseEditorValue(value, record, rowNum, colNum, this);
    } else if (this.parseEditorValue != null) {
        value = this.parseEditorValue(value, record, rowNum, colNum);
    }
    return value;

},

// returns true if edits should not propagate to upstream data sources/sets

// NOTE: This method has been moved up to DataBoundComponent, because there are methods at
// thst level which call it, and not every DBC is a ListGrid (eg, TileGrid)
//shouldSaveLocally : function () {
//    return (!this.dataSource || this.getFullDataPath() != null || this.saveLocally);
//},

//>    @method listGrid.saveEditedValues()    (A)
//  Save edited values that have passed client-side validation.  Fire the callback when
//  save process completes (with param to indiciate failure if appropriate).
//
//    @group    editing
//
//  @param  rowNum  (number)    index of edited row
//  @param  colNum  (number)    index of edited column
//    @param    newValues   (object)    Values returned by the editor.
//  @param  oldValues   (object)    Values before the edit
//  @param  editCompletionEvent (EditCompletionEvent)  event that caused edit completion
//  @param  [saveCallback]    (Callback)  optional callback function to fire on completion of
//                                        saving process
//  @visibility internal
//<
saveEditedValues : function (rowNum, colNum, newValues, oldValues,
                             editValuesID, editCompletionEvent, saveCallback)
{
    // assemble the edit information into a single object, so it can be passed around as part
    // of the context for this event.


    // NOTE: we use an arbitrary "editValuesID" to track edited values, since newly
    // created records won't have primary keys, and rowNums could change if data is shuffled.
    var editInfo = {
        editValuesID:editValuesID,
        rowNum:rowNum,
        colNum:colNum,
        values:newValues,
        oldValues:oldValues,
        editCompletionEvent:editCompletionEvent
    };
    // At this point we have already validated the change (on the client), so we just need to
    // save changes

    // if there's no dataSource, immediately commit changes locally
    if (this.shouldSaveLocally()) {

        // if useRemoteValidators is true, ask the dataSource to validate the change and only
        // continue with the save if validation is successful.

        if (this.useRemoteValidators == true) {
            var hasServerValidators = false,
                ds = this.getDataSource(),
                fields = ds ? ds.getFieldNames() : null;

            if (fields != null) {
                for (var i = 0; i < fields.length; i++) {
                    var field = ds.getField(fields[i]),
                        validators = field.validators;
                    if (validators == null) continue;
                    if (!isc.isAn.Array(validators)) validators = [validators];
                    for (var ii = 0; ii < validators.length;ii++) {
                        if (isc.Validator.isServerValidator(validators[ii])) {
                            hasServerValidators = true;
                            break;
                        }
                    }
                    if (hasServerValidators) break;
                }
            }

            if (hasServerValidators) {
                var requestProperties = {
                    validationMode:"partial",
                    prompt:isc.RPCManager.validateDataPrompt,
                    showPrompt:this.shouldWaitForSave(),
                    internalClientContext:{
                        editInfo:editInfo,
                        saveCallback:saveCallback
                    },
                    componentID:this.ID
                };
                ds.validateData(
                    editInfo.values,
                    // this callback handles calling saveLocally directly.
                    {target:this,methodName:"remoteValidationForLocalSaveComplete"},
                    requestProperties
                );

                // Hang onto a flag noting we're in the process of saving this row
                if (this._savingEdits == null) this._savingEdits = {};
                this._savingEdits[editInfo.editValuesID] = true;

                return;
            }
        }
        // no server-side validation errors so just call saveLocally synchonously.
        return this._saveLocally(editInfo, saveCallback);
    }

    // If we're offline, forbid the save
    if (isc.Offline && isc.Offline.isOffline() && !this.dataSource.clientOnly) {
        isc.warn(this.offlineSaveMessage);
        return false;
    }

    // otherwise, submit change to server, and wait until it returns without error to commit
    var callback = this.getID() + "._updateRecordReply(dsResponse, dsRequest)",
        request = {
        // allow customizing the operation used to save
        operationId : (oldValues == null ? this.addOperation : this.updateOperation) ||
                    this.saveOperation,
        // allow customizing the application the operation is performed in
        application : this.application,

        // so we can see validation errors
        willHandleError : true,
        // show a prompt to block user interaction during save
        showPrompt : this.shouldWaitForSave(),

        // for long transactions, if supported by backend.  NOTE: currently no error codes or
        // frontend UI defined for handling concurrent update error case
        oldValues : oldValues,

        // the actual record object that was edited, if present.  Needed for
        _originalRecord: this.getRecord(rowNum, colNum),

        // Client side context stuff
        internalClientContext:{
            // if we were passed an explicit callback, hang onto this
            // (It will be fired by _updateRecordReply)
            saveCallback : saveCallback,

            // changed values
            newValues : newValues,
            // all other context
            editInfo : editInfo
        },

        componentId: this.ID
    };

    // support for dataBoundComponentField.includeFrom:<dataSourceID>.<fieldName>
    // for fields that are not in the dataSource but pick up their value from
    // a related dataSource
    // In this case simply update the outputs property of the request -- the
    // server will be responsible for actually getting the value from the other
    // dataSource
    var completeFields = this.getAllFields();
    if (completeFields != null) {
        for (var i = 0; i < completeFields.length; i++) {
            if (completeFields[i].includeFrom != null &&
                this.getDataSource().getField(completeFields[i].name) == null)
            {
                if (request.additionalOutputs == null) request.additionalOutputs = "";
                else request.additionalOutputs += ",";
                request.additionalOutputs += [
                        completeFields[i].name,
                        completeFields[i].includeFrom].join("!")

            }
        }
    }

    if (this.saveRequestProperties) {
        isc.addProperties(request, this.saveRequestProperties);
    }

    // unsort to prevent changed data popping out of viewport

    this._unsortOnChange(newValues, oldValues);

    // Hang onto a flag noting we're in the process of saving this row
    if (this._savingEdits == null) this._savingEdits = {};
    this._savingEdits[editInfo.editValuesID] = true;

    var ds = isc.DS.get(this.dataSource);

    if (oldValues == null) {
        var newRecord = isc.addProperties({}, oldValues, newValues);
        ds.addData(newRecord, callback, request);
    } else {
        // combine the new values with primary keys to form the update set for the modified
        // record.  Copy so that we still have the original values intact when the transaction
        // returns.
        var pks = isc.DS.get(this.dataSource).filterPrimaryKeyFields(oldValues),
            updates = isc.addProperties({}, pks, newValues);
        ds.updateData(updates, callback, request);
    }
},

_saveLocally : function (editInfo, saveCallback) {

    var rowNum = editInfo.rowNum,
        colNum = editInfo.colNum,
        oldValues = editInfo.oldValues,
        newValues = editInfo.values;


    var record = this.getCellRecord(rowNum, colNum),
        addNew = (record == null);
    // Note: This should be split into a separate method, which would handle updating
    // local data, or server-side data (and the list would automatically unsort as a result
    // of dataChanged())
    if (addNew) {
        var data = this.getOriginalData();

        // Associate the new record object with the edit values before adding the record to
        // the data array.

        var editValuesContext = this.getEditSession(rowNum);
        editValuesContext._primaryKeys = this.getPrimaryKeys(newValues);

        delete editValuesContext._newRecord;

        if (isc.ResultSet && isc.isA.ResultSet(data)) {
            if (data.allRows != null) {
                data.allRows.add(isc.addProperties({},newValues));
                data.filterLocalData();
            } else {
                this.logWarn("Unable to add data to resultSet - allRows is not set");
            }
        } else {
            data.add(newValues);
            data.dataChanged();
        }

    // update the record in place with 'setRawCellValue()' if we're editing an existing one
    } else {

        var cellValue, undef;

        var seenFieldNames = {};
        for (var i = 0; i < this.completeFields.length; i++) {
            var field = this.completeFields[i],
                fieldLocator = this.getEditorName(rowNum, field, true);
            cellValue = isc.Canvas._getFieldValue(null, field, newValues,
                                                     this, true, "edit");
            if (cellValue !== undef) {
                var fieldNum = this.fields.indexOf(field);
                if (fieldNum != -1) {
                    this.setRawCellValue(record, rowNum, fieldNum, cellValue);
                } else {
                    isc.Canvas._saveFieldValue(null, field, cellValue,
                                                 record, this, true);
                }
            }


            var name = fieldLocator;
            if (fieldLocator.indexOf("/") >= 0) {
                name = isc.DynamicForm._trimDataPath(fieldLocator, this);
            }
            seenFieldNames[name] = true;
        }


        this.combineValuesRecursively(record, newValues, seenFieldNames);
    }

    // un-sort the list if the sorted field value was edited.  Otherwise the record might
    // get sorted out of the viewport.
    this._unsortOnChange(newValues, oldValues);
    this._editCompleteCallback(editInfo, saveCallback);

    // If we are part of a valuesManager notify it of the update

    if (this.valuesManager != null) {
        // _updateMultipleMemberValue handles combining a data-index into a path
        // 2nd param unnecessary since we're modifying the whole record, not a field within it.
        this.valuesManager._updateMultipleMemberValue(rowNum, null, record, this);
    }
},

combineValuesRecursively : function (target, source, skipFields, path, objectCache) {
    if (path == null) path = "";

    // Avoid stack overflows arising from recursion loops
    if (objectCache == null) objectCache = [];
    if (isc.isAn.Object(target) && !isc.isA.Date(target)) {
        if (objectCache.contains(target)) return;
        objectCache.add(target);
    }

    if (skipFields[path]) {
        //isc.logWarn("Skipping object tree rooted at path '" + path + "' because it's in the skip list");
        return;
    }

    if (isc.isAn.Array(source)) {
        //isc.logWarn("Processing array at dataPath '" + path);
        for (var i = 0; i < source.length; i++) {
            var newPath = path == "" ? "" + i : path + "/" + i;
            if (isc.isAn.Object(source[i]) && !isc.isA.Date(source[i])) {
                if (target[i] == null) target[i] = isc.isAn.Array(source[i]) ? [] : {};
                this.combineValuesRecursively(target[i], source[i], skipFields, newPath, objectCache);
            } else {
                target[i] = source[i];
            }
        }
    } else if (isc.isAn.Object(source) && !isc.isA.Date(source)) {
        //isc.logWarn("Processing object at dataPath '" + path);
        for (var attr in source) {
            var newPath = path == "" ? attr : path + "/" + attr;
            if (isc.isAn.Object(source[attr]) && !isc.isA.Date(source[attr])) {
                if (target[attr] == null) target[attr] = isc.isAn.Array(source[attr]) ? [] : {};
                this.combineValuesRecursively(target[attr], source[attr], skipFields, newPath, objectCache);
            } else {
                if (skipFields[newPath]) {
                    //isc.logWarn("Skipping value at dataPath '" + newPath + "' - found it as a leaf in the skip list");
                } else {
                    //isc.logWarn("Setting value at dataPath '" + newPath + "' to " + source[attr]);
                    target[attr] = source[attr];
                }
            }
        }
    }
},


remoteValidationForLocalSaveComplete : function (dsResponse, data, dsRequest) {
    var context = dsResponse.internalClientContext,
        editInfo = context.editInfo;

    // drop the savingEdits flag we set up to ensure we don't kick off multiple save attempts
    // for the same edited values, etc.
    if (this._savingEdits && this._savingEdits[editInfo.editValuesID]) {
        delete this._savingEdits[editInfo.editValuesID];
        if (isc.isAn.emptyObject(this._savingEdits)) delete this._savingEdits;
    }

    if (dsResponse.status < 0 && dsResponse.status != isc.RPCResponse.STATUS_VALIDATION_ERROR) {
        this.logWarn("Server-side validation failed: " + dsResponse.data);
        isc.RPCManager.handleError(dsResponse,dsRequest);
        return;
    }

    var liveEdits = this._getEditValues(editInfo.editValuesID);

    if (dsResponse.errors) {
        var errors = isc.DynamicForm.getSimpleErrors(dsResponse.errors),
            nonEmptyErrors = false;

        // Show server errors
        for (var fieldName in errors) {
            nonEmptyErrors = true;
            if (liveEdits == null) {
                this.logWarn("Asynchronous remote validation failed for attempted save of edits " +
                    "for row:" + editInfo.rowNum + " but edit values have subsequently been " +
                    "discarded. Ignoring validation failure.  Validation failure details:\n" +
                    this.echoFull(errors));
                break;
            }
            var fieldErrors = errors[fieldName],
                field = this.getField(fieldName)
            ;
            if (fieldErrors != null && field != null) {
                // if the edit val has changed since the user attempted to save it, we should
                // presumably not show the validation error for the field.

                var saveVal = editInfo.values[fieldName],
                    liveVal = liveEdits[fieldName];

                if (saveVal != liveVal &&
                    !(isc.isA.Date(liveVal) && isc.isA.Date(saveVal) &&
                        (liveVal.getTime() == saveVal.getTime())))
                {
                    this.logWarn("Asynchronous remote validation failed for attempted save of edits " +
                        "for row:" + editInfo.rowNum + ". Submitted value for field " + fieldName +
                        " was " + saveVal + ", but the edit value has subsequently been modified " +
                        "and is now:" + liveVal + ". Not showing the following validation " +
                        "error[s] for this field:\n" + this.echoAll(fieldErrors));

                    continue;
                }
                if (!isc.isAn.Array(fieldErrors)) fieldErrors = [fieldErrors];
                fieldErrors = fieldErrors.getProperty("errorMessage");

                this.setFieldError(editInfo.rowNum, fieldName, fieldErrors, false);
            }
        }
        if (nonEmptyErrors) return;
    }

    // At this point we know validation was successful - run saveLocally to update the
    // actual data set.
    this._saveLocally(editInfo, context.saveCallback);

},

// unsort() if we are sorted and the sort field will be changed in this update
_unsortOnChange : function (newValues, oldValues) {
    //this.logWarn("unsort on change: sortFieldNum: " + this.sortFieldNum +
    //             ", newValues: " + this.echo(newValues) +
    //             ", oldValues: " + this.echo(oldValues));
    var sortFieldNum = this._getSortFieldNum();
    if (sortFieldNum == null) return;

    var sortField = this.fields[sortFieldNum];
    if (sortField == null) return;

    var sortFieldName = sortField[this.fieldIdProperty];
    var undef;
    if (sortFieldName != null && newValues[sortFieldName] !== undef &&
        (oldValues == null || newValues[sortFieldName] != oldValues[sortFieldName]))
    {
        this.unsort();
        //this.logWarn("called unsort, sortProp is: " + this.data._sortProperty);
    }
},

// callback from server save triggered in commitEditValue
_updateRecordReply : function (dsResponse, dsRequest) {
    var results = dsResponse.data,
        context = dsResponse.internalClientContext,
        editInfo = context.editInfo,
        colNum = editInfo.colNum,
        field = this.fields[colNum];

    // Clear out the _savingEdits flag for this row as the save will have completed now.

    if (this._savingEdits && this._savingEdits[editInfo.editValuesID]) {
        delete this._savingEdits[editInfo.editValuesID];
        if (isc.isAn.emptyObject(this._savingEdits)) delete this._savingEdits;
    }

    // the rowNum may be different by now due to unrelated changes.  Look up the editValues we
    // were trying to save by editValuesId, and see what rowNum they are now associated with.

    var editSession = this.getEditSession(editInfo.editValuesID);

    if (editSession != null) {
        editInfo.rowNum = editSession._rowNum;

    } else {

        if (results && results[0]) {
            editInfo.rowNum = this.findRowNum(results[0]);
            if (this.useCellRecords) editInfo.colNum = this.findColNum(results[0]);
        }

        // If no record was returned, assume it was an attempt to save a new row which
        // failed to save, and for which editVals have now been discarded
        else editInfo.rowNum = -1;
    }

    var rowNum = editInfo.rowNum, colNum = editInfo.colNum;
    if (dsResponse.status < 0) { // error occurred
        // XXX We should really check for the 'validation failed' error code here
        // (results.status) rather than just checking for the presence of errors

        // we store validation errors per row as a simple object with an array
        // of error message strings per field.  dsResponse.errors is allowed to have one of a
        // few possible formats - convert this error block into our internal format before
        // storing / displaying errors
        var errors = this.parseServerErrors(dsResponse.errors);

        this.logInfo("error on save, status: " + dsResponse.status +
                     ", errors: " + this.echo(dsResponse.errors), "gridEdit");

        // The server will return an array of error object - one per record edited.
        // Since we only support saving out one record at a time, this should be a one element
        // array only.
        // Pull out the error object for the edited row, and pass it back to the setRowErrors()
        // method to store and display the errors

        if (isc.isAn.Array(errors)) {
            if (errors.length != 1)
                this.logWarn("Server reports validation errors for multiple records - only " +
                             "displaying errors for the first record.", "gridEdit");

            errors = errors[0];
        }

        // Store, and display the new errors
        if (this.useCellRecords) this.setCellErrors(rowNum, colNum,
                                                    errors[this.getEditorName(rowNum, colNum)]);
        else this.setRowErrors(rowNum, errors);

        // fire the simple callback function indicating the edit failed
        // (The second param would be any additional callback to fire on completion)
        this._editFailedCallback(editInfo, context.saveCallback, dsResponse, dsRequest);

        return false;
    }
    // if we got here, the edit was successful
    this._editCompleteCallback(editInfo, context.saveCallback, dsResponse, dsRequest)
},

// parseServerErrors(): convert the formats allowed for dsResponse.errors to our internal
// error storage format

parseServerErrors : function (errors) {
    if (isc.isAn.Array(errors)) {
        if (errors.length > 1) {
            this.logWarn("server returned errors for multiple records - dropping all but the " +
                        "first record returned");
        }
        errors = errors[0];
    }


    if (errors && errors.recordPath) delete errors.recordPath;

    for (var fieldName in errors) {
        var fieldErrors = errors[fieldName];
        if (fieldErrors == null) fieldErrors = "Unspecified error";

        if (isc.isAn.Array(fieldErrors)) {
            for (var i = 0; i < fieldErrors.length; i++) {
                fieldErrors[i] = fieldErrors[i].errorMessage;
            }
        } else if (fieldErrors.errorMessage) {
            errors[fieldName] = [fieldErrors.errorMessage];
        } else {
            errors[fieldName] = [fieldErrors];
        }
    }
    return errors;
},


//>    @method listGrid._editCompleteCallback    (I)
//
//      Callback method triggered when a saveEditedValues flow completes successfully.
//      Fired whether the save succeeded on the client or on the server.
//      Calls the 'editComplete' string method (with the same signature) if present.
//
//  @group  editing
//
//  @param  editInfo (object)       Details of the edit
//                                  <br>Contains:
//                                  <ul>
//                                  <li>rowNum - index of edited row</li>
//                                  <li>colNum - index of edited column</li>
//                                  <li>values - edited values for the record</li>
//                                  <li>oldValues - values for the record before this edit</li>
//                                  <li>editCompletionEvent - how the edit was completed</li>
//                                  </ul>
//  @param [callback] (function)      Optional callback function to fire
//  @param [dsResponse] (DSResponse)  DSResponse for DataSource saves
//
//  @visibility internal
//<
_editCompleteCallback : function (editInfo, callback, dsResponse, dsRequest) {
    // If the editForm has focus, ensure the latest values have been stored
    if (this._editorShowing && this.getEditForm().hasFocus) this.storeUpdatedEditorValue(true);

    var rowNum = editInfo.rowNum,
        colNum = editInfo.colNum,
        valuesID = editInfo.editValuesID,
        editSession = this.getEditSession(valuesID),
        newRecord = this.isNewEditRecord(valuesID);

    var record;

    // Databound saves will have been passed a dsResponse from the server
    if (dsResponse != null) {

        var updateData = isc.DataSource.getUpdatedData(dsRequest, dsResponse, true),
            record = isc.isAn.Array(updateData) ? updateData[0] : updateData;
        if (record == null) {

            return;
        }

        // If the user was editing a new record, we may not have any primary key field
        // on the editSession object, required to reliably associate the editSession with
        // the record in the dataSet.
        // Catch this case and call newRecordForEditValues() to perform the association
        if (editSession && editSession._primaryKeys == null) {
            this.newRecordForEditValues(valuesID, record);
        }

    } else {
        // for client-only saves assume the save will never change the rowNum
        record = this.getCellRecord(rowNum, colNum)
    }

    // Clear up the editValues that have already been saved to the server.
    // Leave anything that has been edited after the save was committed

    var submittedValues = editInfo.values,
        // use th internal getEditValues() since we already stored the updated value if
        // appropriate
        currentEditValues = this._getEditValues(valuesID),


        stillEditing = this.isEditingRecord(rowNum, colNum),
        primaryKeyFields =
            this.dataSource ? isc.DS.get(this.dataSource).getPrimaryKeyFieldNames()
            : null;

    var hasChanges = false;
    for (var field in currentEditValues) {
        var isPK = primaryKeyFields && primaryKeyFields.contains(field);
        if (this.fieldValuesAreEqual(this.getField(field),
                                     currentEditValues[field], submittedValues[field]))
        {
            // If we're still showing the editor, don't wipe out primary key fields- we always
            // want these included in our edit values. Only required for databound grids.

            if (!(stillEditing && isPK)) this.clearEditValue(valuesID, field, true, true);
        } else {
            if (!isPK) hasChanges = true;
        }
    }
    // If the editor is no longer showing, and there are no outstanding edits, ensure the
    // editValues object has been cleared
    // Note: If the editor is still showing, we'll drop all unchanged edit values when the
    // editor gets hidden.

    if (!stillEditing && !hasChanges) {
        this._clearEditValues(valuesID, colNum);
    }

    // displayUpdatedDSRecord will refresh the mapping between pending edit values and rowNums.
    // We can avoid this if this was a local save (which we can handle by just refreshing
    // the cells in question.
    if (dsResponse != null || newRecord) {
        if (dsResponse != null && this.isGrouped) {
            var ignore = false;
            // New record always requires regrouping so it ends up in the correct group.
            // Existing record only requires regroup if group field value changed.
            if (!newRecord) {
                ignore = true;
            var groupFields = this.getGroupByFields(),
                    fieldNames = dsResponse.context && dsResponse.context.params ? dsResponse.context.params : {}
            ;
            for (var i=0; i<groupFields.length; i++) {
                var key = groupFields[i];
                if (fieldNames[key] != null) {
                    ignore = false;
                    break;
                }
            }
            }
            if (ignore) this._ignoreRegroup = true;
        }

        this.displayUpdatedDSRecord(rowNum, colNum, record);
    }

    // At this point edit context objects match their associated edit rows, and all edit values
    // have been updated.
    // Fire callbacks.



    var newValues = editInfo.values,
        oldValues = editInfo.oldValues,
        editCompletionEvent = editInfo.editCompletionEvent;


    if (rowNum == -1) rowNum = editInfo.rowNum;

    if (this.convertToMethod("editComplete")) {
        this.editComplete(rowNum, colNum, newValues, oldValues, editCompletionEvent, dsResponse);
    }

    this._fireCellChanged(record, rowNum, colNum, newValues, oldValues, dsRequest);


    if (callback) this._fireSaveCallback(callback, rowNum, colNum, editCompletionEvent, true);
},

// displayUpdatedDSRecord - called from editCompleteCallback on databound save
// calls dataChanged() to to refresh the mapping between pending edit values and rowNums,
// and redraw to show new rows


displayUpdatedDSRecord : function (rowNum, colNum, record) {
    if (this._remapEditRows()) {
        this.suppressEditRowRemap = true;
        this.dataChanged();
        this.suppressEditRowRemap = false;
    } else if (this.isGrouped) {
        // If we're grouped, and we didn't fire dataChanged here, explicitly regroup so
        // records' whose group field value changed show up in the right place.

        if (this._ignoreRegroup && !this._markForRegroup) {
            delete this._ignoreRegroup;
            return;
        }
        this.regroup();
    }
},

isEditingRecord : function (rowNum, colNum) {
    return this.getEditRow() == rowNum;
},

// Fires the single callback explicitly passed into saveEdits() as a parameter
_saveCallbackParams:"rowNum,colNum,editCompletionEvent,success",
_fireSaveCallback : function (callback, rowNum, colNum, editCompletionEvent, success) {
    if (callback != null) {
        this.fireCallback(callback,
                          this._saveCallbackParams,
                          [rowNum,colNum,editCompletionEvent,success]
        );
    }
},

_fireCellChanged : function (record, rowNum, colNum, newValues, oldValues, dsRequest) {
    // if a cellChanged was supplied, call that
    // Currently available at field and LG level (should we allow it per-row too?)
    var changeGroup, autoFitField;
    for (var i in newValues) {
        var oldValue = (oldValues == null ? null : oldValues[i]);
        if (newValues[i] == oldValue) continue;
        var currentField = this.fields.find(this.fieldIdProperty, i);

        // if a field was changed which does not exist in the grid's field list,
        // ignore it. It can't be grouped and cellChanged() cannot be called on it.
        if (!currentField) continue;

        // check for the new to re-group this record
        if (this.isGrouped && currentField) {
            var groupFields = this.getGroupByFields();
            if (groupFields.contains(currentField.name)) changeGroup = true;
        }
        if (this.shouldAutoFitField(currentField)) {
            autoFitField = true;
        }

        this._cellChanged(
                            record, currentField,
                            newValues[i], oldValue,
                            rowNum,
                            this.fields.indexOf(currentField)
                          );
    }


    if (autoFitField) {
        if (this.shouldSaveLocally()) {
            // if bound, dataChanged will already handle this
            this.updateFieldWidthsForAutoFitValue("Local edit");
        }
    }
    if (changeGroup) {
        if (this.shouldSaveLocally()) {
            // if bound, dataChanged will already handle this
            this._incrementalRegroup(record, record, newValues);
        }

        // redraw grid
        // regardless of if the data is saved locally, folderToggleObservation is not
        // called before this point, so it must exist outside the above codepath.
        this._suppressFolderToggleRedraw = false;
        this._folderToggleObservation();
    }
},

// Fire the 'cellChanged' string method handler(s) at the field or List level, if defined.
// If an entire row was edited this method (and the public cellChanged handlers) will be fired
// for each field edited.
_cellChanged : function (record, field, newValue, oldValue, recordNum, fieldNum) {

    if (field && field.cellChanged) {
        if (!isc.isA.Function(field.cellChanged)) {
            // (We must use replaceWithMethod rather than convertToMethod as the field is
            // not an ISC class with a string method registry on it)
            isc.Func.replaceWithMethod(field, "cellChanged",
                        "record,newValue,oldValue,rowNum,colNum,grid,recordNum,fieldNum");
        }

        // call() to call the function on the grid rather than the field
        field.cellChanged.call(this,
                               record,newValue,oldValue,recordNum,fieldNum,this,recordNum,fieldNum);

    } else if (this.cellChanged) {
        // call the list.cellChanged method, and note if it returns false (thus rejecting the change)
        this.cellChanged(record,newValue,oldValue,recordNum,fieldNum,this,recordNum,fieldNum);
    }

},

// Called when saving fails due to validation or other errors.  Calls the 'editFailed'
// string method (with the same signature) if present.  Will be called once per record
// if there was an attempt to save multiple records at once (saveAllEdits())

_editFailedCallback : function (editInfo, callback, dsResponse, dsRequest) {

    var rowNum = editInfo.rowNum,
        colNum = editInfo.colNum,
        values = editInfo.values,
        oldValues = editInfo.oldValues,
        eCe = editInfo.editCompletionEvent
    ;

    var record = this.getRecord(rowNum);
    if (record != null && this.canExpandRecord(record, rowNum) &&
        this.expansionMode == "editor")
    {
        // we're expandable and
        var errors = isc.isAn.Array(dsResponse.errors) ? dsResponse.errors[0] : dsResponse.errors,
            record = this.getRecord(rowNum),
            shouldExpand = false
        ;

        if (isc.isAn.Object(errors)) {
            for (var key in errors) {
                var field = this.getField(key);
                if (!field || !this.fieldIsVisible(field)) {
                    shouldExpand = true;
                    break;
                }
            }
        }

        if (shouldExpand) {
            this.expandRecord(record);
            // do we need to validate the form just now?
        }
    }

    // note newValue can be a single value or an entire record
    if (this.convertToMethod("editFailed")) {
        this.editFailed(rowNum, colNum, values, oldValues, eCe, dsResponse, dsRequest);
    }
    if (callback != null) this._fireSaveCallback(callback, rowNum,colNum,eCe,false);


    this.clearSubmittedEditValues(editInfo.editValuesID, values);

},

// see doc near stringMethod declaration, below
editFailed : function (rowNum, colNum, newValues, oldValues, editCompletionEvent, dsResponse, dsRequest) {

    // if this was an attempted server-side commit that returned a failure code from the server
    // (with no specific error messages), pass it on to rpcManager.handleError()

    if (dsResponse != null && dsResponse.errors == null) {
        isc.RPCManager._handleError(dsResponse, dsRequest);
    }
},

// ============================================================================================
// Validation
// ============================================================================================

// Performing validation
// ------------------------------------------------

//> @groupDef gridValidation
// ListGrids support automatic validation of edited cells / records. This group is a collection
// of APIs related to the validation subsystem.
// <p>
// Default validation occurs in response to the user navigating between edit cells
// (see +link{listGrid.validateByCell}) or whenever edited values are to be committed to the
// server for saving.  Standard validation can also be triggered for a cell or row programmatically at
// any time.<br>
// When standard validation occurs, +link{listGridField.validators} will be run on each
// cell to be validated.<br>
// In addition to this standard behavior developers can add custom errors to fields
// via +link{listGrid.setFieldError()} / +link{listGrid.setFieldError()}.
//
// @see group:editing
// @visibility external
//<


//> @method listGrid.validateRow()
// Validate the current set of edit values for the row in question.
// <P>
// Called when the user moves to a new edit row, or when an edited record is to be saved if
// client side validation is enabled for this grid.
// <P>
// This method may also be called directly to perform row level validation at any time.
//
// @param   rowNum (int) index of row to be validated.
// @return  (Boolean) returns true if validation was successful (no errors encountered), false
//                    otherwise.
// @group gridValidation
// @visibility external
//<
validateRow : function (rowNum, suppressRefresh) {
    return this.validateRecord(rowNum, suppressRefresh);
},


// validateRecord()
// Actually perform the validation of edit values for some record
// For one-record-per-cell implementations this will need to be overridden
validateRecord : function (cell, suppressRefresh) {
    var rowNum, colNum;
    if (isc.isAn.Array(cell)) {
        rowNum = cell[0];
        colNum = cell[1];
    } else {
        rowNum = cell;
    }


    var newValues = this.getEditValues(rowNum, colNum);

    // Feed this through _getFieldValue to narrow down by dataPath if necessary
    //if (this.dataPath) {
    //    newValues = isc.Canvas._getFieldValue(this.dataPath, newValues, this);
    //}

    // If the row is not being edited always "pass" validation.
    if (newValues == null) return true;

    var record = this.getCellRecord(rowNum,colNum),
        // Validate every field. This is necessary because of conditional validators
        // that may depend on other field values.
        fields = this.getFields().getProperty(this.fieldIdProperty)
    ;

    // Wrap field validation in a queue so that server validators are
    // sent as a single request.
    var wasAlreadyQueuing = isc.rpc.startQueue();

    if (this._skipServerValidation != true) {
        this._deferServerValidation = true;

        // Field objects that require server validation
        this._fieldsNeedingServerValidation = [];
    }

    var errorBlock = this.validateRowValues(newValues, record, rowNum, fields);

    // Perform deferred server validation if needed
    if (this._deferServerValidation && this._fieldsNeedingServerValidation.length > 0) {
        var validationOptions = {rowNum: rowNum}
        this.validateFieldsOnServer(this._fieldsNeedingServerValidation, this.getCellRecord(rowNum), validationOptions);
    }
    this._deferServerValidation = null;

    // Submit server validation requests queue
    if (!wasAlreadyQueuing) isc.rpc.sendQueue();

    // call setRowErrors() to store, and display the new set of editValues for the row.
    this.setRowErrors(rowNum, errorBlock, suppressRefresh);

    return (errorBlock == null)
},


//>    @method listGrid.validateRowValues()
//  Helper method to perform validation on a row.
//  Called by validateRow() - returns any errors found on the row.
//
//    @group gridValidation
//  @param  newValues   (object)    Field-value mapping showing the edited values for each field
//  @param  oldValues   (object)    Field-value mapping showing the values for each field before
//                                  editing
//  @param  rowNum      (number)    Index of the row being edited
//  @param  fields      (number | string | array)
//                          Fields to be validated.  If more than one field is to be validated,
//                          this will be an array.
//                          Note that each field can be designated either by fieldName or the
//                          numeric index.
//  @return (object)   set of errors returned by validation, in the format
//                      {fieldName:["errorMessage", ...],...}.
//                     will be null if no errors encountered
//  @visibility internal
//<
validateRowValues : function (newValues, oldValues, rowNum, fields) {
    var validationPassed = true,
        errorBlock = {};

    // handle being passed a single field
    if (!isc.isAn.Array(fields)) fields = [fields];

    // Handle editing a new record - oldvalues for each cell will be undefined.
    var isNewRecord = false;
    if (oldValues == null) {
        isNewRecord = true;
        oldValues = {};
    }

    // validate each field separately, then show the errors for all fields
    for (var i = 0; i < fields.length; i++) {

        if (fields[i] == null) continue;
        var fieldName,
            colNum;

        // ensure fields[i] is a fieldName and record the colNum
        if (isc.isA.Number(fields[i])) {
            colNum = fields[i];
            fieldName = fields[i] = this.getFieldName(colNum);
        } else {
            fieldName = fields[i];
            colNum = this.getFields().findIndex(this.fieldIdProperty, fieldName)
        }
        // If the field isn't included in this list, skip it... This is likely to happen
        // as lists won't show every field, but we will include the primary keys in our edit
        // values
        if (colNum < 0) continue;

        // validate if this is an editable cell in a new record (in order to enforce eg the
        // required validator), or if the field has been edited.
        var shouldValidateCell = (
            (isNewRecord && this.canEditCell(rowNum, colNum)) ||
            isc.propertyDefined(newValues, fieldName)

        );
        var newValue, oldValue;

        var field = this.getField(fieldName),
            dataPath = field ? field.dataPath : null,
            undef;
        if (dataPath != null) {
            dataPath = this._trimDataPath(dataPath);
        } else {
            dataPath = field.name;
        }

        newValue = isc.Canvas._getFieldValue(dataPath, field, newValues,
                                                        this, true, "validate");
        oldValue = isc.Canvas._getFieldValue(dataPath, field, oldValues,
                                                            this, true, "validate");

        if (!shouldValidateCell) {
            shouldValidateCell = (newValue !== undef || oldValue === undef || oldValue == null);
        }
        if (!shouldValidateCell) continue;


        var validationErrors = this.validateCellValue(rowNum, colNum, newValue, oldValue);

        if (validationErrors != null) {
            validationPassed = false;
            errorBlock[fieldName] = validationErrors;
        }
    }
    if (!validationPassed) return errorBlock;
    return null;
},


//> @method listGrid.validateCell()
// Validate the current edit value for the cell in question.  Called when the user moves to a
// new edit cell if +link{ListGrid.validateByCell} is true.<br>
// This method may also be called directly to perform cell level validation at any time.
// @param   rowNum (number) index of row to be validated.
// @param   fieldName (string | number) field name (or column index) of field to be validated
// @return  (Boolean) returns true if validation was successful (no errors encountered), false
//                    otherwise.
// @group gridValidation
// @visibility external
//<
validateCell : function (rowNum, fieldName, suppressDisplay, processDependencies) {

    var colNum;
    if (isc.isA.String(fieldName)) colNum = this.getColNum(fieldName);
    else {
        colNum = fieldName;
        fieldName = this.getEditorName(rowNum, colNum);
    }

    // if we're showing an edit item for the field, ensure its value is up to date.

    if (this._editorShowing && (this.getEditRow() == rowNum )
        && this.getEditForm().getItem(fieldName) != null)
    {
        this.storeUpdatedEditorValue(null, colNum);
    }

    var editVals = this.getEditValues(rowNum, colNum),
        hadErrors = this.cellHasErrors(rowNum, fieldName),
        newValue = editVals ? editVals[fieldName] : null,
        record = this.getCellRecord(rowNum, colNum),
        oldValue = record ? record[fieldName] : null;

    // don't validate if the cell has not been edited (unless this is a new record in which
    // case it will be saved as a null value.
    if (record != null && (!editVals || !isc.propertyDefined(editVals, fieldName))) {
        return true;
    }

    var errors = this.validateCellValue(rowNum, colNum, newValue, oldValue, processDependencies);
    if (errors != null) {
        // call setFieldError() to store the error object on the
        // appropriate cell, and display it to the user.
        this.setFieldError(rowNum, fieldName, errors);
        return false;
    }

    // if the cell previously had errors, explicitly clear them out
    if (hadErrors) {
        this.setFieldError(rowNum, fieldName, null);
    }
    return true;
},

//>    @method listGrid.validateCellValue()    (A)
// Helper method for validateCell(): Validate the new value the user supplied for a single cell.
// <p>
// Default implementation falls through to the stringMethod
// list.validateFieldValue(newValue, oldValue, record, field, rowNum, colNum) to
// perform validation based on the edit field's validators.
//
//    @group gridValidation
//
//  @param  rowNum  (integer)   Edited cell's row number
//  @param  colNum (integer)   Edited cell's field number
//    @param    newValue    (any)        Value returned by the editor.
//  @param  oldValue    (any)   Value before editing
//  @return (array) Array of validation error objects, or null if none found
//<
validateCellValue : function (rowNum, colNum, newValue, oldValue, processDependencies) {
    var record = this.getCellRecord(rowNum, colNum);
    var field = this.getField(colNum);
    return this.validateFieldValue(newValue, oldValue, record, field, rowNum, colNum, processDependencies);
},

// Allow cusomization of the 'required field' validation error message

//> @method listGrid.getRequiredFieldMessage()
// Returns the message to display when a user attempts to save a required field with an
// empty value.  By default returns +link{Validator.requiredField}.
// @param field (object) definition of the field being edited
// @param record (object) record object being edited
// @return (string) "Field is required"
// @group i18nMessages, gridValidation
// @visibility external
//<
getRequiredFieldMessage : function (field, record) {
    return isc.Validator.requiredField;
},

//>    @method listGrid.validateFieldValue()    (A)
//
//     Validate the new value the user supplied for a cell via an editCellValue() call against
//  any validators defined on the edit field.
//
//    @group    editing
//
//    @param    newValue    (any)        Value returned by the editor.
//  @param  oldValue    (any)       Value before editing
//  @param  record  (object)    pointer to the record object for the edited cell
//  @param  field   (object)    pointer to the field descriptor object for the edited cell
//  @param  rowNum  (number)    row index of the edited cell
//  @param  colNum  (number)    column index for the edited cell
//  @return (array)     array of error objects, or null if no errors found
//<
validateFieldValue : function (newValue, oldValue, record, field, rowNum, colNum, processDependencies) {
    var errors = [],
        success = true;

    var required = this.cellIsRequired(rowNum, colNum);
    // Check for required fields
    if (required && (newValue == null || isc.isAn.emptyString(newValue))) {
        var errorMessage = this.getRequiredFieldMessage(field, record);
        errors.add(errorMessage);
        success = false;
    }
    // validate the value if the field specifies validators
    var validators = this.getCellValidators(rowNum, colNum);
    if (validators) {

        if (this.logIsDebugEnabled("gridEdit")) {
            this.logDebug((this.useCellRecords ? "At col:" + colNum : "At field: " + field.name) +
                          " applying validators: " + this.echoAll(validators) +
                          " to value:" + newValue,
                          "gridEdit");
        }

        var fieldResult,
            editedRecord = this.getEditedRecord(rowNum,colNum),
            allErrors = null,
            options = {rowNum: rowNum}
        ;

        if (this._skipServerValidation == true) {
            options.skipServerValidation = true;
        } else if (this._deferServerValidation == true) {
            options.deferServerValidation = true;
        }
        if (processDependencies) {
            fieldResult = this.validateFieldAndDependencies(field, validators, newValue,
                                            editedRecord, options);
        } else {
            fieldResult = this.validateField(field, validators, newValue,
                                            editedRecord, options);
        }
        if (fieldResult != null) {
            if (fieldResult.needsServerValidation) {
                this._fieldsNeedingServerValidation.add(field);
            }
            if (fieldResult.valid != true) {
                success = false;
                if (processDependencies)
                    errors = fieldResult.errors[field.name] || [];
                else
                    errors = fieldResult.errors || [];
            } else {

                if (fieldResult.resultingValue != null) {
                    this.setEditValue(rowNum, colNum,
                                      fieldResult.resultingValue,
                                      null, null, null, true);
                    newValue = fieldResult.resultingValue;
                }
            }

            // Even though the changed field may be valid, there may be other fields
            // that are no longer valid because of a dependency. These errors should
            // be shown on the form.
            allErrors = fieldResult.errors;

            if (this.logIsInfoEnabled("gridEdit")) {
                this.logInfo("validateFieldValue, newValue: " + this.echo(newValue) +
                             ", passed validation: " + success +
                             ", resultingValue: " + this.echo(fieldResult.resultingValue),
                             "gridEdit");
            }
        }
    }

    // If dependencies were processed, we may have errors on other fields to show/clear.
    if (processDependencies) {
        for (var errorFieldName in allErrors) {
            if (errorFieldName != field.name) {
                this.setFieldError(rowNum, errorFieldName, allErrors[errorFieldName], true);
            }
        }
    }

    if (success) return null
    else return errors;
},

// cellIsRequired / getCellValidators allows us to separate our validator definitions from
// true field objects (makes custom cubeGrid validation easier to apply)
cellIsRequired : function (rowNum, colNum) {
    var field = this.getField(colNum);
    return field && (field.required || this.isXMLRequired(field));
},

getCellValidators : function (rowNum, colNum) {
    var field = this.getField(colNum);
    return field ? field.validators : null;
},

// Validation Errors
// ---------------------------------------------------------------------------------------

//> @method listGrid.hasErrors()
// Does this grid currently have errors associated with editValues for any row in the grid.
// @group gridValidation
// @return  (Boolean)   true if there are unresolved errors, false otherwise
// @visibility external
// @see listGrid.rowHasErrors()
// @see listGrid.cellHasErrors()
//<
hasErrors : function () {
    var editRows = this.getAllEditRows(true);
    for (var i = 0; i < editRows.length; i++) {
        if (this.rowHasErrors(editRows[i])) return true;
    }
    return false;
},

//> @method listGrid.rowHasErrors()
// Does the specified row have unresolved errors?
// @group gridValidation
// @param rowNum (int)   rowNum to check for errors
// @return  (Boolean)   true if there are unresolved errors, false otherwise
// @visibility external
// @see listGrid.hasErrors()
// @see listGrid.cellHasErrors()
//<
// Note: the rowNum parameter can also be the editValuesID property associated with some
// set of edit values [IE some edited row] for the grid.
rowHasErrors : function (rowNum, colNum) {
    var editData = this.getEditSession(rowNum, colNum);

    return (editData && editData._validationErrors &&
            !isc.isA.emptyObject(editData._validationErrors));
},

//> @method listGrid.cellHasErrors()
// Given a rowNum and a colNum or fieldName, determine whether we currently have stored
// validation errors for the record/field in question.
//  @group gridValidation
//  @param  rowNum  (number)    index of row to check for validation errors
//  @param  fieldID (string | number)    name of field, or index of column to check for validation
//                                      errors
//  @return (Boolean)   true if we have validation errors for the row/col in question
// @visibility external
// @see listGrid.hasErrors()
// @see listGrid.rowHasErrors()
//<
// As with rowHasErrors, the rowNum can be an editValuesID as well as a rowNum

cellHasErrors : function (rowNum, fieldID) {
    var fieldName = this.getEditorName(rowNum, fieldID),
        editData = this.getEditSession(rowNum, fieldID),
        errors = editData ? editData._validationErrors : null;

    return (
        errors != null &&
        errors[fieldName] != null
    );
},


// No obvious use for a listGrid.getErrors() method.

//>!BackCompat 2007.02.14
//> @method listGrid.getRowValidationErrors()
//  Returns any currently stored validation errors for this row in the following format:<br>
//  &nbsp;&nbsp;<code>{fieldName:[validation error array], ...}</code>
//  @group  editing
//  @visibility internal
//  @param  rowNum (number) index of row to check for validation errors.
//  @return (object)   object showing validation error arrays by field for the row passed in - if
//                      no validation errors stored for the row, null is returned.
// @deprecated in favor of +link{listGrid.getRowErrors()}
//<

getRowValidationErrors : function (rowNum) {
    return this.getRowErrors(rowNum);
},
//<!BackCompat

//> @method listGrid.getRowErrors()
//  Returns any currently stored validation errors for this row in the following format:<br>
//  &nbsp;&nbsp;<code>{fieldName:[array of error messages], ...}</code>
//  @group gridValidation
//  @param  rowNum (number) index of row to check for validation errors.
//  @return (object)   object showing validation error arrays by field for the row passed in
//                      - if no validation errors stored for the row, null is returned.
// @visibility external
// @see listGrid.getCellErrors()
//<
getRowErrors : function (rowNum, colNum) {
    var data = this.getEditSession(rowNum, colNum);
    return (data != null ? data._validationErrors : null);
},

//> @method listGrid.getCellErrors()
//  Returns the current set of errors for this cell.
//  @group gridValidation
//  @param  rowNum (number) index of row to check for validation errors.
//  @param  fieldName   (String | number)   field to check for validation errors - can be fieldName
//                                          or index of the column.
//  @return (Array of String) array of error messages (strings) for the specified cell. If no validation
//                  errors are present, returns null.
//  @visibility external
//<
getCellErrors : function (rowNum, fieldName) {
    // assume fieldName is a colNum if we're working with cellrecords
    var data = this.getEditSession(rowNum, fieldName);
    if (data == null) return null;

    if (isc.isA.Number(fieldName)) fieldName = this.getEditorName(rowNum, fieldName);
    var errors = data._validationErrors;
    return (errors == null ? null : errors[fieldName]);
},


// Synonym of setFieldError(), since we already supply getCellErrors()
setCellErrors : function (row, field, error, suppressDisplay) {
    return this.setFieldError(row, field, error, suppressDisplay);
},

//> @method listGrid.setFieldError()
// Set a validation error for some cell.
// @group gridValidation
// @param  rowNum  (number)    row index of cell to add validation error for
// @param  fieldName (String | number)    col index or field name of cell to add validation error for
// @param  errorMessage  (string | array of strings)    validation error/errors for the cell.
// @visibility external
// @see listGrid.getCellErrors()
// @see listGrid.setRowErrors()
//<
setFieldError : function (rowNum, fieldID, errorMessage, dontDisplay) {
    var fieldName = fieldID;
    if (isc.isA.Number(fieldName)) fieldName = this.getEditorName(rowNum, fieldName);

    // If passed an empty set of errors, use delete to clear the error instead
    if (errorMessage == null || (isc.isAn.Array(errorMessage) && errorMessage.length == 0)) {

        if (!this.cellHasErrors(rowNum, fieldID)) return;

        this.logDebug("setFieldError() passed empty errors- clearing the errors for row:" +
                      rowNum + ", field:" + fieldID, "gridEdit");

        var data = this.getEditSession(rowNum, fieldID);
        delete data._validationErrors[fieldName];
        // Dont hang onto an empty errors object - this will confuse hasErrors et al.
        if (isc.isAn.emptyObject(data._validationErrors)) delete data._validationErrors
    } else {
        // If passed a bad fieldNum, for example, just drop it
        if (fieldName == null) {
            this.logWarn("setFieldError() passed field identifier '" + fieldID + "'. " +
                        "Unable to find corresponding field in this grid - not setting error.");
            return;
        }

        // Warn if the user has set errors on non visible fields (or completely random fields)
        var visFields = this.getFields().getProperty(this.fieldIdProperty);
        if (!visFields.contains(fieldName)
            
            )
        {
            this.logWarn("setFieldError() passed field identifier '" + fieldID + "'. This " +
                         "is not a visible field in the grid - error may not be visible to user.");
        }

        var errors;
        if (isc.isAn.Array(errorMessage)) errors = errorMessage
        else errors = [errorMessage];

        var data = this.getEditSession(rowNum, fieldID);
        // Unlikely to happen, but catch the case of being passed a row with no edit values
        if (data == null) {
            var colNum = this.getColNum(fieldID),
                record = this.getCellRecord(rowNum, colNum);
            data = this.createEditSession(rowNum, colNum, record);
        }

        this.logDebug("setFieldError() setting errors for row:" + rowNum + ", field:"
              + fieldID + " to:" + this.echo(errors), "gridEdit");


        if (data._validationErrors == null) data._validationErrors = {};
        data._validationErrors[fieldName] = errors;
    }

    // If appropriate show the errors to the user
    if (!dontDisplay) {

        if (this.useCellRecords) this.showCellErrors(rowNum, fieldID)
        else this.showErrors(rowNum, [fieldName]);
    }
},

// This method passes straight through to setFieldError; it is implemented purely so that
// DBC._handleServerValidationResponse has a standard method to call when populating errors
addFieldErrors : function (fieldName, errorMessage, showErrors, rowNum) {
    this.setFieldError(rowNum, fieldName, errorMessage);
},

//> @method listGrid.setRowErrors()
// Set the validation errors for some row (replacing any pre-existent validation errors)
// @group gridValidation
// @param  rowNum  (number)    row to add validation error for
// @param  errors (any) validation errors for the row in the format
//                          <code>{fieldName:errorMessage, ...}</code> <br>or <br>
//                          <code>{fieldName:[errorMessage1, errorMessage2], ...}</code>
// @visibility external
// @see listGrid.getRowErrors()
// @see listGrid.setFieldError()
//<
setRowErrors : function (rowNum, errors, dontDisplay) {


    if (rowNum == null || rowNum == -1) return;

    var storedErrors = this.getRowErrors(rowNum);
    if (!errors && !storedErrors) return;

    var data = this.getEditSession(rowNum);

    if (data == null) data = this.createEditSession(rowNum, null, this.getRecord(rowNum));
    if (errors) {

        var visibleFields = this.getFields().getProperty(this.fieldIdProperty);
        for (var fieldName in errors) {
            // Warn if passed an error on a non-visible field as this will not show up
            // via the standard 'error cell' styling
            // Should probably have a customizable method to handle this
            if (!dontDisplay && !visibleFields.contains(fieldName)) {
                this.logWarn("setRowErrors() passed error for non-visible field: '" + fieldName + "'");
            }

            // Convert single error message strings into arrays.
            if (!isc.isAn.Array(errors[fieldName])) errors[fieldName] = [errors[fieldName]];
        }
    }
    this.logDebug("setRowErrors() setting errors for row " + rowNum + " to " +
                  this.echo(errors), "gridEdit");

    // Actually store the validation object.
    data._validationErrors = errors;

    if (!dontDisplay) {

        // For efficiency, only refresh cells that actually need to be refreshed due to
        // new errors, or errors being cleared.
        var fields = errors ? isc.getKeys(errors) : [];

        // We need to update each cell in the row to reflect any changes to validation errors,
        // either new errors, or errors that have been cleared.
        for (var fieldName in storedErrors) {
            var colNum = this.getColNum(fieldName);

            if (!fields.contains(fieldName)) fields.add(fieldName);
        }

        // Call showErrors() to actually update the error styling on the cells and show any
        // error messages to the user
        this.showErrors(rowNum, fields);
    }
},

//> @method listGrid.clearFieldError()
// Clears any validation errors for some cell.
// @group gridValidation
// @param  rowNum  (number)    row index of cell to add validation error for
// @param  fieldName (number | string)    col index or field name of cell to add validation error for
// @visibility external
// @see listGrid.setFieldError()
//<
clearFieldError : function (rowNum, fieldName, dontDisplay) {
    return this.setFieldError(rowNum, fieldName, null, dontDisplay);
},


//> @method listGrid.clearRowErrors()
//  Clear any stored validation errors for some row
//  @group  validation
//  @param  rowNum  (number)    index of row to clear validation error for
// @visibility external
// @see listGrid.setRowErrors()
//<
clearRowErrors : function (rowNum, dontDisplay) {
    this.setRowErrors(rowNum, null, dontDisplay);
},



// Showing Validation Errors
// ---------------------------------------------------------------------------------------

//>    @method listGrid.showErrors() ([A])
// Update the specified row to display the current set of validation errors.<br>
// If +link{listGrid.stopOnErrors} is true and the row has errors, this method will also
// call display the error messagese to the user in a warning dialog, and reset focus to
// the first cell that failed validation.
// @param rowNum (number) row to be updated
// @param [fields] (array) Array of fieldNames for which errors should be displayed or cleared.
//                      If not passed defaults to displaying errors for every field in the row.
// @group gridValidation
// @visibility internal
//<
// Notes:
// - This is a potential override point - may want to make this external
// - The fields parameter is for more than just efficiency - it will also modify which
//   errors get displayed to the user via displayRowErrorMessages()
// - falls through to displayRowErrorMessages() to actually show the error messages
//   to the user in a warn dialog if stopOnErrors is true. Not clear that there's much point
//   to exposing this as if a developer wants to perform custom validation display this method
//   would probably be the more natural override point.
showErrors : function (rowNum, fields) {

    if (fields == null) fields = this.getFields().getProperty(this.fieldIdProperty);

    var errors = this.getRowErrors(rowNum);
    if (errors == null) errors = {};

    var firstFieldName;
    for (var i = 0; i < fields.length; i++) {
        var fieldName = fields[i];
        // Update the cell to display error styling and icon (or clear errorIcon and styling)
        this.showCellErrors(rowNum, fieldName);

        if (firstFieldName == null && errors[fieldName]) firstFieldName = fieldName;
    }

    // Split the method to convert the errors into a user-visible string into a separate
    // method for localization
    var errorMessageString = this.assembleErrorMessage(errors, fields);

    // Call a simple (overrideable) method to actually inform the user of the validation errors.

    if (errorMessageString) {
        this.displayRowErrorMessages(errorMessageString);
    }

    // If we're not saving in the background, put focus back into the first cell that failed
    // the edit (only necessary if the validation failed)
    if (this.stopOnErrors && firstFieldName != null) {
        var firstColNum = this.getColNum(firstFieldName);

        if (firstColNum != -1) {
            this.showInlineEditor(rowNum, firstColNum);
            return;
        }
        for (var fieldName in errors) {
            if (this.fieldIsVisible(fieldName) && errors[fieldName]) {
                this.showInlineEditor(rowNum, this.getColNum(fieldName));
            }
        }
    }
},

// showCellErrors()
// Helper method to update a cell in the body to display errors (or clear errors if appropraite)
showCellErrors : function (rowNum, colNum) {

    // Accept the 2nd param as a colNum or fieldName
    var fieldName;
    if (isc.isA.Number(colNum)) {
        this.getEditorName(rowNum, colNum);
    } else {
        fieldName = colNum;
        colNum = this.getColNum(fieldName);
    }
    // If passed a non-visible field, just bail.
    if (colNum == -1) return;

    if (this.showErrorIcons) {
        // refresh cell to rewrite the HTML to show (or hide) the icon.
        this.refreshCell(rowNum,colNum, null, true);
    } else {
        this.body._updateCellStyle(null, rowNum, colNum)
    }
},


//> @method listGrid.assembleErrorMessage()
// This method is called when validation errors occur, and assembles the set of errors into
// a string to be displayed to the user. Overridable for localization.
// @param errors (object) Object containing <code>fieldName:error</code> mappings. Note that the
//              <code>error</code> object for each field may be a simple error message string,
//              a pointer to the validator that failed (so the errorMessage is available as
//              the <code>errorMessage</code> attribute of that object, or
//              an array of validator type objects (for multiple validaiton failures).
// @return (string) By default returns a string in the following format:<pre>
//                  Validation errors occurred:
//                  Field '[fieldTitle]':
//                  * [errorMessage]
//                  * [errorMessage]
//                  </pre>
// @visibility internal
// @group editing, i18nMessages
//<
assembleErrorMessage : function (errors, fields) {

    // Iterate through the set of errors, assembling an
    // error message string to show the user for the row.
    // Note the errors object is expected to be in the format:
    //   {fieldName:["error message 1", "errorMessage 2", ...],
    //    fieldName2:["error message 1", ...], ... }
    var errorMessageFound = false,
        errorMessageString = "Validation Errors occurred:\r\n";

    for (var fieldName in errors) {
        if (fields && !fields.contains(errors)) continue;

        // Assemble the errors together into a string to ultimately be passed to
        // 'displayRowErrorMessages'
        var errorMessage = this._createFieldErrorString(fieldName, errors[fieldName]);

        if (errorMessage != null) {
            errorMessageFound = true;
            errorMessageString += errorMessage;
        }

    }
    return (errorMessageFound ? errorMessageString : null);
},


// Internal helper for assembling all the error messages for a row into a single string
_createFieldErrorString : function (fieldName, errors) {
    // Assemble the errors together into a string to ultimately be passed to
    // 'displayRowErrorMessages'
    var errorMessageString, fieldErrorMessage,
        field = this.getField(fieldName),
        fieldTitle = field && field.title ? field.title : fieldName;

    // Note that we always normalize the format of stored errorMessages to
    // an array of strings per field
    for (var i =0; i < errors.length; i++) {
        var errorMessage = errors[i];
        if (errorMessage != null) {
            if (fieldErrorMessage == null) {
                fieldErrorMessage = "<br>Field '" + fieldTitle + "':";
            }
            fieldErrorMessage += "<br>* " + errorMessage;
        }
    }
    return fieldErrorMessage;
},

//>    @method listGrid.displayRowErrorMessages()
//  A simple method called from 'showErrors' to display errorMessages from validation
//  on some row.
//  Takes a single errorMessage parameter - default implementation alerts this string if
//  this.stopOnErrors is true.<br>
//  Notes:<br>
//  Override this method to display error messages in some other way.<br>
//  For a more sophisticated validation error handling, the 'showErrors()' method can be
//  overridden instead.<br>
//    @group    editing
//
//  @param  rowNum  (number) Index of the record on which the validation errors occurred.
//  @param  errors  (object) Object containing a map of field names to arrays of validation errors.
//  @see    listGrid.validateCellValue()
//  @see    listGrid.showErrors()
//  @see listGrid.assembleErrorMessage()
//<
displayRowErrorMessages : function (errorString) {

    if (this.stopOnErrors) isc.warn(errorString, "var LG=" + this.getID() +
                                            ";if(LG._editRowForm)LG._editRowForm.focus()");
    // -- in this case we might want to stick the error into a hover, etc.
    // - but hold off on that for now
},

// Removal of data
// ---------------------------------------------------------------------------------------



//> @method listGrid.removeData()
// Remove a record from this ListGrid.
// <P>
// If this grid is bound to a DataSource, it will perform a DataSource "remove" operation to
// remove records from this component's DataSource.
// <P>
// Otherwise the data will be removed from the grid's +link{listGrid.data,data} object.
// <P>
// To make changes to the local data object even when a DataSource is present, use
// +link{listGrid.saveLocally}.
//
// @param data (Record) listGrid record, or primary key values of record to delete.
// @param [callback] (DSCallback)  method to call on operation completion.
//   Note that if this is method does not trigger a dataSource remove operation, the callback
//   will still be fired when the data has been removed, but the <code>dsResponse</code> parameter
//   will be null.
// @param [requestProperties] (DSRequest Properties)   additional properties to set on any DSRequest
//                                          that will be issued
//
// @group dataBoundComponentMethods
// @visibility external
//<
removeData : function (recordKeys, callback, requestProperties, fromUserAction) {

    // Hang the original callback onto the request internalClientContext - we'll fire it from
    // 'removeDataComplete()'.


    // copy to avoid problems with re-using the same object
    requestProperties = isc.addProperties({}, requestProperties);


    requestProperties.internalClientContext = {
        removeDataCallback: callback
    };

    var editInfo = {
        fromUserAction:fromUserAction,
        // Note: rowNum / colNum are basically available for callbacks
        rowNum:this.findRowNum(recordKeys),
        colNum:null,
        values:recordKeys,
        editCompletionEvent:isc.ListGrid.PROGRAMMATIC
    };
    requestProperties.internalClientContext.editInfo = editInfo;

    if (this.getDataSource() != null && !this.shouldSaveLocally()) {

        return this.Super("removeData",
                [recordKeys, {target:this, methodName:"removeDataComplete"}, requestProperties],
                arguments);
    }
    if (this.data) {
        var editValuesID = this.getEditValuesID(recordKeys);
        if (editValuesID != null) this.discardEdits(editValuesID);
        if (isc.ResultSet && isc.isA.ResultSet(this.data) && this.shouldSaveLocally()) {
            if (this.data.allRows != null) {
                this.data.allRows.remove(recordKeys);
                this.data.filterLocalData();
            } else {
                this.logWarn("Unable to remove data from resultSet - allRows is not set");
            }
        } else {
            var origData = this.getOriginalData();
            origData.remove(recordKeys);
        }
        this.updateFieldWidthsForAutoFitValue("removeData");
        this.regroup();
    }


    this.fireCallback({target:this, methodName:"removeDataComplete"},
                    "dsResponse,data,dsRequest",
                     [null,recordKeys,requestProperties]);
},

removeDataComplete : function (dsResponse, data, dsRequest) {

    // We set up dsRequest.internalClientContext.editInfo even if this was client-only.
    var context = dsRequest.internalClientContext,
        editInfo = context.editInfo,
        fromUserAction = editInfo.fromUserAction,
        rowNum = editInfo.rowNum,
        colNum = editInfo.colNum,
        values = editInfo.oldValues || editInfo.values,
        editCompletionEvent = editInfo.editCompletionEvent;

    var success = dsResponse == null || dsResponse.status >= 0;
    if (success) {
        // if we have edit vals for the removed data, clear them up.
        if (!isc.isAn.Array(data)) data = [data];
        for (var i = 0; i < data.length; i++) {
            var editValsID = this.getEditValuesID(data[i]);
            if (editValsID != null) {
                this.discardEdits(editValsID);
            }
        }

        // Fire 'editComplete' notification since a remove is a specialized edit.
        // editComplete takes old and new values -- for removal of course this is just
        // the record being removed -- really just the primary key.
        var newValues = editInfo.values || editInfo.oldValues,

            rowNum = editInfo.rowNum;

        if (fromUserAction && this.convertToMethod("editComplete")) {
            this.editComplete(rowNum, colNum, values, values, editCompletionEvent, dsResponse);
        }
    } else {
//        this.logWarn("removeData failed on server:" + data);
        // fire editFailed notification
        if (fromUserAction && this.convertToMethod("editFailed")) {
            this.editFailed(rowNum, colNum, values, values,
                            editCompletionEvent, dsResponse, dsRequest);
        }
    }

    // Note that if the server failed, we still need to fire callbacks - expected in
    // 'showRemoveCompleteAnimation' at least.
    if (context.removeDataCallback) {
        this.fireCallback(context.removeDataCallback,
                        "dsResponse,data,dsRequest", [dsResponse,data,dsRequest]);
    }
},

//> @method listGrid.removeRecordClick()
// Method fired when the user clicks the "remove" icon if +link{canRemoveRecords} is true.
// Default behavior will remove the record from the data set, or if we're
// +link{deferRemoval,deferring removal} mark record as removed [or for records already
// marked as removed, clear this removed marker].
// <P>
// If +link{listGrid.warnOnRemoval} is set, this method will also show a warning dialog to users
// allowing them to cancel the removal.
// <P>
// This method may be called directly to cause a record to be removed or marked for removal
// as if the user had hit the "remove" icon.
// <P>
// <smartgwt>To be notified when a "remove" click occurs, developers
// should add a
// {@link com.smartgwt.client.widgets.grid.ListGrid#addRemoveRecordClickHandler * RemoveRecordClickHandler}.
// </smartgwt>
// <smartclient>May be overridden to perform custom logic on remove click.</smartclient>
// @param rowNum (int) Row on which the icon was clicked
// @visibility external
//<

removeRecordClick : function (rowNum,colNum) {
    var record = this.getRecord(rowNum);
    // if there's no record, nothing to do

    // If this record is explicitly marked to not allow removal ignore the click
    if (this.recordCanRemoveProperty && record != null &&
        record[this.recordCanRemoveProperty] == false)
    {
        return;
    }

    // SGWT event handler entry point.

    if (this.onRemoveRecordClick(rowNum) == false) return;
    var shouldWarn = this.warnOnRemoval,
        shouldDefer = this.shouldDeferRemoval();
    // don't warn on unremoval.
    if (shouldWarn && shouldDefer && this.recordMarkedAsRemoved(rowNum)) shouldWarn = false;

    if (shouldWarn) {
        isc.ask(
            this.warnOnRemovalMessage,
            this.getID() + ".completeRemoveRecordClick(value, " + rowNum + "," + colNum + ")"
        );
    } else {
        this.completeRemoveRecordClick(true, rowNum, colNum);
    }
},

//> @method listGrid.onRemoveRecordClick ()
// Handler fired when the user clicks the "remove" icon if +link{canRemoveRecords} is true.
// Default behavior will remove the record from the data set, or if we're
// +link{deferRemoval,deferring removal} mark the record as removed [or for records already
// marked as removed, clear this removed marker].
// <P>
// If +link{listGrid.warnOnRemoval} is set, this method will also show a warning dialog to users
// allowing them to cancel the removal.
// <P>
// This event may be cancelled to suppress the default behavior.
//
// @param rowNum (int) Row on which the icon was clicked
// @return (boolean) return false to cancel the event
// @visibility sgwt
//<

onRemoveRecordClick : function (rowNum) {
    return true;
},

completeRemoveRecordClick : function (shouldRemove, rowNum, colNum) {
    // user hit "No" on the warn dialog - just bail.
    if (!shouldRemove) return;

    if (this.shouldDeferRemoval()) {
        if (this.recordMarkedAsRemoved(rowNum)) {
            this.unmarkRecordRemoved(rowNum);
        } else {
            this.markRecordRemoved(rowNum);
        }
    } else {
        var editRows = this.getAllEditRows(),
            isEditing = editRows.contains(rowNum);

        if (isEditing) this.discardEdits(rowNum, colNum);

        var record = this.getCellRecord(rowNum,colNum);
        if (record == null) return;
        // use delayCall to actually remove the record in a separate thread.
        // required since we can't redraw immediately in response to a mouseDown
        this.delayCall("removeRecord", [rowNum, record]);
    }
},

// Remove a record from this ListGrid.
// This is called from a click on the remove field.

removeRecord : function (rowNum, record) {
    if (record == null) record = this.data.get(rowNum);
    else rowNum = (this.data ? this.data.indexOf(record) : null)

    // Only support removing the record if it's actually showing up in this grid!
    // (This not removing nodes in closed parent folders for example)
    if (record == null || rowNum == null || rowNum == -1 || !this.data) return;

    if (isc.Offline && isc.Offline.isOffline() && this.dataSource && !this.dataSource.clientOnly) {
        isc.warn(this.offlineSaveMessage);
        return;
    }

    // animating record removal
    // If we're animating the record removal, we want to remove the data before starting the
    // animation. This ensures the record gets successfully cleared (not guaranteed in the case
    // of a server operation).
    // Set up the row animation before removing the data, then kick off the animation when we know
    // row removal was successful.

    var animateRemoveRecord = this.animateRemoveRecord && this.isDrawn() && this.isVisible() &&
                              this.body &&
                              rowNum >= this.body._firstDrawnRow &&
                              rowNum <= this.body._lastDrawnRow;

    // Don't attempt to animate remove rows with embedded components. Since the components
    // won't be clipped by the animation it'll look funky.
    if (this._hasEmbeddedComponents(record)) {
        animateRemoveRecord = false;
    }

    if (animateRemoveRecord) {
        this._suppressRedrawOnDataChanged = true;
        var delta = 1;
        if (isc.isA.Tree(this.data) && this.data.isFolder(record) && this.data.isOpen(record)) {
            var children = this.data.getChildren(record);
            if (children && (!isc.isA.ResultSet(children) || children.lengthIsKnown())) {
                delta += children.getLength();
            }
        }

        this._removeRowDelta = delta;
        // perform the setup for the animated hide while our data is still present in our dataSet
        this.body._initializeShowHideRow(false, rowNum, rowNum+delta);
    }

    var ds = this.getDataSource(),
        removeLocally = (ds == null || this.shouldSaveLocally());
    if (!removeLocally) {
        if (!ds.getPrimaryKeyField()) {
            this.logWarn("DataSource:"+ ds +
                        " has no primary key field - unable to remove records");
            return;
        }
    }
    var callback;
    if (animateRemoveRecord) {
        callback = this.getID() + ".showRemoveCompleteAnimation(" + rowNum + ", data,dsResponse);";
    }
    this.removeData(record, callback, {showPrompt:false}, true);
},

// Callback to animate a removed record out of view. Set up by 'removeRecord()'
showRemoveCompleteAnimation : function (rowNum, record, response) {
    // if this was a server-based removal, check for errors
    if (response && response.status < 0) {
        this.logWarn("Removal of record failed");
        record = null;
    }

    if (!record) {
        delete this._suppressRedrawOnDataChanged;

        if (this.body) {
            // clear up the properties set up by _initializeShowHideRow
            delete this.body._animatedShowStartRow;
            delete this.body._animatedShowEndRow;
            delete this.body._animatedShowRowHeight;
            delete this.body._animatedShowCallback;
        }
        this.markForRedraw();
    } else {
        this.startRowAnimation(false, rowNum, rowNum+this._removeRowDelta,
                                {target:this, methodName:"_removeDataAnimationComplete"},
                                this.animateRemoveSpeed, this.animateRemoveTime);
        delete this._removeRowDelta;
    }
},

_removeDataAnimationComplete : function () {
    delete this._suppressRedrawOnDataChanged;
    this.redraw();
},


// Row Animation
// ---------------------------------------------------------------------------------------

//>Animation
// Animated showing and hiding of rows
//> @method listGrid.startRowAnimation()
// @include gridRenderer.startRowAnimation()
//<
startRowAnimation : function (show, startRow, endRow, callback, speed,
                               duration, effect, slideIn, delayed)
{
    if (!this.body) return;

    // force an immediate finish of any currently running row animation.
    // will no op in most cases
    this.finishRowAnimation();

    this._rowAnimationCompleteCallback = callback;

    // if we have a frozen body too, animate there, but avoid firing the callback twice when
    // both animations complete
    if (this.frozenBody) {
        this.frozenBody.startRowAnimation(show, startRow, endRow,
                                          {target:this, methodName:"rowAnimationComplete",
                                           argNames:"body,hasFrozenBody",
                                           args:[this.frozenBody, true]},
                                          speed,
                                          duration, effect, slideIn, true, delayed);
    }
    this.body.startRowAnimation(show, startRow, endRow,
                                {target:this, methodName:"rowAnimationComplete",
                                 argNames:"body,hasFrozenBody",
                                 args:[this.body, (this.frozenBody != null)]},
                                speed,
                                duration, effect, slideIn, true, delayed);
},

// rowAnimationComplete callback - fires the callback passed to startRowAnimation.
// Will be called twice if we have a frozenBody as well as a liquid body - in this case
// only fire the callback we were passed when both bodies have finished animating
rowAnimationComplete : function (body, hasFrozenBody) {
    var fireCallback = !hasFrozenBody || this._frozenBodyAnimationComplete;

    if (!fireCallback) {
        // just set a flag so the next time this method is run (with the other body), we'll
        // fire our callback.
        this._frozenBodyAnimationComplete = true;
    } else {
        delete this._frozenBodyAnimationComplete;

        var callback = this._rowAnimationCompleteCallback;
        delete this._rowAnimationCompleteCallback;
        if (callback != null) this.fireCallback(callback);
    }
},

finishRowAnimation : function () {
    if (this.body) this.body.finishRowAnimation();
    if (this.frozenBody) this.frozenBody.finishRowAnimation();
},

//> @method listGrid.animateRowHeight()
// @include gridRenderer.animateRowHeight()
//<
animateRowHeight : function (rowNum, newHeight, callback, speed, duration, effect, slideIn) {
    if (!this.body) return;
    return this.body.animateRowHeight(rowNum, newHeight, callback, speed, duration, effect, slideIn, true);
},
//<Animation

//> @method listGrid.setCellHeight()
// Set the cellHeight for the listGrid as a whole. To set the height of individual rows use
// +link{ListGrid.setRowHeight()} instead.
//<
setCellHeight : function (newHeight) {
    var oldHeight = this.cellHeight;
    this.cellHeight = newHeight;
    if (this.body) this.body.cellHeight = newHeight;
    if (oldHeight != newHeight && this.isDrawn()) {
        this.body.markForRedraw("Cell height changed");
        if (this.frozenBody) this.frozenBody.markForRedraw("Cell height changed");
    }

},


//> @method listGrid.setRowHeight()
// @include gridRenderer.setRowHeight()
//<
setRowHeight : function (rowNum, newHeight) {
    if (!this.body) return;
    // keep frozen / unfrozen bodies in synch
    if (this.frozenBody != null) this.frozenBody.setRowHeight(rowNum, newHeight);

    return this.body.setRowHeight(rowNum, newHeight);
},


// Body Drag&Drop (selection, reorder records, drag records out, drop records in)
// --------------------------------------------------------------------------------------------

//> @method listGrid.setDragTracker()
// @include dataBoundComponent.setDragTracker()
// @visibility external
//<

//> @method listGrid.getDragTrackerProperties()
// @include dataBoundComponent.getDragTrackerProperties()
//<

//> @attr listGrid.dragTrackerStyle
// @include dataBoundComponent.dragTrackerStyle
//<


//> @method listGrid.getDragTrackerIcon()
// Return an icon to display as a drag tracker when the user drags some record.<br>
// Default implementation:
// If +link{listGridField.valueIcons} is specified for the title field of this grid
// (see +link{listGrid.getTitleField()}), the appropriate value icon will be displayed.
// If no appropriate valueIcon can be found, the icon will be derived from
// +link{ListGrid.trackerImage}.<br>
// If multiple records are selected, only the first record is examined for valueIcons.
// <p>
// Note: Only called if +link{listGrid.dragTrackerMode} is set to <code>"icon"</code>.
// @param records (Array of ListGridRecord) Records being dragged
// @return (string) Image URL of icon to display
// @group dragTracker
// @visibility external
//<
getDragTrackerIcon : function (records) {
    // we don't expect this method to fire if no records are selected, but if so, just show
    // the generic drag tracker
    var record = records ? records[0] : null,
        icon;
    if (record) {
        var titleFieldName = this.getTitleField();
        icon = this.getValueIcon(this.getField(titleFieldName), record[titleFieldName], record);
    }
    if (icon == null) icon = this.trackerImage;
    return icon;
},

//> @method listGrid.getDragTrackerTitle()
// Return "title" HTML to display as a drag tracker when the user drags some record.<br>
// Default implementation will display the cell value for the title field (see
// +link{listGrid.getTitleField()}) for the record(s) being dragged (including any
// icons / custom formatting / styling, etc).
// <p>
// Note: Only called if +link{listGrid.dragTrackerMode} is set to <code>"title"</code>.
// @param record (ListGridRecord) First selected record being dragged
// @param rowNum (number) row index of first record being dragged
// @return (string) Title for the row. Default implementation looks at the value of the
//                  title-field cell for the row.
// @group dragTracker
// @visibility external
//<
getDragTrackerTitle : function (record, rowNum) {
    var titleField = this.getTitleField(),
        formattedValue = this.getFormattedValue(record, titleField, record ? record[titleField] : undefined);
    return "<nobr>" + formattedValue + "</nobr>";
},

// We have our own drag/drop handling and don't want the Layout level handling
// (showing drop line, allowing user to drop new members in the layout, etc)
_suppressLayoutDrag:true,

//>    @method    listGrid.dragStart()    (A)
// drag start event
// @group events, dragging
// @return (boolean) false non-existent record or !canDrag; true otherwise
//<
dragStart : function () {
    // if dragging means drag-selection, we do nothing until dragMove
    if (this.canDragSelect) return true;

    // get the selection
    var selection = this.selection.getSelection();
    // if the selection is empty or anything in the selection is canDrag == false,
    //    cancel the drag by returning false
    if (!selection || selection.length == 0) return false;
    for (var i = 0; i < selection.length; i++) {
        if (selection[i].canDrag == false) return false;
    }

    // Otherwise store a snapshot of the current selection on the ListGrid object so that we
    // are able to transfer the correct record(s) when the user drops them, in the unlikely
    // event that they change the ListGrid selection (with the keyboard) during the drag
    // operation
    this._selectionAtDragStart = selection;

    return true;
},

dragMove : function () {
    var dropTarget = isc.EH.dropTarget;
    // if you're not allow to drag records out, cancel dragging over anything that isn't this
    // grid
    if (!this.canDragRecordsOut && dropTarget != null &&
        dropTarget != this && !this.contains(dropTarget))
    {
        return false;
    }
},

//> @method listGrid.dropMove() (A)
// Handle a dropMove event.  This will show the drag line in the appropriate position.
// @group events, dragging
// @return (boolean) true if the list can't reorder
//<
dropMove : function () {
    if (this.willAcceptDrop()) {
        // If the list can be reordered, show the dragLine
        if (this.canReorderRecords) this.showDragLineForRecord();

        // In case it was previously set
        this.body.clearNoDropIndicator();

        // We've claimed the drop, so don't bubble
        return false;
    } else {
        // If this.willAcceptDrop() returns false, check whether this drop
        // would be accepted for *some* rows.
        if (this._mightAcceptDrop()) {
            // The drop would have been accepted for some rows, but not this one.
            // In this case, show the no-drop cursor.
            this.body.setNoDropIndicator();

            // But still show the drag-line if reordering is enabled, so the user
            // can see where they would be dropping -- so it's clear what's
            // disallowed.
            if (this.canReorderRecords) this.showDragLineForRecord();

            // We've conclusively denied the drop, so don't bubble
            return false;
        } else {

            return false;
        }
    }
},

dropOver : function () {
    // Execute the dropMove() logic for dropOver, so that we stop bubbling
    // in the appropriate cases, so that our ancestors don't draw dropLines
    // inappropriately.
    return this.dropMove();
},

//>    @method    listGrid.dropOut()    (A)
// handle a dragOut event
// @group events, dragging
//<
dropOut : function () {
    this.body.clearNoDropIndicator();
    this.hideDragLine();
},

//> @method listGrid.dragStop()    (A)
// handle a dragStop event
// @group events, dragging
//<
dragStop : function () {
    this.body.clearNoDropIndicator();
    this.hideDragLine();
    this._selectionAtDragStart = null
},

//>    @method    listGrid.willAcceptDrop()    (A)
//
// This method overrides +link{Canvas.willAcceptDrop()} and works as follows:<br>
// <ul>
// <li>If +link{Canvas.willAcceptDrop()} (the superclass definition) returns false, this
//     method always returns false.  This allows +link{canvas.dragType} and
//     +link{canvas.dropTypes} to be used to configure eligibility for drop.  By default,
//     a ListGrid has no dropTypes configured and so this check will not prevent a drop.</li>
// <li>If this is a self-drop, that is, the user is dragging a record within this list, this is
//     an attempted drag-reorder.  If +link{ListGrid.canReorderRecords} is
//     false, this method returns false.</li>
// <li>If the +link{EventHandler.getDragTarget(),dragTarget} is another widget, if
//     +link{ListGrid.canAcceptDroppedRecords} is false this method returns false.</li>
// <li>If a call to +link{ListGrid.getDragData()} on the <code>dragTarget</code> fails to return
//     an record object or an array of records, this method returns false.</li>
// <li>If the drop target record is disabled or has +link{ListGridRecord.canAcceptDrop}
//     set to false, return false.</li>
// </ul>
// Note that this method may be called repeatedly during a drag-drop interaction to update the
// UI and notify the user as to when they may validly drop data.
//
// @group    events, dragging
// @return (boolean) true if this component will accept a drop of the dragData
// @see ListGridRecord.canAcceptDrop
// @see ListGrid.getDragData()
//
// @visibility external
//<
willAcceptDrop : function () {
    return this._mightAcceptDrop() && this._willAcceptDropForRecord();
},

// Divide up willAcceptDrop into _mightAcceptDrop and _willAcceptDropForRecord.
// _mightAcceptDrop indicates whether this drop could be accepted for some
// record. _willAcceptDropForRecord indicates whether this drop would be
// accepted for the particular record being hovered over.
//
// This is useful for the UI shown in dropMove. If we might accept the drop
// for some records (but not this one), we show a dropLine and a
// noDropIndicator. Otherwise, we don't show a dropLine.
_mightAcceptDrop : function (a, b, c) {
    // Use superclass implementation to check that there is a valid dragTarget, and
    // that dragType / dropTypes (if set) match up.
    // Note - if we're dragReordering a record within this listGrid, the dragTarget will
    // be this widget.  This is because we set up the listGrid body's dragTarget property to
    // point back to this widget (unless this.canDragSelect is true).
    // If we're doing a drag selection, this method will not be called, as drag select doesn't
    // involve dropping.
    // Therefore if the dragTarget is this widget, we can assume we're reordering records within
    // this widget.
    // If dropTypes are specified on this widget, and a developer wants canReorderRecords
    // behavior, they must specify a dragType on this LV that matches a dropType -- they can
    // just make up a "reorderDragDrop" dragType/dropType.
    var EH = this.ns.EH;
    if (!this.invokeSuper(isc.ListGrid, "willAcceptDrop", a, b, c) && EH.dragTarget != this.body) return false;

    if (EH.dragTarget == this) {
        // Bail if we're attempting to drag records within LV, and we can't reorder
        if (!this._canDragRecordsToSelf()) return false;
    } else {
        // Bail if we're attempting to drag from elsewhere and canAcceptDroppedRecords is false
        if (!this.canAcceptDroppedRecords) return false;
    }

    // if not native dragging and the 'getDragData' for the dragTarget doesn't give us a suitable
    // object (Array or Object), bail
    if (!EH.nativeDragging &&
        !EH.wasNativeDragging &&
        !isc.isAn.Object(EH.dragTarget.getDragData()))
    {
        return false;
    }

    return true;
},

_willAcceptDropForRecord : function () {
    isc._useBoxShortcut = true;
    // get the record being dropped on
    var rowNum = this.getEventRecordNum(),
        colNum = this.getEventFieldNum();
    var position = this.getRecordDropPosition(rowNum);
    if (rowNum != -2 && position == isc.ListGrid.AFTER) rowNum += 1;
    isc._useBoxShortcut = false;

    // if cursor is below the list, it's ok (may want to drop below last record), unless
    // canDropInEmptyArea is false
    if (rowNum < -1) {
        if (this.canDropInEmptyArea == false) return false;
        return true;
    }
    // if the cursor is above the list, bail
    if (rowNum == -1) return false;

    // if the record is disabled or can't accept drops, return false to cancel drop
    var record = this.getRecord(rowNum, colNum);
    if (!this.recordIsEnabled(record, rowNum, colNum) ||
        (record != null && record.canAcceptDrop == false)) return false;
    // if we get here, it should be OK!
    return true;
},

//> @method listGrid._canDragRecordsToSelf() (AR)
// Can we drag records within this LV
// @group events, dragging
//<
_canDragRecordsToSelf : function () {
    return this.canReorderRecords;
},

//> @method listGrid.transferSelectedData()
// @include dataBoundComponent.transferSelectedData()
//<

//>    @method    listGrid.drop()    (A)
// handle a drop event
// @return (boolean) true if the list can't reorder or dragging did not begin from the list body;
//                   false if disabled, no selection, or otherwise
// @group events, dragging
// @visibility internal
//<
drop : function () {
    // verify that the drop is valid
    if (this.willAcceptDrop() == false) return false;

    var sourceWidget = this.ns.EH.dragTarget;

    var dropIndex = this.getDropIndex();

    var dropRecords = sourceWidget.cloneDragData();

    var targetRecord = (dropIndex != -2 && this.getTotalRows()>dropIndex ? this.data.get(dropIndex) : null);

    // onRecordDrop() - fires before recordDrop() and gives developers an opportunity to cancel
    // the drop event

    var dropPosition = this.getRecordDropPosition(this.getEventRecordNum());
    if (this.onRecordDrop != null &&
        (this.onRecordDrop(dropRecords,targetRecord,dropIndex,dropPosition,sourceWidget) == false))
    {
        return false;
    }
    return this.recordDrop(dropRecords, targetRecord, dropIndex, sourceWidget);
},


//> @method listGrid.recordDrop()
// Process a drop of one or more records on a ListGrid record.
// <P>
// This method can be overridden to provide custom drop behaviors, and is a more appropriate
// override point than the lower level +link{Canvas.drop()} handler.
// <P>
// If this is a self-drop, records are simply reordered.
// <P>
// For a drop from another widget, +link{ListGrid.transferDragData()} is called, which
// depending on the +link{ListGrid.dragDataAction,dragDataAction} specified on the source
// widget, may either remove the source records from the original list
// (<code>dragDataAction:"move"</code>) or just provide a copy to this list
// (<code>dragDataAction:"copy"</code>).
// <P>
// If this grid is databound, the new records will be added to the dataset by calling
// +link{dataSource.addData()}.  Further, if the new records were dragged from another
// databound component, and +link{DataBoundComponent.addDropValues,addDropValues}
// is true, +link{DataBoundComponent.getDropValues,getDropValues} will be called for every item
// being dropped.
// <P>
// For multi-record drops, Queuing is automatically used to combine all DSRequests into a
// single HTTP Request (see QuickStart Guide, Server Framework chapter).  This allows the
// server to persist all changes caused by the drop in a single transaction (and this is
// automatically done when using the built-in server DataSources with Power Edition and
// above).
// <P>
// Note that reordering records has no effect on a databound grid.
// <P>
// The newly dropped data is then selected automatically.
// <P>
// If these default persistence behaviors are undesirable, return false to cancel them, then
// and implement your own behavior, typically by using grid.updateData() or addData() to add
// new records.
// <p><b>NOTE:</b> the records you receive in this event are the actual Records from the source
// component.  Use +link{DataSource.copyRecords()} to create a copy before modifying the records
// or using them with updateData() or addData().
// <P>
// NOTE: for a drop beyond the last visible record of a ListGrid, <code>targetRecord</code>
// will be null and the <code>index</code> will be one higher than the last record.  This
// includes a drop into an empty ListGrid, where <code>index</code> will be 0.
//
// @param dropRecords (Array[] of ListGridRecord) records being dropped
// @param targetRecord (ListGridRecord) record being dropped on.  May be null
// @param index (int) index of record being dropped on
// @param sourceWidget (Canvas) widget where dragging began
//
// @visibility external
//<
recordDrop : function (dropRecords, targetRecord, index, sourceWidget) {
    this.transferRecords(dropRecords, targetRecord, (this.canReorderRecords ? index : null),
                         sourceWidget);

    // signature "records,rowNum,viewer,sourceWidget"
    if (this.recordsDropped) this.recordsDropped(dropRecords, index, this, sourceWidget);

    // NOTE: we don't need to redraw since we're watching the data and it will change!

    // return false to cancel additional event processing
    return false;
},

//>    @method    ListGrid.transferDragData()    (A)
// @include dataBoundComponent.transferDragData()
// @visibility external
//<

//>    @method    listGrid.getDragData()
// @include dataBoundComponent.getDragData()
//<

// --------------------------------------------------------------------------------------------

viewRow : function (rowNum) {
    this.getDataSource().viewFile(this.getRecord(rowNum));
},

downloadRow : function (rowNum) {
    this.getDataSource().downloadFile(this.getRecord(rowNum));
},

viewCell : function (rowNum, fieldName) {
    this.getDataSource().viewFile(this.getRecord(rowNum), fieldName);
},

downloadCell : function (rowNum, fieldName) {
    this.getDataSource().downloadFile(this.getRecord(rowNum), fieldName);
},

//>    @method    listGrid.autoSizeColumn()    (A)
//        @group    sizing, positioning
//
//      Can only be called after draw()
//
//      Resize this column to the size it needs to be in order to accommodate it's contents.
//<
autoSizeColumn : function (columnNum) {
    // determine the auto-size
    var body = this.body,
        frozen = this.fieldIsFrozen(columnNum),
        localFieldNum = this.getLocalFieldNum(columnNum);
    if (frozen && this.frozenBody) {
        body = this.frozenBody;
    }
    if (!body) return;

    var columnWidth = body.getColumnAutoSize(localFieldNum);
    if (this.includeGridSummaryInAutoFitWidth && this.showGridSummary && this.summaryRow != null) {

        var summaryBody = frozen ? this.summaryRow.frozenBody : this.summaryRow.body;
        if (summaryBody != null) {
            var summaryWidth = summaryBody.getColumnAutoSize(localFieldNum);
            if (summaryWidth > columnWidth) columnWidth = summaryWidth;
        }
    }

    // resize the field
    if (columnWidth != null) this.resizeField(columnNum, columnWidth);
},

//>    @method    listGrid.getRecord()    (A)
// Return the pointer to a particular record by record number.
// Synonym for +link{ListGrid.getCellRecord()}.
//
// @see ListGrid.getCellRecord()
// @see ListGrid.getEditedRecord()
// @param recordNum     (number) row index of record to return.
// @return (ListGridRecord) Record object for the row.
// @visibility external
//<
getRecord : function (rowNum, colNum) {
    return this.getCellRecord(rowNum, colNum);
},

// used by DBC/CellSelection to determine whether to return cellRecords (CubeGrid) or normal
// distinct records (ListGrid)
cellRecordMode: "row",

//>    @method    listGrid.getCellRecord()    (A)
// Return the pointer to a particular record by record number.<br>
// Notes:<br>
// - If this is a databound grid, and the record for some row has not yet been loaded,
//   returns the +link{resultSet.getLoadingMarker(),loading marker}, and a fetch will be
//   initialized to retrieve the record from the server.<br>
// - If this is a new row in an editable ListGrid, and has not yet been saved, this method will
//   return null.
// @see ListGrid.getRecord()
// @see ListGrid.getEditedRecord()
// @param recordNum     (number) row index of record to return.
// @return (ListGridRecord) Record object for the row.
// @visibility external
//<
getCellRecord : function (rowNum, colNum) {
    if (!isc.isA.Number(rowNum)) {
        this.logWarn("getCellRecord called with bad rowNum: " + this.echo(rowNum)
         //+ this.getStackTrace()
        );
        return;
    }
    // ListGrid assumes one record per row
    // Note - this.getTotalRows() may return a value higher than this.data.length. Don't
    // try to fetch records beyond the end of the dataset
    if (!this.data || 0 > rowNum || rowNum >= this.data.getLength()) return null;

    var record = this.data.get(rowNum);

    if (this._hasUnmatchedEdits) this._testRowEditData(record,rowNum);
    return record;
},


_getCachedCellRecord : function (rowNum, colNum) {
    if (!isc.isA.Number(rowNum)) {
        this.logWarn("getCellRecord called with bad rowNum: " + this.echo(rowNum));
        return;
    }
    if (!this.data || 0 > rowNum ||
        (isc.isA.ResultSet(this.data) && !this.data.lengthIsKnown()) ||
        rowNum >= this.data.getLength())
    {
        return null;
    }
    var record = this.data.getCachedRow(rowNum);
    if (this._hasUnmatchedEdits) this._testRowEditData(record, rowNum);
    return record;
},

// Internal method to map edit data for some record to that record's rowNum.

_testRowEditData : function (record, rowNum) {
    if (record == null || record[this.editValuesTestedProperty]) return;

    if (this._editRowMap == null) this._editRowMap = {};
    var noUnmatchedEdits = true;

    for (var i in this._editSessions) {
        var vals = this._editSessions[i];
        if (vals != null && vals._rowNum == null) {
            if (this.comparePrimaryKeys(record, vals._primaryKeys)) {
                vals._rowNum = rowNum;
                this._editRowMap[rowNum] = i;
            } else {
                // We use 'noUnmatchedEdits' to determine whether we have a rowNum for every
                // record in this._editValuesArray.
                // This is used to update the '_hasUnmatchedEdits' flag allowing us to avoid this
                // method entirely when we have matched every set of edit values to a rowNum.
                // Note - if this record is the last unmatched edit, and it matches the record,
                // we  leave this flag set to true, and avoid firing this method again.
                noUnmatchedEdits = false;
            }
        }
    }

    if (noUnmatchedEdits) delete this._hasUnmatchedEdits;
    record[this.editValuesTestedProperty] = true;
},

//>    @method    listGrid.comparePrimaryKeys()    ([AI])
//  Compare a record and a primaryKeys object, returning true if the primary keys match the
//  record.
//  @group  data
//  @param  record  (object)    Record to compare to primary keys object
//  @param  keys    (object)    set of primary keys / values
//  @return (boolean)   True if the primary keys match the record.
//<
comparePrimaryKeys : function (record, keys) {
    var rKeys = this.getPrimaryKeys(record);

    // For local data sets we use a pointer to the record object as primary keys
    if (!this.dataSource || this.shouldSaveLocally()) return (rKeys == keys);

    for (var keyField in rKeys) {
        if (rKeys[keyField] != keys[keyField]) return false;
    }
    return true;

},

// Returns unique primary keys for a record.
// Use 'comparePrimaryKeys()' to compare against some record.

getPrimaryKeys : function (record) {

    // assume a number is a recordIndex
    if (isc.isA.Number(record)) record = this.getRecord(record);

    if (this.shouldSaveLocally()) return record;

    var ds = this.getDataSource(),
        pkArray = ds.getPrimaryKeyFieldNames(),
        keys = {};

    if (!isc.isAn.Array(pkArray)) pkArray = [pkArray];

    for (var i = 0; i < pkArray.length; i++) {
        keys[pkArray[i]] = record[pkArray[i]]
    }
    return keys;
},

//>    @method listGrid.getTotalRows()
// Return the total number of rows in the grid.
// <P>
// Note that, when creating new rows via inline editing, this can be more than the total number
// of rows in the dataset (that is, grid.data.getLength())
//
// @return (int) total number of rows in the grid
// @visibility external
//<
// Override 'getTotalRows' - the total set of rows for the grid are the length of the data, and
// any unsaved edit rows beyond the last record.
getTotalRows : function () {

    if (this.data == null) return 0;
    if (this.data.getLength == null) {
        this.logWarn("GetTotalRows: ListGrid has invalid data object:"+ this.echo(this.data));
        return 0;
    }
    var totalRows = this.data.getLength(),
        lastEditRow = this._getLastEditRow();
    if (lastEditRow != null && lastEditRow+1 > totalRows) totalRows = lastEditRow+1;

    if (this.showNewRecordRow) totalRows += 1;

    return totalRows;
},

// Needed by the cell navigation logic moved down from CubeGrid to ListGrid
getTotalCols : function () { return this.fields.length; },

// Internal method to return the last (highest value) row for which we have edit values.
// Note: returns -1 if we have no edit values for any rows.
_getLastEditRow : function () {

    // Cache the last edit row for performance reasons.  We update this in the methods to
    // set / clear edit values
    if (this._lastEditRow == null) {
        var lastER = -1,
            allEditRows = (this._editRowMap ? isc.getKeys(this._editRowMap) : []);
        for (var i = 0; i < allEditRows.length; i++) {
            // Note the 'parseInt' is required, because the rowNums returned are strings rather
            // than numbers
            var currentER = parseInt(allEditRows[i]);
            if (currentER > lastER) lastER = currentER;
        }
        this._lastEditRow = lastER;
    }

    return this._lastEditRow;
},

//>    @method    listGrid.recordIsEnabled()    (A)
//        @group    selection, appearance
//            return if record recordNum is enabled
//            note: also returns false if the <code>record[this.isSeparatorProperty]</code> is true
//        @param    record            (ListGridRecord)    the record at index recordNum
//        @param    recordNum        (number)    record number to test for enabled
//
//        @return    (boolean)    whether this record is enabled or not
//<
recordIsEnabled : function (record, row, col) {

    if (record == null) return true;
    if (this.recordMarkedAsRemoved(row)) return false;
    return (record[this.recordEnabledProperty] != false && record[this.isSeparatorProperty] != true);
},

cellIsEnabled : function (record, row, col) { return this.recordIsEnabled(record, row, col); },

//>    @method    listGrid.getCellField()    (A)
//      Takes rowNum / colNum as parameters.
//        Return the pointer to the field structure for a cell
//
//        @group    display
//        @param    rowNum  (number)    Row Index of the cell
//      @param  colNum  (number)    Column index of the cell
//
//        @return    (object)    Field description
//<
getCellField : function (rowNum, colNum) {
    return this.getField(colNum);
},

//>    @method    listGrid.getFields()
// Get the array of all <b>currently visible</b> fields for this ListGrid.
// <P>
// This list fields is only valid once the ListGrid has been +link{draw(),drawn} or once
// +link{setFields()} has been called explicitly.  If called earler, only the list of directly
// specified fields will be returned (the Array passed to create()).
// <P>
// This Array should be treated as <b>read-only</b>.  To modify the set of visible fields, use
// +link{showField()}, +link{hideField()} and related APIs.  To update properties of individual
// fields, use +link{setFieldProperties()} or more specific APIs such as
// +link{setFieldTitle()}.
// <P>
// To get the Array of all fields, including fields that are not currently visible or were
// specified implicitly, use +link{getAllFields()}.
//
// @return (Array of ListGridField) Array of all currently visible fields
//<
getFields : function () {
    return this.fields;
},

//> @method listGrid.getAllFields()
// Get the complete array of fields for this ListGrid, including fields that are not currently
// visible or were specified implicitly via +link{listGrid.dataSource}.
// <P>
// This list of fields is only valid once the ListGrid has been +link{canvas.draw(),drawn} or once
// +link{setFields()} has been called explicitly.  If called earlier, only the list of directly
// specified fields will be returned (the Array passed to create()).
// <P>
// This Array should be treated as <b>read-only</b>.  To modify the set of visible fields, use
// +link{showField()}, +link{hideField()} and related APIs.  To update properties of individual
// fields, use +link{setFieldProperties()} or more specific APIs such as
// +link{setFieldTitle()}.
//
// @return (Array of ListGridField) Array of all fields in the ListGrid
// @visibility external
//<
getAllFields : function () {
    return this.completeFields || this.fields;
},

//>    @method    listGrid.getSpecifiedField()    (A)
// Looks up a field object by name or position.
// <p>
// Returns the field from this.completeFields (if available) rather than this.fields,
// allowing you to get a pointer to any field in the completeFields array
//
//        @group    display
//
//        @param    fieldID        (string || number || ListGridField)
//          field number or field.name. If passed a field, it will be returned.
//        @return    (object)    Field description
//<
// XXX This isn't an ideal name - really it's a function to 'getFieldFromCompleteFieldsArray()'.
// We may rename this.completeFields to this.fields, and this.fields to this.visibleFields - when
// this refactoring occurs, this method will become this.getField(), and the current 'getField()'
// method will become this.getVisibleField()
getSpecifiedField : function (fieldID) {
    var fields = this.completeFields;
    if (fields == null) fields = this.fields;
    if (fields == null) return;

    if (isc.isAn.Object(fieldID)) {
        if (fields.contains(fieldID)) return fieldID;
        // Catch the case where we're passed a field that's not present in our
        // fields/completeFields array.


        if (isc.isA.RecordEditor(this) && this.isAFilterEditor()) {
            return this.getSpecifiedField(fieldID[this.fieldIdProperty]);
        }

        return null;

    } else if (isc.isA.String(fieldID)) return fields.find(this.fieldIdProperty, fieldID);
    else return (fields[fieldID]);
},


//>    @method    listGrid.getFieldName()    (A)
// Given a column number or field id, return the field name of a field.
//        @group    display
//        @param    colNum      (number or id)    number or id of the field.
//        @return    (string)    Name of the field.
//      @visibility external
//<
getFieldName : function (fieldNum) {
    var field = this.getField(fieldNum);
    return field ? field[this.fieldIdProperty] : null;
},

//> @method listGrid.getField()
// Given a column number or field name, return the field definition.
// <P>
// When using +link{attr:DataBoundComponent.fields,DataBinding}, the field definition may be
// a mix of information derived from +link{listGrid.fields} and +link{listGrid.dataSource}.
//
//        @param    colNum      (int | ID)    number or id of the field.
//        @return    (ListGridField)    field definition
//      @visibility external
//<

getField : function (id) {
    if (this.fields == null || id == null) return null;

    var field;

    if (this._noNumericFields) {
        field = this.fields[id];
        if (field != null) return field;
    } else {
        // Number: assume index.
        if (isc.isA.Number(id)) return this.fields[id];
    }

    // Object: return unchanged
    if (id._isFieldObject || isc.isAn.Object(id)) return id;

    // String: fieldName / dataPath
    if (isc.isA.String(id)) {
        var isDataPath = id.contains(isc.Canvas._$slash);
        if (!isDataPath) {
            return this.fields.find(this.fieldIdProperty, id);
        } else {
            // The dataPath declared on the field may be full or partial, absolute or
            // relative, and may have a (meaningless) trailing slash as well.  We'll have to
            // look for it in all the various forms it might take.  We'll keep this comparison
            // as sparse as possible - the following compare fields will be undefined unless
            // they are different from the base id (eg, if the base id starts with a slash,
            // "absolute" will remain undefined, because it would otherwise be identical to id)
            var absolute,
                noLeadingSlash,
                noTrailingSlash,
                slashesTrimmed = id.trim(isc.Canvas._$slash),
                complete,
                thisLevelOnly,
                thisLevelPlusTrailing,
                undef;

            if (id.startsWith(isc.Canvas._$slash)) {
                noLeadingSlash = id.substring(1);
                if (id.endsWith(isc.Canvas._$slash)) {
                    noTrailingSlash = id.substring(0, id.length);
                } else {
                    absolute = id + "/";
                }
            } else {
                if (id.endsWith(isc.Canvas._$slash)) {
                    noTrailingSlash = id.substring(0, id.length);
                    absolute = "/" + id;
                } else {
                    absolute = "/" + id + "/";
                }
                complete = "/" + id;
            }
            var lastSlash = slashesTrimmed.lastIndexOf(isc.Canvas._$slash);
            if (lastSlash != -1) {
                thisLevelOnly = slashesTrimmed.substring(lastSlash + 1);
                thisLevelPlusTrailing = thisLevelOnly + "/";
            }

            for (var i = 0; i < this.fields.length; i++) {
                var dataPath = this.fields[i].dataPath;
                if (!dataPath) continue;
                if (dataPath == id ||
                    (noLeadingSlash && dataPath == noLeadingSlash) ||
                    (noTrailingSlash && dataPath == noTrailingSlash) ||
                    (slashesTrimmed && dataPath == slashesTrimmed) ||
                    (complete && dataPath == complete) ||
                    (absolute && dataPath == absolute) ||
                    (thisLevelOnly && dataPath == thisLevelOnly) ||
                    (thisLevelPlusTrailing && dataPath == thisLevelPlusTrailing)
                   )
                {
                    return this.fields[i];
                }
            }
        }
    }

    // otherwise invalid
    return null;
},

//>    @method    listGrid.getFieldNum()    (A)
//        Given a field or field id, return it's index in the fields array
//        @group    display
//        @param    fieldID        (String | number)    field number or field.name
//        @return    (int)    index of the field within this.fields
//      @visibility external
//<
// NOTE: implemented on Canvas

// make 'getColNum' a synonym of getFieldNum() since it's common to refer to 'colNum's
getColNum : function (fieldId) {
    return this.getFieldNum(fieldId);
},


//>    @method    listGrid.getFieldTitle()    (A)
//        Return the title of particular field, specified by number.
//        @group    display
//
//        @param    fieldNum        (number)    number of the field.
//
//        @return    (string)    Field title.
//<
getFieldTitle : function (fieldNum) {

    var field;
    // Allow a field object to be passed in -- useful if the field is currently hidden, so
    // not present in this.fields
    if (isc.isAn.Object(fieldNum)) field = fieldNum;
    else field = this.fields[fieldNum];

    if (!field) return "Unknown field";

    // if the field specifies a "getFieldTitle" method, call that
    if (field.getFieldTitle) {
        // CALLBACK API:  available variables:  "viewer,fieldNum"
        // Convert a string callback to a function
        isc.Func.replaceWithMethod(field, "getFieldTitle", "viewer,fieldNum");

        return field.getFieldTitle(this, fieldNum);
    }

    // otherwise just return the title of the field, or failing that, the field's name
    return field.title || field.name;
},

//>    @method    listGrid.getSummaryTitle()    (A)
// Return the summary title of particular field.  This is the title of
// the field to be used in the show / hide fields context menu. Default implementation will
// use  +link{listGridField.getSummaryTitle()} or +link{ListGridField.summaryTitle} if specified,
// otherwise +link{listGridField.title}.
// @group i18nMessages
// @group display
// @param field (listGridField) field for which we're returning the title
// @return    (string)    Field summary title.
// @visibility external
//<
getSummaryTitle : function (field) {
    var title;
    // If the custom 'summary title' has been specified on the field, use that.
    if (field.getSummaryTitle != null) {
        // CALLBACK API:  available variables:  "viewer,field"
        // Convert a string callback to a function
        isc.Func.replaceWithMethod(field, "getSummaryTitle", "viewer,field");
        title = field.getSummaryTitle(this, field);
    } else if (field.summaryTitle != null) {
        title = field.summaryTitle;
    } else {
        title = this.getFieldTitle(field);
    }

    return title;
},


//>    @method    listGrid.setValueMap()
// Set the +link{listGridField.valueMap, valueMap} for a field.
// See also the +link{ListGrid.setEditorValueMap(), setEditorValueMap()}
// and +link{ListGrid.getEditorValueMap(), getEditorValueMap()} methods which allow further
// customization of the valueMap displayed while the field is in edit mode.
//
// @param fieldID (String | int) Name or index of field to update
// @param map (object) ValueMap for the field
// @visibility external
//<
setValueMap : function (fieldID, map) {

    this.setFieldValueMap(fieldID, map);

    if (this._editorShowing) {
        var fieldName, field;
        var fieldNum = this.getColNum(fieldID);
        // don't crash on hidden fields / bad fieldNames that don't exist
        if (fieldNum != -1 && fieldNum < this.fields.length) {
            var field = this.getField(fieldID),
                fieldName = field[this.fieldIdProperty];
            this._editRowForm.setValueMap(
                fieldName,
                this.getEditorValueMap(field, this.getEditedRecord(this.getEditRow(), fieldNum))
            );
        }
    }
    // If we're showing a summaryRow grid, copy the valueMap across to that so if we do
    // something like show a max, or a mean that has a display value it'll update correctly
    if (this.summaryRow) {
        this.summaryRow.setValueMap(fieldID, map);
    }

    if (this.isDrawn() && this.isVisible()) {
        this._markBodyForRedraw("setValueMap");
    }
},

//> @method listGrid.getDisplayValue()
// Given a field with a specified +link{listGridField.valueMap} or +link{listGridField.displayField}
// this method will return the display value for any underlying data value.
// @param fieldID (string || number || ListGridField) Field or field identifier with valueMap
// @param valueFieldValue (any) Data value for this field
// @return (string) Display value associated with the specified valueFieldValue
// @visibility external
//<
getDisplayValue : function (fieldID, valueFieldValue, record) {
    var field = this.getField(fieldID),
        valueMap = field ? field.valueMap : null;
    var displayValue = this._getDisplayValue(
        field, valueMap, field && field.displayField, valueFieldValue, record);
    if (displayValue != null && !isc.isA.String(displayValue)) displayValue = "" + displayValue;
    return displayValue;
},
_getDisplayValue : function (field, valueMap, displayField, valueFieldValue, record) {
    // If `getDisplayValue()` is overridden by the user then we have to call it.
    if (this.getDisplayValue != isc.ListGrid.getInstanceProperty("getDisplayValue")) {
        return this.getDisplayValue(field, valueFieldValue, record);
    }

    if (field) {
        if (isc.isAn.Object(valueMap) && !isc.isAn.Array(valueMap) &&
            isc.propertyDefined(valueMap, valueFieldValue))
        {
            return valueMap[valueFieldValue];
        } else if (record && displayField) {
            valueFieldValue = record[displayField];
        }
    }

    return valueFieldValue;
},

//>    @method    listGrid.getData()
//        Get the data that is being displayed and observed
//        @return    (object)    The data that is being displayed and observed
//<
getData : function () {
    return this.data;
},

//> @method listGrid.getRecordIndex()
// Get the index of the provided record.
// <P>
// This is essentially the same as calling listGrid.data.indexOf(record), except that
// the currently visible range of records is checked first.  This is important for
// responsiveness in functions that respond to user actions when the user is working near the
// end of a very large dataset (eg 500k records).
//
// @param record (ListGridRecord) the record whose index is to be retrieved
// @return index (int) index of the record, or -1 if not found
// @visibility external
//<
getRecordIndex : function (record) {
    var index = -1;
    if (this.body && this.body.isDrawn()) {
        index = this.data.indexOf(record, this.body._firstDrawnRow, this.body._lastDrawnRow);
    }
    if (index == -1) index = this.data.indexOf(record);
    return index;
},


//> @method listGrid.getRecordCellIndex()
// Get the row and column index of the provided record.
// <P>
// This function is set up with a colNum argument so it can be called on a ListGrid
// and return a meaningful cell object.  When 'selectOnEdit' is ultimately supported
// in CubeGrid, the overriden version of this function there won't use that argument,
// since the record will completely determine the cell to return.
//
// @param record (ListGridRecord) the record whose index is to be retrieved
// @param colNum (number) the cell column position (since record represents entire row)
// @return cell (Array) 2 element array containing the rowNum and colNum of the record
//                      (or -1 in both slots if not found)
//
// @visibility internal
//<
getRecordCellIndex : function (record, colNum) {
    var rowNum = this.getRecordIndex(record);
    return rowNum >= 0 ? [rowNum, colNum] : [-1, -1];
},


//>    @method    listGrid.getEventRow()
// @include gridRenderer.getEventRow()
// @group events
// @visibility external
//<
getEventRow : function (y) {

    // If we're over the header, bail
    // XXX this works, but is a little kludgey - ideally we'd not call getEventRow() unless
    // we were over the body.
    if (this.header && this.header.containsPoint(this.ns.EH.getX(), this.ns.EH.getY())) {
        return -1;
    }
    return this.body.getEventRow(y);
},

//>    @method    listGrid.getEventColumn()
// @include gridRenderer.getEventColumn()
// @group events
// @visibility external
//<
getEventColumn : function (x) {
    var tempX = (x != null ? x : this.getOffsetX()),
        colNum = 0,
        frozenColumns = 0
    ;
    if (this.frozenBody) frozenColumns = this.frozenFields.length;

    if (frozenColumns > 0 && tempX < this.frozenBody.getVisibleWidth()) {
        colNum = this.frozenBody.getEventColumn(x);
    } else {
        colNum = this.body.getEventColumn(x) + frozenColumns;
    }

    return colNum;
},

// local synonyms reflect ListGrid's row<->record, column<->field convention
getEventRecordNum : function (y) {
    return this.getEventRow(y);
},
getEventFieldNum : function (x) {
    return this.getEventColumn(x);
},

// inner drop position logic is reused in a few different places
_getRecordDropPosition : function (recordNum, y, dropAppearance) {
    if (!dropAppearance) dropAppearance = this.recordDropAppearance;

    var localY = y - this.body.getRowTop(recordNum),
        recordHeight = this.body.getRowSize(recordNum);

    if (dropAppearance == isc.ListGrid.BETWEEN) {
        // Top half, drop above, bottom half drop below
        if (localY > Math.round(recordHeight / 2)) {
            return isc.ListGrid.AFTER;
        } else {
            return isc.ListGrid.BEFORE;
        }
    } else if (dropAppearance == isc.ListGrid.BOTH) {
        if (localY < Math.round(recordHeight / 4)) {
            return isc.ListGrid.BEFORE; // top 1/4, drop above,
        } else if (localY > Math.round(3 * recordHeight / 4)) {
            return isc.ListGrid.AFTER;  // bottom 1/4, drop below
        } else {
            return isc.ListGrid.OVER;   // middle - drop into folder
        }
    }
},

//> @method listGrid.getRecordDropPosition()
// Returns the +link{RecordDropPosition} for some record drop operation.
// This value is passed to the +link{recordDrop()} event notification method.
// <P>
// Default implementation determines the position to return based on
// the specified +link{listGrid.recordDropAppearance} for the grid and the
// +link{EventHandler.getY(),y-coordinate of the drop event}.
//
// @return (RecordDropPosition) record drop position.
//
// @visibility external
//<
getRecordDropPosition : function (recordNum, y, dropAppearance) {
    if (this.recordDropAppearance == isc.ListGrid.OVER) return isc.ListGrid.OVER;
    if (this.recordDropAppearance == isc.ListGrid.BODY) return null;

    // If a y-coordinate was not passed, get it from the offset of the last event
    if (y == null) y = this.body.getOffsetY();

    // which row is the mouse over?
    if (recordNum == null) recordNum = this.getEventRow(y);

    // -2 implies we're past the end of the list
    if (recordNum == -2) return isc.ListGrid.AFTER;

    // a number greater than the number of records in the list implies we're past the end
    // of the list, so we want to return before since it's meaningless to position after a
    // non existent row -- we'll actually just be dropping at the end of the list.
    if (recordNum >= this.getTotalRows()) return isc.ListGrid.BEFORE;

    return this._getRecordDropPosition(recordNum, y, dropAppearance);
},

//>    @method    listGrid.getDropIndex()
// Return the drop-index for a given row and reorderPosition.
// <P>
// When there are no rows in the grid, getDropIndex() returns zero.
// <P>
// If parameter <i>recordNum</i> is not passed, the current event row is used, see
// +link{listGrid.getEventRow()}.
// <P>
// Parameter +link{ReorderPosition,reorderPosition} indicates where the drop-item should appear
// in relation to the row at index <i>recordNum</i>.  If no reorderPosition is provided, it is
// calculated based on the physical position of the mouse in the drop-target row when the drop
// occurs; if the mouse is in the top half of a row, the drop-index is before
// <i>recordNum</i>.  Otherwise, the drop-index is after <i>recordNum</i>.
//
// @param [recordNum]        (number)    Index of the record to drop onto
// @param [reorderPosition]  (ReorderPosition) Where to drop in relation to <i>recordNum</i>
// @return (number) The calculated drop-index
// @visibility external
//<
getDropIndex : function (recordNum, reorderPosition) {
    if (this.data.getLength() == 0) {
        // if there's no data, we have to drop in position 0
        return 0;
    }

    if (recordNum == null) recordNum = this.getEventRow();
    if (reorderPosition == null) reorderPosition = this.getRecordDropPosition(recordNum);

    if (this.canReorderRecords) {
        // if drop-record is -2, set to last record to slide after last record
        if (recordNum == -2) recordNum = this.data.getLength() - 1;

        if (reorderPosition == isc.ListGrid.AFTER) recordNum++;
    }

    return recordNum;
},

// cellSelectionChanged is passed through from the GR - include documentation for it.
//>    @method    listGrid.cellSelectionChanged() ([A])
// @include gridRenderer.cellSelectionChanged()
// @example cellSelection
//<
cellSelectionChanged : function () {
},

// selectionChanged is passed through from the GR - include documentation for it.
//> @method listGrid.selectionChanged()
// Called when (row-based) selection changes within this grid. Note this method fires for
// each record for which selection is modified - so when a user clicks inside a grid this
// method will typically fire twice (once for the old record being deselected, and once for
// the new record being selected).
// <P>
// NOTE: For updating other components based on selections or triggering selection-oriented
// events within an application, see the
// +link{dataBoundComponent.selectionUpdated(),selectionUpdated()} event
// which is likely more suitable.  Calls to +link{listGrid.getSelection(),getSelection()}
// from within this event may not return a valid set of selected records if the event has
// been triggered by a call to +link{dataBoundComponent.selectAllRecords(),selectAllRecords()}
// or +link{dataBoundComponent.deselectAllRecords(),deselectAllRecords()} - in this case
// use the +link{dataBoundComponent.selectionUpdated(),selectionUpdated()} event instead.
//
// @param    record  (ListGridRecord)    record for which selection changed
// @param    state   (boolean)    New selection state (true for selected, false for unselected)
// @group selection
// @visibility external
// @example multipleSelect
//<


selectionChanged : function () {
},

// selectionUpdated is defined in DBC - include documentation for it
//> @method listGrid.selectionUpdated()
// @include dataBoundComponent.selectionUpdated()
// @visibility external
//<

getSerializeableFields : function (removeFields, keepFields) {
    removeFields = removeFields || [];
    removeFields.addList(["header", "selection"]);

    return this.Super("getSerializeableFields", [removeFields, keepFields], arguments);
},

// Frozen Fields / Frozen Columns
// ---------------------------------------------------------------------------------------

// detect frozen fields and reorder the fields array to place frozen fields first, also
// establishing this.frozenFields vs this.normalFields.
// Called by deriveVisibleFields(), which in turn is called by setFields and in other
// circumstances where the set of visible fields has changed or been reordered.
// this.fields is assumed to contain only visible fields.
deriveFrozenFields : function () {

    // detect frozen fields
    var frozenFields = this.frozenFields = this.fields.findAll("frozen", true),
        autoFreezeFields = this.fields.findAll("autoFreeze", true),
        autoFrozenFields = this.fields.findAll("__autoFrozen", true)
    ;

    // if all frozen fields were __autoFrozen, unfreeze them all now
    if (frozenFields && autoFrozenFields && frozenFields.length == autoFrozenFields.length) {
        for (var i=0; i<autoFrozenFields.length; i++) {
            var field = autoFrozenFields[i];
            field.frozen = false;
            delete field.__autoFrozen;
        }
        frozenFields = this.frozenFields = null;
    }

    var allFrozenFields = this.completeFields.findAll("frozen", true);

    // Exception - disallow having all frozen fields and no 'unfrozen' fields
    // Our code doesn't currently handle this and it's not clear what the expected behavior would
    // be!
    if (frozenFields && frozenFields.length == this.fields.length) {
        frozenFields = this.frozenFields = null;
        // setting this flag lets us know that if we show additional, unfrozen fields
        // we'll have to do a rebuild to properly "freeze" the frozen fields!

        this._suppressedFrozenFields = true;
    } else this._suppressedFrozenFields = false;

    if (frozenFields) {
        // freeze any unfrozen autoFreeze: true fields, since other fields are frozen
        if (autoFreezeFields) {
            for (var i=0; i<autoFreezeFields.length; i++) {
                var field = autoFreezeFields[i];
                if (!field.frozen) {
                    field.frozen = true;
                    field.__autoFrozen = true;
                    var pos = field.getAutoFreezePosition ? field.getAutoFreezePosition() :
                            this.freezeStart() ? 0 : this.fields.length,
                        allPos = field.getAutoFreezePosition ? field.getAutoFreezePosition() :
                            this.freezeStart() ? 0 : this.completeFields.length
                    ;
                    frozenFields.addAt(field, pos);
                    allFrozenFields.addAt(field, allPos);
                }
            }
        }

        // reorder the main fields array to put frozen fields on left or right
        this.fields.slideList(frozenFields, this.freezeStart() ? 0 : this.fields.length);
        this.completeFields.slideList(allFrozenFields, this.freezeStart() ? 0 : this.completeFields.length);

        // does not currently work
        this.resizeFieldsInRealTime = false;
    }

    for (var i = 0; i < this.fields.length; i++) {
        var field = this.fields[i];
        // mark all fields with a masterIndex - their index in the master fields Array
        // (listGrid.fields)
        field.masterIndex = i;
    }

    if (frozenFields) {
        // put together the normal fields for the primary body (excludes frozen fields)
        var normalFields = this.normalFields = [];
        for (var i = 0; i < this.fields.length; i++) {
            var field = this.fields[i];
            if (!field.frozen) normalFields.add(field);
        }
    } else this.normalFields = null;


},

// rebuild for possible change in frozen fields.  Can be called as an alternative to
// a full setFields() when only the frozenness of fields has changed.  Unlike setFields(), this
// ends inline editing.

_$toggleFrozen:"toggle frozen fields",
rebuildForFreeze : function (forceRebuild) {

    if (!this.body) return; // children have not been created

    // If we're showing a filterEditor, essentially rebuild it for freeze, but without
    // killing the current edit.

    if (this.filterEditor) {
        // Note: this also handles shifting the checkboxField (if we're showing one) into the
        // frozen fields block via deriveFrozenFields()


        this.filterEditor.deriveVisibleFields();
        this.filterEditor.updateBody(forceRebuild);
        this.filterEditor.remapEditFieldsForFreeze();
        this.filterEditor.layoutChildren(this._$toggleFrozen);
    }
    this.endEditing();

    this.deriveVisibleFields();
    // NOTE: will destroy old header and re-create.  Always happens for any change in fields
    this.updateHeader();

    // will recreate bodies if necessary, otherwise just refresh fields
    this.updateBody(forceRebuild);
    this.layoutChildren(this._$toggleFrozen);

    // If we have a frozen body, the 'getDrawRows()' method delegates to the unfrozen body.
    // Since they're both rendered in a layout we can end up with the frozen body being sized
    // and drawn first, meaning the unfrozen body's getDrawRows() is initially unreliable.
    // Handle this by always doing a single immediate redraw of the frozen body after
    // layoutChildren sizes and renders everything.
    if (this.frozenBody) {
        this.frozenBody.markForRedraw("Recalculating draw area from initial sizing of body");
    }
    // If we're showing a gridSummary row, freeze it as well
    if (this.summaryRow && this.showGridSummary) {
        this.summaryRow.setFields(this.completeFields.duplicate());
        this.summaryRow.rebuildForFreeze(forceRebuild);
        this.summaryRow._recalculateSummaries();
    }

    if (this.showRecordComponents && this.recordComponentPoolingMode == "data") {
        // bodies may have been destroyed, which would orphan recordComponents in "data"
        // pooling mode - if there are any orphaned components, loop over them and re-add any
        // that still have records in the loaded data, removing them from the "orphaned" array
        if (this._orphanedRecordComponents) {
            var comps = isc.getValues(this._orphanedRecordComponents),
                len = comps.length
            ;
            for (var i=len-1; i>=0; i--) {
                var comp = comps[i];
                if (!this.data.contains(comp.embeddedRecord)) continue;

                this.addEmbeddedComponent(comp, comp.embeddedRecord);
                delete this._orphanedRecordComponents[comp.ID];
            }
        }
    }
},

//> @method ListGrid.setCanFreezeFields()
// Setter method for +link{ListGrid.canFreezeFields}
// @param canFreeze (boolean) New value for <code>listGrid.canFreezeFields</code>
// @visibility external
//<
setCanFreezeFields : function (canFreeze) {
    this.canFreezeFields = canFreeze;
    delete this._autoDerivedCanFreeze;
},

// refresh the "masterIndex" property on subcomponents that have been created with a copy of
// the fields array
refreshMasterIndex : function () {
    // this.fields may be set before this.body has been built.
    // Ensure the masterIndex property is correct there.

    if (this.fields) this._refreshMasterIndexForFields(this.fields);
    if (!this.body) return;

    if (this.header) this._refreshMasterIndexForFields(this.header.getMembers());
    if (this.body) this._refreshMasterIndexForFields(this.body.fields || []);
    if (!this.frozenFields) return;
    if (this.frozenHeader) this._refreshMasterIndexForFields(this.frozenHeader.getMembers());
    if (this.frozenBody) this._refreshMasterIndexForFields(this.frozenBody.fields);

},
_refreshMasterIndexForFields : function (fields) {
    for (var i = 0; i < fields.length; i++) {
        fields[i].masterIndex = this.fields.findIndex("name", fields[i].name);
    }
},

getFreezeOn : function () {
    return (this.freezeOn != null ? this.freezeOn : (this.isRTL() ? "right" : "left"));
},

// Whether frozen columns are on the left or right
freezeLeft : function () {
    return this.getFreezeOn() == "left";
},

// whether frozen columns are at the start or end (before or after the unfrozen body)
freezeStart : function () {
    if (this.isRTL()) {
        return this.getFreezeOn() == "right";
    } else {
        return this.getFreezeOn() == "left";
    }
},

getLeftBody : function () {
    var freezeLeft = this.freezeLeft()
    return this.frozenFields && freezeLeft ? this.frozenBody : this.body;
},

getRightBody : function () {
    var freezeLeft = this.freezeLeft();
    return this.frozenFields && !freezeLeft ? this.frozenBody : this.body;
},

setFreezeOn : function (side) {
    var oldSide = this.get
    this.freezeOn = side;
    if (side != oldSide) this.rebuildForFreeze(true);
},

// given an Array, return a subArray of just the slots corresponding to the frozen fields
// (getFrozenSlots) or unfrozen fields (getUnfrozenSlots).  Can be used on an Array of fields,
// or an Array containing any other value that corresponds 1 to 1 with the fields, such as
// field sizes
getFrozenSlots : function (array) {
    var frozenFields = this.frozenFields;
    if (!frozenFields) return array;
    return this.freezeStart() ? array.slice(0, frozenFields.length) :
                               array.slice(array.length - frozenFields.length);
},
getUnfrozenSlots : function (array) {
    var frozenFields = this.frozenFields;
    if (!frozenFields) return array;
    return this.freezeStart() ? array.slice(frozenFields.length) :
                               array.slice(0, array.length - frozenFields.length);
},

fieldIsFrozen : function (colNum) {
    // This can happen even if field.frozen is true - see _suppressedFrozenFields
    if (!this.frozenFields) return false;

    var field = this.getField(colNum);
    return field && !!field.frozen;
},

// frozen fields column numbering:
// - frozen fields do appear in this.fields, always first or last depending on which side the
//   frozen body apppears on
// - both header buttons and the fields passed to the body/frozenBody have a masterIndex
//   property indicating their index in this.fields
// - events coming from either body will have a colNum that matches the index in this.fields.
//   This is done via remapping the colNum in makeBodyMethods
// - if you have a colNum with respect to this.fields:
//   - getFieldBody()/getFieldHeader() will give you the body (GR instance) or header (Toolbar
//     instance) where that column appears
//   - getLocalFieldNum() will give you the field/column number within that component

// return the "body" where a column is rendered
getFieldBody : function (colNum) {
    if (!this.frozenFields) return this.body;
    return this.fieldIsFrozen(colNum) ? this.frozenBody : this.body;
},

// return the "header" where a column is rendered
getFieldHeader : function (colNum) {
    if (!this.frozenFields) return this.header;
    return this.fieldIsFrozen(colNum) ? this.frozenHeader : this.header;
},

getFieldHeaderButton : function (fieldNum) {
     // getFieldHeader / getLocalFieldNum will account for frozen fields
    var header = this.getFieldHeader(fieldNum);
    return header ? header.getMember(this.getLocalFieldNum(fieldNum)) : null;
},

// given a colNum, return the index for that column in the body or header where it appears
// (Takes a numeric colNum - not a string or field object)
getLocalFieldNum : function (colNum) {
    // For a null colNum just return null.

    if (colNum == null) return null;

    // when printing the body fields array gets out of synch with that of the listGrid
    // s we directly pull out any shouldPrint:false fields until printing is complete.
    // We need to handle this otherwise we'd end up returning the wrong colNum - this can lead to
    // JS errors with our complex logic to fire passthrough / passback methods as we end up
    // unable to find fields.
    if (this.body && this.body.isPrinting) {
        var field = this.fields[colNum];
        if (!this._suppressedFrozenFields && field.frozen && this.frozenBody) {
            return this.frozenBody.fields.indexOf(field);
        } else {
            return this.body.fields.indexOf(field);
        }
    }
    if (!this.frozenFields) return colNum;

    var offset;

    offset = this.frozenFields.length;

    // if column is in main body, subtract number of frozen fields if fields are frozen on left
    if (!this.fieldIsFrozen(colNum)) return colNum - offset;

    // otherwise, column is in frozen body.
    return this.freezeStart() ? colNum
                             : colNum - (this.fields.length - this.frozenFields.length);
},

// given a colNum within the body / frozen body, return the 'fieldNum' in our fields array
getFieldNumFromLocal : function (localColNum, body) {
    if (localColNum == null) return null;

    // if we're printing, shouldPrint:false fields will have been pulled out of the body
    // so we need to adjust for that.
    if (this.isPrinting) {
        var bodyFields = this.frozenFields != null ?
                            (body == this.frozenBody ? this.frozenFields : this.unfrozenFields) :
                            this.fields;
        var localField = body.fields[localColNum];
        for (var i = 0; i < bodyFields.length; i++) {
            if (bodyFields[i].name == localField.name) break;
            if (bodyFields[i].shouldPrint == false) localColNum++;
        }

    }

    if (!this.frozenFields) return localColNum;

    if (body == this.frozenBody) {
        if (this.freezeStart()) return localColNum;
        else {
            var unfrozen = this.fields.length - this.frozenFields.length;
            return localColNum + unfrozen;
        }
    } else {
        var offset = this.freezeStart() ? this.frozenFields.length : 0;
        return localColNum + offset;
    }
},

// map various methods through to the appropriate body.
// if this set continues to grow, we may want to take a code generation approach instead

//> @method ListGrid.getColumnLeft() ([A])
// Return the left offset (in local coordinate space) of a particular column.
// @param colNum (Integer) number of the column
// @return (Integer) left offset of the passed colNum, or null if not yet drawn or no such column
// @group sizing, positioning
// @visibility external
//<
getColumnLeft : function (colNum) {
    var body = this.getFieldBody(colNum);
    if (!body) return null;
    return body.getColumnLeft(this.getLocalFieldNum(colNum));
},

//> @method ListGrid.getColumnPageLeft() ([A])
// Return the left coordinate for a given column number as a GLOBAL coordinate
// @param colNum (Integer) number of the column
// @return (Integer) page left offset of the passed colNum, or null if undrawn or no such column
// @group sizing, positioning
// @visibility external
//<
getColumnPageLeft : function (colNum) {
    var body = this.getFieldBody(colNum);
    if (!body) return null;
    return body.getColumnPageLeft(this.getLocalFieldNum(colNum));
},

//> @method ListGrid.getColumnWidth() ([A])
// Return the width of a particular column.
// @param colNum (number) number of the column
// @return (Integer) width of the column, or <code>null</code> if undrawn or no such column.
// @group sizing, positioning
// @visibility external
//<
getColumnWidth : function (colNum) {
    var body = this.getFieldBody(colNum);
    if (!body) return null;
    return body.getColumnWidth(this.getLocalFieldNum(colNum));
},

//> @method ListGrid.getCellPageRect() ([A])
// Returns the page offsets and size of the cell at the passed row and column.  If auto-sizing
// is enabled, sizes are not definitive until the grid has finished drawing, so calling this
// method before drawing completes will return the configured column sizes.
// @param rowNum (number) row index of the cell
// @param colNum (number) column index of the cell
// @return (Array of Integer) the page rect of the passed cell, or null if undrawn
// @group sizing, positioning
// @visibility external
//<
getCellPageRect : function (rowNum, colNum) {
    var body = this.getFieldBody(colNum);
    if (!body) return null;
    return body.getCellPageRect(rowNum, this.getLocalFieldNum(colNum));
},

refreshCellStyle : function (rowNum, colNum, className) {
    var body = this.getFieldBody(colNum);
    return body.refreshCellStyle(rowNum, this.getLocalFieldNum(colNum), className);
},

//> @method listGrid.freezeField()
// Freeze the indicated field, so that it remains in place and visible when horizontal
// scrolling occurs.
//
// @param field (ListGridField or Integer or String or Array) field or fields to freeze.
//  fields may be specified as ListGridField objects, field names or colNum.
// @group frozenFields
// @visibility external
//<
freezeField : function (field) {
    return this.toggleFrozen(field, true);
},

//> @method listGrid.unfreezeField()
// Unfreeze a frozen field, so that it will now scroll along with other fields when horizontal
// scrolling occurs.
//
// @param field (ListGridField or Integer or String or Array) field or fields to unfreeze.
//  fields may be specified as ListGridField objects, field names or colNum.
// @group frozenFields
// @visibility external
//<
unfreezeField : function (field) {
    return this.toggleFrozen(field, false);
},

//> @method listGrid.toggleFrozen()
// Freeze or unfreeze the indicated field according to whether it is currently frozen.
// <P>
// Called when the ListGrid freezes or unfreezes fields by user action.
//
// @param field (ListGridField or Integer or String or Array) field or fields to freeze.
//  fields may be specified as ListGridField objects, field names or colNum.
// @group frozenFields
// @visibility external
//<
// isFrozen parameter undocumented - developer would typically call freezeField or unfreezeField
// directly
toggleFrozen : function (field, isFrozen) {
    if (!isc.isAn.Array(field)) field = [field];
    var changed = false;
    for (var i = 0; i < field.length; i++) {
        var currentField = this.getField(field[i]);
        if (!currentField || !this.fieldIsVisible(currentField) ||
            this.isCheckboxField(currentField))
        {
            continue;
        }
        var shouldFreeze = isFrozen;
        if (shouldFreeze == null) shouldFreeze = !currentField.frozen;
        if (currentField.frozen == isFrozen) continue; // field frozeness did not change

        changed = true;
        currentField.frozen = shouldFreeze;
    }

    if (!changed) return false;

    this.rebuildForFreeze();
    // Note: rebuildForFreeze will ultimately call updateBody() which
    // remaps any embedded components to the appropriate colNum based on field name.

    this.handleFieldStateChanged(true);

    return true; // field frozeness changed
},

// Body
// --------------------------------------------------------------------------------------------

// update the body for a change in fields
updateBody : function (forceRebuild) {
    // if frozen fields are being introduced or going away entirely, recreate the body[s] and
    // bodyLayout.  Otherwise, just refresh the fields array being used by each body.

    if (forceRebuild ||
        (this.frozenFields && !this.frozenBody) ||
        (this.frozenBody && !this.frozenFields))
    {
        forceRebuild = true;
        // We're going to destroy the current body or bodies. Drop all recordComponents first
        // so we don't get confused by pointers to destroyed recordComponents.
        this.dropRecordComponents();



        if (this.body) {
            // Un-embed and Destroy all embedded components.

            this._destroyEmbeddedComponentsForRebuild();
        }

        // frozen body being introduced or going away, or freeze side changing
        if (this.bodyLayout) this.bodyLayout.destroy();
        else if (this.body)  this.body.destroy();
        this.body = this.bodyLayout = this.frozenBody = null;
        this.createBodies();
    }

    // If we've already run through "createChildren" we need to explicitly add the
    // body as a member now.
    // Calling code will handle calling "layoutChildren" which'll ensure the fieldWidths are
    // correct, etc.
    if (this.isDrawn()) {
        this.updateGridComponents();
    }

    if (this.body) {
        this.body.fields = this.normalFields || this.fields;
        if (!forceRebuild) this._remapEmbeddedComponentColumns(this.body);
        this.body.markForRedraw("fields change");
    }
    if (this.frozenBody) {
        this.frozenBody.fields = this.frozenFields;
        if (!forceRebuild) this._remapEmbeddedComponentColumns(this.frozenBody);
        this.frozenBody.markForRedraw("fields change");
        if (this.canSelectCells) this._updateDependentSelections();
    }
},

// In various circumstances we automatically unembed embedded components.
// - rebuilding bodies for freeze / unfreeze
// - data change such that an embedded components' record is no longer present in the
//   data set
// - field visibility changes to show/hide cells containing components.
// Should we auto-destroy components when this occurs?

shouldDestroyOnUnembed : function (embeddedComponent, reason) {

    // Respect 'dontAutoDestroy' on embedded components. We explicitly doc this for recordComponent
    // usage
    if (embeddedComponent.dontAutoDestroy) return false;

    if (embeddedComponent.isBackgroundComponent) {
        // if we created the backgroundComponent for some record, and the record has now
        // been lost from our data-set, drop it
        if (reason == this._$dataChanged && embeddedComponent.creator == this) return true;
        // Otherwise keep it around (either the developer explicitly created the canvas,
        // or we are simply changing fields / rebuilding for freeze but still have the
        // data around).
        return false;
    }

    if (embeddedComponent.isRecordComponent) {
        if (reason == this._$dataChanged) return true;
        return false;
    }

    if (embeddedComponent.destroyOnUnembed != null) return embeddedComponent.destroyOnUnembed;

    // Keep components around if we just changed columns, otherwise destroy
    if (reason == this._$columnRemap) return false;
    return true;
},

// Used by updateBody - when we rebuild the entire body we just wipe out all embedded
// components.
_$rebuildingBody:"rebuilding body",
_destroyEmbeddedComponentsForRebuild : function () {
    var components = this.body._embeddedComponents;
    if (this.frozenBody) {
        var allComponents = [];
        if (components == null) components = allComponents;
        else components = allComponents.addList(components);
        components.addList(this.frozenBody._embeddedComponents || []);
    }

    if (components && components.length > 0) {
        for (var i=0; i<components.length; i++) {
            var record = components[i].embeddedRecord,
                shouldCollapse = false,
                embeddedComponents = this._getEmbeddedComponents(record)
            ;
            if (embeddedComponents) {
                for (var j=0; j<embeddedComponents.length; j++) {
                    var component = embeddedComponents[j];
                    shouldCollapse = shouldCollapse || component.isExpansionComponent;

                    if (this.shouldDestroyOnUnembed(component, this._$rebuildingBody)) {
                        component.markForDestroy();
                    } else {
                        component.deparent();
                    }
                }
                this._setEmbeddedComponents(record, null);
                if (shouldCollapse) this._setExpanded(record, false);
                this._deleteEmbeddedComponents(record);
            }
        }
    }
},

createBodies : function () {
    if (this.body != null) return; // already created

    // Update virtual scrolling based on showRecordComponents()

    this._updateVirtualScrollingForRecordComponents();

    // create the primary body and add it as a child
    this.body = this.createBody(this.ID + "_body", this.normalFields || this.fields);
    // unless gridComponents is customized, the header and sorter button are created before the
    // body. Because the sorter button width may depend on the body's scrollbar size, if the
    // sorter button exists, recompute its width.
    if (this.sorter != null) {
        this.sorter.setWidth(this._getSorterWidth(true));
    }

    this.body.resizeTo("100%", "100%");

    this.bodies = [this.body];

    // drag scrolling should target the body
    this.dragScrollTarget = this.body;

    var frozenFields = this.frozenFields;
    if (frozenFields == null || frozenFields.length == 0) {
        return this.body;
    }

    this.body.addProperties({
        _getViewportFillRows : function () {
            // the frozen body may report an additional row if the normal body is showing an
            // h-scrollbar - check for that and increment the endRow accordingly
            var frozenRows = [0,0];
            if (!this.frozen && this.grid && this.grid.frozenBody) {
                frozenRows = this.grid.frozenBody._getViewportFillRows();
            }

            var rows = this.Super("_getViewportFillRows", arguments);
            if (frozenRows[1] > rows[1]) rows[1]++;
            return rows;
        }
    });

    this.frozenBody = this.createBody(this.ID + "_freezeBody", frozenFields, true);
    this.frozenBody.useNativeDrag = false;

    // NOTE: Freezing the fields on the left implies we almost certainly have an HScrollbar
    // for the fields on the right, creating a difference in viewport area
    // Add some space to the bottom of the freeze body to make up this difference in the
    // scrollHeight so when the user scrolls to the bottom the grids stay in synch
    if (this.body.hscrollOn && !this.shrinkForFreeze) {
        this.frozenBody.setEndSpace(this.body.getScrollbarSize());
    }

    if (this.freezeStart()) {
        this.bodies.unshift(this.frozenBody);
    } else {
        this.bodies.add(this.frozenBody);
    }

    this.bodyLayout = this.createAutoChild("bodyLayout", {
        width:"100%", height:"100%",
        autoDraw:false,
        members : this.bodies
    }, isc.HLayout);

    // create separate selection objects for body/frozenBody
    this._separateDependentSelections();
},

//> @method listGrid.getBody()
// Returns the primary +link{ListGrid.body,body}, which, when there are frozen fields, is the
// +link{GridRenderer} used to render the non-frozen portion of the dataset; otherwise, the
// primary body (the only body) is the GridRenderer used to render the entire dataset.
// @return (GridRenderer) the primary body or null if this ListGrid has not been drawn yet.
// @visibility external
//<
getBody : function () {
    return this.body;
},

_separateDependentSelections : function () {
    if (this.canSelectCells) {
        var start = this.freezeStart(),
            normalLength = this.body.fields.length,
            frozenLength = this.frozenBody.fields.length;
        this.body._setSeparateCellSelection(this.selection, start ? frozenLength : 0);
        this.frozenBody._setSeparateCellSelection(this.selection, start ? 0 : normalLength);
    }
},

_updateDependentSelections : function () {
    if (!isc.isA.DependentCellSelection(this.body.selection)||
        !isc.isA.DependentCellSelection(this.frozenBody.selection)) {
            // this.logWarn("ListGrid._updateDependentSelection() called when one or more" +
            //              "GridRenderer bodies are not DependentCellSelections!");
        return;
    }
    var start = this.freezeStart(),
        normalLength = this.body.fields.length,
        frozenLength = this.frozenBody.fields.length;
    this.body.selection._updateDependency(start ? frozenLength : 0, normalLength);
    this.frozenBody.selection._updateDependency(start ? 0 : normalLength, frozenLength);
},

// bodyOverflowed - notification method when adjustOverflow runs on the body
// Updates the endSpace on this.frozenBody if we're showing frozen cells.
bodyOverflowed : function () {
    if (this.summaryRow != null && this.summaryRow.body) {
        this.summaryRow.body.setRightSpace(this.body.vscrollOn ? this.body.getScrollbarSize() : 0);
    }
    if (!this.frozenBody) return;
    this.frozenBody.setEndSpace(this.body.hscrollOn && !this.shrinkForFreeze
                                    ? this.body.getScrollbarSize() : 0);
},

//> @attr listGrid.screenReaderCellSeparator (HTMLString : "/" : IRA)
// When +link{isc.setScreenReaderMode(),screen reader mode} is enabled,
// this invisible separator string is written out between each cell value. This
// string will be picked up by screen readers allowing them to more obviously identify
// to the user the different cell values as the contents of a row are read aloud. The
// +link{ListGrid.screenReaderRowSeparator,screenReaderRowSeparator} string if any will
// be written out after the last cell in a row to separate the last cell's value from
// the screen reader's announcement of the row's number and total number of rows in the grid
// (such as "3 of 20").
// <P>
// This attribute may be set to null to disable the feature.
// <P>
// Note that screen readers vary widely on which punctuation symbols are read aloud, and
// sometimes it depends on the context of the punctuation. However, the widely-used JAWS, NVDA,
// and VoiceOver screen readers all read the forward slash '/' as "slash".
// See +externalLink{http://www.deque.com/blog/dont-screen-readers-read-whats-screen-part-1-punctuation-typographic-symbols/,Why Don’t Screen Readers Always Read What’s on the Screen? Part 1: Punctuation and Typographic Symbols}
// for a table of findings on which punctuation symbols are read aloud by JAWS, NVDA,
// and VoiceOver.
// @group accessibility
// @visibility external
//<
screenReaderCellSeparator:"/",

//> @attr listGrid.screenReaderRowSeparator (HTMLString : "," : IRA)
// When +link{isc.setScreenReaderMode(),screen reader mode} is enabled,
// this invisible string will be written out after the last cell in a row. This
// string will be picked up by screen readers allowing them to more obviously identify
// to the user the end of the row's contents and the screen reader's announcement of the row's
// number and total number of rows in the grid (such as "3 of 20").
// <p>
// This attribute may be set to null to disable writing out an invisible string at the
// end of each row.
// <p>
// Note that screen readers vary widely on which punctuation symbols are read aloud, and
// sometimes it depends on the context of the punctuation. However, the widely-used JAWS, NVDA,
// and VoiceOver screen readers all pause briefly for a comma ',' that is within text.
// See +externalLink{http://www.deque.com/blog/dont-screen-readers-read-whats-screen-part-1-punctuation-typographic-symbols/,Why Don’t Screen Readers Always Read What’s on the Screen? Part 1: Punctuation and Typographic Symbols}
// for a table of findings on which punctuation symbols are read aloud by JAWS, NVDA,
// and VoiceOver.
// @see screenReaderCellSeparator
// @group accessibility
// @visibility external
//<
screenReaderRowSeparator:",",

createBody : function (ID, fields, frozen) {
    // create a body object to show the body of the list
    var body = isc.ClassFactory.getClass(this.bodyConstructor).createRaw();

    body.ID = ID;
    body.autoDraw = false;

    body.grid = this;

    body.fields = fields;

    body.frozen = frozen;

    body.overflow = frozen ? "hidden" : this.bodyOverflow;

    if (this.scrollbarConstructor != null) body.scrollbarConstructor = this.scrollbarConstructor;
    if (this.showCustomScrollbars != null) body.showCustomScrollbars = this.showCustomScrollbars;
    if (this.scrollbarSize != null) body.scrollbarSize = this.scrollbarSize;

    body.backgroundColor = this.bodyBackgroundColor;
    var bodyStyleName = this.bodyStyleName;
    if (this.alternateBodyStyleName != null && this.alternateRecordStyles) {
        bodyStyleName = this.alternateBodyStyleName;
    }
    body.styleName = bodyStyleName;

    // In order to float widgets above the list, we want to allow contents (the
    // actual table) and children to be drawn for this element.
    body.allowContentAndChildren = true;

    // table geometry (NOTE: record -> row terminology translation)

    body.fixedRowHeights = this.fixedRecordHeights;
    body.fixedColumnWidths = this.fixedFieldWidths;
    body.alternateRowStyles = this.alternateRecordStyles;
    body.alternateRowFrequency = this.alternateRecordFrequency;
    body.alternateColumnStyles = this.alternateFieldStyles;
    body.alternateColumnFrequency = this.alternateFieldFrequency;
    body.canSelectText = this.canDragSelectText;
    body.showAllRows = this.showAllRecords;
    if (this.virtualScrolling != null) body.virtualScrolling = this.virtualScrolling;

    // if using drag selection, don't give the Grid a dragTarget
    // - the Grid can handle the selection
    // - the ListGrid won't show a dragTracker
    // Otherwise we're doing dragging of records in and out, so have the ListGrid
    // handle the drag event, and show a tracker.
    body.dragTarget = this.canDragSelect ? null : this;
    body.dragAppearance = isc.EventHandler.NONE;

    body.screenReaderCellSeparator = this.screenReaderCellSeparator;
    body.screenReaderRowSeparator = this.screenReaderRowSeparator;

    // For AutoTest APIs
    body.locatorParent = this;

    // To make checkboxField work with singleCellValue
    body.selectionAppearance = this.selectionAppearance;

    // support for defining GridRenderer override functions and properties directly
    // on the LV instead of via LV.body.
    var propNames = isc.ListGrid._gridPassthroughProperties;

    // pass all of these properties straight through
    for (var i = 0; i < propNames.length; i++) {
        var propName = propNames[i],
            value = this[propName];
        if (value != null) body[propName] = value;
    }

    if (this._originalUseNativeTabIndex != null) {
        body._useNativeTabIndex = this._originalUseNativeTabIndex;
    }

    isc.addProperties(body, this.bodyDefaults, this.bodyProperties);

    // suppress empty message on frozen body - only show it in one place!
    if (frozen) body.showEmptyMessage = false;

    body.completeCreation();
    body = isc.SGWTFactory.extractFromConfigBlock(body);

    // add passthroughs from the body to the ListGrid for pluggable body APIs defined on the grid
    // Note that this has to be after extracting the body from the SGWT config block, if any
    this._addBodyPassthroughMethods(body);

    return body;
},


//>    @method    listGrid.getDrawnRowHeight() ([A])
// Get the drawn height of a row.
//
// @param rowNum (number)
// @return (number) height
// @group sizing, positioning
// @visibility external
//<

//>    @method    listGrid.getRowHeight()
// Return the height this row should be.  Default is this.cellHeight. If
// +link{listGrid.fixedRecordHeights} is false, the row may be rendered taller than this
// specified size.
// <P>
// If records will be variable height,
// you should switch on +link{listGrid.virtualScrolling, virtualScrolling}.
// <P>
// Note if +link{listGrid.allowRowSpanning,row spanning} is enabled,
// this method should return the height of a single row (with rowSpan set to 1).
//
// @param    record  (ListGridRecord)    cell record as returned by getCellRecord
// @param    rowNum    (number)    row number
// @return    (number)    height in pixels
// @visibility external
//<
// Undocumented 'isFrozenBody' param used by the ListGrid
// Freezing and editing
// By default in edit mode we allow form items to overflow the available space (don't fix the edit
// However if we have frozen fields we can have form items of different heights in the 2 bodies
// leading to a mismatch.
// Catch this if we can by applying an explicit min-height matching the space that will be
// required by the edit items (for efficiency only run this logic if we have to)
getRowHeight : function (record,rowNum,isFrozenBody) {
    var cellHeight;

    if (this.allowRowSpanning && this.getRowSpan) {
        // This code makes an assumption that rowSpanning will never increase going from
        // left to right, and you'll always have at least one column with rowSpan set to 1.
        // If we don't make this assumption we'll have to call the getRowSpan method for
        // every column on both bodies to figure out what the minimum rowSpan for each
        // gridRenderer instance is which won't scale well (code to do this included, can
        // be enabled via an undocumented flag).
        if (!this.allowIncreasingRowSpanning) {
            if (isFrozenBody) {
                cellHeight = this.cellHeight
                    * this.frozenBody._getRowSpan(record, rowNum, this.frozenFields.length-1);
            } else {
                cellHeight = this.cellHeight;
            }

        } else {
            var minRowSpan,
                body = isFrozenBody ? this.frozenBody : this.body,
                startField = isFrozenBody ? 0 :
                            (this.frozenFields ? this.frozenFields.length : 0),
                endField = isFrozenBody ? this.frozenFields.length : this.fields.length;

            for (var i = startField; i < endField; i++) {
                if (minRowSpan == null) {
                    // calling the internal version of the method which will round
                    // off if the rowSpan exceeds the number of rows
                    minRowSpan = body._getRowSpan(record, rowNum, i);
                } else {
                    minRowSpan = Math.min(minRowSpan, body._getRowSpan(record, rowNum, i));
                }
                if (minRowSpan == 1) break;
            }
            cellHeight = this.cellHeight * minRowSpan;
        }
    } else {
        cellHeight = this.cellHeight;
    }

//    this.logWarn("getRowHeight called at row: " + rowNum +
//                 " body is: " + (isFrozenBody ? "frozen" : "unfrozen") +
//                 " returning: " + cellHeight);

    if (this.frozenFields && this.getEditRow() == rowNum) {
        var editForm = this.getEditForm(),
            items = editForm ? editForm.getItems() : [];
        for (var i = 0; i < items.length; i++) {
            var itemHeight = (items[i].getHeight() + 2*this.cellPadding);
            if (itemHeight > cellHeight) cellHeight = itemHeight;
        }
    }
    return this.body.updateHeightForEmbeddedComponents(record, rowNum, cellHeight);
},

// Selection / Rollover effects
// ---------------------------------------------------------------------------------------

selectionCanvasDefaults: {

    _constructor: "SelectionOrRollOverCanvas",
    opacity: 20
},

selectionUnderCanvasDefaults: {
},

makeSelectionCanvas : function (record) {

    return this.selectionCanvas = this.createAutoChild("selectionCanvas",
        {
            eventProxy:this.body,
            snapTo:"TL",
            width:"100%", height:"100%",
            bubbleMouseEvents:true,
            destroyOnUnEmbed:false,
            percentSource:this, percentBox:"custom",
            grid:this,
            autoDraw:false
        }
    );
},

makeSelectionUnderCanvas : function (record) {

    return this.selectionUnderCanvas = this.createAutoChild("selectionUnderCanvas",
        {
            eventProxy:this.body,
            snapTo:"TL",
            width:"100%", height:"100%",
            bubbleMouseEvents:true,
            destroyOnUnEmbed:false,
            percentSource:this, percentBox:"custom",
            grid:this,
            autoDraw:false
        }
    );
},

getSelectionCanvas : function () {
    var selectionCanvas = this.selectionCanvas;
    if (selectionCanvas == null || selectionCanvas.destroyed) {
        selectionCanvas = this.makeSelectionCanvas();
    }
    return selectionCanvas;
},

getSelectionUnderCanvas : function () {
    var selectionUnderCanvas = this.selectionUnderCanvas;
    if (selectionUnderCanvas == null || selectionUnderCanvas.destroyed) {
        selectionUnderCanvas = this.makeSelectionUnderCanvas();
    }
    return selectionUnderCanvas;
},

updateSelectionCanvas : function () {
    var selection = this.selection,
        rowNum,
        colNum,
        record;
    if (isc.isA.CellSelection(selection)) {
        var cell = selection.getSelectedCells()[0];
        rowNum = cell ? cell[0] : -1,
        colNum = cell ? cell[1] : -1;
        record = cell ? this.getCellRecord(rowNum,colNum) : null;
    } else {
        record = this.getSelectedRecord();
        rowNum = this.data ? this.data.indexOf(record) : -1;
    }

    var selectionCanvas = this.selectionCanvas,
        selectionUnderCanvas = this.selectionUnderCanvas,
        refSelectionCanvas = selectionCanvas || selectionUnderCanvas,
        currentRecord,
        currentRowNum,
        currentColNum;
    if (refSelectionCanvas != null) {
        currentRecord = refSelectionCanvas.embeddedRecord;
        currentRowNum = refSelectionCanvas._currentRowNum;
        currentColNum = refSelectionCanvas._currentColNum;

    } else {
        currentRecord = null;
        currentRowNum = -1;
        currentColNum = -1;
    }

    // nothing to do if we don't have a selection canvas or a selection, OR
    // we have a selectionCanvas already assigned to the selected record
    var showSelectionOrSelectionUnderCanvas = this.showSelectionCanvas || this.showSelectionUnderCanvas;
    if (refSelectionCanvas == null) {
        if (record == null || !showSelectionOrSelectionUnderCanvas) return;
    } else if (showSelectionOrSelectionUnderCanvas &&
               currentRecord == record &&
               currentRowNum == rowNum &&
               currentColNum == colNum)
    {
        return;
    }

    if (showSelectionOrSelectionUnderCanvas && this.selectionType != isc.Selection.SINGLE &&
        this.selectionType != isc.Selection.NONE)
    {
        this.logWarn((this.showSelectionCanvas
                      ? (this.showSelectionUnderCanvas
                         ? "showSelectionCanvas and showSelectionUnderCanvas are"
                         : "showSelectionCanvas is")
                      : "showSelectionUnderCanvas is") +
                     " set to true, but this is not supported for selectionType " +
                     this.selectionType);
        this.showSelectionCanvas = false;
        this.showSelectionUnderCanvas = false;
        showSelectionOrSelectionUnderCanvas = false;
    }

    // If we're not showing the row, or we have no selection,
    // ensure the selectionCanvas is hidden and bail.
    if (!record || rowNum == -1 || !showSelectionOrSelectionUnderCanvas) {
        if (selectionCanvas != null) {
            this.removeEmbeddedComponent(currentRecord, selectionCanvas);
        }
        if (selectionUnderCanvas != null) {
            this.removeEmbeddedComponent(currentRecord, selectionUnderCanvas);
        }
        return;
    }

    // calling the getters will create the selectionCanvas / selectionUnderCanvas if necessary
    if (this.showSelectionCanvas) {
        selectionCanvas = this.getSelectionCanvas();

        selectionCanvas.record = record;
        selectionCanvas.removeOnHideField = true;
        this.addEmbeddedComponent(selectionCanvas, record, rowNum, colNum, "within");
    }
    if ((this.showSelectionCanvas && this.showSelectionUnderCanvas == null) ||
        this.showSelectionUnderCanvas)
    {
        selectionUnderCanvas = this.getSelectionUnderCanvas();

        selectionUnderCanvas.record = record;
        selectionUnderCanvas.removeOnHideField = true;
        this.addEmbeddedComponent(selectionUnderCanvas, record, rowNum, colNum, "within");
    }
},

// Roll Over Canvas


rollOverCanvasDefaults: {

    _constructor: "SelectionOrRollOverCanvas",
    snapTo: "TL",
    width: "100%",
    height: "100%"
},

rollUnderCanvasDefaults: {
    snapTo: "TL",
    width: "100%",
    height: "100%"
},

makeRollOverCanvas : function () {

    return this.rollOverCanvas = this.createAutoChild("rollOverCanvas",
        {   eventProxy:this.body,
            percentSource:this, percentBox:"custom",
            bubbleMouseEvents:true,
            destroyOnUnEmbed:false,
            grid:this,
            autoDraw:false
        }
    );
},

makeRollUnderCanvas : function () {

    return this.rollUnderCanvas = this.createAutoChild("rollUnderCanvas",
        {
            eventProxy:this.body,
            percentSource:this, percentBox:"custom",
            bubbleMouseEvents:true,
            destroyOnUnEmbed:false,
            grid:this,
            autoDraw:false
        }
    );
},



//> @method listGrid.getRollOverCanvas()
// If the +link{rollOverCanvas,rollOverCanvas} is enabled, this method will be called to create
// the canvas to display over the current row or cell when a user moves their mouse over the row.
// <p>
// Note that for efficiency this should not typically create a new Canvas every time that it is
// called. Instead typically a single rollOver canvas should be created and updated to reflect
// the current rollOver row if necessary.
// <smartgwt><p>
// <b>Note: This is an override point.</b></smartgwt>
//
// @param rowNum (Integer) index of the current rollOver row.
// @param colNum (Integer) index of the current rollOver column. This parameter will be null unless
// +link{useCellRollOvers,useCellRollOvers} is true for the grid.
// @return (Canvas) the embedded component
// @group hoverComponents
// @visibility external
//<
getRollOverCanvas : function (rowNum, colNum) {
    var rollOverCanvas = this.rollOverCanvas;
    if (rollOverCanvas == null || rollOverCanvas.destroyed) {
        rollOverCanvas = this.makeRollOverCanvas();
    }
    return rollOverCanvas;
},

//> @method listGrid.getRollUnderCanvas()
// If the +link{rollUnderCanvas,rollUnderCanvas} is enabled, this method will be called to create
// the canvas to display under the current row or cell when a user moves their mouse over the row.
// <p>
// Note that for efficiency this should not typically create a new Canvas every time that it is
// called. Instead typically a single rollUnder canvas should be created and updated to reflect
// the current rollOver row if necessary.
// <smartgwt><p>
// <b>Note: This is an override point.</b></smartgwt>
//
// @param rowNum (Integer) index of the current rollOver row.
// @param colNum (Integer) index of the current rollOver column. This parameter will be null unless
// +link{useCellRollOvers,useCellRollOvers} is true for the grid.
// @return (Canvas) the embedded component
// @group hoverComponents
// @visibility external
//<
getRollUnderCanvas : function (rowNum, colNum) {
    var rollUnderCanvas = this.rollUnderCanvas;
    if (rollUnderCanvas == null || rollUnderCanvas.destroyed) {
        rollUnderCanvas = this.makeRollUnderCanvas();
    }
    return rollUnderCanvas;
},

updateRollOverCanvas : function (rowNum, colNum, leaving) {
    var removeOnly = false;
    var record;

    if (leaving || (!this.showRollOverCanvas && !this.showRollUnderCanvas) || rowNum == -1) {
        removeOnly = true;
    } else {
        record = this.getCellRecord(rowNum, colNum);

        if (record == null || Array.isLoading(record)) {
            removeOnly = true;
        }
    }

    if (removeOnly) {
        if (this.currentRollOverCanvas && !this.currentRollOverCanvas.destroying && !this.currentRollOverCanvas.destroyed) {
            this.removeEmbeddedComponent(this.currentRollOverCanvas.embeddedRecord, this.currentRollOverCanvas);
        }

        if (this.currentRollUnderCanvas && !this.currentRollUnderCanvas.destroying && !this.currentRollUnderCanvas.destroyed) {
            this.removeEmbeddedComponent(this.currentRollUnderCanvas.embeddedRecord, this.currentRollUnderCanvas);
        }

        return;
    }



    if (!this.useCellRollOvers) {
        colNum = null;
    }

    // Assume we will never be called if the rowNum / colNum hasn't changed since we already
    // have a check for this in GR.mouseMove



    var oldRollOverCanvas = this.currentRollOverCanvas,
        oldRollUnderCanvas = this.currentRollUnderCanvas;
    var rollOverCanvas;
    if (this.showRollOverCanvas) {
        rollOverCanvas = this.getRollOverCanvas(rowNum, colNum);
    } else {
        rollOverCanvas = null;
    }
    this.currentRollOverCanvas = rollOverCanvas;
    var rollUnderCanvas;
    if ((this.showRollOverCanvas && this.showRollUnderCanvas == null) ||
        this.showRollUnderCanvas)
    {
        rollUnderCanvas = this.getRollUnderCanvas(rowNum, colNum);
    } else {
        rollUnderCanvas = null;
    }
    this.currentRollUnderCanvas = rollUnderCanvas;

    if (rollOverCanvas != null) {
        rollOverCanvas.grid = this;
        rollOverCanvas.record = record;
        rollOverCanvas.removeOnHideField = true;
        this.addEmbeddedComponent(rollOverCanvas, record, rowNum, colNum, "within");

    } else if (oldRollOverCanvas != null && !oldRollOverCanvas.destroying) {
        this.removeEmbeddedComponent(oldRollOverCanvas.embeddedRecord, oldRollOverCanvas);
    }

    if (rollUnderCanvas != null) {
        rollUnderCanvas.grid = this;
        rollUnderCanvas.record = record;
        rollUnderCanvas.removeOnHideField = true;
        this.addEmbeddedComponent(rollUnderCanvas, record, rowNum, colNum, "within");

    } else if (oldRollUnderCanvas != null && !oldRollUnderCanvas.destroying) {
        this.removeEmbeddedComponent(oldRollUnderCanvas.embeddedRecord, oldRollUnderCanvas);
    }
},

// handle embedded components' z-indices here - allows us to float selection canvas behind
// the LG table if appropriate
updateEmbeddedComponentZIndex : function (component) {
    if (!component) return;
    if (this.selectionCanvas == component) {
        var tableIndex = this.body.getTableZIndex();
        this.selectionCanvas.setZIndex(tableIndex + 50);
    }
    if (this.selectionUnderCanvas == component) {
        var tableIndex = this.body.getTableZIndex();
        this.selectionUnderCanvas.setZIndex(tableIndex - 100);
    }
    // rollover should appear on top of selection.
    if (this.currentRollOverCanvas == component) {
        var tableIndex = this.body.getTableZIndex();
        component.setZIndex(tableIndex + 100);
    }
    if (this.currentRollUnderCanvas == component) {
        var tableIndex = this.body.getTableZIndex();
        component.setZIndex(tableIndex - 50);
    }

},

_handleEmbeddedComponentResize : function (body, component, deltaX, deltaY) {

    // This logic ensures that we don't allow fields to size smaller than their
    // widest embedded component (and that we change that min on component resize
    // if necessary.
    if (component != null && component._currentFieldName &&
        component.embeddedPosition == this._$within &&
        deltaX != null && deltaX != 0)
    {
        // Ignore components not embedded in a column.
        var fieldName = component._currentFieldName,
            field = this.getField(fieldName);
        // Ignore percentWidth components whose size is driven by the
        // field itself rather than vice versa.
        var percentWidth = component._percent_width;
        if (field &&
            (percentWidth == null || !percentWidth.endsWith("%")))
        {

            var newSize = component.getVisibleWidth(),
                oldSize = newSize - deltaX;

            var currentMaxWidth = field._maxComponentWidth;

            // Assertion - if the "field._maxComponentWidth" is currently null,
            // it's either never been set, or we're pending a re-calculation of it.
            // Either way, no need to fire the notification to recalculate again.
            if (currentMaxWidth != null) {
                if (newSize > currentMaxWidth) {
                    this._fieldComponentWidthsChanged(fieldName, newSize);
                } else if (oldSize == currentMaxWidth) {
                    // Don't pass in a "new width" here - we don't know if
                    // other embedded components in this field exceed this component's
                    // new size so have fieldComponentWidthsChanged simply drop the
                    // cached max-width for lazy recalculation.
                    this._fieldComponentWidthsChanged(fieldName);
                }
                // Nothing to do if neither the new nor old size exceeds the
                // width of some other embedded component in the col.
            }
        }
    }


    // If we have frozen fields and an embeddedComponent in one body resizes vertically,
    // refresh both the frozen and unfrozen body to ensure row heights tay in synch.
    if (this.frozenBody != null && deltaY != null && deltaY != 0) {
        var otherBody = (body == this.frozenBody) ? this.body : this.frozenBody;
        otherBody.markForRedraw("Embedded component requires row resizing");
    }
},

// Header
// --------------------------------------------------------------------------------------------

// get properties common to *both* header buttons and the corner sort button
getButtonProperties : function () {
    var propsFromGrid = {
        // textAlign: wipe out the default alignment, which is center, to cause the default to
        // come from the text direction
        align:null
    };
    if (this.headerTitleStyle != null) propsFromGrid.titleStyle = this.headerTitleStyle;
    // NOTE: for headerButtons, headerBaseStyle needs to be re-applied after init, because if
    // field.baseStyle is set it overrides this default.  field.baseStyle is meant to apply to
    // cells only
    if (this.headerBaseStyle != null) propsFromGrid.baseStyle = this.headerBaseStyle;

    // headerButtonSrc / frozenHeaderButtonSrc
    // In multiple skins we use an ImgButton for our header buttons, and provide a custom src
    // property to get the appearance we want
    // These properties allow the overriding of that property for frozen and non frozen ImgButton
    // based header buttons

    if (this.headerButtonSrc != null) propsFromGrid.src = this.headerButtonSrc;
    // If 'frozenHeaderBaseStyle' / frozenHeaderTitleStyle is specified, store it on the button
    // defaults under a custom property name so we can apply it when creating buttons for frozen
    // fields
    if (this.frozenHeaderBaseStyle != null)
        propsFromGrid.frozenBaseStyle = this.frozenHeaderBaseStyle;
    if (this.frozenHeaderTitleStyle != null)
        propsFromGrid.frozenTitleStyle = this.frozenHeaderTitleStyle;
    if (this.frozenHeaderButtonSrc != null)
        propsFromGrid.frozenSrc = this.frozenHeaderButtonSrc;


    var properties = isc.addProperties({},
                this.headerButtonDefaults, propsFromGrid, this.headerButtonProperties);
    return properties;
},

// get properties for the headerButtons only (not the sort button)
getHeaderButtonProperties : function (props) {

    var properties = this.getButtonProperties();

    if (this.buttonTitleFunction == null) {
        this.buttonTitleFunction = function () {
            return this.parentElement.grid.getHeaderButtonTitle(this);
        };
    }

    isc.addProperties(
                properties,
                {
                    _canHover: true,

                    // header button selection is mutex

                    defaultRadioGroup: this.getID()+"_header_radioGroup",

                    // If this field is sortable, we want the button to be a radio button
                    // (Note - mutually-exclusive selection is handled by default by toolbars for
                    // radio type buttons)
                    getActionType : function (a,b,c,d) {
                        var header = this.parentElement,
                            grid;

                        if (header) grid = header.grid;

                        if (grid && isc.isA.ListGrid(grid)) {

                            var field = grid.fields[header.getButtonNumber(this)];

                            // if the list's canSort is false, or the field's
                            // 'canSort' property is false, then this field can't be sorted
                            var canSort = (grid.canSort != false);
                            if (canSort && field != null) canSort = (grid._canSort(field) != false);

                            if (canSort && grid.selectHeaderOnSort) return isc.Button.RADIO;
                        }

                        // We either are canSort:false, or couldn't get a pointer to the list.
                        return this.invokeSuper(this.getClassName(), "getActionType", a,b,c,d);

                    },
                    getTitle : this.buttonTitleFunction,

                    // Override handleMouseOver / handleMouseOut to show the headerMenuButton if appropriate
                    handleMouseOver : function (event) {
                        var grid = this.parentElement.grid;
                        if (grid.shouldShowHeaderMenuButton(this)) {
                            var hmb = grid._showHeaderMenuButton(this);


                            if (event) {
                                var hmbPageRect = hmb.getPageRect();
                                if (hmbPageRect[0] <= event.x && hmbPageRect[1] <= event.y &&
                                    event.x <= (hmbPageRect[0] + hmbPageRect[2]) &&
                                    event.y <= (hmbPageRect[1] + hmbPageRect[3]))
                                {

                                    this._delayedSuperHandleMouseOverTmrID = this._delayedSuper("handleMouseOver", arguments, null, 50);
                                    return;
                                }
                            }
                        }

                        return this.Super("handleMouseOver", arguments);
                    },
                    handleMouseOut : function () {
                        if (this._delayedSuperHandleMouseOverTmrID != null) {
                            isc.Timer.clear(this._delayedSuperHandleMouseOverTmrID);
                            delete this._delayedSuperHandleMouseOverTmrID;
                        }

                        if (!this.ns.EH._handlingTouchEventSequence()) {
                            var grid = this.parentElement.grid,
                                headerMenuButton = grid.headerMenuButton;
                            // If the HMB is showing over this button, hide it unless the user rolled
                            // onto it.
                            if (isc.EH.getTarget() != headerMenuButton) {
                                grid._hideHeaderMenuButton(this);
                            }
                        }

                        return this.Super("handleMouseOut", arguments);
                    },
                    handleHover : function () {
                        var header = this.parentElement,
                            grid = header.grid,
                            fieldNum = this.masterIndex;

                        // headerHover is not currently a cancellable event, though
                        // this would be easy to implement as such.
                        if (grid.headerHover != null) grid.headerHover(fieldNum);

                        var HTML = grid.defaultHeaderHoverHTML(fieldNum);
                        if (grid.headerHoverHTML) HTML = grid.headerHoverHTML(fieldNum, HTML);

                        grid._showHeaderHover(fieldNum, HTML);
                    }
                },
                props
    );

    return properties;

},

//>    @method    listGrid.makeHeader()    (A)
// Make the headerBar for the body columns.
// @group    gridHeader
//<
// This method will create a single header or a layout containing frozen and unfrozen header if
// there are frozen fields.
// It will not add as a member or draw() - the calling code is responsible for that.
makeHeader : function () {

    if (this.header != null) return;

    var header = this.header = this.makeHeaderForFields(this.normalFields || this.fields || []);
    // for autoTest APIs
    header.locatorParent = this;

    // make and add the sorter
    if (this.sorter == null) {
        this.makeCornerSortButton();
    }

    this.headers = [header];


    if (this.frozenFields) {


        var frozenHeader = this.frozenHeader =
                this.makeHeaderForFields(this.frozenFields, "visible", this.getID() + "_frozenHeader");

        if (this.freezeStart()) {
            this.headers.unshift(this.frozenHeader);
        } else {
            this.headers.add(this.frozenHeader);
        }

        var headerMembers= this.headers.duplicate();

        this.headerLayout = this.createAutoChild("headerLayout", {
            autoDraw: false,
            overflow:"hidden",
            height: this.getHeaderHeight(),
            members : headerMembers
        }, isc.HLayout);
    }

    var outerHeader = this.headerLayout || this.header;


    // We explicitly size the header to fit the specified width less scrollbar-width
    // so we don't want standard layout member-breadth-mgmt code to override this.
    outerHeader.inherentWidth = true;

    this.observe(outerHeader, "moved", function () { this.updateSorter(); });
    this.observe(outerHeader, "resized", function () { this.updateSorter(); });
},

// updateSorter - ensures the sorter is in the correct position and visible (or hidden) as appropriate.
updateSorter : function () {

    if (this.destroying || this.destroyed) return;
    if (!this.sorter || this.sorter.destroying || this.sorter.destroyed) return;

    if (this._overflowQueued || this._deferredOverflow) {
        this.delayCall("updateSorter");
        return;
    }

    // If this.showSort is unset, the sort button's visibility is determined by
    // leaveScrollbarGap, and whether the body has a vertical scrollbar.
    // This may have changed, so ensure the sorter is visible or hidden as appropriate.
    var showSorter = (this.header || this.headerLayout) && this._showSortButton();
    if (showSorter) {
        this.sorter.moveTo(this.getSorterLeft(), this.getSorterTop());
        this.sorter.setHeight(this.getHeaderHeight());
    } else {
        this.sorter.moveTo(0,0);
    }
    this.sorter.setVisibility(
        showSorter ? isc.Canvas.INHERIT : isc.Canvas.HIDDEN
    );
    // if the sorter wasn't created at init, it may not have been drawn yet
    if (showSorter) {
        if (this.isDrawn() && !this.sorter.isDrawn()) this.sorter.draw();
        else this.sorter.moveAbove(this.headerLayout || this.header);
    }

},

makeHeaderForFields : function (fields, overflow, ID) {
    // make and add the header
    var header = this.createHeader({

        ID:ID,
        grid:this,

        // both the header and LV are looking at the same set of objects for field/button
        // config, but they may have them in a different order due to distinct Array instances.
        buttons:fields.duplicate(),

        // don't actually permanently change the order of the members on drop - we'll handle it from
        // reorderField
        reorderOnDrop:false,

        // wipe the default height of the Toolbar.  Use null instead of headerHeight since, for
        // frozen columns, when there are multiple headers in an HLayout, we don't want heights
        // on each header, just on the overall layout (applied in layoutChildren())
        height:null,

        minMemberSize : this.minFieldWidth,

        // don't force the user to tab between the fields in the toolbar
        tabWithinToolbar:false,

        overflow: overflow || "hidden",


        _redrawWithParent:!this.fixedFieldWidths
    })

    // observe the scroll routine of the header to ensure we synch scrolling of the body if the
    // header is scrolled
    // This is necessary when we can natively tab to the button elements (or to focusProxies
    // positioned behind the button elements) because when the native focus goes to a button in
    // the header, the native behavior is to scroll the button into view
    // (if it's not  currently in view).
    this.observe(header, "scrollTo", function () { this.headerScrolled(); });

    return header;
},

//> @method listGrid.setFieldButtonProperties()
// Method to update properties on a field's header button at runtime.
// This property allows customization of any settable properties on the ListGridField's
// header button after it has been generated.  <smartgwt>Note that the provided Canvas should
// only have the minimal needed properties set on it, and should not be a Canvas that's already
// been drawn or added as the child of another widget.</smartgwt>
// @param name (String) Field to update
// @param properties (Canvas properties) new properties to apply to the header button
// @visibility external
//<
// The reasoning behind this is that developers will likely want to customize
// the "src" attribute [and other stuff like capSize] for img / stretchImg based
// header buttons.

setFieldButtonProperties : function (name, properties) {
    var field = this.getField(name)
    if (field != null) {
        var button = this.getFieldHeaderButton(this.getFieldNum(field));
        if (button != null) {
//            this.logWarn("Setting prop:" + button + " > " + this.echo(properties));
            button.setProperties(properties);
        }

    } else {
        field = this.completeFields.find("name", name);
    }
    if (field == null) {
        this.logWarn("setFieldButtonProperties() Unable to find specified field: " + name);
        return;
    }

    // Also copy the properties onto the field definition, so if the button is
    // destroyed / created later, we don't lose the settings.
    // Note that we have some special cases where we pick up a property from a field
    // as part of 'makeButton' and store it on the button under a different name, so
    // remap before applying to the field object. This means we wont (for example)
    // inadvertently clobber the field level "baseStyle" which has different meaning
    // for a ListGridField than for a header button.
    var maskedProps = isc.addProperties({}, properties);
    var remappedPropNames = {
        titleStyle:"headerTitleStyle",
        baseStyle:"headerBaseStyle"
    }
    for (var prop in remappedPropNames) {
        if (maskedProps[prop] != null) {
            maskedProps[remappedPropNames[prop]] = maskedProps[prop];
            delete maskedProps[prop];
        }
    }
    isc.addProperties(field, maskedProps);
},

getHeaderSpanConfig : function (name, spans) {
    if (spans == null) spans = this.headerSpans;
    if (spans == null) return;

    var spanConfig = spans.find("name", name);
    if (spanConfig != null) return spanConfig;
    // Nested header spans - check the "name" property of each span's sub-spans recursively

    for (var i = 0; i < spans.length; i++) {
        var span = spans[i];
        if (span.spans != null) {
            spanConfig = this.getHeaderSpanConfig(name, span.spans);
            if (spanConfig != null) return spanConfig;
        }
    }
},

//> @method listGrid.setHeaderSpanButtonProperties()
// Method to update properties on a headerSpan's header button at runtime.
// This property allows customization of any settable properties on the HeaderSpan's
// header button after it has been generated.
// @param name (String) +link{HeaderSpan.name,name} of span to update
// @param properties (Canvas properties) new properties to apply to the header button
// @visibility external
//<
setHeaderSpanButtonProperties : function (name, properties) {
    var spanConfig = this.getHeaderSpanConfig(name);
    if (!spanConfig) {
        this.logWarn("setHeaderSpanButtonProperties() - unable to locate span named: " + name
                     + this.getStackTrace());
        return;
    }
    if (spanConfig.liveObject) {
        spanConfig.liveObject.setProperties(properties);
    }

    // Also copy the properties onto the header-span definition, so if the button is
    // destroyed / created later, we don't lose the settings.
    // Note that we have some special cases where we pick up a property from a field
    // as part of 'makeButton' and store it on the button under a different name, so
    // remap before applying to the field object. This means we wont (for example)
    // inadvertently clobber the field level "baseStyle" which has different meaning
    // for a ListGridField than for a header button.
    var maskedProps = isc.addProperties({}, properties);
    var remappedPropNames = {
        titleStyle:"headerTitleStyle",
        baseStyle:"headerBaseStyle"
    }
    for (var prop in remappedPropNames) {
        if (maskedProps[prop] != null) {
            maskedProps[remappedPropNames[prop]] = maskedProps[prop];
            delete maskedProps[prop];
        }
    }
    isc.addProperties(spanConfig, maskedProps);
},

// For some span - goes through every nested inner-span and figures out for all branches
// of the tree the maximum depth. Used for apportioning sizing
getNestedSpanDepth : function (span) {
    if (span._nestedSpanDepth != null) return span._nestedSpanDepth;
    if (span.spans) {
        var innerSpanDepth = 0;
        for (var i = 0; i < span.spans.length; i++) {
            innerSpanDepth = Math.max(innerSpanDepth, this.getNestedSpanDepth(span.spans[i]));
        }
        span._nestedSpanDepth = (1 + innerSpanDepth);
        return span._nestedSpanDepth
    }
    // No inner spans so no nesting
    return 1;
},

//>    @method    listGrid.createHeader()    (A)
// Create a header object suitable for labelling rows or columns
// @group gridHeader
//<
createHeader : function (properties) {

    var grid = this;
    var canTabToHeader = this.canTabToHeader || (this.canTabToHeader == null && isc.screenReader);
    var headerProps = isc.addProperties(
        {
            // shouldDragScroll is overridden for the header as the standard implementation is
            // dependent on the header having scrollbars. This header scrolls in sync with the
            // grid body which owns the scrollbars.
            shouldDragScroll: function() { return grid && grid.body && grid.body.hscrollOn;},
            // override makeButton to ignore any baseStyle specified directly on the
            // field definition, and instead always use this.headerBaseStyle
            // Also pick up headerTitleStyle from the field if specified
            makeButton : function (button,a,b,c,d) {
                // this copy is necessary because we pass the actual objects from this.fields
                // into Toolbar.buttons, and hence Toolbar.makeButton receives them, and they
                // should not be modified
                button = isc.addProperties({}, button);
                // The ListGrid already mirrors the button's alignment if reverseRTLAlign is true.
                button.ignoreRTL = true;

                // if the ListGrid is managing the title clipping, then do not enable title
                // clipping in the button. Also set showClippedTitleOnHover to false.
                if (grid.clipHeaderTitles) {
                    button.clipTitle = false;
                    button.showClippedTitleOnHover = false;
                }


                button._redrawLabelWithMaster = true;

                button._getAfterPadding = function () {
                    if (this.masterIndex != null) {
                        var grid = this.grid,
                            field = grid.getField(this.masterIndex),
                            defaultAlign = (this.isRTL() && !grid.reverseRTLAlign
                                            ? isc.Canvas.RIGHT : isc.Canvas.LEFT);
                        if (field != null &&
                            grid.shouldLeaveHeaderMenuButtonSpace(field) &&
                            grid.shouldShowHeaderMenuButton(this) &&
                            (this.wrap || grid._shouldClipHeaderTitle(this.masterIndex)) &&
                            (field.align != defaultAlign ||
                             (grid.headerMenuButton && grid.headerMenuButton.isVisible() &&
                              grid.headerMenuButton.masterElement == this)))
                        {
                            return grid.headerMenuButtonWidth + 1;
                        }
                    }
                    return null;
                };

                if (button.headerTitleStyle) button.titleStyle = button.headerTitleStyle;
                else if (button.frozen && this.buttonProperties.frozenTitleStyle)
                    button.titleStyle = button.frozenTitleStyle;

                if (button.frozen && this.buttonProperties.frozenSrc)
                    button.src = this.buttonProperties.frozenSrc;

                // Override showIf to be unset - we already handle evaluating showIf on listGrid
                // fields - if it returned false the button will never be created - if it returned
                // true for the field we know we want to show the button rather than re-eval
                // field.showIf on button draw
                button.showIf = null;

                button.grid = grid;

                // If the field is marked as autoFitWidth, autoFitApproach:'title',
                // set overflow to visible
                var autoFit = grid.shouldAutoFitField(button),
                    approach,
                    fitTitle, fitValue;
                if (autoFit) {
                    approach = grid.getAutoFitWidthApproach(button);
                    if (approach == "both") {
                        fitTitle = true;
                        fitValue = true;
                    } else if (approach == "title") {
                        fitTitle = true;
                        fitValue = false;
                    } else {
                        fitTitle = false;
                        fitValue = true;
                    }
                }

                if (autoFit && fitTitle) {
                    button.overflow = "visible";
                    button.wrap = false;

                    button.resized = function () {
                        if (this.isDrawn() && this.grid) {
                            this.grid.headerButtonResized(this);
                        }
                    }
                } else {
                    // pick up the default wrap and apply it to the button

                    if (button.wrap == null) {
                        button.wrap = grid.wrapHeaderTitles;
                    }
                }
                // Also if we already calculated a field width based on auto-fit to values,
                // apply it to the header button:
                if (button._calculatedAutoFitWidth) {
                    if (button.width == null || button.width < button._calculatedAutoFitWidth) {
                        button.width = button._calculatedAutoFitWidth
                    }
                // Otherwise we must be auto-fitting to title.
                // Set the default width to min-field-width for the grid. Then the
                // title overflowing will expand the button to accomodate it.
                } else if (button.width == null && fitTitle) {
                    button.width = this.minFieldWidth || 1;
                }
                // This flag ensures that if the header button is implemented as an imgButton
                // we size the actual image to fit the overflowed size of the label rather than
                // sizing to explicit width
                button.sizeImageToFitOverflow = true;

                // If reverseRTLAlign is true, flip titles too
                if (grid.reverseRTLAlign && grid.isRTL()) {
                    if (button.align == isc.Canvas.LEFT) button.align = isc.Canvas.RIGHT;
                    else if (button.align == isc.Canvas.RIGHT) button.align = isc.Canvas.LEFT;
                }

                // ScreenReader support: Mark header buttons as
                // "rowheaders". Include haspopup:true if we support context menus
                // on them
                // Note - the "button" passed in is the field object so we can check on the
                // master-index directly

                if (button.masterIndex != null) {
                    button.ariaRole = "rowheader";
                    var hasPopup = button.showDefaultContextMenu != false &&
                                    grid.getHeaderContextMenuItems(button.masterIndex).length != 0;
                    if (hasPopup) {
                        button.ariaState = isc.addProperties({haspopup:true}, button.ariaState);
                    }
                }
                var button = this.Super("makeButton", [button,a,b,c,d]);

                // pick up field level headerBaseStyle if specified.

                if (button.headerBaseStyle) {
                    button.setBaseStyle(button.headerBaseStyle);
                // otherwise rely on the fact that headerBaseStyle is passed to us as part
                // of our default 'button' config
                } else {
                    if (button.frozen && this.buttonProperties.frozenBaseStyle)
                        button.setBaseStyle(this.buttonProperties.frozenBaseStyle);
                    else if (this.buttonProperties.baseStyle != null)
                        button.setBaseStyle(this.buttonProperties.baseStyle);
                }

                return button;
            },
            // Note: make all headerButton items members of the same radio group for
            // click-sort.  Previously this was taken care of by an auto-init of the radioGroup
            // in the Toolbar class - which was in turn based on the value returned by
            // getActionType() method on the button.  Since our getActionType override assumes a
            // parent chain and Toolbar no longer guarantees that this will be set up before
            // the radio group is auto-inited, we just pass one in here.
            buttonProperties:this.getHeaderButtonProperties(),

            border:this.headerBorder,
            styleName:this.headerBarStyle,
            backgroundColor:this.headerBackgroundColor,
            backgroundImage:this.headerBackgroundImage,

            canResizeItems:this.canResizeFields,
            canReorderItems:this.canReorderFields,
            dontObserve:false,

            childVisibilityChanged : function () {
                this.Super("childVisibilityChanged", arguments);
                this._sizeSpans();
            },

            // use this internal API rather than childResized/resized - this leaves those
            // public APIs available for other use, plus reduces the number of reflows by
            // dealing with the layout directly rather than responding to a cascae of
            // childResize()s
            _layoutChildrenDone : function () {
                this.Super("_layoutChildrenDone", arguments);
                this._sizeSpans();
            },

            addSpan : function (span) {

                if (!this._spans) this._spans = [];
                this._spans.add(span);

                // unless the user passed an explicit, always size the span to the height of
                // the header
                if (this.isDrawn()) {
                    this._sizeSpan(span);
                    this.addChild(span);
                }
            },
            _sizeSpans : function () {
                if (!this._spans || !this.isDrawn()) return;
                // We explicitly suppress this method when resizing field buttons
                // in _adjustSpans (where we've already handled setting span button sizes)
                if (this._suppressSizeSpans) return;

                // diable instantRelayout while sizing the spans.

                var instantRelayout = this.instantRelayout;
                this.instantRelayout = false;

                for (var i = 0; i < this._spans.length; i++) this._sizeSpan(this._spans[i]);
                this.instantRelayout = instantRelayout;
                delete this._sizingSpans;
            },
            // members == actual members of the layout so they correspond to fields in the
            // grid.
            // if we have nested spans, this method will will return all fields covered by the
            // span and it's child-spans
            _getSpannedMembers : function (span) {
                // use the cached _spanButtons if we've already calculated it.
                // This cache is dropped when we add or remove buttons in the
                // toolbar, so we'll lazily recreate it here.
                if (span._spansButtons != null) {
                    return span._spansButtons;
                }
                var fields = grid.getSpannedFields(span);

                var spansButtons = [],
                    count = fields.length,
                    empty = true;
                for (var i = 0; i < this.members.length; i++) {
                    if (span._spansFields[this.members[i].name] == true) {
                        empty = false;
                        spansButtons[spansButtons.length] = this.members[i];
                        count--;
                    }
                    if (count == 0) break;
                }
                span._spansButtons = spansButtons;
                return empty ? null : spansButtons;
            },

            getNestedSpanDepth : function (span) {
                return grid.getNestedSpanDepth(span);
            },
            _sizeSpan : function (span) {
                if (!this.isDrawn()) {
                    return;
                }

                if (span._spanAutoSizeHeight) {
                    if (grid.autoFitHeaderHeights) {
                        span.setHeight(
                            Math.max(grid.getHeaderButtonDefaultHeight(span.spanConfig),
                                     grid.getHeaderButtonMinHeight(span.spanConfig))
                        );
                    } else {

                        // For nested spans, figure out how many parents / children we have so we can
                        // size and align vertically.
                        // Note: We support "asymmetrically nested" spans - for example a top-span
                        // containing one child span which has fields and another which has an additional
                        // layer of nesting.
                        // Therefore we need to figure out the maximum nested depth and divide
                        // the available space to get a consistent appearance.
                        // Note 2: We support allowing header titles to expand header buttons
                        // vertically. When this occurs the header as a whole has to expand -
                        // already handled via getHeaderHeight(). When this occurs we'll
                        // size the actual header buttons to the max of the calculated
                        // height (specified headerHeight/stack depth) and overflowed height
                        // and rely on the fact that getHeaderHeight() already sized the
                        // header as a whole large enough to accomodate this.
                        var currentSpan = span,
                            availableSpace = grid.headerHeight;
                        while (currentSpan.parentSpan != null
                            && currentSpan.parentSpan.liveObject)
                        {
                            var parentSpan = currentSpan.parentSpan.liveObject;
                            availableSpace -= parentSpan.getVisibleHeight();
                            currentSpan = parentSpan;
                        }

                        // Figuring out the max-depth of nesting of child spans is recursive, handled
                        // in a separate method
                        var childDepth = this.getNestedSpanDepth(span);
                        var height = Math.floor(availableSpace / (childDepth + 1));

                        if (height <= 0) height = 1;

                        span.setHeight(height);
                    }
                }
                if (span.parentSpan != null && span.parentSpan.liveObject != null) {
                    span.setTop(span.parentSpan.liveObject.getBottom());
                }
                var autoSizeWidth = grid.autoSizeHeaderSpans;

                var spannedMembers = this._getSpannedMembers(span);

                // buttons are removed from the layout when hidden, so don't break on the lack
                // of spanned members
                var allHidden = spannedMembers == null || spannedMembers.length == 0;
                if (allHidden) {
                    if (this.isVisible()) {
                        span.hide();
                    }
                    return;
                }

                var firstVisibleMember,
                    totalVisibleMembers = 0,
                    spanWidth = 0
                ;
                for (var i = 0; i < spannedMembers.length; i++) {
                    var spannedMember = spannedMembers[i];

                    // disable header reorder drop over spanned columns
                    spannedMember.canReorder = false;

                    // if we're spanned, spans will stack on top, force layoutAlign: bottom for
                    // spanned header
                    spannedMember.layoutAlign = "bottom";

                    if (spannedMember.visibility != isc.Canvas.HIDDEN) {
                        totalVisibleMembers++;
                        spanWidth += spannedMember.getVisibleWidth();
                        if (!firstVisibleMember || this.isRTL()) firstVisibleMember = spannedMember;
                    }
                }

                if (firstVisibleMember) {
                    span.setVisibility(isc.Canvas.INHERIT);
                    span.setLeft(firstVisibleMember.getLeft());
                    span.setWidth(spanWidth);
                    // Force an immediate redraw if the span is dirty so getVisibleWidth()
                    // below picks up on the correct new size which may be effected by
                    // the innerHTML
                    if (span.isDirty()) span.redraw();
                    if (autoSizeWidth) {
                        span.setOverflow(isc.Canvas.VISIBLE);
                        var visibleWidth = span.getVisibleWidth();
                        if (visibleWidth > spanWidth) {
                            span.setWidth(visibleWidth);
                            var delta = visibleWidth - spanWidth;

                            for (var i = 0; i < spannedMembers.length; i++) {
                                var spannedMember = spannedMembers[i];
                                if (spannedMember.visibility != isc.Canvas.HIDDEN) {
                                    var itemDelta = Math.ceil(delta / totalVisibleMembers);
                                    delta -= itemDelta;
                                    totalVisibleMembers -= 1;
                                    spannedMember.resizeBy(itemDelta);
                                }
                            }
                            grid._updateFieldWidths("autoSizeHeaderSpans: Caused fields to expand");
                        }
                        span.setOverflow(isc.Canvas.HIDDEN);
                    }
                }
            },

            _adjustSpans : function (resizeFieldsOnly) {
                if (!this._spans) return;

                var recursive = this._adjustingSpans;
                this._adjustingSpans = true;

                if (!resizeFieldsOnly) this._sizeSpans();
                var thisHeight = this.getVisibleHeight();

                // Flag to suppress reacting to reflow for sizing header buttons by
                // re-sizing spans!
                this._suppressSizeSpans = true;
                // also disable instantRelayout while sizing the field buttons

                var instantRelayout = this.instantRelayout;
                this.instantRelayout = false;

                for (var i = 0; i < this._spans.length; i++) {
                    var span = this._spans[i];
                    if (!this.resizeFieldsOnly) this.addChild(span);

                    var spannedMembers = this._getSpannedMembers(span);
                    if (!spannedMembers) continue;

                    for (var j = 0; j < spannedMembers.length; j++) {
                        var header = spannedMembers[j],
                            headerHeight = header.getVisibleHeight()
                        ;

                        // For nested spans, figure out the total height of nested spans,
                        // and size the field to take up the rest of the header's height

                        var parentSpan = grid.spanMap[header.name],
                            spanHeight = 0;
                        do {
                            // 'liveObject' should have been created if we have
                            // a button for the children!
                            if (parentSpan.liveObject == null) break;
                            spanHeight += parentSpan.liveObject.getVisibleHeight();
                            parentSpan = parentSpan.parentSpan;
                        } while (parentSpan != null)
                        header.setHeight(thisHeight - spanHeight);
                    }
                    span.bringToFront();
                }
                this.instantRelayout = instantRelayout;
                if (!recursive) {
                    delete this._suppressSizeSpans;
                    delete this._adjustingSpans;
                }
            },

            removeButtons : function (buttons) {
                if (grid.headerSpans) grid._removeHeaderSpans(buttons, this);
                this.Super("removeButtons", arguments);
            },
            addButtons : function (buttons, position) {
                this.Super("addButtons", arguments);
                if (grid.headerSpans) grid._addHeaderSpans(buttons, this);
                this._adjustSpans();
            },


            draw : function () {
                if (this._spans && grid.unspannedHeaderVAlign) {
                    for (var i = 0; i < this.buttons.length; i++) {
                        var button = this.buttons[i],
                            isSpanned = grid.spanMap[button.name] != null;
                        if (!isSpanned) {
                            button.valign = grid.unspannedHeaderVAlign;
                        }
                    }
                }
                this.Super("draw", arguments);
                this._adjustSpans();

            }

            // AutoTest subsytem APIs
            // Allow mapping header buttons by field name rather than just position in the
            // header!


            ,getStandardChildLocator : function (canvas) {
                var fieldNum = canvas.masterIndex,
                    grid = this.grid;
                if (fieldNum != null && grid != null) {
                    var fieldName = grid.getFieldName(fieldNum);
                    return isc.AutoTest.createLocatorFallbackPath("headerButton", {fieldName:fieldName});
                }

                return this.Super("getStandardChildLocator", canvas);
            },

            getChildFromLocatorSubstring : function (substring, index, locatorArray, configuration)
            {
                // Don't interfere with standard 'autoChild' / 'scrollbar' identifiers
                if (isc.isA.Canvas(this[substring])) return this[substring];

                var parsedPath = isc.AutoTest.parseLocatorFallbackPath(substring);
                if (this.grid && parsedPath && parsedPath.name == "headerButton") {

                    var fieldName = parsedPath.config.fieldName,
                        fieldNum = this.grid.getFieldNum(fieldName),
                        header = this.grid.getFieldHeader(fieldNum);

                    // field, or header may have been hidden
                    if (fieldNum == -1 || header == null) {
                        this.logWarn("fieldName:" + fieldName + ", gave fieldNum:" + fieldNum
                                + " unable to find header button for this field.");
                        return;
                    }
                    if (header != this) {
                        this.logWarn("AutoTest locator string specified this header for field "
                            + fieldName + " but that header button is contained in header "
                            + header + ". This can happen if headers have been frozen / "
                            + "unfrozen since the locator string was created. Redirecting to that widget.");
                        return header.getChildFromLocatorSubstring(substring);
                    }
                    // return locator header button state value if requested
                    if (configuration && configuration.attribute == isc.Canvas._$Value) {
                        var field = this.grid.fields[fieldNum];
                        if (fieldNum == this.grid.getCheckboxFieldPosition()) {
                            configuration.value = !!field._allSelected;
                        } else configuration.value = field.sortDirection;
                    }
                    return this.members[this.grid.getLocalFieldNum(fieldNum)];
                }

                return this.Super("getChildFromLocatorSubstring", arguments);
            }


        }, properties);
    // if cantabToHeader is false, suppress tabbing to the header!
    if (!canTabToHeader) {
        headerProps.tabIndex = -1;
    }
    if (this.headerButtonConstructor != null)
        headerProps.buttonConstructor = this.headerButtonConstructor;

    var header = this.createAutoChild("header", headerProps,

        isc.Toolbar);

    // if the canReorderFields is true, observe the reorderItem method of the header
    if (header.canReorderItems && !header.dontObserve) {
        this.observe(header, "itemDragReordered", function (itemNum, newPosition) {
            this.headerDragReordered(itemNum, newPosition, header);
        });
        // NOTE: we don't actually do anything in the LV until reorder completes in the header
    }

    // if canResizeFields is true, observe the resizeItem and dragResize* methods of the header
    if (header.canResizeItems && !header.dontObserve) this._observeHeaderResize(header);

    if (this.headerSpans) this._addHeaderSpans(properties.buttons, header);
    return header;
},

// If we're showing overflow:"visible" header buttons (for auto-fitting to titles), we need to
// react to them resizing due to a title change (such as showing the sort arrow!)
headerButtonResized : function (button) {
    if (this._suppressAutoFitToTitle) return;

    // If we're doing an "autoFit" to the field content - this is similar to a drag-resize - no need to react.
    if (this._autoFittingField != null && this._autoFittingField == this.getField(button.name)) {
        return;
    }


    this._updateFieldWidths("header button resized");
},

// If a developer declares header-spans, fields that are spanned by the same header need to be
// next to each other (obviously).
// This means we may need to reorder the fields array as specified in the code.
// If we actually change anything let's log a warning - developers may be confused as to why
// the fields are not matching the specified positions.

reorderFieldsForHeaderSpans : function (spans) {
    if (spans == null) spans = this.headerSpans;
    if (spans == null) return;
    var orderChanged = false;
    for (var i = 0; i < spans.length; i++) {
        var span = spans[i],
            fields = this.getSpannedFields(span);
        if (fields == null || fields.length == 0 || fields.length == 1) continue;

        var gridFields = this.fields,
            length = fields.length,
            positions = [],
            frozen = null;

        for (var ii = 0; ii < length; ii++) {
            var pos = this.completeFields.findIndex("name", fields[ii]);
            // fieldName may not be present (due to setFields call etc)
            if (pos == -1) continue;

            // Frozen fields - currently we just warn if the developer attempted to
            // mix frozen and unfrozen fields within a span.
            if (frozen == null) {
                frozen = !!(this.completeFields[pos].frozen);
            } else {
                if (frozen != !!(this.completeFields[pos].frozen)) {
                    this.logWarn("HeaderSpan with title:" + span.title +
                        " has both frozen and unfrozen fields specified. This is not supported.");

                }
            }
            positions.add({
                field:fields[ii],
                pos:this.completeFields.findIndex("name", fields[ii])
            });
        }

        positions.sortByProperty("pos", Array.ASCENDING);

        if (positions.length > 0 &&
            (positions.last().pos - positions.first().pos) > (positions.length-1))
        {
            this.logWarn("HeaderSpan with title " + span.title
                + " spans fields that are not adjacent in the specified fields array. "
                + "Reordering fields such that they are adjacent and can be spanned.");
            // We don't attempt to maintain the relative order specified in the fields array.
            // This would be difficult with nested spans since we'd potentially have to
            // reorder inner-spans within the parent depending on where fields were defined in the
            // fields array.
            var targetPos = positions.first().pos;
            for (var ii = 0; ii < length; ii++) {
                var field = fields[ii],
                    currentPos = this.completeFields.findIndex("name", field);

                if (currentPos == -1) continue;

                if (targetPos != currentPos) {
                    orderChanged = true;
                    this.completeFields.slide(currentPos, targetPos);
                }
                targetPos += 1;
            }
        // This implies that the fields are all adjacent.
        // Howeve if we have *nested* spans we also need to verify they're adjacent within
        // their various inner-spans
        } else if (span.spans) {
            orderChanged = this.reorderFieldsForHeaderSpans(span.spans);
        }
    }
    return orderChanged;
},

// Make a reverse map of fieldNames to header spans so we can look up
// the span (hierarchy) for a field rapidly.

buildSpanMap : function (spans, parentSpan, forPrinting, spanMap) {
    if (spanMap == null) spanMap = {};
    if (spans == null) spans = this.headerSpans;

    for (var i = 0; i < spans.length; i++) {
        var headerSpanConfig = spans[i];
        if (parentSpan) headerSpanConfig.parentSpan = parentSpan;
        if (headerSpanConfig.spans) {
            this.buildSpanMap(headerSpanConfig.spans, headerSpanConfig, forPrinting, spanMap);
        } else if (headerSpanConfig.fields) {
            // If we're printing, filter out the fields that should not be printed.
            if (forPrinting == true) {
                headerSpanConfig = isc.addProperties({}, headerSpanConfig);
                headerSpanConfig.fields = headerSpanConfig.fields.duplicate();
                for (var j = 0; j < headerSpanConfig.fields.length; ){
                    var fieldName = headerSpanConfig.fields[j];
                    var field = this.getField(fieldName);
                    if (field == null || field.shouldPrint == false) {
                        headerSpanConfig.fields.removeAt(j);
                        // don't increment `j'
                    } else {
                        ++j;
                    }
                }
                headerSpanConfig.fields.removeAll(headerSpanConfig.fields.findAll("shouldPrint", false));
            }
            for (var j = 0; j < headerSpanConfig.fields.length; j++) {
                spanMap[headerSpanConfig.fields[j]] = headerSpanConfig;
            }
        } else {
            this.logWarn("ListGrid passed unexpected header span object with " +
                "neither fields nor spans specified:" + this.echo(headerSpanConfig));
        }
    }

    return spanMap;
},


getSpannedFields : function (span, canHideFieldsOnly) {

    if (span == null) return [];

    if (span.fields) {
        var fields = span.fields;
        // This method is used by the column picker menu - if passed the
        // canHideFieldsOnly flag, ignore any fields marked as canHide:false
        if (canHideFieldsOnly) {
            var finalFields = [];
            for (var i = 0; i < span.fields.length; i++) {
                var fieldObj = (this.completeFields || this.fields).find("name", span.fields[i]);
                if (!fieldObj || fieldObj.canHide == false) {
                    continue;
                }
                finalFields.add(span.fields[i]);
            }
            fields = finalFields;
        }
        return fields;
    }

    var fields = [];
    if (span.spans) {
        for (var i = 0; i < span.spans.length; i++) {
            fields.addList(this.getSpannedFields(span.spans[i], canHideFieldsOnly));
        }
    }
    return fields;
},


_headerSpanVisible : function (fieldNames, span) {
    if (span.spans) {
        for (var i = 0; i < span.spans.length; i++) {
            if (this._headerSpanVisible(fieldNames, span.spans[i])) return true;
        }
        return false;
    } else {
        return (fieldNames.intersect(span.fields).length > 0)
    }
},


// Called recursively to create header-span-buttons and to layout
_addHeaderSpans : function (fields, header, headerSpans, parentSpan) {

    // For convenience, allow calling directly for top level spans.
    if (headerSpans == null) headerSpans = this.headerSpans;

    // find headerSpans that match our set of fields
    var fieldNames = fields.getProperty(this.fieldIdProperty);

// so headerSpans will now be an array of all the spans we actually care about,
// with the inner spans first in the list....

    for (var i = 0; i < headerSpans.length; i++) {
        var headerSpanConfig = headerSpans[i];

        // We don't support both fields and spans on a span - if we hit this
        // warn and drop the fields.
        if (headerSpanConfig.fields != null && headerSpanConfig.spans != null) {
            this.logWarn("ListGrid specified with headerSpan config object containing both " +
                "spans and fields. This is not supported - spanned fields must be all frozen or " +
                "all not frozen. Span Configuration:\n" +
                    this.echo(headerSpanConfig));

            headerSpanConfig.fields = null;
        }

        // If the header span doesn't match any visible fields, skip it
        if (!this._headerSpanVisible(fieldNames, headerSpans[i])) continue;

        // allow a canvas to be passed in

        // default to using the same component that header buttons are made of - this
        // way we get a consistent cross-skin look that's a reasonable default
        var grid = this,
            span = this.createHeaderSpan(header, headerSpanConfig);

        header.addSpan(span);

        // recursively build nested spans and add them to the header as well.
        if (span.spans) {
            this._addHeaderSpans(fields, header, span.spans, span);
        }
    }
},

createHeaderSpan : function (header, headerSpanConfig) {
    var grid = this,
        span = headerSpanConfig.liveObject;
    // If the span is destroyed or destroying, rebuild it.

    if (span == null || span.destroyed || span.destroying || span._pendingDestroy) {
        var config = headerSpanConfig,
            dynamicProperties = isc.addProperties(this.getButtonProperties(), {
            _constructor: header.buttonConstructor,
            height: this.headerSpanHeight,
            // typical to align the span text in the center
            align: "center",
            valign: this.headerSpanVAlign,
            showRollOver: false,
            showContextMenu : function () {
                return grid.headerSpanContextClick(this);
            }
        }, config);

        // Support custom per-span styling:

        // title style useful for stretchImgButtons
        // Note that if you have stretchImgButton based headers, in order to customize
        // appearance you'd probably also want to specify custom src and capSize
        // This is actually supported in both ListGridFields and HeaderSpans, but
        // not explicitly documented.
        if (config.headerTitleStyle) {
            dynamicProperties.titleStyle = config.headerTitleStyle;
        }

        // BaseStyle for simple buttons (more recent skins)
        if (config.headerBaseStyle != null) {
            dynamicProperties.baseStyle = config.headerBaseStyle;
        }

        // override the liveObject title with headerTitle, if it's present
        if (config.headerTitle != null) dynamicProperties.title = config.headerTitle;
        span = config.liveObject = this.createAutoChild("headerSpan", dynamicProperties);

        // auto-size span height unless an explicit height is set somewhere
        var height = config.height ||
                     (this.headerSpanProperties ? this.headerSpanProperties.height : null) ||
                     (this.headerSpanDefaults ? this.headerSpanDefaults.height : null) ||
                     this.headerSpanHeight;

        span._spanAutoSizeHeight = (height == null);
    }


    span._spansFields = {};
    var fields = this.getSpannedFields(headerSpanConfig);
    for (var ii = 0; ii < fields.length; ii++) {
        span._spansFields[fields[ii]] = true;
    }

    // drop the cached live buttons we actually span - we'll lazily regenerate when they're required.
    span._spansButtons = null;

    span.spanConfig = headerSpanConfig;

    return span;
},

_removeHeaderSpans : function (buttons, header) {
    // find headerSpans that match our set of fields
    var headerSpans = [],
        fieldNames = buttons.getProperty(this.fieldIdProperty)
    ;
    for (var i = 0; i < fieldNames.length; i++) {
        var span = this.spanMap[fieldNames[i]];
        while (span != null && span.liveObject != null) {
            span.liveObject._spansButtons = null;
            span = span.parentSpan;
        }
    }

    // no need to actually remove the span from the header - we'll lazily hide it as part of
    // this._sizeSpans()
},


//> @attr listGrid.showHeaderSpanTitlesInFormulaBuilder (Boolean : true : IRW)
// If this grid has specified +link{listGrid.headerSpans,headerSpans}, should field titles be
// prefixed with the titles of the headerSpans in which they are contained when
// using the +link{FormulaBuilder} or +link{SummaryBuilder}.
// @see listGrid.formulaBuilderSpanTitleSeparator
// @visibility external
//<
showHeaderSpanTitlesInFormulaBuilder:true,

//> @attr listGrid.formulaBuilderSpanTitleSeparator (String : " - " : IRW)
// If this grid has specified +link{listGrid.headerSpans,headerSpans}, and
// +link{showHeaderSpanTitlesInFormulaBuilder,showHeaderSpanTitlesInFormulaBuilder} is true,
// this string will be inserted between the headerSpan title(s) and the field title in the
// field chooser grid in the +link{FormulaBuilder} and +link{SummaryBuilder}.
// @group i18nMessages
// @visibility external
//<
formulaBuilderSpanTitleSeparator:" - ",

//> @attr listGrid.showHeaderSpanTitlesInSortEditor (Boolean : true : IRW)
// If this grid has specified +link{listGrid.headerSpans,headerSpans}, should field titles be
// prefixed with the titles of the headerSpans in which they are contained when
// using the +link{MultiSortDialog,multi-sort editor}.
// @see listGrid.sortEditorSpanTitleSeparator
// @visibility external
//<
showHeaderSpanTitlesInSortEditor:true,

//> @attr listGrid.sortEditorSpanTitleSeparator (String : " - " : IRW)
// If this grid has specified +link{listGrid.headerSpans,headerSpans}, and
// +link{showHeaderSpanTitlesInSortEditor,showHeaderSpanTitlesInSortEditor} is true, this
// string will be inserted between the headerSpan title(s) and the field title in the field
// chooser grid on the +link{MultiSortDialog,multi-sort editor}
// @group i18nMessages
// @visibility external
//<
sortEditorSpanTitleSeparator:" - ",

//> @attr listGrid.showHeaderSpanTitlesInHiliteEditor (Boolean : true : IRW)
// If this grid has specified +link{listGrid.headerSpans}, should field titles be
// prefixed with the titles of the headerSpans in which they are contained when
// using the +link{databoundComponent.editHilites,hilite editor}.
// @see listGrid.hiliteEditorSpanTitleSeparator
// @visibility external
//<
showHeaderSpanTitlesInHiliteEditor:true,

//> @attr listGrid.hiliteEditorSpanTitleSeparator (String : " - " : IRW)
// If this grid has specified +link{listGrid.headerSpans}, and
// +link{showHeaderSpanTitlesInHiliteEditor} is true, this string will be inserted
// between the headerSpan title and the field title in the hiliteEditor field chooser
// grid.
// @group i18nMessages
// @visibility external
//<
hiliteEditorSpanTitleSeparator:" - ",

// When getting the set of fields to show in a hilite editor, prefix with
// header span titles where applicable.
getHiliteCriteriaFields : function () {

    var fields = this.Super("getAllFields", arguments);
    if (!fields) return;
    fields = fields.duplicate();
    if (this.showHeaderSpanTitlesInHiliteEditor && this.headerSpans != null) {
        for (var i = 0; i < fields.length; i++) {
            var span = this.spanMap[fields[i].name],
                isSpanned = false,
                spans = [{title:this.getFieldTitle(fields[i])}];
            while (span != null) {
                isSpanned = true;
                spans.addAt(span, 0);
                span = span.parentSpan;
            }
            if (isSpanned) {
                var title = spans.getProperty("title").join(this.hiliteEditorSpanTitleSeparator);

                fields[i] = isc.addProperties({}, fields[i],
                    {title:title});
            }
        }
    }
    return fields;
},

//>    @method    listGrid.dirtyHeader()    (A)
//            let the header know that it should redraw when we get a chance
//        @group    gridHeader
//<
dirtyHeader : function () {
    if (this.header) this.header.markForRedraw();
},

// update an existing header to match the current set of fields.
updateHeader : function () {

    // wipe out the old header if there was one
    var oldHeader = this.frozenHeader ? this.headerLayout : this.header;

    if (oldHeader) {
        // defer destruction to prevent possible IE slow script dialogs for large number of columns
        oldHeader.markForDestroy();
        // need to clear out IDs to prevent ID collision warnings as we recreate the header (it
        // will have the same ID).



        oldHeader.clear();
        if (this.showHeader) {
            var toolbars = [this.frozenHeader,this.header];
            for (var i = 0; i < toolbars.length; i++) {
                if (toolbars[i] == null) continue;
                if (toolbars[i]._spans != null) {
                    for (var ii = 0; ii < toolbars[i]._spans.length; ii++) {
                        var span = toolbars[i]._spans[ii];
                        if (span) span.deparent();
                    }
                }
            }
        }

        oldHeader.clearIDs();

        // clear the pointer to the headerMenuButton (if present) because it is going to be destroyed
        this.headerMenuButton = null;
    }

    this.header = this.frozenHeader = this.headerLayout = null;

    // create a new header if so configured
    var showHeader = this.showHeader,
        headerIndex;
    if (showHeader) {
        headerIndex = this.gridComponents.indexOf(this._$header);
        if (headerIndex == -1) {
            showHeader = false;
            this.logWarn("showHeader set to true, but header not included in grid header components." +
                " The header will not be shown.");
        }
    }
    if (showHeader) {
        // If we're auto-fitting fields to their titles vertically this process may change
        // the header height - drop the cached header heights now.
        // This means that getHeaderHeight() will lazily recalculate the correct value and
        // we'll reflow to accomodate it.
        if (this.autoFitHeaderHeights) {
            this.dropCachedHeaderButtonHeights();
        }

        this.makeHeader();
        // createChildren builds the standard grid components and adds them as members.
        // If this has already occurred, we'll need to explicitly add the header as
        // a member here. Note that calling code typically will cause a resize, etc via
        // a call to layoutChildren or _updateFieldWidths()
        if (this.isDrawn()) {
            this.updateGridComponents();
        }
    // We're hiding the sorter here, since _updateFieldWidths handles positioning and showing
    // the sorter if appropriate
    // sorter may be undef (if showSorter is false)
    } else if (this.sorter) {
        this.sorter.hide();
    }


},

//> @method listGrid.setHeaderHeight()
// Modify the height of a listGrid. To hide the header set height to zero.
// @param height (number) new height for the header
// @visibility external
// @group sizing, gridHeader
//<
_$headerHeightChanged:"header height changed",
setHeaderHeight : function (height) {
    var oldHeight = this.headerHeight;

    if (oldHeight == height) return;

    this.headerHeight = height;

    this._updateHeaderHeight(oldHeight == 0);
},

_updateHeaderHeight : function (wasHidden) {
    // No need to update the header if we're not showing it.
    if (this.showHeader == false) return;

    if (!this.header && this.getHeaderHeight() > 0) {
        // Don't build the header if it's not included in the gridComponents array!
        var headerIndex = this.gridComponents.indexOf("header");
        if (headerIndex == -1) {
            return;
        }
        // If we're not drawn the header will get created when we get drawn
        // Otherwise create and draw it now.
        if (this.isDrawn()) {
            this.updateGridComponents();
        }
    } else {

        if (wasHidden) this.updateHeader();
        // handles resizing (and showing/hiding if appropriate)
        this.layoutChildren(this._$headerHeightChanged)
    }

    this.updateSorter();
    if (this.headerSpans) {

        this.header._adjustSpans(true);
    }
},

//> @method listGrid.setShowHeader()
// Show or hide the ListGrid header.
// @param show (boolean) true to show the header, false to hide it.
// @visibility external
// @group gridHeader
//<
_$headerVisibilityChanged:"header visibility changed",
setShowHeader : function (show) {
    if (show == this.showHeader) return;

    this.showHeader = show;

    this.updateHeader();
    this.layoutChildren(this._$headerVisibilityChanged);
},

// Header Menu button
// -------------------------------------------------------------------------------------------

shouldShowHeaderMenuButton : function (header, skipTouchEventCheck) {

    if (!skipTouchEventCheck && this.ns.EH._handlingTouchEventSequence()) return false;

    var shouldShow;
    if (header.showDefaultContextMenu == false) {
        shouldShow = false;
    }

    else if (header.showHeaderContextMenuButton != null) {
        shouldShow = header.showHeaderContextMenuButton;
    } else {
        shouldShow = this.showHeaderMenuButton;
    }

    if (this.showHeaderContextMenu && shouldShow && header.masterIndex != null) {
        // suppress the headerContextMenuButton if we won't actually show the headerContextMenu
        var items = this.getHeaderContextMenuItems(header.masterIndex);
        return (items && items.length > 0);
    }
    return false;
},

// getHeaderMenuButton
// creates the headerMenuButton autoChild. Lazily creates the button if it hasn't been shown yet
getHeaderMenuButton : function (button) {
    // after certain column operations like freeze, the headerMenuButton may be destroyed
    // so recreate it if so.
    if (!this.headerMenuButton || this.headerMenuButton.destroyed) {
        var dynamicDefaults = {
            snapTo:(this.isRTL() ? "L" : "R"),
            resizeFrom:(this.isRTL() ? "L" : "R")
        };

        // don't default anything to explicit null!
        if (this.headerMenuButtonHeight) dynamicDefaults.height = this.headerMenuButtonHeight;
        if (this.headerMenuButtonWidth) dynamicDefaults.width = this.headerMenuButtonWidth;
        if (this.headerMenuButtonIcon) dynamicDefaults.icon = this.headerMenuButtonIcon;
        if (this.headerMenuButtonIconHeight) {
            dynamicDefaults.iconHeight = this.headerMenuButtonIconHeight;
        }
        if (this.headerMenuButtonIconWidth) {
            dynamicDefaults.iconWidth = this.headerMenuButtonIconWidth;
        }
        if (this.headerMenuButtonBaseStyle) {
            dynamicDefaults.baseStyle = this.headerMenuButtonBaseStyle;
        }
        if (this.headerMenuButtonTitleStyle) {
            dynamicDefaults.titleStyle = this.headerMenuButtonTitleStyle;
        }
        if (this.headerMenuButtonSrc) dynamicDefaults.src = this.headerMenuButtonSrc;

        this.createAutoChild("headerMenuButton", dynamicDefaults, "Button", true);
    }



    this.headerMenuButton.masterIndex = button.masterIndex;

    // correct the position of the header menu button so that it is not obscured by
    // the sorter button or any length of the button on the right that is clipped by
    // the viewport.
    var hmbSnapOffsetLeft = 0,
        fieldIsFrozen = this.fieldIsFrozen(button.masterIndex);
    if (!fieldIsFrozen && this.body != null) {
        var viewportWidth = this.body.getViewportWidth(),
            buttonScrollWidth = button.getScrollWidth();
        if (this.isRTL()) {

        } else {
            var buttonLeft = button.getLeft(),
                offset = buttonLeft - this.body.getScrollLeft(),
                hiddenLength = offset + buttonScrollWidth - viewportWidth;
            if (this._showSortButton()) hiddenLength += this._getSorterWidth();
            if (this.body.vscrollOn) hiddenLength -= this.body.getScrollbarSize();
            if (hiddenLength > 0) {
                hmbSnapOffsetLeft = -hiddenLength;
            }
        }
    } else if (fieldIsFrozen && this.frozenBody != null) {
        var viewportWidth = this.getViewportWidth(),
            buttonScrollWidth = button.getScrollWidth();

        if (this.isRTL()) {

        } else {
            var hiddenLength;
            if (this.sorter != null && this._showSortButton()) {
                hiddenLength = button.getPageLeft() + buttonScrollWidth - this.sorter.getPageLeft();
            } else {
                hiddenLength = button.getLeft() + buttonScrollWidth - viewportWidth;
                if (this.frozenBody.vscrollOn) hiddenLength -= this.frozenBody.getScrollbarSize();
            }

            if (hiddenLength > 0) {
                hmbSnapOffsetLeft = -hiddenLength;
            }
        }
    }
    this.headerMenuButton.setSnapOffsetLeft(hmbSnapOffsetLeft);

    this.headerMenuButton.canDragResize = (button.canDragResize != null
                                           ? button.canDragResize && this.canResizeFields
                                           : this.canResizeFields);
    return this.headerMenuButton;
},

headerMenuButtonDefaults: {
    // Default the title to explicit null rather than the default Button.title property
    // (EG "Untitled Button")
    title:null,
    iconSpacing:0,
    showDisabled:false,
    showDisabledIcon:false,

    click : function () {
        this.creator.headerMenuButtonClick();
    },
    doubleClick : function () {
        this.creator.headerMenuButtonDoubleClick();
    },
    mouseOut : function () {
        var EH = this.ns.EH;
        if (!EH._handlingTouchEventSequence()) {
            var target = EH.getTarget();
            if (!target || !this.parentElement.contains(target)) {
                this.hide();
            }
        }

        // call Super to reset 'over' state
        this.Super("mouseOut", arguments);
    },
    visibilityChanged : function (isVisible) {
        if (!isVisible && this.parentElement != null) this.parentElement.markForRedraw();
    }
},

// called from a click on the header menu button
headerMenuButtonClick : function () {
    var header = this.header,
        buttonIndex = header.containsEvent() ? header.getMouseOverButtonIndex() : -1;
    if (buttonIndex < 0 && this.frozenHeader) {
        header = this.frozenHeader;
        buttonIndex = header.getMouseOverButtonIndex();
    }
    if (buttonIndex == -1 || !this.headerMenuButton) return;

    var headerButton = header.getMember(buttonIndex),
        menuTop = headerButton.getPageBottom(),
        menuWidth = this._cornerMenu ? this._cornerMenu.getVisibleWidth()
                                     : isc.Menu.getPrototype().defaultWidth,
        headerLeft = headerButton.getPageLeft(),
        headerMenuButtonRight = this.headerMenuButton.getPageRight(),
        menuLeft = this.isRTL() ?
                   Math.min(headerLeft, headerMenuButtonRight - menuWidth) :
                   Math.max(headerLeft, headerMenuButtonRight - menuWidth);
    this.displayHeaderContextMenu(
                this.headerMenuButton,
                [menuLeft,menuTop]
            );
    // unmask the hmb -- if a double click occurs on it we want to auto-fit
    this.headerMenuButton.bringToFront();
},

// called from a double click on the header menu button
headerMenuButtonDoubleClick : function () {
    var header = this.header,
        buttonIndex = header.getMouseOverButtonIndex();
    if (buttonIndex == -1 && this.frozenHeader) {
        header = this.frozenHeader;
        buttonIndex = header.getMouseOverButtonIndex();
    }
    if (buttonIndex == -1) return;

    if (this.frozenFields && header == this.header) {
        // headerDoubleClick expects a master field index, not a localized one
        buttonIndex += this.frozenFields.length;
    }

    // we showed the header menu on click - hide it now
    // otherwise when the field resized it shows up in the wrong place...
    isc.Menu.hideAllMenus();

    return this.headerDoubleClick(buttonIndex, header);
},

_showHeaderMenuButton : function (headerButton) {
    var hmb = this.getHeaderMenuButton(headerButton);
    if (hmb.masterElement != null) hmb.masterElement.markForRedraw();
    headerButton.addPeer(hmb);
    hmb.enable();

    hmb.addProperties({
        dragTarget: headerButton
    });
    // bringToFront if necessary
    if (headerButton.zIndex > hmb.zIndex) hmb.bringToFront();
    if (!hmb.isVisible()) {
        hmb.show();
    }
    headerButton.markForRedraw();
    return hmb;
},

_hideHeaderMenuButton : function (headerButton) {
    var hmb = this.headerMenuButton;
    if (hmb != null && hmb.isVisible()) {
        var hmbMasterElement = hmb.masterElement;
        if (headerButton == null) headerButton = hmbMasterElement;

        if (hmbMasterElement == headerButton) {
            hmb.hide();
            if (hmbMasterElement != null) hmbMasterElement.markForRedraw();
        }
    }
},

// Header Buttons
// --------------------------------------------------------------------------------------------

shouldWrapHeaderTitle : function (field) {
    var shouldAutoFit = this.shouldAutoFitField(field);
    if (shouldAutoFit) {
        var approach = this.getAutoFitWidthApproach(field);
        if (approach == "title" || approach == "both") {
            return false;
        }
    }
    if (field.wrap != null) return field.wrap;
    return this.wrapHeaderTitles;
},

_shouldClipHeaderTitle : function (fieldNum) {
    var field = this.fields[fieldNum];
    if (this.autoFitHeaderHeights) return false;


    if (field.wrap == true) return false;
    if (field.wrap == null && this.wrapHeaderTitles) return false;

    var shouldAutoFit = this.shouldAutoFitField(field);
    if (shouldAutoFit) {
        var approach = this.getAutoFitWidthApproach(field);

        if (approach == "title" || approach == "both") {
            return false;
        }
    }
    return true;
},

_$titleClipper:"_titleClipper",
_getHeaderTitleClipperID : function (field) {
    if (!field || !field.name) return null;
    return this._getDOMID(field.name + this._$titleClipper);
},

//> @method listGrid.headerTitleClipped() (A)
// Is the field title for the specified field clipped?
//
// @param fieldNum (number) field number for the header button title to test
// @return (boolean) whether the field title for the specified field is clipped
// @see attr:ListGrid.clipHeaderTitles
// @group gridHeader
// @visibility external
//<
headerTitleClipped : function (fieldNum) {
    var field = this.fields[fieldNum],
        titleClipperID = this._getHeaderTitleClipperID(field);
    if (titleClipperID == null) return false;
    var titleClipperHandle = isc.Element.get(titleClipperID);
    if (titleClipperHandle == null) return false;


    if (isc.Browser.isChrome ||
        (isc.Browser.isMoz && isc.Browser.version >= 7))
    {
        var range = this.getDocument().createRange();
        range.selectNodeContents(titleClipperHandle);
        var contentsBCR = range.getBoundingClientRect();
        var bcr = titleClipperHandle.getBoundingClientRect();
        return bcr.width < contentsBCR.width;

    } else {
        return isc.Element.getClientWidth(titleClipperHandle) < titleClipperHandle.scrollWidth;
    }
},

//>    @method    listGrid.getHeaderButtonTitle()    (A)
// Given a header button (or column number), returns the title for that header button.
// @group    drawing, gridHeader
// @param    button        (number)
// @return    (string)    HTML for header button contents
// @visibility internal
//<

getHeaderButtonTitle : function (button, clipTitle) {
    var fieldNum = button;
    // passed a header button or field
    if (isc.isA.Object(button)) fieldNum = button.masterIndex;
    else {
        button = this.getFieldHeaderButton(fieldNum);
    }

    var field = this.fields[fieldNum];

    // if field is null, we've shrunk the number of columns, so forget it
    if (!field) return "";

    // Support explicit showTitle:false
    if (field.showTitle == false) {
        return isc.nbsp;
    }

    var fieldName = field[this.fieldIdProperty],
        isSortField = this.isSortField(fieldName),
        showSortArrow = (isSortField) &&
                        (!this.showSortArrow || this.showSortArrow == isc.ListGrid.FIELD ||
                         this.showSortArrow == isc.ListGrid.BOTH),
        showSortNumeral = (this.showSortNumerals == false ? false :
            isSortField && this.getSortFieldCount() > 1),
        title = field.headerTitle != null ? field.headerTitle
                                          : this.getFieldTitle(fieldNum);

    if (clipTitle == null) clipTitle = this._shouldClipHeaderTitle(fieldNum);

    var sortIndex;
    if (showSortNumeral) {
        var specifier = this.getSortSpecifier(fieldName);
        sortIndex = specifier.sortIndex;
    }
    var fullTitle = isc.StringBuffer.create(),
        isRTL = this.isRTL(),
        align = this.getFieldHeaderAlign(fieldNum, isRTL);

    // If the button is wrapped, then we want to float the sort arrow and sort numeral. The
    // sort numeral should be placed first, then the sort arrow, then the title so that (in text order)
    // the title will be visible, then the sort arrow, and then the sort numeral.
    // Floating the sort arrow fixes the issue that the arrow would wrap with the title text.

    if (field.wrap || (field.wrap == null && this.wrapHeaderTitles)) {
        if (showSortNumeral) {
            fullTitle.append("<a style='display:inline-block;",
                             (this.isRTL() ? "float:left;margin-right:" : "float:right;margin-left:"),
                             "4px'>", this.getSortNumeralHTML(fieldName, sortIndex), "</a>");
        }
        if (showSortArrow) {
            fullTitle.append(this.getSortArrowImage(fieldNum, false, false, true));
        }

        fullTitle.append(title);

    } else if (!clipTitle || !this.clipHeaderTitles) {
        fullTitle.append(title);
        if (showSortArrow) fullTitle.append(isc.nbsp, this.getSortArrowImage(fieldNum));
        if (showSortNumeral) {
            fullTitle.append(isc.nbsp, this.getSortNumeralHTML(fieldName, sortIndex));
        }

        // append a spacer after the title text if we're showing the headerMenuButtton for this
        // field. That means that if the title is right-aligned, or if we're auto-fitting the field,
        // we leave space for the headerMenuButton to show without occluding the title.
        if (!clipTitle && button && this.shouldLeaveHeaderMenuButtonSpace(field) &&
            this.shouldShowHeaderMenuButton(button))
        {
            fullTitle.append(isc.Canvas.spacerHTML(this.headerMenuButtonWidth, 1));
        }
    } else {
        fullTitle.append("<div style='display:inline-block;max-width:100%;text-align:",
            align, ";vertical-align:middle'>");
        var extraRightWidth = 0;
        if (showSortNumeral) {

            var sortNumeralHTMLWidth = (sortIndex >= 9 ? 12 : 6);
            extraRightWidth += 4 + sortNumeralHTMLWidth;
            fullTitle.append("<a style='",
                (isRTL ? "float:left;margin-right:" : "float:right;margin-left:"),
                "4px;width:", sortNumeralHTMLWidth, "px;overflow:hidden'>",
                this.getSortNumeralHTML(fieldName, sortIndex), "</a>")
            ;
        }
        if (showSortArrow) {
            var img = this.getSortArrowImage(fieldNum, true, clipTitle);
            if (img != null) {
                extraRightWidth += 4 + img.width;
                fullTitle.append(this.getSortArrowImage(fieldNum, false, clipTitle));
            }
        }
        fullTitle.append("<div");
        var titleClipperID = this._getHeaderTitleClipperID(field);
        if (titleClipperID != null) {
            fullTitle.append(" id='", titleClipperID, "'");
        }
        fullTitle.append(" style='overflow:hidden;",
            isc.Browser._textOverflowPropertyName, ":ellipsis;white-space:nowrap");
        if (isc.Browser.isMoz) fullTitle.append(isRTL ? ";margin-left:" : ";margin-right:", extraRightWidth, "px");
        fullTitle.append("'>", title, "</div>", "</div>");
    }

    return fullTitle.release();
},

shouldLeaveHeaderMenuButtonSpace : function (field) {
    if (field.leaveHeaderMenuButtonSpace != null) return field.leaveHeaderMenuButtonSpace;
    if (this.leaveHeaderMenuButtonSpace != null) return this.leaveHeaderMenuButtonSpace;
    return field.align != "center";
},

_setCheckboxHeaderState : function (state) {
    var fieldNum = this.getCheckboxFieldPosition(),
        field = this.fields[fieldNum];

    // sanity check only - this shouldn't happen if we are showing a checkbox field
    if (!this.isCheckboxField(field)) return;
    // if (field._allSelected == state) return;

    var title = (this.canSelectAll == false || this.selectionType == "single") ? isc.nbsp
                 : this._getCheckboxValueIconHTML(state, true, false, field);

    this.setFieldTitle(fieldNum, title);
    field._allSelected = state;
},

updateCheckboxHeaderState : function () {



    var data = this.getOriginalData();
    var validData = (isc.isAn.Array(data) ||
                    (isc.isA.ResultSet(data) && data.allMatchingRowsCached())),
        selection = this.getSelection() || [];

    if (validData) {
        var records = this.isGrouped ? this.getAllRecordsFromGroupTree() : data;
        var recordCount = records.getLength();
        var allSelected = recordCount > 0;
        for (var i = 0; i < recordCount; i++) {
            var record = records.get(i);
            if (this.selection._canSelectItem(record) &&
                !this.selection.isSelected(record))
            {
                allSelected = false;
                break;
            }
        }
        if (allSelected) {
            this._setCheckboxHeaderState(true);
        } else {
            this._setCheckboxHeaderState(false);
        }
//    } else {
//        this.logWarn('Not updating header checkbox as data is not "valid data" (contains unloaded rows, etc)');
    }
},

defaultHeaderHoverHTML : function (fieldNum) {

    var field = this.fields[fieldNum];
    if (field == null) return null;
    if (field.prompt != null) return field.prompt;


    var showTitle = this.clipHeaderTitles && this.showClippedHeaderTitlesOnHover &&
                    this.headerTitleClipped(fieldNum);

    if (showTitle) {
    return field.headerTitle != null ? field.headerTitle
                                     : this.getFieldTitle(fieldNum);
    }
    return null;
},

//> @method listGrid.headerHoverHTML() (A)
// Returns the HTML that is displayed by the default +link{ListGrid.headerHover(),headerHover}
// handler. Return null or an empty string to cancel the hover.
// <smartgwt><p>Use <code>setHeaderHoverFormatter()</code> to provide a custom
// implementation.</smartgwt>
//
// @param fieldNum (number) field number for the header that was hovered
// @param defaultHTML (HTMLString) the HTML that would have been displayed by default
// @return (HTMLString) HTML to be displayed in the hover. If null or an empty string, then the hover
// is canceled.
// @see attr:ListGrid.showClippedHeaderTitlesOnHover
// @see attr:ListGrid.clipHeaderTitles
// @group hovers
// @group gridHeader
// @visibility external
//<
headerHoverHTML : function (fieldNum, defaultHTML) {
    return defaultHTML;
},

//> @attr listGrid.headerHoverStyle (CSSStyleName : null : IRW)
// This property may be set to customize the css style for the
// hover shown on +link{ListGrid.headerHover()}.
// @visibility external
//<

//> @attr listGrid.headerHoverAlign (Alignment : null : IRW)
// This property may be set to customize the alignment for the
// hover shown on +link{ListGrid.headerHover()}.
// @visibility external
//<

//> @attr listGrid.headerHoverVAlign (VerticalAlignment : null : IRW)
// This property may be set to customize the vertical alignment for the
// hover shown on +link{ListGrid.headerHover()}.
// @visibility external
//<

//> @attr listGrid.headerHoverOpacity (Integer : null : IRW)
// This property may be set to customize the opacity for the
// hover shown on +link{ListGrid.headerHover()}.
// @visibility external
//<

//> @attr listGrid.headerHoverHeight (Integer : null : IRW)
// Optional default height for the
// hover shown on +link{ListGrid.headerHover()}.
// @visibility external
//<

//> @attr listGrid.headerHoverWidth (Integer : null : IRW)
// Optional default width for the
// hover shown on +link{ListGrid.headerHover()}.
// @visibility external
//<

//> @attr listGrid.headerHoverWrap (Boolean : null : IRW)
// This property may be set to customize the <code>wrap</code> attribute for the
// hover shown on +link{ListGrid.headerHover()}.
// @visibility external
//<

_showHeaderHover : function (fieldNum, HTML) {
    var field = this.fields[fieldNum];
    if (HTML && !isc.is.emptyString(HTML)) {
        var properties = this._getHeaderHoverProperties(field);
        isc.Hover.show(HTML, properties, (field.hoverRect || this.headerHoverRect));
    } else isc.Hover.clear();
},

_getHeaderHoverProperties : function (field) {
    var props = {};
    if (field) {
        props = isc.addProperties({}, {
            align: (field.hoverAlign != null ? field.hoverAlign : this.headerHoverAlign),
            hoverDelay: (field.headerHoverDelay != null ? field.headerHoverDelay :
                         field.hoverDelay != null ? field.hoverDelay : this.headerHoverDelay),
            height: (field.hoverHeight != null ? field.hoverHeight : this.headerHoverHeight),
            opacity: (field.hoverOpacity != null ? field.hoverOpacity : this.headerHoverOpacity),
            baseStyle: (field.hoverStyle != null ? field.hoverStyle : this.headerHoverStyle),
            valign: (field.hoverVAlign != null ? field.hoverVAlign : this.headerHoverVAlign),
            width: (field.hoverWidth != null ? field.hoverWidth : this.headerHoverWidth),
            wrap: (field.hoverWrap != null ? field.hoverWrap : this.headerHoverWrap)
        });
    } else {
        props = isc.addProperties({}, {
            align: this.headerHoverAlign,
            hoverDelay: this.headerHoverDelay,
            height: this.headerHoverHeight,
            opacity: this.headerHoverOpacity,
            baseStyle: this.headerHoverStyle,
            valign: this.headerHoverVAlign,
            width: this.headerHoverWidth,
            wrap: this.headerHoverWrap
        });
    }

    props.moveWithMouse = this.headerHoverMoveWithMouse;

    return props;
},

// _headerClick - maps the local fieldNum within the header to the master index before
// calling the public headerClick method
_headerClick : function (headerFieldNum, header) {
    var fieldNum = header.getMember(headerFieldNum).masterIndex;
    //this.logWarn("click on field: " + headerFieldNum + " in header: " + header +
    //             " maps to field: " + fieldNum);
    return this.headerClick(fieldNum, header);
},

//>    @method    listGrid.headerClick()    (A)
// Handle a click in the list header.
// <P>
// By default, calls +link{sort()} to sort by the field that was clicked, if
// +link{listGrid.canSort,sorting is enabled}, and calls +link{autoFitField()} if
// +link{canAutoFitFields} is true and +link{headerAutoFitEvent} is set to <code>"click"</code>.
//
// @param fieldNum (number) field number for the header that was clicked
// @group sorting
// @group events
// @group gridHeader
// @visibility external
//<
headerClick : function (fieldNum, header) {

    // 'onHeaderClick' stringmethod can intercept the header click

    if (this.onHeaderClick && (this.onHeaderClick(fieldNum,header) == false)) {
        return;
    }

    var field = this.fields[fieldNum];
    // check if the checkbox column header was clicked
    if (this.isCheckboxField(field) && this.selectionType != "single" && this.canSelectAll != false) {
        if (field._allSelected) {
            this.deselectAllRecords();
            this._setCheckboxHeaderState(false);
        } else {
            this.userSelectAllRecords();
            // Note that due to canSelectRecord() this may not actually select all
            // so call the method to determine whether the checkboxHeaderState should
            // be checked or not.
            this.updateCheckboxHeaderState();
        }
        return false;
    }

    // If canAutoFit is true, autoFit the field on click if headerAutoFitEvent is 'click'
    if (this.canAutoFitFields && this.headerAutoFitEvent == "click") {
        this.autoFitField(fieldNum);
    }

    // if the list's canSort is false, the field wasn't defined or the field's 'canSort' property is
    // false, return false
    if (!this._canSort(field)) return false;

    var fieldName = field[this.fieldIdProperty],
        specifier = this.isSortField(fieldName) ? this.getSortSpecifier(fieldName) : null,
        EH = isc.EventHandler,
        key = EH.getKey(),
        shiftDown = EH.shiftKeyDown(),
        globalSortDir = this._baseSortDirection,
        sortDir = field.sortDirection
    ;

    if (shiftDown && this._canMultiSort() && this._canSortData(field)) {
        // add this field to the sort-configuration, or alter it if it's already sorted
        if (this.isSortField(fieldName)) {
            if (sortDir == globalSortDir || this.canUnsort == false) {
                // reverse the sort-direction of this field
                this.toggleSort(fieldName);
            } else {
                // unsort the field - if this is the only sorted field, or the last field in
                // a multiSort arrangement, no visual resort will occur - otherwise, a visual
                // resort occurs to reapply remaining sort-specifiers
                this.toggleSort(fieldName, "unsort");
            }
        } else {
            // add this field to the sort-configuration
            this._addSort(field);
        }
    } else {
        if (this.isSortField(fieldName) && this.getSortFieldCount() == 1) {
            if (sortDir == globalSortDir || this.canUnsort == false) {
                // toggle the sortDirection on this field
                this.toggleSort(fieldName);
            } else {
                // unsort this field
                this.toggleSort(fieldName, "unsort");
            }
        } else {
            var fieldSortDir = this._getFieldSortDirection(field);
            this.setSort([{
                property: fieldName,
                direction: fieldSortDir
            }]);
        }
    }

    // return false to stop further event processing
    // if we don't do this, clicks in the header will sometimes select items in the list
    return false;
},

// _headerDoubleClick - maps the local fieldNum within the header to the master index before
// calling the public headerDoubleClick method
_headerDoubleClick : function (headerFieldNum, header) {
    if (this.data && this.data.lengthIsKnown && !this.data.lengthIsKnown()) {
        // we only want to run auto-sizing code if the data has already arrived - bail now
        // and set a flag to have dataArrived re-run do the auto-sizing
        this._autoSizeOnDataArrived = true;
        this._autoSizeHeaderFieldNum = headerFieldNum;
        this._autoSizeHeader = header;
        return;
    }
    return this.headerMenuButtonDoubleClick();
},



//>    @method    listGrid.headerDoubleClick()    (A)
// Handle a double click in the list header.
// <P>
// By default, calls +link{autoFitField()} if +link{canAutoFitFields} is true and
// +link{headerAutoFitEvent} is <code>"doubleClick"</code>.
//
// @param fieldNum (number) field number for the header that was clicked
// @group events
// @group gridHeader
// @visibility external
//<
headerDoubleClick : function (fieldNum, header) {

    var field = this.fields[fieldNum];
    if (this.isCheckboxField(field)) return;

    // If canAutoFit is true, autoFit the field on click if headerAutoFitEvent is 'doubleClick'
    if (this.canAutoFitFields && this.headerAutoFitEvent == "doubleClick") {
        this.autoFitField(fieldNum);
    }

    // return false to stop further event processing
    return false;
},

// Field Drag Resize
// --------------------------------------------------------------------------------------------

//>    @method    listGrid.fieldDragResizeStart()    (A)
// Event sent when header column starts drag-resizing.
// <p>
// Current implementation creates a drag line and sizes it to the height of the list body.
// <p>
// Observed from this.header.dragResizeStart()
//
//        @group    dragging
//<
fieldDragResizeStart : function () {
    this._dragResizingField = true;
    // Turn off the logic that expands one field to fill the viewport if autoFitFields is
    // enabled

    this.autoFitFieldsFillViewport = false;
    // make sure the drag line is set up
    this.makeDragLine();

    // and resize it to cover the entire height of the listGrid
    this._dragLine.resizeTo(2,
         this.getVisibleHeight() - (this.body.hscrollOn ? this.body.getScrollbarSize() : 0));

    // set the overflow of the header being dragged to hidden so that the user can clip the title
    // text if he chooses.
    var dt = this.ns.EH.dragTarget;

    // ensure we no longer attempt to autoFit
    var field = this.getField(dt.masterIndex);
    if (field) {
        field.autoFitWidth = false;
        var wrap = field.wrap;
        if (wrap == null) wrap = this.wrapHeaderTitles;
        if (wrap != dt.wrap) {
            dt.setWrap(wrap);
        }
    }

    dt.setWidth(dt.getVisibleWidth());
    dt.setOverflow("hidden");
},

//>    @method    listGrid.fieldDragResizeMove()    (A)
//        Event sent when header column is drag-resizing.
//
//        Current implementation moves the dragLine to show the new boundary.
//
//        Observed from this.header.dragResizeMove()
//        @group    dragging, drawing
//<
fieldDragResizeMove : function () {
    var EH = this.ns.EH,
        resizingField = EH.dragTarget;

    if (this.autoSizeHeaderSpans != null) {
        this._autoSizeHeaderSpans = this.autoSizeHeaderSpans;
        this.autoSizeHeaderSpans = false;
    }

    // move the drag line to show the new field boundary
    var colNum = this.header._resizePosition;
    this.showDragLineForField(colNum);

    // if we're supposed to resize fields as they're dragging
    if (this.resizeFieldsInRealTime) {
        this.realtimeFieldResize(colNum);

    }

    // make sure that the header menu button snapOffsetLeft is 0 to prevent the user from
    // seeing an offset if the column is narrowed to the point where an offset is unnecessary.
    if (this.headerMenuButton && this.headerMenuButton.snapOffsetLeft != 0) {
        this.headerMenuButton.setSnapOffsetLeft(0);
    }
},

realtimeFieldResize : function (colNum) {

    // change the width of the field and redraw
    var EH = isc.EH,
        width = EH.dragResizeWidth,
        minWidth = this.getMinFieldWidth(this.fields[colNum], true);
    // Disallow shrinking below minWidth
    if (width < minWidth) width = minWidth;

    this._fieldWidths[colNum] = width;



    // Ensure the body's h-scroll position is synched with that of the header before
    // resizing body fields

    if (this._delayingBodyScrolling) {
        this.syncBodyScrolling();
    }


    this._settingBodyFieldWidths = true;
    this.setBodyFieldWidths(this._fieldWidths);
    // While resizing fields in real time we want the body's incremental rendering system
    // to use the 'quick' draw ahead ratio.
    this.body.useQuickDrawAheadRatio = true;
    this.body.redraw("fieldDragResize");
    delete this.body.useQuickDrawAheadRatio;
    delete this._settingBodyFieldWidths;

    // if we're scrolled, as the body shrinks it's scroll position may become invalid,
    // so it scrolls back, and the header needs to match

    if (!this.isRTL()) {
        var bodyScrollLeft = this.body.getScrollLeft();
        if (bodyScrollLeft > 0) {
            this.header.scrollTo(bodyScrollLeft, null, "headerResize");
        }


    } else {
        if (this.header.getScrollLeft() < this.header.getLeft()) {
            this.header.scrollTo(this.header.getLeft());
        }
    }
},

//>    @method    listGrid.fieldDragResizeStop()    (A)
//        Event sent when header column is done drag-resizing.
//
//        Current implementation hides the dragLine.
//
//        Observed from this.header.dragResizeStop()
//        @group    dragging, drawing
//<
fieldDragResizeStop : function () {
    if (this._autoSizeHeaderSpans != null) {
        this.autoSizeHeaderSpans = this._autoSizeHeaderSpans;
        delete this._autoSizeHeaderSpans;
    }

    this.hideDragLine();
},



// Reorder / Resize Field APIs
// --------------------------------------------------------------------------------------------

headerDragReordered : function (fieldNum, moveToPosition, header) {
    var masterFieldNum = header.getMember(fieldNum).masterIndex,
        masterMoveToPosition = header.getMember(moveToPosition).masterIndex;
    //this.logWarn("reorder of header field from " + fieldNum +
    //             " to " + moveToPosition + " on header: " + header +
    //             " will move master field from " + masterFieldNum +
    //             " to : " + masterMoveToPosition);
    this.reorderField(masterFieldNum, masterMoveToPosition);
},

//>    @method    listGrid.reorderField()    (A)
// Reorder a particular field
//
// @param    fieldNum        (number)    Number of the field to reorder
// @param    moveToPosition    (number)    New position for that field
//
// @visibility external
//<
reorderField : function (fieldNum, moveToPosition) {
    this.reorderFields(fieldNum, fieldNum+1, moveToPosition - fieldNum);
},

//>    @method    listGrid.reorderFields()    (A)
// Reorder a set of adjacent fields, from start to end exclusive at the end, by distance
// moveDelta.<br><br>
//
// NOTE: start and end coordinates are in terms of the currently visible fields, not the full set of
// fields.
//
// @param    start     (number) Start of the range of fields to move, inclusive
// @param    end       (number) End of the range of fields to move, non-inclusive
// @param    moveDelta (number) Distance to move by
//
// @visibility external
//<
reorderFields : function (start, end, moveDelta) {

    // Note this method is public and can be called programmatically - avoid crashing
    // if various things that get set up on draw, such as _fieldWidths or filterEditor aren't
    // yet present.

    // reorder the fields in the filterEditor if showing

    if (this.showFilterEditor && this.filterEditor != null) {
        this.filterEditor.reorderFields(start, end, moveDelta);
    }

    if (this.summaryRow && this.showGridSummary) {
        this.summaryRow.reorderFields(start,end,moveDelta);
    }

    var startField = this.fields[start],
        endField = this.fields[end -1],
        targetIndex = start + moveDelta,
        targetField = this.fields[targetIndex];


    // preserve the sorted field
    var sortFieldNum = this._getSortFieldNum(),
        sortField = this.fields[sortFieldNum];
    // reorder the field widths for the moved field range
    if (this._fieldWidths != null) this._fieldWidths.slideRange(start, end, start+moveDelta);


    // update this.completeFields, rather than this.fields.
    // Note that we may have hidden fields, so we have to recalculate start, end and target.
    if (this.completeFields == null) this.setFields(this.fields);
    if (this.completeFields.contains(targetField)) {
        var cfStart = this.completeFields.indexOf(startField),
            cfEnd = this.completeFields.indexOf(endField) + 1,
            cfTargetIndex = this.completeFields.indexOf(targetField);
        //this.logWarn("start, end, targetIndex of: " + [start,end,targetIndex] +
        //             " translated to: " + [cfStart, cfEnd, cfTargetIndex] +
        //             " fields: " + this.completeFields.getProperty("name"));
        this.completeFields.slideRange(cfStart, cfEnd, cfTargetIndex);
        //this.logWarn("after slideRange, fields: " +
        //             this.completeFields.getProperty("name"));
    }

    // set the visibleFields based on newly reordered complete set of fields
    this.deriveVisibleFields();

    // update the sortFieldNum to reflect the slide
    if (sortFieldNum != null) {
        sortFieldNum = this.fields.indexOf(sortField);
        // field no longer visible
        if (sortFieldNum == -1) sortFieldNum = null;
        this._setSortFieldNum(sortFieldNum);
    }

    // find the header for this field
    var header;

    if (!this.frozenFields) {
        header = this.getFieldHeader(start);
        // reorder the header items
        if (header) {
            header.reorderItems(this.getLocalFieldNum(start), this.getLocalFieldNum(end),
                                this.getLocalFieldNum(start+moveDelta));
        }

    } else {
        // for frozen fields, just rebuild for freeze

        header = this.getFieldHeader(start);
        this.rebuildForFreeze();
        // re-sync the header with the current position of the body
        this.syncHeaderScrolling(this.body.getScrollLeft());
    }

    // tell that toolbar button to show the sort
    if (sortFieldNum != null && header && this.selectHeaderOnSort) {
        var sortButton = header.getButton(this.getLocalFieldNum(sortFieldNum));
        header.selectButton(sortButton);
        if (isc.Browser.isTouch && this.shouldShowHeaderMenuButton(sortButton, true)) {
            this._showHeaderMenuButton(sortButton);
        }
    }

    // if necessary, preserve the edit field
    var editorShowing = this._editorShowing;
    if (editorShowing) {
        // If the editor is showing, we will need to update the editForm in 2 ways:
        // - the colNum property for each item must be updated
        // - the form items should be reordered so the items are in order wrt to the new
        //   field order.
        // When this has been accomplished, we can rely on the body redraw to handle writing
        // out the appropriate HTML, setting the values of the form items, and updating focus
        // if required.
        var editForm = this._editRowForm,
            editRowNum = this.getEditRow(),
            editField = this.getEditField(),
            newItemsArray = []
        for (var i = 0; i < editForm.items.length; i++) {
            var item = editForm.items[i];
            item.colNum = this.fields.findIndex(this.fieldIdProperty, item.getFieldName());
            for (var j = 0; j <= newItemsArray.length; j++) {
                if (newItemsArray[j] == null || newItemsArray[j].colNum > item.colNum) {
                    newItemsArray.addAt(item, j);
                    break;
                }
            }
        }

        // Update the items array to match the reordered version of itself
        editForm.items.setArray(newItemsArray);
        // Update the 'editColNum' to reflect the repositioned edit field.
        this._editColNum = this.fields.indexOf(editField);
    }

    // We want to remap embedded comopnents to catch the cell change
    // for any components embedded by cell.
    // Note we don't expect a simple reorder of fields to impact frozen/unfrozen
    // state so we need to update either the frozen or unfrozen body embedded
    // columns only
    var body = this.body;
    if (this.frozenFields && start < this.frozenFields.length) {
        body = this.frozenBody;
    }

    if (body) {
        // this may not all be necessary if we rebuilt for freeze.
        this.body.fields = this.normalFields || this.fields;
        if (this.frozenBody) this.frozenBody.fields = this.frozenFields;

        // give the bodies the reordered field widths
        this.setBodyFieldWidths(this._fieldWidths);

        if (body._embeddedComponents != null) {
            this._remapEmbeddedComponentColumns(body);
        }

        // redraw (may be required if fields had matching widths)
        if (!body.isDirty()) this._markBodyForRedraw("reorderFields");
    }

    this.handleFieldStateChanged();
},

// Ensure that the .colNum property is up to date on our editors

remapEditFieldsForFreeze : function () {
    if (this._editorShowing) {
        var form = this.getEditForm(),
            items = form.getItems();
        for (var i = 0; i < items.length; i++) {
            var item = items[i];
            item.colNum = this.fields.findIndex(this.fieldIdProperty, item.getFieldName());
            var field = this.fields[item.colNum];

            if (!this._suppressedFrozenFields && field.frozen) item.containerWidget = this.frozenBody;
            else item.containerWidget = this.body;
        }
    }
},

headerDragResized : function (fieldNum, newWidth, header) {
    var masterFieldNum = header.getMember(fieldNum).masterIndex;
    //this.logWarn("resize of header field: " + fieldNum + " on header: " + header +
    //             " will resize master fieldNum: " + masterFieldNum);

    // Once the user has drag resized a field in the grid, we'll reflow to
    // accomodate the new size.
    // Temporarily suppress *all* dynamic sizing for any fields.
    // In other words, don't reflow "*"/"%" sized fields, or autoFit fields.
    // This means if autoFitHeaderHeights is true, and the horizontal resize of
    // a header changed wrapping, we may end up changing the height of the
    // header as a whole. We don't want the toolbar to then jarringly rerun layout
    // policy logic and change things from your dragged size(s).
    // However make this temporary! If the widget as a whole resizes subsequently,
    // we want the "*" etc sizes to be recalculated

    for (var i = 0; i < this.fields.length; i++) {
        if (i == masterFieldNum) continue;
        this.fields[i]._origWidth = this.fields[i].width;
        this.fields[i]._origAutoFit = this.fields[i].autoFitWidth
        this.fields[i].width = this.getFieldWidth(i);
        this.fields[i].autoFitWidth = false;
        var button = this.getFieldHeaderButton(i);
        if (button) {
            button._prevUserWidth = button._userWidth;
            button._userWidth = this.fields[i].width;
        }
    }
    this.resizeField(masterFieldNum, newWidth, true);
    // Reset dynamic sizes ("*", "n%", etc) so subsequent reflows will respect them.
    for (var i = 0; i < this.fields.length; i++) {

        if (i == masterFieldNum) continue;

        this.fields[i].width = this.fields[i]._origWidth;
        delete this.fields[i]._origWidth;

        this.fields[i].autoFitWidth = this.fields[i]._origAutoFit
        delete this.fields[i]._origAutoFit;

        var button = this.getFieldHeaderButton(i);
        if (button) {
            button._userWidth = button._prevUserWidth;
            delete button._prevUserWidth;
        }
    }

    // clear the _dragResizingField flag set up on drag start and ensure scrolling is
    // all in sync
    this._dragResizingField = false;
    var left = this.body.getScrollLeft();
    this.syncHeaderScrolling(left, top);
    this.syncFilterEditorScrolling(left, top);
    this.syncSummaryRowScrolling(left,top);



},

//> @method ListGrid.setCanResizeFields()
// Setter method for updating +link{ListGrid.canResizeFields} at runtime.
// @param canResize (boolean) new value for this.canResizeFields
// @visibility external
//<
setCanResizeFields : function (canResize) {
    if (this.canResizeFields == canResize) return;
    this.canResizeFields = canResize;
    if(canResize==false) this.canAutoFitFields=false;
    if (this.header) this.header.setCanResizeItems(canResize);
    if (this.frozenHeader) this.frozenHeader.setCanResizeItems(canResize);
    if (this.headerMenuButton) this.headerMenuButton.canDragResize = canResize;
    if (canResize) {
        if (this.header && !this.header.dontObserve) this._observeHeaderResize(this.header);
        if (this.frozenHeader && !this.frozenHeader.dontObserve)
            this._observeHeaderResize(this.frozenHeader);
    } else {
        if (this.header) this._ignoreHeaderResize(this.header);
        if (this.frozenHeader) this._ignoreHeaderResize(this.frozenHeader);
    }
},

_observeHeaderResize : function (header) {
    this.observe(header, "itemDragResized", function (itemNum, newSize) {
        this.headerDragResized(itemNum, newSize, header);
    });
    this.observe(header, "dragResizeMemberStart", function () { this.fieldDragResizeStart(); });
    this.observe(header, "dragResizeMemberMove", function () { this.fieldDragResizeMove(); });
    this.observe(header, "dragResizeMemberStop", function () { this.fieldDragResizeStop(); });
},

_ignoreHeaderResize : function (header) {
    if (this.isObserving(header, "itemDragResized"))
        this.ignore(header,"itemDragResized");
    if (this.isObserving(header, "dragResizeMemberStart"))
        this.ignore(header, "dragResizeMemberStart");
    if (this.isObserving(header, "dragResizeMemberMove"))
        this.ignore(header, "dragResizeMemberMove");
    if (this.isObserving(header, "dragResizeMemberStop"))
        this.ignore(header, "dragResizeMemberStop");
},

//>    @method    listGrid.resizeField()    (A)
// Resize a particular field to a new width. Note that this method
// will also set +link{listGridField.autoFitWidth} to false if it was previously true.
//
// @param    fieldNum    (number)    Number of the field to resize
// @param    newWidth    (number)    New width of the field
//
// @visibility external
//<
// @param storeWidth (boolean) Additional internal parameter - should we update field.width?
//  Updating field.width means if setFields() is called after init with an overlapping set
//  of field objects (typical use case: showing / hiding fields) - we retain the width set here.
//  Otherwise we resize the field but don't store the new width value
resizeField : function (fieldNum, newWidth, storeWidth) {
    this._resizeFields([fieldNum], [newWidth], [storeWidth]);
},
_resizeFields : function (fieldNums, newWidths, storeWidths) {
    for (var i = 0; i < fieldNums.length; i++) {
        var fieldNum = fieldNums[i];
        if (!isc.isA.Number(fieldNum)) fieldNum = this.getFieldNum(fieldNum);
        if (fieldNum == -1) {
            fieldNums.removeAt(i);
            newWidths.removeAt(i);
            storeWidths.removeAt(i);
            i--;
        } else {
            // store the normalized value in case we were passed a name
            fieldNums[i] = fieldNum;

            var minWidth = this.getMinFieldWidth(this.fields[fieldNum], true);
            if (minWidth > newWidths[i]) {
                newWidths[i] = minWidth;
            }
        }
    }
    if (fieldNums.length == 0) return;

    // adjust the header for this field, if we have a header
    var adjustHeader = this.header && this.header.isDrawn();
    if (adjustHeader) {
        // Suppress instant relayout of the header for each field we resize

        var instantRelayout = this.header.instantRelayout,
            frozenInstantRelayout = this.frozenHeader ? this.frozenHeader.instantRelayout
                                                        : null;
        this.header.instantRelayout = false;
        if (this.frozenHeader) this.frozenHeader.instantRelayout = false;
    }

    for (var i = 0; i < fieldNums.length; i++) {
        var fieldNum = fieldNums[i],
            newWidth = newWidths[i];
        if (adjustHeader) {
            var header = this.getFieldHeader(fieldNum),
                headerFieldNum = this.getLocalFieldNum(fieldNum);


            header.getMember(headerFieldNum).setOverflow("hidden");
            header.getMember(headerFieldNum).setWidth(newWidth);
        }

        var storeWidth = storeWidths[i];
        if (storeWidth == null || storeWidth) {
            this.fields[fieldNum].width = newWidth;
            this.fields[fieldNum].autoFitWidth = false;
        }

        // update the body column widths
        if (this._fieldWidths != null) {
            this._fieldWidths[fieldNum] = newWidth;
        }
    }

    if (adjustHeader) {

        this.header.instantRelayout = instantRelayout;
        this.header.reflowNow("resized headers");
        if (this.frozenHeader) {
            this.frozenHeader.instantRelayout = frozenInstantRelayout;
            this.frozenHeader.reflowNow("resized headers");
        }
    }


    // don't crash if not drawn
    if (!this.isDrawn()) return;

    this._settingBodyFieldWidths = true;
    this.setBodyFieldWidths(this._fieldWidths);


    // force an immediate redraw of the body - this will enable us to check the
    // _settingBodyFieldWidths flag in updateFieldWidths

    if (this.body.isDirty()) {
        this.body.redraw("setting body field widths");
    }
    delete this._settingBodyFieldWidths;

    // Field widths can effect rendered heights of titles due to wrapping/reflow
    // if we're autoFitting to header heights we may need to recalculate now.

    if (!this._autoFittingFields && this.header && this.autoFitHeaderHeights) {
        this.dropCachedHeaderButtonHeights();
        this._updateHeaderHeight();
    }
    if (this.isRTL() && this.header != null) {
        this.syncBodyScrolling();
    }

    if (this.showFilterEditor && this.filterEditor) {
        this.filterEditor._resizeFields(fieldNums, newWidths, storeWidths);
    }
    if (this.summaryRow && this.showGridSummary) {
        this.summaryRow._resizeFields(fieldNums, newWidths, storeWidths);
    }

    this.handleFieldStateChanged(true);
},

// Sorter (corner button)
// --------------------------------------------------------------------------------------------

// should we draw the corner sort button in the header?
_showSortButton : function () {
    var showSort = this.showSortArrow;
    if (showSort != null) {
        return (showSort == isc.ListGrid.CORNER || showSort == isc.ListGrid.BOTH);
    }
    // If we haven't been explicitly set to show the corner sort button, show it if we're
    // leaving a gap for the scrollbar.
    return this._shouldLeaveScrollbarGap();
},

// predictScrollbarGap - internal but non obfuscated flag - if true, when leaveScrollbarGap is false
// we predict whether the body will show a vertical scrollbar based on cellHeight and number of
// rows.
// This allows us to render the body at the appropriate width to accommodate a vertical scrollbar
// rather than waiting for the vertical scrollbar to be rendered (which may also introduce a
// horizontal scrollbar), then resizing.
predictScrollbarGap:true,

_shouldLeaveScrollbarGap : function () {
    var verticalAutoFit = (this.autoFitData == isc.Canvas.VERTICAL || this.autoFitData == isc.Canvas.BOTH);
    if (this.leaveScrollbarGap && !verticalAutoFit) return true;
    if (!this.body || this.isEmpty()) return false;

    if (this.body.vscrollOn) return true;

    var body = this.body,

        headerHeight = this.getHeaderHeight(),

        bodyHeight = this.getInnerHeight() - this.body.getVMarginBorder() -
                        (this.showHeader ? headerHeight : 0);

    // exception case - when we're auto-fitting to content the body will be allowed to expand
    if (verticalAutoFit) {
        bodyHeight = (this.getTotalRows() * this.cellHeight);
        var maxHeight = this.getAutoFitMaxBodyHeight();
        if (maxHeight && maxHeight < bodyHeight) {
            bodyHeight = maxHeight;
        }
        if (this.autoFitMaxRows && (this.autoFitMaxRows * this.cellHeight) > bodyHeight) {
            bodyHeight = (this.autoFitMaxRows * this.cellHeight);
        }
    }
    //this.logWarn("predicting gap: bodyHeight: " + bodyHeight +
    //             ", rows: " + this.getTotalRows() +
    //             ", rowsHeight: " + (this.getTotalRows() * this.cellHeight))


    if (bodyHeight <= 0) return false;

    return this.predictScrollbarGap && this.bodyOverflow == isc.Canvas.AUTO &&
           (this.getTotalRows() * this.cellHeight > bodyHeight);
},

// The sorter is implemented as a child (not a member) that we float at the right edge of the
// header (Which we size small enough to accomodate it).

getSorterTop : function () {

    return (this.headerLayout || this.header).getTop();
},
getSorterLeft : function () {
    if (this.isRTL()) {
        return this.getLeftMargin() + this.getLeftBorderSize() + this.getLeftPadding();;
    } else {
        // clamp the sorter to the right of the header (or header-layout)
        var headerLayout = (this.headerLayout || this.header);
        var left = headerLayout.getWidth() + headerLayout.getLeft() - this._getSorterWidth();
        if (this._shouldLeaveScrollbarGap()) {
            left += this.body != null ? this.body.getScrollbarSize() : this.getScrollbarSize();
        }
        return left;
    }
},

_getSorterWidth : function (recompute) {
    var sorter = this.sorter;
    if (recompute || sorter == null) {
        var scrollbarSize = this.body != null ? this.body.getScrollbarSize() : this.getScrollbarSize(),
            sorterWidth;
        if (scrollbarSize <= 0) sorterWidth = 16;
        else sorterWidth = Math.max(5, scrollbarSize);
        return sorterWidth;
    } else {
        return sorter.getWidth();
    }
},

//> @method listGrid.makeCornerSortButton() (A)
// Make the sorter button
// @group drawing
//<
makeCornerSortButton : function () {


    this.sorter = this.createAutoChild("sorter",
        isc.addProperties(this.getButtonProperties(),
            // Explicitly lay sorterDefaults on top of the buttonProperties.
            // If we didn't do this, sorterDefaults would be picked up as defaults but would
            // be overridden by the standard button properties, which means headerDefaults
            // would override sorterDefaults for the button
            this.sorterDefaults,
            {
                ID:this.getID()+"_sorter",
                width:this._getSorterWidth(),
                height:this.getHeaderHeight(),

                backgroundColor:this.headerBackgroundColor,
                imgDir:this.widgetImgDir,
                visibility:(this._showSortButton() ? isc.Canvas.INHERIT : isc.Canvas.HIDDEN),
                ariaState: { label: this.sorterButtonTitle }
            }
        )
    );
    this.addChild(this.sorter, "sorter", false);
},

//> @method listGrid.sorterContextClick() (A)
// Notification method fired when the user right-clicks on the corner
// +link{listGrid.sorterConstructor, sort button}.  Return false to suppress the default
// behavior of showing the sorter's context menu.
//
// @return (boolean) return false to suppress the context menu
// @group events, sorting
// @visibility external
//<

_sorterContextClick : function () {
    // Returning false from sorterContextClick suppresses both the
    // custom context menu and the native context menu

    if (this.sorterContextClick && !this.sorterContextClick()) return false;

    var showContextMenu = this.showHeaderContextMenu;
    //>!BackCompat 2007.02.02
    // If the old showCornerContextMenu attr is defined, have it take precedence
    if (this.showCornerContextMenu != null) showContextMenu = this.showCornerContextMenu;
    //<!BackCompat
    if (showContextMenu) return this.displayHeaderContextMenu(this.sorter);
},

// if the header context menu is enabled, also show it on the headerBar
headerBarContextClick : function (header) {
    var showContextMenu = this.showHeaderContextMenu;
    //>!BackCompat 2007.02.02
    // If the old showCornerContextMenu attr is defined, have it take precedence
    if (this.showCornerContextMenu != null) showContextMenu = this.showCornerContextMenu;
    //<!BackCompat
    if (showContextMenu && (isc.EH.isKeyEvent() || header.containsEvent())) {
        var index = isc.EH.isKeyEvent() ? header.getFocusButtonIndex() :
                                          header.getMouseOverButtonIndex();
        return this.displayHeaderContextMenu(header.getMember(index));
    }
},

headerSpanContextClick : function (span) {
    var menuItems = this.getHeaderSpanContextMenuItems(span);
    if (!menuItems || menuItems.length == 0) return false;
    if (!this.spanContextMenu) {
        this.spanContextMenu = this.createAutoChild(
            "spanContextMenu",
            {
                items: menuItems,
                autoFitFieldWidths: true
            },
            this.getMenuConstructor());
    } else {
       this.spanContextMenu.setItems(menuItems);
    }
    this.spanContextMenu.showContextMenu();
    return false;
},


//> @method ListGrid.displayHeaderContextMenu()
// If +link{attr:listGrid.showHeaderContextMenu} is <code>true</code> this method is fired
// when the user right-clicks on the header for this grid.<br>
// Default implementation will display a menu with entries derived from
// +link{listGrid.getHeaderContextMenuItems()} for the appropriate column.
// @param target (Canvas) which button in the header received the right-click event
//                        (may be the sorter button or header menu button)
// @param [position] (Array) Optional 2-element array specifying position at which the menu should
//                           be shown. If this is not passed in the menu will be shown at the
//                           mouseEvent position (default context menu behavior).
// @visibility external
// @group gridHeader
// @see listgrid.showHeaderContextMenu
//<
displayHeaderContextMenu : function (headerButton, position) {
    // headerButton will be null when contextClick appears on header background
    if (!headerButton) return;
    // don't display a header context menu for the checkbox column
    var field = this.fields[headerButton.masterIndex];
    if (field && field.showDefaultContextMenu == false) return false;
    // Determine the items to show
    var items = this.getHeaderContextMenuItems(headerButton.masterIndex);
    // If there are no items don't show the menu
    if (items.length == 0) return;

    if (!this._cornerMenu) this._cornerMenu = this.getHeaderContextMenu(headerButton);
    this._cornerMenu.setData(items);
    this._showingHeaderContextMenu = true;
    if (position != null && position.length > 0) {
        this._cornerMenu.target = headerButton;
        this._cornerMenu.moveTo(0,0);
        this._cornerMenu.setVisibility("hidden");
        if (!this._cornerMenu.isDrawn()) {

            this._cornerMenu._showOffscreen();
            this._cornerMenu.draw();
        } else {
            this._cornerMenu.redraw();
        }
        this._cornerMenu.placeNear(position[0], position[1]);
        this._cornerMenu.show();
        return false;
    } else {
        return this._cornerMenu.showContextMenu(headerButton);
    }
},



//> @attr ListGrid.sortFieldAscendingText (string : "Sort Ascending" : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid, this
// attribute will be shown as the menu item title to sort a field in ascending order.
// @group i18nMessages
// @visibility external
//<
sortFieldAscendingText: "Sort Ascending",

//> @attr ListGrid.sortFieldDescendingText (string : "Sort Descending" : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid, this
// attribute will be shown as the menu item title to sort a field in descending order.
// @group i18nMessages
// @visibility external
//<
sortFieldDescendingText: "Sort Descending",

//> @attr ListGrid.clearSortFieldText (string : "Clear Sort" : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid, this
// attribute will be shown as the menu item title to clear an existing sort on this field.
// @group i18nMessages
// @visibility external
//<
clearSortFieldText: "Clear Sort",

//> @attr ListGrid.clearAllSortingText (string : "Clear All Sorting" : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid, this
// attribute will be shown as the menu item title to clear any existing sort on all fields.  This
// menu-item is displayed only in the context menu for the sorter button.
// @group i18nMessages
// @visibility external
//<
clearAllSortingText: "Clear All Sorting",

//> @attr ListGrid.clearFilterText (string : "Clear Filter" : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid,
// and a +link{listGrid.showFilterEditor, filter-editor} is visible, this
// attribute will be shown as the menu item title to clear any existing filter.  This
// menu-item is displayed only in the context menu for the sorter button.
// @group i18nMessages
// @visibility external
//<
clearFilterText: "Clear Filter",

//> @attr ListGrid.configureSortText (string : "Configure Sort..." : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid,
// and multi-sorting is enabled, this attribute is used as the title for a menu item that opens
// a +link{class:MultiSortDialog} to configure the sort-specification for this grid.  This
// menu-item is displayed only in the context menu for the sorter button.
// @group i18nMessages
// @visibility external
//<
configureSortText: "Configure Sort...",

//> @attr ListGrid.configureGroupingText (string : "Configure Grouping..." : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid,
// and multi-grouping is enabled, this attribute is used as the title for a menu item that opens
// a +link{class:MultiGroupDialog} to configure the grouping for this grid.
//
// @group i18nMessages
// @visibility external
//<
configureGroupingText: "Configure Grouping...",

//> @attr ListGrid.autoFitFieldText (string : "Auto Fit" : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid,
// and +link{listGrid.canAutoFitFields} is true, this attribute will be shown as the menu
// item title for an item to perform a one-time autoFit of the field to its title or content
// via a call to +link{listGrid.autoFitField()}.
// @group i18nMessages
// @group autoFitFields
// @visibility external
//<
autoFitFieldText: "Auto Fit",

//> @attr ListGrid.autoFitAllText (string : "Auto Fit All Columns" : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid,
// and +link{listGrid.canAutoFitFields} is true, this attribute will be shown as the menu
// item title for an item to perform a one-time autoFit of all visible fields via
// the +link{listGrid.autoFitField()} method.
// @group i18nMessages
// @group autoFitFields
// @visibility external
//<
autoFitAllText: "Auto Fit All Columns",

//> @attr ListGrid.fieldVisibilitySubmenuTitle (string : "Columns" : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid, and
// +link{listgrid.canPickFields,this.canPickFields} is true, this
// attribute will be shown as the title for the menu item which contains a submenu with items
// allowing the user to show and hide fields in the grid.
// @group i18nMessages
// @visibility external
//<
fieldVisibilitySubmenuTitle: "Columns",

//> @attr listGrid.freezeFieldText (HTMLString : "Freeze ${title}" : IRWA)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid and
// +link{listGrid.canFreezeFields,this.canFreezeFields} is true, this string will be shown as the
// title for the menu item to freeze a currently unfrozen field.
// <P>
// This is a dynamic string - text within <code>&#36;{...}</code> will be evaluated as JS code
// when the message is displayed, with <code>title</code> available as a variable containing
// the field title.
// <P>
// Default value returns "Freeze " + the field's summary title.
// @group i18nMessages
// @visibility external
//<
freezeFieldText:"Freeze ${title}",

//> @attr listGrid.unfreezeFieldText (HTMLString : "Unfreeze ${title}" : IRWA)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid and
// +link{listGrid.canFreezeFields,this.canFreezeFields} is true, this string will be shown as the
// title for the menu item to unfreeze a currently frozen field.
// <P>
// This is a dynamic string - text within <code>&#36;{...}</code> will be evaluated as JS code
// when the message is displayed, with <code>title</code> available as a variable containing
// the field title.
// <P>
// Default value returns "Unfreeze " + the field's summary title.
// @group i18nMessages
// @visibility external
//<
unfreezeFieldText:"Unfreeze ${title}",

//> @method listGrid.getToggleFreezeText()
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid and
// +link{listGrid.canFreezeFields,this.canFreezeFields} is true, this string will be shown as the
// title for the menu item to toggle whether a field is frozen or unfrozen.
// <P>
// Default implementation evaluates and returns +link{ListGrid.freezeFieldText} or
// +link{ListGrid.unfreezeFieldText} depending on whether the field is currently frozen.
// @param field (ListGridField) field to get the menu item title for
// @return (HTMLString) Title to show in the menu item
// @group i18nMessages
// @visibility external
//<
getToggleFreezeText : function (field, span) {
    var messageArgs = {
        viewer:this,
        title: span ? span.title : this.getSummaryTitle(field)
    };
    return (!this._suppressedFrozenFields && field.frozen)
            ? this.unfreezeFieldText.evalDynamicString(this, messageArgs)
            : this.freezeFieldText.evalDynamicString(this, messageArgs);
},

//> @attr listGrid.groupByText (HTMLString : "Group by ${title}" : IRWA)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid and
// +link{listGrid.canGroupBy,this.canGroupBy} is true, this string will be shown as the
// title for the menu item to toggle the group by setting for a field.
// <P>
// This is a dynamic string - text within <code>&#36;{...}</code> will be evaluated as JS code
// when the message is displayed, with <code>title</code> available as a variable containing
// the field title.
// <P>
// Default value returns "Group by " + the field's summary title.
// @group i18nMessages
// @visibility external
//<
groupByText : "Group by ${title}",

//> @method listGrid.getGroupByText()
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid and
// +link{listGrid.canGroupBy,this.canGroupBy} is true, this string will be shown as the
// title for the menu item to toggle the group by setting for a field.<br>
// Default implementation evaluates and returns the dynamic +link{listGrid.groupByText}
// string.
// @param field (ListGridField) field to get the menu item title for
// @return (HTMLString) Title to show in the menu item
// @group i18nMessages
// @visibility external
//<
getGroupByText : function (field) {
    var messageArgs = {
        field:field,
        title: this.getSummaryTitle(field),
        viewer:this
    }
    return this.groupByText.evalDynamicString(this, messageArgs);
},




//> @attr ListGrid.ungroupText (string : "Ungroup" : IRW)
// If we're showing a +link{listGrid.showHeaderContextMenu,headerContextMenu} for this grid, and
// +link{listgrid.isGrouped,this.isGrouped} is true, this
// attribute will be shown as the title for the menu item to ungroup the grid.
// @group i18nMessages
// @visibility external
//<
ungroupText: "Ungroup",


_shouldGroupByField : function (field) {
    var field = this.getField(field);
    return !!(field && this._canGroupByField(field) &&
              this.data.getLength() <= this.groupByMaxRecords);
},
_canGroupByField : function (field) {
    var field = this.getField(field);
    return !!(field &&
              ((this.canGroupBy == true && field.canGroupBy != false) ||
               (this.canGroupBy != false && field.canGroupBy == true)));
},



//> @method listGrid.getHeaderSpanContextMenuItems()
// Return the menus items that should be shown in a menu triggered from a
// +link{listGrid.headerSpans,headerSpan}.
//
// @param headerSpan (HeaderSpan) the component representing the headerSpan.  This component will
//                            have all the properties specified via +link{listGrid.headerSpans}.
// @return (Array of MenuItem) return false instead to avoid showing a menu
//
// @group headerSpan
// @visibility external
//<
getHeaderSpanContextMenuItems : function (span) {
    if (!this.showTreeColumnPicker && !this.showHeaderSpanContextMenu) return false;

    var menuItems = [];
    if (this.canPickFields) {
        menuItems[0] = {
            title: this.fieldVisibilitySubmenuTitle,
            submenu: this.getColumnPickerItems(),
            icon: "[SKINIMG]actions/column_preferences.png"
        };
    }

    var grid = this,
        fields = this.getSpannedFields(span),
        frozen
    ;
    if (fields) {
        for (var i = 0; i < fields.length; i++) {
            var fieldName = fields[i],
                field = this.getField(fieldName)
            ;
            if (field && !this._suppressedFrozenFields && field.frozen) {
                frozen = true;
                break;
            }
        }
    }

    // show menu for freezing and unfreezing spans.  Don't allow all fields to be frozen.
    if (this.canFreezeFields && this.fields.length > 1 && !span.parentSpan) {
        // can't freeze the last unfrozen field etc.
        if (frozen || this.frozenFields == null || this.normalFields.length > fields.length) {
            if (menuItems.length > 0) menuItems.add({isSeparator: true});
            menuItems.add({
            // method to get field from completeFields?
                title: this.getToggleFreezeText(
                        this.completeFields.find(this.fieldIdProperty,fields[0]),
                        span),
                grid:this,
                spanFields:fields,
                frozen: frozen,
                icon:(frozen ? "[SKINIMG]actions/unfreeze.png" :
                                    "[SKINIMG]actions/freezeLeft.png"),
                click: function () {
                    var spanFields = this.spanFields;
                    for (var i = 0; i < spanFields.length; i++) {
                        grid.completeFields.find(grid.fieldIdProperty, spanFields[i]).frozen = !this.frozen;
                    }
                    grid.rebuildForFreeze();
                }
            });
        }
    }

    return menuItems;
},

shouldShowColumnPicker : function (fld) {
    // don't allow hiding treeField columns or those marked canHide: false (now includes the checkboxField)
    // - it doesn't make much sense and is not currently supported
    if (fld.treeField || fld.canHide === false) return false;

    var title = this.getSummaryTitle(fld);

    // don't allow hiding columns with no title (because the menu item would then appear
    // blank as well)
    if (title == null || isc.isAn.emptyString(title)) return false;

    return true;
},

getColumnPickerItems : function () {
    var showColumns = [],
        grid = this
    ;

    // create show/hide columns menu items
    for (var i = 0; i < this.completeFields.length ; i++) {
        var fld = this.completeFields[i];

        if (!this.shouldShowColumnPicker(fld)) continue;

        var title = this.getSummaryTitle(fld);

        var spanConfig = this.spanMap ? this.spanMap[fld.name] : null;
        if (this.showTreeColumnPicker && spanConfig) {
            var spanHierarchy = [spanConfig],
                parentSpan = spanConfig.parentSpan;

            while (parentSpan != null) {
                spanHierarchy.add(parentSpan);
                parentSpan = parentSpan.parentSpan;
            }
            var indent = "";
            for (var ii = spanHierarchy.length-1; ii >= 0; ii--) {
                var itemConfig = spanHierarchy[ii];
                // If we're already stuck the span into the menu skip it.
                if (!showColumns.find("spanConfig",itemConfig)) {

                    // Otherwise output span toggle if we haven't already

                    // Interaction with canHide:false fields: If an item spans
                    // canHide:false fields only, omit it from the menu entirely.
                    // If it spans a mixture of canHide:false / canHide:true fields,
                    // show the item and have it toggle just the canHide:true fields.
                    // If all fields are hidden other than canHide:false fields, show
                    // the checkbox (the span *is* visible after all), but have a click
                    // on the item show the other fields.
                    showColumns.add({
                        title: indent + itemConfig.title,
                        // don't auto-dismiss, so that users can show and hide multiple columns at once
                        autoDismiss: false,

                        spanConfig: itemConfig,
                        click : function spanMenu_click (target, item, menu) {
                            var spannedFields = grid.getSpannedFields(item.spanConfig, true);
                            var hide = item.anySpannedFieldsVisible(true);

                            if (hide) grid.hideFields(spannedFields.duplicate())
                            else grid.showFields(spannedFields.duplicate());
                            menu.body.delayCall("markForRedraw");
                        },
                        anySpannedFieldsVisible :function spanMenu_anySpannedFieldsVisible (canHideOnly) {
                            var spannedFields = grid.getSpannedFields(this.spanConfig, canHideOnly);
                            for (var i = 0; i < spannedFields.length; i++) {
                                if (grid.fieldIsVisible(spannedFields[i])) return true;
                            }
                            return false;
                        },
                        enableIf : function spanMenu_enableIf (target, menu, item) {
                            var itemConfig = item.spanConfig;
                            // enabled if any fields *not* in the span are visible
                            var spannedFields = grid.getSpannedFields(itemConfig);
                            for (var i = 0; i < menu.data.length; i++) {
                                var item = menu.data[i];
                                if (item.fieldName && grid.fieldIsVisible(item.fieldName) &&
                                    !spannedFields.contains(item.fieldName)) return true;
                            }
                            return false;
                        },
                        checkIf : function (target, menu, item) {
                            return item.anySpannedFieldsVisible();
                        }
                    });
                }

                indent += "&nbsp;&nbsp;&nbsp;&nbsp;"
            }
            title = indent + title;
        }

        showColumns.add({
            title: title,
            fieldName: fld.name,
            prompt: fld.prompt,
            // don't auto-dismiss, so that users can show and hide multiple columns at once
            autoDismiss: false,
            checkIf : function (target, menu, item) {
                return grid.fieldIsVisible(item.fieldName);
            },
            enableIf : function (target, menu, item) {
                var currentlyVisibleFields = 0;

                for (var i = 0; i < grid.fields.length; i++) {
                    var field = grid.fields[i];
                    // If this field is visible and its an actual field object and not
                    // a special field such as the row number, checkbox or delete column,
                    // then lets count it as a visible field.
                    if (grid.fieldIsVisible(field) && field._isFieldObject) {
                        currentlyVisibleFields++;
                    }
                }

                return !(currentlyVisibleFields === 1 && grid.fieldIsVisible(item.fieldName));
            },
            click : function (target,item,menu) {
                // if we're being shown as a submenu of the grouping menu, disable group
                // item if it is the same as the item who will no longer be visible
                var  parentMenu = menu._parentMenu,
                     groupItemIndex = parentMenu ?
                                     parentMenu.data.findIndex("groupItem", true) : null,
                     groupItem = parentMenu ? parentMenu.data[groupItemIndex] : null,
                     togglingCurrentField = (groupItem && groupItem.fieldName == item.fieldName)
                ;

                if (grid.fieldIsVisible(item.fieldName)) {
                    // make sure length of fields doesn't count checkbox field
                    var fieldsLen = grid.fields.length;
                    if (grid.getCurrentCheckboxField() != null) fieldsLen -= 1;
                    // Don't hide the last visible field
                    if (fieldsLen > 1) {
                        grid.hideField(item.fieldName);
                    }
                    // Disable the last checked column, as we don't want it to be able to be
                    // hidden
                    if (fieldsLen == 1) {
                        var visibleItem = menu.data.find("fieldName", grid.fields[0].name);
                        menu.setItemEnabled(visibleItem, false);
                        this._disabledItem = visibleItem;
                    }
                } else {
                    grid.showField(item.fieldName);
                    // if there is a disabled item, enable it, because now we're guaranteed that
                    // its no longer the last item
                    if (this._disabledItem) {
                        menu.setItemEnabled(this._disabledItem, true);
                        this._disabledItem = null;
                    }
                }
                if (togglingCurrentField) {
                    parentMenu.setItemEnabled(groupItemIndex,
                                              grid._shouldGroupByField(groupItem.fieldName));
                }
                menu.body.delayCall("markForRedraw");

            }
        });
    }

    return showColumns;
},

getColumnPickerMenu : function (showColumns) {
    var showColumnsMenu = {
        canHover:true,
        showIcons:true,
        showHover:true,
        cellHoverHTML:function (record) {
            return record.prompt;
        },
        items:showColumns
    };

    return {
        title: this.fieldVisibilitySubmenuTitle,
        submenu: showColumnsMenu,
        icon: "[SKINIMG]actions/column_preferences.png"
    }
},

//> @method ListGrid.getHeaderContextMenuItems()
// If +link{attr:listGrid.showHeaderContextMenu} is <code>true</code> this method returns
// the menu items to be displayed in the default header context menu.
// <P>
// This method will be called each time the menu is displayed, allowing for dynamic content
// depending on the current state of the fields.
// <P>
// The default set of menu items will includes menu items for freezing fields, showing and
// hiding fields, grouping by fields, or other listGrid features
//
// @param [fieldNum] (integer) Index of the field the user clicked in the
//    +link{listgrid.fields,fields} array. <b>Note:</b> if the user right-clicked the sorter button
//    this parameter will be <code>null</code>.
//
// @return (Array of MenuItem)
// @group gridHeader
// @visibility external
//<
getHeaderContextMenuItems : function (fieldNum) {
    var field = this.getField(fieldNum);

    var menuItems = [],
        needSeparator = false
    ;

    var canSort = this._canSort(field);

    if (canSort || !field) {
        if (field) {
            var sortFieldNum = fieldNum != null ? fieldNum : this._getSortFieldNum();
            menuItems[0] = {
                title: this.sortFieldAscendingText,
                icon: "[SKINIMG]actions/sort_ascending.png",
                click: "menu.doSort(" + sortFieldNum + ", 'ascending')"
            };
            menuItems[1] = {
                title: this.sortFieldDescendingText,
                icon: "[SKINIMG]actions/sort_descending.png",
                click: "menu.doSort(" + sortFieldNum + ", 'descending')"
            };
            needSeparator = true;
        }
        if (this._canMultiSort()) {
            menuItems.add({
                title: this.configureSortText,
                field: field,
                click : function (target, item, menu) {
                    menu.grid.askForSort(this.field == null ? null : this.field[menu.grid.fieldIdProperty]);
                }
            });
            needSeparator = true;
            if (!field || this.isSortField(field[this.fieldIdProperty])) {

                menuItems.add({
                    title: field ? this.clearSortFieldText : this.clearAllSortingText,
                    field: field,
                    enableIf : function (target, menu, item) {
                        return (field || (!field && menu.grid.getSortFieldCount() > 0));
                    },
                    click: field ? "menu.doSort(" + sortFieldNum + ", 'unsort')" :
                        "menu.grid.clearSort();"
                });
                needSeparator = true;
            }
        }
    }

    // Note if autoFitWidth is already set on this field we still want to show the
    // option to force a re-run of the autofit logic. This is required to handle the
    // case where we autoFit to the rendered rows but the user scrolls new rows into
    // view which aren't in the draw area and are clipped. In this case we want the user to
    // have the option to re-auto-fit
    var showAutoFit = this.canAutoFitFields && field;
    if (showAutoFit) {
        menuItems.add({
            title:this.autoFitAllText,
            click:"menu.grid.autoFitFields()"
        });
        menuItems.add({
            title: this.autoFitFieldText,
            click: "menu.grid.autoFitField(" + fieldNum + ",true);"
        });
    }

    if (!field && this.showFilterEditor) {
        if (needSeparator) menuItems.add({ isSeparator: true });
        menuItems.add({
            title: this.clearFilterText,
            click: "menu.grid.clearFilterItemClick();"
        });
        needSeparator = true;
    }
    // add column visibility chooser
    if (this.canPickFields && this.completeFields.length > 1) {
        if (this.useAdvancedFieldPicker &&
            this.completeFields.length >= this.advancedFieldPickerThreshold)
        {
            menuItems.add(this.createFieldPickerWindowMenuItem(
                this.fieldVisibilitySubmenuTitle + "..."));

        } else {
            var showColumns = this.getColumnPickerItems();
            if (showColumns.length > 0) {
                // Never allow the user to hide the last visible field in the grid.
                // If we can toggle visibility on more than one field, show the picker
                var show = showColumns.length > 1;
                // Otherwise only show the picker if there's at least 1 additional
                // (canHide:false) field visible.
                // This prevents the user from hiding all fields in the grid.
                if (!show) {
                    var fieldName = showColumns[0].fieldName;
                    for (var i = 0; i < this.fields.length; i++) {
                        if (this.fields[i].name != fieldName) {
                            show = true;
                            break;
                        }
                    }
                }
                if (show) {
                if (needSeparator) menuItems.add({ isSeparator: true } );
                menuItems.add(this.getColumnPickerMenu(showColumns));
                needSeparator = true;
            }
        }
    }
    }
    // add group option
    // canGroupBy a field if:
    // - this.canGroupBy is true and field.canGroupBy is not false
    // - this.canGroupBy is unset and field.canGroupBy is true
    var canGroupBy = field && this._canGroupByField(field),
        // only consider grouped if current field is not marked canGroupBy: false.
        // this makes no grouping/ungrouping options show up for the field.
        isGrouped = (this.canGroupBy != false) && field && field.canGroupBy != false
                    && this.isGrouped;
    if (needSeparator && (isGrouped || canGroupBy)) {
        menuItems.add({ isSeparator: true } );
    }

    if (canGroupBy) {

        // if there is no custom getGroupValue defined, check if there is a custom groupingModes
        // or else fallback to the default simpleType one - if using the simpleType and it has
        // a getGroupingModes() method, run that, otherwise, just assume groupingModes
        var groupingModes = field.groupingModes ? field.groupingModes :
            (!field.getGroupValue ? ( field._simpleType ? (field._simpleType.getGroupingModes ?
                                                           field._simpleType.getGroupingModes() :
                                                           field._simpleType.groupingModes) : false )
             : false);

        var groupBySubmenu = null;
        if (groupingModes) {
            groupBySubmenu = [];
            var addSubmenuItem = function (mode, title) {
                if (!title) title = mode;
                groupBySubmenu.add({
                    title: title,
                    groupType: mode,
                    targetField: field,
                    fieldName: field.name,
                    prompt: field.prompt,
                    checked: field.groupingMode == mode,
                    //TODO> check if this field is actually grouped but with different option
                    click: function(target, item, menu) {
                        this.targetField.groupingMode = this.groupType;
                        menu._parentMenu.groupField(item);
                    }
                });
            }
            // valueMap support: either an array or object of property bindings
            if (isc.isAn.Array(groupingModes)) {
                for (var i = 0; i < groupingModes.length; i++) {
                    addSubmenuItem(groupingModes[i]);
                }
            } else if (isc.isAn.Object(groupingModes)) {
                for (var property in groupingModes) {
                    addSubmenuItem(property, groupingModes[property]);
                }
            }
        }
        menuItems.add({
            groupItem: true,
            title: this.getGroupByText(field),
            fieldName: field.name,
            targetField: field,
            prompt: field.prompt,
            icon: "[SKINIMG]actions/groupby.png",
            click: function(target, item, menu) {
                this.targetField.groupingMode = this.targetField.defaultGroupingMode || null;
                menu.groupField(item);
            },
            enabled: this._shouldGroupByField(field),
            canSelectParent: true,
            submenu: groupBySubmenu
        });
        if (this.canMultiGroup) {
            var grid = this;
            menuItems.add({
                title: this.configureGroupingText,
                icon: "[SKINIMG]actions/groupby.png",
                targetField: field,
                click: function(target, item, menu) {
                    grid.configureGrouping(this.targetField);
                },
                enabled: this._shouldGroupByField(field)
            });
        }
        needSeparator = true;
    }
    // if grid is grouped, show ungroup option
    if (isGrouped) {
        menuItems.add({
            title: this.ungroupText,
            click: "menu.ungroup()",
            icon: "[SKINIMG]actions/ungroup.png"
        });
        needSeparator = true;
    }

    if (field) {
        // show menu for freezing and unfreezing fields.  Don't allow all fields to be frozen.
        if (this.canFreezeFields && this.fields.length > 1 &&
            (field.frozen ||
             (this.frozenFields == null || this.normalFields.length > 1)) &&
             // disallow freezing of spanned fields - can only freeze the whole spanned set
             // together
             !(this.spanMap && this.spanMap[field.name]) &&
             // allow listGridField.canFreeze to prevent freeze option from appearing in context menu
             field.canFreeze != false)
        {

            if (needSeparator) menuItems.add({ isSeparator: true });

            menuItems.add({
                title: this.getToggleFreezeText(field),
                grid: this,
                field: field,
                icon: (field.frozen ? "[SKINIMG]actions/unfreeze.png" : "[SKINIMG]actions/freezeLeft.png"),
                click: "item.grid.toggleFrozen(this.field,!this.field.frozen)"
            });

            needSeparator = true;
        }
    }

    if (this.canAddFormulaFields) {
        if (needSeparator) menuItems.add({ isSeparator: true });

        if (field && field.userFormula) {
            menuItems.add({
                title: this.editFormulaFieldText,
                grid: this,
                field: field,
                click: "item.grid.editFormulaField(item.field)",
                icon: "[SKINIMG]ListGrid/formula_menuItem.png"
            });

            menuItems.add({
                title: this.removeFormulaFieldText,
                grid: this,
                field: field,
                click: "item.grid.removeField(item.field.name)",
                icon: "[SKINIMG]ListGrid/formula_menuItem.png"
            });
        }

        menuItems.add({
            title: this.addFormulaFieldText,
            grid: this,
            click: "item.grid.addFormulaField()",
            icon: "[SKINIMG]ListGrid/formula_menuItem.png"
        });
        needSeparator = true;
    }

    if (this.canAddSummaryFields) {
        if (needSeparator) menuItems.add({ isSeparator: true });

        if (field && field.userSummary) {
            menuItems.add({
                title: this.editSummaryFieldText,
                grid: this,
                field: field,
                click: "item.grid.editSummaryField(item.field)",
                icon: "[SKINIMG]ListGrid/formula_menuItem.png"
            });

            menuItems.add({
                title: this.removeSummaryFieldText,
                grid: this,
                field: field,
                click: "item.grid.removeField(item.field.name)",
                icon: "[SKINIMG]ListGrid/formula_menuItem.png"
            });
        }

        menuItems.add({
            title: this.addSummaryFieldText,
            grid: this,
            click: "item.grid.addSummaryField()",
            icon: "[SKINIMG]ListGrid/formula_menuItem.png"
        });
    }

    var canEditHilites = this.canEditHilites;
    if (canEditHilites) {
        if (needSeparator) menuItems.add({ isSeparator: true });

        menuItems.add({
            title: this.editHilitesText,
            grid: this,
            click: "item.grid.editHilites()"
        });
    }

    return menuItems;
},

// clearFilterItemClick - fired when the user clicks the "clear filter" menu item
clearFilterItemClick : function () {
    this.setFilterEditorCriteria(null);
    this.filterByEditor();
},


//> @method ListGrid.getHeaderContextMenu()
// If +link{attr:listGrid.showHeaderContextMenu} is <code>true</code> this method returns
// the menu to display when the user right-clicks on the header or corner sort button of
// this listGrid.<br>
// Default implementation displays the list of available columns, and allows the user to
// change the visibility of each column by selecting the appropriate fieldName.
// @group gridHeader
// @visibility internal
//<
// Leave this internal for now - If a developer wishes to display a custom header context menu
// they can override displayHeaderContextMenu() instead.
// Overriding this method is not as clean - the developer would also need to override
// getHeaderContextMenuItems(), [which will be called each time the menu is actually shown]
headerContextMenuConstructor:"Menu",
headerContextMenuDefaults:{
    // On Hide, if we're showing the headerMenuButton, hide it
    hide : function () {
        this.Super("hide", arguments);
        if (!isc.Browser.isTouch && this.grid) {
            this.grid._showingHeaderContextMenu = false;
            this.grid._hideHeaderMenuButton();
        }
    },
    // sorting
    doSort : function (fieldNum, direction) {
        if (direction == "unsort") this.grid.toggleSort(this.grid.getFieldName(fieldNum), direction);
        else this.grid.sort(fieldNum, direction);
    },
    canHover:true,
    showHover:true,
    cellHoverHTML : function(record,rowNum,colNum) { return record.prompt },
    groupField : function (item) {
        var grid = this.grid,
            gridGroupByFields = grid.getGroupByFields();
        if ((item.targetField && item.targetField.groupingMode) ||
            !(gridGroupByFields && gridGroupByFields.length == 1 &&
              gridGroupByFields.contains(item.fieldName)))
        {
            // If a regroup using a different groupingMode is requested, this condition
            // prevented it from regrouping.
            grid.groupBy(item.fieldName);
        }
    },

    ungroup : function () {
        this.grid.ungroup();
    }
},
getHeaderContextMenu : function () {
    return this.createAutoChild("headerContextMenu", {
        ID:this.getID() + "_cornerMenu",
        grid : this
    }, this.getMenuConstructor());
},

//> @method listGrid.getSortArrowImage() (A)
// Return the sort arrow image used in the sorter button and the current sort col header button.
// @group drawing
// @return (ImgProperties | HTMLString) HTML for sorter button
//<
getSortArrowImage : function (fieldNum, returnImg, clipTitle, wrap) {
    if (fieldNum == null) fieldNum = this._getSortFieldNum();
    var img = null;
    if (this.showSortArrow != false && fieldNum != null) {
        var field = this.getField(fieldNum);
        var fieldDirection = field && field.sortDirection;
        img = Array.shouldSortAscending(fieldDirection)
              ? this.sortAscendingImage
              : this.sortDescendingImage;
    }
    if (returnImg) return img;

    if (img != null) {
        return this.imgHTML(isc.addProperties({
            imgDir: this.widgetImgDir,
            extraCSSText: (clipTitle && this.clipHeaderTitles) || wrap
                          ? (this.isRTL() ? "float:left;margin-right:4px" : "float:right;margin-left:4px")
                          : null
        }, img));
    } else {
        return isc.Canvas.spacerHTML(1, 1);
    }
},

//> @method listGrid.sorterClick() (A)
// Notification method fired when the user clicks on the corner
// +link{listGrid.sorterConstructor, sort button}.  Return false to suppress the sort.
//
// @return (boolean) return false to suppress the sort
// @group events, sorting
// @visibility external
//<

_sorterClick : function () {
    if (this.sorterClick && !this.sorterClick()) return;

    var sortFieldNum = this._getSortFieldNum();

    // if the list can't sort, return false
    if (!this._canSort(sortFieldNum)) return false;

    if (sortFieldNum != null) {
        var sortField = this.getField(sortFieldNum),
            fieldName = sortField[this.fieldIdProperty],
            globalSortDir = this._baseSortDirection,
            sortDir = this._getFieldSortDirection(sortField)
        ;
        if (this.isSortField(fieldName)) {
            // we're already sorted - decide whether to toggle or unsort
            if (globalSortDir == sortDir || this.canUnsort == false) {
                this.toggleSort(fieldName);
            } else {
                this.toggleSort(fieldName, "unsort");
            }
        } else {
            this.setSort([{ property: fieldName, direction: globalSortDir }]);
        }
    } else {
        this.sort(
            sortFieldNum,
            (sortFieldNum != null ?
                !Array.shouldSortAscending(this.getField(sortFieldNum).sortDirection) :
                null)
        );
    }
},


shouldShowDragLineForRecord : function(recordNum) {
    return this.canReorderRecords && this.showDropLines != false;
},

// Drag Line
// --------------------------------------------------------------------------------------------
// Note that the row and column drag lines are the same Canvas

//>    @method    listGrid.showDragLineForRecord()    (A)
// Show the drag line relative to a particular record.
// <p>
// If no record number is passed, assumes the one under the mouse.
// <p>
// This is used to show feedback in reordering rows or to insert dragged records at a particular
// row.
//        @group    dragging, drawing
//<
showDragLineForRecord : function (recordNum, position) {
    if (!this.shouldShowDragLineForRecord(recordNum)) return;

    var body = this.body;

    // get the event record
    if (recordNum == null) recordNum = this.getEventRecordNum();

    // If 'recordNum' is -1, we're above the top of the list.
    // This is basically invalid - the user dragged over the header, so just bail.
    // Note: willAcceptDrop() will return false in this case - however
    // this method is called from dropMove() which is tripped by EH without checking
    // for the result of willAcceptDrop
    if (recordNum == -1) {
        if (this._dragLine) this._dragLine.hide();
        return;
    }

    // if 'recordNum' is -2 - we're in the blank area underneath the last record.  Show the
    // drop line underneath the last record.
    var lastVisibleRow = body.getVisibleRows()[1];
    if (recordNum == -2) recordNum = lastVisibleRow;

    // if the record is not enabled, you can't drop on it (unless you're trying to drop at the end
    // of the list, which is special)
    if (this._dragLine && recordNum != lastVisibleRow) {
        var record = this.getCellRecord(recordNum);
        if (!this.recordIsEnabled(record, recordNum)) {
            return this._dragLine.hide();
        }
    }

    // before, after (or over)?
    if (position == null) position = this.getRecordDropPosition(recordNum);
    // now move the dragLine to the proper place and show it
    var leftBody = this.getLeftBody();
    var left = leftBody.getPageLeft()
        + (this.isRTL() && leftBody.vscrollOn ? leftBody.getScrollbarSize() : 0);
    if (position == isc.ListGrid.BEFORE) {
        // drag line above record
        this.showHDragLine(left, body.getRowPageTop(recordNum));
        this._dragLine.afterRow = recordNum -1;
    } else if (position == isc.ListGrid.AFTER) {
        // drag line under record
        this.showHDragLine(left,
                           (body.getRowPageTop(recordNum) + body.getRowSize(recordNum)));
        this._dragLine.afterRow = recordNum;
    } else {
        // if we are not dropping before or after the record, hide the dragLine and bail.
        return this._dragLine.hide();
    }

    if (this.recordDropMove) {
        this.fireCallback("recordDropMove", "viewer,recordNum,record,position",
                          [this, recordNum, this.getRecord(recordNum), position])
    }

},

showHDragLine : function (pageLeft, pageTop) {
    // make sure the drag line is set up
    this.makeDragLine();
    // and resize it to span the viewport.  Note: needs to be based on the overall ListGrid,
    // not the body, since there may be multiple bodies with frozen columns.

    this._dragLine.resizeTo(
            this.getViewportWidth() - (this.body.vscrollOn ? this.body.getScrollbarSize() : 0), 2);

    // place it
    var bodyTop = this.body.getPageTop(),
        bodyBottom = bodyTop + this.body.getVisibleHeight();
    if (pageTop < bodyTop) pageTop = bodyTop;
    else if (pageTop > bodyBottom) pageTop = bodyBottom;
    this._dragLine.setPageRect(pageLeft, pageTop);

    // and show it
    this._dragLine.show();
    this._dragLine.bringToFront();
},


//>    @method    listGrid.showDragLineForField()    (A)
//        Show the drag line relative to a particular field.
//        If no field number is passed, assumes the one under the mouse.
//
//        This is used to show feedback in resizing or moving fields (columns).
//
//        @param    [fieldNum]        (number)    Number of the field to show line for.
//                                            Default is the field under the mouse.
//        @param    [headerOnly]    (boolean)    Show in the header only (true), or over the entire
//                                          list?  Default is over the entire list (false).
//
//        @group    dragging, drawing
//<
showDragLineForField : function (fieldNum, headerOnly) {
    // make sure the drag line is set up
    this.makeDragLine();
    // and resize it to cover the entire height of the listGrid
    if (headerOnly == true || !this.body) {
        this._dragLine.resizeTo(2, this.getHeaderHeight());
    } else {

        this._dragLine.resizeTo(2, this.getHeaderHeight() + this.body.getVisibleHeight());
    }

    // get the event record
    if (fieldNum == null) fieldNum = this.getEventFieldNum();

    if (fieldNum < 0) {
        this._dragLine.hide();
        return;
    }

    // now move the dragLine to the proper place and show it

    // dragLine placement:
    // - we shouldn't use body.getColumnLeft() or similar because the body could be displaying
    //   the empty message
    // - resize is from right edge in LTR, left edge in RTL
    // - since the header object may not be doing live redraw, use dragResizeWidth rather than
    //   current header widget width
    // - adjust by 1px to center the 2px-wide drag line
    var resizingField = this.ns.EH.dragTarget,
        left = (!this.isRTL() ?
            // resize is from right edge
            resizingField.getPageLeft() + this.ns.EH.dragResizeWidth :
            // resize if from left edge
            resizingField.getPageRight() - this.ns.EH.dragResizeWidth) - 1;

    this._dragLine.setPageRect(left, this.getPageTop()
                                     + (this.showFilterEditor ? this.filterEditorHeight : 0));
    this._dragLine.bringToFront();
    this._dragLine.show();
},



// Sorting
// --------------------------------------------------------------------------------------------

//>    @method    listGrid.unsort()
// Turn sorting off, typically because data has changed and is no longer sorted.
// <p>
// Calling <code>unsort()</code> disables visual indication of which columns are sorted,
// and calls <code>unsort()</code> on the underlying dataset.
// <P>
// Note that a grid viewing a paged dataset may not be able to support <code>unsort()</code>
// because the sort order is what establishes the row numbering that allows data to be
// fetched in batches.
// <P>
// <code>unsort()</code> is automatically called when records are dropped or the
// +link{listGrid.setSort(), sort-configuration} is altered.
//
// @group    sorting
// @visibility external
//<
unsort : function () {
    this.setSort(null);
},

//>    @method listGrid.resort()
// If a list has become unsorted due to data modification or a call to
// +link{ListGrid.unsort()}, this method will resort the list by the previous
// +link{listGrid.setSort, sort-specifier} array, if there is one, or by the previous
// sort-field and -direction.
//
// @group sorting
// @visibility external
//<
resort : function () {
    this._resorting = true;
    if (this._sortSpecifiers) return this.setSort(isc.shallowClone(this._sortSpecifiers));
    var sortFieldNum = this._getSortFieldNum();
    if (sortFieldNum != null) {
        var field = this.getField(sortFieldNum),
            sortDirection = this._getFieldSortDirection(field)
        ;

        this.sort(sortFieldNum, sortDirection);
    }
},

//>    @method    listGrid.sort()   ([])
// Sort this grid's data, with the option to explicitly specify a single field to
// sort by and sort direction.
// <P>
// If sortField is not provided and listGrid.sortField is undefined, the data will be sorted
// by the first sortable column according to +link{ListGridField.sortDirection} if specified,
// or +link{ListGrid.sortDirection}.
// <P>
// ListGrids also support multiple-field sorting. See +link{listGrid.setSort()} for details.
//
// @param [sortField] (string | number) the field name or column number to sort by
// @param [sortDirection] (SortDirection) the direction to sort in
// @return (Boolean) sorting worked
//
// @see type:SortDirection
// @group sorting
// @visibility external
//<
_$sort:"sort",
sort : function (sortFieldNum, sortDirection) {
    var sortFieldName,
        sortField
    ;

    if (isc.isA.String(sortFieldNum)) {
        // sortFieldNum is actually a sortFieldName
        sortFieldName = sortFieldNum;
        sortFieldNum = this.getFieldNum(sortFieldName);
        if (sortFieldNum < 0) {
            // the field isn't visible - it might still be in completeFields if specified
            // as hidden in lg.fields, or it might only appear in the DS
            sortField = this.getUnderlyingField(sortFieldName);
        }
    }

    if (!sortField) {
        // remember the current sortField num and direction for redrawing sorter images
        var oldSortFieldNum = this._getSortFieldNum(),
            oldSortDirection =
                (oldSortFieldNum != null ?
                            Array.shouldSortAscending(this.getField(oldSortFieldNum).sortDirection) :
                            null);

        // if no sortField was specified, assume sorting by the current sort field if there is
        // one, or the first sortable field otherwise.
        if (sortFieldNum == null) {
            if (oldSortFieldNum != null) {
                sortFieldNum = oldSortFieldNum;
            } else {
                // if this.sortFieldNum is null, default to the first sortable field
                for (var i = 0; i < this.fields.length; i++) {
                    if (this._canSortData(this.fields[i]) != false) {
                        sortFieldNum = i;
                        break;
                    }
                }
            }
        }
        // if sortFieldNum is still null, no fields are sortable and we should bail
        if (sortFieldNum == null) return false;

        sortField = this.getField(sortFieldNum);
    }

    // if we have no sortField, bail!
    if (sortField == null) {
        return;
    }

    if (sortDirection == null) {
        sortDirection = this._getFieldSortDirection(sortField);
    }

    // If we're already sorted in the specified direction by the specified field, bail!
    if (oldSortFieldNum == sortFieldNum && sortDirection == sortField.sortDirection) return;

    var specifier = {
            property: sortField[this.fieldIdProperty],
            direction: Array.shouldSortAscending(sortDirection) ? "ascending" : "descending"
        }
    ;
    return this.setSort([specifier]);
},

_getSortFieldNum : function () {
    if (this.sortFieldNum != null) return this.sortFieldNum;
    // sortField is doc'd, sortFieldNum is not. If sortField is set, resolve to a number,
    // store, and return the number
    if (this.sortField != null) {
        var fieldNum = this.getFieldNum(this.sortField);
        if (fieldNum == -1) fieldNum = null;
        this.sortFieldNum = fieldNum;
        // normalize the sortField attribute to a fieldName for consistency, in case it
        // was originally specified as a field index.
        var fieldName = this.getFieldName(this.sortFieldNum);
        if (fieldName) this.sortField = fieldName;
        return fieldNum;
    }
    return null;
},

_setSortFieldNum : function (fieldNum) {
    this.sortFieldNum = fieldNum;
    var fieldName = this.getFieldName(fieldNum);
    // update the public 'sortField' attribute in case anyone checks it.
    this.sortField = fieldName;
},

//> @method listGrid.getSortField()
// Returns the current sort field for this grid. Note that if +link{setSort()} has been
// used to sort by multiple fields, you can call +link{getSort()} to retrieve details about
// the complete sort applied to the grid.
//
// @return (string) sort field name
// @group sorting
// @visibility external
//<
getSortField : function () {
    return this.sortField;
},

// Multi-level Sorting
//> @attr listGrid.canMultiSort (Boolean : true : IRW)
// When true, indicates that this ListGrid supports sorting on multiple fields.  Note that even
// when set to true, multi-field sorting may not be available if the grid is databound and the
// +link{DataSource.canMultiSort,DataSource doesn't support multi-sort}, or if sorting for a
// field is +link{listGridField.canSortClientOnly,client-only} but not all data is available.
// @serverDS allowed
// @visibility external
//<
canMultiSort: true,

// Multi-level Grouping

//> @attr listGrid.canMultiGroup (boolean : false : IRW)
// When true, indicates that this ListGrid supports grouping on multiple fields.
// @serverDS allowed
// @visibility external
//<
canMultiGroup: false,

//> @attr listGrid.initialSort (Array of SortSpecifier : null : IR)
// An array of +link{SortSpecifier} objects used to set up the initial sort configuration for
// this grid. If specified, this will be used instead of any +link{listGrid.sortField} specified.
// @group sorting
// @visibility external
//<
//initialSort: null,

//> @method listGrid.toggleSort() (A)
//  Toggles the sort-direction of the field with the passed name and resorts the grid.
// @param fieldName (String) The name of a field, visible, hidden or existing only in the
//   dataSource
// @group sorting
// @visibility external
//<
toggleSort : function (fieldName, direction) {
    // get the current list of specifiers and the one for the passed fieldName
    var field = this.getSpecifiedField(fieldName),
        specifiers = this._sortSpecifiers ? isc.shallowClone(this._sortSpecifiers) : [],
        specifier = specifiers.find("property", fieldName)
    ;

    // if no existing sort-field, check by displayField
    if (!specifier && field.displayField) {
        specifier = specifiers.find("property", field.displayField);
        if (!specifier) return;
    }

    // if no direction passed in, work one out
    if (!direction)
        direction = Array.shouldSortAscending(specifier.direction) ?
            "descending" : "ascending";

    // if we're unsorting, remove the field from the specifiers array, otherwise set the direction
    if (direction == "unsort") specifiers.remove(specifier);
    else specifier.direction = direction;

    this.setSort(specifiers);
},

_addSort : function (sortField, sortDirection) {
    if (!isc.isAn.Object(sortField)) {
        sortField = this.getSpecifiedField(sortField);
    }
    var direction = sortDirection || this._getFieldSortDirection(sortField);
    this.addSort(
        {
            property: sortField[this.fieldIdProperty],
            direction: Array.shouldSortAscending(direction) ? "ascending" : "descending"
        }
    );
},

//> @method listGrid.addSort() (A)
//  Adds another +link{SortSpecifier} to this grid's sort configuration and resorts.
// @param sortSpecifier (SortSpecifier) A SortSpecifier object indicating an additional field
//    and direction to sort by
// @visibility external
//<
addSort : function (sortSpecifier) {
    var specifiers = this._sortSpecifiers ? isc.shallowClone(this._sortSpecifiers) : [];
    specifiers.add(sortSpecifier);
    this.setSort(specifiers);
},

//> @method listGrid.getSort()
// Returns the current +link{SortSpecifier,SortSpecifiers} for this ListGrid. Will return null if this grid
// has never been sorted (and has no specified +link{initialSort} or +link{sortField}).
// <P>
// Note that if sorting was applied via +link{sort()} [rather than +link{setSort()}] the
// sortSpecifiers returned will have been created based on the specified sort field / direction
// passed into +link{sort()}.
//
// @return (Array of SortSpecifier) current sort specifiers for this grid (may be null if
//  this grid is unsorted).
// @group sorting
// @visibility external
//<
getSort : function () {
    // once sort(), or setSort() has actually run this._sortSpecifiers will be set.
    // Always treat this as the definitive set of specifiers, if present
    if (this._sortSpecifiers) {
       return this.removeSortSpecifierMarkers(isc.shallowClone(this._sortSpecifiers));
    // we haven't sorted yet
    // Check for initialSort first (more recent, more advanced option, for multi-field sort)
    } else if (this.initialSort) {
        return isc.shallowClone(this.initialSort);

    // check for sortField / sortDirection being set
    } else {
        // support sortFieldNum and sortField (as a single fieldName or array of sortBy strings)
        var field;
        if (this.sortFieldNum != null) field = this.getField(this.sortFieldNum);
        if (!field && this.sortField != null) {
            // getUnderlyingField() will pick up the field from the
            // completeFields array or the dataSource fields if necessary
            field = this.getUnderlyingField(this.sortField);
        }
        if (field) {
            var direction = this._getFieldSortDirection(field);
            return [{property: field[this.fieldIdProperty], direction: direction }];
        }
    }
},


//> @method listGrid.getSortSpecifier() (A)
//  Returns the +link{SortSpecifier} for the passed fieldName, or null if the field is not
// sorted.
// @param fieldName (String) The name of a field, visible, hidden or existing only in the dataSource
// @return (SortSpecifier) for the passed fieldName, or null if the field is not sorted.
// @visibility external
//<
getSortSpecifier : function (fieldName) {
    var item = null;

    if (this._sortSpecifiers && this._sortSpecifiers.length > 0) {
        item = this._sortSpecifiers.find("property", fieldName);
        if (!item) {
            // see if we're sorted by displayField
            // if field.optionDataSource is specified, the displayu-field doesn't refer to a field
            // value on this data set.
            var field = this.getSpecifiedField(fieldName);
            if (field && field.displayField && !field.optionDataSource) {
                item = this._sortSpecifiers.find("property", field.displayField);
            }
        }
    }
    return item;
},

//> @method listGrid.getSortFieldCount() (A)
//  Returns the number of fields involved in this grid's current sort configuration.
// @return (Number) the number of fields this grid is currently sorted on.
// @visibility external
//<
getSortFieldCount : function () {
    return this._sortSpecifiers ? this._sortSpecifiers.length : 0;
},

//> @method listGrid.isSortField() (A)
//  Returns true if the passed fieldName is in the current sort-specification.
// @param fieldName (String) The name of a field, visible, hidden or existing only in the dataSource
// @return (Boolean) True if the passed field is sorted, false otherwise
// @visibility external
//<
isSortField : function (fieldName) {
    var result = this.getSortSpecifier(fieldName) ? true : false;
    if (!result) {
        // see if we're sorted by displayField
        var field = this.getSpecifiedField(fieldName);

        if (field && field.displayField && !field.optionDataSource) {
            result = this.getSortSpecifier(field.displayField) ? true : false;
        }
    }
    return result;
},

//> @attr listGrid.sortNumeralStyle (CSSStyleName : "sortNumeral" : [IRWA])
// When multiple fields are sorted, the Style to apply to the numeral that appears after the
// sort-arrows in the header-buttons of sorted fields.
// @visibility external
//<
sortNumeralStyle: "sortNumeral",

//> @attr listGrid.showSortNumerals (boolean : null : [IRWA])
// When multiple fields are sorted, set this to false to hide the sort-numeral
// displayed by default after the sort-arrows in the header-buttons of sorted fields.
// @visibility external
//<

//> @method listGrid.getSortNumeralHTML()
// When multiple fields are sorted, this method returns the HTML for the sort-numeral that
// appears after the sort-arrows in the header-buttons of sorted fields.  If you don't want
// sort-numerals in the header-buttons, you can override this
// method to return null or an empty string, or set +link{showSortNumerals} to false.
// <P>
// Note that the sortIndex passed in is zero based. The default implementation of this method
// returns an HTML element with the +link{sortNumeralStyle} applied to it, containing the
// specified sortIndex incremented by 1 (therefore showing the more user-friendly 1-based
// numbering system).
//
// @param fieldName (String) The name of a sort-field to get the
//  +link{listGrid.sortNumeralStyle, sortNumeral} HTML for.
// @param sortIndex (int) The sort index for the field.
// @return (HTMLString) The HTML for this field's sortNumeral
// @visibility external
//<

_$sortNumeralHTMLTemplate: [
    "<span class='",        // [0]
    ,                       // [1] this.sortNumeralStyle
    "'>",                   // [2]
    ,,,                     // [3-5] (sortIndex + 1)
    "</span>"               // [6]
],
getSortNumeralHTML : function (fieldName, sortIndex) {
    var template = this._$sortNumeralHTMLTemplate;
    template[1] = this.sortNumeralStyle;
    isc._fillNumber(template, (sortIndex + 1), 3, 3);
    return template.join(isc.emptyString);
},

//> @method listGrid.clearSort()    (A)
//  This method clears any existing sort on this grid by calling +link{listGrid.setSort} with
// a null parameter.  The internal list of +link{SortSpecifier}s is removed and the grid is
// unsorted.
// @visibility external
//<
clearSort : function () {
    this.setSort(null);
},

//> @method listGrid.setSort()    (A)
// Sort the grid on one or more fields.
// <P>
// Pass in an array of +link{SortSpecifier}s to have the grid's data sorted by the fields in
// each +link{sortSpecifier.property,specifier.property} and in the directions specified.  The
// grid can be sorted by any combination of fields, including fields specified in the fields
// array, whether visible or hidden, and +link{DataSource.fields, unused fields from the
// underlying dataSource}, if there is one.
// <P>
// If multiple fields are sorted, those that are visible show a directional icon and a small
// +link{listGrid.sortNumeralStyle, sort-numeral}
// indicating that field's index in the sort configuration.
// <P>
// See +link{listGrid.addSort, addSort()} and +link{listGrid.toggleSort, toggleSort()} APIs
// for information on making changes to the current sort configuration.
//
// @param sortSpecifiers (Array of SortSpecifier) Array of +link{SortSpecifier} objects
// @visibility external
//<
_$setSort: "setSort",
setSort : function (sortSpecifiers) {
    if (this.logIsInfoEnabled("sorting")) this.logInfo("Entering setSort", "sorting");

    // store and clear the flag that gets set in resort()
    var calledFromResort = this._resorting;
    delete this._resorting;

    // if we get passed something that isn't an array, wrap it in one
    if (sortSpecifiers && !isc.isAn.Array(sortSpecifiers)) sortSpecifiers = [sortSpecifiers];

    // if there are no fields arrays bail

    if (!this.fields && !this.getDataSource()) {

        this.initialSort = sortSpecifiers;
        if (this.logIsInfoEnabled("sorting")) {
            this.logInfo("setSort() not performing sort - no fields", "sorting");
        }
        return false;
    }

    // If we are databound, and we have pending edits, the sort may cause us to lose edit
    // values (because the rows get lost from the local cache). Catch this case and warn the
    // user if appropriate.
    if (this.confirmDiscardEdits && this.dataSource != null) {

        if (this.hasChanges() &&
            // If we have a complete cache, the sort will not drop any edit values
            // (and they will be reassociated with the appropriate rows via remapEditValues)
            isc.ResultSet && isc.isA.ResultSet(this.data) && !this.data.allMatchingRowsCached())
        {

            this.showLostEditsConfirmation({target:this, methodName:"_continueSort"},
                                               {target:this, methodName:"_cancelSort"});
            // this._sortArgs gets cleaned up by the callback methods above
            this._sortArgs = sortSpecifiers
            return;
        }
    }


    // get a list of specifiers that *were* in use but won't be after this setSort() call
    var sortSpecifiers = sortSpecifiers || [],
        removeThese = [],
        firstVisibleField
    ;

    if (this._sortSpecifiers) {
        // get a list of items that *were* in the sort-spec but aren't any more, so we can
        // update their header-states
        for (var i = 0; i < this._sortSpecifiers.length; i++) {
            var oldItem = this._sortSpecifiers[i],
                newItem = sortSpecifiers.find("property", oldItem.property)
            ;
            if (!newItem) {
                removeThese.add(isc.shallowClone(oldItem));
                if (this.logIsInfoEnabled("sorting")) {
                    this.logInfo("In setSort - marking field "+oldItem.property+
                        " for removal", "sorting");
                }
            }
        }
    }

    if (!sortSpecifiers || sortSpecifiers.length==0) {
        // no sort-specifiers passed, we'll forcibly unsort
        if (this.logIsInfoEnabled("sorting")) {
            this.logInfo("null or zero-length sortSpecifiers - unsorting only", "sorting");
        }
    }

    var newSpecifiers = [];

    // handle the normalizer stuff that used to be done in sortData() for each sortSpecifier
    // and store a couple of useful vars on the item, sortIndex (for the numeral) and
    // primarySort, which indicates this is the first Visible field (and, thus, the sortFieldNum)
    sortSpecifiers.removeEmpty();
    for (var i = 0; i < sortSpecifiers.length; i++) {
        if (!sortSpecifiers[i]) continue;

        var item = sortSpecifiers[i],
            field = this.getUnderlyingField(item.property),
            visibleField = this.getField(item.property),
            normalizer=null
        ;

        if (!field) {
            // if getUnderlyingField() didn't return a field, check for a field with
            // a displayField equal to item.property
            var fields = this.getAllFields();
            field = !fields ? null : fields.find("displayField", item.property);
        }

        if (field) {
            if (!this._canSortData(field)) {
                // data in this field can't be sorted because the field specifies client-only
                // sort, but not all data is presently client-side.  Skip this specifier and
                // log a warning.
                this.logWarn("Removing field '" + field[this.fieldIdProperty] + "' from the " +
                    "sortSpecifier array because it specifies canSortClientOnly: true and all " +
                    "data is not yet client-side.", "sorting");
                continue;
            }

            // is this the first visible field? (this will be grid.sortFieldNum)
            if (visibleField) {
                if (!firstVisibleField) firstVisibleField = visibleField;
            }
            // set up a sortNormalizer
            if (!item.normalizer) {
                if (field.sortNormalizer) {
                    // custom normalizer defined
                    normalizer = field.sortNormalizer;
                } else if (field.valueMap &&
                            (field.sortByMappedValue == null || field.sortByMappedValue == true))
                {
                    // if there's a valueMap, use it as the normalizer, so that with eg enums
                    // we sort by the text name rather than the numeric order.
                    normalizer = isc.isA.String(field.valueMap) ?
                            this.getGlobalReference(field.valueMap) : field.valueMap;
                } else if (field.type != null) {
                    // if an explicit field type is declared, pass that in as a normalizer -
                    // the sort method understands this.
                    normalizer = field.type;
                }
                item.normalizer = normalizer;
            }
            // set the default sort-direction if none is provided
            if (item.direction == null) {
                item.direction = this._getFieldSortDirection(field);
            }
            if (firstVisibleField && firstVisibleField == visibleField) {
                item.primarySort = true;
            }
            // store the new sortDirection on the grid from the first sort specifier
            if (item.primarySort) {
                // normalize the sort direction to a boolean and store it on the grid
                this.sortDirection = Array.shouldSortAscending(item.direction);
            }
            if (!item.context) item.context = this;
            item.sortIndex = i;

            newSpecifiers.add(item);
        }
    }

    // store the sortSpecifiers
    // Update initialization property to match new sort specifier state
    this.sortSpecifiers = this._sortSpecifiers = newSpecifiers;

    var firstSpecifier = this._sortSpecifiers[0],
        sortFieldName = firstSpecifier ? firstSpecifier.property : null,
        visibleSortFieldNum = sortFieldName ? this.getFieldNum(sortFieldName) : null,
        sortField = sortFieldName ? this.getUnderlyingField(sortFieldName) : null,
        sortDirection = firstSpecifier ? Array.shouldSortAscending(firstSpecifier.direction) : null
    ;

    if (!sortField) {
        if (firstSpecifier) this.logWarn("sort: field does not exist: " + firstSpecifier.property);
    }

    if (visibleSortFieldNum >= 0) {
        this._setSortFieldNum(visibleSortFieldNum);
    } else {
        this._setSortFieldNum(null);
    }

    if (this.logIsInfoEnabled("sorting")) {
        this.logInfo("In setSort - ready to sort on specifiers:\n"+
            isc.echoAll(this._sortSpecifiers), "sorting");
    }

    if (this._sortSpecifiers && this._sortSpecifiers.length>0) {
        if (calledFromResort && this.data && this.data.resort && this.data._sortSpecifiers) {
            // if called from resort() and the data has a resort() method (ResultSet), AND
            // the data has already been sorted, call resort() instead of setSort() (which
            // will no-op for the same specifiers)
            this.data.resort();
        } else if (this.data &&
            (this.data.setSort || this.data.length > 0
                || isc.isA.ResultTree(this.data) || isc.isA.Tree(this.data)))
        {
            // do the actual sorting
            if (this.data.setSort) {
                if (this.logIsInfoEnabled("sorting")) {
                    this.logInfo("In setSort -  Calling data.setSort with specifiers:\n"
                        +isc.echoAll(this._sortSpecifiers), "sorting");
                }
                this.data.setSort(this._sortSpecifiers);
            } else if (this.data.sortByProperty) {
                if (this.logIsInfoEnabled("sorting")) {
                    this.logInfo("In setSort - Calling data.sortByProperty with specifier:\n"+
                        isc.echoAll(firstSpecifier), "sorting");
                }
                this.data.sortByProperty(
                    firstSpecifier.property,
                    Array.shouldSortAscending(firstSpecifier.direction),
                    firstSpecifier.normalizer,
                    firstSpecifier.context
                );
            }
        } else {
            if (this.logIsInfoEnabled("sorting")) {
                this.logInfo("In setSort - not sorting:\nthis.data is"+this.echoAll(this.data),
                    "sorting");
            }
        }
    } else {

        if (this.data) {
            var unsorted = this.data.unsort != null && this.data.unsort();
            if (!unsorted && this.data.setSort) this.data.setSort([]);
        }

        if (this.invalidateCacheOnUnsort) {
            this.invalidateCache();
        }
    }

    // A note on edit-rows:
    // This sort will change the rowNums for records - if we have unsaved edits for any rows,
    // this means the 'rowNum' will have to be updated for that edit information.
    // We handle this via '_remapEditRows()' called in response to 'dataChanged()', so no need
    // to do any special handling for this here.  Note also that if we have embedded components,
    // these are remapped in the same way via '_remapEmbeddedComponents()'

    var hasHeader = (this.header && isc.isA.Toolbar(this.header));

    // remove the sort indicators from any fields that *were* sorted but now aren't and
    // are still visible
    if (removeThese) {
        if (removeThese.length > 0) {
            if (this.logIsInfoEnabled("sorting")) {
                this.logInfo("In setSort - Removing sort-media from now unsorted fields:\n"+
                    isc.echoAll(removeThese), "sorting");
            }
        }

        for (var i=0; i<removeThese.length; i++) {
            var specifier = removeThese[i],
                field = this.getSpecifiedField(specifier.property),
                fieldsToSelect = []
            ;

            if (field) {
                fieldsToSelect.add(field);
                if (this.logIsInfoEnabled("sorting")) {
                    this.logInfo("In setSort, removing sort-media for fieldName '" +
                        field.name + "'", "sorting");
                }
            }

            var fields = this.getFields();
            if (fields) {
                var byDisplayField = fields.findAll("displayField", specifier.property);
                if (byDisplayField) {
                    // If displayField is specified with an optionDataSource this should
                    // not impact sort - the display field doesn't refer to field values in
                    // this data-set
                    byDisplayField.removeUnless("optionDataSource", null);
                    if (byDisplayField.length > 0) {
                        fieldsToSelect.addList(byDisplayField);
                        if (this.logIsInfoEnabled("sorting")) {
                            this.logInfo("In setSort, removing sort-media for fields with displayField " +
                                "'" + (field ? field.name : "") + "' - these are: " +
                                isc.echoFull(byDisplayField.getProperty("name").join(", ")), "sorting");
                        }
                    }
                }
            }

            for (var j=0; j<fieldsToSelect.length; j++) {
                var localField = fieldsToSelect[j],
                    fieldNum = this.getFieldNum(localField.name)
                ;

                if (localField) {
                    localField.sortDirection = localField.originalSortDirection;
                }

                if (fieldNum>=0 && hasHeader) {
                    // select the appropriate button
                    var sortButton = this.getFieldHeaderButton(fieldNum);

                    if (sortButton && sortButton.setTitle) sortButton.setTitle(sortButton.getTitle());
                }
            }
        }
    }

    // set sort indicators on the current set of sort fields.
    if (this._sortSpecifiers && this._sortSpecifiers.length > 0) {
        this.logInfo("In setSort - Setting sort-media for sorted fields:\n"+
                isc.echoAll(this._sortSpecifiers), "sorting");

        var headerSelected = false;
        // set the sort indicators on any fields that are sorted and are visible
        for (var i=0; i<this._sortSpecifiers.length; i++) {
            var specifier = this._sortSpecifiers[i],
                field = this.getSpecifiedField(specifier.property),
                fieldsToSelect = []
            ;

            if (field) {
                fieldsToSelect.add(field);
                if (this.logIsInfoEnabled("sorting")) {
                    this.logInfo("In setSort, adding sort-media for fieldName '" +
                        field.name + "'", "sorting");
                }
            }

            var fields = this.getFields();
            if (fields) {
                var byDisplayField = fields.findAll("displayField", specifier.property);
                if (byDisplayField) {
                    fieldsToSelect.addList(byDisplayField);
                    if (this.logIsInfoEnabled("sorting")) {
                        this.logInfo("In setSort, adding sort-media for fields with displayField " +
                            "'" + (field ? field.name : "") + "' - these are: " +
                            isc.echoFull(byDisplayField.getProperty("name").join(", ")), "sorting");
                    }
                }
            }

            for (var j=0; j<fieldsToSelect.length; j++) {
                var localField = fieldsToSelect[j],
                    fieldNum = this.getFieldNum(localField.name)
                ;

                if (localField) {
                    if (localField.sortDirection && !localField.originalSortDirection) {
                        localField.originalSortDirection = localField.sortDirection;
                    }
                    localField.sortDirection = specifier.direction;
                }
                if (hasHeader && fieldNum>=0) {
                    // select the appropriate button
                    var sortHeader = this.getFieldHeader(fieldNum),
                        sortButton = this.getFieldHeaderButton(fieldNum);

                    if (sortButton) {
                        if (!headerSelected && this.selectHeaderOnSort) {
                            sortHeader.selectButton(sortButton);
                            if (isc.Browser.isTouch && this.shouldShowHeaderMenuButton(sortButton, true)) {
                                this._showHeaderMenuButton(sortButton);
                            }
                            headerSelected = true;
                        }
                        if (sortButton.setTitle) sortButton.setTitle(this.getHeaderButtonTitle(sortButton));
                    }
                }
            }
        }
    } else if (hasHeader && this.selectHeaderOnSort) {
        // when unsorting, deselect the selected header-button, if there is one
        var selectedButton;
        if (this.header) {
            selectedButton = this.header.getSelectedButton();
            if (selectedButton) {
                selectedButton.deselect();
                this._hideHeaderMenuButton(selectedButton);
            }
        }
        if (this.frozenHeader) {
            selectedButton = this.frozenHeader.getSelectedButton();
            if (selectedButton) selectedButton.deselect();
        }
    }

    if (visibleSortFieldNum >= 0) {
        if (this.logIsInfoEnabled("sorting")) {
            this.logInfo("In setSort - _setSortFieldNum called with fieldNum: "+visibleSortFieldNum+
                " - sortField is now: "+this.sortField+"\ngetSortState() now returns: "+
                isc.echoAll(this.getSortState()), "sorting");
        }
    }

    // if there's a sorter button, redraw that
    if (this.sorter && this.sorter.setTitle) {
        this.sorter.setTitle(this.sorter.getTitle());
    }

    if (this.body && this.showRecordComponents) delete this.body._oldDrawArea;

    // and mark the list as dirty so it will be redrawn
    this._markBodyForRedraw(this._$setSort);

    if (this.logIsInfoEnabled("sorting")) this.logInfo("Leaving setSort", "sorting");

    this.handleSortChanged(this._sortSpecifiers);

    // return true so the caller knows that sorting worked
    return true;
},

// sort change notification - documented in register string-methods.
handleSortChanged : function (sortSpecifiers) {
    this.sortChanged(this.removeSortSpecifierMarkers(isc.shallowClone(sortSpecifiers)));
    this.handleFieldStateChanged(true);
},
sortChanged : function (sortSpecifiers) {
},

// Callback methods fired when the user attempted to sort this grid and was prompted to save
// pending edits.
// _continueSort will finish the sort (fired when the user has discarded or saved edits)
// _cancelSort simply cleans up the temp '_sortArgs' object. Fired if the user chooses to cancel
// the sort to continue editing.
_continueSort : function () {
    var args = this._sortArgs;
    delete this._sortArgs;
    this.setSort(args);
},

_cancelSort : function () {
    var primarySort;
    var primarySort = this._sortArgs ? this._sortArgs[0] : null;
    delete this._sortArgs;
    // if the user clicked a header button to perform the sort, then abandoned it,
    // deselect that header button now.
    if (primarySort != null) {
        var sortFieldNum = this.getFieldNum(primarySort.property);
        if (sortFieldNum != -1 && this.sortFieldNum != sortFieldNum) {
            this.header.deselectButton(sortFieldNum);
        }
    }
},

//>    @method    listGrid.sortData()    (A)
//        @group    sorting
//            actually sort the data according to the sort characteristics of the list
//<
sortData : function () {
    // if the data or fields arrays haven't been defined, skip the sort
    if (!this.data || !this.fields) return;

    // get the field object
    var field = this.fields[this._getSortFieldNum()],
        normalizer = null;

    if (field == null) {
        // if field is null, we've shrunk the number of columns,
        //    sort re-set sort to column 0
        this._setSortFieldNum(0);
        field = this.fields[0];
    }

    if (field.sortNormalizer) {
        // custom normalizer defined
        normalizer = field.sortNormalizer;
    } else if (field.valueMap) {
        // if there's a valueMap, use it as the normalizer, so that with eg enums we sort by the
        // text name rather than the numeric order.
        normalizer = field.valueMap;
        if (isc.isA.String(field.valueMap)) normalizer = this.getGlobalReference(field.valueMap);

    // if an explicit field type is declared, pass that in as a normalizer - the sort method
    // understands this.
    } else if (field.type != null) {
        normalizer = field.type
    }
    // sort by that field's property
    // pass the grid in as the additional 'context' argument for the sort - this will then
    // be available to the normalizer
    this.data.sortByProperty(field[this.fieldIdProperty], field.sortDirection, normalizer, this);
},

// Embedded Components
// --------------------------------------------------------------------------------------------

// These methods actually implemented in GridRenderer

// helper to get the current count of embeddedComponents
getEmbeddedComponentCount : function (componentType) {
    var components = this.body ? this.body._embeddedComponents : null;
    if (this.frozenBody && this.frozenBody._embeddedComponents != null) {
        components = (components ? components.duplicate() : []).addList(this.frozenBody._embeddedComponents);
    }

    if (!components) return 0;

    if (componentType == "recordComponent") {
        components = components.findAll("isRecordComponent", true);
    } else if (componentType == "backgroundComponent") {
        components = components.findAll("isBackgroundComponent", true);
    }

    return components == null ? 0 : components.length;
},

//> @method listGrid.addEmbeddedComponent() [A]
// Attaches the component to the provided record. If <code>position</code> is specified as
// <code>"within"</code> +link{canvas.snapTo} and +link{canvas.snapOffsetLeft},
// +link{canvas.snapOffsetTop} may be set to specify where the component
// will render within the cell or record. If unset, for components embedded within a record
// we will default to embedding at the top/left coordinate, and for components embedded within
// a cell, we will respect the align / valign properties for the cell in question. Any
// percentage sizing will be interpreted as percentage of row size.
// <P>
// Otherwise it will appear to be embedded within the record, underneath the field values.
// <P>
// Embedded components become children of the grid and will stay attached to a record through
// scrolling, sorting and other operations that cause records to shift position.
// <P>
// If <code>position</code> is set to <code>"expand"</code>, embedded components may offer a
// resize interface, eg, by setting +link{listGridField.canDragResize,canDragResize}:true,
// and the grid will react accordingly, growing or shrinking the record to match the embedded
// component's new extents.
// <P>
// Embedded components can be explicitly removed with +link{removeEmbeddedComponent()}.
// <P>
// If a record is removed from the dataset or is replaced in the dataset, for example, it is
// eliminated through filtering (removes record) or is successfully edited in a databound grid
// (replaces record), the component is cleared but not logically removed from the grid. It is the
// responsibility of code that sets up the embedded component to remove it if the record is removed
// from the dataSet.
// <P>
// When embedding components will result in variable height records,
// you should switch on +link{listGrid.virtualScrolling, virtualScrolling}.
//
// @param component (Canvas) component to embed
// @param record (ListGridRecord) record to attach the component to
// @param [rowNum] (Integer) rowNum of the record to attach the component to
// @param [colNum] (Integer) colNum in which to embed the component
// @param [position] (EmbeddedPosition) positioning with respect to the record or cell (Defaults to "expand").
// @visibility external
//<
addEmbeddedComponent : function (component, record, rowNum, colNum, position) {

    if (colNum == null && component._currentFieldName != null) {
        colNum = this.getFieldNum(component._currentFieldName);
    }
    // get the local body before localizing the colNum
    var body = this.getFieldBody(colNum);
    colNum = this.getLocalFieldNum(colNum);
    position = position || component.embeddedPosition
    if (rowNum == null) rowNum = this.getRecordIndex(record);



    body.addEmbeddedComponent(component, record, rowNum, colNum, position);

    // If we have 2 bodies, adding an embedded component to one of them will effect the
    // height of both (the rows "span" both bodies of course and we want them to match up).
    if (this.frozenBody != null) {
        var otherBody = body == this.frozenBody ? this.body : this.frozenBody;
        var isFrozenBody = otherBody == this.frozenBody ? 1 : 0;

        if (rowNum >= 0 && otherBody.isDrawn() && !otherBody.isDirty()) {
            var expectedRowHeight = otherBody.getRowHeight(record,rowNum,isFrozenBody);
            if (expectedRowHeight != body.getRowSize(rowNum)) {

                otherBody.markForRedraw();
            }
        }
    }

},

//> @method listGrid.removeEmbeddedComponent() [A]
// Removes an embedded component previously associated with the provided record. If a Canvas
// is passed as the <code>record</code> parameter, it is assumed to be a component and the
// record is detected automatically from it.  If
// <code>destroyOnUnEmbed</code> is <code>true</code> for the component, it will also be
// destroyed.
//
// @param record (ListGridRecord | Canvas) record that the component was previously attached
//     to or the component itself
// @param [component] (Integer | Canvas) component to unembed, or the colNum in which it appears
// @visibility external
//<
removeEmbeddedComponent : function (record, component, suppressRedraw) {
    var body;

    // support component passed as the "record" param - collect the record from the component
    if (isc.isA.Canvas(record)) {
        component = record;
        record = component.embeddedRecord;
    }

    // support a colNum passed as the "component" param
    if (isc.isA.Number(component)) {
        body = this.getFieldBody(component);
        component = this.getLocalFieldNum(component);
    } else {
        if (!component) {
            // no component passed - assume the first one on the record, if there are any
            // or bail otherwise
            if (!this._hasEmbeddedComponents(record)) return;
            component = this._getEmbeddedComponents(record)[0];
        }
        body = isc.Canvas.getById(component._embedBody);
    }

    if (body == null) {
        return;
    }
    body.removeEmbeddedComponent(record, component, suppressRedraw);
},

//> @method listGrid.getEmbeddedComponent() [A]
// Gets an embedded component previously associated with the provided record.
//
// @param record (ListGridRecord) record that the component was previously attached to
// @param colNum (Number) column in the grid from which to retrieve the component
// @return (Object) the embeddedComponent
// @visibility embeddedComponents
//<
getEmbeddedComponent : function (record, colNum) {
    var body;
    if (isc.isA.Number(colNum)) {
        body = this.getFieldBody(colNum);
        colNum = this.getLocalFieldNum(colNum);
    } else if (colNum == null) {
        // if no colNum was passed, assume this.body
        body = this.body;
    } else {
        // support "component" passed as an object
        body = isc.Canvas.getById(colNum._embedBody);
    }
    return body.getEmbeddedComponent(record, colNum);
},

//> @method listGrid.getRecordComponent()
// Retrieve the +link{showRecordComponents,recordComponent} currently being shown at the given
// coordinates.
// <p>
// <code>recordComponents</code> are dynamically assigned to row/cell coordinates and, depending
// on the +link{listGrid.recordComponentPoolingMode}, any kind of redraw of the containing
// ListGrid (due to sort change, scrolling, editing etc) may cause a
// <code>recordComponent</code> to be assigned to another row, +link{canvas.clear,clear()ed} or
// permanently +link{canvas.destroy,destroy()ed}.
// <p>
// Hence you should always call <code>getRecordComponent()</code> right before taking action on
// the <code>recordComponent</code> - don't cache the component associated with row/cell
// coordinate.  Similarly, it's invalid to call <code>getRecordComponent()</code> during a redraw
// (for example, from +link{listGridField.formatCellValue,formatting} code).
// <p>
// It's always invalid to try to use a <code>recordComponent</code> outside of a ListGrid (by eg
// adding it to some other layout).
// <p>
// If +link{showRecordComponentsByCell} is true and the colNum parameter is not passed, the call
// will return the first component in the passed rowNum.
// <p>
// Returns null if there is no component at the specified coordinates.
//
// @param rowNum (int) row number to get record component for
// @param [colNum] (integer) optional column number to get the record component for
// @return (Canvas) record component, or null if none is shown at these coordintes
// @visibility external
//<
getRecordComponent : function (rowNum, colNum) {
    var record = this.getRecord(rowNum);
    return this.getEmbeddedComponent(record, colNum);
},


// Embedded Editing
// --------------------------------------------------------------------------------------------
// "embedded editing" means editing with a complete DynamicForm as an embedded component
// appearing underneath the edited row.  This is distinct from "inline editing" where we place
// form items in individual cells, and can only edit the currently visible fields.

// get the DataSource for editing this record
getRecordDataSource : function (record) {
    return this.dataSource;
},

//> @method listGrid.openRecordEditor()           (A)
// Start editing a record, using an editor embedded in the row
//
// @param   record   (Object)            record whose detail records should be shown
// @visibility advancedInlineEdit
//<

openRecordEditor : function (record) {
    // don't allow more than one openRecord embedded component at once
    if (this._openRecord != null) this.closeRecord();


    var editValues = isc.addProperties({}, record);

    // create a form to edit the record
    var editor = this.ns.DynamicForm.create(
    this.recordEditorProperties,
    {
        autoDraw:false,
        dataSource : this.getRecordDataSource(record),
        numCols : 4,
        values : editValues,
        saveOperationType : "update",
        _embeddedEditRecord : record
    });
    // place it in a simple stack with a save button
    var component = this.ns.VStack.create({
        autoDraw:false,
        width:this.getAvailableFieldWidth() - this.embeddedComponentIndent,
        left:this.isRTL() ? 0 : this.embeddedComponentIndent,
        destroyOnUnEmbed:true,
        members:[
            editor,
            this.ns.Toolbar.create({
                autoDraw:false,
                width: 200,
                buttons : [
                    { title : this.recordEditorSaveButtonTitle,
                      click : this.getID() + ".embeddedSaveRecord(" + editor.getID() + ")",
                      extraSpace:10
                    },
                    { title : this.recordEditorCancelButtonTitle,
                      record : record,
                      grid : this,
                      click : function () {
                          var stack = this.parentElement.parentElement;
                          this.grid.closeRecord(this.record, stack);
                      }
                    }
                ]
            })
        ]
    });

    component.removeOnHideField = true;

    this.addEmbeddedComponent(component, record, this.data.indexOf(record));

    this._openRecord = record;
    this._openRecordComponent = component;
},

// when the user hits Save during embedded editing, tell the editor to save
embeddedSaveRecord : function (editor) {
    // hold onto the record via the context since we'll need it to get rid of the embedded editor
    editor.saveData({target:this, methodName:"embeddedEditComplete"},
                    {_embeddedEditRecord:editor._embeddedEditRecord,
                     _embeddedEditor:editor});
},

// when the save attempt completes, if there were no errors, remove the embedded editor.
// Otherwise leave it there to show validation errors and allow further editing
embeddedEditComplete : function (response, data, request) {
    if (response.status == 0) {
        this.removeEmbeddedComponent(request._embeddedEditRecord, request._embeddedEditor);
    }
},

//> @method listGrid.closeRecord()
// Close the currently shown embedded component.
//
// @visibility nestedGrid
//<
closeRecord : function (record, component) {
    if (!record) record = this._openRecord;
    if (!component) component = this._openRecordComponent;
    this.removeEmbeddedComponent(record, component);
    this._openRecord = null;
    this._openRecordComponent = null;
},

// Nested Master-Detail
// --------------------------------------------------------------------------------------------

//>    @method    listGrid.openRecordDetailGrid()    (A)
// Show records from another DataSource which are related to this record, in a nested
// ListGrid.
// <P>
// This is often called a "master-detail" view.  The classic example is salesOrder records
// which contain lineItems.
// <P>
// The <code>childDS</code> should be a DataSource that declares a foreignKey relationship to
// the DataSource the current grid is viewing.
//
// @param   record   (Object)            record whose detail records should be shown
// @param   childDS  (DataSource or ID)  dataSource to retrieve detail records from
// @visibility nestedGrid
//<
openRecordDetailGrid : function (record, childDS) {
    // don't allow more than one openRecord embedded component at once
    if (this._openRecord != null) this.closeRecord();

    var subGrid = this.getRecordDetailGrid(record, childDS);
    // and embed it in the target record
    var component = isc.VLayout.create({
        autoDraw:false,
        destroyOnUnEmbed:true,
        height:this.cellHeight, // since the Layout is overflow:visible this is just a minimum
        left:this.isRTL() ? 0 : this.embeddedComponentIndent,
        width:this.getAvailableFieldWidth() - this.embeddedComponentIndent,
        resizeBarSize:4,
        members:[subGrid],
        removeOnHideField: true
    });

    this.addEmbeddedComponent(component, record, this.data.indexOf(record));

    var childDS = isc.DataSource.getDataSource(subGrid.dataSource);
    subGrid.fetchRelatedData(record, this.getRecordDataSource(record));

    this._openRecord = record;
    this._openRecordComponent = component;
},

//>    @method    listGrid.getRecordDetailGrid()    (A)
// Returns a ListGrid to show the records from another DataSource which are related to this
// record via a child dataSource
//
// @param   record   (Object)            record whose detail records should be shown
// @param   childDS  (DataSource or ID)  dataSource to retrieve detail records from
// @visibility internal
//<
getRecordDetailGrid : function (record, childDS) {

    childDS = isc.DataSource.getDataSource(childDS);

    // create a ListGrid to show them
    var subGrid =
        isc.ListGrid.create(
            this.recordDetailGridProperties,
            {
                autoDraw:false,
                dataSource:childDS,
                showResizeBar:true,
                // XXX the embedded grid needs to inherit a lot of properties from it's parent grid -
                // this particular one is critical because otherwise context menu events will bubble to
                // this grid, and it will show non-sequitur context menus on the embedded grid.
                showCellContextMenus:this.showCellContextMenus
            }, this.nestedGridDefaults
        );

    return subGrid;
},

// Charting
// ---------------------------------------------------------------------------------------

// types of charting:
// - take any single row and plot it as a single series
//   - field names label the values
// - take any single column and plot it as a single series
//   - values from another column label the values
// - take a set of rows and a set of columns and plot it as a multi-series
//   - dataset is 2d: both field names *and* values from another column label the values

//> @method listGrid.chartData()
// Chart the data in this listGrid as a multi-series chart.
// <P>
// Each row provides a series of data.  Each series of data is labeled by a value from one
// column, called the <code>labelField</code>.
// <P>
// For example, cell values are sales figures, and fields are "Product", "August",
// "September", "October".  In this case each row gives a series: sales figures for each of 3
// months.  The <code>labelField</code> in this case is the "Product" field, meaning each row
// represents sales figures for each of 3 months for a particular product.  This dataset
// can be charted via any multi-series chart: stacked or clustered bar or column chart, line
// chart with multiple lines, or area chart (stacked lines).
// <P>
// By default, all visible fields other than the label field are assumed to be labels for
// series values, but an explicit list of fields can be provided as <code>dataFields</code>.
// <P>
// By default, all data is charted if all data is loaded, otherwise, data visible in the
// viewport is charted.  An explicit set of rows can be provided via <code>dataRows</code>.
//
// @param labelField (String) name of the field
// @param [dataFields] (Array of String) optional list of fields to use as labels.  By
//      default, all fields are used.
// @param [dataRows] (Array of ListGridRecord) set of records to chart.  Can be obtained by eg
//      +link{ResultSet.getRange,grid.data.getRange()}.
// @param [chartProperties] (FacetChart Properties) properties to pass to the created chart
// @param [labelFieldFirst] (boolean) if true, use the labelField as the "first" set of labels,
//      for example, as the bar labels in a stacked bar chart, whereas the second set of labels
//      would appear as the legend.
//
// @return (FacetChart) created Chart instance
//
// @visibility external
// @example gridCharting
//<
chartData : function (labelField, dataFields, dataRows, properties, labelFieldFirst) {

    // verify the chartConstructor maps to a valid class (if not typically implies some module is
    // missing).
    this.checkChartConstructor();

    if (labelField) labelField = this.getField(labelField);
    if (dataFields) dataFields = this.map("getField", dataFields);
    else {
        // assume all fields other than a labelField contain values
        dataFields = this.fields.duplicate();
        dataFields.remove(labelField);
    }

    // default for dataRows: all rows, or visible rows if paging
    if (!dataRows) {
        if (!isc.ResultSet || !isc.isA.ResultSet(this.data)) {
            dataRows = this.data;
        } else if (this.data.allMatchingRowsCached()) {
            dataRows = this.data.getAllRows();
        } else {
            var visibleRows = this.getVisibleRows();
            dataRows = this.data.getRange(visibleRows[0], visibleRows[1]);
        }
    }

    // form a facet representing the column headers, aka, the fields.  This is an inlinedFacet,
    // meaning it has multiple values per record under property names indicated by its
    // facet.values
    var rowFacet, columnFacet;
    if (dataFields.length >= 1) {
        // give chart a simplified copy of the fields

        for (var i = 0; i < dataFields.length; i++) {
            var field = dataFields[i];
            dataFields[i] = {
                id: field.name,
                title: this.htmlUnescapeExportFieldValue(field.title),
                type: field.type
            }
        }
        columnFacet = {
            id:"columnFacet",
            title:this.valueTitle,
            values: dataFields,
            inlinedValues:true
        };
    }

    // form a facet representing row headers, aka, one column of data intended as labels
    if (dataRows.length >= 1 && labelField) {
        rowFacet = { id: labelField.name };
    }

    // remove unspecified facets to allow just single row or single column charting
    var facets = [columnFacet,rowFacet];
    facets.removeAll([null]);

    // for multi-series charts, allow specifying which facet comes first (eg, for stacked bar
    // charts, appears as a bar label (first) or appears as a legend labelling colors (second)
    if (labelFieldFirst) facets.reverse();

    var props = isc.addProperties({
        data: dataRows,
        facets: facets,
        title: this.chartTitle,
        chartType: this.chartType
    }, properties);

    // single column charting: the (singular) dataField holds the value
    if (rowFacet && !columnFacet) props.valueProperty = dataFields[0].name;
    // otherwise, we have an "inlinedValues" facet: there is no single valueProperty, rather
    // each record contains multiple values stored under the ids of the inlinedFacet's values

    return this.createAutoChild("chart", props, this.chartConstructor);
},

//> @attr listGrid.chartConstructor (Classname : "FacetChart" : IR)
// Name of the SmartClient Class to be used when creating charts.  Must support the
// +link{Chart} interface.
//
// @visibility external
//<
chartConstructor: "FacetChart",

// Helper method to verify that the chartConstructor is set to a valid Chart-based class name.
// Allows us to log a helpful warning when the developer attempts to chartData (or setEnableCharting
// for CubeGrids) if the chartConstructor is set to an unloaded class name.
checkChartConstructor : function () {
    var chartConstructor = this.chartConstructor;
    if (isc.isA.String(chartConstructor)) {
        chartConstructor = window.isc[this.chartConstructor];
    }

    if (chartConstructor == null) {

        this.logWarn('Component chartConstructor attribute set to "' + this.chartConstructor +
            '" - this cannot be resolved to a valid SmartClient class. Verify that all ' +
            'appropriate modules are loaded and that this class exists.');

    // In some cases if all required modules are not loaded,
    // we load a placeholder class to avoid actual JS errors.
    // In this case we mark the class object as "invalid" and provide a custom error message to
    // display

    } else if (chartConstructor.invalidClass) {
        var errorMessage = 'Component chartConstructor attribute set to "' + this.chartConstructor
                    + '". ';

        if (chartConstructor.invalidErrorMessage) errorMessage += chartConstructor.invalidErrorMessage;
        else errorMessage += ".This class is invalid - verify all appropriate modules are loaded.";

        this.logWarn(errorMessage);
    }

},


//> @attr listGrid.chartType (ChartType : "Column" : IRW)
// Default type of chart to plot.
//
// @visibility external
//<
chartType: "Column",


//> @method listGrid.chartRow()
// Chart a single row of data, with each cell value labeled by the column header.
//
// @param rowNum (Number) row to chart
// @param [dataFields] (Array of String) optional list of fields to use as labels.  By
//      default, all fields are used.
// @param [chartProperties] (Chart Properties) properties to pass to the created chart
// @return (Chart) created Chart instance
//
// @visibility external
//<
chartRow : function (rowNum, dataFields, chartProperties) {
    return this.chartData(null, dataFields, [this.getRecord(rowNum)], chartProperties);
},

//> @method listGrid.chartColumn()
// Chart a single column of data, with each cell value labeled by a value from another
// column.
//
// @param dataColumn (String) name of the ListGridField to use as a data
// @param labelColumn (String) name of the ListGridField to use as labels for data
// @param [chartProperties] (Chart Properties) properties to pass to the created chart
// @return (Chart) created Chart instance
//
// @visibility external
//<
chartColumn : function (dataColumn, labelColumn, chartProperties) {
    return this.chartData(labelColumn, [dataColumn], null, chartProperties);
},

// Grouping
// --------------------------------------------------------------------------------------------
// perform the actual operation of grouping the grid. Takes listgrid.data as input,
// and rewrites it (it is also an output); it stores the original object reference in
// listgrid.originalData.


groupIdField: "groupId",
groupParentIdField: "groupParentId",

regroup : function (fromSetData) {

    var asyncRegroupInProgress = this._asyncRegroupInProgress,
        currData;

    // This function is responsible for knowing if grouping ought to be performed.
    // Check all reasonable things here.
    // don't enforce canGroupBy here, so that programmatic grouping can take place
    // even if canGroupBy is false
    var isGrouped = this._isGrouped || this.isGrouped,
        groupFields = isGrouped && (this._groupByFields || this.getGroupByFields()),
        setOriginalDataToCurrData = false;

    if (!isGrouped ||
        groupFields == null || groupFields.length == 0 ||
        this.inhibitRegroup || !(this.originalData || this.data) ||
        // sanity check in case regroup is somehow triggered when data has not been loaded yet
        !this.data)
    {
        currData = null;
    } else {
        // if this.data already contains a grouped grid, use the original source
        // data instead.
        if (this.data.isGroupedOutput && this.originalData) {
            currData = this.originalData;
        } else {
            currData = this.data;

            // this.originalData will be set to currData after the (possibly asynchronous)
            // regroup has finished.
            setOriginalDataToCurrData = true;
        }
    }

    if (currData != null) {
        // Ensure all rows are read and available.
        var len = currData.getLength();
        if (isc.ResultSet && isc.isA.ResultSet(currData)
            && (!currData.lengthIsKnown() ||
                (len <= this.groupByMaxRecords && !currData.rangeIsLoaded(0, len - 1))))
        {
            // getRange will kick off the fetch to pick up a full set of data then _markForRegroup
            // ensures we regroup.
            currData.getRange(0, len);
            this._setMarkForRegroup(true, false, true, false, false, groupFields);
            this.logInfo("postponing grouping until data is loaded", "grouping");
            if (setOriginalDataToCurrData) {
                this.originalData = currData;
            }
            this._suppressRedrawOnDataChanged = true;
            return;
        } else if (len > this.groupByMaxRecords) {
            // If there are too many rows, don't group and display the original data.

            this.logInfo("Results too numerous - disabling grouping.", "grouping");
            if (asyncRegroupInProgress) {
                // Cancel the timer and clear the prompt.
                delete this._asyncRegroupInProgress;
                isc.Timer.clear(this._regroupTimerEvent);
                delete this._regroupTimerEvent;
                isc.clearPrompt();

                // Setting of two properties were postponed until the
                // asynchronous regroup finished and now they need to be set.
                this.setGroupByFieldSummaries(this._newGroupByFieldSummaries);
                this.setShowGroupSummary(this._newShowGroupSummary);
            }
            this.clearGroupBy();
            if (setOriginalDataToCurrData) {
                this.originalData = currData;
            }
            if (!this.selection || (this.data != this.selection.data)) {
                this.createSelectionModel();
            }

            return;
        }
    } else {
        if (asyncRegroupInProgress) {
            // regroup() was called while another asynchronous regroup was running.
            // Clear the timer and prompt.
            delete this._asyncRegroupInProgress;
            isc.Timer.clear(this._regroupTimerEvent);
            delete this._regroupTimerEvent;
            isc.clearPrompt();

            // Setting of two properties were postponed until the
            // asynchronous regroup finished and now they need to be set.
            this.setGroupByFieldSummaries(this._newGroupByFieldSummaries);
            this.setShowGroupSummary(this._newShowGroupSummary);
        }
        return;
    }

    this._setMarkForRegroup(false, false, true, false, false);
    // If this regroup was caused by a call to groupBy() then the `fields` argument will be
    // saved in the `_groupByCompleteFieldsAfterRegroup` property.
    var fields = this._groupByCompleteFieldsAfterRegroup;

    delete this._suppressRedrawOnDataChanged;

    var async = (currData.getLength() > this.groupByAsyncThreshold);
    if (asyncRegroupInProgress && async) {
        // An asynchronous regroup has already begun but regroup() has now been
        // called before it completed.  Cancel the timer but keep the prompt.
        // The flag this._asyncRegroupInProgress is still true.
        isc.Timer.clear(this._regroupTimerEvent);
        delete this._regroupTimerEvent;
    } else if (asyncRegroupInProgress && !async) {
        // An asynchronous regroup has already begun but regroup() has now
        // been called before it completed.  The current regrouping will
        // be performed synchronously so cancel the timer and clear the prompt.
        delete this._asyncRegroupInProgress;
        isc.Timer.clear(this._regroupTimerEvent);
        delete this._regroupTimerEvent;
        isc.clearPrompt();

        // Setting of two properties were postponed until the
        // asynchronous regroup finished and now they need to be set.
        this.setGroupByFieldSummaries(this._newGroupByFieldSummaries);
        this.setShowGroupSummary(this._newShowGroupSummary);
    } else if (!asyncRegroupInProgress && async) {
        // A new asynchronous regrouping will be started - so, unless showAsynchGroupingPrompt
        // is false, set the prompt
        if (this.showAsynchGroupingPrompt != false) isc.showPrompt(this.asynchGroupingPrompt);
        this._asyncRegroupInProgress = true;

        // Save the current settings of two properties that affect whether
        // group summary rows will be shown in the groupTree.  When the
        // _asyncRegroupInProgress flag is cleared then these two properties
        // will be checked again to see if the group summary rows need
        // to be updated.
        this._newShowGroupSummary = this.showGroupSummary;
        this._newGroupByFieldSummaries = this.groupByFieldSummaries;
    }

    // Assemble the list of field names to group by.
    var groupByField = this.groupByField,
        groupByFieldGroupingModes = null;
    if (fields != null) {

        groupByField = [];
        groupByFieldGroupingModes = [];
        for (var i = 0; i < fields.length; i++) {
            // Don't allow the checkbox field to be grouped; Shouldn't be possible, but check
            // just in case.
            if (this.isCheckboxField(fields[i])) continue;
            var hasProperty = fields[i].property,
                fieldName, field;
            if (hasProperty) {
                // If the field has a displayField and no optionDataSource, use the
                // displayField instead of the actual field passed in.
                fieldName = fields[i].property;
                field = this.getUnderlyingField(fieldName);
            } else {
                // If the field has a displayField and no optionDataSource, use the
                // displayField instead of the actual field passed in.
                fieldName = fields[i];
                field = this.getUnderlyingField(fieldName);
            }
            if (field && field.displayField != null && field.optionDataSource == null) {
                var fieldToDisplay = this.getField(field.displayField);
                if (fieldToDisplay) { field = fieldToDisplay; fieldName = field.displayField; }
            }
            groupByField.add(fieldName);
            // Initialize groupingMode if groupingModes is defined.
            var groupingMode = (hasProperty ? fields[i].grouping : null);
            if (field) {
                if (!hasProperty) {
                    groupingMode = field.groupingMode;
                }
                var groupingModes = field.groupingModes;
                if (groupingMode == null && groupingModes != null) {
                    var defaultMode = field.defaultGroupingMode;
                    if (isc.isAn.Array(groupingModes)) {
                        if (groupingModes.contains(defaultMode)) groupingMode = defaultMode;
                        else if (groupingModes.length > 0)       groupingMode = groupingModes[0];
                    } else if (isc.isAn.Object(groupingModes)) {
                        if (groupingModes[defaultMode] != null)  groupingMode = defaultMode;
                    }
                    // Note that the `groupingMode` is not saved to the field until
                    // _regroupFinish() is called.
                }
            }
            groupByFieldGroupingModes.push(groupingMode);
        }
    }
    var groupByFields = (
            groupByField != null && !isc.isAn.Array(groupByField) ?
                [groupByField] : groupByField),
        groupingMode = null;
    // If groupingModes is present, make sure to clear groupingMode.
    if (groupByFields != null) {
        for (var i = 0; i < groupByFields.length; i++) {
            var field = this.getUnderlyingField(groupByFields[i]),
                fieldGroupingMode = (
                    (groupByFieldGroupingModes != null &&
                        groupByFieldGroupingModes[i] != null) ?
                            groupByFieldGroupingModes[i] : field.groupingMode);
            if (fieldGroupingMode && field.name == groupByField) {
                groupingMode = fieldGroupingMode;
            }
        }
    }


    var baton = {
        applyFormulaAfterSummary: this.applyFormulaAfterSummary,
        emptyCellValue: this.emptyCellValue,
        gridSummaryRecordProperty: this.gridSummaryRecordProperty,
        groupByFieldSummaries: (
            isc.isAn.Array(this.groupByFieldSummaries) ?
                this.groupByFieldSummaries.duplicate() : null),
        groupNodeBaseStyle: this.groupNodeBaseStyle,
        groupNodeStyle: this.groupNodeStyle,
        groupStartOpen: (
            isc.isAn.Array(this.groupStartOpen) ?
                this.groupStartOpen.duplicate() : this.groupStartOpen),
        groupSummaryRecordProperty: this.groupSummaryRecordProperty,
        includeInSummaryProperty: this.includeInSummaryProperty,
        nullGroupTitle: this.nullGroupTitle,
        recordBaseStyleProperty: this.recordBaseStyleProperty,
        recordCustomStyleProperty: this.recordCustomStyleProperty,
        recordEditProperty: this.recordEditProperty,
        showCollapsedGroupSummary: this.showCollapsedGroupSummary,
        showGroupSummary: this.showGroupSummary,
        showGroupSummaryInHeader: this.showGroupSummaryInHeader,
        singleCellValueProperty: this.singleCellValueProperty,

        // Use this instead of `this.getGroupByFields()` during regrouping:
        groupByField: groupByField,
        groupByFields: groupByFields,
        groupByFieldGroupingModes: groupByFieldGroupingModes,
        groupingMode: groupingMode,

        currData: currData,
        setOriginalDataToCurrData: setOriginalDataToCurrData,
        sortSpecifiers: (
            isc.isAn.Array(this._sortSpecifiers) ? this._sortSpecifiers.duplicate() : null)
    };
    // Also save all mutable state of the ListGridFields that can be referenced during
    // asynchronous regrouping.
    var fieldsHaveUserFormulas = new Array(this.fields.getLength()),
        fieldsHaveUserSummaries = new Array(this.fields.getLength());
    for (var i = fieldsHaveUserFormulas.length; i--; ) {
        var field = this.fields[i];
        fieldsHaveUserFormulas[i] = field && field.userFormula != null;
        fieldsHaveUserSummaries[i] = field && field.userSummary != null;
    }
    baton.fieldsHaveUserFormulas = fieldsHaveUserFormulas;
    baton.fieldsHaveUserSummaries = fieldsHaveUserSummaries;
    var groupByFieldUserFormulas = new Array(groupByFields.length),
        groupByFieldUserSummaries = new Array(groupByFields.length),
        groupByFieldDisplayFields = new Array(groupByFields.length),
        groupByFieldValueMaps = new Array(groupByFields.length);
    for (var i = groupByFields.length; i--; ) {
        var field = groupByFields[i];
        if (field) {
            groupByFieldUserFormulas[i] = isc.clone(field.userFormula);
            groupByFieldUserSummaries[i] = isc.clone(field.userSummary);
            groupByFieldDisplayFields[i] = field.displayField;

            var valueMap = field.valueMap;
            if (isc.isAn.Array(valueMap)) {
                groupByFieldValueMaps[i] = valueMap.duplicate();
            } else {
                groupByFieldValueMaps[i] = isc.shallowClone(valueMap);
            }
        }
    }
    baton.groupByFieldUserFormulas = groupByFieldUserFormulas;
    baton.groupByFieldUserSummaries = groupByFieldUserSummaries;
    baton.groupByFieldDisplayFields = groupByFieldDisplayFields;
    baton.groupByFieldValueMaps = groupByFieldValueMaps;

    // Regrouping is performed with a sequence of function calls:
    // _regroupRestoreOpenFolder(), _regroupAddNodeToOpenState(),
    // _regroupCreateGroupTree(), _regroupAddRecordsToGroup(),
    // _regroupAddSummaryChildren(), _regroupProcessGroupHeaderNode(),
    // _regroupOpenInitialGroups(), _regroupCreateSelectionModel(),
    // _regroupGetGroupTreeLength(), _regroupCacheSelectionModel(),
    // and finally _regroupFinish().
    // If async is true, then the methods may run asynchronously to
    // perform work in batches of size groupByAsyncThreshold.
    // Each method calls itself with delay 0 until its work is completed
    // and then it calls the next method in the sequence.
    // Each function in the sequence passes the baton object
    // through the next asynchronous call (it is storage for variable
    // assignments that will be needed later in the sequence).
    this._regroupRestoreOpenFolders(async, baton, fromSetData);
},

_regroupRestoreOpenFolders : function (async, baton, fromSetData) {
    var restoreOpenFolders;
    if (this.groupTree) {
        // If we have valid data already, and haven't changed our set of group-by-fields, we'll
        // want to retain our current folder open/closed state
        // Suppress this if canCollapseGroup is false as in that case we always want to open
        // all group members.
        // Also suppress if coming from setData() since we'll have a new data set and should
        // initialize it with standard open state.

        if (!fromSetData && this.canCollapseGroup &&
            this.retainOpenStateOnRegroup &&
            (this.groupTree.getLength() > 0) &&
            (this.groupTree._groupByField == baton.groupByField))
        {
            // if grouping mode had been changed for our groupField we should not persist
            // open/close state for folders
            if (this.groupTree._groupingMode == baton.groupingMode) {
                restoreOpenFolders = [];

                var tree = this.groupTree,
                    node = tree.getRoot();
                if (tree.isOpen(node) && tree.isLoaded(node)) {
                    var stack = [[tree.getFolders(node)], [0]];
                    if (async) {
                        this._regroupTimerEvent = this.delayCall(
                            "_regroupAddNodeToOpenState",
                            [async, baton, stack, tree, restoreOpenFolders], 0);
                    } else {
                        this._regroupAddNodeToOpenState(
                            async, baton, stack, tree, restoreOpenFolders);
                    }

                    // _regroupAddNodeToOpenState() will call _regroupCreateGroupTree().
                    return;
                }
            }
        }
    }

    this._regroupCreateGroupTree(async, baton, baton.currData, restoreOpenFolders);
},

// Similar to ListGrid._addNodeToOpenState(), but rewritten to work asynchronously in batches
// of size groupByAsyncThreshold.
_regroupAddNodeToOpenState : function (async, baton, stack, tree, openState) {
    var childrenStack = stack[0],
        indexStack = stack[1],
        stackCount = childrenStack.length,
        recordCount = 0;

    while (stackCount > 0) {
        var children = childrenStack.pop(),
            i = indexStack.pop();
        --stackCount;

        for (var len = children.length; i < len; ++i, ++recordCount) {
            if (async && recordCount > this.groupByAsyncThreshold) {
                childrenStack.push(children);
                indexStack.push(i);
                this._regroupTimerEvent = this.delayCall(
                    "_regroupAddNodeToOpenState",
                    [async, baton, stack, tree, openState], 0);
                return;
            }

            var node = children[i];
            if (tree.isOpen(node) && tree.isLoaded(node)) {
                var folderInfo = {};
                folderInfo[node.groupName] = node.groupValue;
                openState.add(folderInfo);

                var grandChildren = tree.getFolders(node);
                if (grandChildren != null) {
                    childrenStack.push(children);
                    indexStack.push(i + 1);
                    ++stackCount;

                    children = grandChildren;
                    i = -1;
                    len = children.length;
                }
            }
        }
    }

    if (async) {
        this._regroupTimerEvent = this.delayCall(
            "_regroupCreateGroupTree",
            [async, baton, baton.currData, openState], 0);
    } else {
        this._regroupCreateGroupTree(async, baton, baton.currData, openState);
    }
},

_regroupCreateGroupTree : function (async, baton, currData, restoreOpenFolders) {

    var groupTree = this.createAutoChild("groupTree", {

        // specify idField and parentIdField explicitly
        // Note these field values are arbitrary as we explicitly add nodes to the tree
        idField:this.groupIdField,
        parentIdField: this.groupParentIdField,
        titleProperty: "groupValue",
        childrenProperty: "groupMembers",
        parentProperty: "_groupTree_" + this.ID,
        showRoot: false,
        isGroupedOutput: true,

        // _groupByField is used when sorting - we skip sorting on group headers unless sorting
        // by the group-by-field. Implemented in Tree.js
        // Exception - if showing summaries in headers we want to allow the user to sort the
        // group headers

        alwaysSortGroupHeaders: (baton.showGroupSummary && baton.showGroupSummaryInHeader),
        _groupByField: baton.groupByField,
        _groupByFields: baton.groupByFields,
        _groupingMode: baton.groupingMode,
        separateFolders: true,

        // Define a property used to flag the summary records
        _summaryRecordFlag: "_summaryRecordFlag",
        _summaryTargetNode: "_summaryTargetNode",

        // copy sort specifiers across

        _sortSpecifiers: baton.sortSpecifiers,

        // Implement 'getCriteria()' so "getCriteria()" on the grid as a whole works.
        // Note that DBC.updateDataModel handles applying criteria directly to the underlying
        // resultSet and regrouping so the "originalData" object always has the
        // most current criteria applied to it.
        getCriteria: function () {
            var originalData = this.creator.originalData;
            if (originalData && originalData.getCriteria) return originalData.getCriteria();
            return null;
        },

        // Given a group, returns the live records within it. Handles nested groups by reaching
        // into the sub-tree for outer groups.
        getRecordsInGroup : function (node) {
            var groupByFields = this._groupByFields,
                group = node.groupName,
                children = this.getChildren(node),
                records = [];

            if (group == groupByFields[groupByFields.length-1]) {
                if (!isc.isA.ResultSet(children) || children.lengthIsKnown()) {
                    for (var i = children.getLength(); i--; ) {
                        var child = children.getCachedRow(i);
                        if (child != null && child[this._summaryRecordFlag] !== true) {
                            records.add(child);
                        }
                    }
                }
            } else if (children != null) {
                if (!isc.isA.ResultSet(children) || children.lengthIsKnown()) {
                    for (var i = 0, length = children.getLength(); i < length; ++i) {
                        var child = children.getCachedRow(i);
                        if (child != null) {
                            var subRecords = this.getRecordsInGroup(child);
                            if (subRecords != null && subRecords.length > 0) {
                                records.addList(subRecords);
                            }
                        }
                    }
                }
            }
            return records;
        },

        combineWithEditVals : function (children) {
            var grid = this.creator,
                summaryChildren = [];
            summaryChildren.addList(children);

            // if the grid has any edited rows, pass the 'edited' record objects to
            // the summary function so we pick up user edits
            var editRows = grid.getAllEditRows();
            if (editRows.length > 0) {
                var editedInGroup;
                for (var i = 0; i < summaryChildren.length; i++) {
                    var editSession = grid.getEditSession(summaryChildren[i]);
                    if (editSession) {

                        var editedRecord = isc.addProperties({},
                                                            summaryChildren[i],
                                                            editSession._editValues);
                        summaryChildren[i] = editedRecord;
                    }
                }
            }
            return summaryChildren;
        },

        // override indexOf to handle being passed primary keys!
        primaryKeyFields:this.dataSource?this.getDataSource().getPrimaryKeyFieldNames():null,
        indexOf : function (node, pos, endPos) {
            // This can be called in inner for loops (for example during regrouping).

            var openList = this._getOpenList(),
                findKeys, hasKeys,
                index = -1;

            if (this.primaryKeyFields != null) {

                var keyFields = this.primaryKeyFields;
                findKeys = {};

                for (var i = 0; i < keyFields.length; i++) {
                    var keyField = keyFields[i];
                    if (node[keyField] == null) {
                        hasKeys = false;
                        break;
                    } else {
                        hasKeys = true;
                        findKeys[keyField] = node[keyField];
                    }
                }
            }

            if (hasKeys) {
                // go through the recordSet looking for a record with the same values for
                // the primary keys
                index = openList.findNextIndex(pos, findKeys, null, endPos);
            } else {
                index = openList.indexOf(node, pos, endPos);
            }
            return index;
        },

        // An asynchronous version of Tree.openAll() which operates in batches of
        // size batchSize.
        _openAllAsync : function (node, callback, batchSize, timerEventPropName) {
            if (!node) node = this.root;

            var getDescendantsAsyncCallback = function (nodeList, i) {
                if (i == null) {
                    i = 0;
                }
                for (var j = 0, length = nodeList.length; j < batchSize && i < length; ++i, ++j) {
                    // if the node is not already set to the newState
                    if (!this.isOpen(nodeList[i])) {
                        // call the dataChanged method to notify anyone who's observing it
                        this.changeDataVisibility(nodeList[i], true);
                    }
                }
                if (i < length) {
                    getDescendantsAsyncCallback.apply(this, [nodeList, i]);
                } else {
                    // make the node itself open
                    this.changeDataVisibility(node, true);
                    callback.apply(this, []);
                }
            };

            this._getDescendantsAsync(node, isc.Tree.FOLDERS_ONLY, null, getDescendantsAsyncCallback, batchSize, timerEventPropName);
        },

        // An asynchronous version of Tree.getDescendants() that works in batches of size batchSize.
        // node, displayNodeType, and condition are the original arguments, and callback, batchSize,
        // timerEventPropName, list, stack are additional arguments.  The callback is called after
        // all work is performed.  The total work performed by this method is split into batches of
        // work proportional to batchSize.  The list argument holds the eventual return value.
        // The stack argument holds iteration state that is passed between asynchronous invocations
        // of this method.  timerEventPropName identifies a property of the list grid in which to
        // store any timer events created.
        _getDescendantsAsync : function (node, displayNodeType, condition, callback, batchSize, timerEventPropName, list, stack) {
            var childrenStack, indexStack;
            if (list == null) {
                if (!node) node = this.root;

                // create an array to hold the descendants
                list = [];

                // if condition wasn't passed in, set it to an always true condition
                // XXX convert this to a function if a string, similar to getChildren()
                if (!condition) condition = function(){return true};

                // if the node is a leaf, return the empty list
                if (this.isLeaf(node)) {
                    callback.apply(this, [list]);
                    return;
                }

                // iterate through all the children of the node
                // Note that this can't depend on getChildren() to subset the nodes,
                //    because a folder may have children that meet the criteria but not meet the criteria itself.
                var children = this.getChildren(node);
                if (!children) {
                    callback.apply(this, [list]);
                    return;
                }

                childrenStack = [children];
                indexStack = [0];
                stack = { childrenStack: childrenStack, indexStack: indexStack };
            } else {
                childrenStack = stack.childrenStack;
                indexStack = stack.indexStack;
            }
            var stackCount = childrenStack.length,
                recordCount = 0;

            while (stackCount > 0) {
                var children = childrenStack.pop(),
                    i = indexStack.pop();
                --stackCount;

                // for each child
                if (!isc.isA.ResultSet(children) || children.lengthIsKnown()) {
                    for (var length = children.getLength(); i < length; ++i, ++recordCount) {

                        if (recordCount > batchSize) {
                            childrenStack.push(children);
                            indexStack.push(i);
                            this.creator[timerEventPropName] = this.delayCall(
                                "_getDescendantsAsync",
                                [node, displayNodeType, condition, callback, batchSize, timerEventPropName, list, stack], 0);
                            return;
                        }

                        // get a pointer to the child
                        var child = children.getCachedRow(i);

                        if (child != null) {
                            // if that child is a folder
                            if (this.isFolder(child)) {
                                // if we're not exluding folders, add the child
                                if (displayNodeType != isc.Tree.LEAVES_ONLY && condition(child))
                                    list[list.length] = child;

                                // now concatenate the list with the descendants of the child
                                var grandChildren = this.getChildren(child);
                                if (grandChildren) {
                                    childrenStack.push(children);
                                    indexStack.push(i + 1);
                                    ++stackCount;
                                    children = grandChildren;
                                    i = -1;
                                    length = children.getLength();
                                }
                            } else {
                                // if we're not excluding leaves, add the leaf to the list
                                if (displayNodeType != isc.Tree.FOLDERS_ONLY && condition(child)) {
                                    list[list.length] = child;
                                }
                            }
                        }
                    }
                }
            }

            // finally, return the entire list
            callback.apply(this, [list]);
        },

        addBeforeSummaries : function (record, currRoot) {
            var children = this.getChildren(currRoot);

            var targetIndex = children ? children.length : null;
            while (targetIndex > 0 &&
                   children && children[targetIndex-1][this._summaryRecordFlag])
            {
                targetIndex --;
            }
            this.add(record, currRoot, targetIndex);
        },


        _markForUpdateSummaryRows : function (groupNodes, fastChildren, nodeList) {
            if (!isc.isAn.Array(groupNodes)) groupNodes = [groupNodes];

            for (var i = 0; i < groupNodes.length; i++) {
                var originalNode = groupNodes[i];

                if (!originalNode._isGroup) originalNode = this.getParent(originalNode);
                if (!originalNode || !originalNode._isGroup) continue;

                var parents = this.getParents(originalNode);
                parents.unshift(originalNode);
                for (var j = parents.length - 2; j >= 0; j--) {
                    var node   = parents[j];
                    if (node._refreshGroup == null) {
                        node._refreshGroup = true;
                        if (fastChildren) {
                            var parent = parents[j+1];
                            if (!parent._dirtyChildren) parent._dirtyChildren = [];
                            parent._dirtyChildren.add(node);
                        } else if (nodeList) {
                            nodeList.add(node);
                        }
                    }
                }
            }
            if (nodeList) return nodeList;
        },

        _getDirtyGroups : function (node) {
            var dirtyChildren = node._dirtyChildren;
            if (!dirtyChildren) return [];
            delete node._dirtyChildren;
            return dirtyChildren;
        }
    }, isc.Tree);

    this.logInfo("Adding " + currData.getLength() + " records to groups", "grouping");

    // Create grouped tree from flat data, set group titles (traversing the tree in level order),
    // then finish the grouping and redraw.
    if (async) {
        this._regroupTimerEvent = this.delayCall(
                "_regroupAddRecordsToGroup",
                [async, baton, groupTree, currData, 0, currData.getLength(), restoreOpenFolders], 0);
    } else {
        this._regroupAddRecordsToGroup(
            async, baton, groupTree, currData, 0, currData.getLength(), restoreOpenFolders);
    }
},

_regroupAddRecordsToGroup : function (async, baton, groupTree, currData, startIndex, endIndex, restoreOpenFolders) {

    var finalIndex = endIndex;
    if (async) {
        endIndex = Math.min(startIndex + this.groupByAsyncThreshold, finalIndex);
    }

    // create grouped tree from flat data
    var loadingMarker = (
            isc.ResultSet && isc.isA.ResultSet(currData) ?
                isc.ResultSet.getLoadingMarker() : null);


    var dataOpenList;
    if (this.groupTree == this.data) {
        dataOpenList = this.data._getOpenList();
    }
    for (var i = startIndex; i < endIndex; ++i) {
        var record = currData.get(i);
        if (record != null && record != loadingMarker) {
            this.__addRecordToGroup(
                baton.groupByFields, this.data, baton.emptyCellValue,
                baton.groupSummaryRecordProperty, baton.gridSummaryRecordProperty,
                baton.fieldsHaveUserFormulas, baton.fieldsHaveUserSummaries,
                baton.applyFormulaAfterSummary, baton.groupByFieldUserFormulas,
                baton.groupByFieldUserSummaries, baton.groupByFieldDisplayFields,
                baton.groupByFieldValueMaps, baton.nullGroupTitle, baton.showGroupSummary,
                baton.showGroupSummaryInHeader, baton.recordBaseStyleProperty,
                baton.groupNodeBaseStyle, baton.recordCustomStyleProperty,
                baton.groupNodeStyle, baton.recordEditProperty, baton.singleCellValueProperty,
                groupTree, record, false, restoreOpenFolders, dataOpenList);
        }
    }

    if (async && endIndex < finalIndex) {
        this._regroupTimerEvent = this.delayCall(
                "_regroupAddRecordsToGroup",
                [async, baton, groupTree, currData, endIndex, finalIndex, restoreOpenFolders], 0);

    } else if (async) {
        this._regroupTimerEvent = this.delayCall(
            "_regroupAddSummaryChildren", [async, baton, groupTree], 0);
    } else {
        this._regroupAddSummaryChildren(async, baton, groupTree);
    }
},

// Adds the initial group summary rows, if any, to the groupTree.
_regroupAddSummaryChildren : function (async, baton, groupTree) {

    this.__updateSummaryRows(
        baton.showGroupSummaryInHeader, baton.groupByFields, baton.showCollapsedGroupSummary,
        baton.includeInSummaryProperty, baton.applyFormulaAfterSummary,
        baton.groupSummaryRecordProperty, baton.fieldsHaveUserFormulas, groupTree, null,
        baton.showGroupSummary, [], baton.groupByFieldSummaries);
    if (async) {
        this._regroupTimerEvent = this.delayCall(
            "_regroupProcessGroupHeaderNode", [async, baton, groupTree], 0);
    } else {
        this._regroupProcessGroupHeaderNode(async, baton, groupTree);
    }
},

// Helper method to add/update/remove group summary rows from the groupTree in response to
// changes to the showGroupSummary or groupByFieldSummaries properties of the ListGrid.
// Also handles refreshing summaries to react to data change (see refreshGroupSummary)
_updateSummaryRows : function (
    tree, prevShowGroupSummary, showGroupSummary,
    prevGroupByFieldSummaries, groupByFieldSummaries,
    refreshExisting, records, fields)
{
    return this.__updateSummaryRows(
        this.showGroupSummaryInHeader, this.getGroupByFields(), this.showCollapsedGroupSummary,
        this.includeInSummaryProperty, this.applyFormulaAfterSummary,
        this.groupSummaryRecordProperty, null, tree, prevShowGroupSummary, showGroupSummary,
        prevGroupByFieldSummaries, groupByFieldSummaries, refreshExisting, records, fields);
},
__updateSummaryRows : function (
    showGroupSummaryInHeader, groupByFields, showCollapsedGroupSummary,
    includeInSummaryProperty, applyFormulaAfterSummary, groupSummaryRecordProperty,
    fieldsHaveUserFormulas, tree, prevShowGroupSummary, showGroupSummary,
    prevGroupByFieldSummaries, groupByFieldSummaries, refreshExisting, records, fields)
{
    if (showGroupSummaryInHeader) {
        // In this case the group summaries are not displayed as extra leaves in the tree
        // and so there are no summary rows that need to be added/updated.
        return;
    }

    // A null setting of groupByFieldSummaries means that summaries for all of the groupBy
    // fields should be calculated.
    if (!isc.isAn.Array(prevGroupByFieldSummaries)) {
        prevGroupByFieldSummaries = groupByFields;
    }
    if (!isc.isAn.Array(groupByFieldSummaries)) {
        groupByFieldSummaries = groupByFields;
    }

    // Determine if there is a change in the showGroupSummary property that
    // would require the groupTree to be traversed to add/remove summary rows.
    var addedSummaries,
        removedSummaries,
        refreshSummaries;
    if (!(prevShowGroupSummary || showGroupSummary)) {
        // No group summary rows are currently showing or need to be shown, so
        // there is nothing to do.
        return;
    // Were showing, no longer are...
    } else if (prevShowGroupSummary && !showGroupSummary) {
        removedSummaries = prevGroupByFieldSummaries;
    // Were not showing, but are now
    } else if (!prevShowGroupSummary && showGroupSummary) {
        addedSummaries = groupByFieldSummaries;
    // Were showing, and still are (but may be different fields)
    } else {
        removedSummaries = [];
        if (groupByFieldSummaries) {
            for (var i = prevGroupByFieldSummaries.length; i--; ) {
                var summary = prevGroupByFieldSummaries[i];
                if (!groupByFieldSummaries.contains(summary)) {
                    removedSummaries.push(summary);
                }
            }
        }

        addedSummaries = [];
        refreshSummaries = [];
        for (var i = groupByFieldSummaries.length; i--; ) {
            var summary = groupByFieldSummaries[i];
            if (!prevGroupByFieldSummaries.contains(summary)) {
                addedSummaries.push(summary);
            } else if (refreshExisting) {
                refreshSummaries.push(summary);
            }
        }
    }
    if (!(addedSummaries && addedSummaries.length > 0) &&
        !(removedSummaries && removedSummaries.length > 0) &&
        !(refreshSummaries && refreshSummaries.length > 0)
       )
    {
        // There is nothing to do.
        return;
    }

    // If showCollapsedGroupSummary is true, add summary rows as
    // siblings of the group header rather than children. This ensures
    // they show up after the group members if the group is expanded, and
    // after the group header if the group is collapsed.

    var summariesAreSiblings = showCollapsedGroupSummary,
        fastChildren = records != null && !summariesAreSiblings;

    if (records != null) tree._markForUpdateSummaryRows(records, fastChildren);

    var root = tree.getRoot(),
        rootChildren = null;
    if (root && tree.isFolder(root)) {
        rootChildren = fastChildren ? tree._getDirtyGroups(root) : tree.getChildren(root);
    }
    if (!(root && rootChildren && !rootChildren.isEmpty())) {
        return;
    }



    var flag = tree._summaryRecordFlag,
        targetNodeFlag = tree._summaryTargetNode,
        skipSortFlag = Array.excludeFromSortProperty,
        parentStack = [root],
        siblingsStack = [rootChildren],
        indexStack = [0];


    var dataChanged = false;
    tree._deferDataChanged = true;

    while (indexStack.length > 0) {
        var parent = parentStack.pop(),
            siblings = siblingsStack.pop(),
            i = indexStack.pop();

        // we need to know the length, so skip a result set with in-progress operation
        if (isc.isA.ResultSet(siblings) && !siblings.lengthIsKnown()) continue;

        var len = siblings.getLength();
        for (; i < len; ++i) {
            var node = siblings.getCachedRow(i);
            if (!node) {
                continue;
            }
            if (records != null) {
                if (node._refreshGroup) delete node._refreshGroup;
                else                  continue;
            }

            var group = node.groupName,
                add = addedSummaries && addedSummaries.contains(group),
                remove = removedSummaries && removedSummaries.contains(group),
                children = tree.getChildren(node);

            // Handle "refresh" as remove then re-add.
            if (refreshSummaries && !add && !remove) {
                add = refreshSummaries.contains(group);
                remove = refreshSummaries.contains(group);
            }
            // assert !(add && remove)

            if (remove) {
                // Remove the summary rows, which are the last children.
                if (!summariesAreSiblings) {
                    if (!isc.isA.ResultSet(children) || children.lengthIsKnown()) {
                        var summaryChildren = [];
                        for (var j = children.getLength(); j-- > 0; ) {
                            var child = children.getCachedRow(j);
                            if (child[flag] === true) {
                                summaryChildren.push(child);
                            } else {
                                break;
                            }
                        }
                        dataChanged |= tree.removeList(summaryChildren);
                    }
                } else {
                    var summaries = [],
                        j = 0;
                    for (;;) {
                        var s = siblings.getCachedRow(i + j + 1);
                        if (s && s[flag] == true) {
                            summaries.add(s);
                            ++j;
                        } else {
                            break;
                        }
                    }
                    dataChanged |= tree.removeList(summaries);
                    len -= summaries.length;
                }
            }
            if (add) {
                // We want to call 'getGroupSummaryData' and pass in live records to pick up
                // the summary row(s) based on the actual data.
                // If there are multiple layers of nesting this means we'll have to reach into
                // the sub-tree to get the ultimate descendants (the live records), rather than
                // just looking at the children of the current node (which would be a set of
                // header-nodes unless this is the innermost group).
                var summaryChildren = tree.getRecordsInGroup(node);

                summaryChildren = tree.combineWithEditVals(summaryChildren);

                var summaryRecords = this._getGroupSummaryData(
                        includeInSummaryProperty, applyFormulaAfterSummary,
                        groupSummaryRecordProperty, fieldsHaveUserFormulas,
                        summaryChildren, node, fields);
                if (summaryRecords != null) {
                    if (!isc.isAn.Array(summaryRecords)) {
                        summaryRecords[flag]         = true;
                        summaryRecords[skipSortFlag] = true;
                        if (!summariesAreSiblings) {
                            dataChanged |= tree.add(summaryRecords, node)      != null;
                        } else {
                            dataChanged |= tree.add(summaryRecords, parent, i) != null;
                            i+=1;
                            len+=1;
                        }
                    } else {
                        for (var k = summaryRecords.length; k--; ) {
                            summaryRecords[k][flag]         = true;
                            summaryRecords[k][skipSortFlag] = true;

                            if (summariesAreSiblings) {
                                summaryRecords[k][targetNodeFlag] = node;
                            }
                        }
                        if (!summariesAreSiblings) {
                            dataChanged |= tree.addList(summaryRecords, node)        != null;
                        } else {
                            dataChanged |= tree.addList(summaryRecords, parent, i+1) != null;
                            i+=summaryRecords.length;
                            len+=summaryRecords.length;
                        }
                    }
                }
            }

            if (fastChildren) children = tree._getDirtyGroups(node);

            // Siblings in the groupTree should have the same type (folder or leaf).
            // Check the type of the first child.  This method calculates summaries
            // over folders so skip over the leaves.
            var childrenAreFolders = (children && !children.isEmpty() &&
                                      tree.isFolder(children.first()));
            if (childrenAreFolders) {
                // Recurse
                parentStack.push(parent);
                siblingsStack.push(siblings);
                indexStack.push(i + 1);
                parent = node;
                siblings = children;
                len = siblings.getLength();
                i = -1;
            }
        }
    }


    delete tree._deferDataChanged;
    if (dataChanged) {
        tree._clearNodeCache(true);
        tree.dataChanged();
    }
},

// Iterate through the tree recursively from root, for as many layers of nesting as
// required by the specified groupFields, applying group title and group summary info
// to the header nodes.
_regroupProcessGroupHeaderNode : function (async, baton, tree, state) {
    if (state == null) {
        state = {
            i: 0,
            children: tree.getChildren(tree.getRoot()),
            groupsVisited: 0,
            groupFields: baton.groupByFields,
            indexStack: [],
            childrenStack: []
        };
    }

    var i = state.i,
        children = state.children || [],
        groupsVisited = state.groupsVisited == null ? 0 : state.groupsVisited,
        groupFields = state.groupFields || [],
        indexStack = state.indexStack || [],
        childrenStack = state.childrenStack || [],
        recordCount = 0;
    if (groupFields.length > 0) {
        for (;;) {
            if (!isc.isA.ResultSet(children) || children.lengthIsKnown()) {
                // We know there's a layer of grouping below us, so process each child as a header
                // node as well.
                for ( ; i < children.getLength(); ++i, ++recordCount) {

                    if (async && recordCount > this.groupByAsyncThreshold) {
                        state.i = i;
                        state.children = children;
                        state.groupsVisited = groupsVisited;
                        this._regroupTimerEvent = this.delayCall(
                            "_regroupProcessGroupHeaderNode", [async, baton, tree, state], 0);
                        return;
                    }
                    // process each child that's a group node (and not a summary record)
                    var currNode = children.getCachedRow(i);
                    if (currNode != null && !currNode[this.groupSummaryRecordProperty]) {
                        var fld = this.getUnderlyingField(groupFields[groupsVisited]),
                            groupTitle = this._getGroupTitle(
                                tree, baton.groupByFields, baton.showGroupSummary,
                                baton.showGroupSummaryInHeader, baton.singleCellValueProperty,
                                currNode, fld);

                        // don't set singleCellValue if groupTitleField is set, in which case we want
                        // the groupTitle to be contained within a specific cell
                        if (!this._singleCellGroupHeaders(
                                baton.showGroupSummary, baton.showGroupSummaryInHeader))
                        {
                            currNode.groupTitle = groupTitle;
                        } else currNode[baton.singleCellValueProperty] = groupTitle;

                        if (baton.showGroupSummary && baton.showGroupSummaryInHeader) {
                            recordCount += this._applyGroupSummaryToHeader(
                                baton.groupByFieldSummaries, baton.includeInSummaryProperty,
                                baton.applyFormulaAfterSummary,
                                baton.groupSummaryRecordProperty, baton.fieldsHaveUserFormulas,
                                tree, currNode);
                        }

                        if (groupsVisited + 1 < groupFields.length) {
                            ++groupsVisited;
                            indexStack.push(i + 1);
                            childrenStack.push(children);
                            i = -1; // i will be incremented to zero before the start of the loop
                            children = tree.getChildren(currNode) || [];
                        }
                    }
                }
            }
            if (indexStack.length > 0) {
                --groupsVisited;
                i = indexStack.pop();
                children = childrenStack.pop();
            } else {
                break;
            }
        }
    }

    if (async) {
        this._regroupTimerEvent = this.delayCall(
            "_regroupOpenInitialGroups", [async, baton, tree, baton.restoreOpenFolders], 0);
    } else {
        this._regroupOpenInitialGroups(async, baton, tree, baton.restoreOpenFolders);
    }
},

_regroupOpenInitialGroups : function (async, baton, tree, restoreOpenFolders) {
    // If restoreOpenFolders is non null we will have passed it to 'addRecordToGroup'
    // and lazily instantiated the folders with the appropriate open property
    // Otherwise group fields have changed and we need to reset to initial open state
    if (!restoreOpenFolders) {
        if (!async) {
            this._openInitialGroups(baton.groupStartOpen, tree);
        } else {
            var callback = function () {
                    // 'this' refers to the groupTree AutoChild, so this.creator is the ListGrid.
                    this.creator._regroupCreateSelectionModel(
                        async, baton, baton.currData, baton.setOriginalDataToCurrData, tree);
                };

            // group open options
            var batchSize = 250;
            if (baton.groupStartOpen == "all") {
                tree._openAllAsync(null, callback, batchSize, "_regroupTimerEvent");
                return;
            } else if (baton.groupStartOpen == "first") {
                tree._openAllAsync(tree.getChildren(tree.getRoot()).first(), callback, batchSize, "_regroupTimerEvent");
                return;
            } else if (isc.isAn.Array(baton.groupStartOpen)) {
                // specific field values
                var children = tree.getChildren(tree.getRoot());
                for (var i = 0; i < baton.groupStartOpen.length; i++) {
                    var value = baton.groupStartOpen[i];
                    var folder = children.find("groupValue", value);
                    if (folder) tree.openFolder(folder);
                }
            }
        }
    }

    if (async) {
        this._regroupTimerEvent = this.delayCall(
            "_regroupCreateSelectionModel",
            [async, baton, baton.currData, baton.setOriginalDataToCurrData, tree],
            0
        );
    } else {
        this._regroupCreateSelectionModel(
            async, baton, baton.currData, baton.setOriginalDataToCurrData, tree);
    }
},

_regroupCreateSelectionModel : function (async, baton, currData, setOriginalDataToCurrData, tree) {
    if (setOriginalDataToCurrData) {
        this.originalData = currData;
    }

    // Clean up the previous groupTree, then switch to the current working groupTree.
    if (this.groupTree) {
        this.ignore(this.groupTree, "changeDataVisibility");
        this.groupTree.destroy();
    }
    this.groupTree = tree;

    // Set .data directly, because this may be called from setData() anyway.
    // Note that this also avoids us worrying about 'preserveEditsOnSetData'
    this.data = tree;
    this._observeGroupData(this.data);
    // reset the selection model so the user can select items in the tree
    this.createSelectionModel();

    if (async) {
        // In an asynchronous regroup, pre-calculate the cache length of the
        // groupTree then have this.selection create its internal cache
        // now instead of lazily because it can be expensive to compute.
        // _regroupGetGroupTreeLength() will call _regroupCacheSelectionModel()
        // which will then call _regroupFinish().
        this._regroupTimerEvent = this.delayCall(
            "_regroupGetGroupTreeLength", [async, baton], 0);
    } else {
        this._regroupFinish(async, baton);
    }
},

_regroupGetGroupTreeLength : function (async, baton) {
    // assert async
    // assert this.data == this.groupTree
    var batchSize = 4000;
    this.data._getLengthAsync(this, "_regroupTimerEvent", batchSize, function (length) {
        this._regroupTimerEvent = this.delayCall(
            "_regroupCacheSelectionModel", [async, baton], 0);
    });
},

_regroupCacheSelectionModel : function (async, baton) {
    // assert async
    // assert this.data.getNewSelection == null
    // assert isc.isA.Selection(this.selection) || isc.isA.CellSelection(this.selection)

    var callback = function () {
        this._regroupTimerEvent = this.delayCall("_regroupFinish", [async, baton], 0);
    };

    if (isc.isA.Selection(this.selection)) {
        var batchSize = 4000;
        this.selection._cacheSelectionAsync(this, "_regroupTimerEvent", batchSize, callback);

    } else if (isc.isA.CellSelection(this.selection)) {
        this.selection.getSelectedCells();
        this._regroupTimerEvent = this.delayCall("_regroupFinish", [async, baton], 0);
    }
},

_regroupFinish : function (async, baton) {

    // Store grouped fields for future reference.
    var groupByFields = baton.groupByFields;
    this.groupByField = groupByFields;
    if (baton.groupByFieldGroupingModes != null) {
        for (var i = groupByFields.length; i--; ) {
            var groupingMode = baton.groupByFieldGroupingModes[i];
            if (groupingMode != null) {
                groupByFields[i].groupingMode = groupingMode;
            }
        }
    }
    this.isGrouped = true;
    // Fire the group/viewStateChanged notification here.
    this.handleGroupStateChanged();

    // restore the selected state after a grouping operation
    if (this._lastStoredSelectedState) {
        this.setSelectedState(this._lastStoredSelectedState);
        delete this._lastStoredSelectedState;
    }
    // If we're showing the special 'groupTitleColumn', force it to refresh its width now

    if (this.fields.find("_isGroupTitleColumn", true) != null) {
        this.updateFieldWidthsForAutoFitValue("regroup with group title column showing");
    }

    this.markForRedraw("regroup");

    // Clear the asynchGroupingPrompt message that was set in regroup().
    if (async) {
        delete this._asyncRegroupInProgress;
        delete this._regroupTimerEvent;
        isc.clearPrompt();

        // Setting of two properties were postponed until the
        // asynchronous regroup finished and now they need to be set.
        this.setGroupByFieldSummaries(this._newGroupByFieldSummaries);
        this.setShowGroupSummary(this._newShowGroupSummary);
    }

    var fields = this._groupByCompleteFieldsAfterRegroup;
    if (this.groupByComplete != null && fields !== undefined) {
        this.groupByComplete(fields);
    }
},


retainOpenStateOnRegroup: true,

// Update grouping of the grid for a single changed record. Originally in
// _fireCellChanged.
// NOTE: originalRecord must be a TreeNode from this.data.
_incrementalRegroup : function (record, originalRecord, newValues) {
    // If changes are made locally, this code is responsible for updating
    // groups and records. If this grid is databound, the server update will
    // reset this.data to the new values, and regroup() (called from dataChanged())
    // will update the groups.

    this._suppressFolderToggleRedraw = true;

    var storedSelectedState = false;
    if (this._lastStoredSelectedState == null) {
        storedSelectedState = true;
        this._lastStoredSelectedState = this.getSelectedState(true);
    }

    // first remove record
    var groups = this.data.getParents(originalRecord),
        removeSucceeded = this.data.remove(originalRecord),
        originalGroupNode = null;

    // next recalc group titles and remove empty groups
    for (var i = 0; i < groups.length - 1; i++) {
        var currGroup = groups[i],
            members = currGroup.groupMembers,
            checkForSummaryChildren = this.showGroupSummary && !this.showCollapsedGroupSummary;
        if (checkForSummaryChildren ? !members.find("isGroupSummary", null) :
                                       members.getLength() == 0)
        {
            this.data.remove(currGroup);
        } else {
            var fld = (this.completeFields||this.fields).find(this.fieldIdProperty, currGroup);

            if (this.singleCellGroupHeaders()) {
                currGroup[this.singleCellValueProperty] = this.getGroupTitle(currGroup, fld);
            } else {
                currGroup.groupTitle = this.getGroupTitle(currGroup,fld);
            }
            if (!originalGroupNode) originalGroupNode = currGroup;
        }
    }

    // re-add record to group.
    // record may be null if eg adding a new record to unbound data. If it is,
    // add the new values instead - they represent what was added.
    // if original remove failed and we had data to begin with, don't add -
    // it's already been added elsewhere
    if (!(originalRecord && !removeSucceeded) && (record || newValues)) {
        this._addRecordToGroup(this.groupTree, record || newValues, true, true);
    }


    if (this.showGroupSummary) {
        var recordsToRefresh = [record || newValues];
        if (originalGroupNode) recordsToRefresh.add(originalGroupNode);
        this.refreshGroupSummary(recordsToRefresh);
    }
    // restore the selected state if we removed anything from the tree.
    // This will handle re-selecting a node updated to sit in a new group.
    if (storedSelectedState) {
        if (removeSucceeded) {
            this.setSelectedState(this._lastStoredSelectedState);
        }
        delete this._lastStoredSelectedState;
    }

},

_openInitialGroups : function (groupStartOpen, tree) {
    // group open options
    if (groupStartOpen == "all") {
        tree.openAll();
    } else if (groupStartOpen == "first") {
        tree.openAll(tree.getChildren(tree.getRoot()).first())
    } else if (isc.isAn.Array(groupStartOpen)) {
        // specific field values
        var children = tree.getChildren(tree.getRoot());
        for (var i = 0; i < groupStartOpen.length; i++) {
            var value = groupStartOpen[i];
            var folder = children.find("groupValue", value);
            if (folder) tree.openFolder(folder);
        }
    }
},


//> @method listGrid.groupBy()
// Display the current set of records grouped by their values for the given field or
// fields. With no arguments, disables all grouping.
// <P>
// Grouping transforms the current dataset into a Tree on the fly, then provides a familiar tree
// interface for exploring the grouped data. Note that for performance reasons grouping
// is only available for dataSets with less than +link{listGrid.groupByMaxRecords} entries.
// <P>
// Grouping works automatically with any dataset, providing simple default grouping based on
// each field's declared type.  However, you can use the
// +link{listGridField.getGroupValue,field.getGroupValue()} API to control how records are
// grouped, and the +link{listGridField.getGroupTitle,field.getGroupTitle()} API to control
// how groups are titled.
// <P>
// Grouping can be performed programmatically via this API, or you can set
// +link{canGroupBy,grid.canGroupBy:true} to enable menus that allow the user to perform
// grouping. To group a grid automatically, instantiate the grid with a
//  +link{listgrid.groupByField} setting.  To take action when an end use requests grouping,
// see +link{handleGroupBy()}.
// <P>
// While grouped, the automatically created Tree is available as
// +link{groupTree,grid.groupTree} and the original dataset is available as
// +link{originalData,grid.originalData}.
// <p>
// Grouping is often performed +link{groupByAsyncThreshold,asynchronously} to work around
// problems where browsers will incorrectly detect that a script is taking too long.  To be
// notified when grouping is complete, see +link{groupByComplete}.
//
// @param   [arguments 0-N] (Array of String) name of fields to group by
// @visibility external
// @group grouping
// @example dynamicGrouping
//<
groupBy : function (groupFieldName) {
    // support passing an Array instead of passing as a series of arguments
    var fields = [];

    if (isc.isAn.Array(groupFieldName)) {
        fields = groupFieldName;
    } else {
        for (var i = 0; i < arguments.length; i++) {
            fields[i] = arguments[i];
        }
    }

    if (!this.fields) {
        this.groupState = fields.join(",");
        this._markForRegroup = true;
        return;
    }

    for (var i = 0; i < fields.length; i++) {
        if (fields[i] != null && !this.getUnderlyingField(fields[i])) {
            this.logWarn("groupBy() passed field:" + fields[i] + " - this is not a valid field" +
                " within this grid - ignoring", "grouping");
            return;
        }
    }
    if (fields.length > 0 && fields[0] == null) {

        fields = [];
    }

    // fire handleGroupBy notification, and allow grouping to be cancelled if defined.
    if (this.handleGroupBy != null && this.handleGroupBy(fields) == false) {
        return;
    }

    // if arguments are null, return to original grouping
    if (fields.length == 0) {
        this.clearGroupBy();
        // fire the groupStateChanged / viewStateChanged notification here
        this.handleGroupStateChanged();
        // Fire the groupByComplete() notification.
        if (this.groupByComplete != null) {
            this.groupByComplete(fields);
        }
        return;
    }


    var groupByFields = [];
    for (var i = 0; i < fields.length; i++) {
        if (this.isCheckboxField(fields[i])) continue;
        var fieldName = (fields[i].property || fields[i]),
            field = this.getUnderlyingField(fieldName);
        if (field && field.displayField != null && field.optionDataSource == null &&
            this.getField(field.displayField))
        {
            fieldName = field.displayField;
        }
        groupByFields.add(fieldName);
    }
    this.logInfo("groupBy: " + groupByFields, "grouping");

    var origIsGrouped = this.isGrouped;
    this.isGrouped = true;
    // add the special groupTitleColumn if necessary - note that we have to do this after setting
    // isGrouped so getGroupTitleColumnPositon gives us back the position we want
    if (this.showGroupSummary && this.showGroupSummaryInHeader &&
        this.getGroupTitleField() == null && this.showGroupTitleColumn)
    {
        if (this.groupTitleColumn == null) {
            this.groupTitleColumn = this.getGroupTitleColumn();
        }
        if (this.fields.indexOf(this.groupTitleColumn) == -1) {
            this.addField(this.groupTitleColumn, this.getGroupTitleColumnPosition());
        }
    }
    // `isGrouped` will eventually be set to true in _regroupFinish().
    this.isGrouped = origIsGrouped;

    this._setMarkForRegroup(true, true, false, false, false, fields, groupByFields);
    this.dataChanged();
},

// Actually clears groupByFields / this.isGrouped and resets to original (ungrouped) data.
clearGroupBy : function () {
    this._setMarkForRegroup(false, false, false, true, false);

    this.logInfo("ungrouping", "grouping");

    if (this._asyncRegroupInProgress) {
        delete this._asyncRegroupInProgress;
        isc.Timer.clear(this._regroupTimerEvent);
        delete this._regroupTimerEvent;
        isc.clearPrompt();
    }

    // if groupingModes is present, make sure to clear groupingMode
    var groupByFields = this.getGroupByFields();
    if (groupByFields != null) for (var i = 0; i < groupByFields.length; i++) {
        var field = this.getUnderlyingField(groupByFields[i]);
        if (field) field.groupingMode = null;
    }

    this.isGrouped = false;
    if (this.originalData) {
        var pose = this.preserveEditsOnSetData;
        this.preserveEditsOnSetData = true;
        this.setData(this.originalData);
        this.preserveEditsOnSetData = pose;
        delete this.originalData;

        // Clear our pointer to this.groupTree if we have one.
        // Note: setData will actually destroy the groupTree if it's currently being shown
        // (As it destroys this.data)
        delete this.groupTree;
        if (this.groupByField) this.groupByField = null;
    }
    // in the case where showGroupSummaryInHeader is true we may have created a
    // default summary title column - clear this from our fields array now.
    if (this.groupTitleColumn != null) {
        var completeFields = this.completeFields || this.fields,
            index = completeFields.indexOf(this.groupTitleColumn);
        if (index != -1) {
            // duplicate the array, dropping the auto generated title column
            var newFields = [];
            for (var i = 0; i < completeFields.length; i++) {
                if (i == index) continue;
                newFields[newFields.length] = completeFields[i];
            }
            this.setFields(newFields);
        }
    }
},

groupIconPadding: 5,
// Returns group title for node. If field.getGroupTitle() exists, use that. Otherwise
// use previously computed title.
getGroupTitle : function (node, field) {
    return this._getGroupTitle(
        this.data, this.getGroupByFields(), this.showGroupSummary,
        this.showGroupSummaryInHeader, this.singleCellValueProperty, node, field);
},
_getGroupTitle : function (
    data, groupFields, showGroupSummary, showGroupSummaryInHeader, singleCellValueProperty,
    node, field)
{
    if (!field) {
        // If no field specified, use indentation level of node to index into groupByField
        var groupLevel = data.getLevel(node) - 1,
            field = this.getUnderlyingField(groupFields[groupLevel]);
    }
    if (field == null) {

        return;
    }

    var value;

    if (field.getGroupTitle) {
        return field.getGroupTitle(node.groupValue, node, field, field.name, this);
    }
    else if (!field.getGroupValue && field._simpleType && field._simpleType.getGroupTitle) {
        // Only pick up default groupingModes / groupTitle if getGroupValue has not been
        // overridden. Standard groupingModes will have no meaning to a custom getGroupValue()
        // method. Note that the inverse is not true: groupingModes or getGroupTitle() may be
        // modified for EG localization, but rely on the standard getGroupValue behavior
        return field._simpleType.getGroupTitle(node.groupValue, node, field, field.name, this);
    } else if (!this._singleCellGroupHeaders(showGroupSummary, showGroupSummaryInHeader)) {
        // just return the groupValue if groupTitleField is present
        value = node.groupTitle;
    } else {
        value = node[singleCellValueProperty];
        if (field.valueMap) {
            value = this.resolveValueMap(value, field.valueMap);
        }
    }

    if (field.escapeHTML && !node._escapedHTML && isc.isA.String(value) && value != this._$nbsp)
    {
        value = value.asHTML();
        node._escapedHTML = true;
    }

    return value;
},

//> @method listGrid.getGroupNodeHTML
// Returns the HTML code necessary to render a group node, including icon, title, and padding.
// The amount of the padding is at least +link{listGrid.groupLeadingIndent} pixels, and
// an additional +link{listGrid.groupIndentSize} pixels for each increasing level of the
// node.
// @param node (Object) Specified group node
// @visibility external
//<
getGroupNodeHTML : function (node, gridBody) {

    var isFrozenBody = this.frozenBody === gridBody;

    var hasFrozenBody = this.frozenFields && this.frozenFields.length > 0;
    // If we're rendering one single cell value across the row only render it
    // in the frozen body.
    if (hasFrozenBody && !isFrozenBody && this.singleCellGroupHeaders()) {
        return this.emptyCellValue;
    }
    var isOpen = this.data.isOpen(node),
        state = isOpen ? "opened" : "closed",
        url = isc.Img.urlForState(this.groupIcon, null, null, state),
        iconIndent = isc.Canvas.spacerHTML(this.groupIconPadding, 1),
        groupIndent =
            isc.Canvas.spacerHTML((this.data.getLevel(node) - 1) * this.groupIndentSize +
                                  this.groupLeadingIndent, 1),
        groupTitle = this.getGroupTitle(node);

    var imgConfig = {
            src:url,
            width:this.groupIconSize,
            height:this.groupIconSize,
            align:"middle"
        },
        img = this.imgHTML(imgConfig, null, null, null, null, null, null,
                isc.Canvas._generateSpanForBlankImgHTML);
    var retStr = (this.canCollapseGroup ? groupIndent + img
                                        : groupIndent) +
                 iconIndent + groupTitle;

    return retStr;
},

// helper function for adding a record to an existing group, or creating a new group
_addRecordToGroup : function (groupTree, record, setGroupTitles, openFolders) {
    return this.__addRecordToGroup(
        this.getGroupByFields(), this.data, this.emptyCellValue,
        this.groupSummaryRecordProperty, this.gridSummaryRecordProperty, null, null,
        this.applyFormulaAfterSummary, null, null, null, null, this.nullGroupTitle,
        this.showGroupSummary, this.showGroupSummaryInHeader, this.recordBaseStyleProperty,
        this.groupNodeBaseStyle, this.recordCustomStyleProperty, this.groupNodeStyle,
        this.recordEditProperty, this.singleCellValueProperty, groupTree, record,
        setGroupTitles, openFolders);
},
__addRecordToGroup : function (
    groupFields, data, emptyCellValue, groupSummaryRecordProperty, gridSummaryRecordProperty,
    fieldsHaveUserFormulas, fieldsHaveUserSummaries, applyFormulaAfterSummary,
    groupByFieldUserFormulas, groupByFieldUserSummaries, groupByFieldDisplayFields,
    groupByFieldValueMaps, nullGroupTitle, showGroupSummary, showGroupSummaryInHeader,
    recordBaseStyleProperty, groupNodeBaseStyle, recordCustomStyleProperty, groupNodeStyle,
    recordEditProperty, singleCellValueProperty, groupTree, record, setGroupTitles,
    openFolders, dataOpenList)
{
    var singleCellGroupHeaders = this._singleCellGroupHeaders(
            showGroupSummary, showGroupSummaryInHeader),
        currRoot = groupTree.getRoot(),
        node;


    for (var i = 0; i < groupFields.length; i++) {
        // find index of field w/ fieldName
        var fieldName = groupFields[i],
            field = this.getUnderlyingField(fieldName),
            recordNum;

        // recordNum passed to 'getRawCellValue' should be position of data in this.data
        // in order for things like edit-values to behave as expected.
        // Note that this may not reflect the position in the 'currData' [this.originalData]
        // and indeed can legitimately be -1 at this point if we're reacing to
        // a record being added to our dataSet.

        if (dataOpenList != null) {
            recordNum = dataOpenList.indexOf(record);
        } else {
            recordNum = data.indexOf(record);
        }

        var valueMap = (
                groupByFieldValueMaps == null ? field.valueMap : groupByFieldValueMaps[i]);

        // get the groupValue
        var currentUserFormula = field.userFormula,
            userFormula = (
                groupByFieldUserFormulas == null ?
                    currentUserFormula : groupByFieldUserFormulas[i]),
            currentUserSummary = field.userSummary,
            userSummary = (
                groupByFieldUserSummaries == null ?
                    currentUserSummary : groupByFieldUserSummaries[i]),
            fieldValue = this._getRawCellValue(
                emptyCellValue, groupSummaryRecordProperty, gridSummaryRecordProperty,
                fieldsHaveUserFormulas, fieldsHaveUserSummaries, data,
                applyFormulaAfterSummary, (userFormula != null), userFormula,
                (userSummary != null), userSummary, record, recordNum, fieldName, true);
        if (field.getGroupValue) {
            fieldValue = field.getGroupValue(fieldValue, record, field, fieldName, this);
        } else if (userFormula) {
            field.userFormula = userFormula;
            fieldValue = this.getFormulaFieldValue(field, record);
            field.userFormula = currentUserFormula;
        } else if (userSummary) {
            field.userSummary = userSummary;
            fieldValue = this.getSummaryFieldValue(field, record);
            field.userSummary = currentUserSummary;
        } else {
            var displayField = (
                    groupByFieldDisplayFields == null ?
                        field.displayField : groupByFieldDisplayFields[i]);
            if (displayField) {
                var displayValue = this._getDisplayValue(
                        field, valueMap, displayField, fieldValue, record);
                if (displayValue) {
                    fieldValue = displayValue;
                }
            } else if (field._simpleType && field._simpleType.getGroupValue) {
                fieldValue = field._simpleType.getGroupValue(
                    fieldValue, record, field, fieldName, this);
            }
        }

        if (valueMap && valueMap[fieldValue]) {
            fieldValue = valueMap[fieldValue];
        }
        if (fieldValue == null || isc.isAn.emptyString(fieldValue)) {
            fieldValue = nullGroupTitle;
        }

        // find the group, if it exists
        var nodeChildren = groupTree.getChildren(currRoot);
        if (nodeChildren == null) node = null;
        else node = nodeChildren.find('groupValue', fieldValue,
                                    field.type == "datetime" ? Array.DATETIME_VALUES : null);
        if (!node) { // add new group
            node = {
                groupName: fieldName,
                groupValue: fieldValue, _isGroup: true, canDrag: false,
                canSelect: false
            };

            // only add singleCellValue if groupTitleField isn't present

            if (singleCellGroupHeaders) node.singleCellValue = fieldValue;
            else node.groupTitle = fieldValue;

            // make sorting work
            node[fieldName] = fieldValue;
            // set group style
            node[recordBaseStyleProperty] = groupNodeBaseStyle;
            node[recordCustomStyleProperty] = groupNodeStyle;
            // set non-editable
            node[recordEditProperty] = false
            // if openFolders property is passed respect it now.
            // this allows us to lazily recreate the open/closed state of folders when rebuilding
            // a groupTree with the same set of groupByFields
            if (openFolders != null) {
                node[groupTree.openProperty] = isc.isA.List(openFolders) ?
                    openFolders.find(fieldName, fieldValue) : !!openFolders;
            }

            // add to the current group
            if (showGroupSummary) groupTree.addBeforeSummaries(node, currRoot);
            else                  groupTree.add               (node, currRoot);
        }
        // move to next group
        currRoot = node;
    }
    // add record to group
    if (showGroupSummary) groupTree.addBeforeSummaries(record, currRoot);
    else                  groupTree.add               (record, currRoot);

    // set the group titles if need be, mainly for dragging records in
    // start with the topmost group
    if (setGroupTitles) {
        var groups = groupTree.getParents(record);
        for (var i = groups.length - 2, j = 0; i >= 0; i--, j++) {

            var groupTitle = this._getGroupTitle(
                    groupTree, groupFields, showGroupSummary, showGroupSummaryInHeader,
                    singleCellValueProperty, groups[i]);

            if (!singleCellGroupHeaders) groups[i].groupTitle = groupTitle;
            else groups[i][singleCellValueProperty] = groupTitle;
        }
    }
},

//> @method listGrid.isGroupNode()
// If this listGrid is +link{listGrid.groupBy(),grouped}, is the record
// passed in a group header node.
// @param record (ListGridRecord) record to test
// @return (boolean) returns true if the record passed in is a group header node
// @visibility external
//<
isGroupNode : function (record) {
    if (record == null) return false;
    return !!record._isGroup;
},

applyGroupSummaryToHeader : function (tree, headerNode, fields) {
    return this._applyGroupSummaryToHeader(
        this.groupByFieldSummaries, this.includeInSummaryProperty,
        this.applyFormulaAfterSummary, this.groupSummaryRecordProperty, null,
        tree, headerNode, fields);
},
_applyGroupSummaryToHeader : function (
    groupByFieldSummaries, includeInSummaryProperty, applyFormulaAfterSummary,
    groupSummaryRecordProperty, fieldsHaveUserFormulas, tree, headerNode, fields)
{
    // Skip this if groupByFieldSummaries explicitly excludes this group
    var group = headerNode.groupName;
    if (groupByFieldSummaries != null && !groupByFieldSummaries.contains(group)) {
        return;
    }

    // If we're showing the group summary in the header node, call the
    // method to get group summary data, and copy the relevant properties across
    // [from the first record only - we don't currently support multiple summary lines
    // in the group header...]
    var summaryRecords = tree.combineWithEditVals(tree.getRecordsInGroup(headerNode)),
        summaryData = this._getGroupSummaryData(
            includeInSummaryProperty, applyFormulaAfterSummary, groupSummaryRecordProperty,
            fieldsHaveUserFormulas, summaryRecords, headerNode, fields);

    if (isc.isAn.Array(summaryData)) summaryData = summaryData[0];

    // bail if there's no summary data to process (no auto-summaries based on SimpleType or
    // fields with summary functions)
    if (!summaryData) return 0;

    // We want to apply all our fields (complete, not just visible, and the
    // includeInSummary and isGroupSummary values.
    // We don't want to do a blind 'addProperties' - this picks up additional meta data
    // enabled:false which we shouldn't apply to the header node.
    var fields = this.completeFields || this.fields;
    for (var i = 0; i < fields.length; i++) {
        var field = fields[i];
        var summaryFunc = this.getGridSummaryFunction(field);
        if (summaryFunc && isc.isAn.Array(summaryFunc)) summaryFunc = summaryFunc[0];
        if (summaryFunc == "count") {
            var array = headerNode[this.recordApplyPluralTitleProperty] || [];
            array.add(field.name);
            headerNode[this.recordApplyPluralTitleProperty] = array;
        }
        // don't clobber the groupTitle node value - we need that to display our title value
        var name = fields[i].name;
        if (name == "groupTitle") continue;
        headerNode[name] = summaryData[name];

    }
    headerNode[groupSummaryRecordProperty] = true;

    // Return the number of records in the summary to help determine how
    // to break up the computation of a regroup into asynchronously-run
    // batches.
    return summaryRecords.length;
},

//> @method listGrid.setShowGroupSummaryInHeader()
// Setter for +link{listGrid.showGroupSummaryInHeader}
// @param showGroupSummaryInHeader (boolean) new showGroupSummaryInHeader state
// @visibility external
//<
setShowGroupSummaryInHeader : function (showGroupSummaryInHeader) {
    if (this.showGroupSummaryInHeader == showGroupSummaryInHeader) return;
    this.showGroupSummaryInHeader = showGroupSummaryInHeader;
    var fields = this.getGroupByFields();
    this.ungroup();
    if (fields != null && fields.length > 0) {
        this.groupBy(fields);
    }
},

//> @method listGrid.setShowCollapsedGroupSummary()
// Setter for +link{listGrid.showCollapsedGroupSummary}
// @param showCollapsedGroupSummary (boolean) new showCollapsedGroupSummary value
// @visibility external
//<
setShowCollapsedGroupSummary : function (showCollapsed) {
    if (this.showCollapsedGroupSummary == showCollapsed) return;
    this.showCollapsedGroupSummary = showCollapsed;
    var fields = this.getGroupByFields();
    this.ungroup();
    if (fields != null && fields.length > 0) {
        this.groupBy(fields);
    }
},

//> @method listGrid.ungroup()
// Removes the grouping from the listgrid, restoring its original data
//
// @visibility external
//<
ungroup : function () {
    this._lastStoredSelectedState = this.getSelectedState(true);
    this.groupBy(null);
},

//> @method listGrid.setHeaderSpans()
// Update the headerSpans configuration on the grid dynamically.
//
// @param headerSpans (Array of HeaderSpan) same configuration block as that passed to
//                                          +link{listGrid.headerSpans}.
//
// @group headerSpan
// @visibility external
//<
setHeaderSpans : function (headerSpans) {
    this.headerSpans = headerSpans;
    // re-run setFields. This will reorder the fields to match the order
    // specified by the spans if necessary!
    if (this.completeFields) this.setFields(this.completeFields);
    this.layoutChildren("headerSpans changed");
},


//> @method listGrid.setHeaderSpanTitle()
// Update the title of a +link{listGrid.headerSpans,headerSpan} dynamically.
//
// @param name (String) name of the headerSpan, as specified via +link{headerSpan.name}.
// @param newTitle (String) new title for the headerSpan
//
// @group headerSpan
// @visibility external
//<
setHeaderSpanTitle : function (name, newTitle) {
    var spanConfig = this.getHeaderSpanConfig(name);
    if (!spanConfig) {
        this.logWarn("setHeaderSpanTitle() - unable to locate span named: " + name
                     + this.getStackTrace());
        return;
    }
    spanConfig.title = newTitle;
    if (spanConfig.liveObject && spanConfig.headerTitle == null) {
        spanConfig.liveObject.setTitle(newTitle);
    }
},

//> @method listGrid.setHeaderSpanHeaderTitle()
// Update the headerTitle of a +link{listGrid.headerSpans,headerSpan} dynamically.
//
// @param name (String) name of the headerSpan, as specified via +link{headerSpan.name}.
// @param newTitle (String) new headerTitle for the headerSpan
//
// @group headerSpan
// @visibility external
//<
setHeaderSpanHeaderTitle : function (name, newTitle) {
    var spanConfig = this.getHeaderSpanConfig(name);
    if (!spanConfig) {
        this.logWarn("setHeaderSpanHeaderTitle() - unable to locate span named: " + name
                     + this.getStackTrace());
        return;
    }
    spanConfig.headerTitle = newTitle;
    if (spanConfig.liveObject) spanConfig.liveObject.setTitle(newTitle);
},

//> @method listGrid.setHeaderSpanBaseStyle()
// Update the +link{headerSpan.headerBaseStyle} for a span within the grid at runtime.
// @param name (String) name of the headerSpan, as specified via +link{headerSpan.name}.
// @param newStyle (CSSClass) new baseStyle for the headerSpan
// @visibility external
//<
setHeaderSpanBaseStyle : function (name, baseStyle) {
    var spanConfig = this.getHeaderSpanConfig(name);
    if (!spanConfig) {
        this.logWarn("setHeaderSpanBaseStyle() - unable to locate span named: " + name
                     + this.getStackTrace());
        return;
    }
    spanConfig.headerBaseStyle = baseStyle;

    if (spanConfig.liveObject == null) return;

    // Treat being passed null as an attempt to revert to default base style.
    if (baseStyle == null) {
        var spannedFields = this.getSpannedFields(spanConfig),
            field = spannedFields[0];

        var buttonProperties = this.getHeaderButtonProperties();
        if (field.frozen && buttonProperties.frozenBaseStyle) {
            baseStyle = buttonProperties.frozenBaseStyle;
        } else if (buttonProperties.baseStyle) {
            baseStyle = buttonProperties.baseStyle;
        }

        if (baseStyle == null) {
            baseStyle = spanConfig.liveObject.getClass().getPrototype().baseStyle;
        }

    }
    spanConfig.liveObject.setBaseStyle(baseStyle);
},

//> @method listGrid.setHeaderSpanTitleStyle()
// Update the +link{headerSpan.headerTitleStyle} for a span within the grid at runtime.
// @param name (String) name of the headerSpan, as specified via +link{headerSpan.name}.
// @param newTitle (CSSClass) new titleStyle for the headerSpan
// @visibility external
//<
setHeaderSpanTitleStyle : function (name, titleStyle) {
    var spanConfig = this.getHeaderSpanConfig(name);
    if (!spanConfig) {
        this.logWarn("setHeaderSpanTitleStyle() - unable to locate span named: " + name
                     + this.getStackTrace());
        return;
    }
    spanConfig.headerTitleStyle = titleStyle;

    if (spanConfig.liveObject == null) return;

    if (titleStyle == null) {
        var spannedFields = this.getSpannedFields(spanConfig),
            field = spannedFields[0];

        var buttonProperties = this.getHeaderButtonProperties();
        if (field.frozen && buttonProperties.frozenTitleStyle) {
            titleStyle = buttonProperties.frozenTitleStyle;
        } else if (buttonProperties.titleStyle) {
            titleStyle = buttonProperties.titleStyle;
        }
        if (titleStyle == null) {
            titleStyle = spanConfig.liveObject.getClass().getPrototype().titleStyle;
        }
    }
    spanConfig.liveObject.setTitleStyle(titleStyle);
},

// group summaries

//> @attr listGrid.showGroupSummary (Boolean : false : IRW)
// If this listGrid supports +link{listGrid.canGroupBy,grouping}, setting this property will cause
// the grid to render an extra row at the end of each group when grouped, containing summary
// information for the fields.
// Summary information will be calculated by the +link{listGridField.getGroupSummary()} method
// if specified, otherwise via the specified +link{listGridField.summaryFunction}.
// @see listGrid.groupByFieldSummaries
// @visibility external
//<
showGroupSummary:false,

//> @attr listGrid.groupByFieldSummaries (Array of String : null : IRWA)
// If this grid is +link{listGrid.groupByField,grouped}, and
// +link{listGrid.showGroupSummary} is true, this attribute may be set to an array of
// groupBy field names for which group summaries should appear.
// <P>
// This is particularly useful for listGrids grouped by more than one field as it allows developers
// to display the group summary for a particular nested group without showing a summary for every
// level of the tree.
// @see listGrid.showGroupSummary
// @visibility external
//<


//> @attr listGrid.groupSummaryStyle (CSSStyleName : "gridSummaryCell" : IR)
// +link{listGridRecord.customStyle} for the group-level summary row displayed when
// +link{listGrid.showGroupSummary} is true.
// @visibility external
//<

groupSummaryStyle: "gridSummaryCell",


//> @method listGrid.setShowGroupSummary()
// Setter for the +link{listGrid.showGroupSummary} attribute
// @param showGroupSummary (boolean) new value for this.showGroupSummary
// @visibility external
//<
setShowGroupSummary : function (showGroupSummary) {
    if (this._asyncRegroupInProgress) {
        // Postpone the change in showGroupSummary until after the
        // asynchronous regroup.
        this._newShowGroupSummary = showGroupSummary;
        return;
    }

    var changed = (this.showGroupSummary != showGroupSummary);
    if (changed && this.groupTree) {
        var sums = this.groupByFieldSummaries;
        this._updateSummaryRows(
            this.groupTree,
            this.showGroupSummary, showGroupSummary,
            sums, sums);
    }
    this.showGroupSummary = showGroupSummary;
},

//> @method listGrid.setGroupByFieldSummaries()
// Setter for the +link{listGrid.groupByFieldSummaries} attribute
// @param groupByFieldSummaries (Array of String) new value for this.groupByFieldSummaries
// @visibility external
//<
setGroupByFieldSummaries : function (groupByFieldSummaries) {

    if (this._asynchRegroupInProgress) {
        // Postpone the change in groupByFieldSummaries until after the
        // asynchronous regroup.
        this._newGroupByFieldSummaries = groupByFieldSummaries;
        return;
    }

    // Determine if the new value of groupByFieldSummaries is different from the
    // old value.
    var changed = (this.groupByFieldSummaries != groupByFieldSummaries);
    if (!changed) {
        var isArray = isc.isAn.Array(groupByFieldSummaries);
        changed = (isArray != isc.isAn.Array(this.groupByFieldSummaries));
        if (!changed && isArray) {
            var length = groupByFieldSummaries.length;
            changed = (length == this.groupByFieldSummaries.length);
            if (!changed) {
                for (var i = length; !changed && i--; ) {
                    changed = !this.groupByFieldSummaries.contains(groupByFieldSummaries[i]);
                }
            }
        }
    }

    if (changed && this.groupTree) {
        var show = this.showGroupSummary;
        this._updateSummaryRows(
            this.groupTree,
            show, show,
            this.groupByFieldSummaries, groupByFieldSummaries);
    }
    this.groupByFieldSummaries = groupByFieldSummaries;
},

// Method to refresh the group summary row for some record
refreshGroupSummary : function (records, fields) {

    if (!this.isGrouped || !this.showGroupSummary || !this.groupTree) return;

    // allow a row number to be provided, which we map into node
    if (isc.isA.Number(records)) records = this.groupTree.get(records);

    // If we're showing the summary in the header we have to refresh the header node
    // with the new summary field values (and refresh the row).
    if (this.showGroupSummaryInHeader) {
        if (records != null) {
            var headerNodes = this.groupTree._markForUpdateSummaryRows(records, false, []);
            for (var i = headerNodes.length - 1; i >= 0; i--) {
                var headerNode = headerNodes[i];
                this.applyGroupSummaryToHeader(this.groupTree, headerNode, fields);
                this.refreshRow(this.groupTree.indexOf(headerNode));
                delete headerNode._refreshGroup;
            }
        } else {
            var headerNodes = this.groupTree.findAll("_isGroup", true);
            if (headerNodes != null) {
                for (var i = 0; i < headerNodes.length; i++) {
                    this.applyGroupSummaryToHeader(this.groupTree, headerNodes[i], fields);
                }
            }
            this.markForRedraw("refresh group summaries");
        }

    // Otherwise we have to rerun 'getChildren()' and refresh the relevant group's set of drawn
    // rows.
    } else {


        this._updateSummaryRows(this.groupTree,
            this.showGroupSummary, this.showGroupSummary,
            this.groupByFieldSummaries, this.groupByFieldSummaries,
            true, records, fields);
    }
},

//> @method listGrid.configureGrouping()
// Open a MultiGroupDialog to configure the fields used for grouping.
//
// @visibility external
//<
configureGrouping : function (targetField) {
    var grid = this,
        groupFields = this.getGroupByFields()
    ;
    if (targetField && (!groupFields || groupFields.length == 0)) {
        groupFields = [targetField[this.fieldIdProperty]];
    }
    isc.MultiGroupDialog.askForGrouping(
        this, groupFields,
        function (groupLevels) {
            if (groupLevels) grid.groupBy(groupLevels);
        }
    );
},

// GridRenderer / ListGrid API routing
// --------------------------------------------------------------------------------------------

// if this LV defines any of the GridRenderer's APIs, route those methods through to the LV.  See
// LV.classInit()
addBodyPassthroughMethods : function (body) {
    // if the body hasn't been created yet, bail - we always add the passthrough methods when we
    // create the body
    if (this.body) this._addBodyPassthroughMethods(this.body);
    if (this.frozenBody) this._addBodyPassthroughMethods(this.frozenBody);
},

_addBodyPassthroughMethods : function (body) {

    var body2LGMethods = {},
        methodNames = isc.getKeys(isc.ListGrid._passthroughMethods);

    for (var i = 0; i < methodNames.length; i++) {
        var methodName = methodNames[i],
            methodOnListGrid = this[methodName];

        // the cellContextClick stringMethod is specially overridden
        // - The GR.cellContextClick method is overridden to fire LG._cellContextClick()
        //   That will handle creating the context menu, as well as firing any developer-defined
        //   'cellContextClick' stringMethod.
        if (methodName == "cellContextClick") continue;

        // If the method is being cleared out, ensure we clear out any passthrough method
        // we may have previously set up
        if (methodOnListGrid == null) {

            body2LGMethods[methodName] = body.getClass().getPrototype()[methodName];

        // If the method on the listGrid is something other than a passBack method,
        // ensure the method on the GridBody actually calls it unless:
        // - the gridBody method is allready a passthrough (no need to do anything)
        // - the method on the gridBody has been overridden to be something other than
        //   the instance default
        } else if (methodOnListGrid != isc.ListGrid._passBackMethods[methodName] &&
                  !((body[methodName] != null) &&
                    (body[methodName]._isPassthroughMethod ||
                    (body[methodName] != body.getClass().getPrototype()[methodName])))
                  )
        {
            // install a passthrough method on the body to call the LG
            body2LGMethods[methodName] = isc.ListGrid._passthroughMethods[methodName];
        }

    }
    body.addMethods(body2LGMethods);
},

// anytime methods are added on the fly to a ListGrid instance, install passthrough functions
// on the body.  This makes sure that if you eg add a cellHover function on the fly, we notice
// and set up a passthrough from the body.

addProperties : function (methods) {
    this.Super("addProperties", arguments);
    this.addBodyPassthroughMethods();
},

addMethods : function (methods) {
    this.Super("addMethods", arguments);
    this.addBodyPassthroughMethods();
},

// React to 'setProperties()' modifying drag related properties by updating
// 'canDrag' / 'canAcceptDrop' / etc
_$dragProperties : {
    canDragSelect:true,
    canDragSelectText:true,
    canDragRecordsOut:true,
    canReorderRecords:true,
    canAcceptDroppedRecords:true
},
// React to changes to 'alternateRecordStyles', 'fixedRecordHeights', et al by
// changing the related property ('alternateRowStyles', etc) on the body.
_$gridPropertyRenames : {

        // We have an explicit setBodyOverflow so no need to catch this case
        // overflow:this.bodyOverflow,
        bodyBackgroundColor:"backgroundColor",
        bodyStyleName:"styleName",

        // Note updating fixedRowHeights on the body is required in addition to the fact
        // we pass our "shouldFixRowHeight()" method through to the body.
        // That method has no effect if fixedRowHeights is false on the GridRenderer.
        fixedRecordHeights:"fixedRowHeights",

        fixedFieldWidths:"fixedColumnWidths",
        alternateRecordStyles:"alternateRowStyles",
        alternateRecordFrequency:"alternateRowFrequency",
        showAllRecords:"showAllRows",

        canSelectText:"canDragSelectText"
},
// propertyChanged - fired by setProperties for each modified property.
propertyChanged : function (propName, value) {
    this.invokeSuper(isc.ListGrid, "propertyChanged", propName, value);
    if (this._$dragProperties[propName]) this._dragPropertyChanged = true;
    if (this.body == null) return;
    if (isc.ListGrid._gridPassthroughProperties.contains(propName)) {
        this.body[propName] = value;
    }
    if (this._$gridPropertyRenames[propName] != null) {
        this.body.setProperty(this._$gridPropertyRenames[propName], value);
        // markForRedraw to display the changes

        this.markForRedraw();
    }
},

// doneSettingProperties - fired after all the properties have been updated.
doneSettingProperties : function () {
    if (this._dragPropertyChanged) this._setUpDragProperties();
},

// EditMode
// ---------------------------------------------------------------------------------------
setChildEditableProperties : function (liveObject, properties, editNode, editContext) {
    var schema = isc.DS.get(editNode.type);
    if (schema && schema.inheritsSchema("ListGridField")) {
        // Special case to handle VB editing of LGF with change to hidden property.
        if (!properties.hidden && liveObject.showIf == "false") liveObject.showIf = null;

        isc.addProperties(liveObject, properties);
        this.setFields(this.completeFields || this.fields);
    } else {
        this.Super("setChildEditableProperties", arguments);
    }
},

// ---------------------------------------------------------------------------------------
getExportFieldValue : function (record, fieldName, fieldIndex) {
    // Checkbox icons (and generally all other icons) are a no-no for exported data.
    // If the display value would return a checkbox icon, use the raw cell value instead.
    var field = this.getSpecifiedField(fieldName);
    if (this.isCheckboxField(field)
        || field.valueIcons
        || this._formatBooleanFieldAsImages(field))
    {
        // getCellValue() is avoided in this code path to avoid its icon handling,
        // but there is some code there that must be duplicated here
        var useDisplayFieldValue = (field.displayField != null && !field.valueMap &&
            !field.getCellValue && this._useDisplayFieldValue(field));

        var recordNum = this.getRecordIndex(record),
            value;

        value = this.getRawCellValue(record, recordNum,
            useDisplayFieldValue ? field.displayField : fieldName,
            true);

        var hiliteAfterFormat = field.hiliteHTMLAfterFormat;
        if (hiliteAfterFormat == null) hiliteAfterFormat = this.hiliteHTMLAfterFormat;
        if (hiliteAfterFormat == null) hiliteAfterFormat = true;
        var hilites = this.getFieldHilites(record, field);

        if (!hiliteAfterFormat) {
            // Note that hilites can == 0, so if (hilites) doesn't work
            if (hilites != null) value = this.applyHiliteHTML(hilites, value);
            value = this._formatCellValue(value, record, field, recordNum, fieldIndex);
        } else {
            value = this._formatCellValue(value, record, field, recordNum, fieldIndex);
            if (hilites != null) value = this.applyHiliteHTML(hilites, value);
        }
        return this.htmlUnescapeExportFieldTitle(value.toString());
    }

    return this.Super("getExportFieldValue", arguments);
},

//> @method listGrid.getOriginalData()
// Returns the original, ungrouped data in the grid. If the grid is ungrouped,
// returns +link{listGrid.getData()}.
// @group grouping
//
// @return (object) The ungrouped data that is being displayed and observed
// @visibility external
//<
getOriginalData : function () {
    return (this.isGrouped && this.originalData) ? this.originalData : this.getData();
},

//> @groupDef excelPasting
// +link{DataSource.recordsAsText()} can export a set of DataSource records in
// tab-separated-values format so that it can be copied and pasted into a Microsoft Excel
// spreadsheet.
// <P>
// However, be aware that Excel does a bunch of type guessing on pasted data:
// <ul>
// <li> values that look like dates (eg 1-2-2011 and even just 1-2) will become true date-valued
//      cells (as indicated by Excel rendering them as eg 2-Jan).  Note that the month-day-year
//      interpretation is <b>locale-dependent</b> so be sure text is exported
// <li> values that look numeric, eg "5.0" become true number values (as indicated by Excel
//      showing just "5")
// <li> values that look like times, eg "5:30", will be converted to times (as indicated by
//      Excel showing 5:30:00 AM when editing the value)
// <li> values with a leading "=" will be treated as formulas
// </ul>
// Unfortunately, when these behaviors are undesirable, there is no means of turning them off
// that doesn't have any drawbacks.  You can:
// <ul>
// <li> adding a leading space or other char (but this changes the cell value).
// <li> turning the cell into a trivial formula, eg ="literal value".  But this means that when
//     the cell is edited, it's value is a formula.
// <li> format the cells as text in Excel before pasting data onto them
// </ul>
// The first or second approach can be enabled when exporting text - see
// +link{DataSource.recordsAsText()} and +link{DataSourceField.exportForceText}.
//
// @title Copy and Paste with Excel
//<


//> @method listGrid.getSelectedCellData()
// Returns the selected cells as a series of Records where each field value is stored under
// it's offset from the top-left of the selection.  For example, a 2x2 cell selection starting
// from the first column would return two Records, each with two values stored under the names
// "0" and "1".
// <P>
// If +link{canSelectCells} is not enabled, this API always returns null.
//
// @return (RecordList) list of Records as described above
// @visibility external
//<
getSelectedCellData : function (copyDownFromTopRow, copyRightFromLeftColumn) {
    if (this.canSelectCells) {
        var records = [],
            values = {},
            undef;

        var selection = this.selection.getSelectedCells();
        if (selection.length == 0) return [];

        var value, index = 0,
            origCol = selection[index][1];

        while (index < selection.length) {

            var record = {},
                addRecord = false,
                firstRow = selection[index][0],
                firstCol = selection[index][1];

            while (index < selection.length) {
                var row = selection[index][0];
                var col = selection[index][1];
                if (row != firstRow) break;

                if (col == firstCol || !copyRightFromLeftColumn) {
                    var gridRecord = this.getCellRecord(row, col);
                    if (this.shouldShowRecordComponent(gridRecord, col)) {
                        value = this.getRawCellValue(gridRecord, row, col);
                        // to assign to record, we must first map undef to null
                        if (value === undef && this.copyEmptyCells) value = null;
                        // cell is shown; row matters
                        addRecord = true;
                    } else value = undef;
                }
                var targetCol = col - origCol;
                if (copyDownFromTopRow) {
                    var binding = values[targetCol];
                    if (binding !== undef) value = binding;
                    else values[targetCol] = value;
                }
                if (value !== undef) record[targetCol] = value;
                index++;
            }
            if (addRecord) records.add(record);
        }
        return records;
    }
    return null;
},

//> @method listGrid.applyCellData()
// Applies a set of Records containing coordinate-based data as returned by
// +link{getSelectedCellData()} and applies the data at the current selection.
// <P>
// For consistency with Excel, given a record in the cellData, after the data value with the
// most negative column index is found, the rest of the values in the record are applied
// contiguously to the right of it, using the positional data for ordering only.
// <P>
// Will only modify cells in the grid which are editable, and changes will be applied
// as editValues, exactly as though the user had typed the values in (see
// +link{group:editing,Grid Editing Overview}).
// <P>
// See also +link{applyRecordData()}.
//
// @param cellData (RecordList) list of Records as described above
// @visibility external
//<

applyCellData : function (cellData, preserveOffsets, targetCells) {
    if (!this.canSelectCells || !cellData) return;

    if (!targetCells) targetCells = this.selection.getSelectedCells().slice(0,1);
    if (targetCells.length == 0) return;

    var origCol = targetCells[0][1],
        nTotalRows = this.getTotalRows();

    var rowIterator = isc.CellListIterator.create({cells: targetCells});
    for (var i = 0, row = rowIterator.getNextRow(); i < cellData.length && row < nTotalRows;
             i++,   row = rowIterator.getNextRow()) {

        while (!this._isValidRowForApplyGridData(row)) { row = rowIterator.getNextRow(); }
        if (row >= nTotalRows) break;

        var col,
            record = cellData[i];

        var indices = isc.ListGrid._getCoordinateList(record, !preserveOffsets);
        for (var j = 0; j < indices.length; j++) {
            if (j == 0 || preserveOffsets) col = origCol + indices[j];
            else                           col++;

            for (var field = null; col >= 0 && col < this.getTotalCols(); col++) {
                field = this._getFieldIfNonGenerated(col);
                if (field || preserveOffsets) break;
            }
            if (field && this.canEditCell(row, col)) {
                this.setEditValue(row, col, record[indices[j]]);
            }
        }
    }
},

//> @method listGrid.applyRecordData()
// Applies a list of Records as changes to the current selection.
// <P>
// Values found in each of the passed records will be applied to the same-named fields in the
// Records starting from the top-left of the current selection, in order.
// <P>
// Will only modify cells in the grid which are editable, and changes will be applied
// as editValues, exactly as though the user had typed the values in (see
// +link{group:editing,Grid Editing Overview}).
// <P>
// See also +link{applyCellData()}.
//
// @param recordData (RecordList) list of Records as described above
// @visibility external
//<
applyRecordData : function (recordData) {
    if (!this.canSelectCells || !recordData) return;

    var targetCells = this.selection.getSelectedCells();
    if (targetCells.length == 0) return;

    var undef,
        baseRow = targetCells[0][0],
        nTotalRows = this.getTotalRows();

    for (var i = 0, row = baseRow; i < recordData.length && row < nTotalRows; i++, row++) {

        while (!this._isValidRowForApplyGridData(row)) { row++; }
        if (row >= nTotalRows) break;

        var record = recordData[i];
        for (var name in record) {
            if (record.hasOwnProperty(name)) {
                var value = record[name];
                if (value !== undef) {
                    var column = this.getFieldNum(name);
                    var field  = this.getField(column);
                    if (field && this.canEditCell(row, column)) {
                        this.setEditValue(row, column, value);
                    }
                }
            }
        }
    }
},

_getFieldIfNonGenerated : function (fieldIndex) {
    var field = this.getField(fieldIndex);
    if (!field || field.userFormula || field.userSummary ||
         field.type == this._$summary) return null;
    return field;
},

_isValidRowForApplyGridData : function (row) {
    var record = this.getCellRecord(row);
    if (record == null) return true;
    return !record._isGroup && !record[this.isSeparatorProperty] &&
           !record[this.groupSummaryRecordProperty];
},

_invokeKeyboardCopyPasteShortcut : function (keyName) {

    if (!this.useCopyPasteShortcuts || !this.canSelectCells) return true;

    switch (keyName) {
        case "D":
            this.applyCellData(this.getSelectedCellData(true, null), true,
                               this.selection.getSelectedCells());
            break;
        case "R":
            this.applyCellData(this.getSelectedCellData(null, true), true,
                               this.selection.getSelectedCells());
            break;
        case "C":
            isc.ListGrid._cellClipboard = this.getSelectedCellData();
            break;
        case "V":
            this.applyCellData(isc.ListGrid._cellClipboard);
           break;
    }
    return false;
},

// Advanced Field Picker
// ---------------------------------------------------------------------------------------

//> @attr listGrid.useAdvancedFieldPicker (Boolean : false : IR)
// If set to true, an advanced field picker based on the +link{FieldPicker} will be shown instead
// of the column picker submenu if there are more fields in the grid than
// +link{advancedFieldPickerThreshold}.
// <P>
// When there are large numbers of available fields, the FieldPicker-based interface is more
// usable for both defining visible fields and defining field order.
// @visibility external
//<
useAdvancedFieldPicker: false,

//> @attr listGrid.canEditTitles (boolean : false : IRW)
// If set to true, the +link{listGrid.useAdvancedFieldPicker,advanced field picker} provides
// an interface allowing users to modify fields' titles.
// <P>
// Note that when enabled, the +link{getFieldState(),field state} for this component
// will include field titles.
//
// @visibility external
//<
canEditTitles:false,

//> @attr listGrid.fieldPickerWindow (AutoChild FieldPickerWindow : null : IR)
// Instance of +link{FieldPickerWindow} used if +link{useAdvancedFieldPicker} is set.
// @visibility external
//<

fieldPickerWindowDefaults : {
    autoParent: "none",
    _constructor: "FieldPickerWindow"
},

//> @attr listGrid.fieldPickerFieldProperties (Array of String : null : IR)
// Names of properties on +link{ListGridField} for which the +link{fieldPicker} should
// show an editing interface, for convenience.
// <P>
// For example, specify ["frozen", "decimalPrecision"] to allow end users to modify
// +link{listGridField.frozen} and +link{listGridField.decimalPrecision} respectively.
// @visibility external
//<
fieldPickerFieldProperties: null,

//> @attr listGrid.advancedFieldPickerThreshold (int : 25 : IR)
// When +link{useAdvancedFieldPicker} is set, total number of available fields that must be
// present in the grid before the advanced field picker interface is used instead of the normal
// columns submenu.
// <P>
// Set to 0 to have the advanced picker always used (when useAdvancedFieldPicker is true).
// @visibility external
//<
advancedFieldPickerThreshold: 25,

//> @attr listGrid.fieldPickerShowSampleValues (Boolean : true : IR)
// When set to false, sample values of the FieldPicker are never shown. This property applies to the entire FieldPicker.
// @visibility external
//<
fieldPickerShowSampleValues: true

});

//
// Register 'stringMethods' for ListGrid instances
// NOTE: all GridRenderer StringMethods are available on the LV too; see classInit()

isc.ListGrid.registerStringMethods(isc.GridRenderer._gridAPIs);
isc.ListGrid.registerStringMethods({
    // XXX the list of variables available for an expression is good here, but as method signatures
    // these look wacky.  Should probably be:
    // recordClick:"record,field,value,rawValue,rowNum,colNum"
    recordClick : "viewer,record,recordNum,field,fieldNum,value,rawValue",
    recordDoubleClick : "viewer,record,recordNum,field,fieldNum,value,rawValue",


    recordsDropped : "records,rowNum,viewer,sourceWidget",
    recordDropMove : "viewer,recordNum,record,position",

    //>!BackCompat 2004.12.10
    //> @method listGrid.editValueChanged
    // Callback fired when an edit modifies the value of a cell (before attempting to save that
    // value permanently)
    //
    // @param    rowNum       (number)    row number for the cell
    // @param   fieldName   (string)    name of the edited field
    // @param    newValue   (any)    new value for the cell
    // @param    oldValue   (any)    old value for the cell
    // @visibility internal
    // @deprecated  As of SmartClient 5.5, use +link{listGrid.editorChange}.
    //  @group  editing
    //<
    editValueChanged : "rowNum,fieldName,newValue,oldValue",
    //<!BackCompat

    //> @method listGrid.editorChange
    // Callback fired when an edit modifies the value of a cell (before attempting to save that
    // value permanently)
    //
    // @param   record      (ListGridRecord)    record being edited (null if this is a new row)
    // @param    newValue   (any)    new value for the cell
    // @param    oldValue   (any)    old value for the cell
    // @param    rowNum       (number)    row number for the cell
    // @param   colNum      (number) column number of the cell
    // @visibility advancedInlineEdit
    //  @group  editing
    // @example calculatedCellValue
    //<
    editorChange : "record,newValue,oldValue,rowNum,colNum",

    //> @method listGrid.cellChanged
    // Fires after user edits have been successfully saved to the server, only for cells where the
    // value was actually modified.
    // <p>
    // If you want immediate notification of a changes <b>before</b> changes has been saved to the
    // server, implement +link{listGridField.change,field.change()} or
    // +link{listGridField.changed(),field.changed()} instead.
    // <P>
    // You can alternatively use +link{listGridField.cellChanged} to get notification only of
    // saved changes for a specific field.  If both a listGridField and the containing listGrid
    // have a handler for this event, only the handler defined on the field is called.
    //
    // @param    record     (ListGridRecord)    record for the cell being changed
    // @param    newValue   (any)    new value for the cell
    // @param    oldValue   (any)    old value for the cell
    // @param    rowNum       (number)    row number for the cell
    // @param    colNum       (number)    column number of the cell
    // @param    grid       (ListGrid)    grid where cell was changed.  Also available as "this"
    // @group  editing
    //
    // @see listGridField.cellChanged()
    // @visibility external
    //<
    // NOTE: extra recordNum/fieldNum parameters allow limited backward compatibility, since any
    // time a string expression is provided, parameter order doesn't matter.
    // old signature: recordNum,fieldNum,newValue,oldValue

    cellChanged : "record,newValue,oldValue,rowNum,colNum,grid,recordNum,fieldNum",




    //> @method listGrid.editComplete()
    // Callback fired when inline edits have been successfully saved.
    // <P>
    // No default implementation.
    //
    //  @group  editing
    //
    // @param rowNum (number) current index of the row that was saved
    // @param colNum (number) index of the column that was saved, if applicable
    // @param newValues (Object | Record) new values that were saved
    // @param oldValues (Record) the complete original values from before the save occurred
    // @param editCompletionEvent (editCompletionEvent) Event that led to the save
    // @param [dsResponse] (DSResponse) for DataSource saves, DSResponse object returned
    // @visibility external
    //<
    editComplete : "rowNum,colNum,newValues,oldValues,editCompletionEvent,dsResponse",

    //> @method listGrid.editFailed()
    // Called when an attempt to save inline edits fails, due to a validation error or other
    // server error.
    // <P>
    // The default implementation of editFailed does nothing for normal validation errors,
    // which are displayed before editFailed() is called.  For any other errors, the default
    // implementation will call +link{classMethod:RPCManager.handleError()}, which by default
    // will result in a warning dialog.
    //
    //  @group  editing
    //
    // @param rowNum (number) current index of the row we attempted to save
    // @param colNum (number) index of the column where the edit failed, if applicable
    // @param newValues (Object | Record) new values that we attempted to save
    // @param oldValues (Record) the complete original values from before the save occurred
    // @param editCompletionEvent (editCompletionEvent) Edit completion event that led to the save
    //                                                  attempt
    // @param [dsResponse]   (DSResponse)    DSResponse, for saves through a DataSource
    //
    // @visibility external
    //<
    editFailed : "rowNum,colNum,newValues,oldValues,editCompletionEvent,dsResponse,dsRequest",

    //> @method listGrid.editorEnter()
    // Callback fired when the user starts editing a new cell.
    // <P>
    // This callback is typically used to establish dynamic default values via
    // +link{setEditValue()} or +link{setEditValues()}.
    // <P>
    // Can also be overridden on a per-field basis via
    // +link{listGridField.editorEnter,field.editorEnter}.
    //
    // @param record (ListGridRecord) record for the cell being edited.  <b>Will be null</b>
    //                                for a new, unsaved record.
    // @param value (any) value for the cell being edited
    // @param rowNum (int)    row number for the cell
    // @param colNum (int)    column number of the cell
    // @group editing
    // @see listGridField.editorEnter()
    // @visibility external
    //<
    editorEnter : "record,value,rowNum,colNum",

    //> @method listGrid.rowEditorEnter()
    // Callback fired when the user starts editing a new row.
    //
    // @param    record     (ListGridField)    record for the cell being edited
    // @param    editValues  (object)    edit values for the current row
    // @param    rowNum       (number)    row number for the cell
    //  @group  editing
    //  @see listGrid.editorEnter()
    // @visibility external
    //<
    rowEditorEnter : "record,editValues,rowNum",

    //> @method listGrid.editorExit()
    // Callback fired when the user attempts to navigate away from the current edit cell,
    // or complete the current edit.
    // <P>
    // Return false from this method to cancel the default behavior (Saving / cancelling the
    // current edit / moving to the next edit cell).
    // <P>
    // This callback is typically used to dynamically update values or value maps for related
    // fields (via +link{setEditValue()} and +link{setEditorValueMap()} respectively, or to
    // implement custom navigation (via +link{startEditing(), startEditing(rowNum,colNum)}.
    // <P>
    // Can be overridden at the field level as field.editorExit.
    //
    // @param editCompletionEvent   (EditCompletionEvent) How was the edit completion fired?
    // @param    record     (ListGridRecord)    record for the cell being edited
    // @param    newValue   (any)    new value for the cell being edited
    // @param    rowNum       (number)    row number for the cell
    // @param    colNum       (number)    column number of the cell
    //  @group  editing
    // @return  (boolean)   Returning false from this method will cancel the default behavior
    //                      (for example saving the row) and leave the editor visible and focus
    //                      in this edit cell.
    //  @see listGridField.editorExit()
    // @visibility external
    //<

    editorExit : "editCompletionEvent,record,newValue,rowNum,colNum",

    //> @method listGrid.rowEditorExit()
    // Callback fired when the user attempts to navigate away from the current edit row,
    // or complete the current edit.
    // <P>
    // Return false from this method to cancel the default behavior (Saving / cancelling the
    // current edit / moving to the next edit cell).
    //
    // @param editCompletionEvent   (EditCompletionEvent) How was the edit completion fired?
    // @param    record     (ListGridRecord)    record for the cell being edited
    // @param    newValues  (object)    new values for the record [Note that fields that have
    //                                 not been edited will not be included in this object]
    // @param    rowNum       (number)    row number for the row being left
    //  @group  editing
    // @return  (boolean)   Returning false from this method will cancel the default behavior
    //                      (for example saving the row) and leave the editor visible and focus
    //                      in this edit cell.
    //  @see listGridField.editorExit()
    // @visibility external
    //<
    rowEditorExit : "editCompletionEvent,record,newValues,rowNum",

    // JS doc comments by default implementation of validateCellValue and validateFieldValue
    validateCellValue : "rowNum,colNum,newValue,oldValue",
    validateFieldValue : "newValue,oldValue,record,field,rowNum,colNum",

    //> @method listGrid.formatCellValue()
    // Formatter to apply to values displayed within cells.
    // <P>
    // The value passed to this method is either the field value found in the cell record
    // or, if there are unsaved edits, the current user-entered value for the cell.
    // <b>NOTE:</b> unsaved user edits may contain nulls, bad values or values of the wrong
    // type, so formatters used for editable data should be bulletproof.  For example, if you
    // have a function "myNumberFormatter" that should only be passed actual Numbers, you might
    // define formatCellValue like so:
    // <pre>
    //     isc.isA.Number(parseInt(value)) ?
    //            myNumberFormatter(parseInt(value)) : value
    // </pre>
    // Note that this formatter will not be applied to the value displayed within editors for
    // cells - use <code>formatEditorValue</code> to achieve this.
    // <P>
    // If <code>formatCellValue</code> is defined at the field level for some cell being edited,
    // the field level method will be used to format the edit value and this method will not
    // be called for that cell.<br>
    //
    // @group display_values
    //
    // @param   value    (any)   raw value for the cell being
    // @param   record   (ListGridRecord)
    //   Record object for the cell. Note: If this is a new row that has not been saved, in an
    //   editable grid, it has no associated record object. In this case the edit values will
    //   be passed in as this parameter.
    // @param   rowNum   (number)    row number for the cell
    // @param   colNum   (number)    column number for the cell.
    // @return  (HTMLString) formatted value to display in the cell.
    // @see listGridField.formatCellValue()
    // @see listGrid.formatEditorValue()
    // @visibility external
    //<
    formatCellValue : "value,record,rowNum,colNum",

    //> @method listGrid.formatEditorValue()
    // Formatter to apply to values displayed within editors while a cell is being edited.
    // The value passed to this method is the raw value for the cell.<br>
    // If <code>formatEditorValue</code> is defined at the field level for some cell being edited,
    // the field level method will be used to format the edit value and this method will not
    // be called for that cell.<br>
    // To convert the formatted value displayed within an editor back to a raw value, the
    // <code>parseEditorValue</code> method is used.
    //
    // @group editing
    //
    // @param   value (any)   raw value for the cell being edited
    // @param   record   (ListGridRecord)
    //   Record object for the cell. Note: If this is a new row that has not been saved,
    //   it has no associated record object. In this case the edit values will
    //   be passed in as this parameter.
    // @param   rowNum  (number)    row number for the cell
    // @param   colNum  (number)    column number for the cell.
    // @return  (any)   formatted value to display in the editor
    // @see listGridField.formatEditorValue()
    // @see listGrid.parseEditorValue()
    // @visibility external
    //<

    formatEditorValue : "value,record,rowNum,colNum",

    //> @method listGrid.parseEditorValue
    // Method used to convert the value displayed in an editor for some cell being edited into
    // a raw value for saving.<br>
    // If <code>parseEditorValue</code> is defined at the field level for some cell being edited,
    // the field level method will be used to parse the edit value and this method will not
    // be called for that cell.<br>
    //
    // @group editing
    //
    // @param   value (any)   value displayed in the editor for the cell
    // @param   record (ListGridRecord) record object for the row being edited. May be null if this
    //                          is a new row being added to the end of the list.
    // @param   rowNum  (number)    row number for the cell
    // @param   colNum  (number)    column number for the cell.
    // @return  (any)   value in raw format
    // @see listGridField.parseEditorValue()
    // @see listGrid.formatEditorValue()
    // @visibility external
    //<
    parseEditorValue : "value,record,rowNum,colNum",

    //> @method listGrid.sortChanged
    // Notification method executed when the +link{listGrid.setSort(),sort specifiers} change
    // for this grid.
    // @param sortSpecifiers (Array of SortSpecifier) new sort specifiers - may be empty indicating
    //   the grid was unsorted
    // @visibility external
    //<
    sortChanged:"sortSpecifiers",

    //> @method listGrid.fieldStateChanged
    // Notification method executed when columns are resized or reordered, or fields are
    // shown or hidden. Has no default implementation.
    //
    // @visibility external
    // @group fieldState
    //<
    fieldStateChanged : "",

    //> @method listGrid.groupStateChanged
    // Notification method executed whenever the groupState of this grid changes.
    // Group state is accessible via +link{listGrid.getGroupState()}, and contains group state
    // information.
    //
    // @visibility external
    //<
    groupStateChanged : "",

    //> @method listGrid.viewStateChanged
    // Notification method executed whenever the viewState of this grid changes.
    // View state is accessible via +link{listGrid.getViewState()}, and contains field state
    // information, sort information, selection information, hiliting information and
    // grouping information.
    //
    // @visibility external
    //<
    viewStateChanged : "",

    //> @method listGrid.dataArrived() (A)
    // Notification method fired when new data arrives from the server to be displayed in this
    // ListGrid, (for example in response to the user scrolling a new set of rows into view).
    // Only applies to databound listGrids where the +link{listGrid.data,data} attribute is a
    // +link{ResultSet}. This ResultSet may have been created manually and applied to the grid via
    // a call to +link{listGrid.setData()} or may have been created and automatically assigned if
    // +link{listGrid.fetchData()} was used to populate the grid.  This method is fired directly in
    // response to +link{ResultSet.dataArrived(),dataArrived()} firing on the data object.
    // @param startRow (int) starting index of the newly loaded set of rows
    // @param endRow (int) ending index of the newly loaded set of rows (non inclusive).
    // @visibility external
    //<
    dataArrived:"startRow,endRow",

    //> @method listGrid.headerHover() (A)
    // Handle a hover over a button in the header.
    //
    // @param fieldNum (number) field number for the header that was hovered
    // @see ListGrid.headerHoverHTML()
    // @group events, gridHeader, hovers
    // @visibility external
    //<
    headerHover:"fieldNum",

    // doc'd above
    headerClick:"fieldNum",

    //> @method listGrid.onHeaderClick()
    // Handler fired when the user clicks a header in this listGrid before any other processing
    // occurs.
    // Return false to suppress the default header click handling
    // @param fieldNum (int) index of the field on which the click occurred.
    // @return (boolean) return false to cancel the default header click behavior
    // @visibility sgwt
    //<

    onHeaderClick:"fieldNum",

    //> @method listGrid.onRecordDrop()
    // Handler fired when the user drops a record onto this listGrid before any other processing
    // of the drop occurs.
    // Return false to suppress the default record drop handling.
    // @param dropRecords (Array[] of ListGridRecord) records being dropped
    // @param targetRecord (ListGridRecord) record being dropped on.  May be null
    // @param index (int) index of record being dropped on
    // @param dropPosition (RecordDropPosition) position with respect to the target record
    // @param sourceWidget (Canvas) widget where dragging began
    // @return (boolean) return false to cancel the default record drop handling
    // @visibility sgwt
    //<

    onRecordDrop:"dropRecords,targetRecord,index,dropPosition,sourceWidget",

    //> @method listGrid.onExpandRecord()
    // Handler fired when a record is expanded in a grid with <code>canExpandRecords</code> set to
    // true. Allows the expansion to be cancelled.
    // @param record (ListGridRecord) record being expanded
    // @return (boolean) return false to cancel record expansion
    // @visibility sgwt
    //<

    onExpandRecord:"record",

    //> @method listGrid.onCollapseRecord()
    // Handler fired when a record is collapsed in a grid with <code>canExpandRecords</code> set to
    // true. Allows the collapse to be cancelled.
    // @param record (ListGridRecord) record being collapsed
    // @return (boolean) return false to cancel record collapse
    // @visibility sgwt
    //<

    onCollapseRecord:"record",

    //>    @method    listGrid.drawAreaChanged()    (A)
    //  Notification method that fires when the drawArea changes due to scrolling.  Receives
    // the previous drawArea co-ordinates as parameters.  Call +link{listGrid.getDrawArea()} to
    // get the new drawArea co-ordinates.
    // <P>
    // Note that if this grid is showing any +link{listGridField.frozen,frozen fields}, they
    // will not be included in the <code>oldStartCol</code>, <code>oldEndCol</code> range
    // reported by this method. Frozen fields are assumed never to be scrolled out of view.
    //
    // @param oldStartRow (number) the startRow from before the drawArea changed
    // @param oldEndRow (number) the endRow from before the drawArea changed
    // @param oldStartCol (number) the startCol from before the drawArea changed
    // @param oldEndCol (number) the endCol from before the drawArea changed
    // @visibility external
    //<
    drawAreaChanged:"oldStartRow,oldEndRow,oldStartCol,oldEndCol",

    //> @method listGrid.showRecordComponent()
    // When +link{listGrid.showRecordComponents} is true, return false from this method
    // to prevent the recordComponent system from processing the passed record or cell.
    // <P>
    // <b>If +link{listGrid.showRecordComponentsByCell, showRecordComponentsByCell} is true,
    // it is important to implement this method</b> - especially if the grid has many fields -
    // otherwise, the internal automatic detection will process every visible cell, checking,
    // for example, whether a component already exists for that cell, before eventually running
    // +link{ListGrid.createRecordComponent} to check if one is required.  Implementing this
    // method avoids all that processing for cells that aren't expected to have components.
    // <P>
    // The second parameter is only applicable if +link{showRecordComponentsByCell} is true.
    //
    // @param record (ListGridRecord) record being processed
    // @param [colNum] (integer) column index of the cell in which the record component
    //   may be shown. Will be null unless showRecordComponentsByCell is true.
    // @return (boolean) return false to cancel showRecordComponent behavior
    // @visibility external
    //<
    showRecordComponent:"record,colNum",


    //> @method listGrid.createRecordComponent()
    // When +link{listGrid.showRecordComponents} is true, this method is called to create
    // per-row or per-cell embedded components to display in the grid.
    // <p>
    // This method should create and return a new component for the record passed in every
    // time it is called.
    // <P>
    // Note that the default +link{type:RecordComponentPoolingMode} is "viewport", which means
    // that components are destroyed as soon as their records leave the viewport.
    // <P>
    // For the most efficient implementation, switch the
    // +link{listGrid.recordComponentPoolingMode} to "recycle".  Note that, in this mode, in
    // addition to implementing this method, developers should also implement
    // +link{listGrid.updateRecordComponent()} which allows already created components to be
    // altered for re-use in new records. See the +link{listGrid.showRecordComponents} overview
    // for more information.
    // <P>
    // The colNum parameter is applicable only when
    // +link{listGrid.showRecordComponentsByCell} is true.
    //
    // @param record (ListGridRecord) record to create a component for
    // @param colNum (Integer) cell to which the component applies
    // @return (Canvas) return the component to embed in the passed record
    // @visibility external
    //<
    createRecordComponent:"record,colNum",

    //> @method listGrid.updateRecordComponent()
    // When +link{listGrid.showRecordComponents} is true, this method is called to update
    // components created by +link{listGrid.createRecordComponent()} when they are to be
    // applied to a different record in the grid. See the
    // +link{listGrid.showRecordComponents, record components overview}
    // for more information on recordComponents.
    // <P>
    // The colNum parameter is applicable only when
    // +link{listGrid.showRecordComponentsByCell} is true. Note that if
    // +link{listGrid.poolComponentsPerColumn} is set to false, the component may have been generated
    // by a +link{listGrid.createRecordComponent()} call applied to a different field.
    // <P>
    // Return null to avoid re-adding the component to the row or cell.
    //
    // @param record (ListGridRecord) record to which the passed component applies
    // @param colNum (Integer) cell to which the passed component applies
    // @param component (Canvas) the component to update
    // @param recordChanged (boolean) was the passed component previously embedded in a
    //     different record?
    // @return (Canvas) return the component to embed in the passed record
    // @visibility external
    //<
    updateRecordComponent:"record,colNum,component,recordChanged",


    //> @method updateFilterEditorValues () [A]
    // If specified, and +link{ListGrid.showFilterEditor} is true, this stringMethod
    // will be called whenever fetchData() or filterData (or clearCriteria())
    // is called on this grid and should return a set of values to display in the
    // filterEditor.
    // @param currentCriteria (Criteria) Criteria being applied to the grid. May be null if
    //   the filter is being cleared
    // @return (Object) Values to display in the filter editor.
    // @visibility internal
    //<

    updateFilterEditorValues:"criteria",

    //> @method listGrid.filterEditorSubmit()
    // Optional notification fired when the user performs a filter using the
    // +link{showFilterEditor,Filter Editor}.  May fire as criteria values are being edited if
    // +link{filterOnKeypress} is true, otherwise will fire when the user clicks the filter
    // button or presses the Enter key while focus is in the Filter Editor.
    // <p>
    // <smartgwt>Use event.cancel()</smartgwt>
    // <smartclient>Return false</smartclient>
    // to cancel the default behavior - you <b>must</b> cancel the default behavior if your
    // code is going to call +link{filterData()}, +link{setCriteria()} or any other API that
    // affects the criteria applied to the grid.
    // <P>
    // The <code>criteria</code> parameter contains the current criteria applied to the
    // grid including edits the user has just made using the Filter Editor.  This matches
    // what is returned if you call +link{getFilterEditorCriteria()}.
    // <P>
    // If you wish to access the <code>criteria</code> applied to the grid without picking
    // up any edits to the Filter Editor, use +link{getCriteria()} instead.
    // <P>
    // Developers may wish to perform a filter using the Filter Editor values from code
    // running outside the standard filterEditorSubmit flow.  For example, if you wanted a
    // confirmation dialog to be shown before filtering was performed, you would cancel the
    // default behavior as described above, but then need to replicate the default behavior
    // once the user confirms that they want to proceed.  To replicate the default behavior,
    // just call:
    // <pre>
    //  grid.filterData(grid.getFilterEditorCriteria());
    // </pre>
    // or, to ensure the specified +link{listGrid.autoFetchTextMatchStyle} is picked up
    // <smartclient>
    // <pre>
    //  grid.filterData(grid.getFilterEditorCriteria(),
    //          null, {textMatchStyle:grid.autoFetchTextMatchStyle});
    // </pre>
    // </smartclient>
    // <smartgwt>
    // <pre>
    //  DSRequest request = new DSRequest();
    //  request.setTextMatchStyle(grid.getAutoFetchTextMatchStyle());
    //  grid.filterData(grid.getFilterEditorCriteria(), null,
    //       request);
    // </pre>
    // </smartgwt>
    //
    // @param criteria (Criteria) criteria derived from the filter editor values
    // @return (boolean) returning false will suppress the filter from occurring
    // @visibility external
    //<
    filterEditorSubmit:"criteria",

    //> @method listGrid.handleGroupBy()
    // Callback fired when the user attempts to group or ungroup the listGrid, or when
    // +link{listGrid.groupBy()} is called programmatically.
    // <smartclient>Return false to cancel grouping.</smartclient>
    // <smartgwt>This event may be cancelled.</smartgwt>
    // <P>
    // This notification is fired before the +link{listGrid.groupTree,data} is updated to
    // reflect the grouping. See also +link{listGrid.groupByComplete()}.
    //
    // @param fields (Array of String) ListGrid field names by which the grid is being grouped.
    //   If the user is ungrouping the grid, this param will be an empty array.
    // @return (boolean) return false to cancel groupBy change.
    // @visibility external
    //<
    handleGroupBy:"fields",

    //> @method listGrid.groupByComplete()
    // Callback fired when the listGrid is grouped or ungrouped.
    // <P>
    // Unlike +link{listGrid.handleGroupBy()}, this notification will fire when grouping
    // is complete, and the +link{listGrid.groupTree} has been updated to reflect the
    // grouped data.
    //
    // @param fields (Array of String) ListGrid field names by which the grid is now
    //  grouped. If the user is ungrouping the fields, this parameter will be an
    //  empty array.
    // @visibility external
    //<

    groupByComplete:"fields",

    //> @method listGrid.hilitesChanged()
    // Notification method executed whenever the end user uses the HiliteEditor to change
    // the set of hilites applied to this grid.  This method will not be called after
    // a purely programmatic change to the hilites made with a call to
    // +link{databoundComponent.setHilites(),setHilites()}.  The array of currently applied hilite objects is
    // accessible via +link{databoundComponent.getHilites(),getHilites()}.
    // @group hiliting
    // @visibility external
    //<
    hilitesChanged : ""
});


isc.ListGrid._unskinnedHeaderDefaults =
    isc.addProperties({}, isc.ListGrid.getInstanceProperty("headerDefaults"));
isc.ListGrid._unskinnedHeaderButtonDefaults =
    isc.addProperties({}, isc.ListGrid.getInstanceProperty("headerButtonDefaults"));

isc.ListGrid.classInit();


// AutoTest subsystem supports some configuration of ListGrids
//> @attr ListGrid.locateRowsBy (string : null : IRW)
// When +link{isc.AutoTest.getElement()} is used to parse locator strings generated by
// +link{isc.AutoTest.getLocator()} for a cell in this grid, how should the row be identified?
// <br>
// Note that getLocator() will actually store all available information about the row in
// the generated string -- this attribute effects how a stored string will be parsed only.
// <P>
// Valid options area:
// <ul>
// <li><code>"primaryKey"</code> Only applies to databound grids: If the cell in question has
//  a primary key cell value, use it to identify cells in autoTest locator strings.</li>
// <li><code>"titleField"</code> If the cell in question has a value for the
//  +link{listGrid.getTitleField(),titleField}, use it to identify cells in autoTest
//  locator strings</li>
// <li><code>"targetCellValue"</code> Identify rows by storing the cell value for the target
//  row / field in autoTest locator strings</li>
// <li><code>"index"</code>The rowNum will be used to identify the row.</li>
// </ul>
// If unset, default behavior is to identify by primary key (if available), otherwise by
// titleField (if available), otherwise by cell value (if available), and lastly by index.
// @visibility external
// @group autoTest
//<

//> @attr ListGrid.locateColumnsBy (string : null : IRW)
// When +link{isc.AutoTest.getElement()} is used to parse locator strings generated by
// +link{isc.AutoTest.getLocator()} for a cell in this grid, how should the column be identified?
// <br>
// Note that getLocator() will actually store all available information about the column in
// the generated string -- this attribute effects how a stored string will be parsed only.
// <P>
// Valid options area:
// <ul>
// <li><code>"fieldName"</code> Attempt to identify by fieldName.</li>
// <li><code>"index"</code> Attempt to identify by colNum (index in the fields array).</li>
// </ul>
// If unset, default behavior is to identify by fieldName (if available), otherwise by index.
//
// @visibility external
// @group autoTest
//<


//

// LineEditor
// ---------------------------------------------------------------------------------------
// A minimum-height editor that expands as you add more rows
isc.defineClass("LineEditor", isc.ListGrid).addProperties({
    canEdit:true,
    editEvent: "click",
    editOnFocus: true,
    modalEditing:true,
    // allow new record entry
    enterKeyEditAction: "nextRowStart",
    listEndEditAction: "next",
    // be one row tall initially
    height:50,
    emptyMessage:"Click to add data",
    emptyMessageStyle: "normal", // just gets rid of padding
    // expand as data is added
    autoFitData:"vertical",
    leaveScrollbarGap:false,

    // start editing on a click with an initially empty grid
    click : function () {
        var record = this.getRecord(this.getEventRow());
        //this.logWarn("click call with record: " + this.echo(record));
        this.Super("click", arguments);
        if (record == null && isc.EH.lastEvent.target == this.body) this.startEditingNew();
    }
})


// UserFormula
// ---------------------------------------------------------------------------------------
// Definition of UserFormula object and related events.
// formulaUpdated() is triggered in DataBoundComponent.js
//

//> @object UserFormula
// An object representing a user-created and user-modifiable formula, which can be created and
// edited with a +link{FormulaBuilder} either directly or via the
// +link{ListGrid.canAddFormulaFields} behavior.
//
// @treeLocation Client Reference/Grids/ListGrid
// @visibility external
//<

//> @attr userFormula.text (String : null : IRW)
// Formula to be evaluated.  All variables used by the formula must be single-letter capital
// characters (eg A). These are derived from field values for the record in question
// - see +link{formulaVars}.
// <P>
// In addition to these variables, the keyword <code>record</code> may be used to
// refer directly to the record for which the formula is being displayed.
//
// @visibility external
//<

//> @attr userFormula.formulaVars (Map : null : IR)
// Map from the single-letter capital letters used as formula variables in the +link{UserFormula}
// String to the fieldNames these variables should represent in the context where the formula is
// evaluated.
//
// @visibility external
//<

//> @attr listGridField.userFormula (UserFormula : null : IRW)
// Formula definition for this field.
// <P>
// Advanced applications that wish to save formulas separately from a grid's
// +link{listGrid.getViewState(),viewState} can provide a +link{UserFormula} as part of the field
// definition, and may subsequently access the formula is updated via the
// +link{listGrid.formulaUpdated()} notification.
//
// @visibility external
//<

//> @method listGrid.formulaUpdated()
// Notification fired when a user either creates a new formula field or edits an existing formula field.
// @param field (ListGridField) the formula field
// @param formula (UserFormula) the new or updated formula definition
//
// @visibility external
//<

// UserSummary
// ---------------------------------------------------------------------------------------
// Definition of UserSummary object and related events.
// summaryUpdated() is triggered in DataBoundComponent.js
//

//> @object UserSummary
// An object representing a user-created and user-modifiable summary, which can be created and
// edited with a +link{SummaryBuilder} either directly or via the
// +link{ListGrid.canAddSummaryFields} behavior.
//
// @treeLocation Client Reference/Grids/ListGrid
// @visibility external
//<

//> @attr userSummary.text (String : null : IRW)
// Summary to be evaluated.  All variables used by the summary must be all-capital letter
// names surrounded by braces and escaped with a # sign (eg #{A}).  These are derived
// from field values for the record in question - see +link{summaryVars}.
//
// @visibility external
//<

//> @attr userSummary.summaryVars (Map : null : IR)
// Map from the all-capital letter names used as summary variables in the +link{UserSummary}
// String to the fieldNames these variables should represent in the context where the
// summary is evaluated.
//
// @visibility external
//<

//> @attr listGridField.userSummary (UserSummary : null : IRW)
// Summary definition for this field.
// <P>
// Advanced applications that wish to save summarys separately from a grid's
// +link{listGrid.getViewState(),viewState} can provide a +link{UserSummary} as part of the
// field definition, and may subsequently access the summary is updated via the
// +link{listGrid.summaryUpdated()} notification.
//
// @visibility external
//<

//> @method listGrid.summaryUpdated()
// Notification fired when a user either creates a new summary field or edits an existing
// summary field.
// @param field (ListGridField) the summary field
// @param summary (UserSummary) the new or updated summary definition
//
// @visibility external
//<




//>    @class    TreeGrid
//
// The SmartClient system supports hierarchical data (also referred to as tree data
// due to its "branching" organization) with:
// <ul>
//   <li> the +link{class:Tree} class, which manipulates hierarchical data sets
//   <li> the TreeGrid widget class, which extends the ListGrid class to visually
//        present tree data in an expandable/collapsible format.
// </ul>
// For information on DataBinding Trees, see +link{group:treeDataBinding}.
// <p>
// A TreeGrid works just like a +link{ListGrid}, except one column (specified by
// +link{TreeGridField.treeField}) shows a hierarchical +link{Tree}.  A TreeGrid is not limited
// to displaying just the +link{Tree} column - you can define additional columns (via
// +link{TreeGrid.fields}) which will render just like the columns of a +link{ListGrid}, and
// support all of the functionality of ListGrid columns, such as
// +link{listGridField.formatCellValue,formatters}.
// <p>
// Except where explicitly overridden, +link{ListGrid} methods, callbacks, and properties
// apply to TreeGrids as well.  The +link{ListGrid} defines some methods as taking/returning
// +link{ListGridField} and +link{ListGridRecord}.  When using those methods in a TreeGrid,
// those types will be +link{TreeGridField} and +link{TreeNode}, respectively.
//
// @implements DataBoundComponent
// @treeLocation Client Reference/Grids
// @visibility external
//<

// define us as a subclass of the ListGrid
isc.ClassFactory.defineClass("TreeGrid", "ListGrid");

// Synonym for backCompat.  NOTE: define an alias rather than make a subclass, otherwise,
// attempts to skin the class using the old name would only affect the subclass!
isc.addGlobal("TreeViewer", isc.TreeGrid);

//>    @class    TreeGridBody
//
//  GridRenderer displayed as the body of a TreeGrid. Includes various overrides for
//  specialized event handling and display.
//
//  @treeLocation Client Reference/Grids/TreeGrid/
//  @visibility internal
//<
isc.defineClass("TreeGridBody", isc.GridBody).addProperties({
    // Override the internal _updateCellStyle() method to style the tree-field's internal
    // table (without regening the HTML)
    _$TABLE:"TABLE",
    _$zeroBorderPadding:"padding:0px;border:0px;",


    _getShowClippedValuesOnHover : function () {
        if (this.showClippedValuesOnHover == null) return true;
        return this.showClippedValuesOnHover;
    },

    cellValueIsClipped : function (rowNum, colNum, c, d, e) {
        var grid = this.grid,
            treeFieldNum = grid._treeFieldNum,
            treeFieldBody = grid.getFieldBody(treeFieldNum);
        if (this === treeFieldBody && colNum == grid.getLocalFieldNum(treeFieldNum)) {
            var cell = this.getTableElement(rowNum, colNum);
            if (cell == null) return false;
            var table = cell.firstChild;
            while (table && table.tagName != this._$TABLE) table = table.firstChild;
            if (table && table.rows && table.rows[0]) {
                var titleCell = table.rows[0].lastChild,
                    titleClipperHandle = titleCell;

                if (isc.Browser.isMoz && titleCell.firstChild && titleCell.firstChild.hasAttribute &&
                    titleCell.firstChild.hasAttribute("_titleClipper"))
                {
                    titleClipperHandle = titleCell.firstChild;
                }
                return this._cellValueIsClipped(titleClipperHandle);
            }
        }
        return this.invokeSuper(isc.TreeGridBody, "cellValueIsClipped", rowNum, colNum, c, d, e);
    },

    defaultCellValueHoverHTML : function (record, rowNum, colNum, d, e, f) {
        var grid = this.grid,
            treeFieldNum = grid._treeFieldNum,
            treeFieldBody = grid.getFieldBody(treeFieldNum);
        if (this === treeFieldBody && colNum == grid.getLocalFieldNum(treeFieldNum)) {
            // Call the standard ListGrid.getCellValue() method to give us the formatted title
            // of the cell being dragged, excluding the TreeGrid folder/file icons, etc.
            return this.grid.invokeSuper(isc.TreeGrid, "getCellValue", record, rowNum, treeFieldNum);
        }
        return this.invokeSuper(isc.TreeGridBody, "defaultCellValueHoverHTML", record, rowNum, colNum, d, e, f);
    },

    _updateCellStyle : function (record, rowNum, colNum, cell, className) {
        if (cell == null) cell = this.getTableElement(rowNum, colNum);
        if (cell == null) return; // cell not currently drawn

        if (!this.showHiliteInCells &&
            colNum == this.grid.getLocalFieldNum(this.grid.getTreeFieldNum()))
        {
            if (record == null) record = this.getCellRecord(rowNum, colNum);
            // determine the CSS style className if not provided
            if (className == null) className = this.getCellStyle(record, rowNum, colNum);
            // There will be a clip-div around the table.
            // In IE the NOBR also counts as a node.
            var table = cell.firstChild;
            while (table && table.tagName != this._$TABLE) table = table.firstChild;
            if (table) {

                var customCSSText;

                // Apply custom css text from getCellCSSText to the table element and any cells.
                // Note: this is not required in most cases - we write out no-style-doubling css
                // on these elements so we'll ignore bg color, images, border etc.
                // This is really just required to pick up changes to the text color / weight etc
                if (this.getCellCSSText) {
                    customCSSText = this.getCellCSSText(record, rowNum, colNum);
                    if (customCSSText != null && !isc.isAn.emptyString(customCSSText)) {
                        // we always append no-style-doubling css to the custom css to avoid
                        // doubled borders etc

                        customCSSText += isc.Canvas._$noStyleDoublingCSS;
                    } else customCSSText = null;
                }

                table.className = className;
                if (customCSSText != null) table.cssText = customCSSText;

                var innerRows = table.rows,

                    innerCells = innerRows[0].childNodes;
                if (innerCells && innerCells.length > 0) {
                    for (var i = 0; i < innerCells.length; i++) {
                        innerCells[i].className = className;
                        if (customCSSText) {
                            // Title is the last cell in the tree-title table - this cell
                            // requires additional specification for the icon padding
                            if (i == innerCells.length-1) {
                                customCSSText += (this.isRTL() ? "paddingRight:" : "paddingLeft:")
                                                + this.iconPadding;
                            }
                            innerCells[i].cssText = customCSSText;
                        }
                    }
                }
            }
        }

        // Actually style the cell itself
        return isc.GridRenderer.getPrototype()._updateCellStyle.apply(
                                        this, [record, rowNum, colNum, cell, className]);
    },

    //>    @method    treeGridBody.click()    (A)
    // Handle a click in the "open" area of a record specially
    //        @group    event handling
    //
    //        @return    (boolean)    false == cancel further event processing
    //<
    // Note: We override click rather than putting this logic into rowClick / cellClick, as
    // we don't want folder toggling to occur if the user's mouse is hovering over the open
    // area while the user triggers standard record click handling by hitting the Space-bar.

    click : function (event, eventInfo) {
        if (!this._suppressEventHandling()) {

            var tg = this.grid,
                recordNum = tg.getEventRecordNum(),
                node = recordNum < 0 ? null : tg.getRecord(recordNum);
            // if what they clicked on is a folder, toggle it's state.  The 'open' observation
            // will automaticaly redraw for us
            if (node != null && tg.data.isFolder(node) && tg.clickInOpenArea(node)) {

                if (isc.screenReader) {
                    this._putNativeFocusInRow(recordNum);
                }
                tg.toggleFolder(node);

                // clear out the pointer to the last record clicked, and the last row selected
                // by keyboard navigation. (Prevents index-based keyboard navigation from
                // jumping around due to the number of rows changing as folders toggle)
                tg.clearLastHilite();
                tg._lastRecordClicked = null;

                // Note: if you click in the open area to toggle a folder, no nodeClick or
                // folderClick events will be fired, since we've already taken action in
                // response to your click by toggling a folder
                // Return EH.STOP_BUBBLING to stop propogation.
                return isc.EH.STOP_BUBBLING;
            }
        }
        return this.Super("click", arguments);
    },

    // Override mouseDown and mouseUp in the body to avoid selecting when clicking in open
    // area by default.

    //>    @method    treeGridBody.mouseDown()    (A)
    // Handle a click in the open area on mouseDown specially
    //        @group    event handling
    //
    //        @return    (boolean)    false == cancel further event processing
    //<
    mouseDown : function () {
        // get the item that was clicked on -- bail if not found
        var rowNum = this.getEventRow(),
            node = rowNum < 0 ? null : this.grid.data.get(rowNum);

        if (node != null) {
            if (this.grid.clickInOpenArea(node)) {
                // if they clicked in the open area of the record,
                // just bail because we're supposed to open the folder instead
                // TreeGrid.click() will actually open the folder

                return isc.EH.STOP_BUBBLING;
            } else if (this.grid.clickInCheckboxArea(node) && this.canSelectRecord(node)) {

                // Toggle node selection state
                var selectionType = this.grid.selectionType;
                if (selectionType == isc.Selection.SINGLE) {
                    this.deselectAllRecords();
                    this.selectRecord(node);
                } else if (selectionType == isc.Selection.SIMPLE
                    || selectionType == isc.Selection.MULTIPLE) {
                    if (this.selection.isSelected(node)) this.deselectRecord(node);
                    else this.selectRecord(node);
                }

                // Note: if you click in the checkbox area to select a node, no nodeClick or
                // folderClick events will be fired, since we've already taken action in
                // response to your click by selecting/deselected the node.
                // Return EH.STOP_BUBBLING to stop propogation.
                return isc.EH.STOP_BUBBLING;
            }
        }

        return this.Super("mouseDown", arguments);
    },

    //>    @method    treeGridBody.mouseUp()    (A)
    // Handle a click in the open area on mouseUp specially
    //        @group    event handling
    //
    //        @return    (boolean)    false == cancel further event processing
    //<
    mouseUp : function () {
        // get the item that was clicked on -- bail if not found
        var rowNum = this.getEventRow(),
            node = rowNum < 0 ? null : this.grid.data.get(rowNum);
        if (node != null &&
            (this.grid.clickInOpenArea(node) || this.grid.clickInCheckboxArea(node)))
        {
            // don't select the row; on click() we'll open the folder
            return isc.EH.STOP_BUBBLING;
        } else {
            // proceed to super (select the row)
            return this.Super("mouseUp", arguments);
        }
    },

    // Override to place embedded components for the tree field indented as a title
    // would be if TG.indentRecordComponents == true.
    placeEmbeddedComponent : function (component) {
        if (this.grid.indentRecordComponents) {
            var colNum = component._currentColNum;
            if (colNum == this.grid.getTreeFieldNum() && !component.snapOffsetLeft) {
                var record = component.embeddedRecord;
                if (record != null) {
                    component.snapOffsetLeft
                        = this.grid.getOpenAreaWidth(record) + this.grid.iconPadding;
                }
            }
        }
        return this.Super("placeEmbeddedComponent", arguments);
    },

    getTableHTML : function (colNum, startRow, endRow, discreteCols, asyncCallback, isAsync) {
        var data = this.grid.data,
            preCacheRange = (
                isc.isA.ResultTree(data) &&

                !this.isEmpty());

        var actualStartRow = startRow,
            actualEndRow = endRow;

        if (preCacheRange) {

            var drawRect = this._getTableHTMLDrawArea(startRow, endRow, false);
            actualStartRow = drawRect[0];
            actualEndRow = drawRect[1];


            data._pushCachedRange(actualStartRow, actualEndRow);
        }

        var ret = this.invokeSuper(
                isc.TreeGridBody,
                "getTableHTML",
                colNum, startRow, endRow, discreteCols, asyncCallback, isAsync);

        if (preCacheRange) {
            data._popCachedRange(actualStartRow, actualEndRow);
        }
        return ret;
    }
});

isc.TreeGrid.addClassProperties({

    //=    @const    TreeGrid.TREE_FIELD        default field to display a tree
    TREE_FIELD : {name:"nodeTitle", treeField:true,

                // This flag will avoid standard type formatters running based on field
                // type as part of _formatCellValue.
                // type formatting already occurs in the default getNodeTitle()
                // implementation.
                // This is the correct place for this formatting, since if that method is
                // overridden we would expect it to return a string rather than an object
                // of whatever field type the treeField is.
                _suppressTypeFormatting:true,

                getCellValue : function (list,record,recordNum,colNum) {
                    if (!list.getNodeTitle) {
                        var fieldName = colNum == null ? null : list.getFieldName(colNum);
                        return record == null || fieldName == null ? null : record[fieldName];
                    }
                    return list.getNodeTitle(record,recordNum, this)
                },
                canFilter: false,
                // return the title for the Header Button over the tree column.

                getFieldTitle : function (viewer, fieldNum) {
                    var field = viewer.getField(fieldNum);
                    if (field.name == "nodeTitle") return viewer.treeFieldTitle;

                    // otherwise just return the title of the field, or failing that, the field's name
                    return field.title || field.name;
                }
    },

    // _getTreeCellTemplate - returns the HTML template array used for the start of
    // tree grid cells.
    // This is a dynamic method - it incorporates the standard 'noDoublingCSS' string into the
    // returned HTML template. That string can change at runtime due to setNeverUseFilters()
    // so we need to react to this and regenerate the template.
    _getTreeCellTemplate : function () {
        if (!this._observingDoublingStrings) {
            isc.Canvas._doublingStringObservers.add({
                target:this,
                methodName:"_doublingStringsChanged"
            });
            this._observingDoublingStrings = true;
        }
        if (this._$treeCellTemplate == null) {
            this._$treeCellTemplate = [

                "<table" + (isc.Browser.isIE && isc.screenReader ? " unselectable='on'" : "") +
                " role='presentation' cellpadding=0 cellspacing=0 class='", // [0]
                ,                                                   // [1] - this.getCellStyle()
                "' style='",          // [2]
                ,                                                   // [3] - getCellCSSText()
                // use _$noStyleDoublingCSS to suppress any border / background image etc from the
                // cell style
                // Also use noStyleDoublingCSS and explicitly re-apply the gridrenderer cell style and
                // cell cssText to each cell within the tree-cell table.

                isc.Canvas._$noStyleDoublingCSS + "'><colgroup><col width='",// [4]
                ,                                                   // [5] - indent cell width
                "px'/><col width='",                                // [6]
                ,                                                   // [7] - icon cell width

                "px'/><col/></colgroup><tbody><tr><td" +
                (isc.Browser.isIE && isc.screenReader ? " unselectable='on'" : "") + " style='line-height:0px;", // [8] (indent cell)
                ,                                                   // [9] - getCellCSSText()
                isc.Canvas._$noStyleDoublingCSS + "' class='",      // [10]
                ,                                                   // [11] - getCellStyle()
                "'>",                                               // [12]
                ,                                                   // [13] - indentHTML
                "</td>"                                             // [14]
                // (we'll write the title cell out using _$treeCellTitleTemplate)
            ];
        }
        return this._$treeCellTemplate;
    },

    _getTreeCellTitleTemplate : function () {
        if (!this._observingDoublingStrings) {
             isc.Canvas._doublingStringObservers.add({
                target:this,
                methodName:"_doublingStringsChanged"
            });
            this._observingDoublingStrings = true;
        }

        if (this._$treeCellTitleTemplate == null) {
            this._$treeCellTitleTemplate = [


                "<td" + (isc.Browser.isIE && isc.screenReader ? " unselectable='on'" : "") + " style='line-height:0px;", // [0] (icon(s) cell)
                ,                                                      // [1] - getCellCSSText()
                ";" + isc.Canvas._$noStyleDoublingCSS + "' class='",   // [2]
                ,                                                      // [3] - getCellStyle()

                "'>" + (isc.Browser.isSafari || isc.Browser.isIE ? "<nobr>" : ""), // [4]
                ,                                                   // [5] - opener icon HTML
                ,                                                   // [6] - 'extra' icon if there is one
                ,                                                   // [7] - icon for item (eg folder/file icon)
                (isc.Browser.isSafari || isc.Browser.isIE ? "</nobr>" : "") +
                    "</td><td",                                     // [8] (start of value cell)
                ,                                                   // [9] ID attribute, or null
                (isc.Browser.isIE && isc.screenReader ? " unselectable='on'" : "") + " style='", // [10]
                ,                                                   // [11] - getCellCSSText()
                ";" + isc.Canvas._$noStyleDoublingCSS
                    + (isc.Page.isRTL()
                       ? "padding-left:1px;padding-right:"
                       : "padding-right:1px;padding-left:"),        // [12]
                ,                                                   // [13] - this.iconPadding
                "px;' class='",                                     // [14]
                ,                                                   // [15] - getCellStyle()
                "'>",                                               // [16]
                ,                                                   // [17] - <NOBR> or null
                ,                                                   // [18] - value
                ,                                                   // [19] - </NOBR> or null
                "</td>"                                             // [20]
            ];
        }
        return this._$treeCellTitleTemplate;
    },

    _doublingStringsChanged : function () {
        this._$treeCellTemplate = null;
        this._$treeCellTitleTemplate = null
    }


});


isc.TreeGrid.addProperties({

    //>    @attr    treeGrid.dataSource        (DataSource or ID : null : IRW)
    // @include dataBoundComponent.dataSource
    //<

    //>    @attr    treeGrid.data        (Tree : null : IRW)
    // A +link{class:Tree} object containing of nested +link{object:TreeNode}s to
    // display as rows in this TreeGrid.
    // The <code>data</code> property will typically not be explicitly specified for
    // databound TreeGrids, where the data is returned from the server via databound component
    // methods such as <code>fetchData()</code>
    //      @visibility external
    //        @group    data
    //<

    //> @attr treeGrid.initialData (List of TreeNode : null : IRA)
    // You can specify the initial set of data for a databound TreeGrid using this property.
    // The value of this attribute should be a list of <code>parentId</code>-linked
    // +link{TreeNode}s in a format equivalent to that documented on +link{Tree.data} or, for
    // TreeGrids with +link{treeGrid.dataFetchMode,dataFetchMode} set to
    // +link{type:FetchMode,"paged"}, on +link{ResultTree.data}.
    // <P>
    // If you create a standalone +link{class:Tree} or +link{class:ResultTree} as the
    // TreeGrid's +link{treeGrid.data,data} then you may equivalently specify this initial set
    // of tree nodes in that tree's +link{Tree.data,data} property.
    // @see TreeNode
    // @see Tree.data
    // @see ResultTree.data
    // @visibility external
    // @example initialData
    //<

    //> @attr treeGrid.loadDataOnDemand   (boolean : null : IRW)
    // For databound treeGrid instances, should the entire tree of data be loaded on initial
    // fetch, or should folders load their children as they are opened.
    // <P>
    // If unset, calling +link{fetchData()} will default it to true, otherwise, if a ResultTree
    // is passed to +link{setData()}, the +link{resultTree.loadDataOnDemand} setting is
    // respected.
    // <P>
    // Note that when using <code>loadDataOnDemand</code>, every node returned by the server is
    // assumed be a folder which may load further children.  See
    // +link{resultTree.defaultIsFolder} for how to control this behavior.
    //
    // @see dataFetchMode
    // @group databinding
    // @visibility external
    // @example initialData
    //<


    //> @attr treeGrid.keepParentsOnFilter (boolean : null : IR)
    // @include resultTree.keepParentsOnFilter
    // @visibility external
    //<

    //> @attr treeGrid.dataFetchMode (FetchMode : "basic" : IR)
    // @include resultTree.fetchMode
    // @visibility external
    //<

    //> @attr treeGrid.serverFilterFields (Array of String : null : IR)
    // @include resultTree.serverFilterFields
    // @visibility external
    //<

    //> @attr treeGrid.autoFetchTextMatchStyle (TextMatchStyle : "exact" : IR)
    // With +link{loadDataOnDemand}:true, TreeGrids fetch data by selecting the child nodes of
    // each parent, which should be exact match, so we default to
    // <code>autoFetchTextMatchStyle:"exact"</code>.
    // See +link{listGrid.autoFetchTextMatchStyle} for details.
    //
    // @group dataBinding
    // @visibility external
    //<
    autoFetchTextMatchStyle:"exact",

    //> @attr treeGrid.cascadeSelection (Boolean : false : [IR])
    // Should children be selected when parent is selected? And should parent be
    // selected when any child is selected?
    // @visibility external
    //<
    cascadeSelection:false,

    //> @attr treeGrid.showPartialSelection (Boolean : false : [IRW])
    // Should partially selected parents be shown with special icon?
    // @visibility external
    //<
    showPartialSelection:false,

    //> @attr treeGrid.selectionProperty (string : null : [IRA])
    // @include listGrid.selectionProperty
    // @visibility external
    //<

    //> @attr treeGrid.canSelectAll (boolean : null : [IRW])
    // This property is not supported on TreeGrid, and only applies to the +link{ListGrid}
    // superclass.
    //
    // @group selection
    // @visibility external
    //<


    booleanTrueImage:null,
    booleanFalseImage:null,
    booleanPartialImage:null,

    showClippedValuesOnHover:null,

    //> @attr treeGrid.treeRootValue (any : null : IRA)
    // For databound trees, use this attribute to supply a +link{DataSourceField.rootValue} for this
    // component's generated data object.
    // <P>
    // This property allows you to have a particular component navigate a tree starting from any
    // given node as the root.
    // @group databinding
    // @visibility external
    //<


    //> @attr   treeGrid.fields       (Array of TreeGridField: null : IRW)
    // An array of field objects, specifying the order, layout, dynamic calculation, and
    // sorting behavior of each field in the treeGrid object. In TreeGrids, the fields
    // array specifies columns. Each field in the fields array is TreeGridField object.
    // <p>
    // If +link{TreeGrid.dataSource} is also set, this value acts as a set of overrides as
    // explained in +link{attr:DataBoundComponent.fields}.
    //
    // @group databinding
    // @see TreeGridField
    // @visibility external
    //<

    //> @object TreeGridField
    //
    // An object literal with a particular set of properties used to configure the display of
    // and interaction with the columns of a +link{TreeGrid}.
    // +link{TreeGrid} is a subclass of +link{ListGrid} and as a result, for all fields except
    // the field containing the +link{Tree} itself (specified by
    // +link{treeGridField.treeField}, all properties settable on
    // +link{ListGridField} apply to TreeGridField as well.
    // <p>
    // This class documents just those properties that are specific to TreeGridFields - see
    // +link{ListGridField} for the set of inherited properties.
    //
    // @see ListGridField
    // @see TreeGrid.fields
    // @see ListGrid.setFields
    //
    // @treeLocation Client Reference/Grids/TreeGrid
    // @visibility external
    //<

    // Tree Field
    // ---------------------------------------------------------------------------------------

    //> @attr treeGridField.treeField (Boolean : see below : IRW)
    //
    // The field containing <code>treeField: true</code> will display the +link{Tree}.  If no
    // field specifies this property, if a field named after the +link{Tree.titleProperty} of
    // the Tree is present in +link{TreeGrid.fields}, that field will show the tree.  Note that
    // when using a DataSource, you typically define the title field via
    // +link{DataSource.titleField} and the generated +link{ResultTree} automatically uses this
    // field.
    //
    // If none of the above rules apply, the first field in +link{TreeGrid.fields} is assigned to
    // display the +link{Tree}.
    //
    // @group treeField
    // @visibility external
    //<

    //> @attr treeGridField.canExport (Boolean : null : IR)
    //    Dictates whether the data in this field be exported.  Explicitly set this
    //  to false to prevent exporting.  Has no effect if the underlying
    //  +link{dataSourceField.canExport, dataSourceField} is explicitly set to
    //  canExport: false.
    //
    // @visibility external
    //<

    //>    @attr    treeGrid.treeFieldTitle        (string : "Name" : IR)
    //    Visible title for the tree column (field).
    // @group treeField
    // @visibility external
    //<
    treeFieldTitle:"Name",

    //> @attr treeGrid.autoAssignTreeField (boolean : true : IR)
    // If no field was specified as the "tree-field" (showing indentations for tree hierarchy
    // and tree icons), should we assign one of the other fields to be the tree-field?
    // Useful when we want to display a tree or partial tree as a flattened list.
    // @group treeField
    //<
    autoAssignTreeField:true,

    //>    @attr    treeGrid.showRoot        (Boolean : false : IR)
    // Specifies whether the root node should be displayed in the treeGrid.
    // <P>
    // This property is only available for "children" modelType trees, hence is not allowed for
    // trees that load data from the server dynamically via +link{fetchData()}.
    // <P>
    // To get the equivalent of a visible "root" node in a tree that loads data dynamically,
    // add a singular, top-level parent to the data.  However, note that this top-level parent
    // will technically be the only child of root, and the implicit root object will be
    // returned by +link{tree.getRoot,this.data.getRoot()}.
    //
    // @group treeField
    // @visibility external
    //<
    showRoot:false,

    //>    @attr    treeGrid.separateFolders        (boolean : null : IR)
    // If specified, this attribute will override +link{Tree.separateFolders} on the
    // data for this treeGrid.
    // <P>
    // Specifies whether folders and leaves should be segregated in the treeGrid display.
    // Use +link{tree.sortFoldersBeforeLeaves} to customize whether folders appear before
    // or after their sibling leaves.
    // <P>
    // If unset, at the treeGrid level, the property can be set directly on
    // +link{treeGrid.data,the tree data object} or for dataBound TreeGrids on the
    // +link{treeGrid.dataProperties}.
    //
    // @group treeField
    // @visibility external
    //<
//    separateFolders:false,

    //> @attr treeGrid.dataProperties (Tree : null : IR)
    // For a <code>TreeGrid</code> that uses a DataSource, these properties will be passed to
    // the automatically-created ResultTree.  This can be used for various customizations such as
    // modifying the automatically-chosen +link{tree.parentIdField}.
    // @group databinding
    // @visibility external
    //<
    //
    //> @attr treeGrid.sortFoldersBeforeLeaves (boolean : null : IR)
    // If specified, this attribute will override +link{tree.sortFoldersBeforeLeaves} on
    // the data for this treeGrid.
    // <P>
    // Specifies whether when +link{tree.separateFolders} is true, folders should be displayed
    // before or after their sibling leaves in a sorted tree. If set to true, with
    // sortDirection set to Array.ASCENDING, folders are displayed before their sibling leaves
    // and with sort direction set to Array.DESCENDING they are displayed after. To invert
    // this behavior, set this property to false.
    // @group treeField
    // @see treeGrid.separateFolders
    // @visibility external
    //<
//    sortFoldersBeforeLeaves:null,

    //>    @attr    treeGrid.displayNodeType (DisplayNodeType : isc.Tree.FOLDERS_AND_LEAVES : [IRW])
    //          Specifies the type of nodes displayed in the treeGrid.
    // @see type:DisplayNodeType for options
    // @group treeField
    // @visibility external
    //<
    displayNodeType:isc.Tree.FOLDERS_AND_LEAVES,

    //> @attr treeGrid.autoPreserveOpenState (PreserveOpenState : null : [IR])
    // For dataBound treeGrids this specifies the +link{resultTree.autoPreserveOpenState},
    // governing whether the open state of the tree should be preserved when new data
    // arrives due to cache invalidation.
    //
    // @visibility external
    //<

    // Drag and Drop
    // --------------------------------------------------------------------------------------------
    //>    @attr    treeGrid.canDragRecordsOut        (Boolean : false : IRW)
    //    @include ListGrid.canDragRecordsOut
    //  @group    dragdrop
    // @see TreeNode.canDrag
    // @see TreeNode.canAcceptDrop
    // @visibility external
    // @example treeDropEvents
    //<
    canDragRecordsOut:false,

    // Drag and Drop
    // --------------------------------------------------------------------------------------------

    //>    @attr    treeGrid.canAcceptDroppedRecords        (Boolean : false : IRW)
    //    @include ListGrid.canAcceptDroppedRecords
    // @see TreeNode.canDrag
    // @see TreeNode.canAcceptDrop
    //  @group    dragdrop
    // @visibility external
    // @example dragReparent
    //<
    //canAcceptDroppedRecords:false,

    //>    @attr    treeGrid.canReorderRecords        (Boolean : false : IRWA)
    // @include ListGrid.canReorderRecords
    // @see TreeNode.canDrag
    // @see TreeNode.canAcceptDrop
    // @group dragdrop
    // @visibility external
    // @example dragReparent
    //<
    //canReorderRecords:false,

    //> @attr treeGrid.canDropOnLeaves          (Boolean : false : IRWA)
    // Whether drops are allowed on leaf nodes.
    // <P>
    // Dropping is ordinarily not allowed on leaf nodes unless +link{canReorderRecords} is
    // set.
    // <P>
    // The default action for a drop on a leaf node is to place the node in that leaf's parent
    // folder.  This can be customized by overriding +link{folderDrop()}.
    // <P>
    // Note that enabling <code>canDropOnLeaves</code> is usually only appropriate where you
    // intend to add a custom +link{folderDrop()} implementation that <b>does not</b> add a
    // child node under the leaf.  If you want to add a child nodes to a leaf, instead of
    // enabling canDropOnLeaves, use empty folders instead - see +link{Tree.isFolder} for how
    // to control whether a node is considered a folder.
    //
    // @visibility external
    //<

    //> @attr treeGrid.canDropRootNodes (Boolean : true : IRW)
    // Whether to allow dropping new root nodes for the grid
    // <P>
    // If this property is false, attempts to drop a new root node will result in dropping
    // on the nearest root node instead.
    //<
    canDropRootNodes: true,

    //> @attr treeGrid.canReparentNodes     (boolean : null : IRW)
    // If set this property allows the user to reparent nodes by dragging them from their
    // current folder to a new folder.<br>
    // <b>Backcompat:</b> For backwards compatibility with versions prior to SmartClient 1.5,
    // if this property is unset, but <code>this.canAcceptDroppedRecords</code> is true, we
    // allow nodes to be dragged to different folders.
    // @see TreeNode.canDrag
    // @see TreeNode.canAcceptDrop
    // @group dragdrop
    // @visibility external
    //<
    //canReparentNodes:null,

    //>    @attr    treeGrid.dragDataAction        (DragDataAction : isc.ListGrid.MOVE : IRWA)
    //
    // Specifies what to do with data dragged from this TreeGrid (into another component, or
    // another node in this TreeGrid.  The default action is to move the data.  A setting of
    // "none" is not recommended for trees because Trees maintain the node open state on the nodes
    // themselves, and hence having multiple Tree objects share a reference to a node can have
    // unintended consequences (such as opening a folder in one tree also triggering an open in
    // another tree that shares the same node).
    // <br><br>
    // For DataBound trees (+link{class:ResultTree}), the expectation is that
    // +link{method:TreeGrid.folderDrop} will be overridden to perform whatever action took
    // place as the result of the drag and drop interaction.
    //
    // @see group:sharingNodes
    // @visibility external
    //<
    dragDataAction:isc.ListGrid.MOVE,



    //> @attr treeGrid.openDropFolderDelay (integer : 600 : IRWA)
    // When dragging something over a closed folder, delay in milliseconds before the folder
    // automatically opens.
    //<
    openDropFolderDelay:600,

    // D&D Error Messages
    // error messages for invalid drag and drop situations.  Can be customized on a per
    // instance basis so something more application-specific can be said, eg "a manager cannot
    // become his own employee"

    //> @attr treeGrid.parentAlreadyContainsChildMessage (String : "This item already contains a child item with that name." : IR)
    // Message displayed when user attempts to drag a node into a parent that already contains
    // a child of the same name.
    // @see attr:treeGrid.canDragRecordsOut
    // @see attr:treeGrid.canAcceptDroppedRecords
    // @see attr:treeGrid.canReorderRecords
    // @group i18nMessages
    // @visibility external
    //<
    parentAlreadyContainsChildMessage:"This item already contains a child item with that name.",

    //>    @attr treeGrid.cantDragIntoSelfMessage (String : "You can't drag an item into itself." : IR)
    // Message displayed when user attempts to drop a dragged node onto itself.
    // @see attr:treeGrid.canDragRecordsOut
    // @see attr:treeGrid.canAcceptDroppedRecords
    // @see attr:treeGrid.canReorderRecords
    // @group i18nMessages
    // @visibility external
    //<
    cantDragIntoSelfMessage:"You can't drag an item into itself.",

    //>    @attr treeGrid.cantDragIntoChildMessage (String : "You can't drag an item into one of it's children." : IR)
    // Message displayed when user attempts to drop a node into a child of itself.
    // @see attr:treeGrid.canDragRecordsOut
    // @see attr:treeGrid.canAcceptDroppedRecords
    // @see attr:treeGrid.canReorderRecords
    // @group i18nMessages
    // @visibility external
    //<
    cantDragIntoChildMessage:"You can't drag an item into one of it's children.",

    // Body Rendering
    // --------------------------------------------------------------------------------------------

    //>    @attr    treeGrid.fixedFieldWidths        (boolean : true : IRWA)
    //            make trees fixedFieldWidths by default
    //        @group    appearance
    //<
    fixedFieldWidths:true,

    //>    @attr    treeGrid.wrapCells        (boolean : false : IRWA)
    //            don't wrap, as that will mess up the look of the trees
    //        @group    appearance
    //<
    wrapCells:false,

    //>    @attr    treeGrid.showHiliteInCells        (boolean : false : IRWA)
    // Should the hilite show across the entire record or just in the text of the item itself
    //<
    showHiliteInCells:false,

    // Images: locations, sizes, and names
    // --------------------------------------------------------------------------------------------
    //> @attr treeGrid.indentSize (number : 20 : IRW)
    // The amount of indentation (in pixels) to add to a node's icon/title for each level
    // down in this tree's hierarchy.
    // <p>
    // This value is ignored when +link{treeGrid.showConnectors,showConnectors} is
    // <code>true</code> because fixed-size images are used to render the connectors.
    // @visibility external
    // @group appearance
    //<
    indentSize:20,

    //> @attr treeGrid.extraIconGap (int: 2 : IR)
    // The amount of gap (in pixels) between the extraIcon (see +link{treeGrid.getExtraIcon()})
    // or checkbox icon and the +link{treeGrid.nodeIcon,nodeIcon}/
    // +link{treeGrid.folderIcon,folderIcon} or node text.
    // @group appearance
    // @visibility external
    //<
    extraIconGap:2,

    //>    @attr    treeGrid.iconSize        (number : 16 : [IRW])
    //          The standard size (same height and width, in pixels) of node icons in this
    //          treeGrid.
    // @group treeIcons
    // @visibility external
    //<
    iconSize:16,

    //>    @attr    treeGrid.openerIconSize        (number : null : [IRW])
    // Default width and height in pixels of the opener icons, that is, the icons which show
    // the open or closed state of the node, typically a [+] or [-] symbol, if not overridden
    // by +link{TreeGrid.openerIconWidth}/+link{TreeGrid.openerIconHeight}.
    // <P>
    // If +link{showConnectors} is true, the opener icon includes the connector line, and
    // defaults to +link{listGrid.cellHeight,cellHeight}.
    // <P>
    // Otherwise, <code>openerIconSize</code> defaults to +link{iconSize}.
    //
    // @group treeIcons
    // @visibility external
    //<

    //> @attr treeGrid.openerIconWidth (number : null : [IRW])
    // Width in pixels of the opener icons, that is, the icons which show the open or closed
    // state of the node, typically a [+] or [-] symbol.
    // <P>
    // If not specified, +link{TreeGrid.openerIconSize} is used instead.
    //
    // @group treeIcons
    // @visibility external
    //<

    //> @attr treeGrid.openerIconHeight (number : null : [IRW])
    // Height in pixels of the opener icons, that is, the icons which show the open or closed
    // state of the node, typically a [+] or [-] symbol.
    // <P>
    // If not specified, +link{TreeGrid.openerIconSize} is used instead.
    //
    // @group treeIcons
    // @visibility external
    //<

    //>    @attr    treeGrid.skinImgDir        (URL : "images/TreeGrid/" : IRWA)
    //        Where do 'skin' images (those provided with the class) live?
    //        This is local to the Page.skinDir
    //        @group    appearance, images
    //<
    skinImgDir:"images/TreeGrid/",

    //>    @attr    treeGrid.folderIcon        (SCImgURL : "[SKIN]folder.gif" : [IRW])
    // The URL of the base icon for all folder nodes in this treeGrid. Note that this URL will
    // have +link{treeGrid.openIconSuffix}, +link{treeGrid.closedIconSuffix} or
    // +link{treeGrid.dropIconSuffix} appended to indicate state changes if appropriate -
    // see documentation on  +link{treeGrid.showOpenIcons} and +link{treeGrid.showDropIcons}.
    // @group treeIcons
    //      @visibility external
    // @example nodeTitles
    //<
    folderIcon:"[SKIN]/folder.gif",

    //> @attr   treeGrid.dropIconSuffix   (String : "drop" : [IRW])
    // If +link{treeGrid.showDropIcons} is true, this suffix will be appended to the
    // +link{treeGrid.folderIcon} when the user drop-hovers over some folder.
    // @group treeIcons
    // @visibility external
    //<
    dropIconSuffix:"drop",

    //> @attr   treeGrid.openIconSuffix   (String : "open" : [IRW])
    // If +link{showOpenIcons} is true, this suffix will be appended to the
    // +link{folderIcon} for open folders in this grid.
    // @group treeIcons
    // @visibility external
    //<
    openIconSuffix:"open",

    //> @attr   treeGrid.closedIconSuffix   (String : "closed" : [IRW])
    // This suffix will be appended to the +link{folderIcon} for closed folders.
    // If +link{showOpenIcons} is set to <code>false</code> this suffix will also be
    // appended to open folders' icons.
    // @group treeIcons
    // @visibility external
    //<
    closedIconSuffix:"closed",


    //> @attr   treeGrid.nodeIcon  (SCImgURL : "[SKIN]file.gif" : [IRW])
    // The filename of the default icon for all leaf nodes in this grid. To specify a
    // custom image for an individual node, set the +link{customIconProperty} directly on
    // the node.
    // @group treeIcons
    // @visibility external
    // @example nodeTitles
    //<
    nodeIcon:"[SKIN]/file.gif",

    //>@attr treeGrid.showOpenIcons (Boolean : true : IRW)
    // If true, show a different icon for <code>open</code> folders than closed folders.
    // This is achieved by appending the +link{openIconSuffix} onto the
    // +link{folderIcon} URL [for example <code>"[SKIN]/folder.gif"</code> might be
    // replaced by <code>"[SKIN]/folder_open.gif"</code>.<br>
    // <b>Note</b> If this property is set to <code>false</code> the same icon is shown for
    // open folders as for closed folders, unless a custom folder icon was specified. This will be
    // determined by +link{folderIcon} plus the +link{closedIconSuffix}.
    // @group treeIcons
    // @visibility external
    // @example nodeTitles
    //<
    showOpenIcons:true,

    //>@attr treeGrid.showDropIcons (Boolean : true : IRW)
    // If true, when the user drags a droppable target over a folder in this TreeGrid, show
    // a different icon folder icon.
    // This is achieved by appending the +link{treeGrid.dropIconSuffix} onto the
    // +link{TreeGrid.folderIcon} URL (for example <code>"[SKIN]/folder.gif"</code> may be
    // replaced by <code>"[SKIN]/folder_drop.gif"</code>).
    // @group treeIcons
    // @visibility external
    // @example nodeTitles
    //<
    showDropIcons:true,

    //> @attr   treeGrid.customIconProperty   (String : "icon" : [IRW])
    // This property allows the developer to rename the
    // +link{TreeNode.icon, default node.icon} property.
    // @group treeIcons
    // @visibility external
    //<
    customIconProperty:"icon",

    //> @attr   treeGrid.customIconOpenProperty (string : "showOpenIcon" : [IRWA])
    // This property allows the developer to rename the
    // +link{TreeNode.showOpenIcon, default node.showOpenIcon} property.
    // @see treeGrid.customIconProperty
    // @see treeGrid.showCustomIconOpen
    // @visibility external
    // @group treeIcons
    //<
    customIconOpenProperty:"showOpenIcon",

    //> @attr   treeGrid.customIconDropProperty (string : "showDropIcon" : [IRWA])
    // This property allows the developer to rename the
    // +link{TreeNode.showDropIcon, default node.showDropIcon} property.
    // @see treeGrid.customIconProperty
    // @see treeGrid.showCustomIconDrop
    // @visibility external
    // @group treeIcons
    //<
    customIconDropProperty:"showDropIcon",

    //> @attr   treeGrid.showCustomIconOpen   (Boolean : false : [IRWA])
    // Should folder nodes showing custom icons (set via the +link{customIconProperty}),
    // show open state images when the folder is opened.
    // If true, the +link{openIconSuffix} will be appended to the image URL
    // (so <code>"customFolder.gif"</code> might be replaced with
    // <code>"customFolder_open.gif"</code>).<br>
    // <b>Note</b> that the +link{closedIconSuffix} is never appended to custom folder icons.<br>
    // Can be overridden at the node level via the default property +link{treeNode.showOpenIcon}
    // and that property can be renamed via +link{treeGrid.customIconOpenProperty}.
    // @group treeIcons
    // @visibility external
    //<
    showCustomIconOpen:false,

    //> @attr   treeGrid.showCustomIconDrop   (Boolean : false : [IRWA])
    // Should folder nodes showing custom icons (set via the +link{treeGrid.customIconProperty},
    // default +link{treeNode.icon}),
    // show drop state images when the user is drop-hovering over the folder.
    // If true, the +link{treeGrid.dropIconSuffix} will be appended to the image URL
    // (so <code>"customFolder.gif"</code> might be replaced with
    // <code>"customFolder_drop.gif"</code>).<br>
    // Can be overridden at the node level via the default property +link{treeNode.showDropIcon}
    // and that property can be renamed via +link{treeGrid.customIconDropProperty}.
    // @group treeIcons
    // @visibility external
    //<
    showCustomIconDrop:false,

    //> @attr   treeGrid.showDisabledSelectionCheckbox  (Boolean : false : [IR])
    // Should tree nodes show a disabled checkbox
    // +link{ListGrid.selectionAppearance, selectionAppearance}:"checkbox"
    // is set on the treegrid, and a node can't be selected?
    // <P>
    // If set to <code>false</code> the treeGrid will use
    // +link{treeGrid.leaveSelectionCheckboxGap} to determine whether to leave
    // a blank space where the checkbox would normally appear.
    //
    // @see ListGrid.recordCanSelectProperty
    // @group treeIcons
    // @visibility external
    //<

    //> @attr treeGrid.leaveSelectionCheckboxGap (Boolean : true : [IR])
    // If +link{ListGrid.selectionAppearance, selectionAppearance}:"checkbox"
    // is set on the treegrid, and a node can't be selected, should a gap be left where
    // the checkbox icon would normally appear, in order to make the node's icon and title
    // line up with the content for other nodes in the same parent?
    // <p>
    // Has no effect if +link{showDisabledSelectionCheckbox} is <code>true</code>
    // @see ListGrid.recordCanSelectProperty
    // @group treeIcons
    // @visibility external
    //<
    leaveSelectionCheckboxGap:true,

    // ---------------------------------
    // DEPRECATED ICON PROPERTIES:
    //

    //>    @attr    treeGrid.folderOpenImage        (String : null : [IRW])
    //          The filename of the default icon for all open folder nodes in this treeGrid.
    //      @visibility external
    //  @deprecated as part of SmartClient release 5.5 in favor of +link{TreeGrid.folderIcon}
    //<

    //>    @attr    treeGrid.folderClosedImage        (string : null : [IRW])
    // The filename of the default icon for all closed folder nodes in this treeGrid. Use
    // the node.icon property (null by default) to specify a custom image for an individual
    // folder node. The same custom image will be used for both the open and closed folder
    // images.
    //      @visibility external
    //  @deprecated as part of SmartClient release 5.5 in favor of +link{TreeGrid.folderIcon}
    //<

    //>    @attr    treeGrid.folderDropImage       (String : null : [IRW])
    // The filename of the icon displayed for a folder node that will accept drag-and-drop
    // data when the mouse is released.
    //      @visibility external
    //  @deprecated as part of SmartClient release 5.5 in favor of +link{TreeGrid.folderIcon}
    //<

    //>    @attr    treeGrid.fileImage             (SCImgURL : "[SKIN]file.gif" : [IRW])
    // The filename of the default icon for all leaf nodes in this treeGrid. Use the
    // node.icon property (null by default) to specify a custom image for an individual
    // node.
    //      @visibility external
    //  @deprecated as part of SmartClient release 5.5 in favor of +link{TreeGrid.nodeIcon}
    //<

    // --------------------

    //>    @attr    treeGrid.manyItemsImage        (SCImgURL : "[SKIN]folder_file.gif" : [IRW])
    // The filename of the icon displayed use as the default drag tracker when for multiple
    // files and/or folders are being dragged.
    // @group dragdrop
    //      @visibility external
    //<
    manyItemsImage:"[SKIN]folder_file.gif",

    //>    @attr    treeGrid.showConnectors (Boolean : false : [IRW])
    // Should this treeGrid show connector lines illustrating the tree's hierarchy?
    // <P>
    // For the set of images used to show connectors, see +link{connectorImage}.
    // <P>
    // <b>Note</b>: in order for connector images to be perfectly connected, all styles for
    // cells must have no top or bottom border or padding.  If you see small gaps in connector
    // lines, check your CSS files.  See the example below for an example of correct
    // configuration, including example CSS.
    //
    // @group treeIcons
    // @example connectors
    // @visibility external
    //<

    showConnectors : false,

    //>    @attr    treeGrid.showFullConnectors (Boolean : true : [IRW])
    // If +link{treeGrid.showConnectors} is true, this property determines whether we should show
    // vertical continuation lines for each level of indenting within the tree. Setting to
    // false will show only the hierarchy lines are only shown for the most indented path ("sparse"
    // connectors).
    // @group treeIcons
    // @visibility external
    //<
    // Default to false since older skins won't have all the media required to render full
    // connector lines out.
    // The logic to show full connectors involves iterating through the parents for each node
    // being written out. This is a potential performance hit. We could improve this performance
    // by adding caching logic to the Tree when calculating where the continuation lines should
    // appear if this is a problem.
    showFullConnectors:true,

    //> @attr treeGrid.showOpener (Boolean : true : [IRW])
    // Should the +link{treeGrid.openerImage,opener icon}
    // be displayed next to folder nodes? This is an icon
    // which visually indicates whether the folder is opened or closed (typically via
    // a [+] or [-] image, or a turn-down arrow) and may be clicked to expand or collapse
    // the folder.
    // <P>
    // For folders with no children, this icon is not shown unless
    // +link{treeGrid.alwaysShowOpener} is <code>true</code>. Note that for trees which
    // +link{treeGrid.loadDataOnDemand,load data on demand}, we may not know if a folder
    // has any descendants if it has never been opened. As such we will show the
    // opener icon next to the folder. Once the user opens the icon and a fetch occurs,
    // if the folder is empty, and +link{alwaysShowOpener} is false, the opener icon
    // will be hidden.
    // <P>
    // For more information on load on demand trees, and how we determine whether
    // a node is a a folder or a leaf, please refer to the +link{group:treeDataBinding}
    // documentation.
    //
    // @visibility external
    //<
    showOpener:true,

    //> @attr treeGrid.alwaysShowOpener (Boolean : false : IRW)
    // If +link{treeGrid.showOpener} is true, should we display the opener icon
    // for folders even if they have no children?
    // <P>
    // Note that for trees which
    // +link{treeGrid.loadDataOnDemand,load data on demand}, we may not know if a folder
    // has any descendants if it has never been opened. As such we will show the
    // opener icon next to the folder. Once the user opens the icon and a fetch occurs,
    // if the folder is empty, and this property is false, the opener icon
    // will be hidden.
    // <P>
    // For more information on load on demand trees, and how we determine whether
    // a node is a a folder or a leaf, please refer to the +link{group:treeDataBinding}
    // documentation.
    //
    // @visibility external
    //<
    alwaysShowOpener:false,

    //>    @attr    treeGrid.openerImage        (SCImgURL : "[SKIN]opener.gif" : [IR])
    // The base filename of the opener icon for the folder node when 'showConnectors' is false
    // for this TreeGrid.<br>
    // The opener icon is displayed beside the folder icon in the Tree column for folder nodes.
    // Clicking on this icon will toggle the open state of the folder.<br>
    // The filenames for these icons are assembled from this base filename and the state of the
    // node, as follows:<br>
    // If the openerImage is set to <code>{baseName}.{extension}</code>,
    // <code>{baseName}_opened.{extension}</code> will be displayed next to opened folders, and
    // <code>{baseName}_closed.{extension}</code> will be displayed next to closed folders, or
    // if this page is in RTL mode, <code>{baseName}_opened_rtl.{extension}</code> and
    // <code>{baseName}_closed_rtl.{extension}</code> will be used.
    //
    // @group treeIcons
    //      @visibility external
    //<
    openerImage:"[SKIN]opener.gif",


    //>    @attr    treeGrid.connectorImage        (SCImgURL : "[SKIN]connector.gif" : [IR])
    // The base filename for connector icons shown when +link{TreeGrid.showConnectors} is true.
    // Connector icons are rendered into the title field of each row and show the dotted
    // hierarchy lines between siblings of the same parent node. For each node, a connector icon
    // may be shown:<ul>
    // <li>As an opener icon for folder nodes, next to the folder icon</li>
    // <li>In place of an opener icon for leaf nodes, next to the leaf icon</li>
    // <li>As a standalone vertical continuation line in the indent to the left of the node, to show
    //     a connection between some ancestor node's siblings (only relevant if
    //     +link{TreeGrid.showFullConnectors} is true).</li>
    // </ul>
    // Note that +link{TreeGrid.showFullConnectors} governs whether connector lines will be
    // displayed for all indent levels, or just for the innermost level of the tree.
    // <P>
    // The filenames for these icons are assembled from this base filename and the state of the
    // node.  Assuming the connectorImage is set to <code>{baseName}.{extension}</code>, the
    // full set of images to be displayed will be:
    // <P>
    // <code>{baseName}_ancestor[_rtl].{extension}</code> if +link{TreeGrid.showFullConnectors}
    //  is true, this is the URL for the vertical continuation image to be displayed at the
    //  appropriate indent levels for ancestor nodes with subsequent children.
    // <P>
    // For nodes with no children:
    // <ul>
    // <li><code>{baseName}_single[_rtl].{extension}</code>: Shown when there is no connector line
    //  attached to the parent or previous sibling, and no connector line to the next sibling. For
    //  +link{TreeGrid.showFullConnectors,showFullConnectors:true} trees, there will always be a
    //  connector leading to the parent or previous sibling if its present in the tree so this
    //  icon can only be displayed for the first row.</li>
    // <li><code>{baseName}_start[_rtl].{extension}</code>:  Shown when the there is no connector
    //  line attached to the parent or previous sibling, but there is a connector to the next
    //  sibling. As with <code>_single</code> this will only ever be used for the first row if
    //  +link{TreeGrid.showFullConnectors} is true</li>
    // <li><code>{baseName}_end[_rtl].{extension}</code>:  Shown if we are not showing a connector
    //  line attached to the next sibling of this node (but are showing a connection to the previous
    //  sibling or parent).</li>
    // <li><code>{baseName}_middle[_rtl].{extension}</code>:  Shown where the we have a connector
    //  line leading to both the previous sibling (or parent) and the next sibling.
    // </ul>
    // For folders with children. Note that if +link{TreeGrid.showFullConnectors} is false, open
    // folders will never show a connector to subsequent siblings:
    // <ul>
    // <li><code>{baseName}_opened_single[_rtl].{extension}</code> opened folder node with
    //  children when no connector line is shown attaching to either the folder's previous sibling
    //  or parent, or to any subsequent siblings.</li>
    // <li><code>{baseName}_opened_start[_rtl].{extension}</code>:  opened folder with children
    //  when the there is no connector line attached to the parent or previous sibling, but there
    //  is a connector to the next sibling.</li>
    // <li><code>{baseName}_opened_end[_rtl].{extension}</code>:  opened folder with children
    //  if we are not showing a connector line attached to the next sibling of this node (but are
    //  showing a connection to the previous sibling or parent).</li>
    // <li><code>{baseName}_opened_middle[_rtl].{extension}</code>: opened folder with children
    //  where the we have a connector line leading to both the previous sibling (or parent) and the
    //  next sibling.
    // </ul>
    // <ul>
    // <li><code>{baseName}_closed_single[_rtl].{extension}</code> closed folder node with
    //  children when no connector line is shown attaching to either the folder's previous sibling
    //  or parent, or to any subsequent siblings.</li>
    // <li><code>{baseName}_closed_start[_rtl].{extension}</code>:  closed folder with children
    //  when the there is no connector line attached to the parent or previous sibling, but there
    //  is a connector to the next sibling.</li>
    // <li><code>{baseName}_closed_end[_rtl].{extension}</code>:  closed folder with children
    //  if we are not showing a connector line attached to the next sibling of this node (but are
    //  showing a connection to the previous sibling or parent).</li>
    // <li><code>{baseName}_closed_middle[_rtl].{extension}</code>: closed folder with children
    //  where the we have a connector line leading to both the previous sibling (or parent) and the
    //  next sibling.
    // </ul>
    // (Note '[_rtl]' means that "_rtl" will be attached if isRTL() is true for this widget).
    // @group treeIcons
    //      @visibility external
    //<
    connectorImage:"[SKIN]connector.gif",

    //>    @attr treeGrid.offlineNodeMessage (String : "This data not available while offline" : [IRW])
    // For TreeGrids with loadDataOnDemand: true, a message to show the user if an attempt is
    // made to open a folder, and thus load that node's children, while we are offline and
    // there is no offline cache of that data.  The message will be presented to the user in
    // in a pop-up dialog box.
    //
    // @visibility external
    // @group offlineGroup, i18nMessages
    // @see dataBoundComponent.offlineMessage
    //<
    offlineNodeMessage: "This data not available while offline",

    //> @attr treeGrid.indentRecordComponents (Boolean : true : IRW)
    // For record components placed "within" the +link{TreeGridField.treeField,treeField}
    // column, should the component be indented to the position where a title would normally
    // show?
    // <P>
    // For more general placement of embedded components, see
    // +link{ListGrid.addEmbeddedComponent, addEmbeddedComponent}.
    //
    // @visibility external
    //<
    indentRecordComponents: true,

    //> @attr treeGrid.createDefaultTreeField (Boolean : true : IR)
    // If no fields are specified, create a single field with
    // +link{treeGridField.treeField} set to <code>true</code> to show the tree.
    // <P>
    // This automatically generated field will display values derived by calling
    // +link{treeGrid.getNodeTitle()}, and have the column title set to the specified
    // +link{TreeGrid.treeFieldTitle}.
    // <P>
    // Has no effect if fields are explicitly specified.
    // <P>
    // This is a convenience setting to allow a TreeGrid to be created without specifying a
    // field list.  If fields are specified, refer to the documentation on property
    // +link{treeGrid.autoAssignTreeField} for a way to automatically have one of the fields be
    // use as the tree field if no fields have +link{treeGridField.treeField} set.
    // <P>
    // For databound treeGrids, if there is no explicit fields array specified, developers
    // who wish to pick up all fields from the DataSource definition rather than displaying
    // this single automatically generated tree field may
    // either set this property to false, or set +link{treeGrid.useAllDataSourceFields}
    // to <code>true</code>.
    //
    // @visibility external
    //<
    createDefaultTreeField: true,

    //> @attr treeGrid.useAllDataSourceFields (boolean : null : IRW)
    // @include listGrid.useAllDataSourceFields
    // @visibility external
    //<

    // Disble groupBy for TreeGrids altogether - we're already showing data-derived hierarchy!
    canGroupBy: false,


    ignoreEmptyCriteria: false,

    // users tend to navigate trees by opening and closing nodes more often than by scrolling,
    // so optimize for that use case.
    drawAllMaxCells:50,
    drawAheadRatio:1.0,

    // heavily used strings for templating
    _openIconIDPrefix: "open_icon_",
    _extraIconIDPrefix:"extra_icon_",
    _iconIDPrefix: "icon_",
    _titleField: "nodeTitle"

});

isc.TreeGrid.addMethods({

initWidget : function () {
    this.invokeSuper(isc.TreeGrid, this._$initWidget);

    // if no dataSource is specified on this TG, pick up the dataSource off the data model
    if (!this.dataSource && this.data != null && this.data.dataSource) {
        this.dataSource = this.data.dataSource;
    }

    if (this.createDefaultTreeField &&
        (this.getDataSource() == null || !this.useAllDataSourceFields))
    {
        // if the fields are not set or of zero length, initialize with a single TREE_FIELD
        // NB: it is not safe to try to determine the tree field before setFields has been run,
        // since fields in this.fields might not be shown if they have a showIf:false
        if (!this.fields || this.fields.length == 0) {
            this.fields = [isc.TreeGrid.TREE_FIELD];
        }
    }
},

setDataSource : function (ds, fields) {
    if (this.createDefaultTreeField) {
        // if no fields were passed in, default to showing the tree field.  This matches the
        // behavior if a datbound treeGrid is initialized with no fields.
        if (fields == null || fields.length == 0) {
            fields = [isc.TreeGrid.TREE_FIELD];
        }
    }
    return this.Super("setDataSource", [ds, fields]);
},

cellValueHoverHTML : function (record, rowNum, colNum, defaultHTML) {
    if (colNum == this._treeFieldNum) {
        var returnVal = this.Super("cellValueHoverHTML", arguments);
        if (returnVal != null && !isc.isAn.emptyString(returnVal)) returnVal = "<nobr>" + returnVal + "</nobr>";
        return returnVal;

    // the clipped value hovers are only enabled by default for the tree field.
    } else if (this.showClippedValuesOnHover !== true) return null;
    else return defaultHTML;
},

// make sure one of the fields has been set up as the special "tree field"
_initTreeField : function () {

    // if the fields are not set or of zero length, initialize with a single TREE_FIELD
    if ((!this.fields || this.fields.length == 0) &&
         (this.getDataSource() == null || !this.useAllDataSourceFields))
    {

        if (this.createDefaultTreeField) this.fields = [isc.TreeGrid.TREE_FIELD];
    } else {

        // see which field is the tree field.  Note this handles both the case that the special
        // constant TreeGrid.TREE_FIELD was provided as a field, and the case that the caller
        // marked a field as a the treeField.

        // if none of the fields is specified as the treeField, we look for a "title" field,
        // then we default to the first field in the array; we use this.completeFields so that
        // the treeField property of hidden fields will be checked as well--otherwise we would
        // default another field to be the tree field, and end up with more than one treeField
        // if the hidden treeField became visible again.

        var completeFields = this.completeFields,
            fields = this.fields,
            treeFieldNum;

        for (var i = 0; i < completeFields.length; i++) {
            if (completeFields[i].treeField) {
                treeFieldNum = fields.indexOf(completeFields[i]);
                break;
            }
        }

        if (treeFieldNum == null) {
            // if autoAssignTreeField has been set false, don't assign a default tree field in
            // the absence of an explicit marker
            if (!this.autoAssignTreeField) return;

            // if there is no explicit marker, look for the field that matches the
            // titleProperty declared on the Tree
            var titleProp = this.data.titleProperty,
                fieldNum = fields.findIndex(this.fieldIdProperty, titleProp);
            if (fieldNum != -1) treeFieldNum = fieldNum;
        }

        // use the first field if none were marked as the tree field
        if (treeFieldNum == null) {
            treeFieldNum = 0;
            // Skip any fields where treeField is explicitly marked false
            // this includes auto-generated checkbox, rowNumber, etc fields
            while (this.fields[treeFieldNum] &&
                    this.fields[treeFieldNum].treeField == false)
            {
                treeFieldNum++;
            }
            if (this.fields[treeFieldNum] == null) return;
        }

        // store the chosen fieldNum
        this._treeFieldNum = treeFieldNum;

        // use the properties of TREE_FIELD as defaults for the field
        // Note: We're manipulating the field object in the fields array.
        // this.completeFields also contains a pointer to this object.
        // We don't want to replace the slot in either array with a different object as
        // that would make them out of synch (causes errors sorting, etc.)
        // - instead just copy any unset properties across from the TREE_FIELD field.
        var treeField = fields[treeFieldNum],
            fieldDefaults = isc.TreeGrid.TREE_FIELD,
            // Don't clobber explicit formatCellValue() / displayField with our
            // custom node title logic.
            hasCustomCellValue = treeField.formatCellValue != null
                             || treeField.displayField != null;
        for (var property in fieldDefaults) {
            if (hasCustomCellValue && property == "getCellValue") {
                continue;
            }
            if (treeField[property] == null) {
                treeField[property] = fieldDefaults[property]
            }
        }
    }
},

// because we store _treeFieldNum as a number, we need to recalc when fields are changed or
// their numbering changes.  This include setFields(), reorderFields(), showField() and hideField().
//
// Note that the chosen treeField won't shift on reorder, because we install the TREE_FIELD
// properties into the chosen field, and the TREE_FIELD properties includes the treeField:true
// marker.
deriveVisibleFields : function (a,b,c,d) {
    this.invokeSuper(isc.TreeGrid, "deriveVisibleFields", a,b,c,d);
    this._initTreeField();
},

getEmptyMessage : function () {

    if (this.isOffline()) {
        return this.offlineMessage;
    }

    // can't just check for data != null because ListGrid initWidget sets data to [] if unset
    // and we must make sure we have a tree.
    if (isc.isA.Tree(this.data) && this.data.getLoadState(this.data.getRoot()) == isc.Tree.LOADING)
        return this.loadingDataMessage == null ? "&nbsp;"
                                : this.loadingDataMessage.evalDynamicString(this, {
            loadingImage: this.imgHTML(isc.Canvas.loadingImageSrc,
                                       isc.Canvas.loadingImageSize,
                                       isc.Canvas.loadingImageSize)
        });
    return this.emptyMessage.evalDynamicString(this, {
        loadingImage: this.imgHTML(isc.Canvas.loadingImageSrc,
                                   isc.Canvas.loadingImageSize,
                                   isc.Canvas.loadingImageSize)
    });
},

isEmpty : function () {
    // can't just check for data != null because ListGrid initWidget sets data to [] if unset
    // and we must make sure we have a tree.
    if (!isc.isA.Tree(this.data)) return true;

    var root = this.data.getRoot();
    if (root == null) return true;

    var rootHasChildren = this.data.hasChildren(root);
    if (rootHasChildren || this.showRoot || this.data.showRoot) return false;
    return true;
},

// Folder Animation
// ---------------------------------------------------------------------------------------
// Because of grouping, the implementation of all of these properties is actually on ListGrid, but is
// re-doc'd here for clarity

//> @attr treeGrid.animateFolders (Boolean : true : IRW)
// If true, when folders are opened / closed children will be animated into view.
// @group animation
// @visibility animation
// @example animateTree
//<

//> @attr treeGrid.animateFolderMaxRows (integer : null : IRW)
// If +link{animateFolders} is true for this grid, this number can be set to designate
// the maximum number of rows to animate at a time when opening / closing a folder.
// @see getAnimateFolderMaxRows()
// @group animation
// @visibility external
//<

//> @attr treeGrid.animateFolderTime (number : 100 : IRW)
// When animating folder opening / closing, if +link{treeGrid.animateFolderSpeed} is not
// set, this property designates the duration of the animation in ms.
// @group animation
// @visibility animation
// @see treeGrid.animateFolderSpeed
//<

//> @attr treeGrid.animateFolderSpeed (number : 3000 : IRW)
// When animating folder opening / closing, this property designates the speed of the
// animation in pixels shown (or hidden) per second. Takes precedence over the
// +link{treeGrid.animateFolderTime} property, which allows the developer to specify a
// duration for the animation rather than a speed.
// @group animation
// @visibility animation
// @see treeGrid.animateFolderTime
// @example animateTree
//<

//> @attr treeGrid.animateFolderEffect (AnimationAcceleration : null : IRW)
// When animating folder opening / closing, this property can be set to apply an
// animated acceleration effect. This allows the animation speed to be "weighted", for
// example expanding or collapsing at a faster rate toward the beginning of the animation
// than at the end.
// @group animation
// @visibility animation
//<

//> @attr treeGrid.animateRowsMaxTime (number : 1000 : IRW)
// If animateFolderSpeed is specified as a pixels / second value, this property will cap
// the duration of the animation.
// @group animation
// @visibility animation_advanced
//<

//> @method treeGrid.shouldAnimateFolder ()
// Should this folder be animated when opened / closed? Default implementation will
// return true if +link{treeGrid.animateFolders} is true, the folder being actioned
// has children and the child-count is less than the result of
// +link{treeGrid.getAnimateFolderMaxRows}.
// @group animation
// @param folder (TreeNode) folder being opened or closed.
// @return (boolean) returns true if folders should be animated when opened / closed.
// @visibility external
//<

//> @method treeGrid.getAnimateFolderMaxRows() [A]
// If +link{animateFolders} is true for this treeGrid, this method returns the
// the maximum number of rows to animate at a time when opening / closing a folder.
// This method will return +link{treeGrid.animateFolderMaxRows} if set. Otherwise
// the value will be calculated as 3x the number of rows required to fill a viewport,
// capped at a maximum value of 75.
// @return (integer) maximum number of rows to be animated when opening or closing folders.
// @group animation
// @visibility external
//<

// View State
// ---------------------------------------------------------------------------------------

//> @type TreeGridOpenState
// An object containing the open state for a treeGrid.
// Note that this object is not intended to be interrogated directly, but may be stored
// (for example) as a blob on the server for state persistence across sessions.
//
// @group viewState
// @visibility external
//<
// TreeGridOpenState object is implemented as an array of strings, each of which is the path
// to a currently open folder (all other folders are closed)


//> @method treeGrid.getOpenState()
// Returns a snapshot of the current open state of this grid's data as
// a +link{type:TreeGridOpenState} object.<br>
// This object can be passed to +link{treeGrid.setOpenState()} to open the same set of folders
// within the treeGrid's data (assuming the nodes are still present in the data).
// @return (TreeGridOpenState) current open state for the grid.
// @group viewState
// @see treeGrid.setOpenState()
// @visibility external
//<
getOpenState : function () {
    var tree = this.data;
    if (tree == null) {
        this.logWarn("getOpenState() called for a treeGrid with no data");
        return [];
    }
    // Defer to ResultTree if available
    if (tree.getOpenState) return tree.getOpenState();

    // Must be using a static Tree
    var root = tree.getRoot(),
        openState = [];

    this._addNodeToOpenState(tree, root, openState);
    return isc.Comm.serialize(openState);
},
// _addNodeToOpenState implemented in ListGrid
// Used for groupTree open/closed state maintenance logic

//>    @method    treeGrid.setOpenState()
// Reset this set of open folders within this grid's data to match the
// +link{type:TreeGridOpenState} object passed in.<br>
// Used to restore previous state retrieved from the grid by a call to
// +link{treeGrid.getOpenState()}.
//
// @param openState (TreeGridOpenState) Object describing the desired set of open folders.
// @group viewState
// @see treeGrid.getOpenState()
// @visibility external
//<
setOpenState : function (openState) {
    // Defer to ResultTree if available
    if (this.data && this.data.setOpenState) {
        this.data.setOpenState(openState);
        return;
    }
    // Must be using a static Tree
    openState = this.evalViewState(openState, "openState")
    if (!openState) return;

    if (!this.data) {
        this.logWarn("unable to set open state for this treeGrid as this.data is unset");
        return;
    }
    this.data.closeAll();
    this.data.openFolders(openState);
},

//>    @method    treeGrid.getSelectedPaths()
// Returns a snapshot of the current selection within this treeGrid as
// a +link{type:ListGridSelectedState} object.<br>
// This object can be passed to +link{treeGrid.setSelectedPaths()} to reset this grid's selection
// the current state (assuming the same data is present in the grid).<br>
// @group viewState
// @see treeGrid.setSelectedPaths();
// @visibility external
// @return (ListGridSelectedState) current state of this grid's selection
//<
getSelectedPaths : function () {
    if (!this.selection) return null;

    var selection = this.selection.getSelection() || [],
        selectedPaths = [];

    // store paths only.
    for (var i = 0; i < selection.length; i++) {
        selectedPaths[i] = this.data.getPath(selection[i]);
    }
    return isc.Comm.serialize(selectedPaths);
},


// ----------------------------------------------------------------------------
// panelHeader related methods

showActionInPanel : function (action) {
    return this.Super("showActionInPanel", arguments);
},


//>    @method    treeGrid.setSelectedPaths()
// Reset this grid's selection to match the +link{type:ListGridSelectedState} object passed in.<br>
// Used to restore previous state retrieved from the grid by a call to
// +link{treeGrid.getSelectedPaths()}.
//
// @group viewState
// @param selectedPaths (ListGridSelectedState) Object describing the desired selection state of
//                                              the grid
// @see treeGrid.getSelectedPaths()
// @visibility external
//<
setSelectedPaths : function (selectedPaths) {
    selectedPaths = this.evalViewState(selectedPaths, "selectedPaths")
    if (!selectedPaths) return;

    var selection = this.selection, data = this.data;
    if (data && selection) {
        selection.deselectAll();
        var nodes = [];
        // use find to look up node by path
        for (var i = 0; i < selectedPaths.length; i++) {
            var node = data.find(selectedPaths[i]);
            if (node) nodes.add(node);
        }
        this.selection.selectList(nodes);
    }
},

//> @type   TreeGridViewState
// An object containing the "view state" information for a treeGrid. In addition to the
// state data contained by a +link{type:ListGridViewState} object, this will also contain the
// current open state of the treeGrid in question.<br>
// Note that this object is not intended to be interrogated directly, but may be stored
// (for example) as a blob on the server for view state persistence across sessions.
//
// @group viewState
// @visibility external
//<
// TreeGridViewState object is implemented as a simple JS object containing the following
// fields:
// - selected [an (undocumented) treeGridSelectedState object - an array of selected nodes' paths]
// - field [a ListGridFieldState object]
// - sort [a ListGridSortState object]
// - open [a TreeGridOpenState object]

//>    @method    treeGrid.getViewState()
// Overridden to return a +link{type:TreeGridViewState} object for the grid.
// @return (TreeGridViewState) current view state for the grid.
// @group viewState
// @see type:TreeGridViewState
// @see treeGrid.setViewState();
// @visibility external
//<
getViewState : function () {
    var state = this.Super("getViewState", [true]);
    state.open = this.getOpenState();
    return "(" + isc.Comm.serialize(state) + ")";
},


//>    @method    treeGrid.setViewState()
// Overridden to take a +link{type:TreeGridViewState} object.
//
// @param viewState (TreeGridViewState) Object describing the desired view state for the grid
// @group viewState
// @see treeGrid.getViewState()
// @visibility external
//<
setViewState : function (state) {

    // Ensure we set open state after setting sort state
    this.Super("setViewState", arguments);
    // don't bother warning on error - Super() will have done that already
    state = this.evalViewState(state, "viewState", true)
    if (!state) return;

    if (state.open) this.setOpenState(state.open);
    // Re-apply selection so that nodes just opened can be found.
    if (state.selected) this.setSelectedState(state.selected);
},


// if data is not specified, use an empty Tree.
getDefaultData : function () {
    // NOTE: initializing to a ResultTree would effectively trigger fetch on draw.  Don't want
    // to do this unless fetchData() is called (possibly via autoFetchData property), in which
    // case the empty starter Tree will be discarded and replaced by a ResultTree
    //if (this.dataSource) return this.createResultTree();
    return isc.Tree.create({_autoCreated:true});
},

//>    @method    treeGrid.setData()
// Set the +link{class:Tree} object this TreeGrid will view and manipulate.
//
// @param newData (Tree) Tree to show
// @visibility external
//<
setData : function (newData) {
    if (this.data) {
        if (this.separateFolders != null) this.data.setSeparateFolders(this.separateFolders);
        if (this.sortFoldersBeforeLeaves != null) {
            this.data.setSortFoldersBeforeLeaves(this.sortFoldersBeforeLeaves);
        }
    }

    this.Super("setData", arguments);
    if (!isc.isA.Tree(this.data)) return;

    // Set the `separateFolders` and `showRoot` options of the tree as well.
    if (this.separateFolders != null) this.data.setSeparateFolders(this.separateFolders);
    if (this.sortFoldersBeforeLeaves != null) {
        this.data.setSortFoldersBeforeLeaves(this.sortFoldersBeforeLeaves);
    }

    if (this.showRoot && isc.ResultTree && isc.isA.ResultTree(this.data)) {
        this.logWarn("showRoot may not be set with a databound treeGrid, unexpected " +
                     "results may occur");
    }
    this.data.setShowRoot(this.showRoot);

    // should we show only branches or leaves
    this.data.setOpenDisplayNodeType(this.displayNodeType);

    if (this.autoPreserveOpenState != null) {
        this.data.autoPreserveOpenState = this.autoPreserveOpenState
    }
},

draw : function (a,b,c,d) {

    if (this.initialData && (!isc.ResultSet || !isc.isA.ResultSet(this.data)) &&
        (!this.data || !isc.isA.ResultTree(this.data))) {
        this.setData(this.createResultTree());
    }

    this.invokeSuper(isc.TreeGrid, "draw", a,b,c,d);
},

bodyConstructor:"TreeGridBody",

// Override bodyKeyPress to handle open and closing of trees
// Note: standard windows behavior with Left and Right arrow key presses in a treeGrid is:
// - multiple selection seems to *always* be disallowed, so doesn't come into play
// - arrow right on a closed folder will open the folder
// - arrow right on an open folder (with content) will move selection to the first child node
// - arrow left on an open folder will close the folder
// - arrow left on a node within a folder will move selection to the node's parent folder

bodyKeyPress : function (event) {

    // if exactly one record is selected, mimic windows LV behaviors for arrow left and right
    var selection = this.selection;
    if (this.selectionType != isc.Selection.NONE &&
        this.data.getLength() > 0 &&
        selection.anySelected() &&
        // no multipleSelected on CellSelection
        (!selection.multipleSelected || !selection.multipleSelected()))
    {
        var node = this.getRecord(this.getFocusRow());

        // Left/right arrow key interaction if we can expand records:
        // - allow first right arrow to open a folder
        // - allow second right arrow to expand a folder
        // - allow third right arrow to navigate into the the folder
        //
        // - allow first left arrow to collapse a 'canExpand' folder
        // - allow second left arrow to close a folder
        // - allow third left arrow to navigate up to parent
        // Expand / collapse is implemented at the ListGrid level.
        if (event.keyName == "Arrow_Left") {
            if (!this.canExpandRecords ||
                       !(this._canExpandRecord(node,this.getFocusRow())
                         && this.isExpanded(node))
                        )
            {
            if (this.data.isFolder(node) && this.data.isOpen(node)) {
                this.closeFolder(node);
                    return false;
                } else
                {
                // if node is open and has parent, iterate over nodes, until we
                // reach to parent then navigate to that record directly.
                var parent = this.data.getParent(node);
                if (parent) {
                    var row = this.getFocusRow();
                    while(row>0 && this.getRecord(row) != parent) {
                        row--;
                    }

                    // if parent was found, navigate to it
                    if (this.getRecord(row) == parent) {
                        this._navigateToNextRecord(row-this.getFocusRow(),false);
                            return false;
                        }
                    }
                }
            }

        } else if (event.keyName == "Arrow_Right") {
            if (this.data.isFolder(node)) {
                if (!this.data.isOpen(node)) {
                    this.openFolder(node);
                    return false;
                // If we're collapsed, allow left arrow to collapse, before navigating!
                } else if (!this.canExpandRecords ||
                           !(this._canExpandRecord(node,this.getFocusRow())
                             && !this.isExpanded(node))
                            )
                {
                    if (this.data.hasChildren(node)) {
                        this._navigateToNextRecord(1,false);
                        return false;
                    }
                }
            }
        }
    }

    return this.Super("bodyKeyPress", arguments);

},


// fire synthetic context menu events for nodes
_cellContextClick : function (record, recordNum, fieldNum) {

    if (recordNum < 0 || fieldNum < 0) return true; // not in body, allow native context menu

    var isFolder = this.data.isFolder(record);

    // fire synthetic context click events.  Note any of these can cancel further processing by
    // returning an explicit false, which presumably indicates they've shown a context menu
    if (this.nodeContextClick && this.nodeContextClick(this, record, recordNum) == false) {
        return false;
    }
    if (isFolder) {
        if (this.folderContextClick && this.folderContextClick(this, record, recordNum) == false) {
            return false;
        }
    } else {
        if (this.leafContextClick && this.leafContextClick(this, record, recordNum) == false) {
            return false;
        }
    }

    // fire the superclass implementation of this method to fire 'cellContextClick', if defined,
    // and show the default context menu if appropriate
    return this.Super("_cellContextClick", arguments);

},

//>    @method    treeGrid.handleEditCellEvent()
//        @group    event handling
//            Override handleEditCellEvent to not allow editing if the click / doubleClick event
//          occurred over the open area of the treeGrid
//
//        @return    (boolean)    false == cancel further event processing
//<
handleEditCellEvent : function (recordNum, fieldNum) {

    var record = this.getRecord(recordNum);
    // if they're clicking in the open or checkbox area of the list,
    // don't allow editing to proceed
    if (this.clickInOpenArea(record) || this.clickInCheckboxArea(record)) return false;

    // return the results of a call to the superclass method
    return this.Super("handleEditCellEvent",arguments);
},

//>    @method     treeGrid.canEditCell()
// Overridden to disallow editing of the +link{treeNode.name, name} field of this grid's data
// tree. Also disallows editing of the auto-generated tree field, which displays the result
// of +link{method:Tree.getTitle} on the node.
// @return (Boolean) Whether to allow editing this cell
// @visibility external
//<
canEditCell : function (rowNum, colNum) {

    if (this.Super("canEditCell", arguments) == false) return false;


    if (this.getField(colNum)[this.fieldIdProperty] == this.data.nameProperty) return false;


    if (this.getField(colNum)[this.fieldIdProperty] == this._titleField) return false;

    return true;
},

//> @method treeGrid.startEditingNew()
// This inherited +link{listGrid.startEditingNew,ListGrid API} is not supported by the TreeGrid
// since adding a new tree node arbitrarily at the end of the tree is usually not useful.
// Instead, to add a new tree node and begin editing it, use either of these two strategies:
// <ol>
// <li> add a new node to the client-side Tree model via +link{Tree.add()}, then use
// +link{listGrid.startEditing(), TreeGrid.startEditing()} to begin editing this node.  Note that if using a DataSource, when the
// node is saved, an "update" operation will be used since adding a node directly to the
// client-side +link{ResultTree} effectively means a new node has been added server side.
// <li> use +link{DataSource.addData()} to immediately save a new node.  Automatic cache sync
// by the +link{ResultTree} will cause the node to be integrated into the tree.  When the
// callback to addData() fires, locate the new node by matching primary key and call
// +link{listGrid.startEditing(), TreeGrid.startEditing()} to begin editing it.
// </ol>
//
// @group  editing
//
// @param  [newValues] (object)  Optional initial set of properties for the new record
// @param  [suppressFocus] (boolean) Whether to suppress the default behavior of moving focus
//                                   to the newly shown editor.
// @visibility external
//<

// Override the method to determine the widths of the form items displayed while editing to
// account for the tree-field indents
getEditFormItemFieldWidths : function (record) {

    var level = this.data.getLevel(record);
    if (!this.showRoot) level--;
    var openerIconSize = this.getOpenerIconWidth(record),
        indentSize = level * (this.showConnectors ? openerIconSize : this.indentSize)
    ;
    indentSize += this.iconSize + openerIconSize;
    if (this._getCheckboxIcon(record)) {
        indentSize += (this._getCheckboxFieldImageWidth() + this.extraIconGap);
    } else if (this.getExtraIcon(record)) {
        indentSize += (this.iconSize + this.extraIconGap);
    }

    var widths = this.Super("getEditFormItemFieldWidths", arguments),
        treeFieldNum = this.getTreeFieldNum();

    widths[treeFieldNum] -= indentSize;
    return widths;
},

// return the DataSource for the current record, to allow embedded editing
getRecordDataSource : function (record) {
    return this.data.getNodeDataSource(record);
},

//>    @method    treeGrid.rowClick()
//
// This override to +link{ListGrid.rowClick()}.  This implementation calls through to the
// +link{TreeGrid.nodeClick}, +link{TreeGrid.folderClick}, +link{TreeGrid.leafClick} methods, as
// appropriate unless the click was on the expand/collapse control of a folder - in which case
// those callbacks are not fired.
// <p>
// Do not override this method unless you need a rowClick callback that fires even when the
// user clicks on the expand/collapse control.  If you do override this method, be sure to call
// <code>return this.Super("rowClick", arguments);</code> at the end of your override to
// preserver other handler that are called from the superclass (for example,
// +link{ListGrid.recordClick()}.
// <p>
//
//      @param  record      (TreeNode)    record that was clicked on
//        @param    recordNum   (number)    index of the row where the click occurred
//        @param    fieldNum    (number)    index of the col where the click occurred
//
// @see TreeGrid.nodeClick()
// @see TreeGrid.folderClick()
// @see TreeGrid.leafClick()
// @see ListGrid.recordClick()
//
// @visibility external
//<

rowClick : function (record, recordNum, fieldNum) {

    var node = record;

    // if the're clicking in the open or checkbox area of the list,
    //  it's already been processed properly on mouseDown so just bail
    if (this.clickInOpenArea(node) || this.clickInCheckboxArea(node)) return false;

    this._lastRecordClicked = recordNum;
    if (recordNum < 0 || fieldNum < 0) return false; // not in body

    var node = this.getRecord(recordNum),
        isFolder = this.data.isFolder(node);

    if (this.nodeClick) this.nodeClick(this, node, recordNum);

    if (isFolder) {
        if (this.folderClick) this.folderClick(this, node, recordNum);
    } else {
        if (this.leafClick) this.leafClick(this, node, recordNum);
    }

    // execute the super class click method - to pick up field click and recordClick
    // Note: be sure not to call any handlers the ListGrid will call so as not to get a dup
    return this.Super("rowClick",arguments);
},



//>    @method    treeGrid.recordDoubleClick()
//
// Handle a doubleClick on a tree node - override of ListGrid stringMethod of same name.  If
// the node is a folder, this implementation calls +link{TreeGrid.toggleFolder()} on it.  If
// the node is a leaf, calls +link{TreeGrid.openLeaf()} on it.
//
// @see listGrid.recordDoubleClick()
// @visibility external
//<
recordDoubleClick : function (viewer, record, recordNum, field, fieldNum, value, rawValue) {
    // if the're clicking in the open or checkbox area of the list,
    //  it's already been processed properly on mouseDown so just bail
    if (this.clickInOpenArea(record) || this.clickInCheckboxArea(record)) return false;
    // If this is an editable grid, don't toggle the folder, but do return true to allow
    // editing to proceed.
    if (this.isEditable() && this.editEvent == isc.EH.DOUBLE_CLICK &&
        this.canEditCell(recordNum,fieldNum))
    {
        return true;
    }
    if (this.data.isFolder(record)) {
        return this.toggleFolder(record);
    } else
        return this.openLeaf(record);
},

dataChanged : function () {

    this.Super("dataChanged", arguments);


    var folder = this._pendingFolderAnim;
    if (folder && this.data.isOpen(folder) &&
        this.data.getLoadState(folder) == isc.Tree.LOADED)
    {
        this._startFolderAnimation(folder);
        this._pendingFolderAnim = null;
    }
},


//>    @method    treeGrid.openLeaf()   ([A])
// Executed when a leaf node receives a 'doubleClick' event. This handler must be
// specified as a function, whose single parameter is a reference to the relevant leaf node in
// the tree's data.<br>
// See the ListGrid Widget Class for inherited recordClick and recordDoubleClick events.
//
//      @visibility external
//        @param    node        (TreeNode)        node to open
//      @see    class:ListGrid
//<
openLeaf : function (node) {},


// Drag and Drop
// ----------------------------------------------------------------------------------------

//>    @method    treeGrid.transferDragData()
// @include dataBoundComponent.transferDragData()
//<

// ----------------------------------------------------------------------------------
// Customizations of the drag-tracker for tree grids

//> @method treeGrid.getDragTrackerIcon()
// Return an icon to display as a drag tracker when the user drags some node(s).<br>
// Default implementation:<br>
// If multiple nodes are selected and +link{TreeGrid.manyItemsImage} is defined, this
// image will be returned.<br>
// Otherwise returns the result of +link{TreeGrid.getIcon()} for the first node being
// dragged.
// <p>
// Note: Only called if +link{listGrid.dragTrackerMode} is set to <code>"icon"</code>.
// @param records (Array of ListGridRecord) Records being dragged
// @return (string) Image URL of icon to display
// @group dragTracker
// @visibility external
//<
getDragTrackerIcon : function (records) {

    var icon;
    if (records && records.length > 1 && this.manyItemsImage !=null)
        icon = this.manyItemsImage;
    else if (records && records[0]) icon = this.getIcon(records[0], true);
    return icon;
},

// Override getDragTrackerTitle() to just return the icon and title of the row, not
// the indent, opener icon, etc.
// Override not currently documented as it's essentially the same as the superclass
// implementation - we just reformat the title cell value to avoid it showing the
// indent and opener icon.
getDragTrackerTitle : function (record, rowNum, a,b,c,d) {
    var fieldNum = this.getFieldNum(this.getTitleField());
    if (fieldNum != this.getTreeFieldNum())
        return this.invokeSuper(isc.TreeGrid, "getDragTrackerTitle", record, rowNum, a,b,c,d);

    // We need to apply the base (non selected) standard cellStyle/cssText to the drag tracker
    // table.

    var cellStyle = this.getCellStyle(record, rowNum, fieldNum),
        cellCSSText = this.getCellCSSText(record,rowNum,fieldNum);

    if (this.selection.isSelected(record)) {
        var styleIndex = this.body.getCellStyleIndex(record, rowNum, fieldNum),
            standardSelectedStyle = this.body.getCellStyleName(styleIndex, record,
                                                                rowNum, fieldNum);
        if (standardSelectedStyle == cellStyle) {
            styleIndex -= 2;
            cellStyle = this.body.getCellStyleName(styleIndex, record, rowNum, fieldNum);
        }
    }


    // Call the standard ListGrid.getCellValue() method to give us the formatted title
    // of the cell being dragged, excluding the TreeGrid folder/file icons, etc.
    var value = this.invokeSuper(isc.TreeGrid, "getCellValue",  record, rowNum, fieldNum);

    // Now use _getTreeCellTitleArray() to tack on the icon for the node.
    var titleCell = this._getTreeCellTitleArray(
                        value, record, rowNum, fieldNum, false, cellStyle, cellCSSText ).join(isc.emptyString);


    return ["<table class='", cellStyle,
             "' style='", cellCSSText,
             "'><tr>", titleCell, "</tr></table>"].join(isc.emptyString);
},




//>    @method    treeGrid.willAcceptDrop()    (A)
//
// This method overrides +link{ListGrid.willAcceptDrop()} and works as follows:
// <br><br>
// First, +link{ListGrid.willAcceptDrop()} (the superclass definition) is consulted.  If it
// returns false, then this method returns false immediately.<br>
// This handles the following cases:<br>
// - reordering of records withing this TreeGrid when +link{ListGrid.canReorderRecords} is true<br>
// - accepting dropped records from another dragTarget when +link{ListGrid.canAcceptDroppedRecords}
//   is true and the dragTarget gives us a valid set of records to drop into place.<br>
// - disallowing drop over disabled nodes, or nodes with <code>canAcceptDrop:false</code>
// <br>
// This method will also return false if the drop occurred over a leaf node whos immediate
// parent has <code>canAcceptDrop</code> set to <code>false</code><br>
// If +link{TreeGrid.canReparentNodes} is true, and the user is dragging a node from one folder
// to another, this method will return true to allow the change of parent folder.<br>
// <br><br>
// Otherwise this method returns true.
//
// @group event handling
// @return    (boolean)    true if this component will accept a drop of the dragData
//
// @visibility external
//<
willAcceptDrop : function () {
    // Bail if Superclass willAcceptDrop fails
    // (Checks that the record is enabled, etc.)
    if (!this.Super("willAcceptDrop",arguments)) return false;

    isc._useBoxShortcut = true;
    // get the record being dropped on
    var recordNum = this.getEventRecordNum(null, true),
        newParent = recordNum < 0 ? null : this.data.get(recordNum);
    isc._useBoxShortcut = false;

    // dropping in the body in open space means add to root
    if (newParent == null) {
        if (this.canDropInEmptyArea == false || !this.canDropRootNodes) return false;
        newParent = this.data.getRoot();
    }

    // if we can't get the new parent, or it can't accept drops, return false
    if (!newParent || newParent.canAcceptDrop == false) return false;

    // don't allow drop over non-folder nodes, unless we're allowing record reordering or
    // canDropOnLeaves is set
    var isFolder = this.data.isFolder(newParent);
    if (!isFolder && !(this.canReorderRecords || this.canDropOnLeaves)) return false;

    // check for dropErrors (dropping record over self, etc.)
    var moveList = isc.EH.dragTarget.getDragData();
    if (!isc.isAn.Object(moveList) || this.getDropError(moveList, newParent) != null) {
        return false
    }
    // Even if we are allowing record reordering, don't allow the user to drop into a
    // parent with canAcceptDrop explicitly set to false

    if (!isFolder) {
        newParent = this.data.getParent(newParent);
        if (newParent.canAcceptDrop == false) return false;
    }

    // If we're dragging data in from another listGrid we're done here
    // (this relies on canAcceptDropRecords getting checked by the superClass implementation
    // for this case).
    if (isc.EH.dragTarget != this) return true;

    // If we can reorder records, but not reparent, we need to catch the cases where
    // - records selected currently come from multiple folders
    // - the drop folder doesn't match the source folder for the node[s]
    var canReparent = this.canReparentNodes;
    //>!BackCompat 2006.06.27
    if (canReparent == null && this.canAcceptDroppedRecords) canReparent = true;
    //<!BackCompat

    if (!canReparent) {
        if (!isc.isAn.Array(moveList)) moveList = [moveList];
        var currentParent;
        currentParent = this.data.getParent(moveList[0]);

        if (currentParent != newParent) return false;

        for (var i = 1; i < moveList.length; i++) {
            if (currentParent != this.data.getParent(moveList[i])) return false;
        }
    }

    // if we get here, it should be OK!
    return true;
},

// Override setUpDragProperties to pick up this.canReparentNodes
_setUpDragProperties : function () {

    // set up our specific drag-and-drop properties
    this.canDrag = (this.canDrag || this.canDragRecordsOut ||
                    this._canDragRecordsToSelf() || this.canDragSelect);
    this.canDrop = (this.canDrop || this.canDragRecordsOut || this._canDragRecordsToSelf());
    this.canAcceptDrop = (this.canAcceptDrop || this.canAcceptDroppedRecords ||
                            this._canDragRecordsToSelf());
},


// allow the user to drag records to self if they can be reordered or reparented
_canDragRecordsToSelf : function () {
    var canReparentNodes = this.canReparentNodes;
    //>!BackCompat 2006.06.27
    if (canReparentNodes == null && this.canAcceptDroppedRecords) {
        if (!this._canReparentBackcompatNotified) {
            this.logInfo("'canReparentNodes' is unset. Allowing node reparenting as " +
                         "'canAcceptDroppedRecords' is set to true. For explicit control, " +
                         "use 'canReparentNodes' instead.", "dragDrop");
            this._canReparentBackcompatNotified = true;
        }
        canReparentNodes = this.canAcceptDroppedRecords;
    }
    //<!BackCompat

    return this.canReorderRecords || canReparentNodes;
},

// if there's a problem that makes this drop invalid, return an error string to display
getDropError : function (moveList, newParent) {

    // don't allow a parent to be dropped on it's own descendant
    for (var i = 0, length = moveList.length; i < length; i++) {
        if (this.data.isDescendantOf(newParent, moveList[i])) {
            return this.cantDragIntoChildMessage;
        }
    }

    // make sure they're not trying to drag a folder into itself
    var isFolder = this.data.isFolder(newParent);
    if (isFolder) {
        for (i = 0; i < length; i++) {
            if (moveList[i] == newParent) {
                return this.cantDragIntoSelfMessage;
            }
        }
    }

    return null;
},

//>    @method    treeGrid.dropMove()    (A)
//            mouse is moving over the list while dragging is happening
//            show a hilite in the appropriate record if necessary
//        @group    event handling
//
//        @return    (boolean)    false == cancel further event processing
//<
dropMove : function () {
    var eventRow = this.getEventRow();
    // before the beginning of the rendered area, aka over the header; do nothing
    if (eventRow == -1) return false;

    // bail on drops over foreign widgets if not configured to drag records out
    if (this.canDragRecordsOut == false && isc.EH.dropTarget != this && !this.contains(isc.EH.dropTarget)) return false;

    // bail on drops from foreign widgets if not configured to accept foreign drops
    if (!this.canAcceptDroppedRecords && isc.EH.dragTarget != this) {
        return false;
    }

    // if after the end of the list, choose root
    var eventNode = (eventRow == -2 ? this.data.getRoot() : this.data.get(eventRow)),
        dropFolder = this.getDropFolder(),
        position = (this.canReorderRecords ? this.getRecordDropPosition(eventRow) : null);

    // We used to check willAcceptDrop() here, but that prevented spring-loaded folders
    // from working in the case where the folder being hovered over is will not accept the
    // drop, but one of its children might accept the drop.  So now, we always set the
    // timer to open a folder being hovered on and updateDropFolder() logic checks for
    // willAcceptDrop().

    // suppress duplicate runs, but updateDropFolder() whenever the lastDropFolder, eventNode
    // or lastPosition have changed because event though we may still be within the same
    // dropFolder, we may want to change the dropFolder icon state based on whether the tree
    // willAcceptDrop() at the new location.
    if (dropFolder != this.lastDropFolder ||
        eventNode != this._lastEventNode || position != this._lastPosition) {

        // Set up a function to be executed in the global scope to open the drop folder.
        if (!this._openDropFolder) {
            this._openDropFolder = this.getID() + ".openDropFolder()";
        }

        // If there's a running openDropFolderTimer, clear it
        if (this.openDropFolderTimer) isc.Timer.clear(this.openDropFolderTimer);

        // If the dropFolder is closed, set up a new openDropFolderTimer
        if (!this.data.isOpen(dropFolder)) {
            this.openDropFolderTimer =
                            isc.Timer.setTimeout(this._openDropFolder, this.openDropFolderDelay);
        }

        // remember the new drop-folder as this.lastDropFolder, and update its icon.
        // [note this calls 'willAcceptDrop()']
        this.updateDropFolder(dropFolder);
    }

    // If the drop is disallowed, show the 'no drop' cursor

    if (!this.willAcceptDrop()) {
        this.body.setNoDropIndicator();
    } else {
        this.body.clearNoDropIndicator();
    }

    // Show the drag line if appropriate
    if (this.shouldShowDragLineForRecord(dropFolder)) {
        if (this.data.isOpen(dropFolder)) this.showDragLineForRecord(eventRow, position);
        else this.hideDragLine();
    }
    this._lastEventNode = eventNode;
    this._lastPosition = position;


},

//>    @method    treeGrid.getEventRow()
// @include gridRenderer.getEventRow()
// @group events
// @visibility external
//<


getEventRecordNum : function (y, allowRootNodeRemapping) {
    var recordNum = this.Super("getEventRecordNum", arguments);
    if (recordNum < 0 && allowRootNodeRemapping && !this.canDropRootNodes) {
        var data = this.data,
            children = data.getChildren();
        if (children != null) {
            if (recordNum == -2) {
                for (var i = children.length - 1; i >= 0; i--) {
                    if (data.isFolder(children[i])) return this.getRecordIndex(children[i]);
                }
            } else {
                for (var i = 0; i < children.length; i++) {
                    if (data.isFolder(children[i])) return this.getRecordIndex(children[i]);
                }
            }
        }
    }
    return recordNum;
},

//> @attr treeGrid.recordDropAppearance (RecordDropAppearance : isc.ListGrid.BOTH : [IRW])
// If +link{canAcceptDroppedRecords} is true for this treeGrid, this property governs
// whether the user can drop between, or over records within the grid.
// This controls what +link{type:RecordDropPosition} is passed to the +link{recordDrop()}
// event handler.
//
// @visibility external
//<
recordDropAppearance: isc.ListGrid.BOTH,

//> @method treeGrid.getDropFolder()
// When the user is dragging a droppable element over this grid, this method returns the folder
// which would contain the item if dropped. This is the current drop node if the user is hovering
// over a folder, or the node's parent if the user is hovering over a leaf.
// @group events
// @return (node) target drop folder
// @visibility external
//<
getDropFolder : function () {

    var eventRow = this.getEventRecordNum(null, true),
        data = this.data,
        // before the beginning of the list (over header), or after the end, use root
        eventNode = (eventRow < 0 ? data.getRoot() : data.get(eventRow));

    // if we're over the root, we're going to drop into the root (no choice)
    if (data.isRoot(eventNode)) return data.getRoot();

    var isFolder = data.isFolder(eventNode);

    // if we can't reorder records, it's easy
    if (!this.canReorderRecords) return (isFolder ? eventNode : data.getParent(eventNode));

    var position = this.getRecordDropPosition(eventNode);

    // If we're over a leaf (anywhere), or
    // we're over the "before" or "after" part (top / bottom 1/4) of any folder, or
    // we're over the "after" part (bottom 1/4) of a closed or empty folder, return the
    // parent of the node,
    // except don't return the parent of a folder if the parent is the root node and
    // canDropRootNodes is false; return the node itself in that case.
    if (!isFolder || position == isc.ListGrid.BEFORE || position == isc.ListGrid.AFTER &&
        (!data.isOpen(eventNode) || !data.hasChildren(eventNode)))
    {
        var parent = data.getParent(eventNode);
        return isFolder && !this.canDropRootNodes && data.isRoot(parent) ? eventNode : parent;
    } else {
        // In this case we're either over the "over" position of a closed folder, or the
        // "below" position for an open folder.  In either case we'll want to drop into this
        // folder, before the first child
        return eventNode;
    }

},

//>    @method    treeGrid.openDropFolder()    (A)
// Method to open the folder we're currently hovering over (about to drop)
// Called on a timer set up by this.dropMove
//        @group    event handling
//<
openDropFolder : function () {
    var dropFolder = this.lastDropFolder;

    // if we're not over a closed folder, bail!
    if (!dropFolder ||
        !this.data.isFolder(dropFolder) ||
        this.data.isOpen(dropFolder))          return false;

    // Open the folder
    this.openFolder(dropFolder);
    // show the drag line if we can reorder
    if (this.shouldShowDragLineForRecord(dropFolder)) {
        this.showDragLineForRecord(this.data.indexOf(dropFolder), isc.ListGrid.OVER)
    }

},

getRecordDropPosition : function (recordNum, y, dropAppearance) {
    if (this.recordDropAppearance == isc.ListGrid.OVER) return isc.ListGrid.OVER;
    if (this.recordDropAppearance == isc.ListGrid.BODY) return null;

    // If a y-coordinate was not passed, get it from the offset of the last event
    if (y == null) y = this.body.getOffsetY();

    // which row is the mouse over?
    if (recordNum == null) recordNum = this.getEventRow(y);

    var data = this.data;
    if (!isc.isA.Number(recordNum)) recordNum = data.indexOf(recordNum);

    var record = recordNum < 0 ? null : data.get(recordNum);

    if (record && data.isFolder(record)) {
        return this._getRecordDropPosition(recordNum, y, dropAppearance);
    }

    // if no recordDropAppearance override was supplied, force it to BETWEEN
    // unless +link{TreeGrid.canDropOnLeaves} has been set to true
    if (dropAppearance == null && !this.canDropOnLeaves) {
        dropAppearance = isc.ListGrid.BETWEEN;
    }

    // If we're over a leaf, allow the super method to take care of it.
    return this.invokeSuper(isc.TreeGrid, "getRecordDropPosition", recordNum, y,
                            dropAppearance);
},

// Override showDragLineFor record - if the drop will occur inside a folder, we'll show the
// drag line after the folder (before the first child)
showDragLineForRecord : function (recordNum, position, a,b,c) {

    if (recordNum == null) recordNum = this.getEventRecordNum();
    if (position == null) position = this.getRecordDropPosition(recordNum);

    // If dropping over an open folder, show the drag line before the first child (after the
    // folder)
    if (position == isc.ListGrid.OVER) {
        var node = this.getRecord(recordNum),
            data = this.data;
        if (data.isFolder(node) && data.isOpen(node)) position = isc.ListGrid.AFTER;
    }

    // Have the default implementation actually show the drag line.
    return this.invokeSuper(isc.TreeGrid, "showDragLineForRecord", recordNum, position, a,b,c);
},


//>    @method    treeGrid.dropOut()    (A)
//            mouse just moved out of the range of the list while dragging is going on
//            remove the hilite
//        @group    event handling
//
//        @return    (boolean)    false == cancel further event processing
//<
dropOut : function () {
    // Hide drag line
    this.hideDragLine();
    // clear no-drop indicator
    this.body.clearNoDropIndicator();

    // Clear any remembered drop folder
    this._lastEventNode = null;
    this.updateDropFolder();

    // If we have a timer waiting to open a drop folder, clear it
    // (Note - if it did fire it would bail anyway because lastDropMoveRow has gone, but
    // this is more efficient)
    if (this.openDropFolderTimer) isc.Timer.clear(this.openDropFolderTimer);

},

//>    @method    treeGrid.updateDropFolder()    (A)
// Takes a record (or record index) as a parameter
// Applies the folderDropImage icon to the parameter (or parent folder, if passed a leaf)
// Clears out any folderDropImage applied to another folder.
// Remembers the folder passed in as this.lastDropFolder.
//        @group    drawing, event handling
//
//        @param newFolder (object or index)
//<
updateDropFolder : function (newFolder) {

    var LDF = this.lastDropFolder;
    this.lastDropFolder = newFolder;

    // Set the icons on both the previous and current drop folder
    //
    // Special _willAcceptDrop flag: set for getIcon() and only update to drop state if the
    // body willAcceptDrop() the new folder - see comments in dropMove()
    if (newFolder) {
        newFolder._willAcceptDrop = this.body.willAcceptDrop(newFolder)
        this.setRowIcon(newFolder, this.getIcon(newFolder));
    }

    if (LDF && LDF != newFolder) {
        delete LDF._willAcceptDrop;
        this.setRowIcon(LDF, this.getIcon(LDF));
    }
},


//> @method treeGrid.transferSelectedData()
// Simulates a drag / drop type transfer of the selected records in some other grid to this
// treeGrid, without requiring any user interaction.<br>
// See the +link{group:dragging} documentation for an overview of grid drag/drop data
// transfer.
// @param sourceGrid (ListGrid) source grid from which the records will be transferred
// @param [folder] (TreeNode) parent node into which records should be dropped - if null
//    records will be transferred as children of the root node.
// @param [index] (integer) target index (drop position) within the parent folder
// @param [callback] (Callback) optional callback to be fired when the transfer process has
//                       completed.  The callback will be passed a single parameter "records",
//                       the list of nodes actually transferred to this component (it is called
//                       "records" because this logic is shared with +link{class:ListGrid}).
// @group dragging
// @example dragTree
// @visibility external
//<
transferSelectedData : function (source, folder, index, callback) {

    if (!this.isValidTransferSource(source)) {
        if (callback) this.fireCallback(callback);
        return;
    }

    // don't check willAcceptDrop() this is essentially a parallel mechanism, so the developer
    // shouldn't have to set that property directly.
    if (index == null) index = 0;
    if (folder == null) folder = this.data.getRoot();

    // Call cloneDragData() to pull the records out of the source's dataSet
    // Note we don't need to call 'transferDragData' here - that is all handled after
    // transferNodes now, potentially by a server callback

    var nodes = source.cloneDragData();
    this.transferNodes(nodes, folder, index, source, callback);
},

// Insert after last child if we're not allowed to drop a new root node, drop occurs directly
// on a folder that's open and is a child of the root node, and the drop position is "after".
_dropAfterLastChild : function (position, dropItem, newParent) {
    if (this.canDropRootNodes) return false;
    if (dropItem != newParent) return false;

    var data = this.data,
        parent = data.getParent(dropItem);
    return data.isRoot(parent) && data.isOpen(dropItem) && position == isc.ListGrid.AFTER;
},

//>    @method    treeGrid.drop()    (A)
//        @group    event handling
//            handle a drop in the list
//            if possible, move or copy the items automatically
//            NOTE: at this point, we should be assured that we can accept whatever was dragged...
//        @return    (boolean)    false == cancel further event processing
//<
drop : function () {
    if (!this.willAcceptDrop()) return false;

    // NOTE: we perform some redundant checks with willAcceptDrop(), but this is not a time
    // critical method, and the errors being checked for would corrupt the Tree and so should
    // never be allowed, so it makes sense to check them here as well since willAcceptDrop()
    // might be incorrectly overidden.

    // get what was dropped and where it was dropped
    var moveList = isc.EH.dragTarget.cloneDragData(),
        recordNum = this.getEventRecordNum(null, true),
        position = this.getRecordDropPosition(recordNum),
        dropItem = recordNum < 0 ? null : this.data.get(recordNum),
        newParent = this.getDropFolder();

    // dropping in the body in open space means add to root
    if (!dropItem) dropItem = this.data.getRoot();

    //this.logWarn("valid drop with parent: " + this.echo(newParent));

    // figure out if this is a drag within the same Tree data model.  This can happen within the
    // same TreeGrid or across two TreeGrids.
    var dragTree = isc.EH.dragTarget.getData(),
        dragWithinTree = ( isc.isA.Tree(dragTree) &&
                           isc.isA.Tree(this.data) &&
                           dragTree.getRoot() == this.data.getRoot() );
    // make sure that they're not trying to drag into parent containing child with same name.
    // NOTE: this particular check is postponed until drop() because it's not self-evident why
    // the widget won't accept drop, so we want to warn() the user

    for (var i = 0; i < moveList.length; i++) {

        var child = moveList[i];

        // NOTE: If dragging in from another tree - set dragDataAction to "copy" to test the
        // code below, otherwise you end up with 2 trees pointing at the same object

        // name collision: see if there's already a child under the newParent that has the same
        // name as the child we're trying to put under that parent
        var collision = (this.data.findChildNum(newParent, this.data.getName(child)) != -1);

        // this collision is not a problem if we're reordering under the same parent
        var legalReorder = dragWithinTree && this.canReorderRecords &&
                            newParent == this.data.getParent(child);
        if (collision && !legalReorder) {
            this.logInfo("already a child named: " + this.data.getName(child) +
                         " under parent: " + this.data.getPath(newParent));
            isc.warn(this.parentAlreadyContainsChildMessage);
            return false;
        }
    }

    // At this point, everything looks OK and we are accepting the drop

    // figure out where the dropped should be placed in the parent's children
    var index = null;
    if (this.canReorderRecords) {
        if (recordNum < 0 || this._dropAfterLastChild(position, dropItem, newParent)) {
            // already set dropItem to root
            newParent = dropItem;
            // special case: dropped in empty area of body, make last child of root
            index = this.data.getChildren(newParent).getLength();
        } else if (dropItem == newParent) {
            // if dropped directly on a folder, place at beginning of children
            index = 0;
        } else {
            // otherwise place before or after leaf's index within parent
            index = (position == isc.ListGrid.BEFORE ? 0 : 1) +
                        this.data.getChildren(newParent).indexOf(dropItem);
        }
    }

    var dropPosition = position;
    // if onFolderDrop exists - allow it to cancel the drop

    if (this.onFolderDrop != null &&
        (this.onFolderDrop(moveList,newParent,index,dropPosition,isc.EH.dragTarget) == false)) return false;

    this.folderDrop(moveList, newParent, index, isc.EH.dragTarget);

    // open the folder the nodes were dropped into
    this.data.openFolder(newParent);

    // return false to cancel further event processing
    return false;
},

//> @method treeGrid.recordDrop()
// The superclass event +link{listGrid.recordDrop} does not fire on a TreeGrid, use
// +link{folderDrop} instead.
//
// @visibility external
//<

//> @method treeGrid.folderDrop() [A]
//
// Process a drop of one or more nodes on a TreeGrid folder.
// <P>
// This method can be overridden to provide custom drop behaviors and is a more appropriate
// override point than the lower level +link{Canvas.drop()} handler.
// <P>
// If this is a self-drop, nodes are simply reordered. An "update" operation will
// be submitted to update the +link{tree.parentIdField,parentId} field of the moved node(s).
// <P>
// For a drop from another widget, +link{treeGrid.transferDragData()} is called which,
// depending on the +link{TreeGrid.dragDataAction,dragDataAction} specified on the source
// widget, may either remove the source nodes from the original list (<code>dragDataAction:"move"</code>)
// or just provide a copy to this tree (<code>dragDataAction:"copy"</code>).
// <P>
// In either case the new row(s) appear in the <code>folder</code> at the <code>index</code>
// specified by the arguments of the same name.
// <P>
// If this grid is databound, the new nodes will be added to the dataset by calling
// +link{dataSource.addData()}.  Further, if the new nodes were dragged from another
// databound component, and +link{DataBoundComponent.addDropValues,addDropValues}
// is true, +link{DataBoundComponent.getDropValues,getDropValues} will be called for every item
// being dropped.
// <P>
// As a special case, if the <code>sourceWidget</code> is also databound and a
// +link{dataSourceField.foreignKey,foreignKey} relationship is declared from the
// <code>sourceWidget</code>'s DataSource to this TreeGrid's DataSource, the interaction will
// be treated as a "drag recategorization" use case such as files being placed in folders,
// employees being assigned to teams, etc.  "update" DSRequests will be submitted that
// change the foreignKey field in the dropped records to point to the tree folder that was the
// target of the drop.  In this case no change will be made to the Tree data as such, only to
// the dropped records.
// <P>
// For multi-record drops, Queuing is automatically used to combine all DSRequests into a
// single HTTP Request (see QuickStart Guide, Server Framework chapter).  This allows the
// server to persist all changes caused by the drop in a single transaction (and this is
// automatically done when using the built-in server DataSources with Power Edition and
// above).
// <P>
// If these default persistence behaviors are undesirable, return false to cancel them, then
// and implement your own behavior, typically by using grid.updateData() or addData() to add
// new records.
// <p><b>NOTE:</b> the records you receive in this event are the actual Records from the source
// component.  Use +link{DataSource.copyRecords()} to create a copy before modifying the records
// or using them with updateData() or addData().
//
// @param nodes (Array of TreeNode) List of nodes being dropped
// @param folder (TreeNode) The folder being dropped on
// @param index (int) Within the folder being dropped on, the index at which the drop is
//                        occurring.  Only passed if +link{canReorderRecords} is true.
// @param sourceWidget (Canvas) The component that is the source of the nodes (where the nodes
//                              were dragged from).
// @param [callback] (Callback) optional callback to be fired when the transfer process has
//                       completed.  The callback will be passed a single parameter "records",
//                       the list of nodes actually transferred to this component (it is called
//                       "records" because this is logic shared with +link{class:ListGrid})
//
// @visibility external
// @example treeDropEvents
//<
folderDrop : function (nodes, folder, index, sourceWidget, callback) {
    this.transferNodes(nodes, folder, index, sourceWidget, callback);

},


//> @method treeGrid.transferNodes() [A]
//
// Transfer a list of +link{TreeNode}s from another component (does not have to be a databound
// component) into this component.  This method is only applicable to list-type components,
// such as +link{ListGrid,listGrid}, +link{TreeGrid,treeGrid} or +link{TileGrid,tileGrid}.
// <P>
// This method implements the automatic drag-copy and drag-move behavior and calling it is
// equivalent to completing a drag and drop of the <code>nodes</code>.
// <P>
// Note that this method is asynchronous - it may need to perform server turnarounds to prevent
// duplicates in the target component's data.  If you wish to be notified when the transfer
// process has completed, you can either pass the optional callback to this method or implement
// the +link{dataBoundComponent.dropComplete()} method on this component.
// <P>
// See also +link{transferSelectedData}.
//
// @param nodes (Array of TreeNode) Nodes to transfer to this component
// @param folder (TreeNode) The target folder (eg, of a drop interaction), for context
// @param index (integer) Insert point within the target folder data for the transferred nodes
// @param sourceWidget (Canvas) The databound or non-databound component from which the nodes
//                              are to be transferred.
// @param [callback] (Callback) optional callback to be fired when the transfer process has
//                       completed.  The callback will be passed a single parameter "records",
//                       the list of nodes actually transferred to this component (it is called
//                       "records" because this is logic shared with +link{class:ListGrid})
//
// @visibility external
// @example treeDropEvents
//<
transferNodes : function (nodes, folder, index, sourceWidget, callback) {

    // storeTransferState returns false if a prior transfer is still running, in which case
    // we just bail out (transferNodes() will be called again when the first transfer
    // completes, so we aren't abandoning this transfer, just postponing it)
    if (!this._storeTransferState("transferNodes", nodes, folder, index,
                                  sourceWidget, callback)) {
        return;
    }

    // If parent folder is null, we're dropping into the TreeGrid body, which implies root
    folder = folder || this.data.root;

    // figure out if this is a drag within the same Tree (even if from another TreeGrid)
    var dragTree = sourceWidget.getData(),
        dragWithinTree = ( isc.isA.Tree(dragTree) &&
                           isc.isA.Tree(this.data) &&
                           dragTree.getRoot() == this.data.getRoot() );
    // if we're dropping an item from one tree to another that both share the same root, perform a
    // move instead.  Note that this ignores dragType (eg clone vs copy) completely.
    var dataSource = this.getDataSource(),
        sourceDS = sourceWidget.getDataSource();
    if (dragWithinTree && (this.dragDataAction != isc.TreeGrid.COPY &&
                           this.dragDataAction != isc.TreeGrid.CLONE))
    {
        if (dataSource != null && this.data != null &&
            isc.ResultTree && isc.isA.ResultTree(this.data))
        {
            this._dropRecords[0].noRemove = true;
            var wasAlreadyQueuing = isc.rpc.startQueue();

            // NOTE: We are possibly going to do some client-side reordering here.  Depending
            // on whether we're moving nodes forwards or backwards within their siblings, or
            // neither (if we're reparenting) or both (if we have multiple selected), we'll be
            // changing which index within the parent is the correct one to insert at.  Thus
            // we'll establish upfront which is the correct sibling node to insert before, and
            // always the actual index by reference to that node's current location as the
            // loop progresses
            var currentChildren = dragTree.getChildren(folder);
            var insertBeforeNode, undef;
            if (index != null) {
                if (index < currentChildren.getLength()) {
                    insertBeforeNode = currentChildren.get(index);
                }
            }
            if (insertBeforeNode == undef) {
                insertBeforeNode = currentChildren.last();
            }

            var loadingMarker = isc.ResultSet.getLoadingMarker();
            for (var i = 0; i < nodes.length; i++) {
                var node = nodes[i];
                if (this.shouldSaveLocally() ||
                        node[this.data.parentIdField] == folder[this.data.idField])
                {
                    // The user has dragged a node to a different location within the the same
                    // parent.  This change cannot be automatically persisted, so we'll just
                    // reflect the change locally so it doesn't appear to the user that nothing
                    // has happened (though, in fact, nothing *has* happened - some kind of
                    // index update on the underlying persistent store needs to be performed in
                    // order for a user interaction of this type to persist beyond the current
                    // UI session).
                    // If index is null, it's unclear what we should do.  We could either leave
                    // the node where it is, or move it to the end of the list (as we would if
                    // we were adding to the parent).  This may change, but right now we just
                    // leave it where it is
                    if (index != null) {
                        currentChildren = dragTree.getChildren(folder);
                        dragTree.move(node, folder, currentChildren.indexOf(insertBeforeNode));
                    }
                } else {

                    // NOTE: getCleanNodeData() scrubs off the isOpen flag if it was auto-
                    // generated, but we need to hang onto it, otherwise dragging an open
                    // folder from one parent to another causes it to snap shut.
                    var saveIsOpenFlag = nodes[i]["_isOpen_" + this.data.ID];
                    var node = isc.addProperties({}, this.data.getCleanNodeData(nodes[i], true, false)),
                        oldValues = isc.addProperties({}, node);
                    if (saveIsOpenFlag != null) node["_isOpen_" + this.data.ID] = saveIsOpenFlag;
                    node[this.data.parentIdField] = folder[this.data.idField];
                    var dropNeighbor = null,
                        children = this.data.getChildren(folder);
                    if (index == null) {
                        dropNeighbor = children.last();
                        if (dropNeighbor == loadingMarker) {
                            dropNeighbor = null;
                        }
                    } else if (index > 0) {
                        dropNeighbor = children.get(index - 1);
                        if (dropNeighbor == loadingMarker) {
                            dropNeighbor = null;
                        }
                    }

                    // We pass a number of parameters relating to this drop up to the server,
                    // so that they are available in the callback.  This allows us to give
                    // the impression that a drop has taken place at a particular position
                    // within the parent.  This isn't what has actually happened - see the
                    // above comment about dragging nodes to different locations within the
                    // same parent in a databound TreeGrid.
                    this.updateDataViaDataSource(node, dataSource, {
                        oldValues : oldValues,
                        parentNode : this.data.getParent(nodes[i]),
                        newParentNode : folder,
                        dragTree : dragTree,
                        draggedNode : node,
                        draggedNodeList: nodes,
                        dropNeighbor: dropNeighbor,
                        dropIndex : index
                    }, sourceWidget);
                }
            }

        } else {
            // move the nodes within the tree
              dragTree.moveList(nodes, folder, index);
        }
    } else if (dataSource != null) {
         var canRecat;
        if (this.dragRecategorize == "always" || this.dragRecategorize != "never" &&
            (sourceDS != null && sourceDS != dataSource && this.data != null &&
             isc.ResultTree && isc.isA.ResultTree(this.data) &&
             sourceWidget.dragDataAction == isc.TreeGrid.MOVE))
        {
            // check for a foreign key relationship between some field in the source DS to some
            // field in the treeGrid DS
            var relationship = sourceDS.getTreeRelationship(dataSource);

            if (relationship != null && relationship.parentIdField) {
                var cannotRecat = false,
                    pkFields = sourceDS.getPrimaryKeyFields();

                // If the detected foreignKeyField is a Primary Key, we can't modify it.
                // Catch this case and log a warning

                for (var pk in pkFields) {
                    if (pk == relationship.parentIdField) {
                        this.logWarn("dragRecategorize: data source has dataSource:"
                                    + sourceDS.getID() + ". foreignKey relationship with " +
                                    "target dataSource " + dataSource.getID() +
                                    " is based on primary key which cannot be modified.");
                        cannotRecat = true;
                    }
                }
                if (!cannotRecat) canRecat = true;
                //>DEBUG
                this.logInfo("Recategorizing dropped nodes in dataSource:" + sourceDS.getID());
                //<DEBUG
            }

            // Remember that we performed updates rather than adds, so we don't remove records
            // later on in transferDragData()
            this._dropRecords[0].noRemove = true;

            var wasAlreadyQueuing = isc.rpc.startQueue();
            for (var i = 0; i < nodes.length; i++) {
                var node = {};
                var pks = sourceDS.getPrimaryKeyFieldNames();
                for (var j = 0; j < pks.length; j++) {
                    node[pks[j]] = nodes[i][pks[j]];
                }
                if (canRecat) {
                    node[relationship.parentIdField] = folder[relationship.idField];
                }
                isc.addProperties(node,
                    this.getDropValues(node, sourceDS, folder, index, sourceWidget));

                this.updateDataViaDataSource(node, sourceDS, null, sourceWidget);
            }
        } else {


            if (isc.isA.Tree(dragTree) && sourceWidget.dragDataAction == isc.TreeGrid.MOVE) {
                nodes = dragTree.getCleanNodeData(nodes, sourceWidget.dataSource == null);
            }



            var wasAlreadyQueuing = isc.rpc.startQueue();
            for (var i = 0; i < nodes.length; i++) {
                var data = nodes[i],
                    resultTree = this.data;
                if (resultTree) {
                    data[resultTree.parentIdField] = folder[resultTree.idField];
                }
                isc.addProperties(data,
                    this.getDropValues(data, sourceDS, folder, index, sourceWidget));

                this._addIfNotDuplicate(data, sourceDS, sourceWidget, null, index, folder);
            }
        }
    } else {
        // add the dropped nodes to the tree at the specified point - they could be rows from a
        // ListGrid, or anything - it's up to the developer to have it make sense
        //this.logWarn("adding dragData at parent: " + newParent + ", position: " + position);
        for (var i = 0; i < nodes.length; i++) {
            this._addIfNotDuplicate(nodes[i], sourceDS, sourceWidget, null, index, folder);
        }
    }

    // If this._transferDuplicateQuery is undefined or 0,we didn't need to fire any server
    // queries, so we can call transferDragData to complete the transfer and send the queue
    // of updates to the server
    if (!this._transferDuplicateQuery) {
        isc.Log.logDebug("Invoking transferDragData from inside transferNodes - no server " +
                         "queries needed?", "dragDrop");
        sourceWidget.transferDragData(this._transferExceptionList, this);
        if (dataSource) {
            // send the queue unless we didn't initiate queuing
            if (!this._wasAlreadyQueuing) isc.rpc.sendQueue();
        }
    }

    this._transferringRecords = false;

},

// NOTE: Overrides (but invokes) the DBC version
_updateComplete : function (dsResponse, data, dsRequest) {
    if (!dsRequest.dragTree) return;

    // If the node we're dropping into is not in the tree (ie, it is neither the root node
    // nor the child of another node), warn the user and bail
    if (dsRequest.newParentNode != this.data.root &&
        dsRequest.dragTree.getParent(dsRequest.newParentNode) == null)
    {
        isc.logWarn("Target folder is no longer in the Tree in TreeGrid cache sync");
        return;
    }

    var neighbor = dsRequest.dropNeighbor,
        dragTree = dsRequest.dragTree,
        siblings = dragTree.getChildren(dsRequest.newParentNode),
        nodeList = dsRequest.draggedNodeList,
        idField = dragTree.idField,
        nodePosition = nodeList.findIndex(idField, dsRequest.draggedNode[idField]),
        index, undef;
    if (neighbor == null) {
        index = 0;
    } else {
        for (var i = 0, siblingsLength = siblings.getLength(); i < siblingsLength; ++i) {
            var sibling = siblings.getCachedRow(i);
            if (sibling == neighbor) {
                index = i + 1;
                break;
            }
        }
    }

    if (index !== undef) {
        // Step the insert point forward to ensure that nodes are inserted in the same order
        // they were passed to folderDrop.  This is necessary because some of the nodes may
        // have already been moved into position synchronously (if we had a multi-node drag
        // where the dragged nodes came from several parents)
        //isc.logWarn("nodeList: " + nodeList.getProperty("Name"));
        var siblingsLength = siblings.getLength();
        while (index < siblingsLength) {
            var sibling = siblings.getCachedRow(index);
            if (sibling != null) {
                //isc.logWarn("existing node: " + sibling.Name);
                var existingIndex = nodeList.findIndex(idField, sibling[idField]);
                //isc.logWarn("existingIndex: " + existingIndex + ", nodePosition: " + nodePosition);
                if (existingIndex == -1 || existingIndex > nodePosition) break;
            }
            index++;
        }
    }

    if (index === undef) {
        isc.logWarn("Could not order dropped node by reference to neighbor; trying absolute index");
        index = dsRequest.dropIndex;
    }

    // If index is still undefined, something's gone wrong.  Log a warning and bail
    if (index === undef) {
        isc.logWarn("Unable to determine drop location in TreeGrid cache sync");
        return;
    }

//    dragTree.move(dsRequest.draggedNode, dsRequest.newParentNode, index);
    var nodeToMove = this.data.find(idField, dsRequest.draggedNode[idField]);
    dragTree.move(nodeToMove, this.data.getParent(nodeToMove), index);

    this.Super("_updateComplete", arguments);
},


// Tree-specific HTML generation
// --------------------------------------------------------------------------------------------

//>    @method    treeGrid.getTreeCellValue()
//            Returns the HTML to display a cell with
//          <ul>
//          <li>Indentation</li>
//          <li>Open / Close Icon (folders only)</li>
//          <li>Optional extra icon</li>
//          <li>Folder / Node Icon</li>
//          <li>Value for the cell</li>
//          </ul>
//            OVERRIDE in your subclass for a more complicated presentation
//
//      @param  value           (string)    value to display in the cell
//        @param    record            (TreeNode)    tree node in question
//        @param    recordNum        (number)    number of that tree node
//      @param  fieldNum        (number)    number of the field being output as treeField
//
//        @return    (HTML)    HTML output for the cell
//      @visibility internal
//<
// iconPadding - padding between the folder open/close icon and text.
// Make this customizable, but not exposed - very unlikely to be modified
iconPadding:3,
_$closeTreeCellTable:"</tr></tbody></table>",
_$semi:";",
getTreeCellValue : function (value, record, recordNum, fieldNum, gridBody) {

    // This returns HTML to achieve
    //  - an indent equal to what level of the tree you're viewing
    //  - open / close icon
    //  - an optional additional icon
    //  - Folder / Record icon
    //  - title for the cell.

    // If passed a null or LOADING record just return the value passed in.
    if (record == null || Array.isLoading(record)) {
        return value;
    }
    // get the level of the node
    var level = this.data.getLevel(record),
        template = isc.TreeGrid._getTreeCellTemplate(),
        cssText = this.getCellCSSText(record, recordNum, fieldNum),
        styleName = this.getCellStyle(record, recordNum, fieldNum);


    template[1] = styleName
    template[3] = (this._fixTitleWidth()
                   ? "table-layout:fixed;width:100%;" + (cssText != null ? cssText : "")
                   : cssText);

    // catch custom css text with no closing ";"
    if (template[3] != null && !template[3].endsWith(this._$semi)) template[3] += this._$semi;

    // styling for indent cell
    template[9] = cssText;
    template[11] = styleName;

    template[13] = this.getIndentHTML(level, record, template, 5);

    // Get the HTML for the icons and title from _getTreeCellTitleArray(), and fold them
    // into our template
    var titleCellTemplate = this._getTreeCellTitleArray(value, record, recordNum,
                                fieldNum, this.shouldShowOpenerIcon(),
                                styleName, cssText, template, 7);
    for (var i = 0, j = 15; i < titleCellTemplate.length; i++) {
        template[j] = titleCellTemplate[i];
        j++;
    }
    template[j] = this._$closeTreeCellTable

    return template.join(isc.emptyString);
},

shouldShowOpenerIcon : function () {
    return this.showOpener;
},

_getTreeCellValueID : function (recordNum) {
    return this.ID + "_"+"valueCell" + recordNum;
},

// _getTreeCellTitleArray() - helper method for getTreeCellValue() to return the
// "title" portion of the treeCell value - that is: the icons and the title, without
// any indent

_getTreeCellTitleArray : function (value, record, recordNum, fieldNum, showOpener,
                                   cellStyle, cellCSSText, treeCellTemplate, iconCellWidthOffset) {

    var iconCellWidth = 0;

    if (cellCSSText == null) cellCSSText = this.getCellCSSText(record, recordNum, fieldNum);
    if (cellCSSText == null) cellCSSText = "";
    else cellCSSText += ";";
    if (!this.wrapCells) cellCSSText += "white-space:nowrap;";
    if (this._fixTitleWidth()) {
        cellCSSText += "overflow:hidden;" + isc.Browser._textOverflowPropertyName +
                       ":ellipsis";
    }
    if (cellStyle == null) cellStyle = this.getCellStyle(record, recordNum, fieldNum);

    var template = isc.TreeGrid._getTreeCellTitleTemplate();
    template[1] = cellCSSText;
    template[3] = cellStyle;
    if (showOpener) {
        // opener icon (or small indent)
        var openIcon = this.getOpenIcon(record),
            openIconWidth = this.getOpenerIconWidth(record),
            openIconHeight = this.getOpenerIconHeight(record),
            openerID = (recordNum != null ? this._openIconIDPrefix+recordNum : null);

        if (openIcon) {
            template[5] = this.getIconHTML(openIcon, openerID, openIconWidth, null, openIconHeight);
            iconCellWidth += openIconWidth;
        } else {
            template[5] = this._indentHTML(openIconWidth || this.iconSize);
            iconCellWidth += openIconWidth;
        }
    } else template[5] = null;
    var checkboxIcon = this._getCheckboxIcon(record),
        extraIcon = checkboxIcon || this.getExtraIcon(record),
        extraIconID = (recordNum != null ? this._extraIconIDPrefix+recordNum : null),
        extraIconSize = (checkboxIcon != null ?  this._getCheckboxFieldImageWidth() : this.iconSize),
        extraIconGap = this.extraIconGap,
        icon = this.getIcon(record),
        iconID = (recordNum != null ? this._iconIDPrefix+recordNum : null)
    ;

    // extra icon if there is one
    if (extraIcon) {
        template[6] = this.getIconHTML(extraIcon, extraIconID, extraIconSize, extraIconGap);
        iconCellWidth += extraIconSize + extraIconGap;
    } else template[6] = null;
    // folder or file icon
    template[7] = this.getIconHTML(icon, iconID, record.iconSize);
    iconCellWidth += icon == null ? 0 : (record.iconSize || this.iconSize);

    // When ARIA is enabled, set an ID on the value cell so that we can reference it.
    if (isc.Canvas.ariaEnabled()) {
        template[9] = " id='" + this._getTreeCellValueID(recordNum) + "'";
    } else {
        template[9] = null;
    }

    template[11] = cellCSSText;
    template[13] = this.iconPadding;
    template[15] = cellStyle;

    if (isc.Browser.isIE && isc.Browser.version < 10 && !this.wrapCells) {
        template[17] = "<NOBR>";
        template[19] = "</NOBR>";


    } else if (this._fixTitleWidth() && isc.Browser.isMoz && isc.Browser.version < 21) {
        template[17] = "<div style='overflow:hidden;text-overflow:ellipsis' _titleClipper='true'>";
        template[19] = "</div>";
    } else {
        template[19] = template[17] = null;
    }

    template[18] = value;

    if (treeCellTemplate) treeCellTemplate[iconCellWidthOffset] = iconCellWidth;
    return template;
},

_fixTitleWidth : function () {
    var treeField = this.getTreeFieldNum(),
        frozen = this.fields[treeField] && this.fields[treeField].frozen,
        gettingAutoSize =
            frozen ? (this.frozenBody && this.frozenBody._gettingAutoSizeHTML)
                    : (this.body && this.body._gettingAutoSizeHTML);
    return this.fixedFieldWidths && !gettingAutoSize;
},

//> @method TreeGrid.getCellAlign()
// Return the horizontal alignment for cell contents. Default implementation will always
// left-align the special +link{treeGridField.treeField} [or right-align if the page is in
// RTL mode] - otherwise will return +link{listGridField.cellAlign} if specified, otherwise
// +link{listGridField.align}.
//
//
// @param   record (ListGridRecord) this cell's record
// @param    rowNum    (number)    row number for the cell
// @param    colNum    (number)    column number of the cell
// @return    (Alignment)     Horizontal alignment of cell contents: 'right', 'center', or 'left'
// @visibility external
//<
getCellAlign : function (record, rowNum, colNum) {
    var field = this.getField(colNum);
    if (field && field.treeField) {
        return this.isRTL() ? "right" : "left";
    }
    return this.Super("getCellAlign", arguments);
},


// Override getCellValue() to return custom HTML for the tree-field
// Note: Developers are always advised to override formatCellValue rather than this method
// directly (which could lead to certain conflicts).
getCellValue : function (record, rowNum, colNum, gridBody, b, c, d) {
    var value = this.invokeSuper(isc.TreeGrid, "getCellValue", record, rowNum, colNum, gridBody, b,c,d);
    if (colNum == this.getTreeFieldNum()) {
// that takes an already formatted value and applies stuff to it.
        value = this.getTreeCellValue(value, record, rowNum, colNum, gridBody);
    }
    return value;
},

// overridden to create/clear draw cache
// (Fired for both frozen and normal body)
bodyDrawing : function (body,a,b,c,d) {
    this._drawCache = {};
    return this.invokeSuper(isc.TreeGrid, "bodyDrawing", body,a,b,c,d);
},

//> @method TreeGrid.getNodeTitle()
//
// Returns the title to show for a node in the tree column.  If the field specifies the
// <code>name</code> attribute, then the current <code>node[field.name]</code> is returned.
// Otherwise, the result of calling +link{method:Tree.getTitle} on the node is called.
// <br><br>
// You can override this method to return a custom title for node titles in the tree column.
//
// @param node      (TreeNode)  The node for which the title is being requested.
// @param recordNum (Number)  The index of the node.
// @param field     (DSField) The field for which the title is being requested.
//
// @return (HTML) the title to display.
//
// @see method:Tree.getTitle
//
// @visibility external
//<
getNodeTitle : function (record, recordNum, field) {

    var value;
    if (field.name && field.name != this._titleField) {

        if (recordNum == -1) return record[field.name];
        value = this.getEditedRecord(recordNum)[field.name];
    } else {
        value = this.data.getTitle(record);
    }
    // This will convert to a string etc.
    return this.applyCellTypeFormatters(value, record, field, recordNum, this.getFieldNum(field));

},

//> @method TreeGrid.getTitleField()
// Method to return the fieldName which represents the "title" for records in this
// TreeGrid.<br>
// If <code>this.titleField</code> is explicitly specified for this treeGrid, respect it -
// otherwise always return the tree-field (+link{TreeGrid.treeField}) for this grid.
// @return (string) fieldName for title field for this grid.
//<
getTitleField : function () {
    if (this.titleField != null) return this.titleField;
    return this.getFieldName(this.getTreeFieldNum());
},

//>    @method    treeGrid.getTreeFieldNum()    (A)
//        Return the number of the tree field for this treeGrid.
//
//        @return    (number)    Number for the tree node.
//<
getTreeFieldNum : function () { return this._treeFieldNum; },

//>    @method    treeGrid.getOpenAreaWidth()    (A)
//
//        @param    node        (TreeNode)        tree node clicked on
//
//        @return    (number)    Return the width of the open area (relative to wherever the tree field is)
//<
getOpenAreaWidth : function (node) {
    var openerIconSize = this.showOpener ? this.getOpenerIconWidth(node) : 0,
        indentSize = (this.showConnectors ? openerIconSize : this.indentSize)
    ;
    return ((this.data.getLevel(node)-(this.showRoot?0:1)) * indentSize) + openerIconSize;
},

getOpenerIconSize : function (node) {
    return (this.openerIconSize || (this.showConnectors ? this.cellHeight : this.iconSize));
},
getOpenerIconWidth : function (node) {
    return this.openerIconWidth || this.getOpenerIconSize(node);
},
getOpenerIconHeight : function (node) {
    return this.openerIconHeight || this.getOpenerIconSize(node);
},

//>    @method    treeGrid.clickInOpenArea()    (A)
//            for a given click, was it in the open/close area or on the main part of the item
//            OVERRIDE in your subclasses for different open/close schemes
//        @param    node        (TreeNode)        tree node clicked on
//
//        @return    (boolean)        true == click was in open area, false == normal click
//<
clickInOpenArea : function (node) {
    if (!this.data.isFolder(node) || ! this.showOpener) return false;

    // get some dimensions
    var treeFieldNum = this.getTreeFieldNum(),
        body = this.getFieldBody(treeFieldNum),
        localFieldNum = this.getLocalFieldNum(treeFieldNum),
        fieldLeft = body.getColumnLeft(localFieldNum),
        fieldWidth = body.getColumnWidth(localFieldNum),
        openAreaWidth = this.getOpenAreaWidth(node),
        x = body.getOffsetX()
    ;

    // textDirection: switch based on drawing in left-to-right (default) or right-to-left order
    if (this.isRTL()) {
        fieldLeft += body.getScrollWidth() - body.getViewportWidth();
        var fieldRight = fieldLeft + fieldWidth;
        return x >= (fieldRight - openAreaWidth) && x <= fieldRight;
    } else {
        return x >= fieldLeft && x < fieldLeft + openAreaWidth;
    }
},

//> @method treeGrid.isOverOpenArea()
// Returns true if the last event occurred over the indented area or over the
// open / close icon of a folder node in this TreeGrid. Returns false if the event
// did not occur over a folder node.
//
// @return (Boolean) true if the user clicked the open icon
// @visibility external
//<
isOverOpenArea : function () {
    var node = this.getRecord(this.getEventRow());
    if (node == null) return false;
    return this.clickInOpenArea(node);
},

//> @method treeGrid.clickInCheckboxArea() (A)
// For a given click, was it in the checkbox area?
// @param  node (TreeNode) tree node clicked on
//
// @return (boolean)       true == click was in checkbox area, false == normal click
//<
clickInCheckboxArea : function (node) {
    if (this.selectionAppearance != this._$checkbox) return false;
    return this.isOverExtraIcon(node);
},

//> @method treeGrid.isOverExtraIcon()
// Returns true if the last event occurred over +link{TreeGrid.getExtraIcon(),extra icon}
// for the current node.
// <P>
// Returns false if the event did not occur over an extraIcon, or if no extraIcon is
// showing for the node in question.
//
// @return (Boolean) true if the user clicked the extra icon
// @visibility external
//<

isOverExtraIcon : function (node) {

    if (node == null) node = this.getRecord(this.getEventRow());
    if (node == null) return false;
    // No extra-icon for the node
    var checkboxIcon = this._getCheckboxIcon(node),
        extraIcon = checkboxIcon || this.getExtraIcon(node);

    // No checkbox/extra icon - just bail.
    if (extraIcon == null) return false;

    var extraIconSize = (checkboxIcon != null ?  this._getCheckboxFieldImageWidth() : this.iconSize);

    // get some dimensions
    var treeFieldNum = this.getTreeFieldNum(),
        body = this.getFieldBody(treeFieldNum),
        localFieldNum = this.getLocalFieldNum(treeFieldNum),
        fieldLeft = body.getColumnLeft(localFieldNum),
        fieldWidth = body.getColumnWidth(localFieldNum),
        openAreaWidth = this.getOpenAreaWidth(node),
        x = body.getOffsetX()
    ;

    // textDirection: switch based on drawing in left-to-right (default) or right-to-left order
    if (this.isRTL()) {
        var fieldRight = fieldLeft + fieldWidth;
        return (x >= (fieldRight - openAreaWidth - extraIconSize) &&
            x <= (fieldRight - openAreaWidth));
    } else {
        return (x >= (fieldLeft + openAreaWidth) &&
            x < (fieldLeft + openAreaWidth + extraIconSize));
    }
},

//> @method treeGrid.getIndentHTML() (A)
// Return the HTML to indent a record
// @param level  (number)   indent level (0 == root, 1 == first child, etc)
// @param record (treeNode) record for which we're returning indent HTML
//
// @return (HTML) HTML to indent the child
//<
getIndentHTML : function (level, record, treeCellTemplate, indentCellWidthOffset) {
    var drawLevel = level;
    if (!this.showRoot) drawLevel--;

    var indentWidth = (this.showConnectors ? this.getOpenerIconWidth(record) : this.indentSize),

        shift1px = this.isPrinting || isc.Browser.isIE || isc.Browser.isOpera || isc.Browser.isEdge,
        indentCellWidth = (shift1px ? 1 : 0);

    // If showFullConnectors is true we need to write out vertical connector lines between
    // ancestors who are siblings.

    if (this.showConnectors && this.showFullConnectors) {
        // assume the level passed in is correct
        //var level = this.data.getLevel(record),
        var levels = this.data._getFollowingSiblingLevels(record);
        // we don't care about the innermost level (connector written out as part of opener icon)
        levels.remove(level);
        if (!this.showRoot) levels.remove(0);
        if (levels.length != 0) {
            if (!this._ancestorConnectorHTML) {
                var state = "ancestor";
                if (this.isRTL()) state += "_rtl";

                var connectorURL = isc.Img.urlForState(this.connectorImage, null, null,
                                                        state),
                                    connectorHTML = this.getIconHTML(connectorURL, null,
                                                        this.cellHeight);
                this._ancestorConnectorHTML = connectorHTML;
            }
            var singleIndent = this._indentHTML(indentWidth),
                indent = isc.StringBuffer.create(isc.emptyString)
            ;

            // explicit NOBR tag required in IE6 to ensure the indents don't wrap
            // when they run out of horizontal space
            indent.append("<NOBR>");
            if (shift1px) indent.append(this._indentHTML(1));
            for (var i = (this.showRoot ? 0 : 1); i < level; i ++) {
                if (levels.contains(i)) {
                    indent.append(this._ancestorConnectorHTML);
                    indentCellWidth += this.cellHeight;
                } else {
                    indent.append(singleIndent);
                    indentCellWidth += indentWidth;
                }
            }
            indent.append("</NOBR>");
            indent = indent.release();
            if (treeCellTemplate) treeCellTemplate[indentCellWidthOffset] = indentCellWidth;
            return indent;
        }
    }
    indentCellWidth = drawLevel * indentWidth;
    if (shift1px) indentCellWidth = Math.max(1, indentCellWidth);
    var indentHTML = this._indentHTML(indentCellWidth);

    if (isc.Browser.isIE9 || (isc.Browser.isStrict && (isc.Browser.isIE7 || isc.Browser.isIE8))) {
        indentHTML = "<NOBR>" + indentHTML + "</NOBR>";
    }
    if (treeCellTemplate) treeCellTemplate[indentCellWidthOffset] = indentCellWidth;
    return indentHTML;
},


_indentHTML : function (numPixels) {
    if (numPixels == 0) return isc.emptyString;

    var cache = isc.TreeGrid._indentHTMLCache;
    if (cache == null) cache = isc.TreeGrid._indentHTMLCache = {};

    if (cache[numPixels] == null) cache[numPixels] = isc.Canvas.spacerHTML(numPixels, 1);

    return cache[numPixels];
},

//>    @method    treeGrid.getOpenIcon()    (A)
// Get the appropriate open/close opener icon for a node. Returns null if +link{showOpener} is
// set to false.
//
// @param    node (TreeNode)    tree node in question
// @return    (URL)        URL for the icon to show the node's open state
//
// @visibility external
//<
getOpenIcon : function (record) {
    if (this.showOpener == false) return null;
    if (!this.data) return null;
    if (isc.isA.Number(record)) record = this.data.get(record);
    if (record == null) return null;

    // if the record has a specific openIcon, use that
    if (record.openIcon) {
        return record.openIcon;

    } else {

        var isFolder = this.data.isFolder(record),
            // non-folders can never have children, or be open
            hasChildren = isFolder,
            isOpen = isFolder,
            // does this node have adjacent siblings above or below, or is there a gap
            // between it's next sibling at the same level in either direction.
            start,
            end;

        if (isFolder) {
            // If the folder doesn't have it's data fully loaded show the
            // folder as being closed
            var loadState = this.data.getLoadState(record);
            if (loadState == isc.Tree.UNLOADED ||
                (loadState == isc.Tree.FOLDERS_LOADED &&
                 this.displayNodeType != isc.Tree.FOLDERS_ONLY))
            {
                hasChildren = true;
                isOpen = false;
            // If the data is loaded for the folder, use the data APIs to determine
            // whether this has children or not.
            } else {
                hasChildren = this.data.hasChildren(record, this.displayNodeType);
                isOpen = (hasChildren || this.alwaysShowOpener) && this.data.isOpen(record);
            }

        }

        // if we're an open folder, showing sparse connectors, we have a gap below us
        if (isOpen && !this.showFullConnectors) end = true
        else {
            end = !this._shouldShowNextLine(record);
        }

        start = !this._shouldShowPreviousLine(record);

        // punt it over to getOpenerImageURL which will assmble the URL from the state info.
        return this.getOpenerImageURL(hasChildren, isOpen, start, end);
    }
},

// _shouldShowPreviousLine
// Internal method - should we show a continuation connector line going up to the previous row
// for some record?
// True if the previous row is a sibling of this record, or if this is the first record in
// some folder (so the previous row contains parent of this record)
_shouldShowPreviousLine : function (record) {
    if (!this.data.isEmpty() && this.data.first() == record) {
        return false;
    }

    // always show a previous line if we're showing "full connectors"
    if (this.showFullConnectors) return true;

    var rowNum = this.data.indexOf(record),
        previousRecord = this.getRecord(rowNum - 1),
        parent = this.data.getParent(record);

    if (previousRecord == null) return false;
    return (parent == previousRecord || parent == this.data.getParent(previousRecord));
},

// _shouldShowNextLine
// Internal method - should we show a continuation connector line going down to the next row for
// some record?
// True only if the next row is a sibling of this record.
_shouldShowNextLine : function (record) {
    if (this.showFullConnectors) {
        var data = this.data,
            parent = data.getParent(record),
            children = data.getChildren(parent);
        return (children.indexOf(record) != (children.getLength() - 1));
    }
    var rowNum = this.data.indexOf(record),
        nextRecord = this.getRecord(rowNum +1);

    if (nextRecord == null) return false;
    return (this.data.getParent(record) == this.data.getParent(nextRecord));
},

//>    @method    treeGrid.getOpenerImageURL()    (A)
// Helper method called from getOpenIcon to retrieve the appropriate image URL string for
// the opener.
//
// @param    hasChildren (boolean)   Is the node in question a folder with children?
// @param   isOpen   (boolean)  Is the node an open folder?
// @param   startLine (boolean)   True if the previous row in the TreeGrid is not a sibling
//                                  or the parent of the node in question.  (Node effectively
//                                  starts a new hierarchy continuation line).
// @param   endLine   (boolean)   True if the next row in the TreeGrid is not a sibling
//                                  of the node in question.  (Node effectively ends a
//                                  hierarchy continuation line).
// @return    (string)        URL for the icon to show the node's state
//
// @visibility internal
//<
getOpenerImageURL : function (hasChildren, isOpen, startLine, endLine) {

    // Assemble the appropriate filename based on the base filename for connector / opener
    // images
    // Do this once per TreeGrid since each TreeGrid can have a different tg.openerImage /
    // tg.connectorImage.

    if (!this._openerImageMap) {
        // Assemble the various file names based on the possible states of these
        // images - the "opened" state (opened, closed, or has no children)
        //        - the position within the parent folder state
        //          (start, middle, end, separate)
        //         - RTL if appropriate.

        var img = this.openerImage;

        this._openerImageMap = {
            // use Img.urlForState
            // This handles splitting the base name into base + extension, and plugging in
            // the state name parameter (third parameter).

            // opener opened.  NOTE: doesn't switch with RTL, which is an assumption that it
            // will generally be a downward pointing glyph.
            opened:isc.Img.urlForState(img, null, null, "opened"),
            // opener closed
            closed:isc.Img.urlForState(img, null, null, (this.isRTL() ? "closed_rtl" : "closed")),
            // opener opening

            opening:isc.Img.urlForState(img, null, null, "opening")
        }
    }

    // generate connector / opener w/connector images
    if (this.showConnectors && !this._connectorImageMap) {
        // for connectors, we have start/end/middle for position on a line of connectors,
        // "single" for a node with no peers, and opener/leaf variations.
        var img = this.connectorImage,
            states = ["single", "start", "end", "middle",
                      "opened_single", "opened_start", "opened_middle", "opened_end",
                      "closed_single", "closed_start", "closed_middle", "closed_end"],
            map = {},
            isRTL = this.isRTL(),
            rtl = "_rtl";

        for (var i = 0; i < states.length; i++) {
            var state = states[i],
                suffix = state;
            if (isRTL) suffix += rtl;
            map[state] = isc.Img.urlForState(img, null, null, suffix);
        }

        this._connectorImageMap = map;
    }

    if (this.showConnectors) {
        var imageMap = this._connectorImageMap;

        if (hasChildren) {
            if (isOpen) {

                if (!this.showFullConnectors) {
                    // if we're showing sparse connectors, no need to check for 'end'  - if it's
                    // open this is always true.
                    if (startLine) return imageMap.opened_single;
                    return imageMap.opened_end;
                }
                if (startLine && endLine) return imageMap.opened_single;
                else if (startLine) return imageMap.opened_start;
                else if (endLine) return imageMap.opened_end;
                else return imageMap.opened_middle;

            } else {
                if (startLine && endLine) return imageMap.closed_single;
                if (startLine) return imageMap.closed_start;
                if (endLine) return imageMap.closed_end;
                return imageMap.closed_middle;
            }
        } else {
            // leaf
            if (startLine && endLine) return imageMap.single;
            if (startLine) return imageMap.start;
            if (endLine) return imageMap.end;
            return imageMap.middle;
        }
    } else {
        var imageMap = this._openerImageMap;
        // we don't return any image if we're not showing connectors, and this is not a folder
        // with children.
        if (!hasChildren && !this.alwaysShowOpener) return null;

        if (isOpen) return imageMap.opened;
        return imageMap.closed;
    }
},

_$checkbox:"checkbox",
_getCheckboxIcon : function (record) {
    var icon = null;
    if (this.selectionAppearance == this._$checkbox) {
        var isSel = this.selection.isSelected(record) ? true : false;
        var isPartSel = (isSel && this.showPartialSelection &&
                    this.selection.isPartiallySelected(record)) ? true : false;
        // checked if selected, otherwise unchecked
        icon = isPartSel ? (this.checkboxFieldPartialImage || this.booleanPartialImage)
                             : isSel ? (this.checkboxFieldTrueImage || this.booleanTrueImage)
                                     : (this.checkboxFieldFalseImage || this.booleanFalseImage);
        if (!this.body.canSelectRecord(record)) {
            if (this.showDisabledSelectionCheckbox) {
                // show the disabled checkbox, making sure to capture the
                // disabled state
                if (icon != this._$blank) icon = isc.Img.urlForState(icon, null, null, "Disabled");
            } else {
                if (this.leaveSelectionCheckboxGap) {
                    // record cannot be selected but we want
                    // the space allocated for the checkbox anyway.
                    icon = isc.Canvas._blankImgURL;
                } else {
                    // leaving no gap looks better in some cases (EG showConnectors
                    // set to true)
                    icon = null;
                }
            }
        }
        if (icon == this._$blank) icon = isc.Canvas._blankImgURL;
    }
    return icon;
},

//> @method treeGrid.getExtraIcon() (A)
// Get an additional icon to show between the open icon and folder/node icon for a particular
// node.
// <P>
// NOTE: If +link{listGrid.selectionAppearance} is <code>"checkbox"</code>, this method will
// NOT be called. Extra icons cannot be shown for that appearance.
//
// @param    node (TreeNode)    tree node in question
// @return    (URL)        URL for the extra icon (null if none required)
//
// @visibility external
//<
getExtraIcon : function (record) {
    // Default trees don't make use of this.
    return null;
},

//>    @method    treeGrid.getIcon()
// Get the appropriate icon for a node.
// <P>
// By default icons are derived from +link{folderIcon} and +link{nodeIcon}.
// Custom icons for individual nodes can be overridden by setting the +link{customIconProperty}
// on a node.
// <p>
// If you want to suppress icons altogether, provide an override of this method that simply
// returns null.
// <p>
// Note that the full icon URL will be derived by applying +link{Canvas.getImgURL()} to the
// value returned from this method.
//
// @param    node (TreeNode)    tree node in question
// @return    (URL)        URL for the icon to show for this node
// @visibility external
//<
getIcon : function (node, defaultState) {
    if (isc.isA.Number(node)) node = this.data.get(node);
    if (!node) return null;

    var icon = node[this.customIconProperty],
        customIcon = (icon != null),
        isFolder = this.data.isFolder(node);

    if (!customIcon) {
        if (isFolder) icon = this.folderIcon;
        else icon = this.nodeIcon;
    }
    var state;
    if (isFolder) {
        // Default folder icon is the 'closed' icon. This will be used for dragTrackers, etc
        // Note: check for the special _willAcceptDrop flag set by updateDropFolder() - when a
        // user hovers over a folder for a while, we spring it open, and that causes a redraw,
        // but the folder is not necessarily droppable.
        var isDrop = defaultState ? false : (this.lastDropFolder == node && node._willAcceptDrop),
            isOpen = defaultState ? false : !!this.data.isOpen(node);

        if (isDrop) {
            // backCompat - respect old dropIcon / folderDropImage if specified
            if (node.dropIcon != null) icon = node.dropIcon;
            else if (!customIcon && this.folderDropImage != null) icon = this.folderDropImage;
            else {
                var showDrop;
                if (customIcon) {
                    showDrop = node[this.customIconDropProperty];
                    if (showDrop == null) showDrop = this.showCustomIconDrop;
                } else {
                    showDrop = this.showDropIcons;
                }
                if (showDrop) state = this.dropIconSuffix;
            }
        } else if (isOpen) {

            // backCompat - respect old openIcon / folderOpenImage if specified
            if (node.openedIcon != null) icon = node.openedIcon;
            else if (!customIcon && this.folderOpenImage != null) icon = this.folderOpenImage;
            // Don't override already set drop state
            else {
                var showOpen;
                if (customIcon) {
                    showOpen = node[this.customIconOpenProperty];
                    if (showOpen == null) showOpen = this.showCustomIconOpen;
                } else {
                    showOpen = this.showOpenIcons;
                }
                if (showOpen) state = this.openIconSuffix;

                else if (!customIcon) state = this.closedIconSuffix;
            }
        }  else {

            // Respect old 'folderClosedImage' if specified
            // Otherwise - if the icon is not custom, append "_closed" state

            if (!customIcon) {
                if (this.folderClosedImage) icon = this.folderClosedImage;
                else state = this.closedIconSuffix;
            }
        }
    // not a folder:
    } else {
        // Pick up the old 'fileImage' for back compat, if specified.
        if (!customIcon && this.fileImage) icon = this.fileImage;
    }
    return icon == null ? null : isc.Img.urlForState(icon, false, false, state);
},

// helper method - caches generated image templates on a per-draw basis for faster html generation.
_getIconHTMLCacheKey : function (icon, iconWidth, extraRightMargin, iconHeight) {
    return icon + "#w=" + iconWidth + ",extraRightMargin=" + extraRightMargin + ",h=" + iconHeight;
},
_$absMiddle: "absmiddle",

getIconHTML : function (icon, iconID, iconWidth, extraRightMargin, iconHeight) {

    if (icon == null) return isc.emptyString;

    if (iconWidth == null) iconWidth = this.iconSize;
    if (iconHeight == null) iconHeight = iconWidth;

    // make sure the iconHTML cache exists
    // Note this method can fire before drawCache has been set up due to autoSize logic
    // requesting cell HTML before body draw. If this occurs, just default the
    // cache object.
    if (this._drawCache == null) {
        this._drawCache = {};
    }
    var cache = this._drawCache.iconHTML;
    if (cache == null) cache = this._drawCache.iconHTML = {};

    // if not in cache, generate and store - keyed by the image src
    var cacheKey = this._getIconHTMLCacheKey(icon, iconWidth, extraRightMargin, iconHeight),
        template = cache[cacheKey];
    if (template == null) {


        var extraCSSText;
        if (extraRightMargin) {

            extraCSSText = (this.isRTL() ? "margin-left:" : "margin-right:") + extraRightMargin + "px";
        }

        template = cache[cacheKey] = this._getImgHTMLTemplate({
            src: icon,
            width: iconWidth,
            height: iconHeight,
            name: iconID,
            align: this._$absMiddle,
            extraCSSText: extraCSSText,

            generateSpan: isc.Canvas._generateSpanForBlankImgHTML
        });
    }

    // Note: We need to update the image ID for each icon - this is in the 16'th slot in the
    // array of strings used as a template (see Canvas.imgHTML())
    template[16] = iconID;

    return template.join(isc._emptyString);
},


//>    @method    treeGrid.setRowIcon()    (A)
// Set the icon for a particular record to a specified URL (relative to Page.imgDir + this.imgDir
//
//        @param    record        (TreeNode)    tree node
//        @param    URL        (URL)        URL for the record icon
//<
setRowIcon : function (record, URL) {

    // normalize the record from a number if necessary
    if (!isc.isA.Number(record)) record = this.data.indexOf(record);
    // set the image

    if (record != -1 && this.getIcon(record) != null) {
        this.setImage(this._iconIDPrefix + record, URL, null, isc.Canvas._generateSpanForBlankImgHTML);
    }
},

//> @method treeGrid.setNodeIcon()
// Set the icon for a particular treenode to a specified URL
//
//        @param node        (TreeNode) tree node
//        @param icon        (SCImgUrl) path to the resource
//        @group treeIcons
//        @visibility external
//<
setNodeIcon : function (node, icon) {
    //make the change persist across redraws
    node[this.customIconProperty] = icon;
    //efficiently refresh the image
    this.setImage(this._iconIDPrefix + this.getRecordIndex(node), icon, null, isc.Canvas._generateSpanForBlankImgHTML);
},

// -------------------
// Printing

getPrintHTML : function (printProperties, callback) {
    var expand = this.printExpandTree;
    if (expand == null) expand = printProperties ? printProperties.expandTrees : null;

    if (expand && this.data) {
        if (isc.ResultTree && isc.isA.ResultTree(this.data) && this.data.loadDataOnDemand) {
            this.logWarn("Printing TreeGrid with option to expand folders on print not supported " +
                            "for load on demand trees.");
        } else {
            this.data.openAll();
        }
    }
    return this.Super("getPrintHTML", arguments);
},

// Multiple copies of this string are prepended to the tree field, in order to indent it,
// when exporting tree data via +link{DataBoundComponent.getClientExportData}.
//exportIndentString:null,

getExportFieldValue : function (record, fieldName, fieldIndex) {
    var val = this.Super("getExportFieldValue", arguments);

    // Prepend tree depth indent string, ensuring that children of root are not indented
    if (fieldIndex == this.getTreeFieldNum() && this.exportIndentString) {
        var level = this.data.getLevel(record);
        while (--level > 0) val = this.exportIndentString + val;
    }

    return val;
}
});



// Register "stringMethods" for this class
isc.TreeGrid.registerStringMethods({
//    folderDropMove:"viewer,folder,childIndex,child,position",

    //> @method treeGrid.folderOpened()
    //
    // This method is called when a folder is opened either via the user manipulating the
    // expand/collapse control in the UI or via +link{TreeGrid.openFolder()}.  You can return
    // <code>false</code> to cancel the open.
    //
    // @param node (TreeNode) the folder (record) that is being opened
    //
    // @return (boolean) false to cancel the open, true to all it to proceed
    //
    // @visibility external
    //<
    folderOpened : "node",

    //> @method treeGrid.folderClosed()
    //
    // This method is called when a folder is closed either via the user manipulating the
    // expand/collapse control in the UI or via +link{TreeGrid.closeFolder()}.  You can return
    // <code>false</code> to cancel the close.
    //
    // @param node (TreeNode) the folder (record) that is being closed
    //
    // @return (boolean) false to cancel the close, true to all it to proceed
    //
    // @visibility external
    //<
    folderClosed : "node",

    //> @method treeGrid.folderClick()
    //
    // This method is called when a folder record is clicked on.
    //
    // @param viewer (TreeGrid) The TreeGrid on which folderClick() occurred.
    // @param folder (TreeNode) The folder (record) that was clicked
    // @param recordNum (number) Index of the row where the click occurred.
    //
    // @see treeGrid.nodeClick()
    //
    // @visibility external
    //<
    folderClick : "viewer,folder,recordNum",

    //> @method treeGrid.leafClick()
    //
    // This method is called when a leaf record is clicked on.
    //
    // @param viewer (TreeGrid) The TreeGrid on which leafClick() occurred.
    // @param leaf (TreeNode) The leaf (record) that was clicked
    // @param recordNum (number) Index of the row where the click occurred.
    //
    // @see treeGrid.nodeClick()
    //
    // @visibility external
    //<
    leafClick : "viewer,leaf,recordNum",

    //> @method treeGrid.nodeClick()
    //
    // This method is called when a leaf or folder record is clicked on.  Note that if you set
    // up a callback for <code>nodeClick()</code> and e.g. +link{treeGrid.leafClick()}, then
    // both will fire (in that order) if a leaf is clicked on.
    //
    // @param viewer (TreeGrid) The TreeGrid on which leafClick() occurred.
    // @param node (TreeNode) The node (record) that was clicked
    // @param recordNum (number) Index of the row where the click occurred.
    //
    // @see treeGrid.folderClick()
    // @see treeGrid.leafClick()
    //
    // @visibility external
    // @example treeDropEvents
    //<
    nodeClick : "viewer,node,recordNum",

    //> @method treeGrid.folderContextClick()
    //
    // This method is called when a context click occurs on a folder record.
    //
    // @param viewer (TreeGrid) The TreeGrid on which the contextclick occurred.
    // @param folder (TreeNode) The folder (record) on which the contextclick occurred.
    // @param recordNum (number) Index of the row where the contextclick occurred.
    //
    // @return (boolean) whether to cancel the event
    //
    // @see treeGrid.nodeContextClick();
    //
    // @visibility external
    //<
    folderContextClick : "viewer,folder,recordNum",

    //> @method treeGrid.leafContextClick()
    //
    // This method is called when a context click occurs on a leaf record.
    //
    // @param viewer (TreeGrid) The TreeGrid on which the contextclick occurred.
    // @param leaf (TreeNode) The leaf (record) on which the contextclick occurred.
    // @param recordNum (number) Index of the row where the contextclick occurred.
    //
    // @return (boolean) whether to cancel the event
    //
    // @see treeGrid.nodeContextClick();
    //
    // @visibility external
    //<
    leafContextClick : "viewer,leaf,recordNum",

    //> @method treeGrid.nodeContextClick()
    //
    // This method is called when a context click occurs on a leaf or folder record.  Note that
    // if you set up a callback for <code>nodeContextClick()</code> and
    // e.g. +link{treeGrid.leafContextClick}, then both will fire (in that order) if a leaf
    // is contextclicked - unless <code>nodeContextClick()</code> returns false, in which case
    // no further contextClick callbacks will be called.
    //
    // @param viewer (TreeGrid) The TreeGrid on which the contextclick occurred.
    // @param node (TreeNode) The node (record) on which the contextclick occurred.
    // @param recordNum (number) Index of the row where the contextclick occurred.
    //
    // @return (boolean) whether to cancel the event
    //
    // @see treeGrid.folderContextClick();
    // @see treeGrid.leafContextClick();
    //
    // @visibility external
    //<
    nodeContextClick : "viewer,node,recordNum",

    //> @method treeGrid.dataArrived
    // Notification method fired whenever this TreeGrid receives new data nodes from the
    // dataSource. Only applies to databound TreeGrids where +link{treeGrid.data} is a
    // +link{ResultTree} - either explicitly created and applied via +link{treeGrid.setData()} or
    // automatically generated via a +link{treeGrid.fetchData(),fetchData()} call.
    // @param parentNode (TreeNode) The parentNode for which children were just loaded
    // @visibility external
    //<
    dataArrived:"parentNode",

    //> @method treeGrid.onFolderDrop
    // Notification method fired when treeNode(s) are dropped into a folder of this TreeGrid.
    // This method fires before the standard +link{method:treeGrid.folderDrop} processing occurs
    // and returning false will suppress that default behavior.
    // @param nodes (Array of TreeNode) List of nodes being dropped
    // @param folder (TreeNode) The folder being dropped on
    // @param index (integer) Within the folder being dropped on, the index at which the drop is
    //                        occurring.
    // @param dropPosition (RecordDropPosition) position with respect to the target record
    // @param sourceWidget (Canvas) The component that is the source of the nodes (where the nodes
    //                              were dragged from).
    // @return (boolean) return false to cancel standard folder drop processing
    // @visibility sgwt
    //<
    onFolderDrop:"nodes,folder,index,dropPosition,sourceWidget"
});
isc.defineInterface("Observer").addInterfaceProperties({

registerObserved : isc.ClassFactory.TARGET_IMPLEMENTS,
unregisterObserved : isc.ClassFactory.TARGET_IMPLEMENTS

});

isc.defineInterface("AutoObserver", "Observer").addInterfaceProperties({

observedName: "observed",
observations: {},

registerObserved : function (observed) {
    this[this.observedName] = observed;
    for (var method in this.observations) {
        this.observe(observed, method, this.observations[method]);
    }
    if (isc.isA.Canvas(observed)) {
        this.observe(observed, "destroy", "observer.unregisterObserved(observed)");
    }
},

unregisterObserved : function (observed) {
    this[this.observedName] = null;
    for (var method in this.observations) {
        this.ignore(observed, method);
    }
}

});

isc.defineInterface("GridAutoObserver", "AutoObserver").addInterfaceProperties({
observedName: "grid"
});


isc.defineClass("GridTotalRowsIndicator", "Label", "GridAutoObserver").addProperties({

height: 1,
wrap: false,
overflow: "visible",
valign: "center",

observations: {
    "dataArrived": "observer.gridDataChanged()",
    "setData": "observer.gridDataChanged()"
},

dynamicContents: true,
contents: "Total Rows: ${this.rowCount}",
rowCount: "N/A",

gridDataChanged : function () {
    var data = this.grid.data;
    if (!data) this.rowCount = "N/A";

    if (isc.isA.ResultSet(data)) {
        if (data.lengthIsKnown()) {
            if (data.getLength() != 0 && data.progressiveLoading) {
                var lastRowIndex = data.getLength()-1;
                if (data.rowIsLoaded(lastRowIndex)) this.rowCount = data.getLength();
                else this.rowCount = data.getLength()+"+ (progressive loading)";
            } else {
                this.rowCount = data.getLength();
            }
        }
        else this.rowCount = "N/A";
    } else if (isc.isAn.Array(data)) {
        this.rowCount = data.getLength();
    }

    this.markForRedraw();
}

});


isc.defineClass("ObserverToolStrip", "ToolStrip").addProperties({

initWidget : function () {
    this.Super("initWidget", arguments);

    for (var i = 0; i < this.members.length; i++) {
        var m = this.members[i];
        if (isc.isAn.Observer(m)) {
            m.registerObserved(this.grid);
        }
    }
}

});

isc.defineClass("GridToolStrip", "ObserverToolStrip").addProperties({

membersMargin: 5,

addButtonDefaults: {
    _constructor: "Img",
    size: 16,
    layoutAlign: "center",
    src: "[SKIN]/actions/add.png",
    click: "this.creator.grid.startEditingNew()"
},

removeButtonDefaults: {
    _constructor: "Img",
    size: 16,
    layoutAlign: "center",
    src: "[SKIN]/actions/remove.png",
    click: "this.creator.grid.removeSelectedData()"
},

refreshButtonDefaults: {
    _constructor: "Img",
    size: 16,
    layoutAlign: "center",
    src: "[SKIN]/actions/refresh.png",
    click: "this.creator.grid.invalidateCache()"
},

exportButtonDefaults: {
    _constructor: "IButton",
    title: "Export to CSV",
    layoutAlign: "center",
    click: "this.creator.grid.exportData()"
},

totalRowsIndicatorDefaults: {
    _constructor: "GridTotalRowsIndicator",
    layoutAlign: "center"
},

members: ["autoChild:removeButton", "autoChild:addButton", "autoChild:exportButton",
         "starSpacer",
         "autoChild:refreshButton", "autoChild:totalRowsIndicator"],

initWidget : function () {
    this.Super("initWidget", arguments);
}

});



//////////////////////////////////////////////////////////////////////////////
// Class FieldPickerField

//> @class FieldPickerField
// Class containing properties that configure the display of
// and interaction with the fields showing in a +link{FieldPicker}.
//
// @treeLocation Client Reference/Data Binding/FieldPicker
// @visibility external
//<
isc.ClassFactory.defineClass("FieldPickerField");

isc.FieldPickerField.addClassProperties({

//> @classAttr fieldPickerField.frozenTitle (String : "Is Frozen" : IR)
// The title displayed for the frozen property
// @group i18nMessages
// @visibility external
//<
frozenTitle: "Is Frozen",

//> @classAttr fieldPickerField.precisionTitle (String : "Precision" : IR)
// The title displayed for the precision property
// @group i18nMessages
// @visibility external
//<
precisionTitle: "Precision",

//> @classAttr fieldPickerField.decimalPrecisionTitle (String : "Decimal Precision" : IR)
// The title displayed for the decimalPrecision property
// @group i18nMessages
// @visibility external
//<
decimalPrecisionTitle: "Decimal Precision",

//> @classAttr fieldPickerField.decimalPadTitle (String : "Decimal Pad" : IR)
// The title displayed for the decimalPad property
// @group i18nMessages
// @visibility external
//<
decimalPadTitle: "Decimal Pad",

//> @classAttr fieldPickerField.alignTitle (String : "Header Text Alignment" : IR)
// The title displayed for the align property
// @group i18nMessages
// @visibility external
//<
alignTitle: "Header Text Alignment",

//> @classAttr fieldPickerField.cellAlignTitle (String : "Data Alignment" : IR)
// The title displayed for the cellAlign property
// @group i18nMessages
// @visibility external
//<
cellAlignTitle: "Data Alignment"

});

//////////////////////////////////////////////////////////////////////////////
// Interface FieldPickerGrid



isc.ClassFactory.defineInterface("FieldPickerGrid").addInterfaceProperties({

// filtering

clearCriteriaIfPresent : function () {
    if (!isc.isA.emptyObject(this.getCriteria())) {
        if (this.willFetchData(null, "substring")) {
            this.logWarn("ListGrid.willFetchData() has returned true unexpectedly " +
                         "while trying to clear the filter criteria prior to save.");
        }
        this.filterData();
    }
},

// record expansion/collapse

getExpansionComponent : function (record) {
    var view = this.parentElement,
        changedRecords = view.shuttle.changedRecords,
        changedRecord = changedRecords[record.name];

    if (changedRecord) {
        record = isc.shallowClone(record);
        isc.addProperties(record, changedRecord);
    }

    return view.createExpansionDynamicForm(this.editableProperties, record);
},

saveAndCollapseRecord : function (member, component, record) {

    var view = this.parentElement,
        values = member.getChangedValues(),
        changedRecords = view.shuttle.changedRecords;

    if (changedRecords[record.name]) isc.addProperties(changedRecords[record.name], values);
    else changedRecords[record.name] = values;

    this._collapseRecord(record, component);
},

setCanExpandRecords : function (canExpand) {
    var recalculate = canExpand && !this.canExpand;

    this.Super("setCanExpandRecords", arguments);


    if (recalculate) this.setRecordExpansion(0, this.getTotalRows());
},

setRecordExpansion : function (startRow, endRow) {

    var nFloatApplicableProperties = 0,
        nNumberApplicableProperties = 0,
        nAllTypeApplicableProperties = 0;

    var shuttle = this.parentElement.shuttle,
        dataSource = shuttle.picker.expansionDataSource,
        fieldNames = dataSource.getFieldNames(),
        fields = dataSource.getFields();

    // build a set of all properties and non-float properteis

    for (var i = 0; i < fieldNames.length; i++ ) {
        var name = fieldNames[i];
        if (this.editableProperties.contains(name)) {
            switch (name) {
            default:
                nAllTypeApplicableProperties++;
                // fall through - if applicable to all types it's also applicable to a number
            case "precision":
                nNumberApplicableProperties++;
                // fall through - if applicable to a number it's also applicable to a float
            case "decimalPad":
            case "decimalPrecision":
                nFloatApplicableProperties++;
                break;
            }
        }
    }

    // now, using those property sets, sweep through records

    var dirty = false,
        canExpandProperty = this.canExpandRecordProperty;

    for (var i = startRow; i < endRow; i++) {
        var expand = true, record = this.getFieldRecord(i);

        // don't disable expansion if userSummary/userFormula are present;
        // buttons will be present to allow editing using Builder
        if (!record.userSummary && !record.userFormula) {
            if (isc.SimpleType.inheritsFrom(record.type, "float")) {
                expand = nFloatApplicableProperties > 0;
            } else if (isc.SimpleType.inheritsFrom(record.type, "integer")) {
                expand = nNumberApplicableProperties > 0;
            } else {
                expand = nAllTypeApplicableProperties > 0;
            }
        }

        // redraw grid if at least one record's expansion property changed
        if (record[canExpandProperty] == null || record[canExpandProperty] == true) {
            if (!expand) { record[canExpandProperty] = false; dirty = true; }
        } else {
            if ( expand) { delete record[canExpandProperty];  dirty = true; }
        }
    }

    if (dirty) this.markForRedraw();
},

// force expanded records closed when they leave the FieldPickerGrid

collapseRecordList : function (recordList, revertTitle) {
    for (var i = 0; i < recordList.length; i++) {
        var record = recordList[i];
        if (this.isExpanded(record)) this.collapseRecord(record);
        // revert any title change in the record
        if (revertTitle) record._title = record.title;
    }
},

collapseAllRecords : isc.ClassFactory.TARGET_IMPLEMENTS,

filterData : function () {
    this.collapseAllRecords();
    return this.Super("filterData", arguments);
},

// support validation of changes to the records (DataBoundComponent Fields)

expandRecord : function (record) {
    this.Super("expandRecord", arguments);
    var form = this.getCurrentExpansionComponent(record);
    if (form != null) form.validate();
},

validateFieldRecord : function (record) {

    // if record is already expanded, simply use the DynamicForm validators to validate
    if (this.isExpanded(record)) return this.getCurrentExpansionComponent(record).validate();

    // otherwise, our cached values must be validated
    var shuttle = this.parentElement.shuttle,
        properties = shuttle.changedRecords[record.name];

    if (!properties) return true;

    return shuttle.picker.validateDataSourceField(properties);
},

validateRecordList : function (recordList) {

    if (!this.canExpandRecords) return true;
    var canExpandProperty = this.canExpandRecordProperty;

    for (var i = 0; i < recordList.length; i++) {
        var record = recordList[i];
        if (record[canExpandProperty] != false && !this.validateFieldRecord(record)) {
            // if not already expanded, expand offending record
            if (!this.isExpanded(record)) {
                this.expandRecord(record);
                this.getCurrentExpansionComponent(record).validate();
            }
            // always make the offending record clearly visible
            this.scrollToRow(this.getRecordIndex(record));
            return false;
        }
    }
    return true;
},

validateAllRecords : isc.ClassFactory.TARGET_IMPLEMENTS,

// support for canHide and canReorder ListGridField properties

_filterDropRecords : function (records, crossDrop, resultOnly) {
    var excluded = [],
        component = this.creator.dataBoundComponent,
        canReorder = !isc.isA.ListGrid(component) || component.canReorderFields;

    for (var i = 0; i < records.length; i++) {
        var record = records[i];
        if (crossDrop) {
            if (record.canHide == false) excluded.add(record);
        } else {
            if (record.canReorder == null && !canReorder ||
                record.canReorder == false) excluded.add(record);
        }
    }

    // return true if and only if there are still records to drop
    if (resultOnly) return records.length > excluded.length;
    else {
        if (excluded.length > 0) records.removeList(excluded);
        return records.length > 0;
    }
},

willAcceptDrop : function () {
    var EH = this.ns.EH,
        result = this.Super("willAcceptDrop");
    if (!result) return false;
    return this._filterDropRecords(EH.dragTarget.getDragData(),
                                   EH.dragTarget != this, true);
},

_syntheticTransferDone : function () {},

// Show hovers based on field.prompt if possible
canHover:true,
showHover:true,
cellHoverHTML : function (record, rowNum, colNum) {
    if (!this.creator.showFieldPrompts) return null;
    if (record && record.prompt) return record.prompt;
    return null;
},

// shifting records up and down (current grid only)

getContiguousSelection : function () {
    var selection = this.getSelectedRecords() || [];
    if (selection.length == 0) return null;

    // we allow multiple selections only for contiguous records
    var firstIndex = this.getRecordSetBounds(selection, -1);

    var deselected = [];
    for (var i = 1; i < selection.length; i++) {
        var record = selection[i];
        if (deselected.length == 0 && this.getRecord(firstIndex + i) != record ||
           deselected.length > 0) deselected.add(record);
    }
    this.deselectRecords(deselected);

    return this.getSelectedRecords();
},

getRecordSetBounds : function (records, direction, offset) {
    if (offset == null) offset = 0;
    var nRows = this.getTotalRows();
    switch (direction) {
    case -1:
        var index = this.getRecordIndex(records.first());
        if (index >= 0) {
            return Math.max(index - offset, 0);
        }
        break;
    case 1:
        var index = this.getRecordIndex(records.last());
        if (index >= 0) {
            return Math.min(index + offset + 1, nRows);
        }
        break;
    }
    return null;
},

getCellValue : function (record, recordNum, fieldNum) {
    var result = this.Super("getCellValue", arguments);

    // return the superclass result unless field is title field and no title is defined
    var field = this.getField(fieldNum);
    if (field.name != "_title" || result != this.emptyCellValue) return result;

    // call +link{dataBoundComponent.getFieldTitle()} if available for the title
    var component = this.creator.dataBoundComponent;
    if (isc.isA.Function(component.getFieldTitle)) {
        var componentField = component.getField(record.name);
        if (componentField) return component.getFieldTitle(componentField);
    }

    // as fallback use field name
    return record.name;
}

});

//////////////////////////////////////////////////////////////////////////////
// Class FieldPickerListGrid

isc.ClassFactory.defineClass("FieldPickerListGrid", "ListGrid", "FieldPickerGrid");

isc.FieldPickerListGrid.addMethods({

// access to the field records

getFieldRecord :       isc.ListGrid.getPrototype().getRecord,
getTotalFieldRecords : isc.ListGrid.getPrototype().getTotalRows,
clearFieldRecords : function () {},

// expansion

dataArrived : function (startRow, endRow) {
    if (this.canExpandRecords && startRow < endRow) this.setRecordExpansion(startRow, endRow);
},

collapseAllRecords : function () {
    if (this.canExpandRecords) {
        var recordList = this.data.getAllRows();
        this.collapseRecordList(recordList);
    }
},

validateAllRecords : function () {
    if (this.canExpandRecords) {
        var recordList = this.data.getAllRows();
        return this.validateRecordList(recordList);
    }
    return true;
},

// support for autochaining an in-widget reorder for between widget drags


requestSyntheticReorder : function (dropRecords, targetRecord) {
    // abort reorder if another in progress or not allowed
    if (this._reorderRequest || !this.canReorderRecords) return;
    // listen for completion call
    if (!this.isObserving(this.data, "handleUpdate")) {
        this.observe(this.data, "handleUpdate", "observer.applySyntheticReorder(arguments[1])");
    }
    // set up the reorder request
    var i, keys = {};
    for (i = 0; i < dropRecords.length; i++) keys[dropRecords[i].name] = true;
    this._reorderRequest = {
        target: targetRecord.name,
        total: dropRecords.length,
        arrived: 0,
        keys: keys
    };
},

applySyntheticReorder : function (newRows) {
    var request = this._reorderRequest;

    // if we shouldn't be here, make sure we don't show up again
    if (!request) { this.ignore(this.data, "handleUpdate"); return; }
    // ensure newRows is a list for standard processing
    if (!isc.isAn.Array(newRows)) newRows = [newRows];
    if (newRows == null) return;

    // wait for transferred records to arrive
    for (var i = 0; i < newRows.length; i++) {
        if (request.keys[newRows[i].name]) request.arrived++;
    }
    if (request.arrived != request.total) return;

    // if all records are present, launch reorder
    if (!this.data.lengthIsKnown()) return;
    var records = [],
        length = this.data.getLength(),
        startPos = Math.max(0, length - request.total),
        index = this.data.findIndex("name", request.target);
    // due to filter, records may be hidden
    for (var i = startPos; i < length; i++) {
        var record = this.data.get(i) || {};
        if (request.keys[record.name]) records.add(record);
    }

    this._reorderRequest = null;
    this.ignore(this.data, "handleUpdate");
    this.delayCall("_finishApplySyntheticReorder", [request, records, index]);
},

_finishApplySyntheticReorder : function (request, records, index) {
    this.transferRecords(records, request.target, index, this, this._syntheticTransferDone);
},

// detection of changes for save prompt

dragComplete: function () {
    var view = this.parentElement;
    view.shuttle.markForSave();
},

transferRecords : function (dropRecords, targetRecord, index, sourceWidget, callback) {

    var view      = this.parentElement,
        crossDrop = this != sourceWidget,
        synthetic = callback == this._syntheticTransferDone;

    if (!synthetic) this._filterDropRecords(dropRecords, crossDrop);


    if (crossDrop && !sourceWidget.validateRecordList(dropRecords)) dropRecords.clear();

    if (dropRecords.length > 0) view.shuttle.markForSave();

    // collapse any open records before starting the drop
    if (crossDrop) {
        sourceWidget.collapseRecordList(dropRecords, true);
    }

    // autochain an in-widget reorder for between widget drags
    if (crossDrop && targetRecord != null) {
        this.requestSyntheticReorder(dropRecords, targetRecord);
    }

    return this.Super("transferRecords", arguments);
},

// overrides from FieldPickerGrid

filterData :            isc.FieldPickerGrid.getPrototype().filterData,
expandRecord :          isc.FieldPickerGrid.getPrototype().expandRecord,
willAcceptDrop:         isc.FieldPickerGrid.getPrototype().willAcceptDrop,
setCanExpandRecords :   isc.FieldPickerGrid.getPrototype().setCanExpandRecords,
getExpansionComponent : isc.FieldPickerGrid.getPrototype().getExpansionComponent,
saveAndCollapseRecord : isc.FieldPickerGrid.getPrototype().saveAndCollapseRecord,
cellHoverHTML :         isc.FieldPickerGrid.getPrototype().cellHoverHTML,
getCellValue :          isc.FieldPickerGrid.getPrototype().getCellValue

});

//////////////////////////////////////////////////////////////////////////////
// Class FieldPickerTreeGrid

isc.ClassFactory.defineClass("FieldPickerTreeGrid", "TreeGrid", "FieldPickerGrid");

isc.FieldPickerTreeGrid.addMethods({

createResultTree : function (criteria, callback, requestProperties) {
    var picker = this.parentElement.shuttle.picker;
    if (!requestProperties.dataProperties) requestProperties.dataProperties = {};
    isc.addProperties(requestProperties.dataProperties,{
        modelType: "parent",
        defaultIsFolder: true,
        reportCollisions: false,
        idField: picker.pickerIdField,
        parentIdField: "_" + picker.pickerParentIdField
    });

    var tree = this.Super("createResultTree", arguments);


    if (this.isObserving(this.data, "dataSourceDataChanged")) {
        this.ignore(this.data,"dataSourceDataChanged");
    }
    this.observe(tree, "dataSourceDataChanged", "observer.invalidateCacheIfMissingNodes()");

    return tree;
},

// access to the field records

getTotalFieldRecords : function () {
    this._nodeRecords = this.data.getDescendants();
    return this._nodeRecords.length;
},

getFieldRecord : function (rowNum) {
    return this._nodeRecords[rowNum];
},

clearFieldRecords : function () {
    delete this._nodeRecords;
},

// expansion

dataArrived : function (parentNode) {
    if (this.canExpandRecords && isc.isA.Object(parentNode)) {
        this._nodeRecords = this.data.getChildren(parentNode);
        this.setRecordExpansion(0, this._nodeRecords.getLength());
        delete this._nodeRecords;
    }
},

collapseAllRecords : function () {
    if (this.canExpandRecords) {
        var nodeList = this.data.getDescendants();
        this.collapseRecordList(nodeList);
    }
},

validateAllRecords : function () {
    if (this.canExpandRecords) {
        var nodeList = this.data.getDescendants();
        return this.validateRecordList(nodeList);
    }
    return true;
},

// detection of changes for save prompt

dragComplete: function () {
    var view = this.parentElement;
    view.shuttle.markForSave();
},

transferNodes : function (nodes, folder, index, sourceWidget) {

    var view      = this.parentElement,
        crossDrop = this != sourceWidget;

    this._filterDropRecords(nodes, crossDrop);


    if (crossDrop && !sourceWidget.validateRecordList(nodes)) nodes.clear();

    if (nodes.length > 0) view.shuttle.markForSave();

    sourceWidget._allowInvalidateCache = true;

    // collapse any open records before starting the drop
    if (crossDrop) {
        sourceWidget.collapseRecordList(nodes, true);
    }

    this.Super("transferNodes", arguments);
},

// force sync from DataSource if ResultTree is missing nodes

invalidateCacheIfMissingNodes : function () {
    if (this.data.getAllNodes().length <
        this.dataSource.cacheData.length) {
        // only invalidate the cache once per transfer
        if (!this._allowInvalidateCache) return;
        delete this._allowInvalidateCache;
        this.invalidateCache();
    }
},

// overrides from FieldPickerGrid

filterData :            isc.FieldPickerGrid.getPrototype().filterData,
expandRecord :          isc.FieldPickerGrid.getPrototype().expandRecord,
willAcceptDrop:         isc.FieldPickerGrid.getPrototype().willAcceptDrop,
setCanExpandRecords :   isc.FieldPickerGrid.getPrototype().setCanExpandRecords,
getExpansionComponent : isc.FieldPickerGrid.getPrototype().getExpansionComponent,
saveAndCollapseRecord : isc.FieldPickerGrid.getPrototype().saveAndCollapseRecord,
cellHoverHTML :         isc.FieldPickerGrid.getPrototype().cellHoverHTML,
getCellValue :          isc.FieldPickerGrid.getPrototype().getCellValue

});

//////////////////////////////////////////////////////////////////////////////
// Class FieldPickerGridView

isc.ClassFactory.defineClass("FieldPickerGridView", "VLayout");

isc.FieldPickerGridView.addMethods({

initWidget : function () {

    // call the superclass initWidget
    this.Super(this._$initWidget);

    var sectionStack = isc.SectionStack.create({
        height: 5,
        overflow: "visible",
        sections: [{ title: this.title,
                     controls: this.controls,
                     controlsLayoutProperties: {
                         membersMargin: 2, layoutEndMargin: 2
                     },
                     canCollapse: false }]
    });
    this.addMember(sectionStack);
},

addMember : function (newMember) {

    // call the superclass addMember
    this.Super("addMember", arguments);

    // create a reference to autochild ListGrid when added
    if (isc.isA.ListGrid(newMember)) this.grid = newMember;
},

expansionFormDefaults: {
    _constructor: "DynamicForm",
    width: 5,
    titleAlign: isc.Page.isRTL() ? "right" : "left",
    overflow: "visible",
    wrapItemTitles: false,
    validateOnExit: true,
    validateOnChange: true
},
createExpansionDynamicForm : function (editableProperties, record) {

    var fields = [],
        component = this.shuttle.picker.dataBoundComponent,
        dataSource = this.shuttle.picker.expansionDataSource,
        fieldNames = dataSource.getFieldNames();

    // add fields requested if they're in the expansion DataSource

    for (var i = 0; i < fieldNames.length; i++ ) {
        var name = fieldNames[i],
            show = editableProperties.contains(name);

        switch (name) {
        case "decimalPad":
        case "decimalPrecision":
            if (!isc.SimpleType.inheritsFrom(record.type, "float")) show = false;
            break;
        case "precision":

            if (!isc.SimpleType.inheritsFrom(record.type, "float") &&
                !isc.SimpleType.inheritsFrom(record.type, "integer")) show = false;
            break;
        }

        if (show) fields.add({name: name});
    }

    // allow summary/formula fields to be edited

    if (record.userSummary) {
        fields.add({
            type: "text",
            autoFit: true,
            endRow: false,
            record: record,
            shuttle: this.shuttle,
            editorType: "ButtonItem",
            name: "_fieldPickerUserSummary",
            title: component.editSummaryFieldText,
            click: function () {
                this.shuttle.confirmSaveOK('editSummaryField', this.record);
            }
        });
    }

    if (record.userFormula) {
        fields.add({
            type: "text",
            autoFit: true,
            endRow: false,
            record: record,
            shuttle: this.shuttle,
            editorType: "ButtonItem",
            name: "_fieldPickerUserFormula",
            title: component.editFormulaFieldText,
            click: function () {
                this.shuttle.confirmSaveOK('editFormulaField', this.record);
            }
        });
    }

    if (record.userSummary || record.userFormula) {
        fields.add({
            type: "text",
            autoFit: true,
            startRow: false,
            record: record,
            shuttle: this.shuttle,
            editorType: "ButtonItem",
            name: "_fieldPickerRemoveField",
            title: "Remove Field",
            click: function () {
                this.shuttle.confirmRemoveOK(this.record);
            }
        });
    }

    // create expansion form

    var form = this.createAutoChild("expansionForm", {
        dataSource: fields.length > 0 ? dataSource : null,
        fields: fields
    });
    form.setValues(record);

    return form;
},

cleanup : function () {
    this.grid.collapseAllRecords();
}

});

//////////////////////////////////////////////////////////////////////////////
// Class FieldPickerShuttle

isc.ClassFactory.defineClass("FieldPickerShuttle", "HLayout");

isc.FieldPickerShuttle.addMethods({

// unsaved modified field properties

changedRecords: {},

// apply all pending changes to the DataBoundComponent

applyCompleteFields : function (dataBoundComponent) {

    var i, field, record, completeFields = [];

    var shouldUseTrees = this.picker.shouldUseTrees(),
        parentIdField = this.picker.pickerParentIdField;

    // add the visible fields in the user-specified order

    var visibleFieldsGrid = this.currentView.grid,
        nVisibleRows = visibleFieldsGrid.getTotalFieldRecords();

    for (i = 0; i < nVisibleRows; i++) {
        record = visibleFieldsGrid.getFieldRecord(i);

        field = this.picker.getOriginalField(record);
        field.title = record._title;

        // apply any pending property changes from expanded records
        if (visibleFieldsGrid.isExpanded(record)) {
            var form = visibleFieldsGrid.getCurrentExpansionComponent(record);
            if (form && form.valuesHaveChanged()) {
                var values = form.getChangedValues();
                isc.addProperties(field, values);
                form.setValues(values);
            }
        }

        // commit any reparenting of nodes that has occurred
        if (shouldUseTrees) field[parentIdField] = record["_" + parentIdField];

        // XXX review: we have a number of possible settings and some components use one or
        // more of these.  Be absolutely explicit given that the user is telling us exactly
        // what they want
        field.showIf = "true";
        field.visible = true;
        field.hidden = false;
        completeFields.add(field);
    }

    visibleFieldsGrid.clearFieldRecords();

    // now add hidden fields since we must include all fields

    var hiddenFieldsGrid = this.availableView.grid,
        nHiddenRows = hiddenFieldsGrid.getTotalFieldRecords();

    for (i = 0; i < nHiddenRows; i++) {
        record = hiddenFieldsGrid.getFieldRecord(i);

        field = this.picker.getOriginalField(record);

        // commit any reparenting of nodes that has occurred
        if (shouldUseTrees) field[parentIdField] = record["_" + parentIdField];

        // XXX review: we have a number of possible settings and some components use one or
        // more of these.  Be absolutely explicit given that the user is telling us exactly
        // what they want
        field.showIf = "false";
        field.visible = false;
        field.hidden = true;
        completeFields.add(field);
    }

    hiddenFieldsGrid.clearFieldRecords();

    // add in any pending property changes from expansion editing

    for (i = 0; i < completeFields.length; i++ ) {
        var changes = this.changedRecords[completeFields[i].name];
        if (changes) isc.addProperties(completeFields[i], changes);
    }

    dataBoundComponent.setFields(completeFields);

    this.clearChanges();
},

// reset our state now to reflect no pending changes

clearChanges : function () {
    this.changedRecords = {};
    this.recordsMoved = false;
},

// track changes to the fields

markForSave : function () {
    this.recordsMoved = true;
},

requiresSave : function () {

    // check for open records not yet tracked
    var grid = this.currentView.grid,
        nVisibleRows = grid.getTotalFieldRecords();

    for (var i = 0; i < nVisibleRows; i++) {
        var record = grid.getFieldRecord(i);
        if (grid.isExpanded(record)) {
            var form = grid.getCurrentExpansionComponent(record);
            if (form && form.valuesHaveChanged()) return true;
        }
    }

    grid.clearFieldRecords();

    var changedRecordNames = isc.getKeys(this.changedRecords);
    return this.recordsMoved || changedRecordNames.length > 0;
},

// warn user when a field is about to be removed

confirmRemoveOK : function (record) {
    // OK since we're modal
    this._pendingArgument = record;

    isc.confirm(this.picker.removeText, {target: this, methodName: "completeConfirmRemoveOK"});
},

completeConfirmRemoveOK : function (value) {

    var record = this._pendingArgument;
    delete this._pendingArgument;

    if (value) this.currentView.grid.removeData(record);
},

// warn user if/when changes must be saved out

confirmSaveOK : function (methodName, argument) {

    // OK since we're modal
    this._pendingMethodName = methodName;
    this._pendingArgument   = argument;

    if (this.requiresSave()) {
        isc.confirm(this.picker.confirmText, {target: this, methodName: "completeSaveOK"});
    } else {
        this.completeSaveOK(true);
    }
},

completeSaveOK : function (value) {

    var methodName = this._pendingMethodName,
        argument = this._pendingArgument;

    delete this._pendingMethodName;
    delete this._pendingArgument;

    if (value) this.saveAndExecuteMethod(methodName, argument);
},

saveAndExecuteMethod : function (methodName, argument) {
    var picker = this.picker;

    // do not allow save if records (DBC fields) fail validation
    if (!picker.currentFieldsGrid.validateAllRecords()) return false;

    // ensure that title edits are not lost
    picker.currentFieldsGrid.endEditing();

    // clear any filters so that all records are applied
    picker.currentFieldsGrid.clearCriteriaIfPresent();
    picker.availableFieldsGrid.clearCriteriaIfPresent();

    picker.currentFieldsGrid.collapseAllRecords();
    this.applyCompleteFields(picker.dataBoundComponent);
    if (methodName != null) this.openEditor(methodName, argument);

    return true;
},

openEditor : function (methodName, argument) {

    var dataBoundComponent = this.picker.dataBoundComponent;
    dataBoundComponent[methodName](argument);

    var editBuilderField = true;

    switch(methodName) {
    case "addSummaryField":
    case "addFormulaField":
        editBuilderField = false;
        // fall through by design
    case "editSummaryField":
    case "editFormulaField":
        var window = dataBoundComponent.fieldEditorWindow;
        if (window) {
            var builder = window.items[0];
            if (editBuilderField) window.items[0].saveAddAnotherButton.hide();
            this.observe(builder, "fireOnClose", "observer.picker.refresh(observed)");
            builder.testRecord = this.picker.sampleRecord;
        }
        break;
    case "editHilites":
        var window = dataBoundComponent.hiliteWindow;
        if (window) {
            this.observe(window, "hide", "observer.picker.refresh(null, observed)");
        }
        break;
    }
},

// build tracker - two grids and directional arrows

availableViewDefaults: {
    _constructor: "FieldPickerGridView"
},
currentViewDefaults: {
    _constructor: "FieldPickerGridView"
},

buttonStackDefaults: {
    align: "center",
    overflow: "visible",
    layoutAlign: "center"
},

hilitesButtonConstructor: "IButton",
hilitesButtonDefaults: {
    autoFit: true,
    click: "this.shuttle.confirmSaveOK('editHilites')"
},

initWidget : function () {

    // call the superclass initWidget
    this.Super(this._$initWidget);

    var picker = this.picker,
        component = picker.dataBoundComponent;

    // create the grid for the available fields

    this.availableView = this.createAutoChild("availableView", {
        shuttle: this,
        controls: picker.availableFieldsHeaderControls,
        title: picker.availableFieldsTitle
    });

    // build list of buttons for the current fields

    var currentControls = [];
    var availableControls = picker.currentFieldsHeaderControls;
    if (availableControls == null) {
        availableControls = [];
        if (picker.showHilitesButton) availableControls.add("hilitesButton");
        if (component.canAddFormulaFields || component.canAddSummaryFields) availableControls.add("customFieldsMenuButton");
    }
    for (var i = 0; i < availableControls.length; i++) {
        var availableControl = availableControls[i];
        if (availableControl == "hilitesButton") {
            this.hilitesButton = this.createAutoChild("hilitesButton", {
                shuttle: this,
                title: picker.hilitesText
            });
            currentControls.add(this.hilitesButton);
        } else if (availableControl == "customFieldsMenuButton") {
            var items = [];
            if (component.canAddSummaryFields) items.add({
                shuttle: this,
                title: component.addSummaryFieldText,
                click: "item.shuttle.confirmSaveOK('addSummaryField')",
                icon: "[SKINIMG]ListGrid/formula_menuItem.png"
            });
            if (component.canAddFormulaFields) items.add({
                shuttle: this,
                title: component.addFormulaFieldText,
                click: "item.shuttle.confirmSaveOK('addFormulaField')",
                icon: "[SKINIMG]ListGrid/formula_menuItem.png"
            });
            currentControls.add(isc.MenuButton.create({
                autoFit: true,
                title: picker.addCustomFieldsButtonTitle,
                menu: isc.Menu.create({ data: items })
            }));
        } else if (isc.isA.Canvas(availableControl)) {
            availableControl.addProperties({
                shuttle: this,
                picker: picker
            });
            currentControls.add(availableControl);
        }
    }

    // now use the button list to create current fields grid

    this.currentView = this.createAutoChild("currentView", {
        shuttle: this,
        controls: currentControls,
        title: picker.currentFieldsTitle
    });

    this.initTransferArrows();

    this.addMembers([
        this.availableView, this.horizontalArrows,
        this.currentView,   this.verticalArrows
    ]);

},

// add the arrow stacks to the shuttle, with appropriate handlers
initTransferArrows : function () {

    var current = this.currentView,
        available = this.availableView;

    this.horizontalArrows = isc.VLayout.create({
        width: 1,
        members: [
            isc.ImgButton.create({
                size: 16, showDown:false,
                align: "center",
                src: "[SKINIMG]TransferIcons/right.png",
                click : function () {
                    current.grid.transferSelectedData(available.grid);
                }
            }),
            isc.LayoutSpacer.create({ height: 5 }),
            isc.ImgButton.create({
                size: 16, showDown:false,
                src: "[SKINIMG]TransferIcons/left.png",
                click : function () {
                    available.grid.transferSelectedData(current.grid);
                }
            })]
    }, this.buttonStackDefaults);

    var moveToBoundarySlot = function (direction) {
        var grid = current.grid,
            selection = grid.getSelection() || [],
            targetIndex = direction == -1 ? 0 : grid.getTotalRows();
        grid.transferRecords(selection, null, targetIndex, grid);
        grid.scrollToRow(targetIndex);
    };
    var moveByOneSlot = function (direction) {
        var grid = current.grid,
            selection = grid.getContiguousSelection();
        if (selection) {
            var targetIndex = grid.getRecordSetBounds(selection, direction, 1);
            grid.transferRecords(selection, null, targetIndex, grid);
            grid.scrollToRow(targetIndex - (direction + 1) / 2);
        }
    };

    if (this.picker.showFieldOrderButtons) {
        this.verticalArrows = isc.VLayout.create({
            width: 1,
            members: [
                isc.ImgButton.create({
                    size: 16, showDown:false,
                    src: "[SKINIMG]TransferIcons/up_first.png",
                    click : function () { moveToBoundarySlot(-1); }
                }),
                isc.LayoutSpacer.create({ height: 5 }),
                isc.ImgButton.create({
                    size: 16, showDown:false,
                    src: "[SKINIMG]TransferIcons/up.png",
                    click : function () { moveByOneSlot(-1); }
                }),
                isc.LayoutSpacer.create({ height: 5 }),
                isc.ImgButton.create({
                    size: 16, showDown:false,
                    src: "[SKINIMG]TransferIcons/down.png",
                    click : function () { moveByOneSlot(1); }
                }),
                isc.LayoutSpacer.create({ height: 5 }),
                isc.ImgButton.create({
                    size: 16, showDown:false,
                    src: "[SKINIMG]TransferIcons/down_last.png",
                    click : function () { moveToBoundarySlot(1); }
                })]

        }, this.buttonStackDefaults);
    }
},

cleanup : function () {
    this.currentView.cleanup();
    this.clearChanges();
}

});

//////////////////////////////////////////////////////////////////////////////
// Class FieldPicker

isc.ClassFactory.defineClass("FieldPicker", "VLayout");

//> @class FieldPicker
// FieldPicker provides a configuration dialog that displays, side-by-side, the available and
// currently-displayed fields of a +link{dataBoundComponent}. It allows for easy customization
// of the order in which the fields of a +link{dataBoundComponent} are displayed, and of which
// are visible.  If so configured, it also allows for convenient launching of the HiliteEditor,
// FormulaBuilder, and SummaryBuilder.  A FieldPicker instance runs in its own window,
// a +link{fieldPickerWindow}
// @treeLocation Client Reference/Data Binding
// @visibility external
//<

isc.FieldPicker.addProperties({

layoutMargin: 10,
membersMargin: 10,
originalFields: {},

//> @attr fieldPicker.dataBoundComponent (Canvas : null : IR)
// The component whose fields should be edited.
// @visibility external
//<
dataBoundComponent : null,

//> @attr fieldPicker.dataSource (DataSource : null : IR)
// An optional DataSource that is used to create a disposable
// +link{fieldPicker.dataBoundComponent} if none is provided.
// Has no effect if a +link{fieldPicker.dataBoundComponent} is specified.
// @visibility external
//<

//> @method fieldPicker.callback
// Callback invoked when picker changes are committed, if a disposable
// +link{dataBoundComponent} is present.
// @param fields (Array of ListGridFields) committed fields from disposable component
// @param hilites (Array of Hilite) Array of hilite objects
// @see fieldPicker.dataSource
// @visibility external
//<

//> @attr fieldPicker.sampleRecord (Record|"first"|"firstOpenLeaf" : "first" : IR)
// If a <code>sampleRecord</code> is provided, the FieldPicker will show a second column in the
// Current Fields dialog showing the cell value that will appear for that field given the
// provided sample record.
// <br>
// A value of "first" means the first record.  If the underlying
// +link{fieldPicker.dataBoundComponent} is a +link{TreeGrid}, you can specify "firstOpenLeaf"
// to use the first open leaf as the sampleRecord (this is often desirable in trees where the
// first record may be a folder that's used for organizational purposes only and hence would
// have no actual data for columns other than the tree column).
// @visibility external
//<
sampleRecord: "first",

//> @attr fieldPicker.showHilitesButton (boolean : true : IR)
// Shows a "Highlights..." button that shows an interface for editing hilites in the attached
// DataBoundComponent.  If unset, defaults to the value of
// +link{DataBoundComponent.canEditHilites, canEditHilites} on the associated component.
// @visibility external
//<

//showHilitesButton: true,

//> @attr fieldPicker.showFieldOrderButtons (boolean : true : IR)
// When set to false, hides the right-most set of buttons, used for re-ordering fields in the
// Visible Fields list.
// @visibility external
//<
showFieldOrderButtons: true,

//> @attr fieldPicker.hilitesText (String : "Highlights..." : [IR])
// @group i18nMessages
// @visibility external
//<
hilitesText: "Highlights...",

//> @attr fieldPicker.availableFieldsTitle (String : "Available Fields" : [IR])
// @group i18nMessages
// @visibility external
//<
availableFieldsTitle: "Available Fields",

//> @attr fieldPicker.currentFieldsTitle (String : "Visible Fields" : [IR])
// @group i18nMessages
// @visibility external
//<
currentFieldsTitle: "Visible Fields",

//> @attr fieldPicker.addCustomFieldsButtonTitle (String : "Add Custom Fields" : IR)
// The title displayed for the Add Custom Fields Button
// @group i18nMessages
// @visibility external
//<
addCustomFieldsButtonTitle: "Add Custom Fields",

//> @attr fieldPicker.availableTitleTitle (String : "Name" : IR)
// The title displayed for the title property of the available fields
// @group i18nMessages
// @visibility external
//<
availableTitleTitle: "Name",

//> @attr fieldPicker.currentTitleTitle (String : "Field Title" : IR)
// The title displayed for the title property of the current fields
// @group i18nMessages
// @visibility external
//<
currentTitleTitle: "Field Title",

//> @attr fieldPicker.sampleValueTitle (String : "Sample Value" : IR)
// The title displayed for the sample value property of the current fields
// @group i18nMessages
// @visibility external
//<
sampleValueTitle: "Sample Value",

//> @attr fieldPicker.sampleValueField (String : "_sampleValue" : IR)
// The name used for the sample value property of the complete fields
//<
sampleValueField: "_sampleValue",


//> @attr fieldPicker.showAvailableSampleValue (Boolean : true : IR)
// When set to true, also show the sample value in the available fields list.  Set to false to
// show field names only.
//<
showAvailableSampleValue: true,

//> @attr fieldPicker.showSampleValues (Boolean : true : IR)
// When set to false, sample values are never shown. This property applies to the entire FieldPicker.
//<
showSampleValues: true,

//> @attr fieldPicker.confirmText (String : "Must save pending changes to proceed. OK?" : [IR])
// @group i18nMessages
// @visibility external
//<
confirmText: "Must save pending changes to proceed. OK?",

//> @attr fieldPicker.removeText (String : "You are about to remove the field. Are you sure?" : [IR])
// @group i18nMessages
// @visibility external
//<
removeText: "You are about to remove the field. Are you sure?",

//> @attr fieldPicker.instructions (String : "Drag and drop or use arrows to move fields.  Drag reorder to change field order." : [IR])
// @group i18nMessages
// @visibility external
//<
instructions: "Drag and drop or use arrows to move fields.  Drag reorder to change field order.",

//> @attr fieldPicker.saveAndExitButtonTitle (String : "Apply" : IR)
// The title shown on the Save and Exit button
// @group i18nMessages
// @visibility external
//<
saveAndExitButtonTitle: "Apply",

//> @attr fieldPicker.cancelButtonTitle (String : "Cancel" : IR)
// The title shown on the Cancel button
// @group i18nMessages
// @visibility external
//<
cancelButtonTitle: "Cancel",

//> @attr fieldPicker.removeItemTitle (String : "Remove" : IR)
//The title shown on the 'Visible Fields' grid's context menu item, whose click handler puts the
//selected item back in the 'Available Fields' collection.
//@group i18nMessages
//@visibility external
//<
removeItemTitle: "Remove",


//> @attr fieldPicker.emptyTitleHint (String : "[No title specified]" : IR)
// The hint shown when editing a field with no title defined.
// @group i18nMessages
// @visibility external
//<
emptyTitleHint: "[No title specified]",

//> @attr fieldPicker.showFieldPrompts (boolean : true : IR)
// If a +link{listGridField.prompt,prompt} is specified for the field, should it be
// displayed when the user hovers over the record correlating to the field in the
// fields grids?
// @visibility fieldPicker
//<
showFieldPrompts:true,

//> @attr fieldPicker.sortAvailableFields (boolean : true : IR)
// Should the available fields grid be sorted by default? Grid will be sorted by
// +link{fieldPicker.availableFieldsSortDirection}.
// @visibility fieldPicker
//<
sortAvailableFields:true,

//> @attr fieldPicker.availableFieldsSortDirection (SortDirection : "ascending" : IR)
// If +link{fieldPicker.sortAvailableFields} is <code>true</code>, this property will
// govern the sort-direction for the initial sort applied to the available fields grid.
// @visibility fieldPicker
//<
availableFieldsSortDirection:"ascending",

//> @attr fieldPicker.availableFieldsHeaderControls (Array of Canvas : null : IR)
// Provides a set of controls to appear as
// +link{sectionHeader.controls,section header controls} above the available fields grid.
// @visibility external
//<
availableFieldsHeaderControls: null,

availableFieldsGridDefaults : {
    canGroupBy: false,
    dataFetchMode: "basic",
    dragDataAction: "move",
    canFreezeFields: false,
    showFilterEditor: true,
    filterOnKeypress: true,
    canDragRecordsOut: true,
    loadDataOnDemand: false,
    dragRecategorize: "never",
    keepParentsOnFilter: true,
    autoFitWidthApproach: "both",
    useAllDataSourceFields: true,
    canAcceptDroppedRecords: true,
    createDefaultTreeField: false,
    recordEnabledProperty: "_enabled",
    autoFetchTextMatchStyle: "substring"
},

getOriginalField : function (pickerRecord) {
    return this.originalFields[pickerRecord[this.primaryKeyField]];
},

// datasource determines whether tree or list is used for picking field

shouldUseTrees : function () {
    if (this.dataBoundComponent) {
        var dataSource = this.dataBoundComponent.dataSource;
        return dataSource != null && !!dataSource.showFieldsAsTree;
    }
    return false;
},

// copy the list of supplied fields for use by the FieldPicker; validate fields

duplicateAndValidateFields : function (fields) {
    var result = [],
        component = this.dataBoundComponent,
        canReorder = !isc.isA.ListGrid(component) || component.canReorderFields;

    for (var i = 0; i < fields.length; i++) {
        var field = fields[i];
        if (!field.excludeFromState) result.add(field);
        if ((field.canReorder == false || field.canReorder == null && !canReorder) &&
            field.canHide == false) field._enabled = false;
    }
    return result;
},

// create a DataSource for the available or current fields ListGrid

createDataSourceFromFields : function (fields, exclusions, available, skipDuplicate) {
    var includedFields = skipDuplicate ? fields : this.duplicateAndValidateFields(fields);

    if (exclusions != null) includedFields.removeList(exclusions);

    var fields = [],
        component = this.dataBoundComponent;


    if (available) {
        includedFields = includedFields.filter(function (field) {
            return field.canHide != false;
        });
    }

    var auto, title = available ? this.availableTitleTitle: this.currentTitleTitle;

    if (isc.isA.String(this.sampleRecord)) {
        if (this.sampleRecord == "first") {

            if (isc.isA.ListGrid(component)) {
                if (component.fieldPickerShowSampleValues) {
                    var data = component.getOriginalData();
                    this.sampleRecord = data && data.getLength() > 0 ? data.get(0) : null;
                }
            } else if (this.showSampleValues) {
                this.sampleRecord = component.getRecord(0);
            }
            // ignore e.g. loading markers
            if (!isc.isAn.Object(this.sampleRecord)) this.sampleRecord = null;
        } else if (this.sampleRecord == "firstOpenLeaf" && isc.isA.TreeGrid(component)) {
            var data = component.getData();
            var openList = data.getOpenList(data.getRoot(), isc.Tree.LEAVES_ONLY);
            if (openList.length) this.sampleRecord = openList.get(0);
            else this.sampleRecord = component.getRecord(0);
        }
    }

    if (isc.isA.Object(this.sampleRecord)) {
        for (var i = 0; i < includedFields.length; i++) {
            var field = includedFields[i];
            field[this.sampleValueField] =
                component.getStandaloneFieldValue(this.sampleRecord, field.name);
        }
        if (!available || this.showAvailableSampleValue) {
            fields.add({
                name: this.sampleValueField,
                title: this.sampleValueTitle,
                canFilter: false,
                canEdit: false,
                type: "any"
            });
            auto = true;
        }
    }

    var nameField = {name: "name",   title: title, autoFitWidth: auto, primaryKey: true},
       titleField = {name: "_title", title: title, autoFitWidth: auto,
                     editorProperties: { showHintInField: true, hint: this.emptyTitleHint},
                     canEdit: this.canEditTitles && !available};

    if (this.useTitleField) { nameField.hidden = true; titleField.treeField = true; }
    else                    { titleField.hidden = true; nameField.treeField = true; }

    fields.addListAt([nameField, titleField], 0);

    // if picker is in tree mode, add required extra dataSource fields

    if (this.shouldUseTrees()) {
        var id       = this.pickerIdField,
            parentId = this.pickerParentIdField,
            rootValue = component.dataSource.fieldTreeRootValue;

        fields.addList([
            {name: id,             hidden: true, primaryKey: true },
            {name: "_" + parentId, hidden: true, foreignKey: id, rootValue: rootValue}]);

        if (this.useTitleField) titleField.canFilter = true;
        else                    nameField.canFilter  = true;

        delete nameField.primaryKey;

        includedFields.map(function (field) { field["_" + parentId] = field[parentId]; });
    }

    // our DataSources may receive copied records, so store originals

    for (var i = 0; i < includedFields.length; i++) {
        var field = includedFields[i];
        this.originalFields[field[this.primaryKeyField]] = field;
        if (field.userSummary || field.userFormula) this.customFields = true;
        field._title = field.title;
    }

    var dataSource = isc.DataSource.create({
        fields: fields,
        clientOnly: true,
        dataProtocol: "clientCustom",
        transformRequest : function (dsRequest) {
            var dsResponse = this.getClientOnlyResponse(dsRequest, null);
            this.processResponse(dsRequest.requestId, dsResponse);
            return dsRequest.data;
        }
    });

    dataSource.setCacheData(includedFields);
    return dataSource;
},

// create the DataSource for use by the DynamicForm record editor

createExpansionDataSource : function () {





    var fields = [{ name: "frozen",
                    title: isc.FieldPickerField.frozenTitle,
                    showTitle: false,
                    type: "boolean" },
                  { name: "precision", validators: [
                        { type: "regexp", errorMessage:
                          "Must be between 1 and 21 inclusive",
                          expression: "^([1-9]|1[0-9]|2[0-1])?$" }
                    ],
                    title: isc.FieldPickerField.precisionTitle,
                    type: "integer" },
                  { name: "decimalPrecision",
                    title: isc.FieldPickerField.decimalPrecisionTitle,
                    type: "integer" },
                  { name: "decimalPad", validators: [
                        { type: "regexp", errorMessage:
                          "Must be between 0 and 20 inclusive",
                          expression: "^([0-9]|1[0-9]|20)?$"}
                    ],
                    title: isc.FieldPickerField.decimalPadTitle,
                    type: "integer" },
                  { name: "align",
                    title: isc.FieldPickerField.alignTitle,
                    valueMap: ["left", "center", "right"]},
                  { name: "cellAlign",
                    title: isc.FieldPickerField.cellAlignTitle,
                    valueMap: ["left", "center", "right"]}];

    // remove inappropriate properties for the DetailViewer

    if (isc.isA.DetailViewer(this.dataBoundComponent)) {
        var filterFunction = function (object) {
            switch(object.name) {
            case "frozen":
            case "align":
            case "cellAlign":
                return false;
            default:
                return true;
            }};
        fields = fields.filter(filterFunction);
    }

    this.expansionDataSource = isc.DataSource.create({
        clientOnly: true,
        fields: fields
    });
},

updateFieldConfiguration: function (firstField) {
    this.useTitleField = firstField && firstField.title != null;
    this.primaryKeyField = this.shouldUseTrees() ? this.pickerIdField : "name";
},

updateEditableProperties : function () {
    var properties = this.dataBoundComponent.fieldPickerFieldProperties,
        grid = this.currentFieldsGrid;

    isc.addProperties( grid, {
        editableProperties: properties || [],
        autoFitFieldWidths: isc.isA.Object(this.sampleRecord)
    });
    // expansion needed if either editable field properties or summary/formula fields present
    grid.setCanExpandRecords(isc.isA.Array(properties) && properties.length > 0 ||
                             this.customFields);
},

// validate DataSourceField properties

validateDataSourceField : function (field) {
    // validate argument to dataSourceField.toPrecision
    var precision = field.precision;
    if (precision != null && (precision < 1 || precision > 21)) return false;

    // validate argument to dataSourceField.toFixed
    var decimalPad = field.decimalPad;
    if (decimalPad != null && (decimalPad < 0 || decimalPad > 20)) return false;

    return true;
},

// update the FieldPicker to deal with a changed DataBoundComponent

refresh : function (builder, hiliter) {

    if (builder) {
        var shuttle = this.shuttle;
        shuttle.ignore(builder, "fireOnClose");
        var newWindow = this.dataBoundComponent.fieldEditorWindow;
        // add new observer if builder window has respawned via "save & add another"
        if (!newWindow.destroying && !newWindow.destroyed) {
            shuttle.observe(newWindow.items[0], "fireOnClose",
                            "observer.picker.refresh(observed)");
        }
    }
    if (hiliter) { this.shuttle.ignore(hiliter, "hide"); }

    var currentFields  = this.dataBoundComponent.fields,
        completeFields = this.dataBoundComponent.completeFields,
        dataSource;

    this.updateFieldConfiguration(completeFields[0]);

    this.availableFieldsGrid.setDataSource(
        this.createDataSourceFromFields(completeFields, currentFields, true));
    this.availableFieldsGrid.fetchData();

    this.currentFieldsGrid.setDataSource(
        this.createDataSourceFromFields(currentFields));
    this.currentFieldsGrid.fetchData();

    this.updateEditableProperties();

    this.needsRefresh = false;
},

initWidget : function () {

    // call the superclass initWidget
    this.Super(this._$initWidget);

    // If no DataBoundComponent is supplied, create a disposable DBC based on the
    // fields of supplied DataSource, and return DBC fields, etc. in a callback
    if (this.dataBoundComponent == null && this.dataSource != null) {
        var picker = this,
            properties = isc.addProperties({}, this.creator.fieldPickerProperties, {
                autoDraw: false,
                fieldStateChanged : function () {
                    if (isc.isA.Function(picker.callback)) {
                        picker.callback(this.completeFields, this.getHilites());
                    }
                }}
            );
        var component = isc.ListGrid.create(properties);
        component.setFields(component.fields);
        this.dataBoundComponent = component;
    }

    if (this.showHilitesButton == null) {
        var dbc = this.dataBoundComponent;
        this.showHilitesButton = dbc && dbc.canEditHilites;
    }

    this.addMember( isc.Label.create({
        height: 5,
        overflow: "visible",
        contents: this.instructions
    }));

    // create the shuttle widget for dragging/dropping fields

    var shuttle = isc.FieldPickerShuttle.create({
        picker: this,
        membersMargin: 10
    });
    this.shuttle = shuttle;
    this.addMember( shuttle );

    // AutoChildren class depends upon the DataSource properties
    var gridClass = this.shouldUseTrees() ? isc.FieldPickerTreeGrid :
                                            isc.FieldPickerListGrid;

    // create the ListGrid AutoChildren available/current Fields

    var currentFields  = this.dataBoundComponent.fields,
        completeFields = this.dataBoundComponent.completeFields;

    this.updateFieldConfiguration(completeFields[0]);

    var availableFieldsSort;
    if (this.sortAvailableFields) {
        availableFieldsSort = [{
            property:"_title",
            direction:this.availableFieldsSortDirection
        }];
    }

    this.addAutoChild("availableFieldsGrid", {
        autoFetchData: true,
        initialSort:availableFieldsSort,
        autoFitExpandField: this.showAvailableSampleValue ? this.sampleValueField : null,
        dataSource: this.createDataSourceFromFields(completeFields, currentFields, true),
        rowDoubleClick : function (record, recordNum, fieldNum) {
            var current = shuttle.currentView.grid;
            current.transferSelectedData(this);
        }
    }, gridClass, shuttle.availableView);

    this.addAutoChild("currentFieldsGrid", {
        autoFetchData: true,
        autoFitExpandField: this.sampleValueField,
        dataSource: this.createDataSourceFromFields(currentFields),
        contextMenu : isc.Menu.create({
            autoDraw: false,
            data : [{
                title : this.removeItemTitle,
                click : function () {
                    var current = shuttle.currentView.grid;
                    var available = shuttle.availableView.grid;
                    available.transferSelectedData(current);
                }
            }]
        })
    }, gridClass, shuttle.currentView);

    this.createExpansionDataSource();
    this.updateEditableProperties();

    // create save/cancel buttons

    this.addMember( isc.HLayout.create({

    height: 5,
    align: "right",
    overflow: "visible",
    membersMargin: 10,
        defaultLayoutAlign: "center",

        members: [
            this.createAutoChild("saveAndExitButton", {
                picker: this,
                title: this.saveAndExitButtonTitle
            }),
            this.createAutoChild("cancelChangesButton", {
                picker: this,
                title: this.cancelButtonTitle
            })
        ]
    }));
},
saveAndExitButtonDefaults: {
    _constructor: "IButton",
    click: "this.picker.saveClick()"
},
cancelChangesButtonDefaults: {
    _constructor: "IButton",
    click: "this.picker.closeClick()"

},

//> @method fieldPicker.setAvailableFields()
// Provides a new set of available fields.
// @param newFields (Array of DataSourceField)
// @visibility external
//<
setAvailableFields : function (newFields) {

    var i, undef, fieldPool = {};

    for (i = 0; i < newFields.length; i++) {
        var newField = newFields[i];
        fieldPool[newField.name] = newField;
    }

    var currentGrid = this.currentFieldsGrid,
        currentFields = currentGrid.dataSource.cacheData;

    var newCurrentFields = [];
    for (var i = 0; i < currentFields.length; i++) {
        var field = currentFields[i];
        if (fieldPool[field.name] !== undef) {
            newCurrentFields.add(field);
            delete fieldPool[field.name];
        }
    }
    this.currentFieldsGrid.setDataSource(
        this.createDataSourceFromFields(newCurrentFields, null, false, true));
    this.currentFieldsGrid.fetchData();

    var availableGrid = this.availableFieldsGrid,
        availableFields = availableGrid.dataSource.cacheData;

    var newAvailableFields = [];
    for (var i = 0; i < availableFields.length; i++) {
        var field = availableFields[i];
        if (fieldPool[field.name] !== undef) {
            newAvailableFields.add(field);
            delete fieldPool[field.name];
        }
    }

    for (var name in fieldPool) {
        if (fieldPool.hasOwnProperty(name)) {
            var title = isc.DataSource.getAutoTitle(name);
            newAvailableFields.add({name: name, title: title });
        }
    }

    this.availableFieldsGrid.setDataSource(
        this.createDataSourceFromFields(newAvailableFields, null, true, true));
    this.availableFieldsGrid.fetchData();
},

// save/cancel handling and cleanup

saveClick : function () {
    // only notify the DBC of the field state change and close the
    // window if the save succeeded; otherwise validation failed
    if (this.shuttle.saveAndExecuteMethod()) {
        var component = this.dataBoundComponent;
        component.markForRedraw();
        // not all DBC's define this method!
        if (component.handleFieldStateChanged) {
            component.handleFieldStateChanged();
        }
        // if there's a callback installed, fire it now
        if (this.callback) this.callback(component.completeFields, component.getHilites());
        this.creator.closeClick();
    }
},

closeClick : function () {},

cleanup: function () {

    this.shuttle.cleanup();

    var completeFields = this.dataBoundComponent.completeFields,
        canExpandProperty = this.currentFieldsGrid.canExpandrecordProperty;

    for (var i = 0 ; i < completeFields.length; i++) {
        var field = completeFields[i];
        delete field[this.sampleValueField];
        delete field[canExpandProperty];
        delete field["_" + this.pickerParentIdField];
        delete field._enabled;
        delete field._title;
    }

    this.originalFields = {};
}

});

// Since object initialization order in JavaScript is not specified, initialize
// the currentFieldsGrid AutoChild default properties here to allow us to reuse
// the availableFieldsGrid AutoChild default properties.

isc.FieldPicker.addProperties({

currentFieldsGridDefaults : isc.addProperties(
    isc.shallowClone(isc.FieldPicker.getPrototype().availableFieldsGridDefaults), {
        canSort: false,
        expansionMode: "editor",
        canReparentNodes: true,
        canReorderRecords: true,
        canExpandRecordProperty: "_canExpand"
    })

});

//////////////////////////////////////////////////////////////////////////////
// Class FieldPickerWindow
//> @class FieldPickerWindow
// A dialog for picking fields to display from among the available fields.
// <p>
// This is typically useful in scenarios where there are many more fields than can reasonably
// fit on screen. The application can start off displaying a few of the fields by default (such
// as the most commonly-needed fields), and show a FieldPickerWindow to allow the user to
// customize which fields to display as well as the order in which to display them.
// @example fieldPicker
// @treeLocation Client Reference/Data Binding/FieldPicker
// @visibility external
//<

if (isc.Window != null) {

    isc.ClassFactory.defineClass("FieldPickerWindow", "Window");

    isc.FieldPickerWindow.addProperties({

    //> @attr fieldPickerWindow.title (String : "Field Picker" : [IR])
    // @group i18nMessages
    // @visibility external
    //<
    title: "Field Picker",

    width: 800,
    height: 425,

    isModal: true,
    canDragResize: true,

    // autoCenter by default, rather than calling centerInPage() at draw time - means
    // devs can override the centering behavior
    autoCenter: true,

    // defaults for AutoChild fieldPicker

    fieldPickerDefaults: {
        autoParent: "none",
        _constructor: isc.FieldPicker,
        pickerIdField: "fieldTreeId",
        pickerParentIdField: "fieldTreeParentId"
    },

    initWidget : function () {

        // call the superclass initWidget
        this.Super(this._$initWidget);

        this.addAutoChild("fieldPicker");
        this.addItem(this.fieldPicker);

        this.observe(this.fieldPicker, "closeClick", "observer.closeClick()");
    },

    show : function () {
        if (this.needsRefresh) this.fieldPicker.refresh();
        return this.Super("show", arguments);
    },

    closeClick : function () {
        this.fieldPicker.cleanup();
        this.needsRefresh = true;
        return this.Super("closeClick", arguments);
    }

    });
} else {
    isc.Log.logInfo("Source for standard FieldPickerWindow class included in this module, but required " +
        "related class (Window) is not loaded. This can occur if the Grid module is " +
        "loaded without the Containers module.", "moduleDependencies");

}






//>    @class    ButtonStrip
//
//  Lightweight version of the toolbar based on the GridRenderer
// @visibility internal
//<

isc.ClassFactory.defineClass("ButtonStrip", "GridRenderer");

isc.ButtonStrip.addProperties({
    buttons:[],
    buttonDefaults:{},

    // space / padding for buttons

    buttonSpacing : 3,
    buttonPadding : 2,

    // Height / width - overridden by explicit settings on the buttons, and depends on
    // 'fillSpace' policy
    defaultButtonHeight : 20,
    defaultButtonWidth : 100,

    vertical:false,
    overflow:isc.Canvas.VISIBLE,

    // show per-cell rollover, since each cell represents a button
    showRollOver:true,
    useCellRollOvers:true,

    // set up cell-level drag selection
    canDrag:false,
    canDragSelect:false,
    canSelectCells:false,

    // for faster styling updates in IE
    fastCellUpdates : true,

    // title align - default alignment for button titles
    // (can also set 'align' directly on the button instead)
    // NOTE: aligning each button differently not supported for vertical buttonStrips
    titleAlign : "center",

    // Do the buttons fill the strip, or just expand to accommodate their titles?
    fillSpace : false,

    // we want background transparency by default
    backgroundColor: null,

    baseStyle : "button"
});

//!>Deferred
isc.ButtonStrip.addMethods({

    initWidget : function () {
        this.Super(this._$initWidget);

        if (this.buttonPadding != null) this.cellPadding = this.buttonPadding;
        if (this.buttonSpacing != null) this.cellSpacing = this.buttonSpacing;

        // heights and widths
        var buttonHeights = [];

        for (var i = 0; i < this.buttons.length; i++) {
            this.buttons[i] = isc.addProperties(
                {},
                this.buttonDefaults,
                this.buttons[i]
            );

            // for vertical bars use any specified heights for buttons
            // Not currently supported for horizontal bars.
            // Note: these buttons are not Button widgets, so no need to use 'getHeight()' here
            if (this.vertical) buttonHeights[i] = this.buttons[i].height;
        }
        if (!this.vertical) {
            buttonHeights[0] = this.fillSpace ? this.getHeight() : this.defaultButtonHeight;
        } else {
            if (this.fillSpace) {
                buttonHeights = isc.Canvas.applyStretchResizePolicy(buttonHeights, this.getHeight());
            } else {
                for (var i =0; i < buttonHeights.length; i++) {
                    if (buttonHeights[i] == null) buttonHeights[i] = this.defaultButtonHeight;
                }
            }
        }

        // Hang the heights on the buttons.
        for (var i = 0; i < this.buttons.length; i++) {
            this.buttons[i].height =
                (buttonHeights[i] != null ? buttonHeights[i] : this.defaultButtonHeight);
        }

        // this.fields is used to determine the number of columns, and apply properties to them
        // including widths

        // If we're vertical, we set up a single field, and we'll size all buttons to the same
        // width (for now)
        if (this.vertical) this.fields = [{}];
        else this.fields = this.buttons;

        // allow the buttons to grow if 'fillSpace' is true.
        this.fixedColumnWidths = this.fillSpace;

        // set up button widths using 'setFieldWidths'
        var buttonWidths = [];
        for (var i = 0; i < this.fields.length; i++) {
            buttonWidths[i] = this.fields[i].width;
            // XXX this allows you to set titleAlign for the whole strip, but not each button.
            if (this.fields[i].align == null) this.fields[i].align = this.titleAlign;
        }

        if (this.fillSpace)
            buttonWidths = isc.Canvas.applyStretchResizePolicy(buttonWidths, this.getWidth())

        for (var i = 0; i < buttonWidths.length; i++) {
            if (buttonWidths[i] == null) buttonWidths[i] = this.defaultButtonWidth;
            buttonWidths[i] -= this.cellSpacing;
        }
        this.setColumnWidths(buttonWidths);

    },

    getButton : function (index) {
        return this.buttons[index];
    },

    // a record is basically a button
    getCellRecord : function (rowNum, colNum) {
        var index = (this.vertical ? rowNum : colNum);
        return this.getButton(index);
    },

    // cell values are button titles
    getCellValue : function (record, rowNum, colNum) {
        // not sure why this is necessary!
        if (record == null) record = this.getCellRecord(rowNum, colNum);

        return record.getTitle ? record.getTitle() : record.title;
    },

    // total rows = number of buttons, if vertical, or 1 if horizontal
    getTotalRows : function () {
        return (this.vertical ? this.buttons.length : 1);
    },

    // These heights got set up on widget init
    getRowHeight : function (record, rowNum) {
        // Note - we draw all horizontal buttons the same height (for now)
        if (!this.vertical) return this.buttons[0].height;
        return record.height;
    },

    // Override cellClick to fire click-handlers on the buttons.
    cellClick : function (record, rowNum, colNum) {
        // Just call any click handler defined on the buttons (with no arguments)
        if (record.click) {
            if (isc.isA.String(record.click))
                record.click = isc.Func.expressionToFunction("", record.click);
            record.click();
        }
        if (this.itemClick) this.itemClick(record, this.buttons.indexOf(record))

    }
});

isc.ButtonStrip.registerStringMethods({
    // itemClick handler for when an item is clicked
    itemClick:"item,itemNum"
});
//!<Deferred










//>    @class    RecordEditor
//
//  Component for editing a single record.<br>
//  RecordEditors are implemented as a subclass of ListGrid, showing no header and a single
//  row always drawn in the editable state, allowing the user to modify the values at any time.
//  The +link{recordEditor.actionButton} is automatically shown as a way for the user to act
//  upon the edited values.
//  <P>
//  The RecordEditor class exists as a helper class for ListGrids, used to provide
//  an interface for editing criteria when +link{listGrid.showFilterEditor,filterEditor}
//  is set to true.
//
// @see listGrid.showFilterEditor
// @see listGrid.filterEditor
// @treeLocation Client Reference/Grids/ListGrid
// @visibility external
//<


// declare the class itself
isc.ClassFactory.defineClass("RecordEditor", "ListGrid");

isc.RecordEditor.addProperties({

    //>    @attr    recordEditor.sourceWidget   (ListGrid : undefined : R)
    //          Target ListGrid widget for which we're editing data.  Should be defined on
    //          a per-instance basis at init time.
    //<
    //    sourceWidget:null,

    autoDraw:false,

    cellSpacing:0, cellPadding:0,

    selectionType:"none",
    showRollOver:false,
    //> @attr recordEditor.baseStyle (CSSStyleName : "recordEditorCell" : [IR])
    // @include listGrid.baseStyle
    // @visibility external
    //<
    baseStyle:"recordEditorCell",

    // Don't show the header for the list
    showHeader:false,

    // If we're set up with no fields, we won't be showing the edit row until setFields is
    // called. In this case don't show the empty message.
    showEmptyMessage:false,



    bodyOverflow:"hidden",

    fixedRecordHeights:true,

    drawAllMaxCells:0,

    // disable all field autoFit on a record editor - we'll size fields based on our
    // target grid.
    shouldAutoFitField:function () {return false},

    //> @attr recordEditor.skinImgDir (URL : "images/RecordEditor/" : IR)
    //     Where do 'skin' images (those provided with the class) live?
    // @visibility external
    //<
    // [Note we are not showing a header so do not need access to the standard listGrid images]
    skinImgDir:"images/RecordEditor/",

    //> @attr recordEditor.saveImg (SCImgURL : "[SKIN]add.png" : IR)
    // +link{Button.icon,Icon} to show on the +link{actionButton} if this
    // component is being used for editing records
    // <P>
    // Note that this +link{SCImgURL} will be resolved using the +link{skinImgDir} defined
    // for the RecordEditor.
    //
    // @visibility internal
    //<
    // Internal for now - we currently have no external usage for RecordEditors as
    // true "editors".
    saveImg:"[SKIN]add.png",

    //> @attr recordEditor.filterImg (SCImgURL : "[SKIN]filter.png" : IR)
    // +link{Button.icon,Icon} to show on the +link{actionButton} if this
    // component is being used as a +link{listGrid.filterEditor}.
    // <P>
    // Note that this +link{SCImgURL} will be resolved using the +link{skinImgDir} defined
    // for the RecordEditor.
    //
    // @visibility external
    //<
    filterImg:{src:"[SKIN]filter.png", width:50, height:50, showOver:true},

    //> @attr recordEditor.actionButton (Button AutoChild : null : R)
    // Automatically created Button auto-child shown at the edge of the recordEditor.
    // For a recordEditor acting as a +link{listGrid.filterEditor}, this button will
    // show the +link{filterImg} as an +link{button.icon} by default.
    // <P>
    // Clicking this button will call +link{recordEditor.performAction()} on the editor.
    // <P>
    // May be customized using the standard +link{AutoChild} pattern, by overriding
    // +link{actionButtonProperties}.
    // @visibility external
    //<

    actionButtonConstructor:isc.Button,

    // Hide the title in case the developer changes the filterImg to a blank img.
    actionButtonDefaults:{
        title:"",
        showOver:true,
        showFocusedAsOver:false
    },

    //> @attr recordEditor.actionButtonProperties (Button Properties : null : IRA)
    // Properties to apply to the automatically generated +link{recordEditor.actionButton}.
    // <P>
    // Note that for a recordEditor being used as a +link{listGrid.filterEditor}, the
    // +link{listGrid.filterButtonProperties} can be used to specify actionButton properties
    // directly at the grid level.
    //
    // @visibility external
    //<

    //> @attr recordEditor.actionButtonStyle (CSSStyleName : "normal" : IR)
    // +link{Button.baseStyle,baseStyle} for the +link{actionButton}
    // @visibility external
    //<
    actionButtonStyle:"normal",


    // Setting listEndEditAction to "next" allows this list to show an edit row with no associated
    // record in the data object
    listEndEditAction:"next",

    // Don't show the edit click mask - we are not dismissing edits on click outside.
    _showEditClickMask : function () {},

    // specify canEdit true to allow the editRowForm to be shown.
    // Note that we aren't setting an editEvent - this widget is *always* in editable state
    canEdit:true,

    // Always show the entire row as editable
    editByCell:false,

    // set _resizeWithMaster to false.  We want to resize horizontally with the master, but
    // not vertically - we need to move in order to remain in position below the master instead
    // This is handled by custom logic in listGrid.resizePeersBy()
    _resizeWithMaster:false,

    // Apply "normal" style to the editor and the  body - we don't expect it to have
    // any borders, etc which could get applied to the GR class as a whole
    bodyStyleName:"normal",
    styleName:"normal"

});

//!>Deferred
isc.RecordEditor.addMethods({

    initWidget : function () {

        if (this.sourceWidget != null) {


            // We want the width to match the sourceWidget's width.
            // This allows the fields to align with the sourceWidget's body columns
            var source = this.sourceWidget;

            this.setWidth(source.getInnerContentWidth(true));

            this.observe(source, "resized", "observer.sourceWidgetResized(observed)");

            // If the sourceWidget is not leaving a scrollbar gap, we shouldn't either - the
            // button will still float over the scrollbar area, but this ensures that any
            // fields of width "*" will size the same in this widget as in the sourceWidget
            this.leaveScrollbarGap = this.sourceWidget.leaveScrollbarGap;

            // If this is a filter editor, perform a filter whenever the user changes fields
            // if this.sourceWidget.filterByCell is true
            if (this.isAFilterEditor()) {
                this.actOnCellChange = this.sourceWidget.filterByCell;
                this.actOnKeypress = this.sourceWidget.filterOnKeypress;
            } else {
                // Otherwise, if saveByCell is true, perform a save whenever the user changes
                // fields.
                this.actOnCellChange = this.sourceWidget.saveByCell;
            }

            // ensure that the editForm is autoFocus:false, we don't want to jump focus into
            // the form every time it gets redrawn.
            isc.addProperties(this.editFormDefaults, {autoFocus:false});

            // Pick up field ID and fields from the source widget.
            this.fieldIDProperty = this.sourceWidget.fieldIDProperty;
            this.fields = this.sourceWidget.completeFields.duplicate();

            // always size our row to fit inside ourselves
            this.cellHeight = this.getInnerHeight();

        } else {
            this.logWarn("RecordEditor initialized without a sourceWidget property. " +
                         "This widget is not supported as a standalone component.");
        }

        return this.Super(this._$initWidget);
    },

    destroy : function () {
        this.ignore(this.sourceWidget, "resized");
        this.Super("destroy", arguments);
    },

    // Ensure our width always fits that of our source widget
    // Note that we have to use the *visible* width -- the source grid could be autoFitData:"horizontal"
    // in which case it overflows specified size horizontally.
    sourceWidgetResized : function (source) {
        this.setWidth(source.getInnerContentWidth(true));
    },

    _$filter:"filter",
    isAFilterEditor : function () {
        return (this.actionType == this._$filter);
    },

    // Override formatCellValue - if this is the removeField, don't show an icon
    // No need to explicitly suppress the "removeRecord" behavior - we already
    // suppress all 'recordClick' behavior for fields

    _formatCellValue : function (value, record, field, rowNum, colNum) {
        if (field.isRemoveField) return "&nbsp;"
        return this.Super("_formatCellValue", arguments);
    },

    // Override _useDisplayFieldValue to always return false.
    // If a field has a specified displayField we don't want to be attempting to update that
    // field in the criteria specified when a user changes the editValue for this item.

    _useDisplayFieldValue : function (field) {
        return false;
    },

    // Override 'draw()' to ensure that we're editable
    draw : function () {
        // Ensure we're correctly sized

        var source = this.sourceWidget;
        this.setWidth(source.getInnerContentWidth(true));

        // findNextEditCell signature:
        // rowNum, colNum, direction, stepThroughFields, checkStartingCell,
        //          checkPastListEnd, dontCheckPastRowEnd, ignoreFocus
        // Pass in ignoreFocus - ensures we catch any fields where canEdit is true even
        // if the editor type is not canFocus true (Example: MiniDateRange)
        var firstEditCell = this.findNextEditCell(0,0,1,true,true,false,true,true);
        if (firstEditCell == null) {
            this.logWarn("No editable fields in this record editor." +
                        (this.isAFilterEditor() ?
                            " Check the 'canFilter' property for each field in "
                         :
                            " Check the 'canEdit' property for each field in "
                        ) + this.sourceWidget.getID());
        } else {
            // Set up a starting set of edit values based on
            // for filtering: the current filter if there is one, otherwise the field-wise
            //                defaultFilterValue for each field
            // for editing: the default value of the field

            var isFilter = this.isAFilterEditor(),
                vals;
            if (isFilter) {
                vals = this.sourceWidget._getFilterEditorValues();
            } else {
                var undefined;

                vals = {};
                for (var i = 0; i < this.fields.length; i++) {
                    var field = this.fields[i];

                    if (field.defaultValue !== undefined) {
                        vals[field[this.fieldIdProperty]] = field.defaultValue;
                    }
                }
            }

            if (this.isAFilterEditor()) this.setValuesAsCriteria(vals);
            else this.setEditValues(0, vals);

            var firstEditCol = firstEditCell[1];
            // Note if we don't have any fields, this is a no-op
            this._startEditing(0,firstEditCol);
        }

        this.Super("draw", arguments);
        // Ensure the actionButton shows up on top of the body
        this.actionButton.bringToFront();
    },

    setFields : function () {
        this.Super("setFields", arguments);
        // If we're drawn(), but we didn't have fields before this setFields call, startEditing
        // now
        this.initializeEdit();
    },
    // If we're not already editing, start editing.
    initializeEdit : function () {
        if (!this.isDrawn() || this._editorShowing) return;

        var firstEditCell = this.findNextEditCell(0,0,1,true,true);
        // If we're not showing any filterable fields, firstEditCell may be null
        // In this case we'll just call 'startEditing' on the first cell - it will be showing
        // a static text item so won't really be editable but ensures we show the edit form for
        // when the user shows more fields
        if (firstEditCell == null) firstEditCell = [0,0];
        this._startEditing(0, firstEditCell[1]);
    },
    showField : function () {
        this.Super("showField", arguments);
        // If we're drawn(), but we didn't have any editable fields before this showField
        // call, startEditing now
        this.initializeEdit();
    },

    // Override createChildren to make the actionButton
    createChildren : function () {
        this.Super("createChildren", arguments);

        var editForm = this.getEditForm();
        if (editForm != null && this._initialCriteria) {
            editForm.setValuesAsCriteria(this._initialCriteria);
            delete this._initialCriteria;

            if (this.isAFilterEditor()) editForm.isSearchForm = true;
        }

        // Never allow the body to protrude past the end of the action-button
        if (!this.actionButton) this.makeActionButton();
    },

    // MakeActionButton -- this is a button that will float on top of the RecordEditor body,
    // aligned with the scrollbar of the sourceWidget, with click set to perform the filter
    // or save action using the current set of edit values.
    makeActionButton : function () {
        var actionIcon;

        if (this.isAFilterEditor()) {
            actionIcon = this.filterImg;
        } else {
            actionIcon = this.saveImg;
        }


        var showOver, showFocused
        if (isc.isAn.Object(actionIcon)) {
            showOver = actionIcon.showOver;
            showFocused = actionIcon.showFocused;
            actionIcon = actionIcon.src;
        }

        var sorterWidth = this.sourceWidget._getSorterWidth();
        var dynamicProperties = {
            recordEditor:this,

            // Note we want the height to match the rendered inner height of this ListGrid
            // which is the visible height of the body.  However the body hasn't been drawn
            // yet, so we handle this via an override to adjustOverflow(), which also ensures
            // we'll be resized if the body is resized
            //height:this.body.getVisibleHeight(),
            // Float the button over where the body scrollbar would normally appear
            left:this.isRTL() ? 0 : this.getInnerWidth() - sorterWidth,
            width:sorterWidth,
            // Height is matched to the height of the RecordEditor as a whole.

            autoDraw: false,

            // use button icon only, no title
            baseStyle: this.actionButtonStyle,
            skinImgDir: this.skinImgDir,
            icon: actionIcon,
            iconSize: sorterWidth,

            prompt: this.actionButtonPrompt,

            click : function () {this.recordEditor.performAction();}
        }
        if (showOver != null) dynamicProperties.showRollOverIcon = showOver;
        if (showFocused != null) dynamicProperties.showFocusedIcon = showFocused;


        if (this.tabIndex != null && !this._autoTabIndex) {
            dynamicProperties.tabIndex = this.tabIndex;
        }

        this.actionButton = this.createAutoChild("actionButton", dynamicProperties);
        this.addChild(this.actionButton);

    },

    //> @method recordEditor.performAction()
    // Fired when the user clicks the +link{actionButton} for this RecordEditor.
    // May also be triggered from other user interaction with edit values (for
    // example filter-editor change - see +link{listGrid.filterOnKeypress}).
    // <P>
    // This is the method which initiates a filter in a listGrid
    // +link{listGrid.showFilterEditor,filter editor}. Note that for custom
    // filtering behavior, developers can use the +link{listGrid.filterEditorSubmit()}
    // notification method rather than overriding this method directly.
    //
    // @visibility external
    //<
    performAction : function (suppressPrompt, callback) {
        if (this.isAFilterEditor()) this.performFilter(suppressPrompt, callback);
        else this.performSave(suppressPrompt, callback);
    },

    // setValuesAsCriteria - for when this component is acting as a filterEditor
    // applies criteria (possibly AdvancedCriteria) to the edit form for display / modification
    setValuesAsCriteria : function (criteria, refresh, dropExtraCriteria, dropCriteriaFields) {
        var form = this.getEditForm();
        if (form == null) {
            this._initialCriteria = criteria;
            return;
        }
        form.setValuesAsCriteria(criteria, null, dropExtraCriteria, dropCriteriaFields);
        if (refresh) this.refreshRow(0);
    },

    // Override makeEditForm()
    // This method is called to initially create the edit form, and to update its items
    // when fields are shown/hidden / incrementally rendered etc.
    // Default implementation sets the value based on the current editValues for the row
    // When we're acting as a filterEditor, we need to apply criteria to the form instead.
    makeEditForm : function () {
        var props = this.editFormProperties = this.editFormProperties || {};
        if (props.allowExpressions == null && this.allowFilterExpressions != null)
            props.allowExpressions = this.allowFilterExpressions;

        props.expressionDataSource = this.expressionDataSource;
        if (this.isAFilterEditor()) props.storeAtomicValues = true;

        var editForm = this.getEditForm(),
            criteria;

        props._parseExtraCriteria = true;

        if (editForm == null) {
            criteria = this._initialCriteria;
        } else {
            // If the form already exists simply pick up the current values from it and
            // ensure they're still applied after the items have been reworked
            criteria = editForm.getValuesAsCriteria();
            editForm.expressionDataSource = this.expressionDataSource;
        }
        this.Super("makeEditForm", arguments);

        editForm = this.getEditForm();
        if (editForm != null) {
            editForm.setValuesAsCriteria(criteria);
            delete this._initialCriteria;

            if (this.isAFilterEditor()) editForm.isSearchForm = true;
        }
    },

    updateDataSource : function (ds) {
        this.expressionDataSource = ds;
        var form = this.getEditForm();
        if (form) form.expressionDataSource = this.expressionDataSource;
    },

    // Override getEditDisplayValue()
    // This method is called from refreshCellValue when we are newly showing an edit item,
    // and should return the current value to display in the item.
    // For filterEditors rely on the fact that we set the criteria directly on the DynamicForm
    // based on the criteria applied to the grid when the form is created, and when
    // the criteria is changed programmatically. The values stored in the form are always
    // up to date.

    getEditDisplayValue : function (rowNum, colNum, record) {
        if (this.isAFilterEditor() && this._editRowForm != null) {
            var fieldName = this.getFieldName(colNum);
            return this._editRowForm.getValue(fieldName);
        }
        return this.Super("getEditDisplayValue", arguments);
    },
    // Also override getEditDisplayValues() - this is called from makeEditForm()
    // which gets called from "setFields()". We do this when showing
    // and hiding fields on the main grid
    getEditDisplayValues : function (rowNum, colNum) {
        if (this.isAFilterEditor() && this._editRowForm != null) {
            return this._editRowForm.getValues();
        }
        return this.Super("getEditDisplayValues", arguments);
    },

    // performFilter() - filter the source widget with the values from this widget
    // There is a problem with doing this, reported on the forums: http://forums.smartclient.com/showthread.php?t=1933
    // The problem is: if you have a datasource with a hidden field, and you programmatically
    // filter on that hidden field, and you then try to filter through the filter editor, you lose
    // your programmatic filter.  The solution is to combine the filter editor's values with
    // values in the existing criteria for fields we don't know about.
    performFilter : function (suppressPrompt, callback) {

        var criteria = this._getFilterCriteria(this.sourceWidget.autoFetchTextMatchStyle);
        var context = {};
        if (suppressPrompt) context.showPrompt = false;

        // If criteria is cleared be sure to reset match style back to
        // initial LG.autoFetchTextMatchStyle.
        criteria = isc.DS.checkEmptyCriteria(criteria);
        if (criteria == null || isc.getKeys(criteria).length == 0) {
            // pick up textMatchStyle from LG.autoFetchTextMatchStyle
            context.textMatchStyle = this.sourceWidget.autoFetchTextMatchStyle;
        }

        // If we're going to hit the server, build in a pause so we don't keep diving off whilst
        // the user is typing rapidly
        var rs = this.sourceWidget.data;


        if (
            (isc.isA.ResultSet(rs) && rs.willFetchData(criteria, context.textMatchStyle)) ||
            (isc.isA.Tree(rs) && this.sourceWidget.dataSource != null)
            )
        {
            this.fireOnPause("performFilter", {
                target:this.sourceWidget,
                methodName:"handleFilterEditorSubmit",
                args:[criteria, context, callback]
            },
            this.fetchDelay);
        } else {
            this.sourceWidget.handleFilterEditorSubmit(criteria, context, callback);
        }

    },

    _getFilterCriteria : function (textMatchStyle) {
        var form = this.getEditForm();
        if (form == null) {
            return this._initialCriteria;
        }

        // live values from the edit row form:

        // Note that the 'return nulls' param ensures we get entries back for fields with
        // no value so we (correctly) wipe out the value in oldCriteria
        return form.getValuesAsCriteria(null, textMatchStyle);
    },


    // Add the record to the source widget's data object
    // This method is not completely functional yet - see comments within the method body
    performSave : function (suppressPrompt, callback) {

        // ensure we save the value from the current edit field before saving out the
        // entire set of values.
        var rowNum = this.getEditRow(),
            colNum = this.getEditCol(),
            fieldName = this.getFieldName(colNum),
            newValue = this._editRowForm.getValue(fieldName);
        this.setEditValue(rowNum, colNum, newValue);

        // validate the entire row, and save only if validation succeeds:
        var newValues = this.getEditValues(0),
            fields = this.getFields().getProperty(this.fieldIdProperty);

        // This method will show the validation errors, and put focus into the field for
        // which the validation failed
        if (!this.validateRowValues(newValues, {}, 0, fields)) return;

        // xxx
        // At this point we want to fall through to 'saveEditedValues()', but we need to
        // ensure that the saveEditedValues method on the sourceWidget is aware that we
        // are adding a record rather than overriding existing records.
        // Currently this does not happen, so we will end up overriding existing records
//        targetList.saveEditedValues(
//            targetList.data.getLength(), 0, null, newValues, {}, isc.ListGrid.PROGRAMMATIC
//        );

        // clear out existing edit values, and call startEditing again to start a fresh edit
        this._clearEditValues(0);
        this._startEditing(0,0);
        // This ensures the edit form values get cleared out.
        for (var fieldName in newValues) {
            this.refreshCell(0,colNum);
        }


        if (callback) this.fireCallback(callback);
    },

    // A method to get the current edit-values
    getValues : function () {
        var colNum = this.getEditCol(),
            fieldName = this.getEditFieldName();
        this.setEditValue(0, colNum, this._editRowForm.getValue(fieldName));


        //return isc.addProperties({},this.getEditValues(0));
        var criteria = this._getFilterCriteria();
        return criteria;

    },

    // A method to get the current edit-values as AdvancedCriteria
    getValuesAsCriteria : function (textMatchStyle) {
        var colNum = this.getEditCol(),
            fieldName = this.getEditFieldName();

        var criteria = this._getFilterCriteria(textMatchStyle);
        return criteria;
    },

    // Inline Editor overrides:

    // override canEditCell - if this is a filter we want to look at the static 'canFilter'
    // property on the field (inherited from the source widget).  Otherwise just fall through
    // to the canEditCell implementation on the sourceWidget.
    canEditCell : function (rowNum, colNum) {
        if (this.isAFilterEditor()) {
            var field = this.getField(colNum);

            if (field == null) return false;
            return (field.canFilter != false);
        } else {
            return this.sourceWidget.canEditCell(rowNum, colNum);
        }
    },

    // Override the various methods to determine the edit form item properties.
    // If this is a filter edit row, we want to use the appropriate 'getFilterEditor...'
    // methods on the source widget -- if this is an editor, we simply inherit the
    // 'getEditor...' methods from the source widget.

    getEditorValueMap : function (field, values) {
        if (this.isAFilterEditor()) {
            return this.sourceWidget.getFilterEditorValueMap(field);
        } else {
            return this.sourceWidget.getEditorValueMap(field, values);
        }
    },

    getEditorType : function (field, values) {
        if (this.isAFilterEditor()) {
            return this.sourceWidget.getFilterEditorType(field);
        } else {
            return this.sourceWidget.getEditorType(field, values);
        }
    },

    // function to re-use as change handler on all our items
    _editorChanged : function () {
        this.form.grid.editorChanged(this);
    },

    // Technically this fires on changed, not keypress but it's a cleaner name.
    editorChanged : function (item) {
        var actOnKeypress = item.actOnKeypress != null ? item.actOnKeypress : this.actOnKeypress;
        if (actOnKeypress) {
            // check whether the value of the item is a partially entered filterExpression
            if (this.sourceWidget.allowFilterExpressions && !item.enteredCompleteExpression()) return;
            // if this is a filterEditor, filters caused by keypress events should not select
            // the value on refocus, since subsequent keypresses would then clear the value
            if (this.isAFilterEditor()) this._editorCursorAtEnd = true;
            // fetch with modal dialog to help ensure expected behavior
            this.performAction();
        }
    },

    getEditorProperties : function (field) {
        // Default all items to match this.cellHeight
        var props = {height:this.cellHeight};
        // don't pick a default ODS if there's an explicit setting for ODS or for a specific
        // list of values
        if (!field.optionDataSource && !field.valueMap && !field.filterEditorValueMap) {

            // respect foreignKey if present
            if (field.foreignKey) {
                props.optionDataSource =
                        isc.DS.getForeignDSName(field, this.sourceWidget.getDataSource());
                props.valueField =
                        isc.DS.getForeignFieldName(field, this.sourceWidget.getDataSource());

            } else if (field.displayField) {
                // otherwise if there's a displayField setting with no ODS on the ListGridField
                // this strongly implies that the "large value map" pattern is in use
                // (a display value is embedded in each record).  "foreignKey" was not
                // declared, so this is not being done via includeFrom and having a related
                // DataSource, instead, the DataSource of the grid itself must be returning
                // displayField values, so that's the one to use as an ODS.
                props.optionDataSource = this.sourceWidget.dataSource;
            }
        }

        if (this.isAFilterEditor()) {
            // For filter editors always allow empty values
            props.allowEmptyValue = true;

            // fire our special changed handler on 'changed'
            props.changed = this._editorChanged;
            props.actOnKeypress = field.filterOnKeypress;

            return isc.addProperties(props, this.sourceWidget.getFieldFilterEditorProperties(field));
        } else {
            return isc.addProperties(props, this.sourceWidget.getEditorProperties(field));
        }
    },

    getEditItem : function (editField, record, editedRecord, rowNum, colNum, width) {
        var item = this.Super("getEditItem", arguments);
        if (!this.isAFilterEditor()) return item;

        var filterEditorProps = this.sourceWidget.getFieldFilterEditorProperties(editField),
            undef;
        if (!filterEditorProps) filterEditorProps = {};

        // For filter editors, don't pick up editing related properties such as custom change
        // handlers from the field.
        // These are not appropriate on the filter editor since
        // we're not actually performing an edit of a record in the source widget.
        // Note that the developer can still customize these attributes via the
        // filterEditorProperties block for the field.
        // Note: delete rather than setting to explicit null - this ensures the item can pick up
        // the FormItem class default properties for these attributes
        if (editField.defaultValue!=null && filterEditorProps.defaultValue === undef) {
            delete item.defaultValue;
        }
        if (editField.change != null && filterEditorProps.change === undef) {
            delete item.change;
        }
        // changed already overridden to '_editorChanged'
        if (editField.defaultDynamicValue != null &&
            filterEditorProps.defaultDynamicValue != null)
        {
            delete item.defaultDynamicValue;
        }
        if (editField.icons != null && filterEditorProps.icons === undef) {
            delete item.icons;
        }
        if (editField.showPickerIcon != null && filterEditorProps.showPickerIcon === undef) {
            delete item.showPickerIcon;
        }

        return item;

    },

    // override getDefaultEditValue - for filterEditors, our value will always be explicitly set,
    // either from defaultFilterValue on the source widget, or from specified criteria on a
    // filter operation
    getDefaultEditValue : function (fieldName, field) {
        if (this.isAFilterEditor()) return null;
        return this.Super("getDefaultEditValue", arguments);
    },

    // override getValueIcon() - for canFilter:false fields we want to suppress whatever
    // icon would correspond to "null" from showing in lieu of an editor - simply
    // showing a blank cell is a better indication of non-editability.
    // This is most obvious with boolean fields.
    getValueIcon : function (field, value, record, rowNum) {
        if (this.isAFilterEditor()) return null;
        return this.Super("getValueIcon", arguments);
    },

    // cellEditEnd() is fired when the user completes an edit for some cell by tabbing out
    // of that cell, hitting enter or escape, etc.
    // We override the default implementation to avoid cancelling the edit, or saving the
    // edit values into this.values, and to allow us to fire our default action in response
    // to an enter keypress (or a field change).
    cellEditEnd : function (editCompletionEvent, newValue) {

        // Possible editCompletionEvents are:
        // - CLICK_OUTSIDE - suppressed as we don't show the clickMask for this ListGrid
        // - ESCAPE_KEYPRESS - ignore
        // - UP_ARROW_KEYPRESS - ignore
        // - DOWN_ARROW_KEYPRESS - ignore
        // - EDIT_ROW_CHANGE - will not happen as we don't have more than one row
        // - PROGRAMMATIC - ignore
        //
        // - ENTER_KEYPRESS - fire this.performAction - will perform a filter if this
        //                    is a filter editor (or a save if this is an editor)
        // - TAB_KEYPRESS -
        // - SHIFT_TAB_KEYPRESS -
        // - EDIT_FIELD_CHANGE -
        //              For all field changes, save out the edit values, and move to the
        //              appropriate fields.
        //              If this.actOnCellChange is true, also fire this.performAction().
        if (editCompletionEvent != isc.ListGrid.ENTER_KEYPRESS &&
            editCompletionEvent != isc.ListGrid.TAB_KEYPRESS &&
            editCompletionEvent != isc.ListGrid.SHIFT_TAB_KEYPRESS &&
            editCompletionEvent != isc.ListGrid.EDIT_FIELD_CHANGE) return true;

        var undef;
        if (newValue === undef) newValue = this.getUpdatedEditorValue();

        var rowNum = this.getEditRow(),
            colNum = this.getEditCol();

        // update the locally stored edit info with the new value for the appropriate field
        this.setEditValue(rowNum, colNum, newValue);

        // Save / query on enter keypress
        if (editCompletionEvent == isc.ListGrid.ENTER_KEYPRESS || this.actOnCellChange) {
            this.performAction();
            if (editCompletionEvent == isc.ListGrid.ENTER_KEYPRESS) return;
        }

        var nextCell = this.getNextEditCell(rowNum, colNum, editCompletionEvent);

        if (nextCell == null || nextCell[0] != rowNum) {
            // if we're tabbing past the end of the row, we want to put focus onto the
            // appropriate object on the page.
            // We have to do this explicitly, because we've overridden the key press
            // method on the editor form items to suppress the native tab-behavior.

            if (this.tabIndex != null && !this._autoTabIndex) {
                this._letTabKeypressThrough = true;
                return;
            } else {
                if (editCompletionEvent == isc.ListGrid.TAB_KEYPRESS) {
                    this.body._focusInNextTabElement(true);
                } else {
                    // On shift tab, move focus to the previous widget in the tab order.
                    this._focusInNextTabElement(false);
                }
                return;
            }
        }

        // In this case we want to fall through to the superclass implementation - this
        // will validate the cell value (if necessary), save the editValue locally, and
        // move focus to the appropriate cell.
        // xxx - Could just re-implement the relevant code here rather than calling Super
        //       to avoid (EG) calling 'getNextEditCell()' more than once.
        return this.Super("cellEditEnd", arguments);
    },
    editorKeyPress : function (item, keyName, characterValue) {
        var rv = this.Super("editorKeyPress", arguments);
        if (this._letTabKeypressThrough) {
            this._letTabKeypressThrough = null;
            return true;
        }
        return rv;
    },

    // clearEditValue on the superclass will dismiss the editor if no editValues are left.  The
    // recordEditor never wants to do this.
    clearEditValue : function (editValuesID, fieldName) {
        return this.Super("clearEditValue", [editValuesID, fieldName, null, true]);
    },

    // UI Synching:

    // Ensure the action button stays positioned / sized correctly
    // Override layoutChildren to keep the action button positioned at the right edge of this
    // widget
    layoutChildren : function () {
        this.Super("layoutChildren", arguments);
        // Stick the action button to the left
        var sorterWidth = this.sourceWidget._getSorterWidth();
        if (this.actionButton)
            this.actionButton.setLeft(this.isRTL() ? 0 : this.getInnerWidth() - sorterWidth);

        // Ensure the body does not extend behind the action button - this avoids issues
        // where we can't scroll the source widget horizontally to get at stuff under the
        // V-Scrollbar
        if (this.body) {
            var body = this.body;
            if (this.bodyLayout) body = this.bodyLayout;
            body.setWidth(Math.max(1, this.getInnerWidth() - sorterWidth));
            if (this.actionButton && this.isRTL()) {
                body.setLeft(sorterWidth);
            }
        }


        // layoutChildren sets up a layout's member tab-indices
        // Action button isn't a member, but slot it after the body.
        // Note that we explicitly force focus into it in response to cell-edit-end but this
        // allows tabbing backward through a page to work properly (even in the case where
        // we have a modal mask up and are doing synthetic tab-index mgmt)
        if (this.actionButton && this.body) this.actionButton._setTabAfter(this.body);
    },


    // Override adjustOverflow to render the actionButton as tall as this widget
    adjustOverflow : function () {
        this.Super("adjustOverflow", arguments);
        if (this.actionButton) {
            this.actionButton.setHeight(this.body.getVisibleHeight());
        }
    },

    // We need to keep our UI in sync with our source widget.
    // This means:
    // - Show and Hide with source widget.
    //      Handled by standard peer relationship
    // - Move with source widget
    //      Handled by standard peer relationship
    // - Resize with source widget, and remain positioned below source widget
    //      Handled by ListGrid.resizePeersBy() method
    // - Scroll body with the source widget body
    //      RecordEditor bodyScrolled() overridden below to keep source widget body in sync
    //      with us.
    //      ListGrid bodyScrolled() method handles keeping us in sync with source widget body
    //      scroll position.
    // - Resize fields with the source widget
    //      Handled by source widget resizeField()
    // - Reorder / Change fields with the source widget
    //      Handled by source widget reorderFields()

    // Override bodyScrolled() to keep the body scrolling in sync.
    // Note that bodyScrolled implementation on the source widget will handle scrolling this
    // widget into place.
    bodyScrolled : function (left, top) {
        this.Super("bodyScrolled", arguments);
        // Sanity check - avoid attempting to scroll while not actually drawn/visible
        if (!this.isVisible() || !this.isDrawn()) return;

        // Scroll the sourceWidget body to our position
        // Only do this if it's not already scrolled to the same position as us

        if (this._syncSourceEvent != null) {
            isc.Timer.clear(this._syncSourceEvent);
        }
        this._syncSourceEvent = this.delayCall("syncSourceScrolling", [], 0);
    },

    syncSourceScrolling : function () {
        var sourceWidget = this.sourceWidget,
            sourceWidgetBody;
        if (!sourceWidget || !(sourceWidgetBody = sourceWidget.body)) return;

        var left = this.body.getScrollLeft();

        // RTL mode - account for the fact that scrollLeft zero (i.e. hard left) on the source
        // widget body might not be scrollLeft zero (hard left) on the filter editor body since
        // their viewport left coords might not align. For example, if the source widget body
        // does not have a vertical scrollbar, then scrollLeft zero on the source widget body
        // corresponds to scrollLeft 16 (or whatever the filter button width is) on the filter
        // editor body.
        if (this.isRTL()) {
            var offset = sourceWidgetBody.getViewportWidth() - this.body.getViewportWidth();
            left -= offset;
            if (left < 0) left = 0;
        }

        var maxScrollLeft = sourceWidgetBody.getScrollRight();
        if (left > maxScrollLeft) left = maxScrollLeft;
        if (sourceWidgetBody.getScrollLeft() != left) {
            sourceWidgetBody.scrollTo(left);
        }
    },

    // override 'getFieldWidths' to get the source's field widths.

    getFieldWidths : function () {
        var widths = this.sourceWidget.getFieldWidths();
        // duplicate the widths so modifications on the LG widths array won't directly affect
        // our widths
        if (isc.isA.Array(widths)) {
            widths = widths.duplicate();

            if (this._correctForActionButtonClipping()) {
                var sorterWidth = this.sourceWidget._getSorterWidth();
                var taken = widths.sum(),
                    space = this.getInnerWidth() - sorterWidth;

                if (taken > space) {
                    widths[widths.length-1] -= Math.min(sorterWidth, (taken - space));
                }
            }
        }
        return widths;
    },
    // getEditFormItemFieldWidths():
    // Overridden to reduce the size of the last edit item to ensure it fits in the available
    // space (isn't clipped by the filter button), even if the sourceWidget's field is sized larger
    // than the available space
    // Happens when the source widget shows an HScrollbar but no VScrollbar
    _correctForActionButtonClipping : function () {
        var sourceWidget = this.sourceWidget;
        if (sourceWidget != null && !sourceWidget._showSortButton() && sourceWidget.body != null &&
            !sourceWidget.body.vscrollOn)
        {

            return true;
        }
    },
    getEditFormItemFieldWidths : function (record) {
        var widths = this.Super("getEditFormItemFieldWidths", arguments);
        if (this._correctForActionButtonClipping()) {
            var sorterWidth = this.sourceWidget._getSorterWidth();
            var taken = widths.sum(),
                space = this.getInnerWidth() - sorterWidth;

            if (taken > space) {
                widths[widths.length-1] -= Math.min(sorterWidth, (taken - space));
            }
        }
        return widths;
    },

    // override getCellValue to avoid showing checkbox icons for the checkboxField

    getCellValue : function (record, rowNum, colNum, gridBody) {
        var field = this.fields[colNum];
        if (field &&
            (this.isCheckboxField(field) || this.isExpansionField(field) ||
                this.isRowNumberField(field)))
            return "&nbsp;"
        var value = this.Super("getCellValue", arguments);
        if (colNum == this.fields.length - 1) {
            if (isc.Browser._needOldFilterButtonOffsetApproach) {
                var field = this.getField(colNum),
                    form = this.getEditForm(),
                    item = form != null ? form.getItem(field.name) : null,
                    width = item != null ? item.getWidth() : null;
                if (width != null) {
                    value = ("<div style='text-align:" +
                             this.getCellAlign(record, rowNum ,colNum, true) +
                             ";width:" + width + "px;'>" + value + "</div>");
                }
            }
        }
        return value;
    },

    // Override rebuildForFreeze to no-op
    // We'll instead respond to our source-element's rebuild for freeze
    rebuildForFreeze : function () {
    },

    // Override rowClick / rowDoubleClick to no-op
    // This means if we have any fields which are canFilter:false, we'll avoid executing the
    // field level recordClick / recordDoubleClick handlers when the user clicks them.
    rowClick : function () {},
    rowDoubleClick : function () {}
});

//>IE7
isc.Browser._needOldFilterButtonOffsetApproach = isc.Browser.isIE && !isc.Browser.isIE8Strict;
//<IE7


if (isc.Browser._needOldFilterButtonOffsetApproach) {
    isc.RecordEditor.addProperties({
        getCellAlign : function (record, rowNum, colNum, returnDefault) {
            if (!returnDefault && colNum == this.fields.length - 1) {
                return this.isRTL() ? "right" : "left";
            }
            return this.Super("getCellAlign", arguments);
        }
    });
}
//!<Deferred








//>    @class    Menu
//
// The Menu widget class implements interactive menu widgets, with optional icons, submenus,
// and shortcut keys.
// <p>
// A Menu is initialized with an Array of items, specified as menu.data, each of which represents
// one row in the menu's display and specifies the action to take when that menu item is selected.
// <p>
// Generally to create a context menu for a component, provide a Menu instance for the
// <code>contextMenu</code> property.  Note that some components have special context menu support
// because they have distinct regions or because they have a default set of context menu actions
// available.
// <p>
// If you want a button that pops up a menu when clicked, or a bar of such buttons, see the
// MenuButton and MenuBar classes.
//
// @see attr:Menu.data
// @see attr:Canvas.contextMenu
// @see class:MenuButton
// @see class:MenuBar
//  @treeLocation Client Reference/Control
//  @visibility external
// @example fullMenu
//<



// define us as a subclass of the ListGrid
isc.ClassFactory.defineClass("Menu", "ListGrid");


// add contants
isc.Menu.addClassProperties({
    // These fields left for compatibility purposes and not used.
    ICON_FIELD:{},
    TITLE_FIELD:{},
    KEY_FIELD:{},
    SUBMENU_FIELD:{},

    //> @type MenuFieldID
    // Simple string identifiers for standard menu fields.
    // @value   "icon"  Displays the icon field for the menu. This field contains the items
    //                  specified icon (if there is one), or if the item is checked, the
    //                  checkmark icon for the item.
    // @value   "title" Displays the item's title
    // @value   "key" Displays the key field for the menu. This field contains the name or
    //                title of any shortcut keys for this menu item.
    // @value   "subMenu" Field to display the submenu image for items that have a submenu.
    // @visibility external
    //<

    //>    @classAttr    isc.Menu._openMenus        (attrtype : [] : IRWA)
    //            list of all menus that are currently open
    //<
    _openMenus:[],

    // user-displayable names for modifier key combinations
    // NOTE that on the Mac this is actually generally done with icons.
    //=    @const    isc.Menu.SHIFT
    SHIFT:(isc.Browser.isWin ? "Shift+" : "shift-"),
    //=    @const    isc.Menu.CTRL
    CTRL:(isc.Browser.isWin ? "Ctrl+" : "ctrl-"),
    //=    @const    isc.Menu.ALT
    ALT:(isc.Browser.isWin ? "Alt+" : "option-"),
    //=    @const    isc.Menu.META
    META:(isc.Browser.isWin ? "Windows+" : "command-")

});


isc.Menu.addProperties({

    //> @attr menu.iconFieldProperties (ListGridField properties : null : IR)
    // Custom properties for the automatically generated icon column.
    // <P>
    // See +link{menu.showIcons} for an overview of the icon column.
    // @visibility external
    //<
    //iconFieldProperties:null,

    //> @attr menu.iconFieldDefaults (ListGridField properties : object : IR)
    // Default properties for the automatically generated icon column.
    // Default object includes properties to set width and to show icon for this column.
    // <P>
    // To modify the behavior or appearance of this column, developers may set
    // +link{menu.iconFieldProperties} at the instance level, or override this
    // object at the class level. If overriding this object, we recommend using
    // +link{class.changeDefaults()} rather than replacing this object entirely.
    // <P>
    // See +link{menu.showIcons} for an overview of the icon column.
    // @visibility external
    //<
    iconFieldDefaults:{
        name: "icon",
        width:25,
        getCellValue : function (list, item) { return list.getIcon(item); },

        // a marker to determine if a menu field is a duplicate of this standard field
        _standardMenuIconField: true
    },

    getIconField : function () {
        return isc.addProperties({}, this.iconFieldDefaults, isc.Menu.ICON_FIELD, this.iconFieldProperties);
    },

    //> @attr menu.titleFieldProperties (ListGridField properties : null : IR)
    // Custom properties for the automatically generated title column.
    // @visibility external
    //<
    //titleFieldProperties:null,

    //> @attr menu.titleFieldDefaults (ListGridField properties : object : IR)
    // Default properties for the automatically generated title column.
    // Default object includes properties to set width and to show title for this column.
    // <P>
    // To modify the behavior or appearance of this column, developers may set
    // +link{menu.titleFieldProperties} at the instance level, or override this
    // object at the class level. If overriding this object, we recommend using
    // +link{class.changeDefaults()} rather than replacing this object entirely.
    // @visibility external
    //<
    titleFieldDefaults:{
        name: "title",
        width:"*",
        getCellValue : function (list, item) { return list.getItemTitle(item); }
    },

// EDD
   _getTitleField : function () {
       return isc.addProperties({}, this.titleFieldDefaults, isc.Menu.TITLE_FIELD, this.titleFieldProperties);
   },

    //> @attr menu.keyFieldProperties (ListGridField properties : null : IR)
    // Custom properties for the automatically generated key column.
    // <P>
    // See +link{menu.showKeys} for an overview of the key column.
    // @visibility external
    //<
    //keyFieldProperties:null,

    //> @attr menu.keyFieldDefaults (ListGridField properties : object : IR)
    // Default properties for the automatically generated icon column.
    // Default object includes properties to set width and to show key for this column.
    // <P>
    // To modify the behavior or appearance of this column, developers may set
    // +link{menu.keyFieldProperties} at the instance level, or override this
    // object at the class level. If overriding this object, we recommend using
    // +link{class.changeDefaults()} rather than replacing this object entirely.
    // <P>
    // See +link{menu.showKeys} for an overview of the key column.
    // @visibility external
    //<
    keyFieldDefaults:{
        name: "key",
        width:35,
        getCellValue : function (list, item) { return list.getKeyTitle(item) }
    },

    getKeyField : function () {
        return isc.addProperties({}, this.keyFieldDefaults, isc.Menu.KEY_FIELD, this.keyFieldProperties);
    },

    //> @attr menu.submenuFieldProperties (ListGridField properties : null : IR)
    // Custom properties for the automatically generated submenu column.
    // <P>
    // See +link{menu.showSubmenus} for an overview of the submenu column.
    // @visibility external
    //<
    //keyFieldProperties:null,

    //> @attr menu.submenuFieldDefaults (ListGridField properties : object : IR)
    // Default properties for the automatically generated submenu column.
    // Default object includes properties to set width, align and to show submenu icon for this
    // column.
    // <P>
    // To modify the behavior or appearance of this column, developers may set
    // +link{menu.submenuFieldProperties} at the instance level, or override this
    // object at the class level. If overriding this object, we recommend using
    // +link{class.changeDefaults()} rather than replacing this object entirely.
    // <P>
    // See +link{menu.showSubmenus} for an overview of the submenu column.
    // @visibility external
    //<
    submenuFieldDefaults:{
        name: "submenu",
        width:18,
        align:"right",
        getCellValue : function (list, item) { return list.getSubmenuImage(item) }
    },

    getSubmenuField : function () {
        return isc.addProperties({}, this.submenuFieldDefaults, isc.Menu.SUBMENU_FIELD, this.submenuFieldProperties);
    },

    getStandardField : function (field) {
        if (isc.isA.String(field)) {
            var iconField = this.getIconField();
            if (iconField.name == field) return iconField;

            var titleField = this._getTitleField();
            if (titleField.name == field) return titleField;

            var keyField = this.getKeyField();
            if (keyField.name == field) return keyField;

            var submenuField = this.getSubmenuField();
            if (submenuField.name == field) return submenuField;

            this.logWarn("Menu field specified as :" + field + ". This is not a recognized standard field name");
            return null;
        } else {
            var fieldName = field.name;
            var iconField = this.getIconField();
            if (("menuBuiltin_" + iconField.name) == fieldName) return iconField;

            var titleField = this._getTitleField();
            if (("menuBuiltin_" + titleField.name) == fieldName) return titleField;

            var keyField = this.getKeyField();
            if (("menuBuiltin_" + keyField.name) == fieldName) return keyField;

            var submenuField = this.getSubmenuField();
            if (("menuBuiltin_" + submenuField.name) == fieldName) return submenuField;
        }
        return field;
    },

    //>    @attr menu.data (Array of MenuItem | Array[] of Record | Tree | RecordList : null : IRW)
    // An array of menuItem objects, specifying the menu items this menu should show.
    //
    // Data may also be set to a +link{Tree} in which case a hierarchy of menus and
    // submenus will automatically be generated to match the tree structure.  See also
    // +link{Menu.dataSource} for dynamically fetching menuItems and submenus from a
    // hierachical DataSource.
    //
    // @group data
    // @visibility external
    // @example fullMenu
    //<

    //>    @attr menu.items (Array of MenuItem : null : IRW)
    // Synonym for +link{menu.data}
    // @group data
    // @visibility external
    //<

    //> @attr menu.dataSource (DataSource : null : IR)
    // Optional DataSource to fetch menuItems and submenus from, instead of using +link{menu.items}.
    // <P>
    // The provided DataSource must be set up for hierarchical fetching - see the
    // +link{group:treeDataBinding,Tree Data Binding overview}.
    // @visibility external
    //<

    //> @attr menu.dataProperties (Tree Properties : null : IR)
    // For a <code>Menu</code> that uses a DataSource, these properties will be passed to
    // the automatically-created ResultTree.  This can be used for various customizations such as
    // modifying the automatically-chosen +link{tree.parentIdField}.
    // @group databinding
    // @visibility external
    //<

    //> @attr menu.target (Canvas : null : IRW)
    // Optional target canvas for this menu.  Available as a parameter to dynamic menuItem configuration
    // methods such as +link{MenuItem.checkIf}.
    // <P>
    // Whenever a Menu is shown as a contextMenu by a widget due to +link{Canvas.contextMenu}
    // being set, <code>menu.target</code> is automatically set to the widget that showed the
    // contextMenu.
    // <P>
    // If this item has any +link{menuItem.submenu,submenus} the <code>target</code> will be propagated down
    // to these child menus.
    // @visibility external
    //<

    //> @attr menu.fields (Array of MenuFieldID | Array of ListGridFields : null : IRWA)
    // Array of columns to show for this menu.<br>
    // Standard menu fields may be included by specifying +link{type:MenuFieldID, MenuFieldIDs}
    // directly. Additional custom fields may be specified as +link{ListGridField} objects.<br>
    // If this property is unset, default behavior will show the
    // +link{type:MenuFieldID, standard set of fields}, with the exception of any that have
    // been suppressed via +link{Menu.showIcons}, +link{Menu.showKeys} and +link{Menu.showSubmenus}
    // @visibility external
    // @example menuColumns
    //<

    //> @object MenuItem
    // Menu items are specified are object literals, not class instances.  For example, when
    // developing in JavaScript, a typical initialization block for a Menu would look like
    // this:
    // <pre>
    // Menu.create({
    //     data: [
    //         {title: "item1", click: "alert(1)"},
    //         {title: "item2"}
    //     ]
    // });
    // </pre>
    // And in XML:
    // <pre>
    // &lt;Menu&gt;
    //    &lt;data&gt;
    //        &lt;MenuItem title="item1" click="alert(1)"/&gt;
    //        &lt;MenuItem title="item2"/&gt;
    //    &lt;/data&gt;
    // &lt;/Menu&gt;
    // </pre>
    //
    // @inheritsFrom ListGridRecord
    // @treeLocation Client Reference/Control/Menu
    // @visibility external
    //<

    //> @attr menuItem.title (HTML : null : IR)
    // The text displayed for the menu item
    // @group menuBasic
    // @visibility external
    //<

    //> @attr menuItem.submenu (Menu : null : IR)
    // A reference to another menu, to display as a submenu when the mouse cursor hovers over
    // this menu item.
    // @group menuBasic
    // @visibility external
    // @example fullMenu
    //<

    //> @attr menuItem.canSelectParent (boolean : null : IR)
    // A MenuItem that has a submenu normally cannot be selected, instead clicking or hitting Enter
    // while keyboard focus is on the item shows the submenu.  Setting canSelectParent:true allows
    // a menu item with a submenu to be selected directly.
    // @visibility external
    //<

    //> @attr menuItem.isSeparator (Boolean : false : IR)
    // When set to <code>true</code>, this menu item shows a horizontal separator instead of
    // the +link{menuItem.title} text.  Typically specified as the only property of a menu item,
    // since the separator will not respond to mouse events.
    // @group menuBasic
    // @visibility external
    // @example fullMenu
    //<

    //> @attr menuItem.enabled (Boolean : true : IR)
    // Affects the visual style and interactivity of the menu item.  If set to
    // <code>false</code>, the menu item will not respond to mouse rollovers or clicks.
    // <p>
    // If you need to set this state dynamically, use +link{menuItem.enableIf} instead.
    // @group menuBasic
    // @visibility external
    //<

    //> @attr menuItem.checked (boolean : null : IR)
    // If true, this item displays a standard checkmark image to the left of its title.  You
    // can set the checkmark image URL by setting +link{menu.checkmarkImage}.
    // <p>
    // If you need to set this state dynamically, use +link{menuItem.checkIf} instead.
    // @group menuIcons
    // @visibility external
    // @example fullMenu
    //<

    //> @attr menuItem.disabledIcon (string : null : IR)
    // The filename for this item's custom icon when the item is disabled. If both this property
    // and +link{menuItem.checked} are both specified, only the icon specified by this property
    // will be displayed. The path to the loaded skin directory and the skinImgDir are prepended
    // to this filename to form the full URL.
    // <p>
    // If you need to set this state dynamically, use +link{menuItem.dynamicIcon} instead.
    // @group menuIcons
    // @visibility external
    // @example fullMenu
    //<

    //> @attr menuItem.icon (string : null : IR)
    // The filename for this item's custom icon. If both this property and
    // +link{menuItem.checked} are both specified, only the icon specified by this property will be
    // displayed. The path to the loaded skin directory and the skinImgDir are prepended to
    // this filename to form the full URL. If this item is disabled, and +link{menuItem.disabledIcon}
    // is set, then that icon will be used instead.
    // <p>
    // If you need to set this state dynamically, use +link{menuItem.dynamicIcon} instead.
    // @group menuIcons
    // @visibility external
    // @example fullMenu
    //<

    //> @attr menuItem.iconWidth (number : 16 : IR)
    // The width applied to this item's icon.  The default of <code>16</code> can be changed
    // for all MenuItems by overriding +link{Menu.iconWidth}.
    // @group menuIcons
    // @visibility external
    //<

    fixedIconWidth: true,

    //> @attr menuItem.iconHeight (number: 16 : IR)
    // The height applied to this item's icon.  The default of <code>16</code> can be changed
    // for all MenuItems by overriding +link{Menu.iconHeight}.
    // @group menuIcons
    // @visibility external
    //<

    //> @attr menuItem.keys (KeyIdentifier | Array of KeyIdentifer : null : IR)
    // Shortcut key(s) to fire the menu item action. Each key can be defined as a +link{KeyIdentifier}.
    // To apply multiple shortcut keys to this item, set this property to an array of such key
    // identifiers.
    //
    // @group menuKeys
    // @visibility external
    //<

    //> @attr menuItem.keyTitle (string : see below : IR)
    // A string to display in the shortcut-key column for this item. If not
    // specified, the first KeyName value in +link{menuItem.keys} will be used by default.
    // @group menuKeys
    // @visibility external
    // @example fullMenu
    //<

    //> @attr menuItem.fetchSubmenus (Boolean : true : IR)
    // If false, no submenus will be fetched for this MenuItem. This can be set globally via
    // +link{Menu.fetchSubmenus}.
    // @visibility external
    //<

    //> @method menuItem.enableIf()
    // Contains the condition that will enable or disable the current menuItem. The handler must be specified
    // as a function or string of script.  Return false to disable the menuItem or true to enable it
    // <p>
    // If you don't need to set this state dynamically, use +link{menuItem.enabled} instead.
    // <p>
    // May be defined as a +link{group:stringMethods,stringMethod}.
    // <p>
    // @param target (Canvas) +link{Menu.target,target} attribute for the top level menu.
    // @param menu (Menu) +link{Menu,menu} contains the reference to the menu that contains the current item
    // @param item (MenuItem) contains the reference to the current item
    // @return (boolean) Return true to show a checkmark by this menu item
    //
    // @group dynamicMenuItem
    // @visibility external
    // @example menuDynamicItems
    //<


    //> @method menuItem.checkIf()
    // Contains the condition that will check or uncheck the current menuItem. The handler must be specified
    // as a function or string of script.  Return false to uncheck the menuItem or true to check it
    // <p>
    // If you don't need to set this state dynamically, use +link{menuItem.checked} instead.
    // <p>
    // May be defined as a +link{group:stringMethods,stringMethod}.
    // <p>
    // @param target (Canvas) +link{Menu.target,target} attribute for the top level menu.
    // @param menu (Menu) +link{Menu, menu} contains the reference to the menu that contains the current item
    // @param item (MenuItem) contains the reference to the current item
    // @return (boolean) Return true to show a checkmark by this menu item
    //
    // @group dynamicMenuItem
    // @visibility external
    // @example menuDynamicItems
    //<


    //> @method menuItem.dynamicTitle()
    // Contains the condition that will change the current items' title when met. The handler must be specified
    // as a function or string of script.
    // <p>
    // If you don't need to set this state dynamically, use +link{menuItem.title} instead.
    // <p>
    // May be defined as a +link{group:stringMethods,stringMethod}.
    // <p>
    // @param target (Canvas) +link{Menu.target,target} attribute for the top level menu.
    // @param menu (Menu) +link{Menu, menu} contains the reference to the menu that contains the current item
    // @param item (MenuItem) contains the reference to the current item
    // @return (String) the title of this menuItem
    //
    // @group dynamicMenuItem
    // @visibility external
    // @example menuDynamicItems
    //<


    //> @method menuItem.dynamicIcon()
    // Contains the condition that will change the current items' icon when met. The handler must be specified
    // as a function or string of script.
    // <p>
    // If you don't need to set this state dynamically, use +link{menuItem.icon} instead.
    // <p>
    // May be defined as a +link{group:stringMethods,stringMethod}.
    // <p>
    // @param target (Canvas) +link{Menu.target,target} attribute for the top level menu.
    // @param menu (Menu) +link{Menu, menu} contains the reference to the menu that contains the current item
    // @param item (MenuItem) contains the reference to the current item
    // @return (SCImgURL) the url of this menuItems icon
    //
    // @group dynamicMenuItem
    // @visibility external
    // @example menuDynamicItems
    //<


    //> @method menuItem.click()
    // Executed when this menu item is clicked by the user. The click handler must be specified
    // as a function or string of script.  Return false to suppress the +link{Menu.itemClick()}
    // handler if specified.
    //
    // @param target (Canvas) for a menu shown as a context menu, the Canvas the menu was shown
    //                        on.  Otherwise the +link{Menu} instance of which this
    //                        +link{MenuItem} is a member.
    // @param item   (MenuItem) The +link{MenuItem} that was clicked on.
    // @param menu   (Menu)     The +link{Menu} instance of which this +link{MenuItem} is a
    //                          member.
    // @param [colNum] (number) Index of the column the user clicked. May be null if the
    //                          user activated the menu via a keyboard event.
    // @group menuItemEvents
    // @visibility external
    //<

    // Also support menuItem.action, of type "Action"
    // Documentation is bare-bones at the moment. Has to be visible in order to show up
    // in Visual Builder.
    //> @method menuItem.action()
    // Action to fire when this menu is activated.
    // @group menuBasic
    // @visibility external
    //<

    //>    @attr    menu.styleName        (CSSStyleName : "normal" : IRW)
    //            css class for the layer's contents
    //        @group    appearance
    //<
    // don't use the default ListGrid component/body styles, which usually have partial borders
    styleName:"normal",

    //> @attr menu.bodyStyleName (CSSStyleName : "normal" : IRW)
    // CSS style used for the body of this menu when there is no icon field. When there is an
    // icon field, then +link{iconBodyStyleName,iconBodyStyleName}, if set, will override this setting.
    // <p>
    // If applying a background-color to the body via a CSS style applied using this property,
    // be sure to set +link{listGrid.bodyBackgroundColor,bodyBackgroundColor} to <code>null</code>.
    // @group appearance
    // @visibility external
    //<
    bodyStyleName:"normal",

    //> @attr menu.iconBodyStyleName (CSSStyleName : null : IR)
    // If set, the CSS style used for the body of this menu when there <em>is</em> an icon field.
    // In RTL mode, the iconBodyStyleName is suffixed with "RTL", which allows skins to apply
    // different styles in LTR and RTL modes.
    // <p>
    // Note: Any skin which uses iconBodyStyleName should add "RTL" styles as well, even if
    // identical to LTR styles. Otherwise, menus may lose their styling in RTL mode.
    // @group appearance
    // @visibility external
    //<
    //iconBodyStyleName:null,


    //>    @attr   menu.submenuDelay (integer : 200 : [IRWA])
    //  Number of milliseconds to delay before hiding/changing submenu
    //  NOTE: in Windows this is 400ms, but it's also an official Windows Annoyance.
    //<
    submenuDelay:200,

    // shift the submenu this many pixels right of the right border of the parent menu.
    submenuOffset: -4,

    //>    @attr    menu.defaultWidth        (int : 150 : [IRW])
    //          The default menu width.
    //      @visibility external
    //      @group  sizing
    //<
    defaultWidth:150,

    //>    @attr    menu.defaultHeight        (int : 20 : [IRW])
    //          The default menu height.
    //<
    defaultHeight:20,

    // enforceMaxHeight:
    // If a menu contains enough items that the height of the menu exceeds the result of
    // this.getMaxHeight() [overridden to default to page height if this.maxHeight is unset]
    // we want to introduce scrollbars so the user can access all menu items without
    // scrolling the page.


    //> @attr   menu.enforceMaxHeight (boolean : true : [IRW])
    // If true, don't allow the menu to grow beyond +link{Menu.maxHeight} - or the height of the
    // page if this is unspecified.
    // If menu content exceeds this size, introduce scrollbars to allow the user to access
    // all menu items.
    // @see menu.maxHeight
    // @group sizing
    //<
    enforceMaxHeight:true,

    //> @attr   menu.maxHeight  (number : null : [IRW])
    // Maximum height for this menu before it starts scrolling if +link{Menu.enforceMaxHeight}
    // is true.
    // If unset defaults to the height of the page
    // @group sizing
    // @see menu.enforceMaxHeight
    //<
    // This is an override - canvas.maxHeight is 10000
    maxHeight:null,

    //>    @attr    menu.cellHeight        (number : 20 : [IRW])
    // The height of each item in the menu, in pixels.
    //        @group    sizing
    //      @visibility external
    //      @group  sizing
    //<

    //>    @attr    menu.backgroundColor        (CSScolor : null : IRWA)
    // No background color -- if we had one, it'd be visible below the part of the menu that
    // draws.
    //        @group    appearance
    //<
    backgroundColor:null,

    //>    @attr    menu.overflow        (Overflow : isc.Canvas.VISIBLE : IRWA)
    // Show everything
    //<
    overflow:isc.Canvas.VISIBLE,

    //>    @attr    menu.bodyOverflow        (Overflow : isc.Canvas.VISIBLE : IRWA)
    // Have the body show everything as well
    //<
    bodyOverflow:isc.Canvas.VISIBLE,

    //> @attr menu.arrowKeyAction (string : "focus" : IRWA)
    // Override arrowKeyAction so we focus on menu items when the user hits the up and down
    // arrows, but don't fire the click action unless the user hits space or enter
    //<
    arrowKeyAction:"focus",

    // Disable hiliteOnFocus - we handle this ourselves and don't want (for example)
    // the hilite to be cleared when we show a submenu and put focus there.
    hiliteRowOnFocus:false,

    //>    @attr    menu.selectionType        (SelectionStyle : isc.Selection.NONE : IRWA)
    //            menus aren't really selectable as ListGrids are
    //<
    selectionType:isc.Selection.NONE,

    //> @attr   menu.autoDraw       (Boolean : false : IRWA)
    //      Menus will not draw on initialization, until they're explicitly show()n
    //  @visibility external
    //<
    autoDraw:false,

    //>    @attr    menu.tableStyle        (string : "menuTable" : IRW)
    // CSS style for the menu table
    //<
    tableStyle:"menuTable",

    //>    @attr    menu.showRollOver        (boolean : true : IRW)
    //            Should we show mouseOver effects for the menu itself?
    //<
    showRollOver:true,

    //>@attr    menu.showFocusOutline (boolean : false : IRW)
    // Don't show the native focus outline around menus.
    // @visibility internal
    //<
    showFocusOutline:false,

    //>    @attr    menu.showAllRecords        (boolean : true : IRW)
    //            Show all records in the menu by default
    //<
    showAllRecords:true,

    // allow columns and rows to expand to show full menu item title text
    fixedFieldWidths:false,
    fixedRecordHeights:false,
    // don't leave a gap for a scrollbar
    leaveScrollbarGap:false,

    // CSS styles
    //>    @attr    menu.baseStyle        (CSSStyleName : "menu" : IRW)
    //            CSS style for a normal cell
    // @visibility external
    //<
    baseStyle:"menu",

    //> @attr menu.alternateRecordStyles (Boolean : false : IRW)
    // Explicitly disable alternateRecordStyles at the menu level by default so setting
    // to true for all ListGrids will not impact menus' appearance.
    // @visibility external
    //<
    alternateRecordStyles:false,

    //>    @attr    menu.showHeader        (boolean : false : IRWA)
    //    Don't display a normal header in the menu.
    //<
    showHeader:false,

    //>    @attr    menu.showSortArrow        (SortArrow : isc.ListGrid.NONE : IRWA)
    //        @group    appearance
    //            Don't show any sorting appearance at all.
    //<
    showSortArrow:isc.ListGrid.NONE,

    //>    @attr    menu.canDrag        (boolean : false : IRWA)
    //            can't drag into menus
    //<
    canDrag:false,

    //>    @attr    menu.canAcceptDrop        (boolean : false : IRWA)
    //            can't drop into menus (hmmmmm)
    //<
    canAcceptDrop:false,

    //>    @attr    menu.canReorderRecords        (boolean : false : IRWA)
    //            can't reorder menu items by dragging
    //<
    canReorderRecords:false,

    //>    @attr    menu.useKeys        (Boolean : true : [IRW])
    // A boolean indicating whether this menu should use shortcut keys. Set useKeys to
    // false in a menu's initialization block to explicitly disable shortcut keys.
    //      @visibility external
    //<
    useKeys:true,

    //>    @attr    menu.showKeys        (Boolean : true : [IRW])
    // A boolean, indicating whether the shortcut key column should be displayed. If
    // showKeys is not set, the menu will show the key column only if one of its items
    // specifies a keys property. If showKeys is false, the keys will not be displayed,
    // but will still function.
    //      @visibility external
    //<
    showKeys:true,

    //>    @attr    menu.showIcons        (Boolean : true : [IRW])
    // A boolean, indicating whether the checkmark/custom icon column should be displayed.
    // @visibility external
    //<
    showIcons:true,

    //>    @attr    menu.showSubmenus        (Boolean : true : [IRW])
    // A boolean, indicating whether the submenu indicator column should be displayed. If
    // showSubmenus is not set, the menu will show the indicator column only if one of its
    // items specifies a submenu property. If showSubmenus is false, the submenu arrows
    // will not be displayed, but submenus will still appear on rollover.
    //      @visibility external
    //<
    showSubmenus:true,

    //>    @attr    menu.submenuDirection (string : null : [IRW])
    // Should submenus show up on our left or right. Can validly be set to <code>"left"</code>
    // or <code>"right"</code>. If unset, submenus show up on the right by default in
    // Left-to-right text mode, or on the left in Right-to-left text mode (see +link{isc.Page.isRTL}).
    //      @visibility external
    //<
    submenuDirection:null,

    getSubmenuDirection : function () {
        if (this.submenuDirection != null) return this.submenuDirection;
        if (this.isRTL()) return "left";
        return "right";
    },

    //>    @attr    menu.showFieldsSeparately        (boolean : false : IRWA)
    // We never clip menu contents, so we have no reason to show fields separately, and it's
    // faster to show them together!
    //<
    showFieldsSeparately:false,

    //> @attr menu.emptyMessage (HTMLString : "[Empty menu]" : IRWA)
    // Message to show when a menu is shown with no items.
    // @group i18nMessages
    // @visibility external
    //<
    emptyMessage : "[Empty menu]",

    //>    @attr    menu.cellSpacing        (number : 0 : IRWA)
    //            No cell spacing.
    //<
    cellSpacing:0,

    //>    @attr    menu.cellPadding        (number : 0 : IRWA)
    //            Put some space between the text and cell boundaries
    //<
    cellPadding:2,

    //>    @attr    menu.iconWidth        (number : 16 : [IRW])
    //          The default width applied to custom icons in this menu. This is used whenever
    //          item.iconWidth is not specified.
    //      @visibility external
    //<
    iconWidth:16,

    //>    @attr    menu.iconHeight        (number : 16 : [IRW])
    //          The default height applied to custom icons in this menu. This is used whenever
    //          item.iconHeight is not specified.
    //      @visibility external
    //<
    iconHeight:16,

    //>Animation
    //> @attr menu.showAnimationEffect (string : null : [IRWA])
    // When this menu is shown how should it animate into view? By default the menu will just
    // show at the specified size/position. Options for animated show effects are <code>"fade"</code>
    // to fade from transparent to visible, <code>"slide"</code> to slide the menu into view,
    // or <code>"wipe"</code> to have the menu grow into view, revealing its content as it
    // grows. Can be overridden by passing the 'animationEffect' parameter to 'menu.show()'
    // @visibility animation
    //<
    //<Animation

    //>    @attr    menu.autoSetDynamicItems        (boolean : true : IRWA)
    //            true == check menu items for an 'enableif', etc. properties and update dynamically
    //        @group    appearance
    //<
    autoSetDynamicItems:true,

    //>    @attr    menu.skinImgDir        (URL : "images/Menu/" : IRWA)
    //        @group    appearance, images
    //        Where do 'skin' images (those provided with the class) live?
    //        This is local to the Page.skinDir
    //<
    skinImgDir:"images/Menu/",

    //>    @attr    menu.submenuImage        (Img Properties : {...} : IR)
    // Default image to use for the submenu indicator. Valid src, width and height must be
    // specified. See +link{class:ImgProperties} for format.<br>
    // If +link{menu.submenuDirection} is set to <code>"left"</code>, the image src will have
    // the suffix <code>"_left"</code> appended to it.
    //
    // @visibility external
    //<
    submenuImage:{src:"[SKIN]submenu.gif", width:7, height:7},

    //>    @attr    menu.submenuDisabledImage        (Img Properties : {...} : IR)
    // Default image to use for the submenu indicator when item is disabled. Valid src, width and
    // height must be specified. See +link{class:ImgProperties} for format.<br>
    // If +link{menu.submenuDirection} is set to <code>"left"</code>, the image src will have
    // the suffix <code>"_left"</code> appended to it.
    //
    // @visibility external
    //<
    submenuDisabledImage:{src:"[SKIN]submenu_disabled.gif", width:7, height:7},

    //>    @attr    menu.checkmarkImage        (Img Properties : {...} : IR)
    // Default image to display for checkmarked items. See +link{class:ImgProperties} for format.
    // Valid src, width and height must be specified.
    //
    // @visibility external
    //<
    checkmarkImage:{src:"[SKIN]check.gif", width:9, height:9},

    //>    @attr    menu.checkmarkDisabledImage        (Img Properties : {...} : IR)
    // Default image to display for disabled checkmarked items. See +link{class:ImgProperties}
    // for format. Valid src, width and height must be specified.
    //
    // @visibility external
    //<
    checkmarkDisabledImage:{src:"[SKIN]check_disabled.gif", width:9, height:9},


    useBackMask:true,

    //>    @attr    menu.submenuInheritanceMask (array  : (various)[] : RWA)
    //  Array of the property names that submenus should inherit from their parents (on creation)
    //<
    // This is basically all the properties a developer is likely to apply to a menu
    submenuInheritanceMask : [
        // Allow the developer to specify some custom class for submenus (advanced!)
        "submenuConstructor",

        "_treeData", // Tree data model for tree menus

        "className",
        "submenuDelay",
        "submenuOffset",
        "defaultWidth",

        "backgroundColor",

        "tableStyle",
        "showRollOver",

        "baseStyle",

        "emptyMessage",

        "canDrag",
        "canAcceptDrop",
        "canReorderRecords",

        "useKeys",
        "showKeys",
        "showIcons",
        "showSubmenus",
        "submenuDirection",
        "cellPadding",
        "iconWidth","iconHeight",
        "autoSetDynamicItems",
        "skinImgDir",
        "submenuImage","submenuDisabledImage","checkmarkImage","checkmarkDisabledImage",
        "bodyDefaults",

        // actual behaviors
            "itemClick",
            "canSelectParentItems",

        // updated on the fly
        "childrenProperty",

        // A freeform object - can be used for custom overrides that need to percolate down
        // the submenu chain.
        "inheritedProperties"

    ],

    //>    @attr    menu.mergeSingleParent        (boolean : true : I)
    //        If the menu items are a tree and the top level has just one item, should
    //        we merge that item with its children in the menu for usability? If mergeSingleParent
    //        is true, this single parent item will be appended to its list of child items, after a
    //        separator, so the top level menu will not be just a single item.
    //<
    mergeSingleParent:true,

    //>    @attr    menu.canSelectParentItems        (boolean : null : IRW)
    //        If true, clicking or pressing Enter on a menu item that has a submenu will
    //        select that item (with standard behavior of hiding the menus, calling click
    //        handlers, etc) instead of showing the submenu.
    // @group selection
    // @visibility external
    // @example treeBinding
    //<

    //> @attr menu.autoDismiss (Boolean : true : IRW)
    // When false, when a menu item is chosen (via mouse click or keyboard), the menu is not
    // automatically hidden, staying in place for further interactivity
    // @see Menu.cascadeAutoDismiss
    // @visibility external
    //<
    autoDismiss:true,

    //> @attr menu.cascadeAutoDismiss (Boolean : true : IRW)
    // When true any generated submenus will inherit +link{menu.autoDismiss}
    // from this menu.
    // @visibility external
    //<
    cascadeAutoDismiss:true,

    //> @attr menu.autoDismissOnBlur (Boolean : true : IRW)
    // When false, when a user clicks outside the menu, or hits the Escape key, this menu
    // will not be automatically hidden, staying in place for further interactivity.
    // @visibility external
    //<
    autoDismissOnBlur:true,

    //> @attr menu.submenuConstructor (SCClassName : null : IR)
    // When using a Tree or hierarchical DataSource as the menu's data, optional subclass of
    // Menu that should be used when generating submenus.
    // @visibility external
    //<

    //> @attr menu.fetchSubmenus (Boolean : true : IR)
    // When using a Tree or hierarchical DataSource as the menu's data, submenus are
    // automatically generated from child nodes.  <code>fetchSubmenus</code>
    // can be set to false to disable this for the whole menu, or can be set false on a
    // per-item basis via +link{menuItem.fetchSubmenus}.
    // @visibility external
    //<
    fetchSubmenus:true
});

isc.Menu.addMethods({

//>    @method    menu.initWidget()    (A)
// Initialize this object.
//        @param    [all arguments]    (object)    objects with properties to override from default
//<
initWidget : function () {
    // Show an empty menu as a single disabled item
    this._setUpEmptyMessage();

    // Always define menus to be absolutely positioned.  Placing a MenuBar or MenuButton into
    // the document flow via relative positioning makes sense - but placing a Menu as such
    // would be bizarre and probably indicates a mistake.
    this.position = isc.Canvas.ABSOLUTE;

    var criteria = this.initialCriteria || this.criteria;

    // Handle menus being bound to hierachichal structured datasources

    var requestOperationId = this._requestOperationId;
    if (this.dataSource != null && !this.hasFlatDataSource()) {
    //if (this.dataSource != null && isc.ResultTree) {

        // Don't show the prompt as we load child menu data.
        var menu = this;
        var tree = this.createResultTree(criteria, null, {
            showPrompt:false,
            dataProperties: isc.addProperties( {
                autoOpenRoot: false,
                getOperationId : function (childDS, parentDS, parentNode) {
                    if (childDS == menu.dataSource && requestOperationId !== undefined) {
                        return requestOperationId;
                    } else {
                        return this.Super("getOperationId", arguments);
                    }
                }
            }, this.dataProperties)
        });
        // In order to have the ability to combine hand-specified items with tree loading
        // behavior, we must add the hand specified items as children of the tree root here.
        if (this.items) tree.addList(this.items, tree.getRoot());

        // If we're loading all the data upfront, do this now rather than lazily when submenus
        // are being generated
        if (this.loadDataOnDemand == false) {
            tree.loadChildren(tree.getRoot(), {caller:this, methodName:"treeDataLoaded"});
        }
        this.data = tree;
    // we have flat data, so create a resultSet instead
    } else if (this.dataSource != null) {
        var ds = isc.DataSource.get(this.dataSource),
            requestProperties;
        if (requestOperationId !== undefined) {
            requestProperties = { operationId: requestOperationId };
        }
        ds.fetchData(criteria, {caller:this, methodName:"flatDataLoaded"}, requestProperties);
    }

    // make items a synonym for data
    if (this.data == null && this.items != null) this.data = this.items;

    // if items were specified as a tree or resultTree, skip over the root
    // (check for isc.Tree first to eliminate dependency)
    if (isc.Tree && isc.isA.Tree(this.data)) {
        this.setTreeData(this.data, false);

        // submenus will be passed a treeParentNode - the node whose children this menu will
        // display.  It's also possible to have a Menu show a portion of a Tree by passsing a
        // treeParentNode in as part of the constructor.
    }
//    alert(this.data);

    this.Super(this._$initWidget);

    // if the fields are not set, set with the class default items
    if (!this.fields) {
        // this flag can be used when creating submenus to detect the case where we have
        // explicitly specified fields that should be copied down to our children
        this._standardFields = true;
        this.fields = [];

        var submenusOnLeft, submenuFieldAtStart;
        if (this.showSubmenus) {
            submenusOnLeft = (this.getSubmenuDirection() == this._$left);
            submenuFieldAtStart = (submenusOnLeft != this.isRTL());
        }
        if (submenuFieldAtStart && this.showSubmenus) this.fields.add(this.getSubmenuField());
        if (this.showIcons) this.fields.add(this.getIconField());
        this.fields.add(this._getTitleField());
        if (this.showKeys) this.fields.add(this.getKeyField());
        if (!submenuFieldAtStart && this.showSubmenus) this.fields.add(this.getSubmenuField());
    } else {
        this._standardFields= false;
        for (var i = 0; i < this.fields.length; i++) {
            var field = this.fields[i];
            field = this.getStandardField(field);
            if (field == null) {
                this.fields.removeAt(i);
                i -= 1;
            } else {
                this.fields[i] = field;
            }
        }
    }


    if (this.iconBodyStyleName != null && this.fields != null) {
        for (var i = 0; i < this.fields.length; i++) {
            if (this.fields[i] == "icon" || this.fields[i]._standardMenuIconField === true) {
                var styleName = this.iconBodyStyleName;
                if (this.isRTL()) styleName += "RTL";
                this.bodyStyleName = styleName;
                break;
            }
        }
    }

    // If you place an image without text into a table cell safari aligns it to the top of
    // the cell.  Providing align=BOTTOM forces the image into the center of the cell.
    if (isc.Browser.isSafari) {
        isc.addProperties(this.submenuImage, { align: "BOTTOM"});
        isc.addProperties(this.submenuDisabledImage, { align: "BOTTOM"});
    }

    if (isc.Browser.isStrict) {
        isc.addProperties(this.submenuImage, { align: "absmiddle"});
        isc.addProperties(this.submenuDisabledImage, { align: "absmiddle"});

    }

    // set up key listening if necessary
    if (this.useKeys) this.setUpKeyListening();

    // tree mode
    if (this._treeData) {
        if (!this.treeParentNode) this.treeParentNode = this._treeData.getRoot();
        this.setTreeNode(this.treeParentNode);
    }
},

setTreeData : function (treeData, setParentNode) {

    // hang onto the Tree object since we set this.data to an Array of nodes
    this._treeData = treeData;

    // Hang onto the childrenProperty, so we can easily tell which menu should
    // have auto-generated submenus
    this.childrenProperty = treeData.childrenProperty;

    // if only one top-level item and it has children, we can merge it with its children
    // for usability.

    var topLevel = treeData.getChildren(),
        mergeSingleParent = this.mergeSingleParent && !isc.isA.ResultTree(this._treeData) &&
                            topLevel.length == 1 && treeData.hasChildren(topLevel[0]);

    if (mergeSingleParent) {
        var topLevelItem = topLevel[0];
        // make a copy of the children array so we don't munge the data model
        this.data = treeData.getChildren(topLevelItem).duplicate();
        // add a separator between the children and the original top-level item
        this.data.add({isSeparator:true});
        // copy the top-level item, minus children
        var copiedItem = {};
        isc.addProperties(copiedItem, topLevelItem);
        // clear out the 'children property' on the merged item, so we don't get another
        // submenu
        copiedItem[this.childrenProperty] = null;
        // append it to the new top-level list
        this.data.add(copiedItem);

    // otherwise just start with the top level of the tree
    } else {

//            this.data = topLevel
        this.data = null;

    }
    // call setTreeNode to set up logic to 'setData' in response to fetch etc.
    // Skipped if this is called from init (as init handles this directly, including
    // supporting submenus with an explicitly specified treeParentNode).
    if (setParentNode && this._treeData) {
        this.treeParentNode = this._treeData.getRoot();
        this.setTreeNode(this.treeParentNode);
    }
},

hasFlatDataSource : function () {
    var ds = isc.DataSource.get(this.dataSource);
    var flatDS = ds.childrenField != null || (this.dataProperties && this.dataProperties.modelType == "children");
    var names = ds.getFieldNames();
    var hasPK = false, hasFK = false;
    for (var i=0; i < names.length; i++) {
        var fld = ds.getField(names[i]);
        if (fld.primaryKey) hasPK = true;
        if (fld.foreignKey) hasFK = true;
    }
    return !(flatDS || (hasPK && hasFK));
},

// Override setFields - if called with custom fields set the _standardFields flag to false
setFields : function (fields, a,b,c,d) {
    if (fields && (fields != this.fields)) {
        this._standardFields = false;
    }
    this.invokeSuper(isc.Menu, "setFields", fields, a,b,c,d);
},

// If we're loading our data up-front, this is notification that our data has been loaded.
treeDataLoaded : function () {
    // call the standard treeDataArrived method - this handles showing the data in the menu

    this.treeDataArrived(this._lastNode);
},

setTreeNode : function (node) {
    var loadState = this._treeData.getLoadState(node);
    this._lastNode = node;

    // If the children array is already loaded ensure it's visible in the menu as items
    if (loadState == isc.Tree.LOADED) {
        this.treeDataArrived(node);

    // Otherwise, if we've never kicked off a fetch for the data (first time this menu shown)
    // do so now.
    } else if (loadState != isc.Tree.LOADING) {
        this._treeData.loadChildren(node, this.getID()+".treeDataArrived(node)");
        this._loadingTreeNode = true;
        this.setData(null); // show loading message instead of current menu
    }
},

// called through from ResultTree whenever we get tree data from the server.
treeDataArrived : function (node) {
    delete this._loadingTreeNode;
    if (node == this._lastNode) {
        var children = this._treeData.getChildren(node);
        if (isc.isA.ResultSet(children)) {
            children = children.getAllLoadedRows();
        }
        this.setData(children);

        // Note: only show the submenu if we're still visible - the user may have made a
        // selection before the data came back.
        if (this.masterMenu && this.masterMenu.isVisible())
            this.masterMenu.placeSubmenu(node, this);
    }

    if (this._loadedByKeyPress) {
        delete this._loadedByKeyPress;
        this._navigateToNextRecord(1);
    }
},

flatDataLoaded : function (dsResponse, data) {
    this.setData(data);
},

getEmptyMessage : function () {
    if (this._loadingTreeNode) {
        return this.loadingDataMessage == null ? "&nbsp;"
            : this.loadingDataMessage.evalDynamicString(this, {
                    loadingImage: this.imgHTML(isc.Canvas.loadingImageSrc,
                                               isc.Canvas.loadingImageSize,
                                               isc.Canvas.loadingImageSize)
                });
    }
    return this.Super("getEmptyMessage", arguments);
},

isEmpty : function () {
    if (this._loadingTreeNode) return true;
    return this.Super("isEmpty", arguments);
},


// Ensure that the empty message shows up like a disabled menu item
_setUpEmptyMessage : function () {
    isc.addProperties(this, {
        emptyMessageTableStyle : this.tableStyle,
        emptyMessageStyle : this.baseStyle + isc.GridRenderer.standardStyleSuffixes[4]
    });
},


//>    @method menu._observeData() (A)
//        Override the _observeData method to set up the enableIf etc. functions
//         for the menu items.
//    @param    data    (object)        new data to be observed
//<
_observeData : function (data, a,b,c,d) {

    // If we're working with a tree-menu, this.data will just be the array of children
    // which is not expected to change directly.
    // Therefore in this case observe this._treeData instead.
    var alreadyObserving;
    if (this._treeData) {
        // Note: For tree submenus, setData() is used to populate the menu with
        // data, in response to dataArrived(), or a change of parent node.
        // setData() falls through to _observeData(). Therefore we may already be observing
        // the tree-menu data - if so, avoid calling the method to observe the data again.
        alreadyObserving = this.isObserving(this._treeData, "dataChanged");
        data = this._treeData;
    }
    if (!alreadyObserving) this.invokeSuper(isc.Menu, "_observeData", data, a,b,c,d);
},


// Event handling
// --------------------------------------------------------------------------------------------


//>    @method    menu.rowClick()    (A)
//        Handle the rowClick pseudo-event in the menu. Selects the appropriate menu item
//        @return    (boolean)    false == stop processing this event
//        @group    event handling
//<
rowClick : function (record, rowNum, colNum) {
    this.Super("rowClick", arguments);
    this.selectMenuItem(rowNum, colNum);
},

//>    @method    menu.selectMenuItem()    (A)
// Handle a selected menu item, either through a menu key or clicking on the item itself.
// <p>
// Calls item.click() or itemClick() for the selected item
//        @param    item        (item | number)        pointer to or number of the item that was clicked on
//      @param  colNum      (number)    Index of column that received the click. May be null if
//                                      the item was selected via keyboard selection.
//        @return    (boolean)    false == stop processing this event
//<
selectMenuItem : function (item, colNum) {
    if (item == null) item = this.getEventRecordNum();
    // normalize item to the item pointer in case a number was passed in
    item = this.getItem(item);

    var returnValue = true;

    // if the item was not found, bail
    if (item == null || !this.itemIsEnabled(item)) {
        isc.Menu.hideAllMenus("itemClick");
        return false;
    }

    // if the item has a submenu or children, and parent selection is not
    // enabled, show the submenu
    // (clear the submenu timer and hide any other submenu first)
    if (this.hasSubmenu(item) && !this.canSelectParentItems && !item.canSelectParent) {
        if (this.submenuTimer) this.submenuTimer = isc.Timer.clear(this.submenuTimer);
        if (this._openItem != item) this.hideSubmenu();
        this.showSubmenu(item);
        // return false so subclasses know not to do their thing
        return false;
    }

    // hide all menus automatically

    // get the autoDismiss property from the top level menu or the menu item
    var rootMenu = this;
    while (rootMenu._parentMenu) {
        rootMenu = rootMenu._parentMenu;
    }
    if (this.autoDismiss && (item.autoDismiss || item.autoDismiss == null) ) {
        isc.Menu.hideAllMenus("itemClick");
    }


    // if the item that was clicked on has an action, or click handler, call that
    if (item.action) {
        // Actions are a particular format of objects.
        // Also handle being pased an action string expression
        if (!isc.isA.Function(item.action)) {
            isc.Func.replaceWithMethod(item, "action", "");
        }
        if (item.action() == false) return false;

    }

    if (item.click) {
        // if the handler was defined as a string, convert it to a function
        if (!isc.isA.Function(item.click)) {
            isc.Func.replaceWithMethod(item, "click", "target,item,menu,colNum");
        }
        var target = (this.target ? this.target : this);
        // now call the handler -- return whether or not it returned false
        returnValue = item.click(target,item,this,colNum);
    }
    if (returnValue != false) {
        // otherwise call the generic itemClick() handler
        returnValue = this.itemClick(item, colNum);
    }
    // refresh the row after click if autoDismiss is false
    if (!(this.autoDismiss && (item.autoDismiss || item.autoDismiss == null))) {
        this.refreshRow(this.getRecordIndex(item));
    }
    return returnValue;
},

//>    @method    menu.mouseOver()    (A)
//          special mouseOver handler for submenus - simulates mouse over behavior for the
//          appropriate parent menu item.  Ensures that item is hilited and this submenu
//          won't be hidden by the parent menu's submenuTimer
//        @group    events, hiliting
//
//<
mouseOver : function () {
    // Make sure the appropriate parent menu item is hilighted
    // Will only be fired if necessary
    var parentMenu = this._parentMenu;
    if (parentMenu && parentMenu.body.lastOverRow != this._parentItemNum){
        // Prevent this submenu from being killed by the parent's submenuTimer
        if (parentMenu.submenuTimer) parentMenu.submenuTimer = isc.Timer.clear(parentMenu.submenuTimer);

        // Update the parent's hilight to point to the appropriate row
        parentMenu._hiliteRecord(this._parentItemNum);
    }

},

//>    @method    menu.rowOver()    (A)
// When the mouse goes over a row, start the submenu timer to show the appropriate submenu
//        @group    events, hiliting
//<
rowOver : function (row, field) {
    if (this.submenuTimer) this.submenuTimer = isc.Timer.clear(this.submenuTimer);
    this.submenuTimer = isc.Timer.setTimeout({target:this,method:this.changeSubmenu},
                                             this.submenuDelay);
},

//>    @method    menu.itemClick()    ([A])
//          Executed when a menu item with no click handler is clicked by the user. This
//          itemClick handler must be specified as a function. It is passed an item parameter that
//          is a reference to the clicked menu item.
//
//      @visibility external
//        @param    item        (object)        pointer to the item in question
//      @param  [colNum]  (number)  Index of the column clicked by the user. May be null if
//                                  this menu item was activated in response to a keyboard event.
//
//        @return    (boolean)    false if event processing should be stopped, true to continue
//      @example menuColumns
//<
itemClick : function (item, colNum) {
    // don't do anything by default
},

// show / hide submenus on right/left click [depending on which side they're being shown on]
getShowSubmenuKey : function () {
    return this.getSubmenuDirection() == "right" ? "Arrow_Right" : "Arrow_Left";
},

getHideSubmenuKey : function () {
    return this.getSubmenuDirection() == "right" ? "Arrow_Left" : "Arrow_Right";
},

//>    @method    menu.bodyKeyPress()
// Handler for keypress events called from this.body.keyPress - overridden to allow arrow
// key navigation of submenus, proper handling of "Enter" to select a menu item,
// and "Escape" to hide the menu.
//        @group    events
//
//        @return    (boolean)    false == stop processing this event
//<
bodyKeyPress : function (event, eventInfo) {
    var keyName = isc.EventHandler.lastEvent.keyName;

    // Navigate submenus with arrow left / arrow right
    if (keyName == this.getHideSubmenuKey()) {
        if (this._parentMenu != null) {
            this._parentMenu.hideSubmenu();
            this._parentMenu.focus();
            return false;
        }
    } else if (keyName == this.getShowSubmenuKey()) {
        var item = this.getItem(this.getFocusRow());

        if (this.hasSubmenu(item)) {
            this.changeSubmenu();
            // Note: Windows handling of submenus seems to be that if you open one via keyboard
            // the first item is hilighted by default, otherwise no item is hilighted.
            // therefore hilight first item here, but not in changeSubmenu()
            var submenu = this._open_submenu;
            if (submenu._loadingTreeNode) submenu._loadedByKeyPress = true;
            else                          submenu._navigateToNextRecord(1);
            return false;   // stop propogation
        }

    // hide the menu if escape is hit

    } else if (keyName == "Escape" && this.autoDismissOnBlur != false) {
        if (this._parentMenu != null) {
            this._parentMenu.hideSubmenu();
            this._parentMenu.focus();
        } else {
            isc.Menu.hideAllMenus("outsideClick");
        }
        return false;

    // override keypress on "Enter" to do a single record click rather than a double
    // (which has no meaning for menus)
    } else if (keyName == "Enter") {
        return this._generateFocusRecordClick();
        // hilite the first item?
    }

    return this.Super("bodyKeyPress", arguments);
},

// Override _navigateToNextRecord() to close the menu when the user attempts to select record
// '-1'. (IE: They have pressed the Arrow Up key while the first item in the menu is hilighted)
_navigateToNextRecord : function (step) {

    var newSelectionIndex = this.getFocusRow();

    // default to starting at zero
    if (newSelectionIndex == null) newSelectionIndex = 0;

    // If we're attempting to navigate to the previous item in the menu, check whether we'll
    // iterate up off the top of the menu.
    if (step == -1) {
        do {
            newSelectionIndex += step;

            // hide the list if you go off the top
            // (will focus back in whatever previously had focus)
            if (newSelectionIndex < 0) {
                this.hide();
                return false;   // cancel event propogation
            }

        } while (!this.itemIsEnabled(newSelectionIndex))
    }

    return this.Super("_navigateToNextRecord", arguments);
},

// Showing and hiding
// --------------------------------------------------------------------------------------------
//>    @method    menu.show()
// Show the menu, hiding other visible menus as appropriate.
// <p>
// Sets up to dismiss on any click outside the menu.
//        @group    visibility
//<
//>Animation
// @param animationEffect (string) Allows the user to specify an animation effect for showing
// the menu - valid options are <code>"fade"</code>, <code>"slide"</code> and <code>"wipe"</code>
// If no value is passed for this parameter, checks <code>this.showAnimationEffect</code>
//<Animation

show : function (animationEffect) {

    // If the menu is currently offscreen, shift it back to last onscreen position before
    // showing.
    if (this._isOffscreen) {
        this.moveTo(this._onscreenPosition[0], this._onscreenPosition[1]);
        this._isOffscreen = null;

    }

    //>Animation
    // Note: if an animation effect was passed in we call this.animateShow(), which will
    // call this method again - that's why we need the _animating parameter
    if (animationEffect == null) animationEffect = this.showAnimationEffect;
    var shouldAnimate = !this._animating && (animationEffect != null) &&
                                            (animationEffect != "none");
    if (shouldAnimate) {
        this._animating = true;
        this.animateShow(animationEffect, "this._showComplete()")
        return;
    }
    //<Animation

    // reset the enabled and title of menu items if necessary
    if (this.autoSetDynamicItems) this.setDynamicItems()

    // if the menu hasn't been drawn, draw it now
    if (!this.isDrawn()) {
        // Pass in the 'showing' parameter to avoid draw() calling show again.
        this.draw(true);
    }

    // ensure that when we get hidden, we focus back into whatever previously had focus
    this.body.focusOnHide = isc.EH.getFocusCanvas();

    // now add this menu to the list of _openMenus so it can be hidden automatically
    isc.Menu._openMenus.add(this);

    // if this is the first menu being opened, show the click mask
    if (this.autoDismissOnBlur) {

        isc.Menu._showMenuClickMask();
    }

    // bring this menu above everything else
    this.bringToFront();


    this.Super("show", arguments);

    if (!this._animating) this._showComplete();
},


_showComplete : function () {
    if (this._animating) delete this._animating;


    if (isc.Browser.isMoz) {
        this.getClipHandle().offsetLeft;
    }
    // grab focus for keyboard handling
    this.body.focus();
},


//>    @method    menu.hide()
//            hide this menu.
//          Overridden to clear all selections and clear open submenu pointers
//        @group    visibility
//
//<
hide : function () {
    // no-op if no change in visibility
    if (this.visibility == isc.Canvas.HIDDEN) return;

    // We occasionally get menus that are as taller than the page (introducing v-scrollbars).
    // In this case we don't want the height of the hidden menu to continue to effect the
    // page scroll height, so shift offscreen when hiding.
    this._moveMenuOffscreen();

    this.Super("hide", arguments);

    // clear hilite, as menus should always start with no hilite
    this.clearLastHilite();

    this._lastRecordClicked = null;

    if (this._openItem) delete this._openItem;
    if (this.submenuTimer) isc.Timer.clearTimeout(this.submenuTimer);


},

// Context menu handling
// --------------------------------------------------------------------------------------------

//>    @method    menu.showContextMenu()
// Show this menu as a context menu, that is, immediately adjacent to the current mouse position.
//
// @visibility external
//
//        @group    visibility
//        @return    (Boolean)    false == stop processing this event
//<
showContextMenu : function (event) {

    if (event && (event.target == this || (this.body && event.target == this.body))) {
        if (this.body) {


            if (isc.Browser.isSafari) {
                this.body._mouseDownRow = this.getEventRow();
                this.body._mouseDownCol = this.getEventColumn();
            }

            this.body.click();
        }
        return false;
    }

    var target;
    // if we were explicitly passed a target canvas, use it
    if (isc.isA.Canvas(event)) target = event;
    // otherwise, if passed an event, use the target of the event
    if (event != null && event.target != null) target = event.target;

    if (target != null) this.target = target;

    this.positionContextMenu();
    this.show();

    return false;
},



getMaxHeight : function () {
    if (this.maxHeight != null) return this.maxHeight;
    return isc.Page.getHeight() - this.getScrollbarSize();
},

_showOffscreen : function () {
    if(!this.isDrawn()) {
        // draw, but avoid the call to 'show()' since we don't want to focus on this widget
        this.setVisibility(isc.Canvas.HIDDEN);
        this.draw();
    }

    this.setVisibility(isc.Canvas.VISIBLE);

    this._moveMenuOffscreen();

    if (this.isDirty() || this.body.isDirty()) this.redraw();

    if (this._overflowQueued) this.adjustOverflow();

    // If we're enforcing max height, handle this now - introducing scrollbars if necessary
    // (Can skip if we've already calculated the height and the content / sizing of the
    // menu has not changed).
    if (!this._heightCalculated && this.enforceMaxHeight) {
        this.doEnforceMaxHeight();
    }

    this.setVisibility(isc.Canvas.HIDDEN);
},

doEnforceMaxHeight : function () {
    // If we're currently showing scrollbars, reset to overflow visible and default sizing
    // to ensure that they're necessary

    if (this.overflow != isc.Canvas.VISIBLE) {
        this.leaveScrollbarGap = false;
        this.setOverflow(isc.Canvas.VISIBLE);
        this.setHeight(this.defaultHeight);
        this.setWidth(this._origWidth || this.defaultWidth);
        this.adjustOverflow();
    }
    var height = this.getVisibleHeight(),
        width = this.getVisibleWidth(),
        maxHeight = this.getMaxHeight();
    if (this.overflow == isc.Canvas.VISIBLE && height > maxHeight) {
        this.leaveScrollbarGap = true;
        this.setHeight(maxHeight);
        this._origWidth = this.getWidth();  // remember the user-specified width so we can
                                                // set back to it if we have fewer items
        this.setWidth(this.getVisibleWidth() + this.getScrollbarSize())
        this.setOverflow(isc.Canvas.AUTO);
        this.adjustOverflow();
    }

    this._heightCalculated = true;

    // page resizes may affect maxHeight, so pay attention to them
    if (!this._pageResizeEvent) {
        this._pageResizeEvent = isc.Page.setEvent(
           "resize",
            this,
            null,
           "pageResized"
        );
    }
},

pageResized : function () {
    if (this.enforceMaxHeight) this.doEnforceMaxHeight();
},

// If our set of items changes (due to a setData() call, or a change to our data object)
// we will have to determine whether we exceed this.maxHeight (and thus need scrollbars)
// or not.
// For tree data, each generated submenu observes the main tree, so this
// method will fire for every submenu when the tree data changes, (causing a redraw of every
// visible menu to show the new data).
dataChanged : function (a,b,c,d) {
    // for tree submenus, if our parent node has been removed, self-destruct.
    // Note: We re-use tree submenus for each folder at any level of a tree.
    // This means that if our parent node has been removed from the tree, this menu is not
    // necessarily obsolete - it could be re-used for other folder nodes in the parent folder.
    // Rather than destroy() ing the submenu here, we could just hide it, but this would mean
    // in the case where there are no other folder nodes in the parent folder we'd end up with
    // orphaned menus that would never get destroyed.
    if (this._treeData && this._lastNode != null) {
         if (!this._treeContains(this._lastNode)) {

            // If this is the root menu, don't destroy - instead we need to bind to the new
            // root node.
            if (this._parentMenu == null) {
                this.setTreeNode(this._treeData.getRoot());

            } else {
                // Note this will also fire for any submenus of this menu
                this.destroy(true);
                return;
            }
        }
    }
    var rv = this.invokeSuper(isc.Menu, "dataChanged", a,b,c,d);
    delete this._heightCalculated;

    return rv;
},

_treeContains : function (node) {
    while (node) {
        if (this._treeData.isRoot(node)) return true;
        node = this._treeData.getParent(node);
    }
    return false;
},

//> @method Menu.setData()
// Change the set of items to display in this menu
// @param items (array of MenuItems) new items for this menu
// @group data
// @visibility external
//<
setData : function (data,b,c,d) {

    var rv;
    if (isc.Tree && isc.isA.Tree(data)) {

        this.setTreeData(data, true);
    } else {
        rv = this.invokeSuper(isc.Menu, "setData", data,b,c,d);
    }
    delete this._heightCalculated;
    return rv;
},

//> @method Menu.setItems()
// Synonym for +link{Menu.setData()}.
// @param items (array of MenuItems) new items for this menu
// @group data
// @visibility external
//<
setItems : function (a,b,c,d) {
    return this.setData(a,b,c,d);
},

//>EditMode add a method to retrieve menu items by a "name" property, discoverable
// by type.
getMenuItem : function (name) {
    return isc.Class.getArrayItem(name, this.data, "name");
},
//<EditMode

// hang a flag on this item when we move it offscreen so we can tell if we need to reposition
// on show.

_moveMenuOffscreen : function () {
    if (this.parentElement != null) return;
    // No op if we're already offscreen
    if (this._isOffscreen) return;

    this._onscreenPosition = [this.getLeft(), this.getTop()];
    this.moveTo(0, -9999);

    this._isOffscreen = true;
},

// Override moveBy: If the menu has been moved offscreen, and is being moved again, clear
// out the 'isOffscreen' flag, so we don't incorrectly reposition to original onscreen position
// on show().

moveBy : function () {
    var returnVal = this.Super("moveBy", arguments);
    if (this._isOffscreen) this._isOffscreen = false;
    return returnVal;
},

// Override resizesBy: If we have calculated the drawn size of the menu
// (potentially with scrollbars) and the developer changes the specified height or width
// we're going to have to recalculate when we next show the menu.
resizeBy : function (dX, dY, a,b,c,d) {
    if ((dX != null && dX != 0) || (dY != null && dY != 0)) delete this._heightCalculated;
    return this.invokeSuper(isc.Menu, "resizeBy", dX,dY, a,b,c,d);
},

//>    @method    menu.hideContextMenu()
// Hide the context menu - alias for hide()
// @group    visibility
// @visibility external
//<
hideContextMenu : function () {
    this.hide();
},

// place the context menu immediately adjacent to the mouse pointer, keeping it onscreen
positionContextMenu : function () {

    this._showOffscreen();

    // place the menu immediately adjacent to the mouse pointer, keeping it onscreen

    var event = isc.EH.getLastEvent(),
        left = event.x,
        top = event.y
    ;

    if (this.isRTL()) {
        // place the menu to the left of the mouse in RTL mode
        left -= this.getVisibleWidth();
    }
    this.placeNear(left, top);

},

// Menu items and submenus
// --------------------------------------------------------------------------------------------


//>    @method    menu.getItem()
// Get a particular MenuItem by index.
// <P>
// If passed a MenuItem, returns it.
//
// @param item (int) index of the MenuItem
// @return (MenuItem) the MenuItem, Pointer to the item, or null if not defined
// @group menuItems
// @visibility external
//<
getItem : function (item) {
    return isc.Class.getArrayItem(item, this.data, "name");
},

//> @method menu.setItemProperties()
// Set arbitrary properties for a particular menu item.
//
// @param    item (int) index of the MenuItem
// @param    properties (MenuItem Properties) properties to apply to the item
// @visibility external
//<
// NOTE: little testing has been done on which properties can actually be set this way

setItemProperties : function (item, properties) {
    var item = this.getItem(item);
    if (item != null) {
        isc.addProperties(item, properties);
    }
    if (this.isVisible()) this.redraw();
},

//>    @method    menu.getItemNum()
// Given a MenuItem, return it's index in the items array.
//
// @param item (MenuItem | int)    index of the item or MenuItem itself
// @return (int) index of the item, or -1 if not defined.
// @group menuItems
// @visibility external
//<
getItemNum : function (item) {
    return isc.Class.getArrayItemIndex(item, this.data, "name");
},

//> @method menu.getItems()
// Get all the MenuItems in the current menu as an Array.
//
// @return (Array of MenuItem)
// @visibility external
//<
getItems : function () {
    return this.getData();
},

//>EditMode
addItem : function (item, index) {
    if (index == null) index = this.data.getLength();
    this.data.addAt(item, index);
    this.markForRedraw();
},
removeItem : function (item) {
    this.data.remove(item);
    this.markForRedraw();
},
//<EditMode

//>    @method    menu.changeSubMenu()    (A)
// Hide the last open submenu, if any, and show the submenu for this.body.lastOverRow, if
// specified
//        @group    visibility
//<
changeSubmenu : function () {

    var overItem = this.getItem(this.body.lastOverRow);
    // If the menu to be shown is already showing, just bail.
    if (overItem && this._openItem == overItem) return;

    this.hideSubmenu();
    if (overItem != null) this.showSubmenu(overItem);
},

// Helper method to determine whether an item has a submenu
hasSubmenu : function (item) {
    if (!item) return false;
    if (item.submenu) return true;

    // flag to disable loading submenus when
    if (this.fetchSubmenus == false || item.fetchSubmenus == false) return false;

    if (isc.isA.Tree(this._treeData)) {
        // If this is a client-only tree we can avoid showing submenus if the node
        // has no children.
        return (this._treeData.isFolder(item) &&
                ((isc.ResultTree && isc.isA.ResultTree(this._treeData)) ||
                 this._treeData.hasChildren(item)));
    }

    return false;
},


//>    @method    menu.showSubmenu()    (A)
// Show the submenu for the specified item, if it has one.
// <P>
// Normally triggered automatically by user interaction.
//
//        @group    visibility
//        @param    item        (MenuItem | number)        the item in question, or it's index
// @visibility external
//<
showSubmenu : function (item) {
    var submenu = this.getSubmenu(item);
    if (!submenu) return;

    this.placeSubmenu(item, submenu);
},

//> @method menu.getSubmenu()  (A)
// Get the submenu for a particular menu item.
// <P>
// Override to provide dynamic generation of submenus.
//
// @param    item        (MenuItem | number)        the item in question, or it's index
// @return (Menu) the submenu
// @visibility external
//<

getSubmenu : function (item) {

    // normalize item to the item pointer in case a number was passed in
    item = this.getItem(item);

    // if there's no submenu (or no children in tree-mode), bail
    if (!this.hasSubmenu(item)) return;


    // Force the submenu to inherit most developer defined UI properties
    // from this menu.
    // Note: This properties block will be applied to the submenus that we generate here,
    // but not to a submenu that's already a Menu widget
    var properties = isc.applyMask(this, this.submenuInheritanceMask);
    // Have submenus inherit any custom fields (need to duplicate rather than just inherit
    // the fields array)
    if (!this._standardFields) {
        var fields = [];
        for (var i = 0; i < this.fields.length; i++) {
            // do a deep duplicate of our fields so we don't manipulate our
            // submenus' field objects by accident
            fields[i] = isc.addProperties({}, this.fields[i]);
        }
        properties.fields = fields;
    }
    if (this.cascadeAutoDismiss) {
        properties.autoDismiss = this.autoDismiss;
    }

    var newID = this.getID() + "_generatedSubmenu";
    var submenu = item.submenu;

    if (item[newID]) {
        if (item[newID].destroying || item[newID].destroyed) {
            delete item[newID];
        } else {
            submenu = item[newID];
        }
    }

    // tree mode
    if (!submenu) {
        // item is a parent in a Tree; create a submenu based on the Tree children of this
        // parent.

        var rootMenu = (this._rootMenu || this),
            menuLevel = (rootMenu == this ? 0 : this._menuLevel);

        if (!rootMenu._submenus) rootMenu._submenus = [];

        //>DEBUG
        this.logDebug("RootMenu:" + rootMenu.getID() + ", submenus:" +
                      rootMenu._submenus + ", Level:" + menuLevel);
        //<DEBUG

        // check if we've already created a submenu at this level
        submenu = rootMenu._submenus[menuLevel];
        if (!submenu) {

            // create the menu
            isc.addProperties(properties, {
                ID:(rootMenu.getID() + "_childrenSubMenu_" + menuLevel),
                _rootMenu:rootMenu,
                _menuLevel:menuLevel + 1,   // it's submenu will be one level below itself
                autoDraw:false,
                // for treeMenus, pass the parentNode to the submenu so it can initiate a fetch
                // for the relevant node
                treeParentNode: this._treeData ? item : null,
                masterMenu: this
            });

            var cons = this.submenuConstructor || this.menuConstructor || isc.Menu;

            submenu = isc.ClassFactory.newInstance(cons, properties);

            // cache the submenu for this level
            rootMenu._submenus[menuLevel] = submenu;

            // Ensure that we clean up when submenus get destroy()d
            rootMenu.observe(submenu, "destroy", function () {
                this.submenuDestroyed(menuLevel);
            });

        }
    } else if (!isc.isA.Menu(submenu)) {
        // keep track of autoGenerated submenus so we can auto-destroy
        if (!this._submenus) this._submenus = [];

        if (isc.isA.String(submenu)) {
            submenu = window[submenu];
        } else if (isc.isAn.Array(submenu)) {
            submenu = this.getMenuConstructor().create({autoDraw: false, data: submenu}, properties);
            this._submenus.add(submenu);
        } else if (isc.isAn.Object(submenu)) {
            submenu = this.getMenuConstructor().create(isc.addProperties({autoDraw: false}, properties, submenu));
            this._submenus.add(submenu);
        }


        item[newID] = submenu;


        if (submenu && submenu.__sgwtRelink) submenu.__sgwtRelink();
    }

    if (this._treeData) submenu.setTreeNode(item);
    return submenu;
},

// Method invoked when a generated submenu gets destroy()d.
// Clean out our reference to it to avoid leaks.
submenuDestroyed : function (menuLevel) {
    delete this._submenus[menuLevel];
},

// show a menu as a submenu next to the given item
placeSubmenu : function (item, submenu) {
    // No op if called repeatedly

    if (this._openItem == item && this._open_submenu == submenu) return;

    // remember the submenu so we can close it later
    this._openItem = item;
    this._open_submenu = submenu;

    // Show the submenu offscreen.  This ensures that is has been drawn (so we can determine
    // it's size), but is not visible to the user.
    // It also prevents Moz's strange "flash" in the old position.
    submenu._showOffscreen();

    // place the menu adjacent to the item that's showing it
    // Note: use '_placeRect()' to avoid placing submenus offscreen (and avoid occluding the
    // super-menu)
    var itemNum = this.getItemNum(item),
        submenuRect = submenu.getPeerRect(),
        pos = isc.Canvas._placeRect(
            submenuRect[2], submenuRect[3],
            {left:this.getPageLeft() - this.submenuOffset,
             width:this.getVisibleWidth()  + this.submenuOffset,
             top:this.body.getRowPageTop(itemNum)
             // No need for height - we want it to be as close to that point as possible
             },
             this.getSubmenuDirection() == this._$left ? this._$left : this._$right,
             false
        )


    submenu.setPageRect(pos[0], pos[1]);

    // the "target" property is passed to the click() function.  if the main menu has been
    // given a "target" other than itself (the default), give it to the submenu if it also has
    // no target.
    if (this.target != this && submenu.target != submenu) {
        submenu.target = this.target;
    }

    submenu.show();
    submenu._parentMenu = this;
    submenu._parentItemNum = itemNum;

    // If we just placed a submenu, we don't want to do a delayed 'placeSubmenu' for another
    // menu that we're waiting on data from.
    if (isc.Menu._submenuQueue) delete isc.Menu._submenuQueue[this.getID()];
},


//>    @method    menu.hideMenuTree()    (A)
//            Hide this menu and any open submenus of this menu
//        @group    visibility
//<
hideMenuTree : function () {
    this.hideSubmenu();
    this.hide();
},

//>    @method    menu.hideSubmenu()    (A)
//            hide the open sub menu of this menu if there is one
//        @group    visibility
//<
hideSubmenu : function () {
    if (this._open_submenu) {
        this._open_submenu.hideSubmenu();
        this._open_submenu.hide();
        delete this._open_submenu;
        delete this._openItem;
    }
},


//>    @method    menu.getSubmenuImage()    (A)
//            return the icon that indicates that there's a submenu in this item (if there is one)
//
//        @param    item        (paramtype)    item in question
//<
_$left:"left", _$right:"right",
getSubmenuImage : function (item) {

    if (!this.hasSubmenu(item)) return "&nbsp;";

    if (!this._submenuImage) {
        var leftSM = (this.getSubmenuDirection() == this._$left),

            subImgObj = isc.addProperties({}, this.submenuImage),
            subDisImgObj = isc.addProperties({}, this.submenuDisabledImage);

        subImgObj.src = isc.Img.urlForState(subImgObj.src, null, null,
                                                (leftSM ? this._$left : null));
        subDisImgObj.src = isc.Img.urlForState(subDisImgObj.src, null, null,
                                                (leftSM ? this._$left : null));

        this._submenuImage = this.imgHTML(subImgObj);
        this._submenuDisabledImage = this.imgHTML(subDisImgObj);
    }

    return (this.itemIsEnabled(item) ? this._submenuImage : this._submenuDisabledImage);
},


// Enabling and disabling items, setting titles, showing icons, etc.
// --------------------------------------------------------------------------------------------

//>    @method    menu.itemIsEnabled()    (A)
//        @group enable
//            Return if a particular item (specified by number) is enabled
//        @param    item        (item | number)        pointer to (or number of) the item in question
//        @return            (boolean)    true == item is enabled, false == disabled
//<
itemIsEnabled : function (item) {
    // normalize item to the item pointer in case a number was passed in
    item = this.getItem(item);
    return (item && item.enabled != false && item.isSeparator != true);
},

//>    @method    menu.setDynamicItems()    (A)
// This method will, for all items:
// * enable/disable them automatically based on an 'enableif' property
// * change their titles based on a 'dynamicTitle' property
// * set their icons based on a 'dynamicIcon' property
// * check/uncheck items based on a 'checkIf' property
//
// setDynamicItems() first calls +link{Menu.enableIf}, which, if set, is a function
// (possibly a string function) taking the target and this menu as arguments and
// returning a +link{ValueMap}.  Thereafter setDynamicItems() loops through the menu
// items and calls the dynamic methods MenuItem.enableIf(), checkIf(), dynamicTitle(),
// and dynamicIcon().  Those four methods take the target, this menu, and the menu item
// as function arguments, and if they are string methods then they take extra arguments
// defined by the value map returned from Menu.enableIf().  The keys of the value map
// are used as the argument names and the values are used as the corresponding variable
// bindings.
//<

setDynamicItems : function () {
    if (this.enableIf) {
        isc.Func.replaceWithMethod(this, "enableIf", "target,menu");
    }

    var changed = false,
        target = this.target,
        menu = this,
        extraVars = isc.isA.Function(this.enableIf) && this.enableIf(target, menu),
        extraVarNames = isc.isAn.Object(extraVars) && isc.getKeys(extraVars),
        numExtraArgs = extraVarNames ? extraVarNames.length : 0,
        argsString = "target,menu,item" + (extraVarNames ? "," + extraVarNames.join(",") : ""),
        args = [target, menu, null];
    if (numExtraArgs > 0) {
        // Set the other arugments that will be passed to the menu items' dynamic methods.
        args.addListAt(isc.getValues(extraVars), 3);
    }

    if (!this.data) return;

    // iterate through the items, checking for enableIf or dynamicTitle properties
    for (var i = 0, len = this.data.length; i < len; ++i) {
        var item = this.getItem(i);
        if (!item) continue;

        // The third argument is always the specific menu item
        args[2] = item;



        if (item.enableIf != null) {
            var enabled;
            if (item.enableIf === this._$true) {
                enabled = true;
            } else if (item.enableIf === this._$false) {
                enabled = false;
            } else {
                isc.Func.replaceWithMethod(item, "enableIf", argsString);
                enabled = item.enableIf.apply(item, args);
            }
            changed |= this._setItemEnabled(i, enabled);
        }

        if (item.checkIf) {
            var checked;
            if (item.checkIf === this._$true) {
                checked = true;
            } else if (item.checkIf === this._$false) {
                checked = false;
            } else {
                isc.Func.replaceWithMethod(item, "checkIf", argsString);
                checked = item.checkIf.apply(item, args);
            }
            changed |= this._setItemChecked(i, checked);
        }

        if (item.dynamicTitle) {
            isc.Func.replaceWithMethod(item, "dynamicTitle", argsString);
            changed |= this.setItemTitle(i, item.dynamicTitle.apply(item, args));
        }

        if (item.dynamicIcon) {
            isc.Func.replaceWithMethod(item, "dynamicIcon", argsString);
            changed |= this.setItemIcon(i, item.dynamicIcon.apply(item, args));
        }
    }

    if (changed && this.isDrawn()) {
        this.redraw("dynamic item change");

        if (isc.Browser.isIE) {
            this.body.setRowStyle(0);
        }
    }
},

refreshRow : function () {
    // force execution of setDynamicItems() on row refresh to pick up new title, checkIf, etc.
    if (this.autoSetDynamicItems) this.setDynamicItems();
    return this.Super("refreshRow", arguments);
},

// called when evaluating expressions like checkIf.  Calling setItemEnabled(item) with no
// "newState" argument should enable the item, however, an enableIf expression that returns
// null or undef should disable the item, rather than requiring that expression to return false
// explicitly.
_setItemEnabled : function (item, newState) {
    return this.setItemEnabled(item, !!newState);
},
_setItemChecked : function (item, newState) {
    return this.setItemChecked(item, !!newState);
},

//>    @method    menu.setItemEnabled()   ([])
// Enables or disables the menu item according to the value of newState, and redraws
// the menu if necessary. Returns true if there's a change in the enabled state.
//
// @param item (MenuItem or number) MenuItem in question, or it's index
// @param [newState] (boolean) true to enable the menu item, false to disable it.  If not
//                             passed, true is assumed
//
// @return    (boolean)    true if the enabled state was changed
// @visibility external
//<
setItemEnabled : function (item, newState) {
    if (newState == null) newState = true;

    // normalize item to the item pointer in case a number was passed in
    item = this.getItem(item);
    if (! item) return;

    // set the enable of the item
    if (item.enabled != newState) {
        item.enabled = newState;

        // mark the menu to be redrawn to show the new state
        this.markForRedraw("itemEnabled");

        // changed something -- return true so the caller knows to redraw
        return true;
    }

    // nothing changed -- return false
    return false;
},


//>    @method    menu.setItemChecked()
// Checks or unchecks the menu item according to the value of newState, and redraws
// the menu if necessary. Returns true if there's a change in the checked state.
//
// @param item (MenuItem or number) MenuItem in question, or it's index
// @param [newState] (boolean) true to check the menu item, false to uncheck it.  If not
//                             passed, true is assumed
//
// @return    (boolean)    true if the checked state was changed
//
// @visibility external
//<
setItemChecked : function (item, newState) {
    if (newState == null) newState = true;

    // normalize item to the item pointer in case a number was passed in
    item = this.getItem(item);
    if (! item) return;

    // set the enable of the item
    if (item.checked != newState) {
        item.checked = newState;

        // mark the menu to be redrawn to show the new state
        this.markForRedraw("itemChecked");

        // changed something -- return true so the caller knows to redraw
        return true;
    }
    // nothing changed -- return false
    return false;
},


//>    @method    menu.setItemTitle() ([])
// Sets the title of a particular menu item to the string specified by newTitle and
// redraws the menu if necessary.
//
// @param    item        (MenuItem or number) MenuItem in question, or it's index
// @param    newTitle    (string)    new title
//
// @return    (boolean)    true if the title was changed, and false otherwise
// @visibility external
//<
// NOTE: this is dependent on the title coming directly from the 'title' property... ???
setItemTitle : function (item, newTitle) {
    // normalize item to the item pointer in case a number was passed in
    item = this.getItem(item);
    if (! item) return;

    // set the title
    if (item.title != newTitle) {
        item.title = newTitle;

        // mark the menu to be redrawn to show the new state
        this.markForRedraw("item title change");

        // return true so the caller knows that something has changed
        return true;
    }
    // nothing changed -- return false
    return false;
},


//>    @method    menu.setItemIcon()
// Sets the icon and disabled icon (if specified) for a particular menu item and redraws
// the menu if necessary. Returns true if the icon changed.
//
// @param    item        (MenuItem or number) MenuItem in question, or it's index
// @param    newIcon                (string)    new icon URL
// @param    [newDisabledIcon]    (string)    new icon URL for disabled image
//
// @return    (boolean)    true == something changed, redraw is called for
// @visibility external
//<
setItemIcon : function (item, newIcon, newDisabledIcon) {
    // normalize item to the item pointer in case a number was passed in
    item = this.getItem(item);
    if (! item) return;

    // set the title
    if (item.icon != newIcon) {
        item.icon = newIcon;
        if (newDisabledIcon) item.disabledIcon = newDisabledIcon;

        // mark the menu to be redrawn to show the new state
        this.markForRedraw("item icon change");

        // return true so the caller knows that something has changed
        return true;
    }
    // nothing changed -- return false
    return false;
},


//>    @method    menu.getIcon()    (A)
//            get the icon for a particular item of the list
//
//        @param    item        (object)        menu item (member of this.data)
//
//        @return    (HTML)    HTML for the icon field for this item
//<
getIcon : function (item) {
    // NOTE: separators are caught before this is called
    //        to change the separator, change ListGrid.getCellValue()

    var shouldFixIconWidth = this.fixedIconWidth && this.getRecordIndex(item) == 0,
        iconSpacerWidth = shouldFixIconWidth ? this.iconWidth : null;

    var imgHTML;
    if (item.icon) {
        var icon = (this.itemIsEnabled(item) || !item.disabledIcon ? item.icon
                                                                    : item.disabledIcon);
        var iconConfig = {src:icon};
        iconConfig.width = item.iconWidth || this.iconWidth;
        iconConfig.height = item.iconHeight || this.iconHeight;

        if (isc.Browser.isStrict) {
            iconConfig.align = "absmiddle";
        }
        imgHTML = this.imgHTML(iconConfig);
        if (shouldFixIconWidth && (item.iconWidth == null || item.iconWidth >= iconSpacerWidth)) {
            shouldFixIconWidth = false;
        } else {
            iconSpacerWidth -= item.iconWidth;
        }
    }
    if (item.checked) {
        imgHTML = this.getCheckmarkImage(item);
        if (shouldFixIconWidth) {
            // we size our checkmark by directly applying a width to the image
            var checkmarkWidth = this.checkmarkImage ? this.checkmarkImage.width : this.iconWidth;
            if (checkmarkWidth < iconSpacerWidth) {
                iconSpacerWidth -= checkmarkWidth;
            } else {
                shouldFixIconWidth = false;
            }
        }
    }

    if (shouldFixIconWidth) {
        if (imgHTML) return imgHTML + this.imgHTML("[SKIN]/../blank.gif", iconSpacerWidth, 1);
        else return this.imgHTML("[SKIN]/../blank.gif", iconSpacerWidth, 1);
    }
    return imgHTML || "&nbsp;";
},


//>    @method    menu.getItemTitle()    (A)
// Get the title for this menu item
//        @group    appearance
//
//        @param    item        (object)        menu item (member of this.data)
//
//        @return    (HTML)        HTML for the keyTitle field
//<
getItemTitle : function (item) {
    var title;
    // if we're a treeMenu, use the tree to derive the item title
    if (this._treeData) {
        title = this._treeData.getTitle(item);

    // otherwise try title and other likely properties
    } else {
        title = item.title || item.name || item.id;
    }

    // ensure non-blank
    title = title || "&nbsp;";

    return title;
},


//>    @method    menu.getKeyTitle()    (A)
// Get the keyTitle for a particular item of the list
//        @group    appearance
//
//        @param    item        (object)        menu item (member of this.data)
//
//        @return    (HTML)        HTML for the keyTitle field
//<
getKeyTitle : function (item) {
    if (item.keyTitle) return item.keyTitle;
    return "&nbsp;";
},


//>    @method    menu.getCheckmarkImage()    (A)
// Return the checkmark image for a item.
//        @group    appearance
//
//        @param    item        (object)        menu item (member of this.data)
//        @return    (HTML)    HTML for the checkmark image
//<
getCheckmarkImage : function (item) {
    // cache the HTML for the image so it doesn't have to be calculated over and over
    if (!this._checkmarkImage) {
        // From experimentation the default textTop align looks bad for checkmark images
        // in strict mode
        if (isc.Browser.isStrict) {
            this.checkmarkImage.align = this.checkmarkDisabledImage.align = "absmiddle";
        }
        this._checkmarkImage = this.imgHTML(this.checkmarkImage);
        this._checkmarkDisabledImage = this.imgHTML(this.checkmarkDisabledImage);
    }
    return (this.itemIsEnabled(item) ? this._checkmarkImage : this._checkmarkDisabledImage);
},


// Keyboard Handling
// ----------------------------------------------------------------------------------------


//>    @method    menu.setUpKeyListening()    (A)
// Set up the key listening for this menu. <P>
// After this, when a key corresponding to a menu item is pressed, it's like they clicked
// on that item
//            NOTE: each item can have:
//                * no keys
//                * a single key designated by a string (key name, "A", "Space" etc)
//              * a single key designated by a keyIdentifier object
//                ({keyName:"A", shiftKey:true}, etc)
//                * an array contining strings / objects
//            the keys are stored in the item.keys variable
//        @group    event handling
//<
// Note: this could be done in a more desirable manner by making them ctrl+key handlers
// (for example)
setUpKeyListening : function () {
    var output = "";

    // iterate through the items, checking for keys property
    var item,
        keys,
        length = this.data.length;

    for(var i = 0; i < length; i++) {
        // get the menu item and it's keys
        item = this.getItem(i);
        if (!item) continue;
        keys = item.keys;

        // if no keys are defined, skip to the next item
        if (!keys) continue;

        // normalize the keys into an array
        if (!isc.isAn.Array(keys)) keys = [keys];
        // for each item in the array
        for (var key, k = 0, klen = keys.length; k < klen; k++) {
            key = keys[k];

            // if this key is null, skip this
            //    this lets us work cleanly with special menu keys, set up above
            if (key == null) continue;

            // register the event with the Page
            isc.Page.registerKey(key, "target.menuKey(" + i + ");", this);

            // keep track of the keys we registered so we can unregister on destroy()
            if (!this.registeredKeys) this.registeredKeys = [];
            this.registeredKeys.add(key);
        }
        // set up the keyTitle if it hasn't been set up already to the first key
        if (! item.keyTitle) this.setItemKeyTitle(item, keys[0]);
    }
},

destroy : function (fromDataChanged) {
    // unregister any registered keys
    if (this.registeredKeys) {
        for (var i = 0; i < this.registeredKeys.length; i++) {
            isc.Page.unregisterKey(this.registeredKeys[i], this);
        }
    }
    if (this._submenus) this._submenus.map("destroy");

    if (this._treeData){
        // if the data was autoCreated, by us, destroy it to clean up RT<->DS links
        // componentID check required to avoid issue where submenus could erroneously clean up
        // their parents' data object
        // "fromDataChanged" parameter tells us that we're destroying because the RT changed
        // and no longer has nodes for this menu. In this case we of course wouldn't
        // want to destroy the ResultTree

        var data = this._treeData;


        if (!fromDataChanged && data._autoCreated && isc.isA.Function(data.destroy) &&
            (data.componentId == this.ID))
        {
            data.destroy();
        } else {
            // ignore so we don't leak memory from the observation references
            this._ignoreData(data);
        }
        delete this._treeData;
    }

    if (this._pageResizeEvent) isc.Page.clearEvent("resize", this._pageResizeEvent);

    this.Super("destroy", arguments);
},

//>    @method    menu.menuKey()    (A)
// A key that we've registered interest in was pressed
//
//        @param    item        (item | number)        pointer to (or number of) the item in question
//
//        @return    (boolean)    false == stop handling this event
//<
menuKey : function (item) {
    // if we have an setDynamicItems function, call that to updated the enabled state
    if (this.autoSetDynamicItems) this.setDynamicItems();

    // call the 'click' handler for the menu with the item
    //    to actually do whatever should be done for the menu item
    // Note: in this case the colNum param is null
    return this.selectMenuItem(item);
},


//>    @method    menu.setItemKeyTitle()
//            set the keyTitle of an item according to the menuKey passed in
//        @param    item        (object)        menu item
//        @param    menuKey        (string)        key that invokes this item
//<
setItemKeyTitle : function (item, menuKey) {
    var keyTitle;
    if (isc.isA.String(menuKey)) keyTitle = menuKey
    else if (isc.isAn.Object(menuKey)) {
        if (menuKey.title) keyTitle = menuKey.title
        else keyTitle = menuKey.keyName;
    }
    item.keyTitle = keyTitle;
}

});



isc.Menu.addClassMethods({

//>    @classMethod    Menu.hideAllMenus()
// Hide all menus that are currently open. This method is useful to hide the current set of
// menus including submenus, and dismiss the menu's clickMask.
//
// @visibility external
//<
// @param dismissEvent (string) If passed, indicates what event caused this to occur. Options
// are "itemClick" or "outsideClick".
hideAllMenus : function (dismissEvent) {
    var fromItemClick = dismissEvent == "itemClick",
        fromOutsideClick = dismissEvent == "outsideClick";

    var hidingAllMenus = true;

    // if there are any open menus
    if (isc.Menu._openMenus.length > 0) {

        // hide each of them

        var menus = isc.Menu._openMenus,
            forceFocusOnHide = false,
            topMenu,
            focusCanvas = isc.EH.getFocusCanvas();

        isc.Menu._openMenus = [];

        for (var i = menus.length -1; i >= 0; i--) {
            var currentMenu = menus[i];

            // Note hide() doesn't remove menus from the _openMenus array
            if (!currentMenu.isVisible()) {
                continue;
            }
            if (fromItemClick && currentMenu.autoDismiss == false) {
                isc.Menu._openMenus.addAt(currentMenu, 0);
                hidingAllMenus = false;
                continue;
            }
            if (fromOutsideClick && currentMenu.autoDismissOnBlur == false) {
                isc.Menu._openMenus.addAt(currentMenu, 0);
                hidingAllMenus = false;
                continue;
            }

            if (currentMenu._isVisibilityAncestorOf(focusCanvas)) {
                if (topMenu == null) topMenu = currentMenu;
                forceFocusOnHide = true;
            }
            currentMenu.hide();
        }

        if (forceFocusOnHide && isc.isA.Canvas(topMenu.body.focusOnHide)) {
            topMenu.body.focusOnHide.focus();
        }
    }

    // Kill any submenus pending show() on data fetch returns
    isc.Menu._submenuQueue = {};
    // hide the canvas clickmask (may not always be necessary, as this method is typically called
    // from a clickMask click and the CM is autoHide true, but this allows the method to also be
    // called programmatically).
    // Note: the clickMask was shown when the first menu was opened, and the ID recorded on the
    // Menu class object
    if (hidingAllMenus) {
        if (isc.Menu._menusClickMask) {
            isc.EH.hideClickMask(isc.Menu._menusClickMask);
            isc.Menu._menusClickMask = null;
        }
    } else {

        // The clickMask is soft - if we're leaving any menus up, but have hidden
        // the click mask due to an outside click, re-show it.
        if (!isc.EH.clickMaskUp(isc.Menu._menusClickMask)) {
            this._showMenuClickMask();
        }

    }

    // If the menu(s) were triggered from a menubutton click, notify the menubutton that we're
    // in the process of hiding the menu so it doesn't re-show in response to the current click
    // Also clear this flag on mouseUp.  Only apply this fix if event target is the menu button.
    if (isc.Menu._currentMenuButton != null) {
        var button = isc.Menu._currentMenuButton;
        delete isc.Menu._currentMenuButton;
        if (isc.EH.lastEvent.eventType == isc.EH.MOUSE_DOWN &&
            isc.EH.lastEvent.target    == button)
        {

            button._hidingMenuClick = true;

            isc.Page.setEvent(isc.EH.CLICK, button,
                              isc.Page.FIRE_ONCE, "_hidingMenuClickComplete");
        }
    }

},

_getAutoDismissOnBlurMenus : function () {
    if (this._openMenus == null || this._openMenus.length == 0) return [];
    var menus = [];
    for (var i = 0; i < this._openMenus.length; i++) {
        if (this._openMenus[i].autoDismissOnBlur != false) menus.add(this._openMenus[i]);
    }
    return menus;
},

_showMenuClickMask : function () {
    // if this is the first menu being opened, show the click mask
    if (isc.Menu._getAutoDismissOnBlurMenus().length > 0 &&
        (isc.Menu._menusClickMask == null ||
            !isc.EH.clickMaskUp(isc.Menu._menusClickMask)))
    {
        isc.Menu._menusClickMask = isc.EH.showClickMask("isc.Menu.hideAllMenus('outsideClick')",
                                        true);
    }
},

//>    @classMethod    menu.menuForValueMap()
// Given a valueMap like that displayed in a ListGrid or FormItem, create a Menu that allows picking
// an item from that valueMap.
// <P>
// This is typically used to show a contextMenu of the values in the valueMap, for example:<pre>
//    var menu = Menu.menuForValueMap(valueMap);
//    menu.showContextMenu(target);
// </pre>
// When an item is selected, the target will receive a call<pre>
//            target.valueMapMenuSelected(newValue)
// </pre> where <code>newValue</code> is the selected value from the valueMap.
// <P>
// NOTE: By default, the same menu is recycled every time this is called --
//    this saves memory and time.  However, to create a unique menu for
//    your valueMap, pass "false" for the <code>reuseMenu</code> parameter.
//
//        @param    valueMap        (object | array)    ValueMap as either an object of key:value pairs or an array of strings.
//        @param    [reuseMenu]    (boolean : true)    If false is passed in, we create a new menu as the result of this call.
//                                                    Passing true (or leaving the parameter blank) re-uses the same menu,
//                                                    thus saving system resources.
//        @return                    (Menu)                Menu for this value map.
//<
menuForValueMap : function (valueMap, reuseMenu) {
    // get the list of items for the menu
    var itemList = [];

    // if the valueMap is a string, eval it
    if (isc.isA.String(valueMap)) valueMap = this.getPrototype().getGlobalReference(valueMap);

    if (isc.isAn.Array(valueMap)) {
        for (var i = 0; i < valueMap.length; i++) {
            itemList[i] = {value:valueMap[i], title:valueMap[i]};
        }
    } else {
        for (var value in valueMap) {
            itemList.add({value:value, title:valueMap[value]});
        }
    }

    var menu = isc.Menu._valueMapMenu;
    if (reuseMenu == false || isc.Menu._valueMapMenu == null) {
        menu = isc.Menu.newInstance({
            autoDraw:false,
            itemClick : function(item) {
                if (this.target.valueMapMenuSelected) {
                    this.target.valueMapMenuSelected(item.value)
                }
            }
        });
    }

    // remember the menu if we're supposed to reuse them
    if (reuseMenu != false && isc.Menu._valueMapMenu == null) isc.Menu._valueMapMenu = menu;

    // assign the menu items to the menu
    menu.setData(itemList);

    // now return the menu
    return menu;
}

});

//
//  'registerStringMethods()' - add all the instance properties that can be defined as strings
//  to evaluate (or as methods) to a central registry, together with their arguments as comma
//  separated strings.
//
isc.Menu.registerStringMethods({
    // note - JSDoc comment attached to default (no-op) implementation
    itemClick:"item"
})

// Override the ListGrid 'showHeaderContextMenu' property to show the menu by default, as we know
// that the Menu class has been defined
isc.ListGrid.addProperties({showHeaderContextMenu:true});
// Ditto with the showHeaderMenuButton property
isc.ListGrid.addProperties({showHeaderMenuButton:false});








//>    @class    MenuButton
//
//  Simple subclass of button associated with a menu widget (gets shown below the button).
// @visibility external
// @treeLocation Client Reference/Control
//<


//>    @class    IMenuButton
//
//  StretchImgButton based version of the +link{MenuButton} class.
//
// @visibility external
// @treeLocation Client Reference/Control
//<




isc._commonMenuButtonProperties = {

    //>    @attr    menuButton.title        (string : "Show Menu" : IRW)
    //            Default title for the button.
    //  @visibility external
    // @group i18nMessages
    //<

    //> @attr iMenuButton.title (string : "Show Menu" : IRW)
    // @include menuButton.title
    //<
    title:"Show Menu",

    //>    @attr    menuButton.height        (Number or String : 22 : IRW)
    //            Default height of the button.
    // @visibility external
    //<

    //> @attr iMenuButton.height (Number or String : 22 : IRW)
    // @include menuButton.height
    //<
    height:22,

    //> @attr menuButton.showMenuButtonImage (Boolean : true : IR)
    // Show menu button image (up / down arrowhead) for this menu button.
    //
    // @visibility external
    //<

    //> @attr iMenuButton.showMenuButtonImage (Boolean : true : IR)
    // @include menuButton.showMenuButtonImage
    //<
    showMenuButtonImage:true,

    // We use the standard button icon handling to write out the menu button image
    // We need both standard (down) and "up" variations because of the menu showing above
    // or below the button.

    //>    @attr    menuButton.menuButtonImage        (SCImgURL : "[SKIN]menu_button.gif" : IRA)
    // Image for menu button indicating that the button expands a menu.  This image is shown
    // for menus expanding down from the button.   Menu direction is controlled by
    // +link{MenuButton.showMenuBelow}.
    //
    // @see menuButton.menuButtonImageUp
    // @visibility external
    //<

    //> @attr iMenuButton.menuButtonImage (SCImgURL : "[SKIN]menu_button.gif" : IRA)
    // @include menuButton.menuButtonImage
    //<
    menuButtonImage:"[SKIN]menu_button.gif",

    //>    @attr    menuButton.menuButtonImageUp        (SCImgURL : "[SKIN]menu_button_up.gif" : IRA)
    // Image for menu button indicating that the button expands a menu.  This image is shown
    // for menus expanding up from the button.   Menu direction is controlled by
    // +link{MenuButton.showMenuBelow}.
    //
    // @see menuButton.menuButtonImage
    // @visibility external
    //<

    //> @attr iMenuButton.menuButtonImageUp (SCImgURL : "[SKIN]menu_button_up.gif" : IRA)
    // @include menuButton.menuButtonImageUp
    //<
    menuButtonImageUp:"[SKIN]menu_button_up.gif",

    //> @attr   menuButton.hiliteAccessKey  (Boolean : true : IR)
    // If this MenuButton has a specified +link{canvas.accessKey, accessKey}, underline it
    // in the title of the button by default
    // @visibility external
    //<

    //> @attr iMenuButton.hiliteAccessKey (Boolean : true : IR)
    // @include menuButton.hiliteAccessKey
    //<
    hiliteAccessKey:true,

    // Default menuButtonImage size is 7px
    iconWidth:7,
    iconHeight:7,

    // Show the arrow on the right of the title, aligned with the right edge of the button.
    iconOrientation:"right",
    iconAlign:"right",

    // Align the title to the left of the button (away from the icon) by default.
    align:"left",

    //>    @attr    menuButton.showMenuBelow        (Boolean : true : IRW)
    //            The menu drops down below the menu button.
    //            Set to false if the menu should appear above the menu button.
    // @visibility external
    //<

    //> @attr iMenuButton.showMenuBelow (Boolean : true : IRW)
    // @include menuButton.showMenuBelow
    //<
    showMenuBelow:true,

    //>    @attr    menuButton.alignMenuLeft        (boolean : true : IRW)
    //            If true align the menu with the left edge of the menu button. Otherwise align
    //          with the right edge of the menu button
    //<
    //> @attr iMenuButton.alignMenuLeft (boolean : true : IRW)
    // @include menuButton.alignMenuLeft
    //<

    alignMenuLeft: true,


    //>    @attr menuButton.menu (Menu : null : IRW)
    // The menu to show.
    // <P>
    // For a menu button with no menu (menu: null) the up/down arrow image can
    // be suppressed by setting
    // +link{menuButton.showMenuButtonImage, showMenuButtonImage}: <code>false</code>.
    //
    // @visibility external
    //<
    //> @attr iMenuButton.menu (Menu : null : IRW)
    // @include menuButton.menu
    //<
    menu:null,

    //>Animation
    //> @attr menuButton.menuAnimationEffect (string : null : IRWA)
    // Allows you to specify an animation effect to apply to the menu when it is being shown.
    // Valid options are "none" (no animation), "fade", "slide" and "wipe".
    // If unspecified falls through to <code>menu.showAnimationEffect</code>
    // @visibility animation
    //<

    //> @attr iMenuButton.menuAnimationEffect (string : null : IRWA)
    // @include menuButton.menuAnimationEffect
    //<

    //<Animation

    // Choose the appropriate icon depending on whether the menu will be shown above or
    // below this menubutton.
    initWidget : function (a,b,c,d) {
        if (this.showMenuButtonImage)  this._setUpIcon();
        return this.invokeSuper(isc.MenuButton, "initWidget", a,b,c,d);
    },

    //> @attr menuButton.autoDestroyMenu (Boolean : true : IRW)
    // If this menuButton is +link{canvas.destroy(),destroyed}, should it also destroy
    // its +link{menuButton.menu}?
    // @visibility external
    //<
    //> @attr iMenuButton.autoDestroyMenu (Boolean : true : IRW)
    // @include menuButton.autoDestroyMenu
    //<
    autoDestroyMenu:true,
    destroy : function (a,b,c,d,e) {
        if (this.menu != null && this.autoDestroyMenu && this.menu.destroy != null &&
            !this.menu.destroyed && !this.menu.destroying)
        {
            this.menu.destroy();
            this.menu = null;
        }
        return this.invokeSuper(isc.MenuButton, "destroy", a,b,c,d,e);
    },

    // setter for showMenuButtonImage and showMenuBelow - required since we need to update the
    // icon property and trip a redraw
    //> @method MenuButton.setShowMenuButtonImage
    // Setter for the 'showMenuButtonImage' property - shows/hides the menu button image
    // at runtime.
    //
    // @param show (boolean) Should the image be shown
    // @visibility external
    //<

    //>@method iMenuButton.setShowMenuButtonImage()
    // @include menuButton.setShowMenuButtonImage()
    //<
    setShowMenuButtonImage : function (show) {
        if (show == this.showMenuButtonImage) return;
        this.showMenuButtonImage = show;
        if (show) this._setUpIcon();
        else this.icon = null;
        if (this.isDrawn()) this.markForRedraw();
    },

    //>@method MenuButton.setShowMenuBelow
    // Setter for the 'showMenuButtonBelow' property - determines whether the menu will be
    // shown above or below the menubutton.
    // @param below (boolean) True if the menu should be shown below the menubutton.
    // @visibility external
    //<

    //>@method iMenuButton.setShowMenuBelow()
    // @include menuButton.setShowMenuBelow()
    //<
    setShowMenuBelow : function (below) {
        if (below != this.showMenuBelow) {
            this.showMenuBelow = below;
            // If we're not showing the icon we don't need to update anything until we show the icon.
            if (this.showMenuButtonImage) {
                this._setUpIcon();
                if (this.isDrawn()) this.markForRedraw();
            }
        }
    },

    // helper to convert the menubutton image to an icon
    _setUpIcon : function () {
        var icon = this.showMenuBelow ? this.menuButtonImage : this.menuButtonImageUp;

        if (isc.isAn.Object(icon)) {
            if (icon.width) this.iconWidth = icon.width;
            if (icon.height) this.iconHeight = icon.height;
            this.icon = icon.src;
        } else {
            this.icon = icon;
        }
    },


    // If this click didn't come from dismissing a menu shown by this button,
    // show our menu on click

    handleClick : function () {
        if (this.Super("handleClick", arguments) == false) return;

        if (this._hidingMenuClick) return;
        this.showMenu();
    },

    // This method is fired in response to the Page level click event following the
    // dismissing of the menu we were showing.
    // Use it to clear out this._hidingMenuClick

    _hidingMenuClickComplete : function () {
        if (this._hidingMenuClick) {
            var theMenu = this;
            isc.Page.setEvent(isc.EH.IDLE,
                              function () { theMenu._hidingMenuClick = null },
                              isc.Page.FIRE_ONCE);
        }
    },

    // override keypress to show the menu on arrow_down keypress
    keyPress : function () {
        if (isc.EventHandler.lastEvent.keyName == "Arrow_Down") {
                this.showMenu();
                return false;
        }
        return this.Super("keyPress", arguments);
    },

    // Override _updateCanFocus to redraw the button when canFocus is changed - this will
    // regenerate the titleHTML, with the appropriate focus behavior.
    _updateCanFocus : function () {
        this.Super("_updateCanFocus", arguments);
        this.markForRedraw();
    },

    _addRollOverOverrides : function (menu, menuButton) {
        if (!menu || ! menuButton) return;

        menu.rootMenuButton = menuButton;
        menu.autoDismissOnBlur = false;

        menu.mouseOver = function () {
            var root = this.rootMenuButton;
            if (root.showMenuOnRollOver) root.checkRollOverMouseOver(root, this);
            return this.Super("mouseOver", arguments);
        };

        menu.mouseOut = function () {
            var root = this.rootMenuButton;
            if (root.showMenuOnRollOver) root.checkRollOverMouseOut(root, this);
            return this.Super("mouseOut", arguments);
        };

    },

    // Add mouseOver handlers to XXX_vscroll and to XXX_vscroll_thumb
    // This is to assure that the Menu is not hidden when the Menu has rollOver enabled
    // and the mouse enters the vscroll area
    _addExtraRollOverOverrides : function (menu) {
        // If menu does not have peers, just return
        if (menu.peers == null) return;
        for (var i = 0; i < menu.peers.getLength(); i++) {
            var peer = menu.peers[i];
            if(peer.getID().endsWith("_vscroll")) {
                peer.rootMenuButton = menu.rootMenuButton;
                var peerHandleMouseOver = function (event) {
                    var root = this.rootMenuButton;
                    if (root.showMenuOnRollOver) root.checkRollOverMouseOver(root);
                };
                var peerHandleMouseOut = function (event) {
                    var root = this.rootMenuButton;
                    if (root.showMenuOnRollOver) root.checkRollOverMouseOut(root);
                };
                peer.addProperties({handleMouseOver: peerHandleMouseOver,
                                    handleMouseOut: peerHandleMouseOut});
                if(peer.peers == null) break;
                for (var j = 0; j < peer.peers.getLength(); j++) {
                    var peerPeer = peer.peers[j];
                    if(peerPeer.getID().endsWith("_vscroll_thumb")) {
                        peerPeer.rootMenuButton = peer.rootMenuButton;
                        var peerPeerHandleMouseOver = function (event) {
                            var root = this.rootMenuButton;
                            if (root.showMenuOnRollOver) root.checkRollOverMouseOver(root);
                        };
                        var peerPeerHandleMouseOut = function (event) {
                            var root = this.rootMenuButton;
                            if (root.showMenuOnRollOver) root.checkRollOverMouseOut(root);
                        };
                        peerPeer.addProperties({handleMouseOver: peerPeerHandleMouseOver,
                                                handleMouseOut: peerPeerHandleMouseOut});
                        break;
                    }
                }
                break;
            }
        }
    },

    checkRollOverMouseOver : function (root, menu) {
        if (root._hoverMenuTimer != null) {
              // if a timer's already set, clear it now
              isc.Timer.clear(root._hoverMenuTimer);
        }
        // If the menu isn't visible, hide any others and show it now
        if (isc.isA.Menu(root.menu) && !(root.menu.isVisible() && root.menu.isDrawn())) {
            isc.Menu.hideAllMenus();
            root.showMenu();
        }
    },

    checkRollOverMouseOut : function (root, menu) {
        if (root._hoverMenuTimer != null) {
            // clear old timer if set
            root._hoverMenuTimer = null;
        }
        // If the menu's visible, start a timer to hide it
        if (isc.isA.Menu(root.menu) && root.menu.isVisible() && root.menu.isDrawn()) {
            // hide the menu on a short timer so we can move among menus and suchlike
            root._hoverMenuTimer = isc.Timer.setTimeout(function() {
                // Make sure the menu's still showing before hiding it
                if (isc.isA.Menu(root.menu) && root.menu.isVisible() && root.menu.isDrawn()) {
                    isc.Menu.hideAllMenus();
                    root._hoverMenuTimer = null;
                }
            }, root.rollOverMenuHideDelay);
        }
    },

    //> @method MenuButton.showMenu()
    // Programmaticly forces this MenuButton to show it's menu.
    // @visibility external
    //<
    showMenu : function () {

        // lazily create the menu if necessary, so we can init with, or set menu to, an object
        // properties block
        if (isc.isA.String(this.menu)) this.menu = window[this.menu];
        if (!isc.isA.Menu(this.menu)) this._createMenu(this.menu);
        if (!isc.isA.Menu(this.menu)) return;


        isc.Menu._currentMenuButton = this;

        var menu = this.menu;


        if (this.showMenuOnRollOver) {
            var _this = this;
            this._addRollOverOverrides(menu, this);
            menu.getSubmenu = function (item) {
                // override getSubmenu() so we can add mouse-handlers
                var submenu = this.Super("getSubmenu", arguments);
                if (submenu) _this._addRollOverOverrides(submenu, _this);
                return submenu;
            };
        }

        // draw offscreen so that we can figure out what size the menu is
        // Note that we use _showOffscreen which handles figuring out the size, and
        // applying scrollbars if necessary.
        menu._showOffscreen();
        this.positionMenu(menu);
        // Add event handlers to vscroll and vscroll_thumb if scroll bar exists
        if (this.showMenuOnRollOver) this._addExtraRollOverOverrides(menu);
        menu.show(this.menuAnimationEffect);
    },

    positionMenu : function (menu) {
        if (!menu) return;
        // figure out the left coordinate of the drop-down menu
        var left = this.getPageLeft();

        if (!this.alignMenuLeft) {
            left = left - (menu.getVisibleWidth() - this.getVisibleWidth());
        }

        if (this.isRTL()) {
            // align the right-edge of the menu to the right edge of the menuButton
            left -= (menu.getVisibleWidth() - this.getVisibleWidth());
        }

        var top = this.showMenuBelow ? this.getPageTop()+this.getVisibleHeight()+1 :
                                       this.getPageTop()-menu.getVisibleHeight()+2;
        // don't allow the menu to show up off-screen
        menu.placeNear(left, top);
    },

    repositionMenu : function () {
        var menu = this.menu;
        if (menu && isc.isA.Menu(menu) && menu.isDrawn() && menu.isVisible()) {
            this.positionMenu(menu);
        }
    },

    _createMenu : function (menu) {
        if (!menu) return;
        menu.autoDraw = false;

        if (this.showMenuOnRollOver) {
            var _this = this;
            menu.getSubmenu = function (item) {
                var submenu = this.Super("getSubmenu", arguments);
                _this._addRollOverOverrides(submenu, _this);
                return submenu;
            };
        }

        var cons = this.menuConstructor || isc.Menu;
        this.menu = isc.ClassFactory.newInstance(cons, menu);
    },

    handleMoved : function (deltaX, deltaY) {
        // move the menu with the MenuButton
        this.Super("handleMoved", arguments);
        this.repositionMenu();
    },

    handleParentMoved : function (parent, deltaX, deltaY) {
        // move the menu with the MenuButton
        this.Super("handleParentMoved", arguments);
        this.repositionMenu();
    },

    // RollOver menus

    //> @attr menuButton.showMenuOnRollOver (Boolean : false : IRA)
    // When set to true, show the menu automatically when the mouse moves over the button.
    // @group appearance
    // @visibility external
    //<

    //> @attr menuButton.rollOverMenuHideDelay (Number : 200 : IRA)
    // When +link{menuButton.showMenuOnRollOver, showMenuOnRollOver} is true, this is the delay
    // in milliseconds before the menu is automatically hidden following mouseOut.
    // @group appearance
    // @visibility external
    //<
    rollOverMenuHideDelay: 250,

    mouseMove : function () {
        if (this.showMenuOnRollOver) this.mouseOver();
    },

    mouseOver : function() {
        if (this.showMenuOnRollOver) this.checkRollOverMouseOver(this, this);
        return this.Super("mouseOver", arguments);
    },

    mouseOut : function() {
        if (this.showMenuOnRollOver) this.checkRollOverMouseOut(this, this);
        return this.Super("mouseOut", arguments);
    }

};



// define us as a subclass of the Button
isc.ClassFactory.defineClass("MenuButton", "Button");

isc.MenuButton.addProperties(isc._commonMenuButtonProperties);

isc.MenuButton.addProperties({
    //>    @attr menuButton.skinImgDir (URL : "images/Menu/" : IRA)
    // Where do 'skin' images (those provided with the class) live?
    // This is local to the Page.skinDir
    // @group appearance, images
    //<
    skinImgDir:"images/Menu/",

    //>    @attr    menuButton.baseStyle        (CSSStyleName : "menuButton" : IRW)
    //            CSS baseStyle for the button
    //<
    baseStyle:"menuButton"

});


// IMenuButton is a subclass of IButton.
//
isc.ClassFactory.defineClass("IMenuButton", "StretchImgButton");

isc.IMenuButton.addProperties(isc._commonMenuButtonProperties);

isc.IMenuButton.addProperties({
    //>    @attr    iMenuButton.labelSkinImgDir        (URL : "images/Menu/" : IRA)
    // skinImgDir to apply to the title label for this button only.
    // This is typically where the icon media for the menu will live.
    // This is local to the Page.skinDir
    // @group    appearance, images
    //<
    labelSkinImgDir:"images/Menu/"

});


//> @class ToolStripMenuButton
// Simple subclass of IMenuButton with appearance appropriate for a ToolStrip menu button.
// Can be used to create an icon-only menu button, and icon with text, or a text only button by setting the
// icon and title attibutes as required.
// @treeLocation Client Reference/Layout/ToolStrip
// @visibility external
//<
isc.defineClass("ToolStripMenuButton", "IMenuButton").addProperties({
   showTitle:true,
   showRollOver:true,
   showDown:true,
   labelVPad:0,
   labelHPad:7,
   autoFit:true,
   src:"[SKIN]/ToolStrip/button/button.png",
   capSize:3,
   height:22
});






// Button which will create a hierarchical menu structure based on its own
// data or dataSource property.
// Values can be selected in the menu and will be displayed in the button's title.
// Path to the selected value will also be hilited in the menu.


// SelectionTreeMenu - simple subclass of menu created by the TreeMenuButton.
// Shows the selected node's path in a custom style.
isc.ClassFactory.defineClass("SelectionTreeMenu", "Menu")

isc.SelectionTreeMenu.addMethods({

    // On click fire the click handler on our button.
    itemClick : function (item) {  this.inheritedProperties.button._itemSelected(item); },

    // Show a custom base / over style for the selected record's path

    getBaseStyle : function (record, rowNum, colNum) {
        var button = this.inheritedProperties.button;
        if (button._inSelectionPath(record)) return button.selectedBaseStyle;
        return this.Super("getBaseStyle", arguments);
    },

    // Override show() to ensure we show the updated selection path by resetting our
    // rows' styles
    show : function () {
        if (this.body) {
            for (var i = 0; i < this.getTotalRows(); i++) {
                this.body.setRowStyle(i);
            }
        }
        return this.Super("show", arguments);
    },

    getItemTitle : function (item, a,b,c,d) {
        // Specifying a display field on the button ensures it is used to display items' titles
        var button = this.inheritedProperties.button;
        if (button.displayField) {
            return item[button.displayField];
        }
        // Otherwise pick up the title field from the data-source as normal
        return this.invokeSuper(isc.SelectionTreeMenu, "getItemTitle", item, a,b,c,d);
    }
});


//> @class TreeMenuButton
//
//  Button used to display a hierarchical Menu group for representing / selecting tree data.
// <P>
// <i><b>Important Note:</b> this class should not be used directly - it is exposed purely for
// +link{group:i18nMessages, i18n reasons.}</i>
//
// @inheritsFrom MenuButton
// @treeLocation Client Reference/Control
// @visibility external
//<

// Used by the TreeMenuItem class.
// Supports selection:
// - Displays the selected value as the button's title by default.
// - Hilites the path to the selected record in its menu

isc.ClassFactory.defineClass("TreeMenuButton", "MenuButton");

//> @class ITreeMenuButton
//
//  Button used to display a hierarchical Menu group for representing / selecting tree data.
//  This is derived from the +link{class:IMenuButton} and is +link{class:StretchImgButton} based.
// <P>
// <i><b>Important Note:</b> this class should not be used directly - it is exposed purely for
// +link{group:i18nMessages, i18n reasons.}</i>
//
// @treeLocation Client Reference/Control
// @visibility external
//<

isc.ClassFactory.defineClass("ITreeMenuButton", "IMenuButton");

isc._treeMenuButtonProps = {

    //> @attr treeMenuButton.title (string : null : IRW)
    // Title for this button. If not specified, the selected value from the tree will
    // be displayed instead.
    // @visibility external
    //<
    title:null,

    //> @attr treeMenuButton.unselectedTitle (string : "Choose a value" : IRW)
    // If +link{treeMenuButton.title, title} is null, this value will be displayed as a title
    // when the user has not selected any value from the hierarchical menu.
    // @visibility external
    // @group i18nMessages
    //<
    unselectedTitle:"Choose a value",

    //> @attr treeMenuButton.emptyMenuMessage (string : null : IRW)
    // If this button's menu (or any of its submenus) are empty, this property can be used
    // to specify the message to display (as a disabled item) in the empty menu.
    // @visibility external
    //<
    //emptyMenuMessage : null,

    //> @attr treeMenuButton.showPath (boolean : false : IRW)
    // If +link{treeMenuButton.title, title} is null, when the user selects an item, should we
    // show the full path to the item, or just the item's title as the button's title?
    // @visibility external
    //<
    showPath:false,

    //> @attr treeMenuButton.pathSeparatorString (HTML : "&nbsp;&gt;&nbsp;" : IRW)
    // If +link{treeMenuButton.showPath, showPath} is true, this property specifies what will
    // appear between the folders in the selected value's path.
    // @visibility external
    //<
    pathSeparatorString : "&nbsp;&gt;&nbsp;",

    //> @attr treeMenuButton.selectedBaseStyle (CSSStyleName: "treeMenuSelected" : IRW)
    // Base style to apply to the selected path within the menu. (The "over" version of this
    // style should also be defined in the stylesheet applied to this widget).
    // @visibility external
    //<
    selectedBaseStyle : "treeMenuSelected",

    // The title is going to keep changing width, so allow overflow.
    overflow:isc.Canvas.VISIBLE,

    // Override the menuConstructor so our menu is created as a SelectionTreeMenu
    menuConstructor : isc.SelectionTreeMenu,

    //> @attr treeMenuButton.loadDataOnDemand (boolean : null : IRW)
    // If this button is showing a databound treeMenu, this attribute dictates whether the data
    // should the data be loaded on demand or upfront.  The default is to load on demand.
    //<
    //loadDataOnDemand : true,

    //> @attr treeMenuButton.dataProperties (Tree : null : IR)
    // For a <code>TreeMenuButton</code> that uses a DataSource, these properties will be passed to
    // the automatically-created ResultTree.  This can be used for various customizations such as
    // modifying the automatically-chosen +link{tree.parentIdField}.
    // @group databinding
    // @visibility external
    //<

    // Optional class-level defaults for the menu
    //menuDefaults : {}


    // METHODS:

    // The title of the button should reflect the selected value (if there is one)
    getTitle : function () {
        // Allow the developer to specify an explicit (static) title.
        if (this.title) return this.title;

        var selection = this.getSelectedItem();
        if (selection) {

            if (!this.showPath) {
                if (!isc.isA.Menu(this.menu)) this._createMenu(this.menu);
                return this.menu.getItemTitle(selection);
            } else {
                // calling getTree automatically creates this.menu
                var tree = this.getTree();
                var parents = tree.getParents(selection),
                    titleArray = [];

                for (var i = parents.length-1; i >=0; i--) {
                    if (!tree.showRoot && i == parents.length -1) continue;
                    titleArray.add(this.menu.getItemTitle(parents[i]));
                }
                titleArray.add(this.menu.getItemTitle(selection));
                return titleArray.join(this.pathSeparatorString);
            }
        } else {
            return this.unselectedTitle;
        }
    },

    // Override the method to actually create the menu (called lazily when the menu is to be
    // shown for the first time).
    _createMenu : function (properties) {
        properties = isc.addProperties(this.menuDefaults || {}, properties, {

            // All the submenus should have a pointer back to this item.
            inheritedProperties : {
                button:this
            },
            submenuConstructor:this.menuConstructor,

            // Don't want to have to set this property at the menu level.
            canSelectParentItems : this.canSelectParentItems,

            dataSource:this.dataSource,
            // If criteria are specified at the button level, pass these through to
            // the actual Menu
            criteria:this.criteria,

            data:this.data,

            // EDD
            dataProperties:this.dataProperties

        });

        // If we have one, apply a custom messge for empty submenus to the menu
        if (this.emptyMenuMessage) properties.emptyMessage = this.emptyMenuMessage;
        if (this.loadDataOnDemand != null) properties.loadDataOnDemand = this.loadDataOnDemand;

        var returnVal = this.Super("_createMenu", [properties]);

        this.observe(this.menu, "treeDataLoaded", "observer._treeDataLoaded()");

        return returnVal;

    },

    // _treeDataLoaded. If our menu gets its data from a ResultTree, and loadDataOnDemand is
    // false (so all the data is loaded upfront), this method will be fired on initial load.
    _treeDataLoaded : function () {
        //!DONTCOMBINE
        if (this.treeDataLoaded) this.treeDataLoaded();
    },

    // getTree method to retrieve a pointer to our tree data object
    getTree : function () {

        if (!isc.isA.Menu(this.menu)) this._createMenu(this.menu);
        // Use this.menu.treeData - this avoids us having to create our own ResultTree if
        // we are databound.
        return this.menu._treeData;

    },

    // Helper to update the menu data at runtime
    setData : function (data) {
        this.data = data;
        if (this.menu != null) {

            if (!isc.isA.Menu(this.menu)) this._createMenu(this.menu);
            this.menu.setData(data);
        }
    },

    // On selection we want to:
    // - fire any developer specified handler
    // - change our title to display the selected item
    // - hilite the selection item path when the menu is visible
    _itemSelected : function (item) {
        //!DONTCOMBINE
        // If the developer's change handler returns false, cancel the selection.
        if (this.itemSelected && this.itemSelected(item, this._selectedItem) == false)
            return;

        this.setSelectedItem(item);
    },

    setSelectedItem : function (item) {
        // We don't need a full selection object - simply hang onto the last clicked node.
        this._selectedItem = item;
        // setTitle will dynamically recalc the title and update
        // (Note that redraw() will not, if we're showing a label. Noted in StatefulCanvas).
        this.setTitle();

    },

    // getSelectedItem() returns the selected item
    getSelectedItem : function () {
        return this._selectedItem;
    },

    // Is some node an ancestor of the current selection?
    // Used for hiliting the current selection in our menus.
    _inSelectionPath : function (node) {
        var selection = this.getSelectedItem(),
            tree = this.getTree();

        while (selection) {
            if (node == selection) return true;
            selection = tree.getParent(selection);
        }

        return false;
    }

};

isc.TreeMenuButton.addProperties(isc._treeMenuButtonProps);
isc.ITreeMenuButton.addProperties(isc._treeMenuButtonProps);


isc.TreeMenuButton.registerStringMethods({
    // itemSelected - handler fired when the user changes the selection.
    itemSelected : "item, oldItem"
});

isc.ITreeMenuButton.registerStringMethods({
    // itemSelected - handler fired when the user changes the selection.
    itemSelected : "item, oldItem"
});







//>    @class    TileLayout
// Lays out a series of components, calls "tiles", in a grid with multiple tiles per row.
//
// @treeLocation Client Reference/Grids
// @visibility external
//<
isc.ClassFactory.defineClass("TileLayout", "Canvas");

isc.TileLayout.addProperties({

//> @attr tileLayout.tiles (Array of Canvas : null : IR)
// List of tiles to lay out.
//
// @visibility external
//<

//> @attr tileLayout.layoutPolicy (TileLayoutPolicy : "fit" : IR)
// Policy for laying out tiles.  See +link{type:TileLayoutPolicy} for options.
//
// @group layoutPolicy
// @visibility external
//<
layoutPolicy: "fit",

//> @attr tileLayout.tileSize (int : 50 : IR)
// Size of each tile in pixels.  Depending on the +link{layoutPolicy}, <code>tileSize</code>
// may be taken as a maximum, minimum or exact size of tiles, or may be irrelevant.
// <P>
// Width and height may be separately set via +link{tileHeight} and +link{tileWidth}.
//
// @group sizing
// @visibility external
//<
tileSize: 50,

//> @attr tileLayout.tileWidth (int : null : IR)
// Width of each tile in pixels.  See +link{tileSize}.
// If +link{layoutPolicy} is "fit", +link{expandMargins} is false, +link{tilesPerLine} is set,
// +link{orientation} is "horizontal",
// and tileWidth is not set, tileWidth will be computed automatically based on +link{tilesPerLine}.
//
// @group sizing
// @visibility external
//<

//> @attr tileLayout.tileHeight (int : null : IR)
// Height of each tile in pixels.  See +link{tileSize}.
// If +link{layoutPolicy} is "fit", +link{expandMargins} is false, +link{tilesPerLine} is set,
// +link{orientation} is "vertical",
// and tileHeight is not set, tileHeight will be computed automatically based on +link{tilesPerLine}.
//
// @group sizing
// @visibility external
//<

//> @attr tileLayout.tileMargin (int : 10 : IR)
// Margin in between tiles.  Can be set on a per-axis basis with +link{tileHMargin} and
// +link{tileVMargin}.
//
// @group layoutMargin
// @visibility external
//<
tileMargin: 10,

//> @attr tileLayout.tileHMargin (int : null : IR)
// Horizontal margin in between tiles.  See +link{tileMargin}.
//
// @group layoutMargin
// @visibility external
//<

//> @attr tileLayout.tileVMargin (int : null : IR)
// Vertical margin in between tiles.  See +link{tileMargin}.
//
// @group layoutMargin
// @visibility external
//<

//> @attr tileLayout.layoutMargin (integer : 5 : IR)
// A margin left around the outside of all tiles.
//
// @group layoutMargin
// @visibility external
//<
layoutMargin:5,

//> @attr tileLayout.paddingAsLayoutMargin (Boolean : true : IRWA)
// @include layout.paddingAsLayoutMargin
//
// @group layoutMargin
// @visibility external
//<
paddingAsLayoutMargin:true,

//> @attr tileLayout.animateTileChange (Boolean : true : IRWA)
// If set, tiles animate to their new positions when a tile is added, removed, or reordered via
// drag and drop.
//
// @group appearance
// @visibility external
//<
animateTileChange: true,

//> @attr tileLayout.orientation (Orientation : "horizontal" : IR)
// Direction of tiling.  See also +link{type:TileLayoutPolicy}.
//
// @group layoutPolicy
// @visibility external
//<
orientation : "horizontal",

//> @attr tileLayout.tilesPerLine (int : null : IRW)
// Number of tiles to show in each line.  Auto-derived from +link{tileSize} for some layout
// modes.  See +link{type:TileLayoutPolicy}.
// This can also affect +link{tileWidth} or +link{tileHeight}. See those properties for details.
//
// @group layoutPolicy
// @visibility external
//<

//> @attr tileLayout.overflow   (Overflow : "auto" : IRW)
// Normal +link{type:Overflow} settings can be used on TileLayouts, for example, an
// overflow:auto TileLayout will scroll if members exceed its specified size, whereas an
// overflow:visible TileLayout will grow to accommodate members.
//
// @group sizing
// @visibility external
//<
overflow: "auto",

// canFocus must be true or else a tileLayout with no scrollbars will be unable to
// respond to keyboard events
canFocus: true,

//> @attr tileLayout.expandMargins (Boolean : true : IR)
// With +link{layoutPolicy}:"fit", should margins be expanded so that tiles fill the
// available space in the TileLayout on the breadth axis?
// This can also affect +link{tileWidth} or +link{tileHeight}. See those properties for details.
//
// @group layoutMargin
// @visibility external
//<
expandMargins: true,

//> @attr tileLayout.autoWrapLines (Boolean : true : IR)
// When +link{layoutPolicy} is "flow", should we automatically start a new line when there's
// not enough room to fit the next tile on the same line?
// <P>
// If set to false, a new line will only be started if a tile specifies tile.startLine or
// tile.endLine.
//
// @group layoutPolicy
// @visibility external
//<
autoWrapLines: true,

//> @type TileLayoutPolicy
// Policy for laying out tiles.
// <P>
// Because a TileLayout can be either horizontally or vertically oriented, the general term
// "line" is used to mean either a row or column of tiles.
// <P>
// <b>NOTE</b>: for typical form layouts (a variety of input fields and buttons in a
// tabular layout with col-spanning and row-spanning support), use a +link{DynamicForm} and
// see the +link{group:formLayout} topic.
//
// @value "fit"
// Each line has the same number of tiles, based on +link{tileLayout.tilesPerLine} if set, otherwise,
// based on fitting as many tiles per line as possible consistent with
// +link{tileLayout.tileSize,tileSize} and +link{tileLayout.tileMargin}.
// <P>
// Tiles that do not specify a size will be sized to fill available space.
//
// @value "flow"
// Tiles are laid out with varying numbers of tiles per line according to each tile's size,
// the +link{tileLayout.tileMargin,tileMargin}, and the available space.
// <P>
// Tiles are never resized by the TileLayout and +link{tileLayout.tileSize} is ignored.
//
// @visibility external
//<

//> @attr tileLayout.dragLine (AutoChild Canvas : null : R)
// Canvas used to display a drop indicator when a another canvas is dragged over this widget.
//
// @visibility external
//<
dragLineDefaults: {
    overflow: "hidden",
    styleName: "layoutDropLine"

},

initWidget : function () {
    this._enforceLegalLayoutPolicy();
    this.invokeSuper(isc.TileLayout, "initWidget");
    if (!this.tiles) this.tiles = [];
    // this makes d & d code work with tileLayout and tileGrid equally well
    this.data = this.tiles;


},

draw : function (a, b, c, d) {
    this.invokeSuper(isc.TileLayout, "draw", a, b, c, d);
    // set tile width or height if it unset based on tilesPerLine
    this._setTileSize();
    this.logDebug('calling layoutTiles from draw', "TileLayout");
    this.layoutTiles();

},

layoutChildren : function () {
    this.Super("layoutChildren", arguments);
    this.logDebug('calling layoutTiles from layoutChildren', "TileLayout");


    this.layoutTiles();
},

childResized : function (child, deltaX, deltaY, reason) {
    this.invokeSuper(isc.TileLayout, "childResized", child, deltaX, deltaY, reason);
    // need to use a timeout here otherwise the child doesn't get resized
    this.logDebug('calling layoutTiles from childResized', "TileLayout");
    this.delayCall("layoutTiles", null, 100);
},

// make sure that a legal layoutPolicy is set or code breakage will ensue
_enforceLegalLayoutPolicy : function () {
    if (this.layoutPolicy != "fit" && this.layoutPolicy != "flow") {
        this.layoutPolicy = "fit";
    }
},


willScroll : function () {
    var isHoriz = this.orientation == "horizontal";
    var topMargin = this._topMargin;
    var bottomMargin = this._bottomMargin;
    var leftMargin = this._leftMargin;
    var rightMargin = this._rightMargin;
    // make sure that only user-visible tiles are iterated
    var numTiles, userVisibleTiles;
    if (this._enableUserHiding) {
        userVisibleTiles = this.getUserVisibleTiles();
        numTiles = userVisibleTiles.getLength();
    } else {
        numTiles = this.getLength();
    }
    if (this.layoutPolicy == "flow") {
        if (this.overflow != "auto" && this.overflow != "scroll") return false;

        var availBreadth =  isHoriz ? this.getInnerWidth() - rightMargin : this.getInnerHeight() - bottomMargin;
        var availLength = isHoriz ? this.getInnerHeight() - bottomMargin : this.getInnerWidth() - leftMargin;

        var currBreadthPos = isHoriz ? leftMargin : topMargin;
        var currLengthPos = isHoriz ? topMargin : leftMargin;
        var currMaxLength = 0;
        // keep track of rows for debugging purposes mainly
        var numRows = 0;
        var tileBreadthMargin = isHoriz ? this.getTileHMargin() : this.getTileVMargin();
        var tileLengthMargin = isHoriz ? this.getTileVMargin() : this.getTileHMargin();
        for (var i = 0; i < numTiles; i++) {
            var newTile = userVisibleTiles ? userVisibleTiles[i] : this.getTile(i);
             // check for startLine flag
            if (!this.autoWrapLines && newTile.startLine) currBreadthPos = 0;
            var tileBreadth = isHoriz ? newTile.getVisibleWidth() : newTile.getVisibleHeight();

            // check for end of line
            if ((this.autoWrapLines && currBreadthPos + tileBreadth > availBreadth)
                    || (!this.autoWrapLines && newTile.endLine)) {
                // reset breadth position
                currBreadthPos = isHoriz ? leftMargin : topMargin;;
                // currMaxLength + lengthMargin gives us length position of next line
                currLengthPos += currMaxLength + tileLengthMargin;
                currMaxLength = 0;
                numRows++;
            }
            var tileLength = isHoriz ? newTile.getVisibleHeight() : newTile.getVisibleWidth();
            // if the children are already passed our length, we will scroll, so return true
            if (currLengthPos + tileLength > availLength) return true;
            // keep track of max length
            if (currMaxLength < tileLength) {
                currMaxLength = tileLength;
            }
            // move to the next breadth position
            currBreadthPos += tileBreadth + tileBreadthMargin;

        }
        // no scrolling, so return false
        return false;
    } else {
        // scrolling will happen if...
        // this.length / (tileLength + tileMargin) * tiles per line > data length
        var availLength = isHoriz ? this.getVisibleHeight() - (topMargin + bottomMargin)
                                    : this.getVisibleWidth() - (rightMargin + leftMargin);
        var tileLength = isHoriz ? this.getTileHeight() : this.getTileWidth();
        var tileLengthMargin = isHoriz ? this.getTileVMargin() : this.getTileHMargin();
        var tpl = this.getTilesPerLine();
        var maxLinesTillScroll = Math.floor(availLength / (tileLength + tileLengthMargin));
        var maxTilesTillScroll = tpl * maxLinesTillScroll;
        return (numTiles > maxTilesTillScroll);
    }
},

layoutTiles : function (mode) {

    if (!this.isDrawn()) return;

    // keep track of visible tiles for animation purposes.
    // Tiles are added here in processTile(). Used in _animateChange()
    this._visibleTiles = [];
    // keep track of # of tiles processed so extra ones can be hidden
    this._numTilesProcessed = 0;
    // cache our dimensions to determine if a tile is visible in processTile()
    this._viewportRect = [this.getScrollLeft(), this.getScrollTop(), this.getInnerWidth(), this.getInnerHeight()];
    // do margin calculations first so they can be used for both flow and fit layouts
    var tPad, bPad, lPad, rPad;
    if (this.paddingAsLayoutMargin) {
        var padding = this._calculatePadding();
        lPad = padding.left; rPad = padding.right;
        tPad = padding.top; bPad = padding.bottom;
    }
    // cache margin calculations
    var topMargin = this._topMargin = this._firstNonNull(this.layoutMargin, tPad, 0);
    var bottomMargin = this._bottomMargin = this._firstNonNull(this.layoutMargin, bPad, 0);
    var leftMargin = this._leftMargin = this._firstNonNull(this.layoutMargin, lPad, 0);
    var rightMargin = this._rightMargin = this._firstNonNull(this.layoutMargin, rPad, 0);
    // make sure that only user-visible tiles are iterated
    // NOTE could have explored skipping hidden tiles in the layout loops, but culling visible
    // tiles beforehand seemed cleaner/easier.
    var numTiles, userVisibleTiles;
    if (this._enableUserHiding) {
        userVisibleTiles = this.getUserVisibleTiles();
        numTiles = userVisibleTiles.getLength();
    } else {
        numTiles = this.getLength();
    }
    // flow layout
    if (this.layoutPolicy == "flow") {
        this.logDebug("starting flow layout", "TileLayout");
        var isHoriz = this.orientation == "horizontal";
        // only subtract right or bottom margin from available breadth, as top/left margin is already
        // accounted for when setting current breadth/length position
        var availBreadth =  isHoriz ? this.getInnerWidth() - rightMargin :
                                    this.getInnerHeight() - bottomMargin;
        // if scrolling will occur, take the scrollbar into account
        if (this.willScroll()) availBreadth -= this.getScrollbarSize();

        var currBreadthPos = isHoriz ? leftMargin : topMargin;
        var currLengthPos = isHoriz ? topMargin : leftMargin;
        var currMaxLength = 0;
        // keep track of rows for debugging purposes mainly
        var numRows = 0;
        var tileBreadthMargin = isHoriz ? this.getTileHMargin() : this.getTileVMargin();
        var tileLengthMargin = isHoriz ? this.getTileVMargin() : this.getTileHMargin();

        for (var i = 0; i < numTiles; i++) {
            var newTile = userVisibleTiles ? userVisibleTiles[i] : this.getTile(i);
             // check for startLine flag
            if (!this.autoWrapLines && newTile.startLine)  currBreadthPos = 0;
            var tileBreadth = isHoriz ? newTile.getVisibleWidth() : newTile.getVisibleHeight();
            // check for end of line
            if ((this.autoWrapLines && currBreadthPos + tileBreadth > availBreadth)
                || (!this.autoWrapLines && newTile.endLine)) {
                // reset breadth position
                currBreadthPos = isHoriz ? leftMargin : topMargin;;
                // currMaxLength + lengthMargin gives us length position of next line
                // When the first tile is larger than available breadth, don't add a margin
                // to currLengthPos; we just want it to remain the same in this case, which is 0.
                currLengthPos += currMaxLength + (i == 0 ? 0 : tileLengthMargin);
                currMaxLength = 0;
                numRows++;
            }
            // horizontal : top = length position, left = breadth position
            // vertical : top = breadth position, left = length position
            var nTop = isHoriz ? currLengthPos : currBreadthPos;
            var nLeft = isHoriz ? currBreadthPos : currLengthPos;
            var tileToUse = userVisibleTiles ? userVisibleTiles[i] : i;
            this.processTile(tileToUse, nTop, nLeft);
            // keep track of max length
            var tileLength = isHoriz ? newTile.getVisibleHeight() : newTile.getVisibleWidth();
            if (currMaxLength < tileLength) {
                currMaxLength = tileLength;
            }
            // move to the next breadth position
            currBreadthPos += tileBreadth + tileBreadthMargin;

        }
    // fit layout
    } else {
        this.logDebug("starting fit layout:" + this._animating, "TileLayout");
        var tPerLine = this.getTilesPerLine();
        var tHeight = this.getTileHeight();
        var tWidth = this.getTileWidth();
        var isHoriz = this.orientation == "horizontal";
        var tHMargin, tVMargin, extraPixels;

        var totalTiles, tileNum, numLines, spacerCanvas, startLine, endLine;
        if (this.shouldUseIncrRendering()) {
            this.logDebug("fit layout, using incremental rendering", "TileLayout");
            var tileRange = this.getVisibleTiles();
            // store visible tile range for subclasses that may want to use it (TileGrid, etc.)
            // these values will be returned by getDrawnStartIndex() getDrawnEndIndex(), which subclasses
            // should use to access these values
            this._lastVisibleTiles = tileRange;
            tileNum = tileRange[0];
            totalTiles = numTiles;
            var lineRange = this.getVisibleLines();
            startLine = lineRange[0];
            endLine = lineRange[0] + lineRange[1];
            // make sure all visible tiles are present. If not, return.
            // Relevent for databound subclasses (TileGrid etc)
            if (!this.hasAllVisibleTiles(tileRange, true)) return;
        } else {
            this.logDebug("fit layout, rendering all tiles", "TileLayout");
            totalTiles = numTiles;
            tileNum = 0;
            startLine = 0;
            endLine = Math.ceil(totalTiles / tPerLine);
            tHMargin = this.getTileHMargin();
            tVMargin = this.getTileVMargin();
        }

        // set up the spacer canvas for all rendering options so that the layoutMargin can
        // always be created and show up on the bottom.
        var totNumLines = Math.ceil(numTiles / tPerLine);
        var tVMargin = this.getTileVMargin();
        var tHMargin = this.getTileHMargin();
        var tileLength = isHoriz ? tHeight : tWidth;
        var lengthMargin = isHoriz ? tVMargin : tHMargin;
        // total lines length is tile length + margin length times # of lines
        // subtract 1 extra tilemargin length from the end, and add either top + bottom margin
        // (for horizontal orientation) or left + right margin (for vertical)
        var totLen = ((tileLength + lengthMargin) * totNumLines) - lengthMargin
            + (isHoriz ? topMargin + bottomMargin : leftMargin + rightMargin);
        spacerCanvas = this._getSpacerCanvas();
        isHoriz ? spacerCanvas.setHeight(totLen) : spacerCanvas.setWidth(totLen);
        spacerCanvas.sendToBack();

        // get the extra pixels before begining actual tile layout
        extraPixels = this.getExtraMarginPixels(tPerLine, tHeight, tWidth, tHMargin, tVMargin);

        for (var i = startLine; i < endLine; i++) {
            // keep track of extra pixels to divide among tiles
            var exPixels = extraPixels;
            for (var j = 0; j < tPerLine; j++) {
                // for horizontal, top will remain constant and left will vary.
                // for vertical, top will vary and left will remain constant.
                var topPos = isHoriz ? i : j;
                var leftPos = isHoriz ? j : i;

                // top = (current top position * tile height) + (current top position * tile vertical margin)
                //      + top layout margin
                var nTop = (topPos * tHeight) + (topPos * tVMargin ) + topMargin;
                // if we're vertical and the current vertical position is less than the number of
                // extra pixels, use our vertical position as the number of extra pixels to add to
                // the current top coordinate to get the proper placement
                if (!isHoriz && j + 1 <= extraPixels) nTop += j + 1;
                // left = same logic as top, but substitute left
                var nLeft = (leftPos * tWidth) + (leftPos * tHMargin) + leftMargin;
                // if we're horizontal and the current horizontal position is less than the number of
                // extra pixels, use our horizontal position as the number of extra pixels to add to
                // the current left coordinate to get the proper placement
                if (isHoriz && j + 1 <= extraPixels) nLeft += j + 1;
                var tileToUse = userVisibleTiles ? userVisibleTiles[tileNum] : tileNum

                var newTile = this.processTile(tileToUse, nTop, nLeft,
                        this.getTileHeight(), this.getTileWidth());

                tileNum++;
                // check for the case where more images are returned than will fit
                if (tileNum >= totalTiles) break;
            }
            if (tileNum >= totalTiles) break;
        }
    }

},

// This function is more for the sake of databound superclasses (TileGrid).
// For tilelayout just return true.
hasAllVisibleTiles : function () { return true; },

// flag so that superclasses (i.e. TileGrid) can turn manual tile hiding off
_enableUserHiding: true,
// helper function used in layoutTiles to get only tiles not hidden by an external call to hide()
getUserVisibleTiles : function () {
    var visTiles = [];
    for (var i = 0; i < this.getLength(); i++) {
        var curr = this.getTile(i);
        if (!curr._userHidden) visTiles.add(curr);
    }
    return visTiles;
},

// tileNum can be the index of a given tile (TileGrid), or the tile itself (TileLayout)
processTile : function (tileNum, top, left, height , width) {
    var tile;
    //isc.logWarn("processing tile:" + [top, left, height, width]);
    if (this._animating) {

        if (isc.isA.Canvas(tileNum)) tile = tileNum;
        else tile = this.getRecordTile(tileNum);
        if (!tile) return;
        // verify that tile was visible before
        if (this._oldVisibleTiles != null && this._oldVisibleTiles.findIndex("ID", tile.ID) == -1) return;
        //isc.logWarn("tile:" + tile);
        if (!tile) return;

        if (height) tile.setHeight(height);
        if (width) tile.setWidth(width);
        // only track the tile for animation if it will actually change position. This helps
        // prevent excess show/hide cycles.
        if (tile.getTop() != top || tile.getLeft() != left) {
            // store the top and left to which this tile should be animated (see _animateChange())
            tile._newTop = top;
            tile._newLeft = left;
            this._tilesToAnimate.add(tile);
        }

        var tileRect = [left, top, tile.getVisibleWidth(), tile.getVisibleHeight()];

        // make sure tile is actually visible before adding to visibleTiles
        if (isc.Canvas.rectsIntersect(this._viewportRect, tileRect)) {
             if (!this._visibleTiles.contains(tile)) this._visibleTiles.add(tile);
        }
    } else {

        if (isc.isA.Canvas(tileNum)) tile = tileNum;
        else tile = this.getTile(tileNum);
        //isc.logWarn('processing tile: ' + this.echoFull(tile));
        if (!tile) return;
        // redraw dirty tiles
        if (tile.isDirty()) tile.redraw();
        // set height and width here
        if (height) tile.setHeight(height);
        if (width) tile.setWidth(width);
        tile.setTop(top);
        tile.setLeft(left);
        // add tile to children
        this.addChild(tile);
        // must do this because tiles may be hidden from a call to cleanupExtraTiles() from
        // within dataChanged(). see dataChanged() for more info.

        if (isc.TileGrid && isc.isA.TileGrid(this)) {
            tile._suppressVisibilityChange = true;
            tile.show();
            tile._suppressVisibilityChange = null;
        } else {
            if (tile.visibility == "hidden") tile._userHidden = true;
            else                             tile._userHidden = null;
        }
        // add tile to _visibleTiles for animation purposes. See _animateChange.
        // we keep track of this in TileGrid and not TileLayout, because this pertains more to
        // TileGrid functionality than to tileLayout, thus keeping the code cleaner.
        // Not true anymore, tileLayout now has animations 11/3/08
        var tileRect = [tile.getLeft(), tile.getTop(), tile.getVisibleWidth(), tile.getVisibleHeight()];

        // make sure tile is actually visible before adding to visibleTiles
        if (isc.Canvas.rectsIntersect(this._viewportRect, tileRect)) {
             if (!this._visibleTiles.contains(tile)) this._visibleTiles.add(tile);
        }
        // keep track of number of tiles processed so that extra tiles can be hidden if needed
        // in layoutTiles()
        this._numTilesProcessed += 1;
        return tile;
    }

},

_getSpacerCanvas : function () {
    if (!this._spacerCanvas) {
        this._spacerCanvas = isc.Canvas.create({autoDraw:false, width:1, height:1});
        // spacer canvas to make the scroll behave appropriately
        this.addChild(this._spacerCanvas);
    }
    return this._spacerCanvas;
},

getDrawnStartIndex : function () {
    // this check should be adequate. We aren't supporting changing incremental rendering on the
    // fly, so this will always return null if its off..._lastVisibleTiles is only created when
    // incremental rendering is on. (see shouldUseIncrRendering())
    if (this._lastVisibleTiles) return this._lastVisibleTiles[0];
    else return null;
},

getDrawnEndIndex : function () {
    if (this._lastVisibleTiles) return this._lastVisibleTiles[1];
    else return null;
},

shouldUseIncrRendering : function () {
    if (this._animating) {
        return true;
    } else if (!this.showAllRecords && this.layoutPolicy == "fit"
        && (this.overflow == "auto" || this.overflow == "hidden")) {
        return true;
    } else {
        return false;
    }
},

getLength : function () {
    if (!this.tiles) return 0;
    else return this.tiles.getLength();
},

getTilesPerLine : function () {
    if (this.tilesPerLine) return this.tilesPerLine;
    else {
        // variable names are in terms of horizontal orientation
        var tSize = this.orientation == "horizontal" ? this.getTileWidth() : this.getTileHeight();
        var tMargin = this.orientation == "horizontal" ? (this.tileHMargin || this.tileMargin) :
                            (this.tileVMargin || this.tileMargin);
        var thisSize = this.orientation == "horizontal" ? this.getInnerWidth() : this.getInnerHeight();
        // first just get the number of tiles, without margin, that will fit
        // NOTE margin between the peripheral tiles and widget boundaries are not counted
        var numTiles = Math.floor(thisSize / tSize);

        // now factor in the margins; if the total width is greater than our width, subtract
        // 1 from the the tile count.
        var marginOffset = this.orientation == "horizontal" ? this._leftMargin + this._rightMargin :
                                                              this._topMargin + this._bottomMargin;
        var totLineWidth = tMargin * (numTiles - 1) + (numTiles * tSize) + marginOffset;
        // total line size may be over the limit by more than 1 tile size
        if (totLineWidth > thisSize) {
            var numTilesOver = Math.ceil((totLineWidth - thisSize) / tSize);
            numTiles -= numTilesOver;
        }
        // at least one tile per line
        if (numTiles < 1) numTiles = 1;

        return numTiles;
    }
},

_setTileSize : function () {
    var isHoriz = this.orientation == "horizontal";
    // only set tile width/height if layoutPolicy = fit, expandMargins = false,
    // tilesPerLine is set, and tileWidth/height is not set
    if (this.layoutPolicy != "fit" || this.expandMargins || !this.tilesPerLine
        || (isHoriz && this.tileWidth) || (!isHoriz && this.tileHeight)) return;
    var tMargin = isHoriz ? (this.tileHMargin || this.tileMargin) :
                            (this.tileVMargin || this.tileMargin);
    var thisSize = isHoriz ? this.getInnerWidth() : this.getInnerHeight();
    // getInnerWidth does not take margin into account
    // for some reason, this._leftMargin etc. aren't set yet here so use layoutMargin directly
    var marginOffset = this.layoutMargin * 2;
    var totLineWidth = thisSize - marginOffset;
    if (!this.tilesPerLine) {
        // Ensure tileSize has a default

        if (this.tileSize == null) this.tileSize = 50;
    } else {
        var tSize = Math.floor(totLineWidth / this.tilesPerLine);
        tSize -= tMargin;
        //isc.logWarn('setTileSize:' + [tSize, thisSize, this.layoutMargin]);
        if (this.orientation == "horizontal") this.defaultTileWidth = tSize;
        else this.defaultTileHeight = tSize;
    }
},

getVisibleLines : function () {
    var horizontal = (this.orientation == "horizontal");
    var scrollPos = horizontal ? this.getScrollTop() : this.getScrollLeft();
    var lineHeight = horizontal ? this.getTileHeight() + this.getTileVMargin() :
                                  this.getTileWidth() + this.getTileHMargin();
    var windowSize = horizontal ? this.getInnerHeight() : this.getInnerWidth();

    var numLinesOffScreen = Math.floor(scrollPos / lineHeight);
    // overestimate start line by adding a line to the front
    if (numLinesOffScreen > 0) numLinesOffScreen--;

    var numLinesOnScreen = Math.ceil(windowSize / lineHeight) + 2;

    return [numLinesOffScreen, numLinesOnScreen];
},

getVisibleTiles : function () {
    var visibleLines = this.getVisibleLines();
    var tilesPerLine = this.getTilesPerLine();
    var start = visibleLines[0] * tilesPerLine;
    var end = (visibleLines[0] + visibleLines[1]) * tilesPerLine;

    return [start, end];
},

scrolled : function () {
    if (this.shouldUseIncrRendering()) {
        if (this._layoutEventId) isc.Timer.clear(this._layoutEventId);
        this._layoutEventId = this.delayCall("layoutAfterScroll");
    }
},

layoutAfterScroll : function () {
    this.logDebug('layoutAfterScroll', "TileLayout");
    if (this.shouldLayoutTiles()) {
        this.logDebug('calling layoutTiles from layoutAfterScroll', "TileLayout");
        this.layoutTiles();
    }
},

shouldLayoutTiles : function () {
    var currTiles = this.getVisibleTiles();
    if (currTiles[0] == this.getDrawnStartIndex() && currTiles[1] == this.getDrawnEndIndex()) {
        return false;
    } else {
        return true;
    }

},

getTileWidth : function () {
    if (this.tileWidth) {
        if (isc.isA.String(this.tileWidth)) {
            this.tileWidth = parseInt(this.tileWidth);
            if (!isc.isA.Number(this.tileWidth)) this.tileWidth = this.tileSize;
        }
        return this.tileWidth;
    } else if (this.defaultTileWidth) {
        return this.defaultTileWidth;
    } else {
        return this.tileSize;
    }
},

getTileHeight : function () {
    if (this.tileHeight) {
        if (isc.isA.String(this.tileHeight)) {
            this.tileHeight = parseInt(this.tileHeight);
            if (!isc.isA.Number(this.tileHeight)) this.tileHeight = this.tileSize;
        }
        return this.tileHeight;
    } else if (this.defaultTileHeight != null) {
        return this.defaultTileHeight;
    } else {
        return this.tileSize;
    }
},

// helper function to consolidate code used in various places
getInnerBreadth : function () {
    /*


    var breadth = this.orientation == "horizontal" ? this.getVisibleWidth() - this.getHBorderPad()
        : this.getVisibleHeight() - this.getVBorderPad();
    if (this.willScroll()) breadth -= this.getScrollbarSize();
    // account for the tileLayout having edges
    if (this.showEdges && this._edgedCanvas) {
        var edgesSize, ec = this._edgedCanvas;
        if (this.orientation == "horizontal") edgesSize = ec._leftEdge + ec._rightEdge;
        else edgesSize = ec._topEdge + ec._bottomEdge;
        breadth -= edgesSize;
    }
    */
    // getInnerWidth/Height takes care of all borders and edges
    var breadth = this.orientation == "horizontal" ? this.getInnerWidth()
        : this.getInnerHeight();
    //if (this.willScroll()) breadth -= this.getScrollbarSize();
    return breadth;
},

getTileHMargin : function () {

    var hMargin;
    if (this.tileHMargin) {
        hMargin = this.tileHMargin;
    } else {
        hMargin = this.tileMargin;
    }
    if (this.layoutPolicy == "fit" && this.expandMargins && this.orientation == "horizontal") {
        var tpl = this.getTilesPerLine();

        // expanded margin = (viewable width - all tiles in a line width - layoutMargins) / number of margins in a line
        // remainder is calculated later, in getExtraMarginPixels
        var lMargins = this._leftMargin + this._rightMargin;
        var marginsInALine = tpl - 1;
        // can't have zero margins in a line
        if (marginsInALine == 0) marginsInALine = 1;
        var exMargin = Math.floor((this.getInnerBreadth() - (tpl * this.getTileWidth()) - lMargins) / marginsInALine);
        // don't return less than hMargin
        if (exMargin < hMargin) return hMargin;
        else return exMargin;
    } else {
        return hMargin;
    }


},

getTileVMargin : function () {
   // if (this.tileVMargin) return this.tileVMargin;
   // else return this.tileMargin;

    var vMargin;
    if (this.tileHMargin) {
        vMargin = this.tileVMargin;
    } else {
        vMargin = this.tileMargin;
    }
    if (this.layoutPolicy == "fit" && this.expandMargins && this.orientation == "vertical") {
        var tpl = this.getTilesPerLine();
        // expanded margin = (viewable height - tilesPerLine height) / marginsPerLine
        // remainder is calculated later, in getExtraMarginPixels
        var lMargins = this._topMargin + this._bottomMargin;
        var marginsInALine = tpl - 1;
        // can't have zero margins in a line
        if (marginsInALine == 0) marginsInALine = 1;
        var exMargin = Math.floor((this.getInnerBreadth() - (tpl * this.getTileHeight()) - lMargins) / marginsInALine);
        // don't return less than hMargin
        if (exMargin < vMargin) return vMargin;
        else return exMargin;
    } else {
        return vMargin;
    }
},

// this calculates the remainder of pixels after expanded margins are calculated in getTileHMargin
// and getTileVMargin. These pixels will then be spread out between tiles in layoutTiles() for fit layout mode
getExtraMarginPixels : function (tpl, tHeight, tWidth, tHMargin, tVMargin) {
    if (this.expandMargins && this.orientation == "horizontal") {
        // viewable width - (tilesPerLine width + marginsPerLine width)
        var usedPixels = (tpl * tWidth) + ((tpl - 1) * tHMargin) + (this._leftMargin + this._rightMargin);
        return this.getInnerBreadth() - usedPixels;
    } else if (this.expandMargins && this.orientation == "vertical") {
        // viewable height - (tilesPerLine height + marginsPerLine height)
        var usedPixels = (tpl * tHeight) + ((tpl - 1) * tVMargin) + (this._topMargin + this._bottomMargin);
        return this.getInnerBreadth() - usedPixels;

    } else {
        return 0;
    }
},

//> @method tileLayout.getTile()
// Retrieve a tile by index.
// <P>
// The TileLayout consistently uses this method to access tiles, in order to allow subclasses
// to create tiles on demand.
//
// @param index (int) index of the tile
// @return (Canvas) the tile
//
// @visibility external
//<
getTile : function (index) {

    return isc.Class.getArrayItem(index, this.tiles);
},

//> @method tileLayout.addTile()
// Add a tile to the layout, dynamically.
//
// @param tile (Canvas) new tile to add
// @param [index] (integer) position where the tile should be added.  Defaults to adding the tile
// at the end.
// @visibility external
//<
addTile : function (tile, index) {
     if (!this.tiles) return;
     if (index !== 0 && !index) index = this.tiles.getLength();
     this.tiles.addAt(tile, index);
     this.reLayout();
},

//> @method tileLayout.removeTile()
// Remove a tile from the layout.
//
// @param tileID (Canvas or int or ID) index or String ID of the tile
// @return (boolean) whether a tile was found and removed
// @visibility external
//<
removeTile : function (tile) {
    if (!this.tiles) return;

    if (!isc.isA.Canvas(tile)) {
        // first assume tile is a canvas ID
        var index = this.tiles.findIndex("ID", tile);
        // if not, assume tile is an index into tiles
        if (index == -1) tile = this.tiles.get(tile);
        else tile = this.tiles.get(index);
    }
    // removeChild handles internal cleanup as well
    this.removeChild(tile);
},

// override removeChild to do internal cleanup and layout tiles again after the child is removed
removeChild : function (child, name) {
    this.tiles.removeWhere("ID", child.ID);
    if (this._visibleTiles) {
        this._visibleTiles.removeWhere("ID", child.ID);
    }
    if (this._oldVisibleTiles && this._oldVisibleTiles != this._visibleTiles) {
        this._oldVisibleTiles.removeWhere("ID", child.ID);
    }
    if (this._tilesToAnimate) {
        this._tilesToAnimate.removeWhere("ID", child.ID);
    }
    this.invokeSuper(isc.TileLayout, "removeChild", child, name);
    this.reLayout();
},

getRecordTile : function (recordIndex) {
    if (recordIndex == null) return null;
    return this.tiles.get(recordIndex);
},

// override here to track external calls to tile.hide(), so that we won't forcibly show
// those tiles in layoutTiles
childVisibilityChanged : function (child, newVisibility) {
    if (!this._animating && !child._suppressVisibilityChange) {
        // _userHidden is used in getUserVisibleTiles()
        if (newVisibility == "hidden") child._userHidden = true;
        else child._userHidden = null;
        this.reLayout();
    }
},

relayoutProperties:["tilesPerLine", "orientation", "tileWidth", "tileHeight", "expandMargins"],
propertyChanged : function (propertyName, value) {
    this.invokeSuper(isc.TileLayout, "propertyChanged", propertyName, value);
    // tileMargin et al
    if (isc.endsWith(propertyName, "Margin") ||
        this.relayoutProperties.contains(propertyName))
    {
        this.layoutTiles();
    }
},

// --------------------------Drag and Drop-----------------------------------------------------
// for dragging records out, use the drag tracker
//dragAppearance:isc.EH.TRACKER,
//dragTrackerMode: "title",
canDrop: true,
_dragTrackerThickness: 2,

//>    @method    tileGrid.showDragLineForRecord()    (A)
// Show the drag line relative to a particular record.
// <p>
// If no record number is passed, assumes the one under the mouse.
// <p>
// This is used to show feedback in reordering rows or to insert dragged records at a particular
// row.
//        @group    dragging, drawing
//<
showDragLineForRecord : function () {
  // isc.logWarn('tilelayout.showDragLineForRecord: ' + this.data);
     if (isc.isAn.Array(this.data) || (isc.isA.ResultSet(this.data))) {
        var x = this.getOffsetX(), y = this.getOffsetY(), xBase = this.getPageLeft(), yBase = this.getPageTop();
        if (this.data.getLength() == 0) {
            return;
        }

        // get index of tile that mouse is over
        var currIndex = this.findIndexForCoord(x, y);
        // keep track of the index where dragging started to detect a self-drop in this.drop()
        // would have used dragStart, but it doesn't fire for tileLayout (does for tileGrid)
        if (this._dragStartIndex == null) this._dragStartIndex = currIndex;
        var tLeft, tTop, tile = this.getRecordTile(currIndex);
        //isc.logWarn('TL.showDragLine:' + [currIndex, this._dragStartIndex]);

        // make the cursor move to the next tile if the mouse is more than half way
        // across the current tile
        if (tile != null) {
           if ((this.orientation == "horizontal" && x > tile.getLeft() + (tile.getVisibleWidth() / 2)) ||
               (this.orientation == "vertical" && y > tile.getTop() + (tile.getVisibleHeight() / 2))) {
                  currIndex++;
                  tile = this.getRecordTile(currIndex);
               }
        }
        // if no tile is moused over, make the index point to the end of the array and place
        // the tracker at the end of the last tile
        if (currIndex == null || tile == null) {
            currIndex = this.data.getLength();
            tile = this.getRecordTile(currIndex - 1);
            if (this.orientation == "horizontal") {
                tLeft = tile.getLeft() + tile.getVisibleWidth() - this._dragTrackerThickness;
                tTop = tile.getTop();
            } else {
                tLeft = tile.getLeft();
                tTop = tile.getTop() + tile.getVisibleHeight() - this._dragTrackerThickness;
            }
        } else {
            tLeft = tile.getLeft();
            tTop = tile.getTop();
        }

        // store this for drop
        this._lastDropIndex = currIndex;

        var lineWidth, lineHeight;
        if (this.orientation == "horizontal") {
            lineHeight = tile.getVisibleHeight();
            lineWidth = this._dragTrackerThickness;
        }
        else {
            lineWidth = tile.getVisibleWidth();
            lineHeight = this._dragTrackerThickness;
        }

        this.showDragLine(xBase + (tLeft - this.getScrollLeft()),
                    yBase + (tTop - this.getScrollTop()), lineWidth, lineHeight);

    }
},

showDragLine : function (left, top, width, height) {

    // make sure the drag line is set up
    this.makeDragLine();
    // make sure the drag line isn't hanging over the top or bottom edges of
    // the tilegrid. If it is, adjust the top and height params accordingly.

    // cache tilegrid visible height
    var visHeight = this.getVisibleHeight();
    // calculate the border around this tilegrid
    var border = Math.round((visHeight - this.getInnerHeight()) / 2);
    // cache the pageTop
    var pTop = this.getPageTop();
    // get the top and bottom y coords of the inner part of this tilegrid
    var gridTop = pTop + border;
    var gridBottom = pTop + visHeight - border;
    // if the dragline is sticking out above the grid, make the height = the
    // amount its sticking out, and move it down so its aligned with the top edge
    // of the tilegrid
    if (top < gridTop) {
        height = height - (gridTop - top);
        top = gridTop;
    // otherwise if the dragline is sticking out below the tilegrid, truncate
    // the dragline so its within the grid borders
    } else if (top + height > gridBottom ) {
        // border condition: if a record is dragged over this tilegrid and
        // we can reorder tiles, there are certain cases where the dragLine
        // will placed completely below and outside of the grid. In this case,
        // just make the tracker height 0 so it doesn't appear at all.
        if (top >= gridBottom) {
            height = 0;
        } else {
            height = height - ((top + height) - gridBottom);
        }
    }

    this._dragLine.moveTo(left, top);
    this._dragLine.resizeTo(width, height);
    this._dragLine.show();
},

dropOut : function () {

    this.hideDragLine();
},

dropMove : function () {
    this.showDragLineForRecord();

},

// returns the index of the first record whose tile left and top is greater than the passed in
// left and top coords
findIndexForCoord : function (left, top) {
    // first see if we can use drawnStart/endIndex. This is the case for showAllRecords:false
    var start = this.getDrawnStartIndex(), end = this.getDrawnEndIndex();
    // if not, then just traverse the whole data array
    if (start == null || end == null) {
        start = 0;
        end = this.data.getLength();
    }
    // obtain the ID of the drag target if it exists. It will be one of the
    // tiles if dragAppearance = 'target' on the tile
    var eh = this.ns.EH;
    var dragTarg = eh.dragTarget, dragID;
    if (dragTarg) dragID = dragTarg.ID;
    for (var i = start; i < end; i++) {
        var tile = this.getRecordTile(i);
        if (!tile) continue;
        // skip the drag target, otherwise the only valid index will ever be
        // the start index of the target.
        if (tile.ID == dragID) continue;
        if (tile.getLeft() + tile.getVisibleWidth() > left
            && tile.getTop() + tile.getVisibleHeight() > top) return i;
    }

    return null;
},

//>    @method    tileLayout.drop()    (A)
//            handle a drop event
//        @return    (boolean)    true if the list can't reorder or dragging did not begin from the list body;
//                            false if disabled, no selection, or otherwise
//        @group    events, dragging
//<
drop : function () {
    var index = this._lastDropIndex || 0;
    var dropRecords = this.ns.EH.dragTarget;
    var dragStartIndex = this._dragStartIndex;
    //isc.logWarn('dropped:' + index);
    // reset _dragStartIndex so the next drag will start over
    // NOTE can probably remove this code from here, as only TileGrid drop uses dragStartIndex
    // currrently. But leave it for now.
    this._dragStartIndex = null;

    if (!isc.isAn.Array(dropRecords)) dropRecords = [dropRecords];
    var fromCanvas = dropRecords[0].parentElement;

    var targetRecord = this.data.get(index);
    this.transferRecords(dropRecords, targetRecord, index, fromCanvas);
    this.reLayout();
},

//>    @method    tileLayout.transferDragData()
// @include dataBoundComponent.transferDragData()
//<

//>    @method    tileLayout.getDragData()
// @include dataBoundComponent.getDragData()
//<

//>    @attr    tileLayout.dragDataAction
// @include dataBoundComponent.dragDataAction
//<

// --------------------------Animation-----------------------------------------------------

reLayout : function () {
    // prevent tileLayout in the process of destroying from setting timers and causing errors
    if (this.destroying) return;

    if (this.animateTileChange) {
        this.delayCall("_animateChange", null, 200);
    } else {
        this.logDebug('calling layoutTiles from reLayout', "TileLayout");
        this.layoutTiles();
    }
},

_animateChange : function () {
    this.logDebug("starting _animateChange()", "TileLayout");
    // tell this.processTile() that we're animating, which will be called from tileGrid.layoutTiles()
    this._animating = true;
    // keep track of animation ID's so they can be cancelled later if another dataChange event
    // happens while an animation is going on.
    this._animationIDs = [];
    // hold on to the array of visibleTiles, as this._visibleTiles will get reset when
    // we run layoutTiles() from here
    var visTiles = this._oldVisibleTiles = this._visibleTiles;
    // 1. calling layoutTiles will build an array of tiles that need to be animated (i.e. that were
    // visible before and are still visible now) by calling processTile(), and
    // storing these tiles in this._tilesToAnimate
    this._tilesToAnimate = [];
    this.layoutTiles();
    // 2. hide tiles pointing to data that will no longer be visible
    // check that visTiles exists, based on an alleged issue with tileGrid in a sectionStack
    if (visTiles != null) {
        for (var i = 0; i < visTiles.length; i++) {
            // if the currently visible tiles contain the old visible tiles, hide those tiles.
            // if we just hide them all without the check, there is a visible flash for tiles
            // that stay in the same place.
            if (!this._visibleTiles.contains(visTiles[i]))  {
                visTiles[i].hide();
            }
        }
    }
    // 3. animate the tiles that need to be moved
    var numTiles = this._tilesToAnimate.length;
    if (numTiles == 0) {
        this._finishAnimating();
        return;
    }
    for (var i = 0; i < numTiles; i++) {
        var tile = this._tilesToAnimate[i];
        // see the comment below step 2, we need to show here because the tile may have been
        // hidden from a call to cleanupExtraTiles() within dataChanged().
        tile.show();
        // if last tile, pass a callback
        var animID;
        if (i == numTiles - 1) {
            animID = tile.animateMove(tile._newLeft, tile._newTop, this.getID() + "._finishAnimating()");
        } else {
            animID = tile.animateMove(tile._newLeft, tile._newTop);
        }
        // keep track of animations for later cancellation. see _layoutAfterDataChange()
        this._animationIDs.add({ID:animID, tile:tile});
    }

},

_finishAnimating : function () {
    // no longer animating
    this._animating = false;
    // clean up internal arrays
    this._oldVisibleTiles = null;
    delete this._oldVisibleTiles;
    this._tilesToAnimate = null;
    delete this._tilesToAnimate;
    // layout tiles
    this.logDebug('calling layoutTiles from _finishAnimating', "TileLayout");
    this.layoutTiles();
},

//> @method TileLayout.isAnimatingTileLayout()
//
// Returns whether the TileLayout is currently performing animation as defined
// by its _animating flag.  Dinstinct from +link{Canvas.isAnimating()}.
// Useful for writing Selenium scripts that depend on operations completing.
//<
isAnimatingTileLayout : function () {
    return this._animating === true;
},

// -----------------------tileLayout-----------------------------------------------------------
//>    @method    tileLayout.setTileSize()
// Sets the height and width of tiles.
// @param (Integer) size
//
// @group tileLayout
// @visibility external
//<
setTileSize : function (size) {
    this.tileSize = size;
    this.layoutTiles();
},

//>    @method    tileLayout.setTileWidth()
// Sets the width of tiles.
// @param (Integer) width
//
// @group tileLayout
// @visibility external
//<
setTileWidth : function (width) {
    this.tileWidth = width;
    this.layoutTiles();
},

//>    @method    tileLayout.setTileHeight()
// Sets the height of tiles.
// @param (Integer) height
//
// @group tileLayout
// @visibility external
//<
setTileHeight : function (height) {
    this.tileHeight = height;
    this.layoutTiles();
},

//> @method tileLayout.setTilesPerLine()
// Sets the number of tiles per line.
// @param tilesPerLine (Integer) New +link{tileLayout.tilesPerLine} value
// @group tileLayout
// @visibility external
//<
setTilesPerLine : function (tilesPerLine) {
    this.tilesPerLine = tilesPerLine;
    // re-run _setTileSize to recalculate default width/height if necessary
    this._setTileSize();
    this.layoutTiles();

},

//>    @method    tileLayout.setTileMargin()
// Sets the vertical and horizontal margin of tiles.
// @param (Integer) margin
//
// @group tileLayout
// @visibility external
//<
setTileMargin : function (margin) {
    this.tileMargin = margin;
    this.layoutTiles();
},

//>    @method    tileLayout.setTileHMargin()
// Sets the horizontal margin of tiles.
// @param (Integer) width
//
// @group tileLayout
// @visibility external
//<
setTileHMargin : function (margin) {
    this.tileHMargin = margin;
    this.layoutTiles();
},

//>    @method    tileLayout.setTileVMargin()
// Sets the vertical margin of tiles.
// @param (Integer) width
//
// @group tileLayout
// @visibility external
//<
setTileVMargin : function (margin) {
    this.tileVMargin = margin;
    this.layoutTiles();
}
});

//>    @class FlowLayout
// Arranges a set of Canvas components into rows, flowing into available space so that
// different numbers of components may appear in each row.
// <p>
// <code>FlowLayout</code> is essentially just a subclass of +link{TileLayout} where the
// default +link{tileLayout.layoutPolicy} is "flow" instead of "fit".
//
// @see tileLayout.layoutPolicy
// @treeLocation Client Reference/Grids
// @visibility external
//<
isc.ClassFactory.defineClass("FlowLayout", "TileLayout");

isc.FlowLayout.addProperties({
        layoutPolicy: "flow"
});




//>    @class TileGrid
// A TileGrid is a +link{DataBoundComponent} that displays a list of objects as a set
// of "tiles", where each tile represents one object, and the tiles are laid out in a grid with
// multiple tiles per row.  Each tile displays one or more properties of the object it
// represents.
//
// @implements DataBoundComponent
// @treeLocation Client Reference/Grids
// @visibility external
//<
isc.ClassFactory.defineClass("TileGrid", "TileLayout", "DataBoundComponent");

isc.TileGrid.addProperties({

//> @attr tileGrid.fields (Array of DetailViewerField : null : IR)
// Array of field definitions to control the default rendering of tiles.
// <P>
// If not specified, if the DataSource has an +link{dataSource.iconField,iconField}, only the
// <code>iconField</code> and +link{dataSource.titleField,titleField} will be shown.
// Otherwise, all non-+link{dataSourceField.hidden,hidden}
// non-+link{dataSourceField.detail,detail} fields will be shown, similar to the default set of
// fields shown by a +link{ListGrid}.
// <P>
// Only applicable if using the default +link{SimpleTile} class for tiles.
// <P>
// For SimpleTiles, it is possible to use +link{DetailViewerField.getCellStyle()} and
// +link{StatefulCanvas.getStateSuffix()} to make a single field statefully styled:
// <pre>
// <smartclient>
// isc.TileGrid.create({
//      fields:[
//          {name:'animalName',
//           getCellStyle : function (value, field, record, viewer) {
//               if (value == "Tiger") return "tigerStyle" + viewer.currentTile.getStateSuffix();
//               else return viewer.tileGrid.tileValueStyle + viewer.currentTile.getStateSuffix();
//           }
//          }
//      ]
// });
// </smartclient>
// <smartgwt>
// final TileGrid tileGrid = new TileGrid();
// DetailViewerField animalNameField = new DetailViewerField("countryName");
// animalNameField.setCellStyleHandler(new CellStyleHandler() {
//     public String execute(Object value, DetailViewerField field, Record record) {
//         SimpleTile tile = tileGrid.getCurrentTile();
//         if (value == "Tiger") return "tigerStyle" + tile.getStateSuffix();
//         else return "nonTigerStyle" + tile.getStateSuffix();
//     }
// });
// tileGrid.setFields(animalNameField);
// </smartgwt>
// </pre>
//
// @visibility external
//<

//>    @attr tileGrid.showDetailFields (Boolean : false : IR)
// By default, TileGrids will not show fields marked +link{dataSourceField.detail,detail:true}
// in the DataSource.  See also +link{tileGrid.fields}.
// @visibility external
//<

//> @attr tileGrid.tileValueStyle (CSSClassName : "tileValue" : IR)
// When using the default +link{SimpleTile}, CSS style for each value shown within a tile.
// @visibility external
//<
tileValueStyle:"tileValue",

// @attr TileGrid.valuesShowRollOver (boolean : false : IR)
// Should tile values change state when the mouse goes over them?
// @visibility external
valuesShowRollOver: false,

// @attr TileGrid.valuesShowSelected (boolean : true : IR)
// Should tile values change state when they are selected?
// @visibility external
valuesShowSelected: true,

// @attr TileGrid.valuesShowDown (boolean : false : IR)
// Should tile values change state when the mouse goes down on them?
// @visibilty external
valuesShowDown: false,

//> @attr tileGrid.tileValueAlign   (String : "center" : IR)
// Horizontal alignment for tile values: "left", "right" or "center".
// @visibility external
//<
tileValueAlign:"center",

// ability to show labels dubious and not doc'd yet - better approach probably to support
// optionally showing a label on a per-line basis, or suggest using formatters
showLabels:false,
tileLabelStyle:"tileLabel",

//> @attr tileGrid.wrapValues (Boolean : false : IR)
// Whether values should be allowed to wrap by default, or should be shown on one line
// regardless of length.
// @visibility external
//<
wrapValues: false,

// allows sorting via panelHeader by default
canSortFields: true,

//>    @attr tileGrid.data (Array[] of Record | Array[] of TileRecord | RecordList : null : IRW)
// A List of TileRecord objects, specifying the data to be used to create the
// tiles.
// <p>
// This property will typically not be explicitly specified for databound TileGrids, where
// the data is returned from the server via databound component methods such as
// +link{fetchData()}. In this case the data objects will be set to a
// +link{class:ResultSet,resultSet} rather than a simple array.
//
// @group    data
// @see TileRecord
// @setter setData()
// @visibility external
//<

//> @attr tileGrid.printTilesPerLine (number : null : IR)
// How many tiles should be present in a line when printing?
//
// @visibility external
//<

// ---------------------------inherited from dataBoundComponent--------------------------------
//>    @attr TileGrid.dataSource        (DataSource or ID : null : IRW)
// @include dataBoundComponent.dataSource
//<

//> @method TileGrid.fetchData()
// @include dataBoundComponent.fetchData()
// @group dataBoundComponentMethods
// @visibility external
// @example databoundFetch
//<

//> @method TileGrid.filterData()
// @include dataBoundComponent.filterData()
// @group dataBoundComponentMethods
// @visibility external
//<

//>    @attr TileGrid.autoFetchData       (boolean : false : IR)
// @include dataBoundComponent.autoFetchData
// @group databinding
// @visibility external
// @example fetchOperation
//<

//> @method TileGrid.invalidateCache()
// @include dataBoundComponent.invalidateCache()
// @group dataBoundComponentMethods
// @visibility external
//<

//> @method TileGrid.removeSelectedData()
// @include dataBoundComponent.removeSelectedData()
// @group dataBoundComponentMethods
// @visibility external
// @example removeOperation
//<

//> @method TileGrid.getSelection()
// @include dataBoundComponent.getSelection()
//
// @group  selection
// @visibility external
//<

//> @method TileGrid.getSelectedRecord()
// @include dataBoundComponent.getSelectedRecord()
//
// @group  selection
// @return (TileRecord) first selected record, or null if nothing selected
// @visibility external
//<

//> @method TileGrid.selectRecord()
// @include dataBoundComponent.selectRecord()
//<

//> @method TileGrid.deselectRecord()
// @include dataBoundComponent.deselectRecord()
//<

//> @method TileGrid.selectRecords()
// @include dataBoundComponent.selectRecords()
//<

//> @method TileGrid.selectRange()
// @include dataBoundComponent.selectRange()
//<

//> @method TileGrid.deselectRecords()
// @include dataBoundComponent.deselectRecords()
//<

//> @method TileGrid.deselectRange()
// @include dataBoundComponent.deselectRange()
//<

//> @method TileGrid.selectAllRecords()
// @include dataBoundComponent.selectAllRecords()
//<

//> @method TileGrid.deselectAllRecords()
// @include dataBoundComponent.deselectAllRecords()
//<

//> @method TileGrid.anySelected()
// @include dataBoundComponent.anySelected()
//<

//>    @attr TileGrid.autoFetchTextMatchStyle       (TextMatchStyle : "substring" : IR)
// @include dataBoundComponent.autoFetchTextMatchStyle
// @group databinding
// @visibility external
//<
autoFetchTextMatchStyle:"substring",

//> @attr TileGrid.initialCriteria   (Criteria : null :IR)
// @include dataBoundComponent.initialCriteria
// @visibility external
//<

//> @attr tileGrid.dataFetchMode (FetchMode : "paged" : IRW)
// @include dataBoundComponent.dataFetchMode
//<

//>    @attr tileGrid.selectionType        (SelectionStyle : isc.Selection.MULTIPLE : [IRW])
// Defines a tileGrid's clickable-selection behavior.
//
// @group    selection, appearance
// @see type:SelectionStyle
// @visibility external
// @example tilingEditing
//<
selectionType: isc.Selection.MULTIPLE,

//autoChildren

//> @attr tileGrid.tile (MultiAutoChild SimpleTile : null : IR)
// A TileGrid automatically creates one tile per record in the dataset, via the
// +link{AutoChild} pattern.
// <P>
// By default, the +link{SimpleTile} class will be used, and will contain content as rendered
// by a +link{DetailViewer}, based on the provided +link{TileGrid.fields} (or on the default
// set of fields).
// <P>
// <smartclient>
// To create a completely different appearance, override +link{tileConstructor} with the name
// of the custom SmartClient class to use for each tile.  For example, subclass
// +link{SimpleTile} and override +link{Canvas.getInnerHTML,getInnerHTML()}, returning custom
// HTML for each tile.
// <pre>
//     isc.defineClass("MyCustomTile", "SimpleTile").addProperties({
//        getInnerHTML : function () {
//           return this.Super("getInnerHTML", arguments) +
//                this.getRecord().width + " x " + this.getRecord().height;
//        }
//     });
//
//     isc.TileGrid.create({
//        tileConstructor:"MyCustomTile"
//     });
// </pre>
// <P>
// Note that you can also override tile behaviors on a per-record basis, via
// +link{tileRecord.tileConstructor} and +link{tileRecord.tileProperties}.
// </smartclient>
// <smartgwt>
// To create a completely different appearance set +link{tileConstructor} to custom Canvas
// class name. You can use SmartGWT classes too with help of Reflection. For example, you can
// extend a DynamicForm with any layout:
// <pre>
//   public class CarTile extends DynamicForm {
//     public CarTile() {
//       StaticTextItem name = new StaticTextItem("name");
//       name.setShowTitle(false);
//       StaticTextItem price = new StaticTextItem("price");
//       price.setShowTitle(false);
//       this.setFields(pictureItem, name, price);
//     }
//   }
// </pre>
// Register the class factory and use it by passing it's class to tileConstructor
// <pre>
//   public interface CarTileMetaFactory extends BeanFactory.MetaFactory {
//     BeanFactory<CarTile> getCarTileFactory();
//   }
//   public void onModuleLoad() {
//     GWT.create(CarTileMetaFactory.class);
//     TileGrid tileGrid = new TileGrid();
//     tileGrid.setTileConstructor(CarTile.class.getName());
//     tileGrid.draw();
//   }
// </pre>
// </smartgwt>
//
// @visibility external
//<

//> @attr tileGrid.tileProperties (Canvas Properties : null : IRW)
// Common properties to use when creating every tile.
// @visibility external
//<

//> @attr tileGrid.tileConstructor (SCClassName : "SimpleTile" : [IRW])
// Classname of a SmartClient component to use for each tile rendered by this TileGrid.  Tiles
// are created by the +link{AutoChild} pattern; see +link{tileGrid.tile}.
// <P>
// Any subclass of Canvas is allowed, but typically any custom class will derive from
// +link{SimpleTile}.
// <p>
// When using a custom component for tileConstructor, DataBoundComponents that display multiple
// Records (ListGrid, DetailViewer) will have data provided via +link{listGrid.setData()}, and
// components that display a single Record (DynamicForm) will have
// +link{dynamicForm.setValues()} called on them.
// <p>
// If the component is not a recognized DataBoundComponent subclass, the Record can be
// accessed via
//  <smartclient><code>this.record</code>.</smartclient>
//  <smartgwt><code>this.getAttributeAsRecord("record").</code></smartgwt>
// @example fullyCustomTiles
// @visibility external
//<
tileConstructor: "SimpleTile",

//> @attr tileGrid.detailViewer (AutoChild DetailViewer : null : IR)
// DetailViewer instance used to render the content shown in Tiles by default.
// @visibility external
//<
detailViewerDefaults: {
    _constructor: "DetailViewer",
    visibility: "hidden"
},

//> @attr tileGrid.detailViewerProperties (DetailViewer Properties : null : IR)
// Properties for the +link{DetailViewer} that is automatically created to render the contents of tiles
// by default.
// @visibility external
//<

// whether to reuse tiles, may want to doc at some point
recycleTiles: true,

//> @attr tileGrid.showAllRecords (Boolean : false : IR)
// Whether tiles are created and drawn for all records, or only for those currently visible.
//
// @group basics
// @visibility external
//<

//> @attr tileGrid.animateTileChange (Boolean : true : IRWA)
// If set, when the dataset changes due to filtering, sorting or other actions, any tiles that
// were showing before and after the change will animate from their old positions to their new
// positions.
//
// @group appearance
// @visibility external
//<
animateTileChange: true,

//> @attr tileGrid.styleName (CSSStyleName : "tileGrid" : IR)
// Style for the overall TileGrid component.
// @group appearance
// @visibility external
//<
styleName:"tileGrid",

// set this flag to false so that databound tiles can't be hidden by an explicit call to
// hide()
_enableUserHiding: false,

//> @object TileRecord
// A TileRecord is a JavaScript Object whose properties contain values for each
// TileField. A TileRecord may have additional properties which affect the record's
// appearance or behavior, or which hold data for use by custom logic or other, related
// components.
//
// @treeLocation Client Reference/Grids/TileGrid
// @visibility external
//<

//> @attr tileRecord.tileConstructor (String : null : IRW)
// SmartClient Class to use to construct the tile for this particular record.
//
// @visibility external
//<

//> @attr tileRecord.tileProperties (Canvas Properties : null : IRW)
// Additional properties to be passed when creating a tile for this record.
//
// @visibility external
//<

initWidget : function () {
    this._enforceLegalLayoutPolicy();

    // disable summary and formula fields if the required components aren't present
    if (isc.FormulaBuilder == null) this.canAddFormulaFields = false;
    if (isc.SummaryBuilder == null) this.canAddSummaryFields = false;

    if (this.layoutPolicy == "flow") {
        isc.logWarn("TileGrid does not support layoutPolicy 'flow'; there may be unexpected behavior. " +
                    "Use a TileLayout instead for flow layout.");
    }

    this._setUpDragProperties();
    // skip tileLayout init; we want to completely replace that here
    this.invokeSuper(isc.TileLayout, "initWidget");
    if (!this.tiles) this.tiles = [];
    // make sure we don't try to recycle tiles when we're showing all records
    if (this.showAllRecords) this.recycleTiles = false;

    // set up tile map for record <-> tile mapping when databound
    var ds = this.getDataSource();
    if (isc.isA.DataSource(ds)) {
        this._tileMap = {};
        if (ds.getPrimaryKeyFieldNames().length == 0) {
            // don't animate tiles if there is no primary key because there's no way of knowing
            // that the tiles will be the same
            this.animateTileChange = false;
        }
    }

    //TODO: don't create this if it won't be used (if we're not using SimpleTile)
    // internal detailViewer for creating SimpleTiles
    this.detailViewer = this.createAutoChild("detailViewer", {
            tileGrid: this,
            showLabel: this.showLabels,
            showBorder: false,
            cellStyle: this.tileValueStyle,
            labelStyle: this.tileLabelStyle,
            blockStyle: "normal",
            wrapValues: this.wrapValues,
            cellPadding: 0,
            valueAlign: this.tileValueAlign,
            // to force detailViewer table width to be 100%
            // NOTE 6/29/09 this needs to be false for the tiles to have their
            // content centered properly. Seems to not break the strict example.
            useInnerWidth: false,
            clipValues: true,
            // width and height should be set in makeTile
            width: 10,
            height: 10,
            data: [],
            dataSource: ds,
            getCellStyle: function (value, field, record, viewer) {
                var base = (field.cellStyle || this.cellStyle);
                if (this.tileGrid.valuesShowRollOver && this.currentTile.state == isc.StatefulCanvas.STATE_OVER) {
                    base += this.currentTile.getStateSuffix();
                } else if (this.tileGrid.valuesShowDown && this.currentTile.state == isc.StatefulCanvas.STATE_DOWN) {
                    base += this.currentTile.getStateSuffix();
                } else if (this.tileGrid.valuesShowSelected && this.currentTile.isSelected()) {
                    base += this.currentTile.getStateSuffix();
                }
                return base;
            }
    }, this.detailViewerProperties);

    // set field state if necessary, call setFields otherwise
    if (this.fieldState != null) this.setFieldState(this.fieldState);
    else this.setFields(this.fields, true);

    this.membersMargin = this.tileMargin;

    this.setData(this.data);
},

setDataSource : function (dataSource, fields) {
    this.Super("setDataSource", arguments);
    // set up tile map for record <-> tile mapping when databound
    var ds = this.getDataSource();
    if (isc.isA.DataSource(ds)) {
        this._tileMap = {};
        if (ds.getPrimaryKeyFieldNames().length == 0) {
            // don't animate tiles if there is no primary key because there's no way of knowing
            // that the tiles will be the same
            this.animateTileChange = false;
        }
    }
},


// return whether this component wants to use the field when binding to a DataSource.
// TileGrid-specific override of the DBC method to account for the fact that a TileGrid
// always wants the icon field if there is one
shouldUseField : function (field, ds) {

    if (this.Super("shouldUseField", arguments)) return true;

    ds = isc.DS.get(ds);
    if (isc.isA.DataSource(ds)) {
        var iconField = ds.getIconField();
        if (field == iconField || field.name == iconField ||
            (iconField && field.name == iconField.name))
        {
            return true;
        }
    }

    return false;
},


setFields : function (newFields, cancelLayout) {
    var ds = this.getDataSource();
    if (!newFields && isc.isA.DataSource(ds)) {
        // if the DataSource has an icon field, show just the icon and the title
        var iconField = ds.getIconField();
        if (iconField) {
            newFields = [];
            newFields.add({name:iconField, type: iconField.type});
            newFields.add({name:ds.getTitleField()});
        }
    }

    if (this.completeFields == null) this.fields = [];

    // bind the passed-in fields to the DataSource and store
    this.completeFields = this.bindToDataSource(newFields);

    if (this.completeFields == null) this.completeFields = [];
    // tilegrid was crashing without this line:
    if (!this.completeFields) return;

    this.deriveVisibleFields();
    this.invalidateUserCache();

    this.detailViewer.fields = this.completeFields.duplicate();
    if (!cancelLayout) {
        this.logDebug('calling layoutTiles from setFields', "TileGrid");
        this.layoutTiles();
    }
},

deriveVisibleFields : function () {
    // NOTE: we use setArray() so that this.fields remains the same array instance.
    this.fields.setArray(this.getVisibleFields(this.completeFields));
},

getVisibleFields : function (fields) {
    var viewer = this.detailViewer,
        valueList = this.getData(),
        returnFields = fields.duplicate();
    for (var i=0; i<fields.length; i++) {
        var field = fields.get(i);
        if (!viewer.fieldShouldBeVisible(field, valueList) ||
            field.visible==false) returnFields.remove(field);
    }
    return returnFields;
},

computeTileDimensions : function (forceCompute) {
    // don't compute tile dimensions if they're already known
    if (((this.tileHeight && this.tileWidth) || (this.tileSize)) && !forceCompute) return;
    // don't compute if we don't have all the data on hand.
    if (!((isc.ResultSet && isc.isA.ResultSet(this.data)
            && this.data.resultSize >= this.data.getLength())
         || isc.isAn.Array(this.data))) {
         return;
    }
    // only get dimensions when layoutPolicy is 'fit' and we have a tile array
    if (this.layoutPolicy != "fit") return;
    // iterate through tiles collection and find the greatest width and height
    var maxHeight = 0, maxWidth = 0;
    // very important; we don't want clipping
    this.detailViewer.clipValues = false;
    for (var i=0; i < this.data.getLength(); i++) {
        // render the tile so its sized by its own content
        var t = this.getTile(i);
        var currOverflow = t.overflow;
        t.setOverflow("visible");
        t.redraw();
        t.show();
        var tHeight = t.getVisibleHeight();
        var tWidth = t.getVisibleWidth();
        if (tHeight > maxHeight) maxHeight = tHeight;
        if (tWidth > maxWidth) maxWidth = tWidth;
        // reset the tile to its prior state
        t.setOverflow(currOverflow);
        t.hide();
    }
    // set the detailViewer back to its default state
    this.detailViewer.clipValues = true;
    if (!this.tileHeight && maxHeight > 0) this.tileHeight = maxHeight;
    if (!this.tileWidth && maxWidth > 0) this.tileWidth = maxWidth;

},

// get/setTileID ensure that tile-to-record mapping remains stable when databound.
// The expando approach doens't work when databound because the expando gets wiped out
// on update.
getTileID : function (record) {
    if (!record) return null;
    var ds = this.getDataSource();
    if (isc.isA.DataSource(ds) && ds.getPrimaryKeyFieldNames().length > 0) {
        var pks = ds.getPrimaryKeyFields();
        var pk = "";
        for (var pkName in pks) {
            pk += record[pkName];
        }
        return this._tileMap[pk];
    } else {
        return record._tileID;
    }
},

setTileID : function (record, tileID) {
    var ds = this.getDataSource();
    if (isc.isA.DataSource(ds) && ds.getPrimaryKeyFieldNames().length > 0) {
        var pks = ds.getPrimaryKeyFields();
        var pk = "";
        for (var pkName in pks) {
            pk += record[pkName];
        }
        this._tileMap[pk] = tileID;
    } else {
        record._tileID = tileID;
    }
},

//> @method TileGrid.getTileRecord()
// Given a tile within this this tile-grid, this method will return the associated
// record from the TileGrid's data.
// @param tile (Canvas) Tile you want to get the record for
// @return (TileRecord) Record associated with the specified tile
// @visibility external
//<
getTileRecord : function (tile) {
    //return tile._record;
    var list = this;
    var data = list.data;
    var start, end;

    // new dataset load in progress, don't try to access data or fetches will be triggered
    if (isc.isA.ResultSet(this.data) && !this.data.lengthIsKnown()) return null;

    // may want to optimize this
    if (this.showAllRecords || list.getDrawnStartIndex() == null || list.getDrawnEndIndex() == null) {
        start = 0;
        end = data.getLength()
    } else {
        // this approach leads to duplicate entries in the tileMap, which seems to be ok because
        // only a certain range is used when recycling tiles. If problems arise, try converting this
        // to use a record pointer on the tile instead. You'll also need to setTileRecord() in
        // the right places.
        start = list.getDrawnStartIndex();
        end = list.getDrawnEndIndex() + 1;
        if (end > data.getLength()) end = data.getLength();
    }
    for (var i = start; i < end; i++) {
        var rec = data.get(i);
        if (list.getTileID(rec) == tile.ID) return rec;
    }
    return null;
},

setTileRecord : function (tile, record) {
    //tile._record = record;
    return null;
},

setData : function (newData) {

    // if we're animating prevent new data from arriving. See _layoutAfterDataChange()
    if (this._animating) {
        return false;
    }

    if (!newData) return;

    if (this.data) {
        this.ignore(this.data, "dataChanged");
        this.ignore(this.data, "dataArrived");
    }
    // if newData was passed in, remember it
    if (newData) this.data = newData;

    // if data is not set, bail
    if (!this.data) return;

    //isc.logEchoAll(this.data);
    if (this.data) {
        if (isc.ResultSet && isc.isA.ResultSet(this.data)) {
            this.observe(this.data, "dataArrived",
                            "observer.dataArrived(arguments[0],arguments[1])");
            this.observe(this.data, "dataChanged",
                 "observer.dataChanged(operationType, originalRecord, rowNum, updateData)");
        } else {
            // dataChanged has no params for an array
            this.observe(this.data, "dataChanged", "observer.dataChanged()");
        }
    }

    // create a new selection if we don't have one or if we receive new data
    if (!this.selection || (this.data != this.selection.data)) {

        this.createSelectionModel();
    }
    // fire dataChanged here. If the resultset is empty, dataChanged won't do anything
    this.dataChanged();
},

getData : function () {
    return this.data;
},

// getPrimaryKeys() - Returns unique primary keys for a record.
// Use 'comparePrimaryKeys()' to compare against some record.

getPrimaryKeys : function (record) {
    var data = this.data;
    if (!isc.ResultSet || !isc.isA.ResultSet(data)) return record;

    var ds = this.getDataSource(),
        keys = {};
    if (isc.isA.DataSource(ds)) {
        var pkArray = ds.getPrimaryKeyFieldNames();


        for (var i = 0; i < pkArray.length; i++) {
            keys[pkArray[i]] = record[pkArray[i]]
        }
    }

    return keys;
},


// setRecordValues()
// Method to update client-side data in place
// This is called directly by DynamicForms when saving valuess if this is acting as the selection
// component for a form.
setRecordValues : function (pks, values) {

    if (!this.data) return;

    var rowNum = this.data.indexOf(pks);
    if (rowNum == -1) return;
    var record = this.data.get(rowNum);
    isc.combineObjects(record, values);

    this.invalidateUserCache(record);

    // if we have a valuesManager, explicitly notify it about the change
    if (this.valuesManager != null) {
        this.valuesManager._updateMultipleMemberValue(rowNum, null, record, this);
    }

    // refresh display.
    this.logDebug('calling layoutTiles from setRecordValues', "TileGrid");
    this.layoutTiles();
},



// no-op function to be overridden
dataArrived : function (startRecord, endRecord) {
},

dataChanged : function (operationType, originalRecord, rowNum, updateData) {
    this.invalidateUserCache();

    if (!this.data ||
        (isc.ResultSet && isc.isA.ResultSet(this.data) && !this.data.lengthIsKnown()))
    {
        this.logDebug("dataChanged: returning due to no data yet", "TileGrid");
        return;
    }
    // compute tile dimensions here. We need data to have arrived to do this.
    this.computeTileDimensions();
    // track data to help determine if deleting tiles is needed
    if (!this._oldDataLength) this._oldDataLength = 0;

    this.detailViewer.setHilites(this.hilites);

    // the following terms will be used in the subsequent comments:
    // recycle = when recycleTiles is true and tiles are reused
    // incremental = when incremental rendering is occuring, and tiles are drawn on demand, and
    // remain even when scrolled offscreen
    // showall = see tileGrid.showAllRecords

    // Add
    // recycle - layoutTiles() should suffice to pickup any changes resulting from the new
    //      tile in the flow.
    // incremental - layoutTiles() will refresh the visible tiles, and subsequent scrolls should
    //      refresh tiles as well.
    // showAll - since a new tile is added, we may need to reflow a significant number of tiles,
    //      so layoutTiles() should be called, even though its expensive in this case
    if (operationType == "add") {
        this.logDebug("add", "TileGrid");
        this.layoutTiles();
    // Remove
    // recycle - delete the tile if it exists i.e. if it were a visible tile, then layoutTiles()
    // incremental - delete the tile if its been created, then layoutTiles()
    // showall - delete the tile if its been created, then layoutTiles()
    } else if (operationType == "remove") {
        this.logDebug("remove", "TileGrid");
        if ((this.recycleTiles
            && this.data.getLength() < this.getDrawnEndIndex() - this.getDrawnStartIndex() + 1)
            || !this.recycleTiles) {
            // after removing last tile will not be used, but removed tile will be reused
            // and all tiles will be shifted
            var oldTile = this.tiles[this.tiles.length - 1];
            this.tiles.remove(oldTile);
            oldTile.destroy();
        }

        this.layoutTiles();
    // Update
    // recycle, incremental, showall - tried to be smart about this by only redrawing the updated
    // tile, but we lose tile & selection pointers on update, so its better just to layoutTiles()
    } else if (operationType == "update") {
        this.logDebug("update", "TileGrid");
        this.layoutTiles();
    // Filter, sort, etc., and the new data is as long or longer than the old data
    // recycle, incremental, showall - just layoutTiles() should be neccessary, as calling getTile()
    // will create new tiles as needed, and scrolling should take care of updating out-of-sync offscreen
    // tiles.
    } else if (this.data.getLength() >= this._oldDataLength) {
        this.logDebug("filter or sort, new data same or longer", "TileGrid");

        // only trigger animations if we had data before
        if (this._oldDataLength > 0) this._layoutAfterDataChange();
        else this.layoutTiles();

    // Filter, sort, etc., and the new data is shorter than the old data
    // recycle - only delete tiles if new data length < visibleTiles.length, then layoutTiles
    // incremental, showall - delete extra tiles, then layoutTiles
    } else {
        this.logDebug("filter or sort, new data shorter", "TileGrid");
        this.selection.deselectAll();
        // doesn't seem like this is necessary, as we call cleanUpExtraTiles() at the end
        // of tileLayout(), but we'll leave this here for now just in case...
        /*
        if (this.recycleTiles && !this.showAllRecords) {
            var start = this.data.getLength();
            if (start < this.getDrawnEndIndex() - this.getDrawnStartIndex() + 1) {
                this.cleanupExtraTiles(start);
            }
        } else {
            var start = this.data.getLength();
            this.cleanupExtraTiles(start);
        }
        */


        // here we bank on the fact that getDrawnEndIndex returns a cached value from the previous
        // data, since layoutTiles() hasn't been called yet.
        var prevLastDrawnNum = this.getDrawnEndIndex() + 1;
        var newLastDrawnNum = prevLastDrawnNum > this.data.getLength() ? this.data.getLength() :
            prevLastDrawnNum;
        var tpl = this.getTilesPerLine();
        // at first glance it seems like if data.length is less than prevLastDrawnNum, scrolling
        // will always be invalid; however there is the case when data length is smaller, but
        // still produces the same number of lines, in which case the scrollTop is still valid.
        // Also scrollTop == 0 is a special case...always valid
        if (Math.floor(prevLastDrawnNum / tpl) > Math.floor(newLastDrawnNum / tpl)
            && this.getScrollTop() != 0 && this.recycleTiles) {
            this.scrollToTop();
            this.layoutTiles();
        } else {
            this._layoutAfterDataChange();
        }

    }
    // set oldData length now, as next time this method is called, this.data may be new data
    this._oldDataLength = this.data.getLength();
    //isc.logWarn('dataChanged:' + [operationType, originalRecord, rowNum]);
    // need to clean up potential extra tiles when data length is zero (i.e. after a filter that
    // returns no results), because layoutTiles won't actually run in that case.
    if (this.data.getLength() == 0) {
        this.cleanupExtraTiles(0);
    }

},

// helper function to handle tile animations, only called when data operation is not singular
// (not add, update, or remove)
_layoutAfterDataChange : function () {
    if (this.destroying) return;
    if (this.animateTileChange) {

        if (this._animating) {
            var arr = this._animationIDs;
            for (var i = 0; i < arr.length; i++) {
                this.finishAnimation(arr[i].ID);
                arr[i].tile.hide();
            }
            // for now just return - seems to work fine, but if problems arise look into
            // setting a flag that will trigger _layoutAfterDataChange again from _finishAnimating
            return;
        }
        this.fireOnPause("tileGridAnimate", this._animateChange);
    } else {
        this.logDebug('calling layoutTiles from layoutAfterDataChange', "TileGrid");
        this.layoutTiles();
    }
},

cleanupExtraTiles : function (start) {
    var tileArray = this.tiles;
    for (var i = start;i < tileArray.length; i++) {
        var tile = tileArray[i];
        tile.hide();
        // absolutely essential: without this, the tileGrid thinks its scrollable area is
        // larger than it really should be.
        tile.moveTo(0, 0);
    }

},

destroy : function () {
    if (this.data){
        this.ignore(this.data, "dataChanged");
        this.ignore(this.data, "dataArrived");
        // if the data was autoCreated, destroy it to clean up RS<->DS links
        if (this.data._autoCreated && isc.isA.Function(this.data.destroy))
            this.data.destroy();
    }

    this.Super("destroy", arguments);
},

_getTileID : function (tileNum) {
    return this.ID + "_tile_" + tileNum;
},

getLength : function () {
    if (!this.data
        || (isc.ResultSet && isc.isA.ResultSet(this.data) && !this.data.lengthIsKnown())) return 0;
    else return this.data.getLength();
},

makeTile : function (record, tileNum) {

    var props = {
        ID: this._getTileID(tileNum),
        tileNum: tileNum,
        canHover: true,
        //canDragReposition: true,
        handleHover : function () {
            if (this.creator.itemHover) this.creator.fireCallback("itemHover", "item", [this]);
            return this.Super("handleHover", arguments);

        },

        mouseDown : function () {

            this.creator._tileMouseDown(this);
            this.creator.focus();
        },
        rightMouseDown : function () {

            var returnVal = this.creator._tileRightMouseDown(this);
            if (returnVal == false) return false;
            this.creator.focus();
            return returnVal;
        },

        mouseUp : function () {
            this.creator._tileMouseUp(this);
        },
        doubleClick : function () {
            var tileRecord = this.creator.getTileRecord(this);
            return this.creator.recordDoubleClick(this.creator, this, tileRecord);
        }

    };

    if (record.tileProperties) isc.addProperties(props, record.tileProperties);
    var theConstructor = record.tileConstructor ? record.tileConstructor : this.tileConstructor;
    // store new tile in a local var for debug purposes
    var newTile = this.createAutoChild("tile", props, theConstructor);
    //newTile.setWidth(this.getTileWidth());
    //newTile.setHeight(this.getTileHeight());
    // HACK this is neccessary to avoid the tile being sized by its content. Otherwise,
    // the tile will grow past its set width when css borders are used.
    this.detailViewer.setWidth(newTile.getInnerWidth());
    this.detailViewer.setHeight(newTile.getInnerHeight());

    return newTile;
},

setHilites : function (hilites) {
    this.Super("setHilites", arguments);
    this.dataChanged();
},

//> @method tileGrid.getTileHTML()
// When using the default +link{SimpleTile} class as +link{tileGrid.tileConstructor}, this
// method provides the HTML to be displayed within each tile.  See +link{tileGrid.tile}.
//
// @param tileRecord (TileRecord) the tile for which HTML should be retrieved
// @return (HTML) HTML contents for the tile, as a String
// @visibility external
//<
getTileHTML : function (tileRecord) {
    return this.detailViewer.getBlockHTML([tileRecord]);
},

//> @method tileGrid.getTile()
// Returns the tile for the passed record or record index.
// <P>
// Note that this method may be overridden but developers should be aware that this
// method may be called repeatedly for the same record each time the TileGrid refreshes
// that row. If you override this API, you will need to cache and re-use the same
// tile objects per record. Typically this would be achieved by storing a pool of Tile
// objects that are re-used if a Record with the same primaryKey is passed to getTile().
// <P>
// When calling this method directly, if +link{showAllRecords} is false, this may
// return null for records that are not currently visible.
//
// @param tile (TileRecord or int) record or index of record in this.data
// @return (Canvas) tile for this record
//
// @visibility external
//<
getTile : function (tile) {
    var tileID, record, tileIndex;

    if (!this.data) return null;
    if (isc.isAn.Object(tile)) { // record is passed in
        record = tile;
        tileIndex = this.data.indexOf(tile);
        tileID = this.getTileID(tile);
    } else { // index is passed in, get the record

        record = this.data.get(tile);
        if (!record) return null;
        // instead of reusing the tile that the record was pointing to, use the tile that is
        // pointed to by forming an ID from the current index. This creates a smoother reuse of tiles,
        // e.g. when filtering you won't have disjointed tiles reclaimed, but rather tiles
        // will be sequentially reclaimed. With the other approach, when filtering a
        // bottom-scrolled tileGrid we were getting overlapping tiles:
        // tileID = this.getTileID(record);
        tileID = this._getTileID(tile);
        tileIndex = tile;
    }
    // set the tileID here to what it would be if created from scratch, so that it will fall into
    // the second else if branch below and we can cleanly reclaim it if a tile w/ that ID exists
    // Case which spawned this: adding a record to a sorted list with showAllRecords: false
    // overwrote the new tile because of an ID conflict
    if (!tileID) tileID = this._getTileID(tileIndex);

    var recTile = null;
    if (this.canReclaimTile(tileIndex) && !record.tileConstructor) {
        //this._limitLog('recycling tile:' + [tileIndex, record.commonName], "a");
        recTile = this._reclaimTile(tileIndex);
        if (recTile.isA("SimpleTile")) {

            recTile.redraw();
            // check if the record is selected and sync the tile state with that. Since tiles
            // are reclaimed, selection will be out of sync without this check.
            if (this.selection.isSelected(record)) {
                recTile.setSelected(true);
            } else {
                recTile.setSelected(false);
            }
        }
    // if there is a tileID, return the tile. Otherwise, make the new tile
    } else if (tileID && window[tileID]) {
        //this._limitLog('reclaiming existing tile:' + [tileIndex, tileID, record.commonName], "b");
        // pass the actual tile into _reclaimTile, to handle the bookeeping stuff
        recTile = this._reclaimTile(tileIndex, window[tileID]);
        if (recTile.isA("SimpleTile")) {
            // same conditions apply as detailed in comments above regarding the need to redraw and
            // setSelected
            recTile.redraw();
            if (this.selection.isSelected(record)) {
                recTile.setSelected(true);
            } else {
                recTile.setSelected(false);
            }
        }
    } else { // create a new tile
        //this._limitLog('creating new tile:' + [tileIndex,record.commonName], "c");
        var tId = this._getTileID(tileIndex);
        this.setTileID(record, tId);
        recTile = this.makeTile(record, tileIndex);
        if (!recTile.isA("SimpleTile")) {
            if (recTile.setFields && recTile.getFields &&
                    (recTile.getFields() == null || recTile.getFields().length == 0)) {
                recTile.setFields(isc.clone(this.fields));
            }
        }
        // add the created tile to tiles[] so it can be reclaimed
        if (!this.tiles) this.tiles = [];
        this.tiles.add(recTile);
    }

    if (!recTile.isA("SimpleTile")) {
        if (recTile.setValues) {
            recTile.setValues(record);
        } else if (recTile.setData) {
            recTile.setData([record]);
        } else {
            recTile.record = record;
        }
    }
    return recTile;
},

//> @method TileGrid.getTileIndex()
// Returns the index of the specified tile.
// @param tile (Canvas) Tile you want to get the index for
// @return (int) index of the tile in this tileGrid. Will return -1 if the specified
// tile is not displayed within this grid.
// @visibility external
//<
getTileIndex : function (tile) {
    return this.tiles.indexOf(tile);
},

//> @method TileGrid.getCurrentTile()
// Returns the tile currently under the mouse.
// @return (SimpleTile) the tile currently under the mouse
// @visibility external
//<
getCurrentTile : function () {
    return this.detailViewer.currentTile;
},

// debug functions to limit the number of logwarns produced in long loops
_logs: [],
_logLimit: 10,
_clearLogs : function () {this._logs = [];},
_limitLog : function (message, key) {
    if (!this._logs.find("key", key)) {
        this._logs.add({key:key, logs:this._logLimit});
    }
    if (this._logs.find("key", key).logs > 0) {
        isc.logWarn(message);
        this._logs.find("key", key).logs -= 1;
    }
},

layoutTiles : function () {
    this.requestVisibleRows();
    this.computeTileDimensions();
    //this._clearLogs();
    this.invokeSuper(isc.TileGrid, "layoutTiles");
    // in the case of scrolling to the end of the list when recycling tiles, its possible that
    // there will be leftover tiles from a previous call to layoutTiles(). These need to be hidden
    // or else they will be superimposed on the newer tiles and interfere with selection.
    // Theres also a case (non specific to recycling tiles) where tiles from before a filter
    // are superimposed on the new tiles, left over from the animation. Clean those up here.
    var tilesLen = this.tiles ? this.tiles.length : 0;
    var visLen = this._numTilesProcessed;
 //isc.logWarn('laying out tiles:' + [visLen, this._animating]);
    if (!this._animating && visLen < tilesLen) this.cleanupExtraTiles(visLen);
},

invalidateCache : function () {
    this.Super("invalidateCache", arguments);
    this.requestVisibleRows();
},

requestVisibleRows : function () {
    var data = this.data;
    if (data == null || !isc.isA.ResultSet(data)) return;
    // Kick off an initial fetch if the data length is unknown [has never fetched].
    // This is a no-op if the length is known. In that case we allow
    // 'hasAllVisibleTiles', called from logic in TileLayout.js, to fetch as-yet-unloaded
    // individual records required for each tile.
    if (!data.lengthIsKnown()) {
        // if we don't have a set tileSize, just fetch the first record
        if (this.tileSize == null) data.get(0);
        else {
            var tileRange = this.getVisibleTiles();
            data.getRange(tileRange[0], tileRange[1]);
        }
    }
},

// Tile reclamation
// whenever a new tile is created, it is added to this.tiles. When a tile is reclaimed, the index
// of the record it represents is mapped to this.tiles, and that tile is returned.
_reclaimTile : function (tileIndex, tile) {
    var record = this.data.get(tileIndex), reclaimedTile;
    //this._limitLog('_reclaimTile:' + [tileIndex, tile, this.getDrawnStartIndex()], "e");
    if (!tile) {
        // we're guaranteed to have some tiles by the call to canReclaimTile()
        // this.tiles stores only enough tiles to fill the viewport, so we need to map the passed
        // tile index to a reclaimed index by subtracting the drawn start index
        var index = tileIndex - this.getDrawnStartIndex();

        reclaimedTile = this.tiles[index];
    } else {
        reclaimedTile = tile;
    }
    // remove the pointer from the reclaimed tiles' current record to itself, if the record exists.
    // this is important for record<->tile integrity. If we don't do this, its possible to have
    // more than one record pointing to the same tile, which we don't want for obvious reasons
    var oldRec = this.getTileRecord(reclaimedTile);
    if (oldRec) this.setTileID(oldRec, null);
    // set up record -> tile pointer
    this.setTileID(record, reclaimedTile.ID);
    // store tileNum for selection
    reclaimedTile.tileNum = tileIndex;

    return reclaimedTile;
},

canReclaimTile : function (index) {
    var startIndex = this.getDrawnStartIndex() || 0;
    if (this.recycleTiles && this.tiles && this.tiles.length > index - startIndex) {
        //this._limitLog('canReclaimTile:' + [this.tiles.length, index, startIndex], "d");
        return true;
    } else {
        return false;
    }
},

_tileRightMouseDown : function (tile) {
    var tileRecord = this.getTileRecord(tile);
    if (this.recordContextClick != null) {
        if (this.recordContextClick(this, tile, tileRecord) == false) return false;
    }
    // allow selection on mouse down
    return this._tileMouseDown(tile);
},

_tileMouseDown : function (tile) {
    var tileRecord = this.getTileRecord(tile);

    if (tileRecord) this.selection.selectOnMouseDown(this, tile.tileNum);

    this.recordClick(this, tile, tileRecord);

    // check that the tile is scrolled into view
    // scrolled off the top edge
    var xPos, yPos;
    if (tile.getTop() <  this.getScrollTop()) {
        yPos = "top";
    // scrolled off the bottom edge
    } else if (tile.getTop() + tile.getVisibleHeight() > this.getScrollTop() + this.getInnerHeight()) {
        yPos = "bottom";
    }
    // scrolled off the left edge
    if (tile.getLeft() < this.getScrollLeft()) {
        xPos = "left";
    // scrolled off the right edge
    } else if (tile.getLeft() + tile.getVisibleWidth() > this.getScrollLeft() + this.getInnerWidth()) {
        xPos = "right";
    }
    // if there is some portion of the tile offscreen, scroll it into view
    if (xPos || yPos) {
        this.scrollIntoView(tile.getLeft(), tile.getTop(), tile.getVisibleWidth(),
           tile.getVisibleHeight(), xPos, yPos, true);
    }
},

_tileMouseUp : function (tile) {
    this.selection.selectOnMouseUp(this, tile.tileNum);
},

//>    @method    tileGrid.recordClick()
// Executed when the tileGrid receives a 'click' event on a
// tile. The default implementation does nothing -- override to perform some action
// when any record is clicked.<br>
// A record event handler can be specified either as
// a function to execute, or as a string of script to evaluate. If the handler is defined
// as a string of script, all the parameters below will be available as variables for use
// in the script.<br>
// If you want to cancel the click based on the parameters, return false. Otherwise, return
// true so that the click event be registered with the tile.
//
// @group    events
//
// @param viewer (TileGrid) the TileGrid itself
// @param tile (Canvas) the tile that was clicked on
// @param record (TileRecord) the record that was clicked on
//
// @example tilingEditing
// @visibility external
//<
recordClick : function () {
    return true;
},

//>    @method    tileGrid.recordDoubleClick()
// Executed when the tileGrid receives a 'doubleclick' event on a
// tile. The default implementation does nothing -- override to perform some action
// when any record is doubleclicked.<br>
// A record event handler can be specified either as
// a function to execute, or as a string of script to evaluate. If the handler is defined
// as a string of script, all the parameters below will be available as variables for use
// in the script.<br>
// If you want to cancel the doubleclick based on the parameters, return false. Otherwise, return
// true so that the doubleclick event be registered with the tile.
//
// @group    events
//
// @param viewer (TileGrid) the TileGrid itself
// @param tile (Canvas) the tile that was doubleclicked on
// @param record (TileRecord) the record that was doubleclicked on
//
// @example tilingEditing
// @visibility external
//<
recordDoubleClick : function () {
    return true;
},

//>    @method    tileGrid.recordContextClick()
// Executed when the tileGrid receives a context-click (right mouse button) event on a
// tile. The default implementation does nothing -- override to perform some action
// when any record is right-clicked.<br>
// <smartclient>Return <code>false</code> to cancel the native behavior (suppressing
// the browser context menu).</smartclient>
// <smartgwt>Cancel the event to suppress the native browser context menu.</smartgwt>
// <P>
// A record event handler can be specified either as
// a function to execute, or as a string of script to evaluate. If the handler is defined
// as a string of script, all the parameters below will be available as variables for use
// in the script.<br>
// If you want to cancel the click based on the parameters, return false. Otherwise, return
// true so that the click event be registered with the tile.
//
// @group    events
//
// @param viewer (TileGrid) the TileGrid itself
// @param tile (Canvas) the tile that was clicked on
// @param record (TileRecord) the record that was clicked on
// @return (boolean) return false to suppress the native browser context menu.
//
// @visibility external
//<
recordContextClick : function () {
},

// Selection
// --------------------------------------------------------------------------------------------

//> @method    tileGrid.selectionChanged() ([A])
// Called when selection changes within this tileGrid. Note this method fires for
// each record for which selection is modified - so when a user clicks inside a tileGrid this
// method will typically fire twice (once for the old record being deselected, and once for
// the new record being selected).
//
// @param    record  (Record)    record for which selection changed
// @param    state   (boolean)    New selection state (true for selected, false for unselected)
// @group selection
// @visibility external
//<

// DONE-make sure to change _rowSelectionChanged in GridRenderer, also change _cellSelectionChanged
// called from within the selection object via the target property
selectionChange : function (record, state) {
    //isc.logWarn('selectionChange:' + [record.title, this.getTileID(record), state]);
    // call user-defined handler and bail (don't hilite rows) if it returns false
    if (this.selectionChanged && (this.selectionChanged(record, state) == false)) return false;

    // refresh the affected records to visually indicate selection
    var selection = this.selection,
        lastItem = selection.lastSelectionItem;

    var selTile = window[this.getTileID(lastItem)];
    if (selTile && selTile.setSelected) {
        selTile.setSelected(state);
    }

},

_$ArrowUp:"Arrow_Up", _$ArrowDown:"Arrow_Down",
_$ArrowLeft:"Arrow_Left", _$ArrowRight:"Arrow_Right",
// This method provides for arrow-key handling in TileGrid without interfering with normal
// Canvas keyPress handling or user overrides of keyPress()
widgetHandleKeyPress : function (event, eventInfo) {
    // don't let keypresses happen until we're done animating
    if (this.isAnimating("scroll")) return false;
    var lastItem = this.selection.lastSelectionItem;
    if (!lastItem) return;
    var keyName = event.keyName,
        lastItemIndex = this.selection.data.indexOf(lastItem),
        isHoriz = this.orientation == "horizontal",
        newIndex
    ;
    if (keyName == this._$ArrowUp) {
        newIndex = isHoriz ? this._adjacentTileIndex(lastItemIndex, "above")
                            : lastItemIndex - 1;
    } else if (keyName == this._$ArrowDown) {
        newIndex = isHoriz ? this._adjacentTileIndex(lastItemIndex, "below")
                            : lastItemIndex + 1;

    } else if (keyName == this._$ArrowLeft) {
        newIndex = isHoriz ? lastItemIndex - 1 :
                   this._adjacentTileIndex(lastItemIndex, "above");
    } else if (keyName == this._$ArrowRight) {
        newIndex = isHoriz ? lastItemIndex + 1 :
                   this._adjacentTileIndex(lastItemIndex, "below");
    } else {
        return;
    }

    // need the data length check for when we're at the end and adding 1 to the index above
    if (newIndex == -1 || newIndex > this.data.getLength() - 1) return;
    // prevent errors getting thrown from multiple keypresses + scroll animation that make it
    // through the cracks (between the isAnimating check and layoutTiles completing)
    if (newIndex == null) return false;
    var newRec = this.selection.data.get(newIndex),
        newTile = window[this.getTileID(newRec)];
    if (newTile) {
        this._tileMouseDown(newTile);

    }

    return false;
},

// get the tile that is most adjacent to the passed in tile (via startIndex).
_adjacentTileIndex : function (startIndex, direction) {
    // first find the next line from the currently selected tile, in the passed in direction
    var data = this.selection.data,
        lineIndex = startIndex,
        startTile = window[this.getTileID(data.get(lineIndex))],
        isHoriz = this.orientation == "horizontal",
        startTileLengthPos = isHoriz ? startTile.getTop() : startTile.getLeft(),
        startBreadthPos = isHoriz ? startTile.getLeft() : startTile.getTop(),
        startBreadth = isHoriz ? startTile.getVisibleWidth() : startTile.getVisibleHeight(),
        currTile = startTile
    ;
    //lineIndex = direction == "above" ? lineIndex - 1 : lineIndex + 1;
    //var currTile = window[data.get(lineIndex)._tileID];
    while (startTileLengthPos == (isHoriz ? currTile.getTop() : currTile.getLeft())) {
        lineIndex = direction == "above" ? lineIndex - 1 : lineIndex + 1;
        // last row special cases: if we're at an edge row, return -1 to trigger a cancel
        // of the selection
        if (lineIndex < 0 || lineIndex > data.getLength() - 1) {
            return -1;
        }
        currTile = window[this.getTileID(data.get(lineIndex))];
        // prevent any errors when an arrow key is pressed right when a scroll animation ends
        // but before layoutTiles has completed
        if (!currTile) return -1;
    }
    // find the tile that is most adjacent to the start tile
    // this may be tricky for different tile sizes, so we have to iterate the data looking
    // for the most adjacent tile
    var linePos = isHoriz ? currTile.getTop() : currTile.getLeft();
    var bestMatchIndex = -1, bestMatchPixels = 0;
    while ((isHoriz ? currTile.getTop() : currTile.getLeft()) == linePos) {
        // get the number of pixels that the current tile overlaps the start tile by, and keep
        // track of it if its the max so far
        var currBreadthPos = isHoriz ? currTile.getLeft() : currTile.getTop(),
            currBreadth = isHoriz ? currTile.getVisibleWidth() : currTile.getVisibleHeight(),
            range = this._getCommonRange([startBreadthPos, startBreadthPos + startBreadth],
                                          [currBreadthPos, currBreadthPos + currBreadth])
        ;
        if (range > bestMatchPixels) {
            bestMatchIndex = lineIndex;
            bestMatchPixels = range;
        }
        lineIndex = direction == "above" ? lineIndex - 1 : lineIndex + 1;
        // last row special cases: don't do anything if we're already on an edge row
        if (lineIndex < 0 || lineIndex > data.getLength() - 1) break;
        currTile = window[this.getTileID(data.get(lineIndex))];
        // special case for recycled tiles: the next row may not exist,  which will cause the
        // while loop check to throw an error.
        if (!currTile) break;

    }

    return bestMatchIndex;
},

// first and second are arrays in the form of: [range start, range end]
// what will be returned is how much of second is within first.
_getCommonRange : function(first, second) {
    // make sure there is a actually an intersection
    if ((second[0] >= first[0] && second[0] <= first[1])
        || (second[1] >= first[0] && second[1] <= first[1])
        || (second[0] <= first[0] && second[1] >= first[1]))
    {
        // get the first range start or second range start, whichever is greater
        var start = second[0] > first[0] ? second[0] : first[0];
        // get the first range end or second range end, whichever is less
        var end = second[1] > first[1] ? first[1] : second[1];
        // the difference between the start and end is the intersection
        return end - start;
    }
    return 0;
},

//> @method tileGrid.addTile()
// This is not allowed for tileGrid. Instead, use +link{tileGrid.addData}.
//
// @visibility external
//<
addTile : function () {
     return false;
},

//> @method tileGrid.removeTile()
// This is not allowed for tileGrid. Instead, use +link{tileGrid.removeData}.
//
// @visibility external
//<
removeTile : function () {
    return false;
},

//> @method tileGrid.addData()
// @include dataBoundComponent.addData()
// @group dataBoundComponentMethods
// @visibility external
// @example databoundAdd
//<

//> @method tileGrid.removeData()
// @include dataBoundComponent.removeData()
// @group dataBoundComponentMethods
// @visibility external
// @example databoundRemove
//<

getRecordTile : function (recordIndex) {
    if (recordIndex == null) return null;
    // avoid logWarn.
    // maybe should consider making that logwarn (out of bounds get()) a logDebug.
    if (!this.data) return null;
    if (recordIndex >= this.data.getLength()) return null;
    var tId = this.getTileID(this.data.get(recordIndex));
    if (!tId) return null;
    else return window[tId];
},

childVisibilityChanged : function (child, newVisibility) {
    // skip the tileLayout implementation of this method
    this.invokeSuper(isc.TileLayout, "childVisibilityChanged", child, newVisibility);
},

// @method tileGrid.hasAllVisibleTiles()
// @param range (array) data range to check for
// @param fetch (boolean) should we fetch the range if not present
//<
hasAllVisibleTiles : function (range, fetch) {
    if (isc.isA.ResultSet(this.data)) {

        // new dataset load in progress
        if (!this.data.lengthIsKnown()) return false;

        var rangeEnd = range[1] + 1;
        if (rangeEnd > this.data.getLength()) rangeEnd = this.data.getLength();
        if (this.data.rangeIsLoaded(range[0], rangeEnd)) {
            return true;
        } else {
            if (fetch) {
                this.logDebug("in hasAllVisibleTiles, fetching range: " + range[0] +
                              " to " + rangeEnd + ", total length: " + this.data.getLength(),
                             "TileGrid");
                this.data.getRange(range[0], rangeEnd);
            }
            //isc.logWarn('data loading, returning:' + [range[0], range[1]]);
            return false;
        }

    } else {
        return true;
    }
},

// --------------------------Drag and Drop-----------------------------------------------------
dragAppearance:isc.EH.TRACKER,
dragTrackerMode: "title",

_setUpDragProperties : function () {

    // set up our specific drag-and-drop properties

    // set these properties before we set canDrag internally. If canDrag has
    // been set by the user, we want these props to be set as well.
    this.canReorderTiles = (this.canDrag || this.canReorderTiles);
    this.canDragTilesOut = (this.canDrag || this.canDragTilesOut);
    // like above, if canAcceptDrop is explicitly set, make sure that canAcceptDroppedRecords
    // is also set to keep backwards compatability
    this.canAcceptDroppedRecords = (this.canAcceptDrop || this.canAcceptDroppedRecords)

    this.canDrag = (this.canDrag || this.canDragTilesOut || this.canReorderTiles);
    //this.canDrop = (this.canDrop || this.canDragRecordsOut || this.canReorderRecords);
    this.canAcceptDrop = (this.canAcceptDrop || this.canAcceptDroppedRecords || this.canReorderTiles);

},

//>    @attr    TileGrid.tileDragAppearance        (DragAppearance : isc.EventHandler.TRACKER : IRWA)
// Visual appearance to show when the tile is being dragged.
// @visibility external
// @see Canvas.dragAppearance
// @group dragdrop
//<
tileDragAppearance: isc.EH.TRACKER,

//>    @attr tileGrid.canReorderTiles (Boolean : false : [IRW])
// Indicates whether tiles can be reordered by dragging within this tileGrid.
// @visibility external
// @setter setCanReorderTiles()
// @group dragging
//<
//>    @method tileGrid.setCanReorderTiles()
// Setter for +link{tileGrid.canReorderTiles}.
// @visibility external
// @group dragging
//<
setCanReorderTiles : function (canReorderTiles) {
    this.canReorderTiles = canReorderTiles;
    this._setUpDragProperties();
},

//>    @attr tileGrid.canDragTilesOut (Boolean : false : [IRW])
// Indicates whether tiles can be dragged from this tileGrid and dropped elsewhere.
// @visibility external
// @setter setCanDragTilesOut()
// @group dragging
//<
//>    @method tileGrid.setCanDragTilesOut()
// Setter for +link{tileGrid.canDragTilesOut}.
// @visibility external
// @group dragging
//<
setCanDragTilesOut : function (canDragTilesOut) {
    this.canDragTilesOut = canDragTilesOut;
    this._setUpDragProperties();
},

//>    @attr tileGrid.canAcceptDroppedRecords (Boolean : false : [IRW])
// Indicates whether records can be dropped into this TileGrid.
// @visibility external
// @group dragging
// @setter setCanAcceptDroppedRecords()
// @example dragListMove
//<
//>    @method tileGrid.setCanAcceptDroppedRecords()
// Setter for +link{tileGrid.canAcceptDroppedRecords}.
// @visibility external
// @group dragging
//<
setCanAcceptDroppedRecords : function (canAcceptDroppedRecords) {
    this.canAcceptDroppedRecords = canAcceptDroppedRecords;
    this._setUpDragProperties();
},

//> @method tileGrid.setDragTracker()
// @include dataBoundComponent.setDragTracker()
// @visibility external
//<

//> @method tileGrid.getDragTrackerProperties()
// @include dataBoundComponent.getDragTrackerProperties()
//<

//> @attr tileGrid.dragTrackerStyle (CSSStyleName : "gridDragTracker" : IRW)
// @include dataBoundComponent.dragTrackerStyle
//<

//> @method tileGrid.getTitleField()
// @include dataBoundComponent.getTitleField()
//<

//> @method tileGrid.getDragTrackerTitle()
// Return "title" HTML to display as a drag tracker when the user drags some record.<br>
// Default implementation will display the cell value for the title field (see
// +link{listGrid.getTitleField()}) for the record(s) being dragged (including any
// icons / custom formatting / styling, etc).
// <p>
// Note: Only called if +link{listGrid.dragTrackerMode} is set to <code>"title"</code>.
// @param record (ListGridRecord) First selected record being dragged
// @param rowNum (number) row index of first record being dragged
// @return (string) Title for the row. Default implementation looks at the value of the
//                  title-field cell for the row.
// @group dragTracker
// @visibility external
//<
getDragTrackerTitle : function (record) {
    //if (this.tileDragAppearance == "tracker") {
    var titleField = this.getTitleField(),
        value = record[titleField];
    //if (!value) value = record[0];
    return "<nobr>" + value + "</nobr>";

},

//>    @method    tileGrid.drop()    (A)
//            handle a drop event
//        @return    (boolean)    true if the list can't reorder or dragging did not begin from the list body;
//                            false if disabled, no selection, or otherwise
//        @group    events, dragging
//<
drop : function () {
    var index = this._lastDropIndex || 0;
    // the check below fixes an issue that occurs when dropping multiple tiles by dragging from
    // an empty area of the grid. _lastDropIndex would get set to current data length
    // (see tileLayout.showDragLineForRecord()), but when the tiles were dropped and removed,
    // this index is no longer valid.
    if (index > this.data.getLength()) index = 0;
    var source = this.ns.EH.dragTarget;

    var dragStartIndex = this._dragStartIndex;
    // reset _dragStartIndex so the next drag will start over
    this._dragStartIndex = null;
    // don't check willAcceptDrop() this is essentially a parallel mechanism, so the developer
    // shouldn't have to set that property directly.

    var dropRecords = source.cloneDragData();

    var targetRecord = this.data.get(index);

    this.transferRecords(dropRecords, targetRecord, index, source);

},

dropMove: function () {
    // if the TileGrid can't be reordered, bail
    if (!this.canReorderTiles) return true;
    // bail on drops from foreign widgets if not configured to accept foreign drops
    if (!this.canAcceptDroppedRecords && isc.EH.dragTarget != this) return true;

    this.showDragLineForRecord();

},

dragMove : function () {
    // if you're not allowed to drag tiles out, cancel dragging over anything that isn't this
    // grid
    var dropTarget = isc.EH.dropTarget;
    if (!this.canDragTilesOut && dropTarget != null && dropTarget != this) {
        return false;
    }
    var record = this.getSelectedRecord();
    if (this.tileDragAppearance == "outline") {
        var EH = this.ns.EH;
        var tId = this.getTileID(record);

        var tile = window[tId];

        //EH.dragMoveTarget = EH.getDragOutline(tile);
        //if (!EH.dragMoveAction) EH.dragMoveAction = EH._moveDragMoveTarget;
        var trackerHTML = "<div style='width:" + tile.getVisibleWidth() +
                            ";height:" + tile.getVisibleHeight() + "'>" +
                            EH.getDragOutline(tile).getInnerHTML() + "</div>";
        EH.setDragTracker(trackerHTML);
    } else if (this.tileDragAppearance == "target") {
        var EH = this.ns.EH;
        var tId = this.getTileID(record);
        // create the tracker html by wrapping the tile innerHTML in a div
        // that creates a fixed size boundary for the tile html
        // The approach of just using the tile itself as the dragMoveTarget
        // didn't work well because the tile was stuck inside of its parent
        // and wouldn't follow the mouse outside of this tilegrid.
        var tile = window[tId];
        this.detailViewer.clipValues = false;
        var trackerHTML = "<div style='width:" + tile.getVisibleWidth() +
                            ";height:" + tile.getVisibleHeight() + "'>" +
                            tile.getInnerHTML() + "</div>";
        this.detailViewer.clipValues = true;

        EH.setDragTracker(trackerHTML);
        // hide the tile to simulate what it would look like if the tile itself
        // were the dragMoveTarget
        tile.hide();
        this._hiddenDragTile = tile;

        if (!EH.dragMoveAction)  EH.dragMoveAction = EH._moveDragMoveTarget;

        // If the canvas wants to show a shadow on drag, show it now.
        if (EH.dragTarget.showDragShadow) EH._showTargetDragShadow();

        // If the canvas should change opacity on drag, handle this now.
        if (EH.dragTarget.dragOpacity != null) EH._setTargetDragOpacity();
    }
},

willAcceptDrop : function () {

    var EH = this.ns.EH;
    if (!this.Super("willAcceptDrop",arguments)) return false;
    var theTarget = EH.dragTarget;

    //isc.logWarn('willAcceptDrop:' + theTarget);
    if (theTarget == this) {
        // Bail if we're attempting to drag records within TileGrid, and we can't reorder
        if (!this.canReorderTiles) return false;
    } else {
        // Bail if we're attempting to drag from elsewhere and canAcceptDroppedRecords is false
        // This gives us the granularity to allow drag reording (which will always set
        // canAcceptDrop to true, see _setUpDragProperties), but disable dropping from
        // external sources
        if (!this.canAcceptDroppedRecords) return false;
    }

    // if the 'getDragData' for the dragTarget doesn't give us a suitable object (Array or
    // Object), bail
    if (!isc.isAn.Object(theTarget.getDragData())) return false;

    return true;
},

dragStop : function () {
    this.Super("dropOut",arguments);
    // clean up potentially hidden tile. This happens when tileDragAppearance is
    // 'target'; we set _hiddenDragTile to point to the tile being dragged, because
    // that tile is manually hidden to properly simulate the 'target' appearance.
    // This means we have to manually show() it again after drag completes, if the
    // drop is not allowed.
    if (this._hiddenDragTile) {
        this._hiddenDragTile.show();
        this._hiddenDragTile = null;
    }
},

//>    @method    tileGrid.transferDragData()
// @include dataBoundComponent.transferDragData()
//<

//>    @method    tileGrid.getDragData()
// @include dataBoundComponent.getDragData()
//<

//>    @attr    tileGrid.dragDataAction
// @include dataBoundComponent.dragDataAction
//<

//> @method tileGrid.transferSelectedData()
// @include dataBoundComponent.transferSelectedData()
//<

// Formula/Summary builder required methods
getCellValue : function (record, field) {
    return this.detailViewer.getStandaloneFieldValue(record, field[this.fieldIdProperty]);
},

// DBC level override to call local getCellValue implementation - Formula/Summary builders
getStandaloneFieldValue : function (record, fieldName) {
    var value = this.getCellValue(record, this.getField(fieldName));
    return value;
},

// Formula/summary -related overrides from DBC
getTitleFieldValue : function (record) {
    var ds = this.getDataSource(),
        titleField = ds.getTitleField(),
        title = this.getCellValue(record, ds.getField(titleField));

    return title;
},

// basic show and hide methods
hideField : function (fieldName) {
    this.getField(fieldName).showIf = "false";
    this.getField(fieldName).hidden = true;
    this.fieldStateChanged();
},
showField : function (fieldName) {
    this.getField(fieldName).showIf = "true";
    this.getField(fieldName).hidden = false;
    this.fieldStateChanged();
},

//>    @method    tileGrid.getField()    (A)
//            return a field by fieldName
//
//        @return    (DetailViewerField) requested field or null
//<
getField : function (fieldId) {
    if (!this.fields) return null;
    return isc.Class.getArrayItem(fieldId, this.fields, this.fieldIdProperty);
},
getFields : function () {
    return this.fields;
},
getAllFields : function () {
    return this.fields;
},

// ---------------------------------------------------------------------------------------
// FieldState
//

//>    @method    tileGrid.setFieldState()
// Sets some presentation properties (visibility, width, userFormula and userSummary) of the
// grid fields based on the +link{type:ListGridFieldState} object passed in.<br>
// Used to restore previous state retrieved from the grid by a call to +link{tileGrid.getFieldState()}.
//
// @group viewState
// @param fieldState (ListGridFieldState) state to apply to the grid's fields.
// @visibility external
// @see tileGrid.getFieldState()
//<
setFieldState : function (fieldState) {
    if (isc.isA.String(fieldState)) fieldState = this.evalViewState(fieldState, "fieldState")
    if (fieldState) {
        this.completeFields = this._setFieldState(fieldState);
        this.setFields(fieldState);
        this.markForRedraw();
        this.fieldStateChanged();
    }
},

//>    @method    tileGrid.getFieldState()
// Returns a snapshot of the current presentation of this grid's fields as
// a +link{type:ListGridFieldState} object.
// <P>
// This object can be passed to +link{tileGrid.setFieldState()} to reset this grid's fields to
// the current state.
// <P>
// Note that the information stored includes the current width and visibility of each of this
// grid's fields.
//
// @return (ListGridFieldState) current state of this grid's fields.
// @group viewState
// @see tileGrid.setFieldState();
// @visibility external
//<


// ----------------------------------------------------------------------------
// panelHeader related methods
// ----------------------------------------------------------------------------
// panelHeader related methods

showActionInPanel : function (action) {
    // specifically add the "sort" action, which is not added by default
    if (action.name == "sort") return true;
    return this.Super("showActionInPanel", arguments);
},

// ---------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// printing
// ---------------------------------------------------------------------------
getPrintHTML : function () {
    // new dataset load in progress
    if (!this.data.lengthIsKnown()) {
        isc.logWarn("Attempt to print TileGrid " + this.ID + " while data is loading will be ignored");
        return "";
    }

    var len = this.data.getLength();
    // bail if we are trying to print a partially loaded resultset
    if (!this.data.rangeIsLoaded(0, len)) {
        isc.logWarn("Make sure all data is loaded before attempting to print " +
            "TileGrid: " + this.ID);
        return "";
    }
    var table;
    var tpl = this.printTilesPerLine ? this.printTilesPerLine : this.getTilesPerLine();
    if (this.orientation == "horizontal") {
        var width = this.getInnerWidth();
        table = "<table width='" + width + "'>";

        for (var i = 0; i < len; i++) {
            var currTile = this.getTile(i);
            // for every time we lay out <tilesPerLine> tiles, create a new row
            if (i % tpl == 0) {
                // first row
                if (i == 0) table += "<tr>";
                // middle row with tiles left
                else if (i < len - 1) table += "</tr><tr>";

            }
            table += "<td>" + currTile.getPrintHTML() + "</td>";
        }
        table += "</tr></table>";
    } else {
        //var height = this.getInnerHeight();
        table = "<table>";
        // number of rows is determined by tpl
        for (var i = 0; i < tpl; i++) {
            table += "<tr>";
            // for each row, layout tiles by skipping every tpl number of tiles,
            // starting at the current row number
            for (var j = i; j < len; j += tpl ) {
                var currTile = this.getTile(j);
                table += "<td>" + currTile.getPrintHTML() + "</td>"
            }
            table += "</tr>";
        }
        table += "</table>";
    }
    return table;
}

});

isc.ClassFactory.defineClass("SimpleTile", "StatefulCanvas");

//> @class SimpleTile
// Default class used by a +link{TileGrid} to render each tile.  See +link{tileGrid.tile}.
// <P>
// SimpleTiles should not be created directly, instead, use a TileGrid and provide data and
// SimpleTile instances are created for you.
//
// @treeLocation Client Reference/Grids/TileGrid
// @visibility external
//<
isc.SimpleTile.addProperties({
    //> @attr simpleTile.baseStyle (CSSClassName : "simpleTile" : IR)
    // CSS style for the tile as a whole.  As with +link{StatefulCanvas.baseStyle}, suffixes
    // are appended to this style to represent various states ("Over", "Selected", etc).
    //
    // @visibility external
    //<
    baseStyle: "simpleTile",

    overflow:"hidden",

    showRollOver: true,

    redrawOnStateChange: true,

    _redrawWithParent: false,

    initWidget: function () {
        this.invokeSuper(isc.SimpleTile, "initWidget", arguments);
        this.showDown = this.creator.valuesShowDown;
    },
    //> @method simpleTile.getInnerHTML()
    // The default implementation will call +link{tileGrid.getTileHTML()}.
    // @return (HTML) HTML contents for the tile, as a String
    // @visibility external
    //<
    getInnerHTML : function () {
        this.creator.detailViewer.currentTile = this;
        var tileRec = this.creator.getTileRecord(this);
        if (!tileRec) return null;
        return this.creator.getTileHTML(tileRec);
    },

    //> @attr simpleTile.creator (TileGrid : null : IR)
    // The +link{TileGrid} that created this SimpleTile.
    // @visibility external
    //<

    //> @method simpleTile.getRecord()
    // Return the record that this tile should render.
    // <P>
    // NOTE: a TileGrid that is doing data paging may reuse tiles with different records, so a
    // subclass of SimpleTile should not cache the record returned by getRecord().
    // <P>
    // @return (TileRecord) the TileRecord associated with this tile
    // @visibility external
    //<
    getRecord : function () {
        return this.creator.getTileRecord(this);
    }

});

isc.TileGrid.registerStringMethods({

    //> @method tileGrid.dataArrived() (A)
    // Notification method fired when new data arrives from the server to be displayed in this
    // tileGrid, (for example in response to the user scrolling a new set of tiles into view).
    // Only applies to databound tileGrid where the +link{tileGrid.data,data} attribute is a
    // +link{ResultSet}.
    // This method is fired directly in
    // response to +link{ResultSet.dataArrived(),dataArrived()} firing on the data object.
    // @param startRecord (int) starting index of the newly loaded set of records
    // @param endRecord (int) ending index of the newly loaded set of records (non inclusive).
    // @visibility external
    //<
    dataArrived:"startRecord,endRecord",

    selectionChanged:"record,state",

    itemHover : "item",
    itemClick : "item",
    recordClick : "viewer,tile,record",
    recordDoubleClick : "viewer,tile,record",
    recordContextClick: "viewer,tile,record",
    fieldStateChanged : ""
});





//>    @class ColumnTree
//
// The SmartClient system supports hierarchical data (also referred to as tree data
// due to its "branching" organization) with:
// <ul>
//   <li> the +link{class:Tree} class, which manipulates hierarchical data sets
//   <li> the TreeGrid widget class, which extends the ListGrid class to visually
//        present tree data in an expandable/collapsible format.
//   <li> the ColumnTree widget class, which visually
//        presents tree data in a so-called
//        "<a href=http://en.wikipedia.org/wiki/Miller_Columns>Miller Column</a>" format.
// </ul>
// For information on DataBinding Trees, see +link{group:treeDataBinding}.
// <p>
// A ColumnTree shows a single branch of the underlying +link{class:Tree} horizontally, from
// left to right. Thus, the leftmost column shows all the top-level nodes. When the user clicks
// one of those nodes, a new column is shown immediately to the right of the top-level column,
// showing the selected node's children. Additional columns are shown as required to present
// lower-level children. The behavior of ColumnTree is similar to that of the Browser interface
// in the Apple&trade; iTunes&trade; application.
//
// @implements DataBoundComponent
// @treeLocation Client Reference/Grids
// @visibility external
//<

// define us as a subclass of Layout
isc.ClassFactory.defineClass("ColumnTree", "Layout", "DataBoundComponent");

isc.ColumnTree.addClassProperties({

    //=    @const    ColumnTree.TREE_FIELD        default field to display a ColumnTree's nodes
    TREE_FIELD : {
        // Arbitrary name - ListGrids expect their fields to be identified by a name, even
        // though in this case we're not representing a field in the record objects
        name:"treeField",
        width:"*",
        getCellValue : function (list,record,recordNum,colNum) {
            return list.creator.getCellValue(list, record, recordNum, colNum);
        }
    }

});

isc.ColumnTree.addProperties({
    orientation: "horizontal",

    animateMemberEffect:{effect:"slide", startFrom:"L", endAt:"R"},

    //>    @attr    columnTree.dataSource        (DataSource or ID : null : IRW)
    // @include dataBoundComponent.dataSource
    //<

    //>    @attr columnTree.data (Tree : null : IRW)
    // A +link{class:Tree} object consisting of nested +link{object:TreeNode}s to
    // display in this ColumnTree.
    // The <code>data</code> property will typically not be explicitly specified for
    // databound ColumnTrees, where the data is returned from the server via databound component
    // methods such as <code>fetchData()</code>
    // @setter setData()
    // @group data
    // @visibility external
    //<

    //> @attr columnTree.dataProperties (Tree : null : IR)
    // For a <code>ColumnTree</code> that uses a DataSource, these properties will be passed to
    // the automatically-created ResultTree.  This can be used for various customizations such as
    // modifying the automatically-chosen +link{tree.parentIdField}.
    // @group databinding
    // @visibility external
    //<

    //> @method columnTree.fetchData()
    // Uses a "fetch" operation on the current +link{DataSource,grid.dataSource} to retrieve data
    // that matches the provided criteria, and displays the matching data in this component as a
    // tree.
    // <P>
    // This method will create a +link{ResultTree} to manage tree data, which will
    // subsequently be available as <code>columnTree.data</code>.  DataSource records
    // returned by the "fetch" operation are linked into a tree structure according to
    // +link{dataSourceField.primaryKey,primaryKey} and
    // +link{dataSourceField.foreignKey,foreignKey} declarations on DataSource fields.  See the
    // +link{group:treeDataBinding} topic for complete details.
    // <P>
    // By default, the created ResultTree will use folder-by-folder load on demand, asking the
    // server for the children of each folder as the user opens it.
    // <P>
    // The +link{ResultTree} created by <code>fetchData()</code> can be customized by setting
    // +link{columnTree.dataProperties} to an Object containing properties and methods to apply to
    // the created ResultTree.  For example, the property that determines whether a node is a
    // folder (+link{Tree.isFolderProperty,isFolderProperty}) can be customized, or
    // level-by-level loading can be disabled via
    // +link{resultTree.loadDataOnDemand,loadDataOnDemand:false}.
    // <P>
    // The callback passed to <code>fetchData</code> will fire once, the first time that data is
    // loaded from the server.  If using folder-by-folder load on demand, use the
    // +link{resultTree.dataArrived()} notification to be notified each time new nodes are loaded.
    // <P>
    // Note that, if criteria are passed to <code>fetchData()</code>, they will be passed every
    // time a new "fetch" operation is sent to the server.  This allows you to retrieve multiple
    // different tree structures from the same DataSource.  However note that the server is expected
    // to always respond with an intact tree - returned nodes which do not have parents are dropped
    // from the dataset and not displayed.
    //
    // @include dataBoundComponent.fetchData()
    // @group dataBoundComponentMethods
    // @visibility external
    //<

    //>    @attr   columnTree.autoFetchData       (boolean : false : IR)
    // @include dataBoundComponent.autoFetchData
    // @group databinding
    // @visibility external
    //<

    //>    @attr columnTree.autoFetchTextMatchStyle       (TextMatchStyle : null : IR)
    // @include dataBoundComponent.autoFetchTextMatchStyle
    // @group databinding
    // @visibility external
    //<

    //> @method columnTree.filterData()
    // @include dataBoundComponent.filterData()
    // @group dataBoundComponentMethods
    // @visibility external
    //<

    //> @attr columnTree.initialCriteria   (Criteria : null :IR)
    // @include dataBoundComponent.initialCriteria
    // @visibility external
    //<

    //>    @attr columnTree.showDetailFields (Boolean : true : IR)
    // @include dataBoundComponent.showDetailFields
    //<

    //> @attr columnTree.dataFetchMode (FetchMode : "paged" : IRW)
    // @include dataBoundComponent.dataFetchMode
    //<



    //>    @attr    columnTree.folderIcon        (SCImgURL : "[SKIN]folder.gif" : [IRW])
    // The URL of the base icon for all folder nodes in this columnTree. Note that this URL will
    // have +link{openIconSuffix} or +link{closedIconSuffix} appended to
    // indicate state changes if appropriate - see documentation on  +link{showOpenIcons}
    // @group treeIcons
    // @visibility external
    //<
    folderIcon:"[SKIN]/folder.gif",

    //> @attr   columnTree.customIconProperty   (String : "icon" : [IRW])
    // This property allows the developer to customize the icon displayed next to a node.
    // Set <code>node[grid.customIconProperty]</code> to the URL of the desired icon to display and
    // it will be shown instead of the standard +link{nodeIcon} for this node.<br>
    // Note that if +link{showCustomIconOpen}
    // is true for this grid, customized icons for folder nodes will be appended with the
    // +link{openIconSuffix} suffix on state change,
    // as with the standard +link{folderIcon}.  Also note that for
    // custom folder icons, the +link{closedIconSuffix} will never be appended.
    // @group treeIcons
    // @visibility external
    //<
    customIconProperty:"icon",

    //>    @attr    columnTree.skinImgDir        (URL : "images/TreeGrid/" : IRWA)
    //        Where do 'skin' images (those provided with the class) live?
    //        This is local to the Page.skinDir. By default the ColumnTree shares icons with
    //      the TreeGrid class.
    //        @group    appearance, images
    //<
    skinImgDir:"images/TreeGrid/",

    //>    @attr   columnTree.nodeIcon       (SCImgURL : "[SKIN]file.gif" : [IRW])
    // @include treeGrid.nodeIcon
    // @group treeIcons
    // @example millerColumns
    // @visibility external
    //<
    nodeIcon: "[SKIN]file.gif",

    //>    @attr   columnTree.openIconSuffix   (String : "open" : [IRW])
    // @include treeGrid.openIconSuffix
    // @group treeIcons
    // @visibility external
    //<
    openIconSuffix: "open",

    //>    @attr   columnTree.closedIconSuffix   (String : "closed" : [IRW])
    // @include treeGrid.closedIconSuffix
    // @group treeIcons
    // @visibility external
    //<
    closedIconSuffix: "closed",

    //>    @attr   columnTree.showOpenIcons   (Boolean : true : [IRW])
    // @include treeGrid.showOpenIcons
    // @group treeIcons
    // @example millerColumns
    // @visibility external
    //<
    showOpenIcons: true,

    //> @attr   columnTree.showCustomIconOpen   (Boolean : false : [IRWA])
    // @include treeGrid.showCustomIconOpen
    // @group treeIcons
    // @visibility external
    //<
    showCustomIconOpen:false,

    //> @attr   columnTree.customIconOpenProperty (string : "showOpenIcon" : [IRWA])
    // @include treeGrid.customIconOpenProperty
    // @group treeIcons
    // @visibility external
    //<
    customIconOpenProperty:"showOpenIcon",

    //> @attr columnTree.showMultipleColumns   (boolean : null : [IRW])
    // When set to false, only displays a single column at a time, showing a slide animation
    // when moving between columns.
    // @group treeIcons
    // @visibility external
    //<
    //showMultipleColumns: true,

    //>    @attr   columnTree.loadDataOnDemand    (boolean : null : IR)
    // For databound columnTree instances, should the entire tree of data be loaded on initial
    // fetch, or should each column be loaded as needed. If unset the default
    // ResultTree.loadDataOnDemand setting will be used.
    // @group databinding
    // @visibility external
    //<

    //> @attr columnTree.column (MultiAutoChild ListGrid : null : IR)
    // Instance of ListGrid used to display each column of the tree.
    // @visibility external
    //<

    // column is an autoChild so columnConstructor / defaults / properties are all implied.
    // Explicitly doc columnProperties to make this an obvious override point, and to
    // have it be picked up by SGWT
    //> @attr columnTree.columnProperties (ListGrid properties : null : IRA)
    // Standard set of properties to apply to each generated +link{columnTree.column,column}
    // in this columnTree. Developers may also override +link{columnTree.getColumnProperties()}
    // to return dynamic properties based on the node being displayed.
    // @visibility external
    //<

    showColumn:true,
    columnConstructor:"ListGrid",

    columnDefaults: {

        animateTime: 100,
        animateEffect: "slide",

        // disable canAddFormulaField / canAddSummaryField
        canAddFormulaFields:false,
        canAddSummaryFields:false,

        canSort:false,
        canGroupBy:false,
        showHeaderMenuButton:false,
        selectionChanged: function (record, state) {
            if (state) {
               this.creator.nodeSelected(this, record);
            }
        },
        bodyProperties: {
            _updateCellStyle : function(record, rowNum, colNum, cell, className) {
                var zeroBorderPadding = "padding:0px;border:0px;";
                if (cell == null) cell = this.getTableElement(rowNum, colNum);
                if (cell == null) return; // cell not currently drawn

                if (!this.showHiliteInCells)
                {
                    if (record == null) record = this.getCellRecord(rowNum, colNum);
                    // determine the CSS style className if not provided
                    if (className == null) className = this.getCellStyle(record, rowNum, colNum);
                    // There will be a clip-div around the table.
                    // In IE the NOBR also counts as a node.
                    var table = cell.firstChild;
                    while (table && table.tagName != "TABLE") table = table.firstChild;
                    if (table) {
                        table.className = className;
                        if (this.getCellCSSText) {
                            // Use this._getCompleteCellCSSText
                            // This handles the overflow settings for Moz, converting the
                            // getCellCSSText stringMethod to a method, etc.
                            cell.style.cssText = isc.StringBuffer.concat(
                                                    this._getCompleteCellCSSText(
                                                       record, rowNum, colNum, className),
                                                    this.zeroBorderPadding);
                        }
                    }
                }

                // Actually style the cell itself
                return isc.GridRenderer.getPrototype()._updateCellStyle.apply(
                                                this, [record, rowNum, colNum, cell, className]);
            }
        }
    },

    //> @attr columnTree.fields (Array of ListGridField : null : IRW)
    // An array of field objects, specifying the order, layout, dynamic calculation, and
    // sorting behavior of each field in each column in the columnTree object. In ColumnTrees,
    // the fields array specifies sub-columns within each main column.
    // Each field in the fields array is a ListGridField object.
    // <p>
    // If +link{ColumnTree.dataSource} is also set, this value acts as a set of overrides as
    // explained in +link{attr:DataBoundComponent.fields}.
    //
    // @group databinding
    // @see ListGridField
    // @visibility external
    //<

    //> @attr columnTree.showHeaders (Boolean : false : IR)
    // If set, each column in the ColumnTree will show a header with the title of the selected
    // node from the column to the left.
    // @visibility external
    //<
    showHeaders: false,

    //> @attr columnTree.firstColumnTitle (String : " " : IR)
    // A title for the leftmost column if +link{showHeaders} is set (the remaining columns
    // have their titles derived from the item selected in the column to the left). Ignored
    // if +link{showHeaders} is not set.<br><br>
    // Note: if you do not want a heading for the first column leave this attribute at its
    // default value of " ". If you set it to null or the empty string, SmartClient will
    // fall back to displaying the field's name in the heading.
    // @visibility external
    //<
    firstColumnTitle: "&nbsp;",

    //> @attr columnTree.showNodeCount (Boolean : false : IR)
    // If set, and +link{showHeaders} is also set, each column's header will show
    // a count of the number of nodes in that column
    // @visibility external
    //<
    showNodeCount: false,

    // don't wrap, as that will mess up the look of the trees
    wrapCells: false,

    // iconPadding - padding between the folder open/close icon and text.
    // Make this customizable, but not exposed - very unlikely to be modified
    iconPadding: 3,


    ignoreEmptyCriteria: false,

    //>    @attr columnTree.backButtonTitle (String : "Back" : [IRW])
    // When using +link{columnTree.showMultipleColumns, single-column mode}, this i18n property
    // dictates the title for the +link{columnTree.backButton, button} docked to the top left
    // which allows navigation back through the column tree.
    // @group i18nMessages
    // @visibility external
    //<
    backButtonTitle: "Back",

    //> @attr columnTree.backButton (AutoChild IButton : null : [IRW])
    // When using +link{columnTree.showMultipleColumns, single-column mode}, this is the
    // "Back" button that you see hovering above the column UI and that allows backward
    // navigation.
    // @visibility external
    //<
    backButtonDefaults: {
        _constructor: "IButton",
        snapTo: "TR",
        left: 5,
        top: 5,
        autoFit: true,
        click : function () {
            this.creator.navigateBack();
        }
    },

    overflow: "hidden"

});

isc.ColumnTree.addMethods({

// Can't do this with a columnDefaults attribute because we need to clone the fields
getDynamicDefaults : function(autoChildName) {

    if (autoChildName == "column") {   // That's our only autoChild at the moment
        return {
            autoDraw: false,
            showHiliteInCells: true,
            leaveScrollbarGap: false,
            selectionType: "single",
            showHeader: false,
            fields: isc.clone(this.fields)
        };
    }

},

initWidget : function () {
    this.Super("initWidget", arguments);

    // default showMultipleColumns to false if it's unset and we're running on a handset
    if (this.showMultipleColumns == null)
        this.showMultipleColumns = !isc.Browser.isHandset;

    this.columns = [];

    // if no dataSource is specified, pick up the dataSource off the data model
    if (!this.dataSource && this.data != null && this.data.dataSource) {
        this.dataSource = this.data.dataSource;
    }

    // if the fields are not set or of zero length, initialize with a single TREE_FIELD
    if (!this.fields || this.fields.length == 0) {
        this.fields = [isc.ColumnTree.TREE_FIELD];
    }

    if (this.showMultipleColumns == false && this.showHeaders && this.showBackButton != false) {
        this.backButton = this.createAutoChild("backButton", { title: this.backButtonTitle, disabled: true });
        this.addChild(this.backButton);
        this.backButton.bringToFront();
    }

    // Show the first column (the direct children of root in the underlying tree)
    this.columns[0] = this.createAutoChild("column",
        this.getColumnProperties(this.data ? this.data.getRoot() : null, 0), null, false);
    this.addColumn(this.columns[0], 0);

    this.currentColumn = 0;

    if (this.data) this.populateFirstColumn();

},

populateFirstColumn : function () {
    if (this.data.showRoot) {
        this.columns[0].setData([this.data.getRoot()]);
    } else {
        var children = this.data.getChildren(this.data.getRoot());
        if (isc.isA.ResultSet(children)) {
            children = children.getAllLoadedRows();
        }
        this.columns[0].setData(children);
    }

    if (this.shouldShowHeader(null, 0)) {
        this.columns[0].setShowHeader(true);
        this.columns[0].setFieldProperties(0, {title: this.getColumnTitle(null, 0)});
    }
},

// Return true when this column should not affect columns to its right.
// Useful for "non-tree" columns that should not close other columns when selected.
treeIsTied : function (column, node) {
    return false;
},

//> @method columnTree.getSelectedRecord()
// Get the selected record, that is, the parent of the nodes in the rightmost visible column.
// <P>
// This is generally the most recently clicked node unless programmatic navigation has taken
// place.
// <P>
// If only the first column is showing, the root node is returned (which can be detected via
// +link{Tree.isRoot()}).
//
// @return (Record) the selected record
// @visibility external
//<
getSelectedRecord : function () {
    if (this.currentColumn <= 0) return this.data.getRoot();
    var column = this.getColumn(this.currentColumn-1);
    return column.getSelectedRecord();
},

//> @method columnTree.navigateBack()
// Navigate to the previous column.
//
// @visibility external
//<
navigateBack : function () {
    if (this.currentColumn <= 0) return;

    // nodeSelected takes the column that was clicked and the record that was clicked in that
    // column.  Navigating backward is the same as if a record was just clicked *2* levels
    // back.  In other words, if you are in column 1 (two levels showing), column 1 will go
    // away, column 0 will be the only column visible, so it's as though the root node was just
    // selected.
    var column, record;
    if (this.currentColumn >= 2 ) {
        column = this.getColumn(this.currentColumn-2);
        record = column.getSelectedRecord();
    } else {
        column = this.getColumn(0);
        record = this.data.getRoot();
    }

    this.logInfo("navigating to column: " + (this.currentColumn-1) +
                 " to node: " + this.data.getTitle(record));

    this.nodeSelected(column, record, true);
},


slideTransition : function (oldPane, newPane, container, right) {
    if (!isc.Browser.isWebKit) {
        if (right) {
            newPane.deselectAllRecords();
            newPane.animateShow();
        } else {
            oldPane.animateHide();
            newPane.show();
        }
        newPane.bringToFront();

        return;
    }

    this.logInfo((right ? "right" : "left") + " slideTransition from: " +
                 oldPane + " to " + newPane + " within " + container);

    // draw the new pane hidden
    newPane.hide();
    container.addChild(newPane);
    if (!newPane.isDrawn()) newPane.draw();

    var oldStyle = oldPane.getStyleHandle();
    var newStyle = newPane.getStyleHandle();

    // place the new element offscreen right (instantly)
    newStyle.setProperty("-webkit-transition", "none");
    var translation = "translate3d(" + (right ? "-" : "") + container.getViewportWidth() + "px, 0%, 0%)";
    newStyle.setProperty("-webkit-transform", translation);

    var oldOverflow = container.overflow;
    container.setOverflow("hidden");
    // will be initially invisible since clipped
    newPane.show();


    isc.Timer.setTimeout(function () {

        // set both to animate
        oldStyle.setProperty("-webkit-transition", "-webkit-transform 0.3s ease-in-out");
        newStyle.setProperty("-webkit-transition", "-webkit-transform 0.3s ease-in-out");

        // move old offscreen
        translation = "translate3d(" + (right ? "" : "-") + container.getViewportWidth() + "px, 0%, 0%)";
        oldStyle.setProperty("-webkit-transform", translation);

        // undo translation on new
        newStyle.setProperty("-webkit-transform", "translate3d(0px, 0%, 0%)");

        isc.Timer.setTimeout(function () {
            oldPane.hide();
            container.setOverflow(oldOverflow);
        }, 350);

    }, 0);
},

//> @method columnTree.nodeSelected()
// Called when a node is selected in any column.  Default behavior is to show the next level
// of the tree in a column to the right of the current column.
// <P>
// The new column will be created if it is not already showing.  Any columns further to the
// right, showing deeper levels of the tree, will be removed.
// @param column [ListGrid] the column where a node was selected
// @param node [TreeNode] the node that was selected
// @return (boolean) override and return false to cancel the default action
// @visibility external
//<
nodeSelected : function (column, node, backward) {
    // Give the 'onNodeSelected' handler an opportunity to suppress default handling if present

    if (this.onNodeSelected != null && (this.onNodeSelected(column,node) == false)) {
        return;
    }

    var idx = this.getColumnIndex(node),
        isFolder = this.data.isFolder(node);

    // Hide columns to the right of the column immediately to the right of the one we clicked.
    // The column immediately to the right of the one we clicked is going to be repopulated
    // with the clicked node's children, and possibly a new heading; anything further to
    // the right is no longer relevant.
    var nextColumnIdx = idx + 1;

    if (!isFolder) nextColumnIdx -= 1;
    var nextColumn = this.columns[nextColumnIdx];
    if (!this.treeIsTied(column, node)) {
        if (this.showMultipleColumns != false) this.hideColumnsToRight(nextColumnIdx);

        if (!isFolder) return;

        this.data.openFolder(node);

        // Create or re-use a list grid, as appropriate
        if (isc.isA.ListGrid(nextColumn)) {

            nextColumn.deselectAllRecords();
            var children = this.data.getChildren(node);
            if (isc.isA.ResultSet(children)) {
                children = children.getAllLoadedRows();
            }
            nextColumn.setData(children);
            this.addColumn(nextColumn, nextColumnIdx);
        } else {
            nextColumn = this.columns[nextColumnIdx] = this.createAutoChild("column",
                this.getColumnProperties(node, idx+1), null, false);
            var children = this.data.getChildren(node);
            if (isc.isA.ResultSet(children)) {
                children = children.getAllLoadedRows();
            }
            nextColumn.setData(children);
            this.addColumn(nextColumn, nextColumnIdx);
        }

        // Fix up column headings
        if (this.shouldShowHeader(node, nextColumnIdx)) {
            nextColumn.setShowHeader(true);
            var newTitle = this.getColumnTitle(node, nextColumnIdx);
            nextColumn.setFieldProperties(0, {title: newTitle});
        }

        // If the data is already locally cached, add the node count (if required)
        // This will be done asynchronously if we need a data fetch here
        if (nextColumn.data.getLength() > 0) {
            this.updateHeadingNodeCount(node);
        }
    }

    //var columnToHide = (backward ? nextColumn : column);
    //var columnToShow = (backward ? column : nextColumn);

    var columnToHide = (backward ? this.columns[this.currentColumn] : column);
    var columnToShow = nextColumn; //(backward ? column : nextColumn);

    if (this.showMultipleColumns == false) {
        this.slideTransition(columnToHide, columnToShow, this, backward ? true : false);
    } else {
        columnToShow.show();
    }

    this.currentColumn = (nextColumnIdx < 0 ? 0 : nextColumnIdx);

    this.logInfo("currentColumn is now: " + this.currentColumn);
    if (this.backButton) {
        //this.columns[nextColumn].addChild(this.backButton);
        this.backButton.bringToFront();
        this.backButton.setDisabled(this.currentColumn <=0);
    }

},



addColumn : function (column, index) {
    if (this.showMultipleColumns == false) {
        column.resizeTo("100%", "100%");
        this.addChild(column, index);
    } else {
        this.addMember(column, index);
    }
},


getCurrentTitle : function () {
    return this.columns[this.currentColumn].getFieldTitle(0);
},

getPreviousTitle : function () {
    if (this.currentColumn <= 0) return "";
    return this.columns[this.currentColumn - 1].getFieldTitle(0);
},

updateHeadingNodeCount : function (parentNode) {

    var idx = this.getColumnIndex(parentNode);
    if (!this.shouldShowHeader(parentNode, idx) || !this.showNodeCount) return;

    if (idx < 0) return;  // Node count is not applicable to the first column
    if (this.columns[idx+1].data.getLength() == 0) return;

    var newTitle = this.data.getTitle(parentNode);
    if (this.showNodeCount) {
        newTitle = newTitle + " (" + this.columns[idx+1].data.getLength() + ")";
    }
    this.columns[idx+1].setFieldProperties(0, {title: newTitle});

},

getColumnIndex : function (treeNode) {
    if (this.data.showRoot) {
        return this.data.getLevel(treeNode);
    } else {
        var level = this.data.getLevel(treeNode);
        //return level - (level==0 ? 0 : 1);
        return level-1;
    }
},

hideColumnsToRight : function (idx) {
    for (var i = idx+1; i < this.columns.length; i++) {
        this.columns[i].hide();
        this.columns[i].deselectAllRecords();
    }
},


//> @method columnTree.shouldShowHeader()
// Whether the indicated column should show a header.  Returns this.showHeaders by default,
// override for different behavior.
//
// @param node (TreeNode) parent node for the nodes to be shown in the column
// @param colNum (int) index of the column
// @visibility external
//<

shouldShowHeader : function (node, colNum) {
    return this.showHeaders;
},

//> @method columnTree.getColumnTitle()
// Returns the title to show for the header of indicated column.  Only called if
// +link{shouldShowHeader()} returns true for this column.
// <P>
// By default, returns +link{firstColumnTitle} for the first column, and for subsequent
// columns, the result of +link{Tree.getTitle(),this.data.getTitle()} called on the
// <code>node</code> passed to this function.
//
// @param node (TreeNode) parent node for the nodes to be shown in the column
// @param colNum (int) index of the column
// @visibility external
//<

getColumnTitle : function (node, colNum) {
    if (colNum == 0) {
        return this.firstColumnTitle;
    } else {
        return this.data.getTitle(node);
    }
},

//> @method columnTree.getRecord()
// Retrieve a record by index.
// <P>
// If <code>colNum</code> is passed, returns the record found in that column at that index,
// or null if the column doesn't exist or the index is too high.
// <P>
// With no <code>colNum</code> parameter, a record's index is it's position counting from the
// first record of the first column and including all records in each column. Note that both
// index and colNum are zero-based - so the first column is column 0, not column 1.
//
// @param index (int) index of record to return.
// @param [colNum] (Integer) optional index of the column
// @return (TreeNode) node at the specified index
// @visibility external
//<

getRecord : function (index, colNum) {
    if (index == null || index < 0) return null;

    if (colNum != null) {
        if (colNum < 0 || colNum > this.columns.length) {
            return null;
        }
        if (index > this.columns[colNum].data.length ||
            !this.columns[colNum].isVisible()) {
            return null;
        }
        return this.columns[colNum].data[index];
    }

    var count = 0;
    for (var idx = 0; idx < this.columns.length; idx++) {
        if (!this.columns[idx].isVisible()) continue;
        if (count + this.columns[idx].data.length > index) {
            return this.columns[idx].data[index-count];
        }
        count += this.columns[idx].data.length;
    }

    return null;
},

// These HTML generation functions were copied from TreeGrid and are very similar.
// This code needs to be factored out of both ColumnTree and TreeGrid, and placed in ListGrid

_$treeCellTemplate:[
    "<table cellpadding=0 cellspacing=0 class='",       // [0]
    ,                                                   // [1] - this.getCellStyle()
    "' style='",                                        // [2]
    ,                                                   // [3] - get.getCellCSSText()

    "border:0px;padding:0px;'><tr><td>",                // [4]
    ,                                                   // [5] - indentHTML
    "</td>",


    "<td>" + (isc.Browser.isSafari || isc.Browser.isIE ? "<nobr>" : ""), // [6],                                             // [6]
    ,                                                   // [7] - opener icon HTML
    ,                                                   // [8] - 'extra' icon if there is one
    ,                                                   // [9] - icon for item (eg folder/file icon)
    (isc.Browser.isSafari ? "</nobr>" : "") +
        "</td><td style='padding-left:",                // [10]
    ,                                                   // [11] - this.iconPadding
    "px;'>",                                            // [12]
    ,                                                   // [13] - NOBR or null
    ,                                                   // [14] - value
    "</td>",
    "</tr></table>"
],

getTreeCellValue : function (value, list, record, recordNum, fieldNum) {

    // This returns HTML to achieve
    //  - open / close icon
    //  - an optional additional icon
    //  - Folder / Record icon
    //  - title for the cell.
    // (It differs from the equiv. function in TreeGrid in that it doesn't add
    //  an indent dependent on level in the tree)
    // If passed a null record just return the value passed in.
    if (record == null) {
       return value;
    }

    var template = this._$treeCellTemplate;

    template[1] = list.getCellStyle(record, recordNum, fieldNum);
    template[3] = list.getCellCSSText(record, recordNum, fieldNum);

    // Get the HTML for the icons and title from _getTreeCellTitleArray(), and fold them
    // into our template
    var titleCellTemplate = this._getTreeCellTitleArray(value, record, recordNum, true);
    for (var i = 0; i < 10; i++) {
        template[6+i] = titleCellTemplate[i];
    }
    return template.join(isc.emptyString);
},

// _getTreeCellTitleArray() - helper method for getTreeCellValue() to return the
// "title" portion of the treeCell value - that is: the icons and the title, without
// any indent

_$treeCellTitleTemplate:[

    "<td>" + (isc.Browser.isSafari || isc.Browser.isIE ? "<nobr>" : ""), // [0]
    ,                                                   // [1] - opener icon HTML
    ,                                                   // [2] - 'extra' icon if there is one
    ,                                                   // [3] - icon for item (eg folder/file icon)
    (isc.Browser.isSafari ? "</nobr>" : "") +
        "</td><td style='padding-left:",                // [4]
    ,                                                   // [5] - this.iconPadding
    "px;'>",                                            // [6]
    ,                                                   // [7] - NOBR or null
    ,                                                   // [8] - value
    "</td>"
],
_getTreeCellTitleArray : function (value, record, recordNum, showOpener) {

    var template = this._$treeCellTitleTemplate;
//    if (showOpener) {
        // opener icon (or small indent)
//        var openIcon = this.getOpenIcon(record),
//            openIconSize = this.openerIconSize || (this.showConnectors ? this.cellHeight : null),
//            openerID = (recordNum != null ? this._openIconIDPrefix+recordNum : null);

//        if (openIcon) {
//            template[1] = this.getIconHTML(openIcon, openerID, openIconSize);
//        } else {
//            template[1] = this._indentHTML(openIconSize || this.iconSize);
//        }
//    } else
      template[1] = null;

//    var extraIcon = this.getExtraIcon(record),
//        extraIconID = (recordNum != null ? this._extraIconIDPrefix+recordNum : null),
      var icon = this.getIcon(record),
          iconID = (recordNum != null ? this._iconIDPrefix+recordNum : null);

    // extra icon if there is one
//    template[2] = (extraIcon ? this.getIconHTML(extraIcon, extraIconID) : null);
    template[2] = null;
    // folder or file icon
    template[3] = this.getIconHTML(icon, iconID, record.iconSize);

    template[5] = this.iconPadding;
    template[7] = this.wrapCells ? null : "<NOBR>"
    template[8] = value;
    return template;
},


// Override getCellValue() to return custom HTML for the tree-field
// Note: Developers are always advised to override formatCellValue rather than this method
// directly (which could lead to certain conflicts).
getCellValue : function (list, record, rowNum, colNum, a, b, c, d) {
    var value = this.getNodeTitle(record, rowNum);
    value = this.getTreeCellValue(value, list, record, rowNum, colNum);
    return value;
},

//>    @method    columnTree.getIcon()
// @include treeGrid.getIcon
// @visibility external
//<
getIcon : function (node, defaultState) {
    if (isc.isA.Number(node)) node = this.data.get(node);
    if (!node) return null;

    var icon = node[this.customIconProperty],
        customIcon = (icon != null),
        isFolder = this.data.isFolder(node);

    if (!customIcon) {
        if (isFolder) icon = this.folderIcon;
        else icon = this.nodeIcon;
    }
    var state;
    if (isFolder) {
        // Default folder icon is the 'closed' icon. This will be used for dragTrackers, etc
        // Note: check for the special _willAcceptDrop flag set by updateDropFolder() - when a
        // user hovers over a folder for a while, we spring it open, and that causes a redraw,
        // but the folder is not necessarily droppable.
        var isDrop = defaultState ? false : (this.lastDropFolder == node && node._willAcceptDrop),
            isOpen = defaultState ? false : !!this.data.isOpen(node);

        if (isDrop) {
            // backCompat - respect old dropIcon / folderDropImage if specified
            if (node.dropIcon != null) icon = node.dropIcon;
            else if (!customIcon && this.folderDropImage != null) icon = this.folderDropImage;
            else {
                var showDrop;
                if (customIcon) {
                    showDrop = node[this.customIconDropProperty];
                    if (showDrop == null) showDrop = this.showCustomIconDrop;
                } else {
                    showDrop = this.showDropIcons;
                }
                if (showDrop) state = this.dropIconSuffix;
            }
        } else if (isOpen) {

            // backCompat - respect old openIcon / folderOpenImage if specified
            if (node.openedIcon != null) icon = node.openedIcon;
            else if (!customIcon && this.folderOpenImage != null) icon = this.folderOpenImage;
            // Don't override already set drop state
            else {
                var showOpen;
                if (customIcon) {
                    showOpen = node[this.customIconOpenProperty];
                    if (showOpen == null) showOpen = this.showCustomIconOpen;
                } else {
                    showOpen = this.showOpenIcons;
                }
                if (showOpen) state = this.openIconSuffix;

                else if (!customIcon) state = this.closedIconSuffix;
            }
        }  else {

            // Respect old 'folderClosedImage' if specified
            // Otherwise - if the icon is not custom, append "_closed" state

            if (!customIcon) {
                if (this.folderClosedImage) icon = this.folderClosedImage;
                else state = this.closedIconSuffix;
            }
        }
    // not a folder:
    } else {
        // Pick up the old 'fileImage' for back compat, if specified.
        if (!customIcon && this.fileImage) icon = this.fileImage;
    }
    return isc.Img.urlForState(icon, false, false, state);
},

// cut/paste from TreeGrid - in TG, this was doing some caching, but the existence of the
// cache depended on a method called bodyDrawing() running, which I don't have because of my
// different inheritance stack.
_$absMiddle: "absmiddle",
_imgParams: {},
getIconHTML : function (icon, iconID, iconSize) {
    if (icon == null) return isc.emptyString;

    if (iconSize == null) iconSize = this.iconSize;


    var imgParams = this._imgParams;
    imgParams.src = icon;
    imgParams.width = imgParams.height = iconSize;
    imgParams.name = iconID;
    imgParams.align = this._$absMiddle;
    var template = this._getImgHTMLTemplate(imgParams);

    // Note: We need to update the image ID for each icon - this is in the 16'th slot in the
    // array of strings used as a template (see Canvas.imgHTML())
    template[16] = iconID;

    return template.join(isc._emptyString);
},

//> @method columnTree.getNodeTitle()
//
// Returns the title to show for a node in the ColumnTree.  The default implementation
// returns the result of calling +link{method:Tree.getTitle} on the node.
// <br><br>
// You can override this method to return a custom title for nodes in the tree.
//
// @param node (TreeNode) The node for which the title is being requested.
// @param recordNum (Number) The index of the node.
// @param field (DSField) The field for which the title is being requested.
//
// @return (HTML) the title to display.
//
// @see method:Tree.getTitle
//
// @visibility external
//<
getNodeTitle : function (record, recordNum, field) {

    return this.data.getTitle(record);
},


//>    @method    columnTree.getData()
// Returns the +link{class:Tree} object this ColumnTree is viewing
//
// @visibility external
//<
getData : function () {
    return this.data;
},

//>    @method    columnTree.setData()
// Set the +link{class:Tree} object this ColumnTree will view
//
// @param newData (Tree) Tree to show
// @visibility external
//<
setData : function (newData,a,b,c) {

    if (!isc.isA.Tree(newData)) return;

    this.data = newData;

    this.data.columnTree = this;
    this.data.dataArrived = "this.columnTree.updateHeadingNodeCount(parentNode);";

    // Set the `separateFolders` and `showRoot` options of the tree as well.
    this.data.setSeparateFolders(this.separateFolders);

    if (this.showRoot && isc.isA.ResultTree(this.data)) {
        this.logWarn("showRoot may not be set with a databound columnTree, unexpected " +
                     "results may occur");
    }
    this.data.setShowRoot(this.showRoot);

    // Should we show only branches or leaves.
    this.data.setOpenDisplayNodeType(this.displayNodeType);

    // Initiate the component
    this.data.openFolder(this.data.root);
    // If we have any other columns open, hide them now.

    this.hideColumnsToRight(0);
    this.populateFirstColumn();
},


// Override the normal data model implementations, because we want a ResultTree...
useExistingDataModel : function (criteria, operation, context) {
    return false;  // ColumnTree always builds a new tree
},

createDataModel : function (criteria, operation, context) {
    return this.createResultTree(criteria, context.afterFlowCallback, context, null);
},

updateDataModel : function (criteria, operation, context) {
   // return this.createResultTree(criteria, context.callback, context, null);
},

//> @method columnTree.getColumn() [A]
// Advanced API - get the ListGrid representing the indicated column.
// @param column (int or TreeNode) column number, or parent node of the nodes shown in the
// column
// @return (ListGrid) ListGrid that renders the indicated column, or null if column is not
// shown
// @visibility external
//<
getColumn : function (col) {
    if (isc.isAn.Object(col)) { // assume a TreeNode
        var idx = this.getColumnIndex(col) + 1;
        if (this.columns[idx] && this.columns[idx].isVisible()) return this.columns[idx];
    } else {
       if (this.columns[col] && col <= this.currentColumn) return this.columns[col];
    }
    return null;
},

//> @method columnTree.getColumnProperties() [A]
// Additional properties to apply to the ListGrid that will show the indicated column.
// Returns nothing by default. This method can be overridden to allow, for example, different
// styling, icons, or row heights per column.
// @param node (TreeNode) parent node for the nodes to be shown in the column
// @param colNum (int) index of the column
// @return (ListGrid Properties) properties to be applied to the column
// @visibility external
//<
getColumnProperties : function (node, colNum) {

},

// Selection
// --------------------------------------------------------------------------------------------

// Simple helper methods to avoid having to refer directly to this.selection
// Actually implemented in DataBoundComponent

//> @method columnTree.selectRecord()
//
// Select/deselect a +link{Record} passed in explicitly, or by index.
//
// @param record (Record | number) record (or row number) to select
// @param [newState] (boolean) new selection state (if null, defaults to true)
// @param [colNum] (number) Column number
//
// @group selection
// @visibility external
//<

//> @method columnTree.deselectRecord()
//
// Deselect a +link{Record} passed in explicitly, or by index.
// <P>
// Synonym for <code>selectRecord(record, false)</code>
//
// @param record (Record | number) record (or row number) to deselect
// @param [colNum] (number) Column number
//
// @group selection
// @visibility external
//<

//> @method columnTree.selectRecords()
//
// Select/deselect a list of +link{Record}s passed in explicitly, or by index.
//
// @param records (Array of Record | numbers) records (or row numbers) to select
// @param [newState]  (boolean) new selection state (if null, defaults to true)
// @param [colNum] (number) Column number
//
// @group selection
// @visibility external
//<

//> @method columnTree.deselectRecords()
//
// Deselect a list of +link{Record}s passed in explicitly, or by index.
// <P>
// Synonym for <code>selectRecords(records, false)</code>
//
// @param records (Array of Record | numbers) records (or row numbers) to deselect
// @param [colNum] (number) Column number
//
// @group selection
// @visibility external
//<

//> @method columnTree.selectAllRecords()
// Select all records in the supplied column (the first column if none is passed)
//
// @param [colNum] (number) Column number
// @group selection
// @visibility external
//<
selectAllRecords : function (colNum) {
    if (colNum == null) colNum = 0;
    if (!this.columns[colNum]) return;
    this.columns[colNum].selectAllRecords();
},

//> @method columnTree.deselectAllRecords()
// Deselect all records in the supplied column (the first column if none is passed)
//
// @param [colNum] (number) Column number
// @group selection
// @visibility external
//<
deselectAllRecords : function (colNum) {
    if (colNum == null) colNum = 0;
    if (!this.columns[colNum]) return;
    this.columns[colNum].deselectAllRecords();
},

//> @method columnTree.anySelected()
// Whether at least one item is selected in the supplied column (the first column if none is passed)
//
// @param [colNum] (number) Column number
// @return (Boolean) true == at least one item is selected in the supplied column,
// false == nothing at all is selected in the supplied column (note that there may be selections
// in other columns in this columnTree)
// @group selection
// @visibility external
//<
anySelected : function (colNum) {
    if (colNum == null) colNum = 0;
    if (!this.columns[colNum]) return false;
    return this.columns[colNum].anySelected();
},

//> @method columnTree.getSelection()
//
// Returns the array of objects selected in the specified column of the columnTree
//
// @param colNum (number) Column within the columnTree to return the selection for
// @return (Array of ListGridRecord) list of records, empty list if nothing selected
//
// @group selection
// @visibility internal
//<
getSelection : function (colNum) {
    if (colNum == null) colNum = 0;
    if (!this.columns[colNum]) return [];
    return this.columns[colNum].getSelection();
},

//> @method columnTree.getSelectionObject()
//
// Returns the +link{Selection} object associated with the specified column of the columnTree
//
// @param colNum (number) Column within the columnTree to return the selection for
// @return (Selection) The specified column's underlying +link{Selection} object
//
// @group selection
// @visibility internal
//<
getSelectionObject : function (colNum) {
    if (colNum == null) colNum = 0;
    if (!this.columns[colNum]) return null;
    return this.columns[colNum].selection;
}

});


isc.ColumnTree.registerStringMethods({
    // itemSelected - handler fired when the user changes the selection.
    nodeSelected : "column, node",

    //> @method ColumnTree.onNodeSelected()
    // Notification method fired when a node is selected. Return false to suppress default
    // behavior.
    // @param column (ListGrid) The column (ListGrid instance) in which the node was selected
    // @param node (TreeNode) The selected node
    // @return (boolean) Return false to cancel default behavior
    // @visibility sgwt
    //<

    onNodeSelected : "column,node"
})




//> @class TableView
// Shows a listing of records with one or more fields from each record, with
// built-in support for navigation and editing of lists of records.
// <p/>
// The TableView provides built-in controls such as +link{tableView.showNavigation,navigation arrows} and
// shows fields from the provided records in one of several built-in +link{type:RecordLayout}s.
// <p/>
// NOTE: This widget is intended primarily for creating handset/phone-sized interfaces
// and does not have an appearance in any skin other than Mobile.
//
// @treeLocation Client Reference/Grids
// @visibility external
//<

isc.ClassFactory.defineClass("TableView", "ListGrid");

isc.TableView.addClassProperties({

    //> @type TableMode
    // Controls the display mode of TableView record display
    // @value  TableView.PLAIN    The default mode which displays a list of rows
    PLAIN:"plain",
    // @value  TableView.GROUPED  Grouped table is a set of rows embedded in a rounded rectangle
    GROUPED:"grouped",
    // @visibility external
    //<

    //> @type RecordLayout
    // Controls the style of TableView record display
    // @value  TableView.TITLE_ONLY  Show +link{tableView.titleField, title field} only
    TITLE_ONLY:"titleOnly",
    // @value  TableView.TITLE_DESCRIPTION  Show +link{tableView.titleField, title} and
    //                                      +link{tableView.descriptionField, description} fields only
    TITLE_DESCRIPTION:"titleAndDescription",
    // @value  TableView.SUMMARY_INFO  Show +link{tableView.titleField, title},
    //                                      +link{tableView.descriptionField, description} and
    //                                      +link{tableView.infoField, info} fields only
    SUMMARY_INFO:"summaryInfo",
    // @value  TableView.SUMMARY_DATA  Show +link{tableView.titleField, title},
    //                                      +link{tableView.descriptionField, description} and
    //                                      +link{tableView.dataField, data} fields only
    SUMMARY_DATA:"summaryData",
    // @value  TableView.SUMMARY_FULL  Show +link{tableView.titleField, title},
    //                                      +link{tableView.descriptionField, description},
    //                                      +link{tableView.infoField, info} and
    //                                      +link{tableView.dataField, data} fields similar to the
    //                                      iPhoneOS Mail application
    SUMMARY_FULL:"summaryFull",
    // @visibility external
    //<

    //> @type NavigationMode
    // Controls the navigation mode of records.
    // @value  TableView.WHOLE_RECORD Clicking anywhere on the record navigates
    WHOLE_RECORD: "wholeRecord",
    // @value  TableView.NAVICON_ONLY Only clicking directly on the navigation icon
    //                                triggers navigation
    NAVICON_ONLY: "navIconOnly"
    // @visibility external
    //<

});

isc.TableView.addProperties({

    //> @attr tableView.iconField  (String : "icon" : IRW)
    // This property allows the developer to specify the icon displayed next to a record.
    // Set <code>record[tableView.iconField]</code> to the URL of the desired icon to display.
    // Only applies if +link{showIconField} is <code>true</code>.
    //
    // @visibility external
    //<
    iconField: "icon",

    //>    @attr tableView.showIconField  (Boolean : true : IRW)
    // Should an icon field be shown for each record? A column in the table is set
    // aside for an icon as specified on each record in the +link{iconField}.
    //
    // @visibility external
    //<
    showIconField: true,

    //> @attr tableView.titleField  (String : "title" : IRW)
    // Field to display for an individual record as the main title.
    //
    // @visibility external
    //<
    titleField: "title",

    //> @attr tableView.infoField  (String : "info" : IRW)
    // Field to display as part of individual record in "summary" +link{recordLayout}s.
    //
    // @visibility external
    // @see recordLayout
    //<
    infoField: "info",

    //> @attr tableView.dataField  (String : "data" : IRW)
    // Field to display as part of individual record in "summary" +link{recordLayout}s.
    //
    // @visibility external
    //<
    dataField: "data",

    //> @attr tableView.descriptionField  (String : "description" : IRW)
    // Field to display as part of individual record in all +link{recordLayout}s
    // except "titleOnly".
    //
    // @visibility external
    //<
    descriptionField: "description",

    //>    @attr tableView.recordNavigationProperty  (String : "_navigate" : IRW)
    // Boolean property on each record that controls whether navigation controls are shown for
    // that record. If property is not defined on the record a navigation icon is shown
    // if +link{showNavigation} is <code>true</code>.
    //
    // @visibility external
    //<
    recordNavigationProperty: "_navigate",

    //> @attr tableView.tableMode  (TableMode : "plain" : IRW)
    // The display mode of the table.
    //
    // @visibility external
    //<
    tableMode: isc.TableView.PLAIN,

    //> @attr tableView.recordLayout (RecordLayout : "titleOnly" : IRW)
    // Sets the arrangement of data fields from the record.
    // <p/>
    // Note that controls supported by the TableView itself, such as navigation icons, are
    // implicitly added to the data fields described in the RecordLayout.  If an
    // +link{iconField} has been configured, it too is an implicitly shown field, to the left
    // of the area controlled by RecordLayout.
    //
    // @visibility external
    //<
    recordLayout: isc.TableView.TITLE_ONLY,

    //> @attr tableView.navIcon (SCImgURL : "[SKINIMG]/iOS/listArrow_button.png" : IRW)
    // The navigation icon shown next to records when
    // +link{showNavigation} is true and +link{navigationMode} is set to
    // "navIconOny".
    //
    // @visibility external
    //<
    navIcon: "[SKINIMG]/iOS/listArrow_button.png",

    //> @attr tableView.wholeRecordNavIcon (SCImgURL : "[SKINIMG]/iOS/listArrow.png" : IRW)
    // The navigation icon shown next to records when +link{showNavigation}
    // is true and +link{navigationMode} is set to "wholeRecord".
    //
    // @visibility external
    //<
    wholeRecordNavIcon: "[SKINIMG]/iOS/listArrow.png",

    //> @attr tableView.showNavigation (boolean : null : IRW)
    // Whether to show navigation controls by default on all records.  Can also be configured
    // per-record with +link{recordNavigationProperty}.
    //
    // @visibility external
    //<


    //> @attr tableView.navigationMode (NavigationMode : "wholeRecord" : IRW)
    // Set navigation mode for this TableView.
    //
    // @visibility external
    //<
    navigationMode: isc.TableView.WHOLE_RECORD,

    // SKINNING --------------------------------------------------

    //> @attr tableView.recordTitleStyle (CSSStyleName : "recordTitle" : IRW)
    // Default style for title.
    // @visibility external
    //<
    recordTitleStyle: "recordTitle",

    //> @attr tableView.recordDescriptionStyle (CSSStyleName : "recordDescription" : IRW)
    // Default style for description.
    // @visibility external
    //<
    recordDescriptionStyle: "recordDescription",

    //> @attr tableView.recordDataStyle (CSSStyleName : "recordData" : IRW)
    // Default style for data field.
    // @visibility external
    //<
    recordDataStyle: "recordData",

    //> @attr tableView.recordInfoStyle (CSSStyleName : "recordInfo" : IRW)
    // Default style for info field.
    // @visibility external
    //<
    recordInfoStyle: "recordInfo",

    // COLUMN DEFINITIONS --------------------------------------------------

    iconFieldDefaults: {
        width: 50,
        imageSize: 30,
        align: "center",
        type: "image"
    },

    titleFieldDefaults: {
        name: "TVtitleField",
        width: "*",
        type: "text",
        formatCellValue : function (value, record, rowNum, colNum, grid) {
            // Defer to user-provided record formatter if provided
            if (grid.formatRecord != null) {
                return grid.formatRecord(record);
            }

            var title = grid._getFormattedFieldValue(record, grid.titleField),
                description = grid._getFormattedFieldValue(record, grid.descriptionField),
                info = grid._getFormattedFieldValue(record, grid.infoField),
                data = grid._getFormattedFieldValue(record, grid.dataField),
                html = ""
            ;
            if (grid.recordLayout == isc.TableView.SUMMARY_INFO ||
                grid.recordLayout == isc.TableView.SUMMARY_FULL)
            {
                html += "<span class='" + grid.recordInfoStyle + "'>" +
                        info + "</span>";
            }
            html += "<span class='" + grid.recordTitleStyle + "'>" + title + "</span>";
            if (grid.recordLayout != isc.TableView.TITLE_ONLY) {
                html += "<span class='" + grid.recordDescriptionStyle + "'>" +
                        description + "</span>";
            }
            if (grid.recordLayout == isc.TableView.SUMMARY_DATA ||
                grid.recordLayout == isc.TableView.SUMMARY_FULL)
            {
                html += "<span class='" + grid.recordDataStyle + "'>" +
                        data + "</span>";
            }
            return html;
        }
    },

    navigationFieldDefaults: {
        name: "TVnavigationField",
        width: 54,
        align: "right",
        formatCellValue : function (value, record, rowNum, colNum, grid) {
            if (grid.getShowNavigation(record)) {
                var icon = isc.Img.create({
                    autoDraw: false,
                    autoFit: true,
                    imageType: "normal",
                    src: grid.getNavigationIcon(record)
                });
                return icon.getInnerHTML();
            }
            return grid._$nbsp;
        }
    },

    groupByFieldDefaults: {
        // Hidden
        showIf: "false"
    },

    // DEFAULT GRID STYLING --------------------------------------------------

    // disable canAddFormulaField / canAddSummaryField
    canAddFormulaFields:false,
    canAddSummaryFields:false,

    showHeader: false,

    // Selection management done by tableView.recordClick handler
    selectionType: "none",

    skinImgDir: "images/iOS/",
    baseStyle: "tableCell",
    border: "0px",

    // don't wrap, as that will mess up the look of the trees
    wrapCells: false,
    cellHeight: 44,
    alternateRecordStyles: false,
    canCollapseGroup: false,
    groupStartOpen: "all",


    ignoreEmptyCriteria: false
});

isc.TableView.addMethods({

    initWidget : function () {
        this.Super("initWidget", arguments);

        // Holds fieldName->colNum mapping to optimize value formatting calls
        this._colIndexes = {};

        // If formatRecord was passed to us as a string, convert it to a method
        if (this.formatRecord != null && !isc.isA.Function(this.formatRecord))
            isc.Func.replaceWithMethod(this, "formatRecord", "record");
    },

    // Override to hide any user-defined fields and to add table view display fields
    setFields : function(newFields) {
        this.invokeSuper(isc.TableView, "setFields", this._defineTableFields(newFields));
    },

    // Hide any pre-defined fields, add table view display fields, and create
    // fields for undefined groupBy fields.
    _defineTableFields : function (baseFields) {
        var columns = baseFields || [];

        // Hide all pre-defined fields
        for (var i = 0; i < columns.length; i++) {
            columns[i].showIf = "false";
        }

        // Setup display fields

        // Icon column
        if (this.showIconField) {
            var existingColumn = columns.find(this.fieldIdProperty, this.iconField);
            if (existingColumn) columns.remove(existingColumn);

            this._iconCell = columns.length;
            columns[columns.length]
                = isc.addProperties({name: this.iconField},
                                    this.iconFieldDefaults,
                                    this.iconFieldProperties);
        }

        // Title column
        var existingColumn = columns.find(this.fieldIdProperty, this.titleFieldDefaults.name);
        if (existingColumn) columns.remove(existingColumn);

        columns[columns.length]
            = isc.addProperties({}, //{name: this.titleField},
                                this.titleFieldDefaults,
                                this.titleFieldProperties);

        // Navigation icon column
        var existingColumn = columns.find(this.fieldIdProperty, this.navigationFieldDefaults.name);
        if (existingColumn) columns.remove(existingColumn);

        this._navigateCell = columns.length;
        columns[columns.length]
            = isc.addProperties({},
                                this.navigationFieldDefaults,
                                this.navigationFieldProperties);

        if (this.groupByField) {
            // Create a hidden column for each groupBy field if not
            // already user-defined.
            var fields;
            if (isc.isA.Array(this.groupByField)) {
                fields = this.groupByField;
            } else {
                fields = [ this.groupByField ];
            }
            for (var i = 0; i < fields.length; i++) {
                var field = columns.find(this.fieldIdProperty, fields[i]);
                if (field) {
                    // Apply specific properties
                    isc.addProperties(field,
                                      this.groupByFieldDefaults,
                                      this.groupByFieldProperties);
                } else {
                    // Define groupBy field
                    columns[columns.length]
                        = isc.addProperties({name: fields[i]},
                                            this.groupByFieldDefaults,
                                            this.groupByFieldProperties);
                }
            }
        }

        return columns;
    },

    // Get formatted value for a field
    _getFormattedFieldValue : function (record, fieldName) {
        var value = record[fieldName] || this._$nbsp,
            colNum = this._colIndexes[fieldName],
            undef
        ;
        if (colNum == null || colNum == undef) {
            colNum = isc.Class.getArrayItemIndex(fieldName, this.getAllFields(),
                                                 this.fieldIdProperty);
            this._colIndexes[fieldName] = colNum;
        }
        if (colNum >= 0) {
            value = this.getFormattedValue(record, fieldName, value);
        }
        return value;
    },

    //> @method tableView.getNavigationIcon
    // Icon to display as a NavigationIcon per record. Default behavior returns
    // +link{navIcon} or +link{wholeRecordNavIcon} depending on
    // +link{navigationMode} but could be overridden to customize this
    // icon on a per-record basis.
    //
    // @param record (Record) the record
    // @return (Image) the image
    //<
    getNavigationIcon : function (record) {
        return (this.navigationMode == isc.TableView.NAVICON_ONLY
            ? this.navIcon
            : this.wholeRecordNavIcon);
    },

    //> @method tableView.getShowNavigation
    // Whether to show navigation controls for some specific record. If the
    // +link{recordNavigationProperty, record navigation property} is set
    // on the record in question, this will be respected, otherwise will return the
    // result of the value set via +link{showNavigation}.
    //
    // @param record (Record) record to be checked for navigation state
    // @return (boolean) true if navigation controls should be shown for this record
    //<
    getShowNavigation : function (record) {
        if (record && record[this.recordNavigationProperty] != null) {
            return record[this.recordNavigationProperty];
        }
        return this.showNavigation;
    },

    recordClick : function (viewer, record, recordNum, field, fieldNum, value, rawValue) {
        if (fieldNum != this._iconCell &&
            fieldNum != this._navigateCell &&
            this.canSelectRecord(record))
        {
            this.selectSingleRecord(record);
        }

        if (fieldNum == this._navigateCell || this.navigationMode == isc.TableView.WHOLE_RECORD) {
        if (this.recordNavigationClick) {
        // CALLBACK API:  available variables:  "record"
        // Convert a string callback to a function
        isc.Func.replaceWithMethod(this, "recordNavigationClick", "record");
                this.recordNavigationClick(record);
            }
        } else if (fieldNum == this._iconCell) {
        if (this.imageClick) {
        // CALLBACK API:  available variables:  "record"
        // Convert a string callback to a function
        isc.Func.replaceWithMethod(this, "imageClick", "record");
                this.imageClick(record);
            }
        }
    },

    // Override grid.getBaseStyle to handle grouped styling
    getBaseStyle : function (record, rowNum, colNum) {
        if (this.isGrouped) {
            var node = this.data.get(rowNum),
                first = this.data.isFirst(node),
                last = this.data.isLast(node)
            ;
            if (first && last) {
                return (colNum == 0 ? "cellOnlyLeft"
                                    : (colNum == this.fields.length-1 ? "cellOnlyRight"
                                                                      : "cellOnly"));
            } else if (first) {
                return (colNum == 0 ? "cellTopLeft"
                                    : (colNum == this.fields.length-1 ? "cellTopRight"
                                                                      : "cellTop"));
            } else if (last) {
                return (colNum == 0 ? "cellBottomLeft"
                                    : (colNum == this.fields.length-1 ? "cellBottomRight"
                                                                      : "cellBottom"));
            }
        }
        return this.Super("getBaseStyle", arguments);
    }
});

isc.TableView.registerStringMethods({
    //> @method tableView.recordNavigationClick
    // Executed when the user clicks on a record, or on the navigate icon for a
    // record depending on +link{navigationMode}.
    //
    // @param  record (ListGridRecord)  record clicked
    // @visibility external
    //<
    recordNavigationClick : "record",

    //> @method tableView.imageClick
    // Executed when the user clicks on the image displayed in a record if
    // +link{iconField} has been specified.
    //
    // @param  record (ListGridRecord)  record clicked
    // @visibility external
    //<
    imageClick : "record",

    //> @method tableView.formatRecord()
    // Formatter to apply to record display.
    //
    // @param  record (ListGridRecord)  record to format
    // @return (HTML) formatted record contents
    // @visibility external
    //<
    formatRecord : "record"

});
 
 
// DOMTree
// Builds 
isc.ClassFactory.defineClass("DOMTree", "Tree");

//!>Deferred
isc.DOMTree.addMethods({

    // root of the DOM to build a tree for
    //rootElement : null,

    // property of the TreeNode objects where the DOM node tag name should be copied
    tagNameProperty : "tagName",

    // property on each node where the DOM element is stored
    elementProperty : "_element",

    // a list of other attributes to copy from the DOM node to the TreeNode
    //copyAttributes : [],

    // Tree behavior to cause folders to be opened at initialization in a breadth-first manner
    // until a certain number of nodes (loadBatchSize) is visible
    loadOnInit : true,  

    // true in the sense that tree nodes are dynamically and lazily created from DOM nodes
    loadDataOnDemand:true,

    // stick the root element on the root node, for uniformity in accessing node._element
    makeRoot : function () {    
        var root = this.Super("makeRoot", arguments);
        root[this.elementProperty] = this.rootElement;
        return root;
    },

    getElement : function (node) { return node[this.elementProperty]; },

    // whether to show text nodes as distinct elements
    hideTextNodes: true,

    // Node Titles
    // ---------------------------------------------------------------------------------------
    getElementTitle : function (element) {
        // look for an attributes named after the title or "name" properties.  If this produces
        // undesireable results, just set titleProperty/nameProperty to some attribute that
        // doesn't exist
        var title = element.getAttribute(this.titleProperty);
        if (!this.valueIsEmpty(title)) return title;

        title = element.getAttribute(this.nameProperty);
        if (!this.valueIsEmpty(title)) return title;
        
        if (!isc.xml.hasElementChildren(element)) {
            title = isc.xml.getElementText(element);
            if (!this.valueIsEmpty(title)) return title;
        }
    
        // fall back to the tagName
        return element.tagName || element.nodeName;
    },
    valueIsEmpty : function (value) {
        return value == null || isc.isAn.emptyString(value);
    },

    // Folderness
    // ---------------------------------------------------------------------------------------
    isFolder : function (node) {
        if (node == this.root || node.children != null) return true;
        var element = node[this.elementProperty];
        if (!element || !element.childNodes || element.childNodes.length == 0) return false;

        // if we're not hiding text nodes, any child qualifies as a folder
        if (!this.hideTextNodes) return true;

        // otherwise show it as a folder only if it has element children, not just text
        return isc.xml.hasElementChildren(element);
    },

    // Modification
    // ---------------------------------------------------------------------------------------
    moveList : function (moveList, newParent, index) {
        var moveNode = moveList[0],
            moveElement = this.getElement(moveNode);

        this.logWarn("moveList: " + this.echoAll(moveList) +
                     ", newParent: " + this.echo(newParent) +
                     ", index: " + index);

        
        //moveElement.parentNode.removeChild(moveElement);
        this._addToDOM(moveElement, newParent, index);
        this.Super("moveList", arguments);
    },
    remove : function (node) {
        var element = this.getElement(node);
        element.parentNode.removeChild(element);
        return this.Super("remove", arguments);
    },
    // NOTE: overriding add() to auto-detect being passed an XML Element is painfully slow due
    // to the Super call..
    addElement : function (element, parent, position) {
        this._addToDOM(element, parent, position);
        // an already loaded parent needs add called - otherwise the child will be
        // discovered when loadChildren is called
        if (this.isLoaded(parent)) {
            var node = this.nodeForElement(element);
            this.add(node, parent, position);
        } else {
            // otherwise we need to fire dataChanged() in order to cause an opener icon to be
            // rendered on any node that becomes a folder due to DOM addition
            this.dataChanged();
        }
    },
    
    _addToDOM : function (addElement, parent, position) {
        var parentElement = this.getElement(parent);
        if (position == null) {
            this.logWarn("appending: " + this.echoLeaf(addElement) +
                         " to: " + this.echoLeaf(parentElement));
            parentElement.appendChild(addElement);
        } else {
            // NOTE: the index in the visible nodes may not be the same as the index in the
            // DOM, since a DOMTree may not show all DOM nodes (eg it might hide text nodes).
            var beforeNode = this.getChildren(parent)[position],
                beforeElement = this.getElement(beforeNode);
            this.logWarn("inserting into: " + this.echoLeaf(parentElement) + 
                         ", before: " + this.echoLeaf(beforeElement));
            parentElement.insertBefore(addElement, beforeElement);
        }
    },

    // Loading (creating TreeNodes from DOM nodes)
    // ---------------------------------------------------------------------------------------

    // create a TreeNode from a DOM element
    nodeForElement : function (element) {
        var node = {};

        node[this.elementProperty] = element;

        // heuristically determine a title
        node[this.titleProperty] = this.getElementTitle(element);

        // copy the tagName from the DOM node to the tagName property of the TreeNode
        if (this.tagNameProperty) {
            node[this.tagNameProperty] = element.tagName || element.nodeName;
        }

        // copy other attributes if so configured
        if (this.copyAttributes) {
            for (var j = 0; j < this.copyAttributes.length; j++) {
                var attribute = this.copyAttributes[j];
                node[attribute] = element.getAttribute(attribute);
            }
        }
        return node;
    },

    loadChildren : function (parentNode) {
        if (this.isLoaded(parentNode)) return;
        try {
        //this.logWarn("loading children of: " + 
        //             (parentNode == this.root ? "root" : this.echoLeaf(parentNode._element)));

        var element = parentNode._element;
        if (element == null) return;
        var childNodes = element.childNodes;

        // allow inspection through IFRAMEs to get into SVGs in Moz
        if (isc.Browser.isMoz && element.contentDocument) {
            childNodes = [element.contentDocument.documentElement];
        } else {
            // defer loading uninteresting children if loading a batch
            // (IFRAMEs are exempt from this check)
            if (this.loadingBatch() && !isc.xml.hasElementChildren(element)) return;
        }

        parentNode[this.openProperty] = true;

        if (childNodes != null) {
            // make a node for each DOM child
            for (var i = 0; i < childNodes.length; i++) {
                var domNode = childNodes[i];
                // skip text, CDATA and comment nodes if so configured
                if (this.hideTextNodes && domNode.nodeName.startsWith("#")) continue;

                var node = this.nodeForElement(domNode);

                this.add(node, parentNode);
            }
        }

        this.setLoadState(parentNode, isc.Tree.LOADED);
        } catch (e) {
            this.logWarn("parent node: " + this.echo(parentNode) +
                         ", at path: " + this.getPath(parentNode) + 
                         ", error: " + this.echo(e) +
                         this.getStackTrace());
        }
    }
});
//!<Deferred



//> @class DOMGrid
//
// Provides a tree view of any DOM-compliant structure, such as an XML or HTML document.
//
// @treeLocation Client Reference/Grids
//
// @visibility external
//<
isc.defineClass("DOMGrid", "TreeGrid").addMethods({
    initWidget : function () {
        this.Super(this._$initWidget);
        if (this.url) {
            isc.xml.loadXML(this.url,
                            this.getID() + ".setRootElement(xmlDoc.documentElement)");
        }
    },

    // implement getDefaultData() so we don't end up with a Tree instance
    getDefaultData : function () {
        return null;//isc.DOMTree.create();
    },

    //> @attr domGrid.rootElement   (DOMElement : null : IRW)
    // Root element (or document) to view in the tree.
    // @visibility external
    //<

    //> @attr domGrid.htmlMode (boolean : false : IR)
    // In html mode, a DOMGrid shows a limited set of properties appropriate for inspecting
    // sizing and styling issues in an HTML document.
    //
    // Internal for now, used only by the DOMInspector and with capricious non-configurable
    // attributes shown..
    // @visibility internal
    //<

    // D&D DOM changes
    // ---------------------------------------------------------------------------------------
    canDragRecordsOut : true,
    canAcceptDroppedRecords : true,
    canReorderRecords : true,

    //
    // ---------------------------------------------------------------------------------------
    getElement : function (node) {
        return this.data.getElement(node);
    },

    // ---------------------------------------------------------------------------------------
    showRoot:true,

    //> @method domGrid.setRootElement()
    // Set the root element (or document) to view in the tree.
    // @param rootElement (DOMElement) new root element
    // @visibility external
    //<
    setRootElement : function (rootElement) {
        this.rootElement = rootElement;
        var tree = isc.DOMTree.create({
            rootElement : rootElement
        });
        this.setData(tree);
    },

    // ---------------------------------------------------------------------------------------
    // suppress folder/file icons
    getIcon : function () { },

    // Node Titles
    // ---------------------------------------------------------------------------------------
    _$lt : "&lt;",
    _$gt : "&gt;",
    getNodeTitle : function (node, recordNum, field) {
        if (node == null) return null;

        // cache generated titles
        if (node._title) return node._title;

        //try {
            var title = this.htmlMode ? this._getHTMLNodeTitle(node)
                                      : this._getXMLNodeTitle(node);
        //} catch (e) {
        //    this.logWarn("exception trying to retrieve title: " + this.getStackTrace());
        //    title = "<BAD NODE/>";
        //}

        return (node._title = title);
    },

    // wipe out cached titles on dataChanged
    dataChanged : function () {
        this.Super("dataChanged", arguments);
        this.data.getOpenList().setProperty("_title", null);
    },

    // getXMLNodeTitle: shows all attributes
    _attrTemplate : [" ",,'="',,'"'],
    _getXMLNodeTitle : function (node) {
        if (node._element == null) {
            this.logWarn("no element for node: " + this.echo(node));
            //return;
        }

        var element = node._element,
            ns = isc.emptyString,
            buffer = this._titleSB;

        if (buffer == null) buffer = this._titleSB = isc.SB.create();
        else buffer.clear();

        buffer.append(this._$lt, (element.tagName || element.nodeName));

        var attrs = element.attributes;
        if (attrs != null) {
            var template = this._attrTemplate;
            for (var i = 0; i < attrs.length; i++) {
                var attr = attrs[i];
                template[1] = attr.name;
                template[3] = attr.value;
                buffer.append(template);
            }
        }

        if (!isc.xml.hasElementChildren(element)) {
            // just text
            buffer.append(this._$gt, isc.xml.getElementText(element),
                          "&lt;/", (element.tagName || element.nodeName), this._$gt);
        } else if (element.childNodes.length > 0) {
            buffer.append(this._$gt);
        } else {
            buffer.append("/&gt;");
        }
        return buffer.toString();
    },

    // getHTMLNodeTitle: shows only a selection of common HTML attributes
    _getHTMLNodeTitle : function (node) {
        var element = node._element,
            ns = isc.emptyString,
            width, height;

        if (isc.Browser.isIE && element.scopeName == "VML") {
            // VML elements JS error in IE if you access element.width or element.height.
            width = (element.style ? element.style.width : null);
            height = (element.style ? element.style.height : null);
        } else {
            width = element.width || (element.style ? element.style.width : null);
            height = element.height || (element.style ? element.style.height : null);
        }
        var isCell = (element.tagName && element.tagName.toLowerCase() == "td");

        return isc.SB.concat(
            this._$lt, (element.tagName || element.nodeName),
                   (element.id ? " ID=" + element.id : ns),
                   (!this.valueIsEmpty(width) ? " WIDTH=" + width : ns),
                   (!this.valueIsEmpty(height) ? " HEIGHT=" + height : ns),
                   (isCell && element.rowSpan > 1 ? " ROWSPAN=" + element.rowSpan : ns),
                   (isCell && element.colSpan > 1 ? " COLSPAN=" + element.colSpan : ns),
            this._$gt);
    },

    valueIsEmpty : function (value) {
        return value == null || isc.isAn.emptyString(value);
    },

    // Colorize nodes
    // ---------------------------------------------------------------------------------------
    colorMap : {
        table : "#009900",
        tr : "#333399",
        td : "#663366",

        form : "#CC6600",
        input : "#3333FF",
        textarea : "#3333FF",

        div : "#663300",
        span : "#663300"
    },

    colorPrefix : "color:",
    _tagCache : {},
    getCellCSSText : function (node, rowNum, fieldNum) {
        var element = this.data.getElement(node);
        if (element == null) return null;
        var tagName = element.tagName;
        if (tagName == null) return null;

        // translate to canonical case and cache
        if (this._tagCache[tagName]) {
            tagName = this._tagCache[tagName];
        } else {
            this._tagCache = tagName = tagName.toLowerCase();
        }

        //this.logWarn("cellCSS: " + tagName);

        if (this.colorMap[tagName] != null) {
            return isc.SB.concat(this.colorPrefix, this.colorMap[tagName], isc.semi);
        }
    }

});







//>    @class    MenuBar
//      A MenuBar is a bar of buttons used to show a set of menus.
//  @treeLocation Client Reference/Control
//  @visibility external
//<

// declare the class itself
isc.ClassFactory.defineClass("MenuBar", "Toolbar");

// synonym: used to be named with lowercase "b"
isc.addGlobal("Menubar", isc.MenuBar);

// add default properties to the class
isc.MenuBar.addProperties( {
    //>    @attr    menuBar.menus        (Array of Menu : null : [IRW])
    // An array of menu object initializers or instantiated menu objects. Buttons for each
    // menu item will automatically be created. See the Menu Widget Class for fundamental
    // menu properties and other properties.  Titles for the buttons are derived from the
    // <code>title</code> property of each menu.
    //  @visibility external
    //  @see class:Menu
    //<
    //menus:null,

    //> @attr menu.menuButtonWidth (int : null : IR)
    // For a menu that has a +link{MenuButton} generated for it automatically (for example when
    // included in a +link{MenuBar}, the width that the MenuButton should have.  If unset, the
    // MenuButton will be as wide as <code>menu.width</code>.
    //
    // @visibility external
    //<

    overflow:isc.Canvas.VISIBLE,

    //>    @attr    menuBar.defaultHeight        (number : 22 : IRW)
    // Default to height for menu bars
    //        @group    sizing
    //<
    defaultHeight:22,

    menuConstructor:"Menu",

    //    menuDefaults:{},

    //>    @attr    menuBar.buttonConstructor        (Class : MenuBarButton : IRWA)
    // Default constructor for menuBar buttons.  Change to a more exotic class if you dare!
    // (untested)
    //        @group    appearance
    //<
    buttonConstructor:"MenuBarButton",

    //> @attr menuBar.tabIndex  (number : -1 : IRWA)
    // By default exclude menubars from the page's tab order. To include a menubar in the page's
    // tab order, set tabIndex to an explicit tab index, or <code>null</code> for automatically
    // assigned tabIndex
    // @visibility external
    //<
    tabIndex:-1,

    // We want arrow keys to move us around the toolbar, not tabs.
    tabWithinToolbar:false,


    //>    @attr    menuBar.buttonDefaults        (object : (see below) : IRWA)
    //    The following are defaults for all menuBar buttons.
    //    To add properties to all buttons of ALL menuBars, change the below.
    //    To add properties to all buttons of a particular menuBar you're creating,
    //        add a "button" property to the menuBar constructor with the defaults
    //        you want applied to the buttons.  This will automatically be added to each button.
    //        @group    appearance
    //<
    buttonDefaults: {
        showDown:false,
        showRollOver:true,

        showFocused:true,
        showFocusedAsOver:true
    }

});

isc.MenuBar.addMethods({

// Instantiates each Menu with automatically-generated IDs, using menuBar.menuConstructor and
// menuBar.menuDefaults
initWidget : function () {
    // call the superclass function
    this.Super("initWidget",arguments);

    //Note: we don't instantiate menus until they are displayed -- reduces init time.
},


//>    @method    menuBar.setButtons()
// Invalid to call on Menubar, use +link{setMenus()} instead.
// @param [newButtons] (Array of Button Properties) invalid; do not call
// @visibility external
//<
// Automatically creates MenuButtons from this.menus.
// Called internally to initialize the menuBar on draw, and from setMenus only.
setButtons : function () {
    var newButtons = [];

    if (this.menus) {
        // create a button for each menu
        for (var i = 0; i < this.menus.length; i++) {
            var menu = this.menus[i];
            newButtons[i] = this._getButtonProperties(menu,i);
        }
    }
    return this.Super("setButtons", [newButtons], arguments);
},

// Helper method to get the properties for a menu button.
_getButtonProperties : function (menu, index) {
    return {
                title:menu.title,
                width:(menu.menuButtonWidth ? menu.menuButtonWidth : menu.width),
                menuNum:index,

                focusChanged:function (hasFocus) {
                    if (isc.Browser.isMoz && hasFocus) this.bringToFront();
                }
            };

},

//>    @method    menuBar.setMenus()
// Dynamically reset the set of menus displayed by this menu bar.
// @param menus (array) array of new menus for this menubar
// @visibility external
//<
setMenus : function (menus) {
    if (!isc.isAn.Array(menus)) menus = [menus];

    // get rid of old observations
    for (var i = 0; i < this.members.length; i++) {
        var member = this.members[i],
            menu = this.menus[member.menuNum];
        if (member.isObserving(menu, "hide")) {
            member.ignore(menu, "hide");
        }
    }

    // assign menus as new menus
    this.menus = menus;
    // create menuButtons for the menus we just assigned as this.menus
    this.setButtons();
},

// Helper method fired to update buttons in response to the menus shifting order
// - clears out old observations (for buttons that may be removed from the menubar)
//   and remaps menu numbers

_remapButton : function (button, index) {
    if (!button) return;

    // in most cases we're still going to point at the same menu, but the position of that
    // menu in the menus array will have changed.
    // If index == -1 though, the menu is being removed, so clean up the observation
    if (index == -1) {
        var menuNum = button.menuNum,
            oldMenu = this.menus[button.menuNum];
        if (button.isObserving(oldMenu, "hide")) {
            button.ignore(oldMenu, "hide");
        }
    }
    // Update the menuNum so the members match the menus
    button.menuNum = index;
},

//>    @method    menuBar.addMenus()
// Dynamically update the menuBar to include additional menus. Will update the visible set
// of buttons as appropriate
// @param newMenus (array) Array of new menus to add
// @param position (number) desired starting position of the new menus in the existing menus
//  array
// @visibility external
//<
addMenus : function (newMenus, position) {
    if (!newMenus) return;
    if (!isc.isAn.Array(newMenus)) newMenus = [newMenus];

    if (!this.menus) this.menus = [];
    if (position == null) position = this.menus.length;

    // If we have not yet initialized the buttons, we simply need to add the menus
    // the new buttons will be init'd along with the ones for pre-existent menus
    if (!this._buttonsInitialized) {
        this.menus.addListAt(newMenus, position);
    } else {

        // Every button AFTER the new buttons' position will need to be remapped
        for (var i = position; i < this.members.length; i++) {
            this._remapButton(this.members[i], (i + newMenus.length));
        }

        this.menus.addListAt(newMenus, position);

        var newButtons = [];
        for (var i = 0; i < newMenus.length; i++) {
            var index = this.menus.indexOf(newMenus[i]);
            newButtons[i] = this._getButtonProperties(newMenus[i], index);
        }
        this.addButtons(newButtons, position);
    }
},

//>    @method    menuBar.removeMenus()
// Dynamically remove menus from the menuBar. Will update the visible set of buttons as
// appropriate.
// @param menus (array) Array of menus to remove (will accept actual Menu components,
//                      or numbers representing the index of the menus in the current menus array)
// @visibility external
//<
removeMenus : function (menus) {

    if (menus == null) return;
    if (!isc.isAn.Array(menus)) menus = [menus];

    var membersToRemove = [],
        // make a new Array so existing Menu indices are stable while we form the new array
        newMenusArray = this.menus.duplicate();

    for (var i = 0; i < menus.length; i++) {
        var menu = menus[i];
        if (isc.isA.Number(menu)) menu = this.menus[menu];
        else if (!this.menus.contains(menu)) continue;

        newMenusArray.remove(menu);

        // form a list of generated MenuBarButtons to remove
        var index = this.menus.indexOf(menu);
        if (this._buttonsInitialized) membersToRemove.add(this.members[index]);
    }

    if (!this._buttonsInitialized) {
        this.menus = newMenusArray;
        return;
    }

    for (var i = 0; i < this.menus.length; i++) {
        if (this.menus[i] == newMenusArray[i]) continue;
        this._remapButton(this.members[i], newMenusArray.indexOf(this.menus[i]));
    }

    this.menus = newMenusArray;
    this.removeButtons(membersToRemove);
},


//>    @method    menuBar.showMenu()
// Shows (opens) a menu.
// @param   menu    (Menu | integer) menu to show (may be specified as a menu object, or index of
//                                   the menu from +link{menuBar.menus, this.menus}).
// @visibility external
//<
showMenu : function (menuNum) {
    var menu;
    if (isc.isA.Number(menuNum)) menu = this.menus[menuNum];
    else {
        menu = menuNum;
        menuNum = this.menus.indexOf(menu);
    }
    // could be a canvas or an object here, I guess
    if (!menu) {
        //>DEBUG
        this.logWarn("showMenu() called with invalid menu number: " + menuNum + ".  No effect.");
        //<DEBUG
        return;
    }

    var button;
    for (var i = 0; i < this.members.length; i++) {
        if (this.members[i].menuNum == menuNum) {
            button = this.members[i];
        }
    }

    if (!isc.isA.Canvas(menu)) {
        if (menu.ID == null) menu.ID = this.getID()+"_menu"+menuNum;
        menu.autoDraw = false;
        menu = this.menus[menuNum] =
            isc.ClassFactory.newInstance(this.menuConstructor, menu, this.menuDefaults);
    }

    // hide whichever menu is currently showing (leaving the clickMask up)
    if (this.activeMenu != null) {
        this.menus[this.activeMenu].hideMenuTree();
    }


    menu.keyEventParent = this;

    // move the menu into place and show it    (automatically will be moved above clickmask)
    if (!this.vertical) {
        menu.moveTo(button.getPageLeft(), button.getPageBottom());
    } else {
        menu.moveTo(button.getPageRight(), button.getPageTop());
    }
    menu.show();

    // Don't set the "down" state on the button til after the menu has shown
    // Otherwise when the clickMask shows, the button will be returned to state "up"
    // Also, while the menu is showing, avoid respoding to mouseOvers
    button._previousShowOver = button.showRollOver;
    button.showRollOver = false;
    button.setState(isc.StatefulCanvas.STATE_DOWN);

    // update this.activeMenu.
    // Note - when a menu is shown, the menuBar-button observes the hide method on the menu, to
    // reset it's visible state, and to clear 'activeMenu' from the menuBar.
    this.activeMenu = menuNum;
    if (!button.isObserving(menu,"hide")) {
        button.observe(menu,"hide","observer.menuHidden(observed)");
    }

    // bring the menuBar to the front so that they can switch to other menus
    var EH = isc.EH;
    if (EH.targetIsMasked(this)) this.bringToFront();

    // When the clickMask was shown by the first button clicked, one of our menu buttons had focus
    // and was therefore remembered as the maskedFocusCanvas.
    // Our buttons are no longer masked (bringToFront() above) so focussing on them won't update
    // the maskedFocusCanvas meaning it is essentially out of date at this point.
    // Clear this property so focus doesn't go back to the first button pushed when the clickMask
    // is hidden by menu.hideAllMasks()
    var topMask = EH.clickMaskRegistry.last(),
        maskedFocusCanvas = EH.getMaskedFocusCanvas(topMask);
    if (this.members.contains(maskedFocusCanvas)) EH.setMaskedFocusCanvas(null, topMask);

    // Update menu.focusOnHide to ensure that when the menu hides focus goes to the appropriate
    // button
    menu.body.focusOnHide = button;
},

_focusInNextButton : function (forward) {
    if (!this.activeMenu == null) return this.Super("_focusInNextButton", arguments);
    if (forward == null) forward = true;

    // In this case, we're showing a menu, and the user pressed the left or right arrow
    // key.
    // Instead of shifting focus to the next button, show the next menu
    var activeMenu = this.activeMenu,
        step = forward ? 1 : -1,
        currentMenu = activeMenu + step,
        members = this.getMembers();

    while (activeMenu != currentMenu) {
        if (currentMenu < 0) currentMenu = members.length -1;
        else if (currentMenu >= this.members.length) currentMenu = 0;

        var button = members[currentMenu];
        if (!button.isDisabled()) {
            button.showMenu();
            break;
        }
        currentMenu += step;
    }
},

// override getFocusButtonIndex to return the button with an activeMenu, if one is defined, even
// if it doesn't have focus
getFocusButtonIndex : function () {
    if (this.activeMenu != null) return this.activeMenu;
    return this.Super("getFocusButtonIndex",arguments);

}

});





//>    @class    MenuBarButton
// Subclass of Button only used to show buttons in a MenuBar widget.
// <p>
// Allows mousing between MenuButtons in the same MenuBar to pop open Menus without a click.
// Also ensures that only one Menu from the MenuBar is showing at any given time.
//
//  @treeLocation Client Reference/Foundation
//<

isc.ClassFactory.defineClass("MenuBarButton","MenuButton");
isc.MenuBarButton.addProperties({
    showMenuButtonImage:false,
    showDown:false,
    autoDraw:false,
    align:"center"
});
isc.MenuBarButton.addMethods({

    // show menu on mouseOver if another menu in the menuBar is showing its menu
    // XXX unfortunately we don't get mouseMove while the mouse is down if the mouse went down
    // on another widget.  See EventHandler for why.
    mouseOver : function () {
        this.Super("mouseOver", arguments);

        // if another menu is currently being shown, show our menu instead.
        var activeMenuNum = this.parentElement.activeMenu;
        if (activeMenuNum != null && activeMenuNum != this.menuNum) {
            this.showMenu();
        }
    },

    // override mouseDown to show menu (rather than click)
    mouseDown : function () {
        // if this menu button is the one showing it's menu, hide it
        if (this.parentElement.activeMenu == this.menuNum) {
            isc.Menu.hideAllMenus();
        // otherwise show it
        } else {
            this.showMenu();
        }
    },
    // override mouseUp and click to do nothing
    mouseUp : function () {},
    click : function () {},
    mouseOut : function () {
        if (this.parentElement.activeMenu != this.menuNum) {
            this.Super("mouseOut", arguments);
        }
    },

    // Override handleKeyPress - we're going to be firing showMenu() rather than click()
    handleKeyPress : function (event, eventInfo) {

        if (event.keyName == "Space" || event.keyName == "Enter") return this.showMenu();

        if (this.keyPress) {
            this.convertToMethod("keyPress");
            return this.keyPress(event, eventInfo);
        }
    },

    // override showMenu to delegate up to the menuBar
    showMenu : function () {
        this.parentElement.showMenu(this.menuNum);
    },

    menuHidden : function (menu) {
        if (isc._traceMarkers) arguments.__this = this;
        // When the menu got shown we put the button into a 'down' state
        // Clear this now.

        if (this.state == isc.StatefulCanvas.STATE_DOWN) {
            if (this.hasFocus && this.showFocused) this.setState(isc.StatefulCanvas.STATE_OVER);
            else this.setState(isc.StatefulCanvas.STATE_UP);
        }

        this.showRollOver = this._previousShowOver;
        delete this._previousShowOver;
        this.menuIsDown = false;
        if (this.parentElement.activeMenu == this.menuNum) {
            this.parentElement.activeMenu = null;
        }
        // clear the eventParent setting we set up on show - if the menu is shown elsewhere
        // we don't want to recieve bubbled events!
        delete menu.eventParent;
        // clear the observation - we'll re-observe when we re-show!
        this.ignore(menu, "hide");

    }
});








//> @class CellSelection
//
// Maintains a representation of selection over a 2-dimensional grid of objects.<br>
//  Automatically created to manage cell-selection on +link{class:CubeGrid} widgets.
//
// @visibility external
// @treeLocation Client Reference/System/Selection
//<
// When you create a CellSelection object, set:
// <ul>
//        <li>cellSelection.data - to an array of records referenced by this selection
//        <li>cellSelection.numCols - to the number of columns/fields covered by this selection
//        <li>cellSelection.selectionProperty - to an alternative name for the selection property,
//            if you need to access this property manually
// </ul>

//    Implementation:
//        A uniquely-named selection property is added to each row/record of the data.
//        This property holds an array of numbers, one for each "chunk" of 32 columns,
//        whose bits (0-31) represent the selection state of the cell in the corresponding
//        column/field.

//    TODO:
//        * special case selectSingleCell/deselectSingleCell when we know only one cell can be selected
//        * convert 'select all' to not actually select all, but rather to set a bit?
//        * maybe also special case row/col selection for performance?
//        * hardcode COL_SELECTION_FLAGS table

//
//    create the CellSelection class
//
isc.ClassFactory.defineClass("CellSelection");



isc.CellSelection.addClassProperties({
    _selectionID : 0,     //> @classAttr isc.CellSelection._selectionID  (number : 0 : IRWA)
                          // number to generate a unique ID and selectionProperty for each selection
                          // @group selection
                          // @visibility internal
                          //<

    COL_SELECTION_FLAGS : null    // generate when the first cellSelection is instantiated
});



isc.CellSelection.addClassMethods({

generateFlagTable : function () {
    isc.CellSelection.COL_SELECTION_FLAGS = [];
    for (var i = 0; i < 32; i++)
        isc.CellSelection.COL_SELECTION_FLAGS[i] = Math.pow(2,i);
}

});



isc.CellSelection.addProperties( {
    data:null,
    numCols:0,
    selectionProperty:null,
    _dirty:true,
    _selectedCells:[],
    lastSelectedCell:[],
    changedCells:[]
});



isc.CellSelection.addMethods({



//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~ Setup ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

//> @attr cellSelection.data  (Array | List: null : IRW)
//    Array (or +link{class:List}) of records to be referenced by this cell selection
//  @visibility serverSelection
//  @group selection
//<

//> @attr cellSelection.numCols (number : null : IRW)
//      the number of columns/fields covered by this selection
//  @visibility internal
//  @group selection
//<

//> @attr cellSelection.selectionProperty (string : null : IRWA)
//      A name for the selection property.  This property will be set on the records in this
//      selection's data, and used to determine whether records are selected or not.
//      Automatically generated if not specified.
//  @visibility serverSelection
//  @group selection
//<

//> @attr cellSelection.cellRecordMode (CellRecordMode | "cell" : IRW)
// Whether calls to +link{getSelection} on this class should return one record per selected
// cell, as with +link{class:CubeGrid}, or one record per row that has any selected cells, as
// with +link{class:ListGrid}.
// @visibility @external
// @group selection
//<
cellRecordMode: "cell",

//> @method cellSelection.init()    (A)
//  Initialize this selection instance.<br>
//  Note: if the <code>data</code> property is not set at init time, it should be passed to
//  the selection using the <code>selection.setData</code> method
//
//  @group selection
//
//  @param [all arguments] (object) objects with properties to override from default
// @visibility serverSelection
//<
init : function () {
    if (!isc.CellSelection.COL_SELECTION_FLAGS) isc.CellSelection.generateFlagTable();

    // get unique ID and selection properties
    if (!this.selectionProperty) this.selectionProperty = "_cellSelection_"+isc.CellSelection._selectionID++;

    // set the data object so we get notification for add and delete, etc.
    // NOTE: if the data object wasn't set, use a new arrays
    this.setData((this.data ? this.data : []));
},



//> @method cellSelection.setData()     (A)
//   Initialize selection data.<br><br>
//   Call this method to associate the selection with a different data object.<br>
//          <i>Note: No need to call this if the contents of the selection's data is modified</i>
//  @group selection
//  @param  newData (array)  new data to maintain selection in
// @visibility serverSelection
//<
setData : function (newData) {

    // if we are currently pointing to data, stop observing it
    if (this.data != null) this.ignoreData(this.data);

    // remember the new data
    this.data = newData;

    // observe the new data so we will update automatically when it changes
    if (this.data != null) this.observeData(this.data);
},


//> @method cellSelection.observeData() (A)
//   Observe methods on the data so we change our state.
//   Called automatically by cellSelection.setData().
//  @group selection
//
// @param data (array)  new data to be observed
// @visibility internal
//<
observeData : function (data) {
    this.observe(data, "dataChanged", "observer._dirty = true");
},



//> @method cellSelection.ignoreData() (A)
//   Stop observing methods on data when it goes out of scope.
//   Called automatically by setData
//  @group selection
//
//  @param data (array)  old data to be ignored
// @visibility internal
//<
ignoreData : function (data) {
    this.ignore(data, "dataChanged");
},



//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~ Selection Tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



//> @method cellSelection.cellIsSelected()
// Return true if a particular item is selected
// @param rowNum (int) row index of the cell to check
// @param colNum (int) column index of the cell to check
// @return (Boolean) true == object is selected
//                      false == object is not selected
// @group selection
// @visibility external
//<
cellIsSelected : function (rowNum, colNum) {
    var row = isc.isAn.Object(rowNum) ? rowNum : this.data.get(rowNum),
        rowSelection = (row ? row[this.selectionProperty] : null),
        rowChunkSelection = (rowSelection ? rowSelection[Math.floor(colNum/32)] : null),
        colSelectionFlag = isc.CellSelection.COL_SELECTION_FLAGS[colNum%32];
    return (rowChunkSelection != null && ((rowChunkSelection & colSelectionFlag) != 0));
},



rowHasSelection : function (rowNum) {
    var row = this.data.get(rowNum),
        rowSelection = (row ? row[this.selectionProperty] : null),
        numRowChunks = Math.ceil(this.numCols/32);

    // if row doesn't exist or row selection property is null/zero, return false
    if (!row || !row[this.selectionProperty]) return false;

    // otherwise check each chunk of the row for a selection (true == nonzero value)
    for (var i = 0; i < numRowChunks; i++) {
        if (rowSelection[i]) return true;
    }
    // made it through all row chunks with no selection, so return false
    return false;
},


colHasSelection : function (colNum) {
    if (colNum > this.numCols - 1) return false;

    var colSelectionFlag = isc.CellSelection.COL_SELECTION_FLAGS[colNum%32],
        rowChunkNum = Math.floor(colNum/32),
        data = this.data;

    // if data hasn't loaded yet don't attempt to traverse rows
    if (isc.isA.ResultSet != null && isc.isA.ResultSet(data) && !data.lengthIsKnown()) {
        return false;
    }

    // iterate through all rows of data
    var numRows = data.getLength();
    for (var i = 0; i < numRows; i++) {

        // getCachedRow() won't trigger fetches if row isn't loaded
        var row = data.getCachedRow(i);
        if (row == null) continue;

        // get selection property for the current row
        var rowSelection = row[this.selectionProperty];
        if (!rowSelection) continue;

        // if selection property exists, selection chunk is nonzero, and the flag for this column is set, return true
        if (rowSelection && rowSelection[rowChunkNum] && ((rowSelection[rowChunkNum] & colSelectionFlag) != 0)) {
            return true;
        }
    }
    // made it through all rows with no selection, so return false
    return false;
},


//> @method cellSelection.anySelected()
// Is anything in the list selected?
// @return (Boolean) true == at least one item is selected
//                   false == nothing at all is selected
// @visibility external
// @group selection
//<
anySelected : function () {
    if (!this._dirty) return (this.selectedCells != null && this._selectedCells.length > 0);

    var data = this.data;

    // if data hasn't loaded yet don't attempt to traverse rows
    if (isc.isA.ResultSet != null && isc.isA.ResultSet(data) && !data.lengthIsKnown()) {
        return false;
    }

    var numRowChunks = Math.ceil(this.numCols/32);

    // iterate through all rows of data
    var numRows = data.getLength();
    for (var i = 0; i < numRows; i++) {

        // getCachedRow() won't trigger fetches if row isn't loaded
        var row = data.getCachedRow(i);
        if (row == null) continue;

        // get selection property for the current row
        var rowSelection = row[this.selectionProperty];
        if (!rowSelection) continue;

        // check each chunk of the row for a selection (true == nonzero value)
        for (var j = 0; j < numRowChunks; j++) {
            if (rowSelection[j]) return true;
        }
    }
    // made it through all rows with no selection, so return false
    return false;
},



//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~ Selection Getters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

//> @method cellSelection.getSelectedCells()
//  Returns an array of the currently selected cells.  Each cell is returned as a 2 element
//  array in the form <code>[rowNum, colNum]</code>.
// @return (array)  an array of the selected cells, as 2 element arrays
// @visibility external
// @group selection
//<
getSelectedCells : function () {
    // first see if we already have a clean list of selected cells
    if (!this._dirty) return this._selectedCells;

    var selectedCells = [],
        data = this.data,
        colSelectionFlags = isc.CellSelection.COL_SELECTION_FLAGS,
        numRowChunks = Math.ceil(this.numCols/32),
        numRows = data.getLength();

    // if data hasn't loaded yet don't attempt to traverse rows
    if (isc.isA.ResultSet != null && isc.isA.ResultSet(data) && !data.lengthIsKnown()) {
        this._dirty = false;
        return selectedCells;
    }

    // iterate through all rows of data
    for (var i = 0; i < numRows; i++) {

        // getCachedRow() won't trigger fetches if row isn't loaded
        var row = data.getCachedRow(i);
        if (row == null) continue;

        // get selection property for the current row
        var rowSelection = row[this.selectionProperty];
        if (!rowSelection) continue;

        // iterate through all chunks in this row
        for (var j = 0, rowChunkSelection, numColsInChunk; j < numRowChunks; j++) {
            // get selection flags for this chunk
            rowChunkSelection = rowSelection[j];
            if (!rowChunkSelection) continue;

            // how many columns in this chunk (32 for all but the last chunk)
            // NOTE: should use this.numCols%32 for non-chunked CellSelection too
            numColsInChunk = (j == numRowChunks - 1 && this.numCols%32 != 0) ? this.numCols%32 : 32;

            // iterate through the flags to find the selected cells in this chunk
            for (var k = 0; k < numColsInChunk; k++) {
                if ((rowChunkSelection & colSelectionFlags[k]) != 0) {
                    selectedCells[selectedCells.length] = [i,j*32+k];
                }
            }
        }
    }

    // cache and return the list of selected cells
    this._selectedCells = selectedCells;
    this._dirty = false;
    return selectedCells;
},


//    returns an array containing the numbers of all rows that have at least one cell selected
getSelectionRowNums : function () {
    var selectionRowNums = [],
        numRowChunks = Math.ceil(this.numCols/32),
        data = this.data,
        numRows = data.getLength();

    // if data hasn't loaded yet don't attempt to traverse rows
    if (isc.isA.ResultSet != null && isc.isA.ResultSet(data) && !data.lengthIsKnown()) {
        return selectionRowNums;
    }

    // iterate through all rows of data
    for (var i = 0; i < numRows; i++) {

        // getCachedRow() won't trigger fetches if row isn't loaded
        var row = data.getCachedRow(i);
        if (row == null) continue;

        // get selection property for the current row
        var rowSelection = row[this.selectionProperty];
        if (!rowSelection) continue;

        // iterate through chunks in this row
        for (var j = 0, numColsInChunk; j < numRowChunks; j++) {
            // if any selection flags are set, add the current row's number to the list we'll return
            if (rowSelection[j]) {
                selectionRowNums[selectionRowNums.length] = i;
                break;
            }
        }
    }
    return selectionRowNums;
},


//> @method cellSelection.getSelectedRecord()
// Returns the first record that has any cells selected.
// @return (ListGridRecord) first selected record, or null if nothing selected
// @visibility external
// @group selection
//<
getSelectedRecord : function () {
    if (this.cellRecordMode == "cell") return this.getSelection()[0];

    var rowNums = this.getSelectionRowNums(),
        rowNum = rowNums.length == 0 ? null : rowNums[0] ,
        record = rowNum == null ? null : this.data.get(rowNum)
    ;

    return record;
},

// return all records that have any selected cells - see cellRecordMode
getSelection : function () {
    if (this.cellRecordMode == "cell") {
        // return cellRecords - eg, CubeGrid usage
        var selectedCells = this.getSelectedCells();
        if (selectedCells == null) return null;

        var cellRecords = [];
        for (var i = 0; i < selectedCells.length; i++) {
            var selectedCell = selectedCells[i],
                cellRecord = this.target.getCellRecord(selectedCell[0], selectedCell[1]);
            if (cellRecord == null) continue; // record for this cell
            cellRecords.add(cellRecord);
        }
        return cellRecords;
    } else {
        // return one record per row that has any selected cells - eg, ListGrid usage
        var rowNums = this.getSelectionRowNums(),
            records = [];

        if (rowNums && rowNums.length > 0) {
            for (var i = 0; i < rowNums.length; i++) {
                var record = this.data.get(rowNums[i]);
                if (record == null) continue;
                records.add(record);
            }
        }
        return records;
    }
},

selectList : function (list, newState) {
    if (newState == null) newState = true;
    if (!list) return false;

    for (var i = 0; i < list.length; i++) {
        var record = list.get(i),
            index = this.data.indexOf(record)
        ;
        if (index == null || index < 0) continue;
        if (newState) this.selectRow(index);
        else this.deselectRow(index);
    }
},

getSelectionColNums : function () {
    var selectionColNums = [],
        allRowSelections = [],
        colSelectionFlags = isc.CellSelection.COL_SELECTION_FLAGS,
        numRowChunks = Math.ceil(this.numCols/32),
        data = this.data,
        numRows = data.getLength();

    // if data hasn't loaded yet don't attempt to traverse rows
    if (isc.isA.ResultSet != null && isc.isA.ResultSet(data) && !data.lengthIsKnown()) {
        return selectionColNums;
    }

    // bitwise-OR the selection flags for every row into allRowSelections
    for (var i = 0; i < numRows; i++) {

        // getCachedRow() won't trigger fetches if row isn't loaded
        var row = data.getCachedRow(i);
        if (row == null) continue;

        // get selection property for the current row
        var rowSelection = row[this.selectionProperty];
        if (!rowSelection) continue;

        // iterate through chunks in this row
        for (var j = 0, numColsInChunk; j < numRowChunks; j++) {
            // if any selection flags are set, bitwise-OR with selection flags for all previous rows
            if (rowSelection[j]) {
                allRowSelections[j] = allRowSelections[j] | rowSelection[j];
            }
        }
    }

    // if no selections, return now
    if (allRowSelections.length == 0) return selectionColNums;

    // compare allRowSelections flags against each constant in colSelectionFlags
    // to determine which columns have a selected cell
    for (var i = 0, numCols = this.numCols; i < numCols; i++) {
        if ((allRowSelections[Math.floor(i/32)] & colSelectionFlags[i%32]) != 0)
            selectionColNums[selectionColNums.length] = i;
    }

    return selectionColNums;
},


getSelectionBounds : function () {
    var rows = this.getSelectionRowNums(),
        cols = this.getSelectionColNums();
    return [rows.first(), cols.first(), rows.last(), cols.last()];
},

canSelectCell : function(rowNum, colNum) {
    return false;
},


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~ Selection Setters (internal) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



//> @method cellSelection._setCellSelection()
// Select or deselect a particular cell.  All other selection routines call this one.
// <p>
// This method:<pre>
// - Initializes row selection property if none.
// - Saves cell coordinates in this.lastSelectedCell if selection state of
//   this cell changes.
// - Marks selection as dirty if selection state of this cell changes.
// </pre>
//
// @param rowNum (number) number of row
// @param colNum (number) number of column
// @param newState (boolean) desired selection state for this cell
//
// @return (boolean) true if selection state of this cell is changed,
//                    false otherwise
// @visibility internal
//<
_setCellSelection : function (rowNum, colNum, newState) {
    //In order to avoid firing this more often than we need, don't call it if the new state matches the existing state for the cell.
    if (newState && !this.cellIsSelected(rowNum, colNum)) {
        if (!this.target.canSelectCell(rowNum, colNum)) { return false;}
    }

    var row = this.data.getCachedRow(rowNum);
    if (row == null) return false;

    var rowSelection = (row ? row[this.selectionProperty] : null),
        rowChunkNum = Math.floor(colNum/32),
        rowChunkSelection = (rowSelection ? rowSelection[Math.floor(colNum/32)] : 0),
        colSelectionFlag = isc.CellSelection.COL_SELECTION_FLAGS[colNum%32];

    // if the row or column does not exist, return false
    if (!row || colNum > this.numCols - 1) return false;

    // if the row is not enabled, return false
    if (row.enabled == false) return false;

    // if the row has no selection property, initialize it now
    if (rowSelection == null) {
        rowSelection = row[this.selectionProperty] = [];
        for (var i = 0, numChunks = Math.ceil(this.numCols/32); i < numChunks; i++) rowSelection[i] = 0;
    }
    // if this chunk in the row has no selection flags yet, initialize them now
    // NOTE: this will only happen if numCols is changed without throwing away this selection...would this ever happen?
    else if (rowChunkSelection == null) {
        rowSelection[rowChunkNum] = 0;
    }

    // if the cell's selection state is already set to the new state, return false
    if (((rowChunkSelection & colSelectionFlag) != 0) == newState) return false;

    // cell exists, is enabled, and has a different state; so change the state via bitwise XOR
    rowSelection[rowChunkNum] = rowChunkSelection ^ colSelectionFlag;

    // if selecting, remember that this is the last selected cell
    if (newState) this.lastSelectedCell = [rowNum, colNum];

    // mark the cached selection as dirty
    this._dirty = true;

    // return true to indicate that the cell's state was actually changed
    return true;
},

setCellRangeSelection : function (startRowNum, startColNum, endRowNum, endColNum, newState) {
    this.changedCells =
        this._setCellRangeSelection(startRowNum, startColNum, endRowNum, endColNum, newState);
    return this._cellSelectionsChanged();
},

// returns array of [rowNum,colNum] arrays representing cells whose selection state was actually
// changed
_setCellRangeSelection : function (startRowNum, startColNum, endRowNum, endColNum, newState) {

    var changedCells = [],
        minRowNum, maxRowNum, minColNum, maxColNum;

    var data = this.data;
    if (isc.isA.ResultSet != null && isc.isA.ResultSet(data) && !data.lengthIsKnown()) {
        return changedCells;
    }

    if (startRowNum <= endRowNum) {
        minRowNum = startRowNum;
        maxRowNum = endRowNum;
    } else {
        minRowNum = endRowNum;
        maxRowNum = startRowNum;
    }

    if (startColNum <= endColNum) {
        minColNum = startColNum;
        maxColNum = endColNum;
    } else {
        minColNum = endColNum;
        maxColNum = startColNum;
    }

    //>DEBUG
    if (this.logIsDebugEnabled()) {
        this.logDebug((newState ? "selecting " : "deselecting ") +
                      [minRowNum, minColNum] + " through " + [maxRowNum, maxColNum]);
    }
    //<DEBUG

    for (var rowNum = minRowNum; rowNum <= maxRowNum; rowNum++) {
        for (var colNum = minColNum; colNum <= maxColNum; colNum++) {
            if (this._setCellSelection(rowNum, colNum, newState)) {
                changedCells[changedCells.length] = [rowNum, colNum];
            }
        }
    }

    return changedCells;
},


//    sets this.changedCells
//    calls this.selectionChanged()
setCellListSelection : function (cellList, newState) {
    if (!cellList) return false;
    var changedCells = [];

    for (var i = 0, length = cellList.length, rowNum, colNum; i < length; i++) {
        rowNum = cellList[i][0];
        colNum = cellList[i][1];
        if (this._setCellSelection(rowNum, colNum, newState))
            changedCells[changedCells.length] = [rowNum, colNum];
    }

    this.changedCells = changedCells;

    return this._cellSelectionsChanged();
},



// helper called by a bunch of other methods
_cellSelectionsChanged : function () {
    if (this.changedCells.length > 0) {
        this.selectionChanged();
        return true;
    } else
        return false;
},

// routines for maintaining the shift click/arrow navigation origin
_updateSelectionOrigin : function (target, rowNum, colNum) {
    var grid = isc.isA.GridRenderer(target) ? target.grid : target;

    var oldRow = this.originRow,
        oldCol = this.originCol;
    if (rowNum != null && colNum != null) {
        this.originRow = rowNum;
        this.originCol = colNum;
        grid.refreshCellStyle(rowNum, colNum);
    } else {
        delete this.originRow;
        delete this.originCol;
    }
    if (oldRow != null && oldCol != null) {
        grid.refreshCellStyle(oldRow, oldCol);
    }
},

_validateSelectionOrigin : function (target) {
    if (this.originRow == null || this.originCol == null) return false;

    if (!this.cellIsSelected(this.originRow, this.originCol)) {
        this._updateSelectionOrigin(target);
        return false;
    }
    var grid = isc.isA.GridRenderer(target) ? target.grid : target;
    grid.refreshCellStyle(this.originRow, this.originCol);
    return true;
},

_isCellSelectionOrigin : function (rowNum, colNum) {
    if (this.originRow != rowNum || this.originCol != colNum) return false;
    return this.cellIsSelected(rowNum, colNum);
},

//> @method cellSelection.selectionChanged()
// Observable handler fired whenever the cell selection is modified
//
// @visibility external
// @group selection
//<
selectionChanged : function () {},



//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~ Selection Setters (public) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


setCellSelection : function (rowNum, colNum, newState) {
    if (this._setCellSelection(rowNum, colNum, newState)) {
        this.changedCells = [[rowNum, colNum]];
        this.selectionChanged();
        return true;
    } else
        return false;
},



//> @method cellSelection.selectCell()
// Select a particular cell
//
// @param rowNum (int) row index of cell to select
// @param colNum (int) column index of cell to select
// @return (Boolean) true == selection actually changed, false == no change
// @visibility external
// @group selection
//<
selectCell : function (rowNum, colNum) {
    return this.setCellSelection(rowNum, colNum, true);
},



//> @method cellSelection.deselectCell()
// Deselect a particular cell
//
// @param rowNum (int) row index of the cell to select
// @param colNum (int) column index of the cell to select
// @return (Boolean) true == selection actually changed, false == no change
// @visibility external
// @group selection
//<
deselectCell : function (rowNum, colNum) {
    return this.setCellSelection(rowNum, colNum, false);
},


selectCellRange : function (startRowNum, startColNum, endRowNum, endColNum) {
    this.changedCells = this._setCellRangeSelection(startRowNum, startColNum, endRowNum, endColNum, true);
    return this._cellSelectionsChanged();
},



deselectCellRange : function (startRowNum, startColNum, endRowNum, endColNum) {
    this.changedCells = this._setCellRangeSelection(startRowNum, startColNum, endRowNum, endColNum, false);
    return this._cellSelectionsChanged();
},

// simply setting the selectionProperty for each row to 0 or ~0 would seem a
// good shortcut in the following methods, but this wouldn't tell us
// exactly which cells have changed--and setting the style of a cell unnecessarily
// is ~really~ expensive

selectRow : function (rowNum) {
    return this.selectCellRange(rowNum, 0, rowNum, this.numCols-1);
},

deselectRow : function (rowNum) {
    return this.deselectCellRange(rowNum, 0, rowNum, this.numCols-1);
},

selectCol : function (colNum) {
    return this.selectCellRange(0, colNum, this.data.getLength()-1, colNum);
},

deselectCol : function (colNum) {
    return this.deselectCellRange(0, colNum, this.data.getLength()-1, colNum);
},

selectAll : function () {
    return this.selectCellRange(0, 0, this.data.getLength()-1, this.numCols-1);
},

deselectAll : function () {
    return this.deselectCellRange(0, 0, this.data.getLength()-1, this.numCols-1);
},


//> @method cellSelection.selectCellList()
//   select an array of cells
//  @group selection
//  @param  list (array[]) Array of cells to select. Each cell can be specified
//                                      as a 2 element array <code>[rowNum, colNum]</code>
//  @return    (boolean) true == selection actually changed, false == no change
//  @visibility external
//<
selectCellList : function (cellList) {
    return this.setCellListSelection(cellList, true);
},

//> @method cellSelection.deselectCellList()
//   deselect an array of cells
//
//  @group selection
//  @param  list (array[]) Array of cells to deselect. Each cell can be specified
//                                      as a 2 element array <code>[rowNum, colNum]</code>
//  @return    (boolean) true == selection actually changed, false == no change
//  @visibility external
//<
deselectCellList : function (cellList) {
    return this.setCellListSelection(cellList, false);
},



//> @method cellSelection.selectSingleCell()
//   select a single cell and deselect everything else
//  @group selection
//  @param  rowNum (int) row index of cell to select
//  @param  colNum (int) column index of cell to select
//  @return    (Boolean) true == selection actually changed, false == no change
//  @visibility external
//<
selectSingleCell : function (rowNum, colNum) {
    // remember whether this cell was selected before we deselect all cells
    var cellWasSelected = this.cellIsSelected(rowNum, colNum);

    //    deselect all cells, using the helper method so we don't call selectionChanged() yet
    this.changedCells = this._setCellRangeSelection(0, 0, this.data.getLength()-1, this.numCols-1, false);

    //    select this cell
    this._setCellSelection(rowNum, colNum, true);

    //    if this cell wasn't selected before, add it to changedCells
    if (!cellWasSelected && rowNum >= 0)
        this.changedCells[this.changedCells.length] = [rowNum, colNum];
    // XXX else remove it from changedCells...

    return this._cellSelectionsChanged();
},



selectSingleRow : function (rowNum) {
    var changedCells = [];

    // deselect rows before this one
    if (rowNum > 0)
        changedCells = this._setCellRangeSelection(0, 0, rowNum-1, this.numCols-1, false);

    // select this row
    changedCells = changedCells.concat(this._setCellRangeSelection(rowNum, 0, rowNum, this.numCols-1, true));

    // deselect rows after this one
    if (rowNum < this.data.getLength()-1)
        changedCells = changedCells.concat(this._setCellRangeSelection(rowNum+1, 0, this.data.getLength()-1, this.numCols-1, false));

    this.changedCells = changedCells;

    return this._cellSelectionsChanged();
},



selectSingleCol : function (colNum) {
    var changedCells = [];

    // deselect columns before this one
    if (colNum > 0)
        changedCells = this._setCellRangeSelection(0, 0, this.data.getLength()-1, colNum-1, false);

    // select this column
    changedCells = changedCells.concat(this._setCellRangeSelection(0, colNum, this.data.getLength()-1, colNum, true));

    // deselect columns after this one
    if (colNum < this.numCols-1)
        changedCells = changedCells.concat(this._setCellRangeSelection(0, colNum+1, this.data.getLength()-1, this.numCols-1, false));

    this.changedCells = changedCells;

    return this._cellSelectionsChanged();
},


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~ Event-based Selection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


//> @method cellSelection.selectOnMouseDown() (A)
//  @group selection, mouseEvents
//   Update the selection as the result of a mouseDown event.
//   Handles shift, control, etc. key selection as well.
//   Call this from a mouseDown handler.
//  @see GridRenderer.mouseDown()
//      @see    ListGrid.mouseDown()
//
//  @param target (Canvas) target object
//  @param recordNum (number) record number mouse went down on
//
//  @return   (boolean) true == selection was changed, false == no change
//<
selectOnMouseDown : function (target, rowNum, colNum) {
    var selectionType = target.selectionType;

    // if the target's selectionType is NONE, just bail
    if (selectionType == isc.Selection.NONE) return false;

    // clear flags for deselecting cells on mouseUp
    // these are set in the simple and normal cases below (3 and 5)
    // see selectOnMouseUp() for details
    this.deselectCellOnMouseUp = false;
    this.deselectOthersOnMouseUp = false;

    var cellSelected = this.cellIsSelected(rowNum, colNum);

    // Case 1: Shift-selection (select contiguous range of cells)
    if (isc.EventHandler.shiftKeyDown() &&
        selectionType != isc.Selection.SINGLE)
    {
        if (this._validateSelectionOrigin(target)) {
            this.startRow = this.originRow;
            this.startCol = this.originCol;
            this.selectOnDragMove(target, rowNum, colNum);
            return true;
        }
        // re-establish origin by forcing fall-through as SIMPLE mode
        selectionType = isc.Selection.SIMPLE;
        cellSelected  = false;
    }

    // remember mouseDown location in case we start drag selecting
    this.startRow = rowNum; delete this.lastRow;
    this.startCol = colNum; delete this.lastCol;

    // Case 2: SINGLE selection
    if (selectionType == isc.Selection.SINGLE) {
        this.selectSingleCell(rowNum, colNum);
        return true;

    // Case 3: SIMPLE selection (toggle selection of this cell, but defer deselection until mouseUp)
    } else if (selectionType == isc.Selection.SIMPLE) {
        if (!cellSelected) {
            this.selectCell(rowNum, colNum);
            this._updateSelectionOrigin(target, rowNum, colNum);
            return true;
        } else {
            this.deselectCellOnMouseUp = true;
            return false;
        }

    // Case 4: meta-key selection (simply toggle selection of this record)
    } else if (isc.EventHandler.modifierKeyDown()) {
        this.setCellSelection(rowNum, colNum, !cellSelected);
        if (!cellSelected) this._updateSelectionOrigin(target, rowNum, colNum);
        return true;

    // Case 5: normal selection (no modifier keys)
    } else {
        if (!cellSelected) {
            // if you click outside of the selection, select the new cell and deselect everything
            // else
            this.selectSingleCell(rowNum, colNum);
            this._updateSelectionOrigin(target, rowNum, colNum);
            return true;
        } else if (isc.EventHandler.rightButtonDown()) {
            // never deselect if you right click on the selection, unless you start drag selecting
            this.deselectOnDragMove = true;
            return false;
        } else {
            // simpleDeselect mode: this mode is designed to make it easy to entirely get rid of
            // your selection, so you don't have to know about ctrl-clicking.  In a nutshell, if you
            // click on the existing selection, it will be entirely deselected.

            if (this.dragSelection) {
                if (this.simpleDeselect) {
                    // if you click on the selection, deselect the entire selection including the
                    // clicked-on cell.  Later, if a drag begins, select the clicked-on cell.
                    this.deselectAll();
                    this.selectOriginOnDragMove = true;
                    return true;
                }
                // for a drag selection, deselect others immediately; otherwise we'll be dragging
                // out a new selection within/overlapping with an existing selection, which we only
                // want to do on a ctrl-click.  This matches Excel.
                this.selectSingleCell(rowNum, colNum);
                this._updateSelectionOrigin(target, rowNum, colNum);
                return true;
            } else {
                if (this.simpleDeselect) {
                    // deselect everything on mouseUp, including the cell clicked on
                    this.deselectAllOnMouseUp = true;
                } else {
                    // if we click in a multiple selection, deselect everything but the clicked-on
                    // item, but don't do it until mouseUp in order to allow dragging the current
                    // selection.  This matches Windows Explorer.
                    var selection = this.getSelectedCells();
                    this.deselectOthersOnMouseUp = (selection.length > 1);
                    this._updateSelectionOrigin(target, rowNum, colNum);
                }
                return false;
            }
        }
    }
},

//> @method cellSelection.selectOnDragMove() (A)
// during drag selection, update the selection as a result of a dragMove event
// @group selection, mouseEvents
//<
selectOnDragMove : function (target, currRow, currCol) {
    var startRow = this.startRow,
        startCol = this.startCol;

    // if not present, init last row, col from the start
    if (this.lastRow == null) this.lastRow = this.startRow;
    if (this.lastCol == null) this.lastCol = this.startCol;

    var lastRow = this.lastRow,
        lastCol = this.lastCol;

    if (currRow < 0 || currCol < 0) {
        //>DEBUG
        this.logWarn("selectOnDragMove: aborting due to negative coordinate: " +
                     [currRow, currCol]);
        //<DEBUG
        return;
    }

    if (currRow == lastRow && currCol == lastCol) return; // no change

    // nop for shift click/navigation; meaingful for new drag operations
    if (target.selectionType != isc.Selection.SINGLE) {
        this._updateSelectionOrigin(target, startRow, startCol);
    }

    // single selection - just shift to the current target cell!
    if (target.selectionType == isc.Selection.SINGLE) {
        this.selectSingleCell(currRow, currCol);
        return;
    }

    //this.logWarn("selectOnDragMove: start: " + [startRow, startCol] +
    //             ", last: " + [lastRow, lastCol] +
    //             " current: " + [currRow, currCol]);

    var changedCells = [];
    if (this.selectOriginOnDragMove) {
        this._setCellSelection(startRow, startCol);
        changedCells.add([startRow, startCol]);
        this.selectOriginOnDragMove = false;

    } else if (this.deselectOnDragMove || this.deselectAllOnMouseUp ||
                this.deselectOthersOnMouseUp)
    {
        // deselect on dragMove is for right-dragging.  The others flags are failsafes in case you
        // use drag selection without setting the flag.
        this.selectSingleCell(startRow, startCol);
        this.deselectAllOnMouseUp = this.deselectOthersOnMouseUp = this.deselectOnDragMove = false;
    }

    // If the mouse has moved further away from the start position since the last dragMove, select
    // more cells.  If it's moved closer to the start position, deselect cells.

    // There are 6 possible orderings of start, last and current position, thus 6 cases per
    // direction.

    // The two orderings (per direction) of (last, start, current) and (current, start, last)
    // indicate the mouse has moved across the start position.  In this case the old selection area
    // and new selection area are guaranteed non-overlapping, except for the single start cell.
    if (
        // if moved on the "rows" axis and crossed the origin
        (currRow != lastRow &&
         ((lastRow >= startRow && startRow >= currRow) ||
          (currRow >= startRow && startRow >= lastRow)))
        ||
        // or we moved on the "cols" axis and crossed the origin
        (currCol != lastCol &&
         ((lastCol >= startCol && startCol >= currCol) ||
          (currCol >= startCol && startCol >= lastCol)))
        )
    {
        // NOTE: The start cell doesn't change, but it would appear to deselect and reselect if we
        // naively deselected the old rect and selected the new, so we manually deselect and select
        // the start cell, so it isn't listed in the changed cells.  Addendum: If we pre-select
        // the start cell in the case where it's not selected, it's state is not updated properly

        // deselect the entire range that has been drag selected so far
        var startCellChanged = this._setCellSelection(startRow, startCol, false);
        changedCells.addList(
            this._setCellRangeSelection(startRow, startCol, lastRow, lastCol, false));

        // then select the new area.
        if (startCellChanged) this._setCellSelection(startRow, startCol, true);
        changedCells.addList(
            this._setCellRangeSelection(startRow, startCol, currRow, currCol, true));

        this.changedCells = changedCells;
        this._cellSelectionsChanged();

        this.lastRow = currRow;
        this.lastCol = currCol;
        return;
    }

    // The other four orderings of last, start and current indicate a shrinking or growth of the
    // selection area, with the new selection area overlapping the old.

    // The increase or decrease in selection always consists of two rectangles, one for the
    // increase/decrease of selection in the horizontal direction, one for the increase/decrease of
    // selection in the vertical direction.  These rectangles will potentially overlap at the
    // corner, so the vertical case includes the corner and the horizontal case does not.

    // NOTE: we manually combine the changed cell list from the two selection changes, so that
    // there's only one selectionChanged() event seen by observers.

    if (currRow >= 0 && currRow != lastRow) {
        // moved vertically
        if (startRow >= lastRow && lastRow > currRow) {
            // increasing selection upward (last < start)
            changedCells.addList(
                this._setCellRangeSelection(currRow, startCol, lastRow-1, lastCol, true));
        } else if (startRow >= currRow && currRow > lastRow) {
            // decreasing selection downward (last < start)
            changedCells.addList(
                this._setCellRangeSelection(lastRow, startCol, currRow-1, lastCol, false));
        } else if (startRow <= currRow && currRow < lastRow) {
            // decreasing selection upward (last > start)
            changedCells.addList(
                this._setCellRangeSelection(currRow+1, startCol, lastRow, lastCol, false));
        } else if (startRow <= lastRow && lastRow < currRow) {
            // increasing selection downward (last > start)
            changedCells.addList(
                this._setCellRangeSelection(lastRow+1, startCol, currRow, lastCol, true));

        }

        // NOTE: we change lastRow because we want the horizontal case to handle the corner area
        // between current and last.
        lastRow = this.lastRow = currRow;
    }

    if (currCol >= 0 && currCol != lastCol) {
        // moved horizontally
        if (startCol >= lastCol && lastCol > currCol) {
            // increasing selection on the left (last < start)
            changedCells.addList(
                this._setCellRangeSelection(startRow, currCol, lastRow, lastCol-1, true));
        } else if (startCol >= currCol && currCol > lastCol) {
            // decreasing selection on the left (last < start)
            changedCells.addList(
                this._setCellRangeSelection(startRow, lastCol, lastRow, currCol-1, false));
        } else if (startCol <= currCol && currCol < lastCol) {
            // decreasing selection on the right (last > start)
            changedCells.addList(
                this._setCellRangeSelection(startRow, currCol+1, lastRow, lastCol, false));
        } else if (startCol <= lastCol && lastCol < currCol) {
            // increasing selection on the right (last > start)
            changedCells.addList(
                this._setCellRangeSelection(startRow, lastCol+1, lastRow, currCol, true));

        }

        this.lastCol = currCol;
    }

    this.changedCells = changedCells;
    this._cellSelectionsChanged();
},

//> @method cellSelection.selectOnMouseUp() (A)s
//   Update the selection as the result of a mouseUp event.
//   We currently use this to defer deselection for drag-and-drop of multiple records.
//   Call this from a mouseUp handler.
//  @group selection, mouseEvents
//  @see ListGrid.mouseUp()
//
//  @param target (Canvas) target object
//  @param recordNum (number) record number mouse went down on
//
//  @return   (boolean) true == selection was changed, false == no change
//<
selectOnMouseUp : function (target, rowNum, colNum) {
    // if the target's selectionType is NONE, just bail
    if (target.selectionType == isc.Selection.NONE) return false;

    var grid = isc.isA.GridRenderer(target) ? target.grid : target;

    var value = false;

    //        If multiselection is on and no modifier keys are down, we need to
    // deselect any cells other than the one that is clicked. BUT, we can't do this in
    // selectOnMouseDown() because the user might be clicking on a cell in a multiple selection
    // to initiate a drag operation with all of the selected cells. So in selectOnMouseDown()
    // we set a deselectOthersOnMouseUp flag that we can check here and do the deselection
    // if necessary.
    if (this.deselectOthersOnMouseUp) {
        this.selectSingleCell(rowNum, colNum);
        this.deselectOthersOnMouseUp = false;
        value = true;
    //        Similarly, if SIMPLE selection is enabled we don't want to deselect the current
    // cell if the user is initiating a drag. We set a deselectCellOnMouseUp flag in this case.
    } else if (this.deselectCellOnMouseUp) {
        this.deselectCell(rowNum, colNum);
        this.deselectCellOnMouseUp = false;
        value = true;
    } else if (this.deselectAllOnMouseUp) {
        this.deselectAll();
        this.deselectAllOnMouseUp = false;
        value = true;
    }
    var validated = this._validateSelectionOrigin(target);
    if (validated && !value) {
        // fire selectionUpdated here to deal with cell-click
        grid.fireSelectionUpdated();
    }
    return value;
},

// create a DependentCellSelection for this CellSelection
getDependentCellSelection : function (numCols) {
    var selection = isc.DependentCellSelection.create({
        numCols: numCols,
        rootSelection: this
    });
    selection.observe(this, "selectionChanged",
                      "observer._selectionChanged()");
    return selection;
}

});


isc.CellSelection.getPrototype().isSelected = isc.CellSelection.getPrototype().cellIsSelected;

// END isc.CellSelection.addMethods()


//> @class DependentCellSelection
//
// A DependentCellSelection object acts like a CellSelection, implementing several
// CellSelection APIs, but has no existence independent from a root real CellSelection
// object.  Its purpose is to present a column-shifted and column-narrowed version
// of its root CellSelection to the client, and it has no state of its own other
// than these two properties.
//<

isc.ClassFactory.defineClass("DependentCellSelection");

isc.DependentCellSelection.addMethods({

_updateDependency : function (firstCol, numCols) {
    if (this.firstCol != firstCol || this.numCols != numCols) {
        this.rootSelection.deselectAll();
    }
    this.firstCol = firstCol;
    if (numCols != null) this.numCols = numCols;
},

cellIsSelected : function (rowNum, colNum) {
    if (colNum < 0 || colNum >= this.numCols) return false;
    return this.rootSelection.cellIsSelected(rowNum, colNum + this.firstCol);
},

selectOnMouseDown : function (target, rowNum, colNum) {
    if (colNum < 0 || colNum >= this.numCols) return false;
    return this.rootSelection.selectOnMouseDown(target, rowNum, colNum + this.firstCol);
},

selectOnDragMove : function (target, currRow, currCol) {
    if (currCol < 0 || currCol >= this.numCols) return false;
    return this.rootSelection.selectOnDragMove(target, currRow, currCol + this.firstCol);
},

selectOnMouseUp : function (target, rowNum, colNum) {
    if (colNum < 0 || colNum >= this.numCols) return false;
    return this.rootSelection.selectOnMouseUp(target, rowNum, colNum + this.firstCol);
},

_selectionChanged : function () {
    var rootChangedCells = this.rootSelection.changedCells;
    var localChangedCells = [];
    for (var i = 0; i < rootChangedCells.length; i++) {
        var cell = rootChangedCells[i],
            col = cell[1] - this.firstCol;
        if (col >= 0 && col < this.numCols) {
            localChangedCells.add([cell[0], col]);
        }
    }
    this.changedCells = localChangedCells;
    this.selectionChanged();
},

destroy : function () {
    if (this.isObserving(this.rootSelection, "selectionChanged"))
        this.ignore(this.rootSelection, "selectionChanged");
    this.rootSelection.deselectAll();
    this.Super("destroy", arguments);
},

selectionChanged : function () {}

}); // END isc.DependentCellSelection.addMethods()

//> @class CellListIterator
//
// The CellListIterator class provides a way to iterate through the rows present in
// a cell list (as generated by +link{CellSelection.getSelectedCells}.  After the
// cell list has been exhausted, and each row present has been returned exactly once by
// +link{CellListIterator.getNextRow}, the function will continue returning consecutive
// integers following the last row actually present, ad infinitum.
//<
isc.ClassFactory.defineClass("CellListIterator");

isc.CellListIterator.addMethods({

    init : function () {
        if (!this.cells) this.cells = [];
        this.currCell = 0;
        this.currRow = -1;
    },

    getNextRow : function () {
        if (this.currCell < this.cells.length) this.currRow = this.cells[this.currCell][0];
        else                                   this.currRow++;

        while (this.currCell < this.cells.length) {
            if (this.cells[this.currCell][0] != this.currRow) break;
            this.currCell++;
        }
        return this.currRow;
    }

}); // END isc.CellListIterator.addMethods()
if (isc.Window) {
    
isc.ClassFactory.defineClass("FieldEditor", "Window");

isc.FieldEditor.addProperties({
        //fields:null, // to be set by the component that uses this
        isModal: true,
        showMinimizeButton: false,
        autoCenter: true,
        autoSize:true,
        defaultWidth:475,
        visibleFieldsConstructor: "ListGrid",
        hiddenFieldsConstructor: "ListGrid",
        showFooter: false,
        title:"Customize Fields",
        showInstructionsPane:true,
        bodyProperties:{ layoutMargin : 5 },
        instructionsPaneDefaults : {
            _constructor:isc.HTMLFlow,
            padding:5,
            height:1
        },
        instructions:"Drag fields between grids to control which fields are visible " +
                     "and the order in which fields are displayed",

        initWidget : function () {
            this.invokeSuper(isc.FieldEditor, "initWidget");

            if (!this.fields) {
                isc.logWarn('FieldEditor can not be created because no fields were provided');
                return;
            }

            this.addItem(this.addAutoChild("instructionsPane", {
                contents:this.instructions
            }));

            this.visibleFieldsDefaults = this.hiddenFieldsDefaults = {
                height:200, width: 200,
                leaveScrollbarGap:false,
                canDragRecordsOut: true,
                canAcceptDroppedRecords: true,
                canReorderRecords: true,
                dragDataAction: "move"                
            };
    

            var visFieldsGrid = this.visibleFieldsGrid = this.createAutoChild("visibleFields", {
                fields:[{
                    name:"title", title: "Visible Fields",
                    formatCellValue : "value || record.name"
                }]
            });
    
            var allFields = this.fields;
    
            var vFields = allFields.findAll("visible", null);
            var hFields = allFields.findAll("visible", false);
    
            visFieldsGrid.setData(vFields);
            var hidFieldsGrid = this.hiddenFieldsGrid = this.createAutoChild("hiddenFields", {
                canReorderRecords: false,
                fields:[{
                    name:"title", title: "Hidden Fields",
                    formatCellValue : "value || record.name"
                }]
            });
   
            hidFieldsGrid.setData(hFields);
            var container = isc.HLayout.create({membersMargin:10,
                layoutMargin: 5,
                height: 1, overflow:"visible",
                members:[
                    visFieldsGrid,
                    isc.VStack.create({width:32, height:74, layoutAlign:"center", membersMargin:10, 
                    members:[
                        isc.Img.create({src:"[SKINIMG]actions/back.png", width:16, height:16,
                            visFieldsGrid: visFieldsGrid, hidFieldsGrid: hidFieldsGrid,
                            layoutAlign:"center",
                            click:"this.visFieldsGrid.transferSelectedData(this.hidFieldsGrid)"
                        }),
                        isc.Img.create({src:"[SKINIMG]actions/forward.png", width:16, height:16,
                            layoutAlign:"center",
                            visFieldsGrid: visFieldsGrid, hidFieldsGrid: hidFieldsGrid,
                            click:"this.hidFieldsGrid.transferSelectedData(this.visFieldsGrid)"
                        })
                    ]}),
                    hidFieldsGrid
                ]
            });
       
            this.addItem(container);
    
            var okButton = this.createAutoChild("okButton", {
                autoDraw: false,
                title: "Done",
                fieldEditor: this,
                click: function () { this.creator.okClick(); },
                layoutAlign:"center"
            }, isc.IButton);
            this.addItem(okButton);
        },


        okClick : function () {
            var vFields = isc.clone(this.visibleFieldsGrid.data);
            var hFields = isc.clone(this.hiddenFieldsGrid.data);
                        
            vFields.setProperty("visible", null);
            hFields.setProperty("visible", false);
                       
            vFields.addList(hFields);

            var fieldState = vFields.getProperties(["name", "visible"]);

            this.done(vFields, fieldState);

            this.hide();
            this.destroy();
        },
        done : function (vFields, fieldState) {
        }
        
});

} // end if (isc.Window)



// ----------------------------------------------------------------------------------------

//> @class FormulaBuilder
// Shows an interface allowing a user to enter simple formulas by typing them into a text
// field.
// <P>
// Available values for the formula are determined by the DataSource fields, and are given
// simple single-letter aliases (such as "A", "B", ...) similar to column names in Excel.
// The set of available values is shown in the +link{formulaBuilder.fieldKey} as a simple
// mapping between the +link{dataSourceField.title,field title} and it's short name.
// <P>
// By default, available math functions are shown in a hover from the
// +link{formulaBuilder.helpIcon,helpIcon} that appears after the formula field.
//
// @treeLocation Client Reference/Data Binding
// @group formulaFields
// @visibility external
//<
isc.ClassFactory.defineClass("FormulaBuilder", "VLayout");

isc.FormulaBuilder.addProperties({
// attributes
vertical: true,
padding: 10,

//> @attr formulaBuilder.dataSource (DataSource or String : null : IRW)
// DataSource providing the available fields for the formulaBuilder.
// <P>
// By default the formulaBuilder will include <b>only</b> fields of numeric type or derived
// from a numeric type.  Set +link{formulaBuilder.fields} to override this.
//
// @group formulaFields
// @visibility external
//<

//> @attr formulaBuilder.dataSources (Array of DataSource : null : IR)
// Set of DataSources providing the available fields for the formulaBuilder.
// <P>
// By default the formulaBuilder will include <b>only</b> fields of numeric type or derived
// from a numeric type.  Set +link{formulaBuilder.fields} to override this.
// <P>
// Note that when multiple dataSources are supplied via the <code>dataSources</code> attribute,
// values to be used in the formula are expected to be nested in the data to which the
// formula is applied using dataSource IDs as prefixes. For example, for a formulaBuilder with
// dataSources set to ["countryDS", "worldDS"], where both countryDS and worldDS have a numeric
// field called "area", a valid record object might look like:
// <pre>
//    {countryDS:{countryName:"United Kingdom", area:243610},
//     worldDS:{area:510072000, landArea:148940000, oceanArea:361132000}}
// </pre>
// This allows the user to include field data from either dataSource in calculations (so in this
// case it would be easy to show percentage of total world surface area, for example).
//
// @group formulaFields
// @visibility rules
//<


//> @attr formulaBuilder.fields (Array of Field : null : IRW)
// Set this to override the underlying set of available fields.
//
// @group formulaFields
// @visibility external
//<

//> @attr formulaBuilder.field (Field : null : IR)
// The Field object representing the field being created or edited.
//
// @group formulaFields
// @visibility external
//<

//> @attr formulaBuilder.editMode (boolean : false : IR)
// Are we editing an existing field?
//
// @group formulaFields
// @visibility external
//<

//> @attr formulaBuilder.defaultSummaryFunction (SummaryFunction or Array of SummaryFunction : undefined : IR)
// The value to supply for a new field's summaryFunction if set; otherwise, if undefined,
// the summaryFunction for a new field is taken from the first input field (if any) of
// the formula or summary (see +link{listGridField.summaryFunction}).
//<

//> @attr formulaBuilder.formulaField (AutoChild TextItem : null : IR)
// TextItem that users type into when entering a formula.
//
// @group formulaFields
// @visibility external
//<
showFormulaField: true,
formulaFieldDefaults: {
    type: "text",
    formItemType: "AutoFitTextAreaItem",
    height: 20,
    width: "*",
    hoverWidth: 300,
    keyPress : function () {
        if (this.form.creator.autoTest) {
            this.fireOnPause("autoTest", {
                target: this.form.creator,
                methodName: "testFunction"
            }, this.form.creator.autoTestDelay);
        }
    }
},

//> @attr formulaBuilder.titleField (AutoChild TextItem : null : IR)
// TextItem that allows users to set the title for this field.
//
// @group formulaFields
// @visibility external
//<
showTitleField: true,
titleFieldDefaults: {
    selectOnFocus: true,
    type: "text",
    width: "*"
},

//> @attr formulaBuilder.showHelpIcon (boolean : true : IR)
// Whether to show the help icon that appears after the +link{formulaField}.
//
// @group formulaFields
// @visibility external
//<
showHelpIcon: true,

//> @attr formulaBuilder.helpIcon (AutoChild FormItemIcon : null : IRA)
// Icon that appears after the +link{formulaField}, showing help on hover.
//
// @group formulaFields
// @visibility external
//<
helpIconDefaults: { src: "[SKIN]actions/help.png"
},

//> @attr formulaBuilder.autoHideCheckBoxLabel (String : "Auto hide fields used in formula" : IRW)
// Text label for the checkbox that allows the user to automatically hide the
// fields used in the formula.
//
// @group i18nMessages
// @visibility external
//<
autoHideCheckBoxLabel: "Auto hide fields used in formula",

//> @attr formulaBuilder.showAutoHideCheckBox (boolean : true : IR)
// Whether to show a checkbox offering the user the ability to automatically
// hide any fields involved in the formula.
//
// @group formulaFields
// @visibility external
//<
showAutoHideCheckBox: true,

//> @attr formulaBuilder.autoHideCheckBox (AutoChild TextItem : null : IR)
// CheckBox that, when selected, hides columns in the component that are used in this formula.
//
// @group formulaFields
// @visibility external
//<
autoHideCheckBoxDefaults: { type: "boolean", align: "right"
},

//> @attr formulaBuilder.builderTypeText (String : "Formula" : IR)
// Indicates whether to use "formula" or some other keyword in various captions and text
//
// @group i18nMessages
// @visibility external
//<
builderTypeText: "Formula",

//> @attr formulaBuilder.helpTextIntro (String : "For basic arithmetic, type in symbols (+-/%) directly.<P>The following functions are also available:" : IR)
// Text that appears in the hover from the +link{helpIcon}, as a pre-amble to the list of
// available functions.
//
// @group i18nMessages
// @visibility external
//<
helpTextIntro: "For basic arithmetic, type in symbols (+-/%) directly.<P>The following functions are also available:",

//> @attr formulaBuilder.mathFunctions (Array of String : null : IR)
// The list of math functions available in this FormulaBuilder, as an array of
// +link{MathFunction, MathFunction} names.
// <P>
// The following function list is supported in FormulaBuilders by default: min(), max(),
// round(), ceil(), floor(), abs(), pow(), sin(), cos(), tan(), ln() and log().
//
// @group formulaFields
// @visibility external
//<

//> @attr formulaBuilder.cancelled (Boolean : false : R)
// Was the builder operation cancelled?  Set to true when the user cancels with the cancel
// button or the dialog's close-button.
//
// @group formulaFields
// @visibility external
//<

//> @attr formulaBuilder.valuePrefix (String : null : IR)
// The prefix to apply to the variable that is inserted in response to a record click in the
// grid that shows the list of available fields.
//
// @group formulaFields
// @visibility external
//<

//> @attr formulaBuilder.valueSuffix (String : null : IR)
// The suffix to apply to the variable that is inserted in response to a record click in the
// grid that shows the list of available fields.
//
// @group formulaFields
// @visibility external
//<

// ------------------------------------------
// autoChildren
//
//> @attr formulaBuilder.fieldKey (AutoChild ListGrid : null : IR)
// ListGrid displaying the list of available fields and their corresponding formula keys.
//
// @group formulaFields
// @visibility external
//<
fieldKeyDefaults: {_constructor: "ListGrid",
    leaveScrollbarGap: false,
    autoFitData: "both",
    autoFitMaxRecords: 8,
    autoFitMaxWidth: 500,
    autoFetchData: true,
    // show a rollover to make it clear which field will be pasted when clicked, but no
    // selection as there's nothing showing detail for a selection
    showRollOver:true, selectionType:"none",
    // Allow the full title of fields to show (may introduce h-scrolling of course)
    autoFitFieldWidths:true,
    autoFitWidthApproach:"both",
    autoFitExpandField:"title",
    detailField: "title",
    canHover: true,
    hoverMode: "detailField",
    defaultFields: [
        {name: "mappingKey", width: 40},
        {name: "title", width: "*"},
        {name: "sourceDS", width: "*", showIf: "list.creator.dataSources != null"},
        {name: "name", showIf: "false"},
        {name: "type", showIf: "false"},
        {name: "length", showIf: "false"}
    ],
    // It's intuitive to attempt to click on the key grid to input entries.
    // Therefore allow this.
    recordClick : function (viewer, record) {
        var formulaField = this.creator.formulaField;
        if (formulaField) {
            // force focus in item before attempting to get the selectionRange otherwise it
            // will return null regardless of selection/caret position
            formulaField.focusInItem();
            var insertValue = this.creator.getInsertValueForRecord(record);
            if (insertValue != null) {
                // apply valuePrefix/valueSuffix
                if (this.creator.valuePrefix != null) insertValue = this.creator.valuePrefix+insertValue;
                if (this.creator.valueSuffix != null) insertValue += this.creator.valueSuffix;
                var value = formulaField.getEnteredValue() || "";
                var selectionRange = formulaField.getSelectionRange(),
                    caretPos;
                if (selectionRange != null) {
                    value = value.substring(0,selectionRange[0])
                        + insertValue + value.substring(selectionRange[1]);
                    caretPos = selectionRange[0] + insertValue.length;
                } else {
                    value += insertValue;
                    caretPos = insertValue.length;
                }
                formulaField.setValue(value);
                formulaField.focusInItem();
                // Ensure the cursor ends up after the newly added formula value.
                formulaField.setSelectionRange(caretPos,caretPos);
                if (this.creator.autoTest) {
                    this.fireOnPause("autoTest", {
                        target: this.creator,
                        methodName: "testFunction"
                    }, this.creator.autoTestDelay);
                }
            }
        }
    }
},

getInsertValueForRecord : function (record) {
    if (record == null) return "";
    var key = record.mappingKey;
    var escapeKey = this.insertEscapedKeys;
    if (escapeKey == null) escapeKey = this.allowEscapedKeys;
    if (escapeKey) {
        // #A is a synonym for #{A} - the latter is preferable for cases where there are
        // enough fields that #AA would be technically ambiguous.
        key = "#{" + key + "}";
    }
    return key;
},


//> @attr formulaBuilder.instructionsTextStart (HTMLString : "The following fields are available for use in this ${builderType}": IRWA)
// The text to display as a preamble to the instruction text that appears in the
// +link{formulaBuilder.instructions, instructions label}.
// <P>
// This is a dynamic string - text within <code>&#36;{...}</code> are dynamic variables and will
// be evaluated as JS code when the message is displayed.
// <P>
// Only one dynamic variable, builderType, is available and represents the type of this
// builder - either Formula or Summary.
// <P>
// Default value returns <P>
// <code>
// <i>The following fields are available for use in this [Formula/Summary]</i>
// </code>
// @group i18nMessages
// @visibility external
//<
instructionsTextStart: "The following fields are available for use in this ${builderType}",

//> @attr formulaBuilder.instructions (AutoChild Label : null : IR)
// Label displaying the instruction text above the fieldKey grid.
//
// @visibility external
//<
instructionsDefaults: {
    _constructor: "Label",
    height: 1,
    extraSpace: 10,
    overflow: "visible"
},

// defaults for dynamicForms that host the various controls
titleFormDefaults: { _constructor: "DynamicForm", extraSpace: 5, autoFocus: true
},
formulaFormDefaults: { _constructor: "DynamicForm", extraSpace: 5
},
hideFieldsFormDefaults: { _constructor: "DynamicForm", extraSpace: 5
},

sampleHeaderDefaults : { _constructor: "Label",
    height: 15,
    extraSpace: 5
},
sampleLabelDefaults : { _constructor: "Canvas",
    height: 40,
    width: "100%",
    align: "center",
    valign: "top",
    extraSpace: 10,
    showHover: true,
    overflow:"hidden",
    styleName: "sampleOutput"
},

//> @attr formulaBuilder.messageLabel (AutoChild Label : null : IR)
// Label used for displaying messages related to the validity of the current formula.
//
// @group formulaFields
// @visibility external
//<
messageLabelDefaults : { _constructor: "Label",
    height: 20,
    width: "100%",
    align: "right",
    valign: "center",
    overflow:"hidden",
    showHover: true
},

// buttonLayout - HLayout to organise the buttons
buttonLayoutDefaults: { _constructor: "HLayout",
    width: "100%",
    height: 20,
    align: "right"
},

//> @attr formulaBuilder.cancelButton (AutoChild Button : null : IR)
// Button to Cancel this FormulaBuilder.  The formula is not tested, formulaBuilder.cancelled
// is set to true and formulaBuilder.fireOnClose is fired.
//
// @group formulaFields
// @visibility external
//<
cancelButtonDefaults: {_constructor: "IButton",
    autoParent: "buttonLayout",
    autoFit:true,
    extraSpace: 10,
    click: function () {
        this.creator.completeEditing(true);
    }
},

//> @attr formulaBuilder.testButton (AutoChild Button : null : IR)
// Button to Test the formula by generating it's function and executing it
//
// @group formulaFields
// @visibility external
//<
testButtonDefaults: {_constructor: "IButton",
    autoParent: "buttonLayout",
    autoFit:true,
    extraSpace: 10,
    click: function () {
        this.creator.testFunction();
    }
},

//> @attr formulaBuilder.saveAddAnotherButton (AutoChild Button : null : IR)
// Button to Save the formula, by generating it's function, testing it and firing
// formulaBuilder.fireOnClose, and then start editing another, new one.
//
// @group formulaFields
// @visibility external
//<
saveAddAnotherButtonDefaults: {_constructor: "IButton",
    autoParent: "buttonLayout",
    autoFit: true,
    extraSpace: 10,
    click: function () {
        if (!this.creator.showTitleForm || this.creator.titleForm.validate()) this.creator.saveAddAnother();
    }
},


//> @attr formulaBuilder.saveButton (AutoChild Button : null : IR)
// Button to Save the formula, by generating it's function, testing it and firing
// formulaBuilder.fireOnClose
//
// @group formulaFields
// @visibility external
//<
saveButtonDefaults: {_constructor: "IButton",
    autoParent: "buttonLayout",
    autoFit: true,
    click: function () {
        // only validate the titleForm if its showing, of course!
        if (!this.creator.showTitleForm || this.creator.titleForm.validate()) this.creator.save();
    }
},

fieldType:"float",
// when true, allow #A syntax as well as A
allowEscapedKeys: false,

//> @attr formulaBuilder.invalidBuilderPrompt (HTMLString : "Invalid ${builderType}: ${errorText}" : IRWA)
// When +link{formulaBuilder.testFunction, testFunction} reports an invalid formula,
// this attribute provides the error-text to display in the
// +link{formulaBuilder.messageLabel, message-label}.
// <P>
// This is a dynamic string - text within <code>&#36;{...}</code> are dynamic variables and will
// be evaluated as JS code when the message is displayed.
// <P>
// The dynamic variables in this case, builderType and errorText, represent the type of this
// builder, either Formula or Summary, and the description of the error, respectively.
// <P>
// The default output is:<P>
// <code>
// <i>Invalid [Formula/Summary]: + the description of the error detected </i>
// </code>
// @group i18nMessages
// @visibility external
//<
invalidBuilderPrompt: "Invalid ${builderType}: ${errorText}",

//> @attr formulaBuilder.defaultErrorText (String : "[No Explicit Error]" : IRW)
// If an invalid builder prompt is displayed, but no explicit error message was returned when
// attempting to evaluate the formula, this string will be used as a default.
// @see invalidBuilderPrompt
// @group i18nMessages
// @visibility external
//<
defaultErrorText : "[No Explicit Error]",


//> @attr formulaBuilder.invalidBlankPrompt (HTMLString : "Invalid blank ${builderType}" : IRWA)
// When +link{formulaBuilder.testFunction, testFunction} reports an empty formula,
// this attribute provides the error-text to display in the
// +link{formulaBuilder.messageLabel, message-label}.
// <P>
// This is a dynamic string - text within <code>&#36;{...}</code> are dynamic variables and will
// be evaluated as JS code when the message is displayed.
// <P>
// Only one dynamic variable, builderType, is available and represents the type of this
// builder, either Formula or Summary.
// <P>
// The default output is:<P>
// <code>
// <i>Invalid blank [Formula/Summary] </i>
// </code>
// @group i18nMessages
// @visibility external
//<
invalidBlankPrompt: "Invalid blank ${builderType}",

//> @attr formulaBuilder.validBuilderPrompt (HTMLString : "Valid ${builderType}" : IRWA)
// When +link{formulaBuilder.testFunction, testFunction} reports a valid formula and no other
// errors, this attribute provides the error-text to display in the
// +link{formulaBuilder.messageLabel, message-label}.
// <P>
// This is a dynamic string - text within <code>&#36;{...}</code> are dynamic variables and will
// be evaluated as JS code when the message is displayed.
// <P>
// Only one dynamic variable, builderType, is available and represents the type of this
// builder, either Formula or Summary.
// <P>
// The default output is:<P>
// <code>
// <i>Valid [Formula/Summary] </i>
// </code>
// @group i18nMessages
// @visibility external
//<
validBuilderPrompt: "Valid ${builderType}",

//> @attr formulaBuilder.helpWindowTitle (HTMLString : "${builderType} Help" : IRWA)
// The title for the window that opens when the +link{formulaBuilder.helpIcon, Help icon}
// is clicked.
// <P>
// This is a dynamic string - text within <code>&#36;{...}</code> are dynamic variables and will
// be evaluated as JS code when the message is displayed.
// <P>
// Only one dynamic variable, builderType, is available and represents the type of this
// builder, either Formula or Summary.
// <P>
// The default output is:<P>
// <code>
// <i>[Formula/Summary] Help</i>
// </code>
// @group i18nMessages
// @visibility external
//<
helpWindowTitle: "${builderType} Help",

//> @attr formulaBuilder.titleFieldTitle (string : "Title" : IRWA)
// The text to display next to the "Title" field.
// @group i18nMessages
// @visibility external
//<
titleFieldTitle: "Title",

//> @attr formulaBuilder.defaultNewFieldTitle (string : "New Field" : IRWA)
// The default value for new Formula and Summary fields.
// @group i18nMessages
// @visibility external
//<
defaultNewFieldTitle: "New Field",

//> @attr formulaBuilder.keyColumnTitle (string : "Key" : IRWA)
// The default title for the "Key" column in +link{formulaBuilder.fields}.
// @group i18nMessages
// @visibility external
//<
keyColumnTitle: "Key",

//> @attr formulaBuilder.sourceFieldColumnTitle (string : "Source Field" : IRWA)
// The default title for the "Source Field" column in in +link{formulaBuilder.fields}.
// @group i18nMessages
// @visibility external
//<
sourceFieldColumnTitle: "Source Field",

//> @attr formulaBuilder.sourceDSColumnTitle (string : "Source DataSource" : IRWA)
// The default title for the "Source DataSource" column in in
// +link{formulaBuilder.fields}. Only shown if +link{formulaBuilder.dataSources} is used to
// specify multiple dataSources.
//
// @group i18nMessages
// @visibility rules
//<
sourceDSColumnTitle: "Source DataSource",

//> @attr formulaBuilder.cancelButtonTitle (string : "Cancel" : IRWA)
// The default title for the "Cancel" button.
// @group i18nMessages
// @visibility external
//<
cancelButtonTitle: "Cancel",

//> @attr formulaBuilder.saveAddAnotherButtonTitle (string : "Save /& Add Another" : IRWA)
// The default title for the "Save & Add Another" button.
// @group i18nMessages
// @visibility external
//<
saveAddAnotherButtonTitle: "Save & Add Another",

//> @attr formulaBuilder.saveButtonTitle (string : "Save" : IRWA)
// The default title for the "Save" button.
// @group i18nMessages
// @visibility external
//<
saveButtonTitle: "Save",

//> @attr formulaBuilder.saveConfirmationPrompt (HTMLString : "Save changes to this ${builderType}?" : IRWA)
// The text to display in the dialog that opens when there are unsaved changes and the user
// cancels the builder.
// <P>
// This is a dynamic string - text within <code>&#36;{...}</code> are dynamic variables and will
// be evaluated as JS code when the message is displayed.
// <P>
// Only one dynamic variable, builderType, is available and represents the type of this
// builder, either Formula or Summary.
// <P>
// The default output is:<P>
// <code>
// <i>Save changes to this [Formula/Summary]?</i>
// </code>
// @group i18nMessages
// @visibility external
//<
saveConfirmationPrompt: "Save changes to this ${builderType}?",

//> @attr formulaBuilder.invalidGeneratedFunctionPrompt (HTMLString : "The generated function is invalid - Check your ${builderType} and retry." : IRWA)
// When +link{formulaBuilder.testFunction, testFunction} reports an attempt to generate a
// function from an invalid formula, this is the text to display in the
// +link{formulaBuilder.messageLabel, message-label}.
// <P>
// This is a dynamic string - text within <code>&#36;{...}</code> are dynamic variables and will
// be evaluated as JS code when the message is displayed.
// <P>
// Only one dynamic variable, builderType, is available and represents the type of this
// builder, either Formula or Summary.
// <P>
// The default output is:<P>
// <code>
// <i>The generated function is invalid - Check your [Formula/Summary] and retry.</i>
// </code>
// @group i18nMessages
// @visibility external
//<
invalidGeneratedFunctionPrompt: "The generated function is invalid - Check your ${builderType} and retry.",

//> @attr formulaBuilder.sampleHeaderTitle (string : "Sample:" : IRWA)
// The default title for the "Sample" panel, which displays a sample result for the formula.
// @group i18nMessages
// @visibility external
//<
sampleHeaderTitle: "Sample:",

//> @attr formulaBuilder.testButtonTitle (string : "Test" : IRWA)
// The default title for the "Test" button.
// @group i18nMessages
// @visibility external
//<
testButtonTitle: "Test"

});

isc.FormulaBuilder.addMethods({
// methods
getValue : function () {
    return this.formulaField ? this.formulaField.getValue() : null;
},

setValue : function (newValue) {
    if (this.formulaField) {
        this.formulaField.setValue(newValue);
    }
},

//> @method formulaBuilder.setFormula()
// Call to set the formula-string in this FormulaBuilder.
// <P>
// Note that calling setFormula() will update the UI, generate the formula's function and
// test it automatically.
//
// @param (String) The new formula-string for this builder
// @group formulaFields
// @visibility external
//<
setFormula : function (newValue) {
    this.setValue(newValue);
},

getFieldIdProperty : function () {
    return this.getClass().getFieldIdProperty(this.component);
},

getTitle : function () {
    return this.titleField ? this.titleField.getValue() : null;
},

setTitle : function (newTitle) {
    if (this.titleField) {
        this.titleField.setValue(newTitle);
    }
},

getFieldFromMappingKey : function (mappingKey) {
    var fields = this.getAvailableFields();

    for (var i=0; i<fields.length; i++) {
        var item = fields.get(i);
        if (item.mappingKey == mappingKey) return item;
    }
    return null;
},

getFields : function () {
    if (this.fields) return this.fields;

    if (this.component) return this.component.getAllFields();
    var values;
    if (this.dataSources) {
        values = [];
        for (var i =0; i < this.dataSources.length; i++) {
            var ds = this.dataSources[i],
                fields = ds.getFields();
            for (var field in fields) {
                var fbField = isc.addProperties({}, fields[field],
                    {name:ds.getID() + "." + field,
                     sourceDS:ds.getID()});
                values.add(fbField);
            }
        }
    } else {
        values = isc.getValues(this.dataSource.getFields());
    }
    return values;
},

shouldHideUsedFields : function () {
    if (this.showAutoHideCheckBox && this.autoHideCheckBox && this.autoHideCheckBox.getValue()) {
        return this.autoHideCheckBox.getValue();
    } else return false;
},

//> @method formulaBuilder.getHelpText()
// Call to retrieve the text the FormulaBuilder would show by default for help, or override to
// provide alternate help text.
//
// @return (String) The results of getHoverText()
// @group formulaFields
// @visibility external
//<
getHelpText : function () {
    return this.getHoverText();
},

initWidget : function () {
    this.Super("initWidget", arguments);

    // get the dataSource so we know what fields to support
    if (this.dataSource) this.dataSource = isc.DataSource.get(this.dataSource);
    if (this.dataSources) {
        var liveDSs = [];
        for (var i = 0; i < this.dataSources.length; i++) {
            liveDSs[i] = isc.DataSource.get(this.dataSources[i]);
        }
    }

    var availableFields = this.getAvailableFields();

    if (!this.field) {
        this.field = {
            name: this.getUniqueFieldName(),
            title: this.defaultNewFieldTitle,
            type: this.fieldType,
            width: "50",
            canFilter: false,
            canSortClientOnly: true,
            canFilterOnClient: true,
            originalOrder: this.availableFields.length,
            summaryFunction: this.defaultSummaryFunction
        };
    }
    // --------------
    // draw the layout

    // add the fieldKey that displays the list of available fields
    this.instructions = this.createAutoChild("instructions", {
        contents: this.instructionsTextStart.evalDynamicString(this, { builderType: this.builderTypeText })
    });
    this.addMember(this.instructions);

    this.fieldKeyDS = isc.DataSource.create({
        ID: this.getID()+"DS",
        clientOnly: true,
        testData: availableFields,
        fields: [
            {name: "mappingKey", title: this.keyColumnTitle, width: 40},
            {name: "title", title: this.sourceFieldColumnTitle, width: "*"},
            {name:"sourceDS", title: this.sourceDSColumnTitle,
                // showIf overridden in fieldKeyDefaults
                showIf:"false"},

            {name: "name", showIf: "false", primaryKey: true},
            {name: "type", showIf: "false"},
            {name: "length", showIf: "false"}
        ]
    });

    this.fieldKey = this.createAutoChild("fieldKey", {
        dataSource: this.fieldKeyDS
    });
    if (this.fieldKey.showFilterEditor !== false && this.fieldKey.autoFitMaxRecords &&
        availableFields.length > this.fieldKey.autoFitMaxRecords)
    {
        this.fieldKey.setShowFilterEditor(true);
    }
    this.addMember(this.fieldKey);

    // add the titleField that allows the user to re-caption the Field
    if (this.showTitleField) {
        this.addAutoChild("titleForm", {
            fields: [isc.addProperties(
                this.titleFieldDefaults,
                this.titleFieldProperties,
                { title: this.titleFieldTitle, name: "titleField" }
            )]
        });
        this.titleField = this.titleForm.getField("titleField");
        this.setTitle(this.field.title || isc.DataSource.getAutoTitle(this.field.name));
    }

    // add the formulaField TextItem - maybe override visibility of this because FormulaBuilder
    // is useless without a formula!
    if (this.showFormulaField) {
        this.addAutoChild("formulaForm", {
            fields: [isc.addProperties({ title: this.builderTypeText },
                this.formulaFieldDefaults, this.formulaFieldProperties,
                this.showHelpIcon ? {
                icons: [isc.addProperties({ prompt: this.getHelpText() },
                    this.helpIconDefaults, this.helpIconProperties,
                    { click: "form.creator.showHelpWindow();" }
                )]
                } : {},
                { name: "formulaField"}
            )]
        });
        this.formulaField = this.formulaForm.getField("formulaField");
        if (this.showHelpIcon) this.helpIcon = this.formulaField.icons[0];
    }

    // display the test status or error here following a call to testFunction()
    this.addAutoChild("messageLabel");
    // display the test-case here following a call to testFunction()
    this.addAutoChild("sampleHeader", { contents: this.sampleHeaderTitle });
    this.addAutoChild("sampleLabel");

    // add the checkbox that allows hiding of fields used in the formula
    if (this.showAutoHideCheckBox) {
        this.addAutoChild("hideFieldsForm", {
            fields:[ isc.addProperties( { title: this.autoHideCheckBoxLabel },
                this.autoHideCheckBoxDefaults,
                this.autoHideCheckBoxProperties,
                { name: "autoHide" }
            )]
        });
        this.autoHideCheckBox = this.hideFieldsForm.getField("autoHide");
    }

    // show the buttons in a layout
    this.addAutoChild("buttonLayout");
    this.addAutoChild("cancelButton", { title: this.cancelButtonTitle});
    if (!this.autoTest) this.addAutoChild("testButton", { title: this.testButtonTitle});
    this.addAutoChild("saveAddAnotherButton", { title: this.saveAddAnotherButtonTitle });
    this.addAutoChild("saveButton", { title: this.saveButtonTitle });

    if (this.showTitleField) this.titleForm.focusInItem(this.titleField);
    else this.formulaForm.focusInItem(this.formulaField);

    // set the initialValue specific to FormulaBuilder.  Override in subclasses
    this.setInitialValue();

    if (this.editMode && this.autoTest) this.testFunction();
},

getUniqueFieldName : function () {
    return this.getNewUniqueFieldName("formulaField");
},

getNewUniqueFieldName : function (namePrefix) {
    // assume return values in the format "fieldXXX" if namePrefix isn't passed
    if (!namePrefix || namePrefix == "") namePrefix = "field";
    var component = this.component,
        useUUID = (component && component.fieldNamingStrategy || "simple") == "uuid",
        fields = this.getFields(),
        keyLength = namePrefix.length,
        result
    ;

    if (useUUID) {
        result = namePrefix + isc.Math.randomUUID();
    } else {
        var attrName = namePrefix + "Index";
        // if there's a component instance, maintain a count of namePrefix uses on it do we don't duplicate
        if (component && !component.fieldNameGenerator[attrName]) component.fieldNameGenerator[attrName] = 1;
        var maxIncrement = component ? component.fieldNameGenerator[attrName] : 1;

        // find the next available increment for the namePrefix
        for (var i = 0; i<fields.length; i++) {
            var item = fields.get(i);
            if (item.name.startsWith(namePrefix)) {
                var suffix = item.name.substr(keyLength),
                    increment = new Number(suffix);
                if (increment && increment >= maxIncrement) maxIncrement = increment + 1;
            }
        }
        if (component) component.fieldNameGenerator[attrName] = maxIncrement;
        result = namePrefix + maxIncrement;
    }
    // return the new fieldName
    return result;
},

destroy : function () {
    if (this.fieldKeyDS) this.fieldKeyDS.destroy();
    this.Super("destroy", arguments);
},

// set the initialValue specific to FormulaBuilder (field.userFormula). Override in subclasses
setInitialValue : function () {
    if (this.editMode && this.field.userFormula) {
        this.initialValue = this.field.userFormula.text;
        if (this.field.userFormula.allowEscapedKeys) {
            this.allowEscapedKeys = this.field.userFormula.allowEscapedKeys;
        }
    }

    this.initialValue = this.initialValue || "";

    this.setValue(this.initialValue);
},

showHelpWindow : function () {
    var window = this.locatorParent,
        top = window ? window.getTop() : this.top,
        left = window ? window.getRight() : this.left,
        width = window ? window.getVisibleWidth() : this.width,
        height = window ? window.getVisibleHeight() : this.getVisibleHeight();

    if (this.helpWindow && this.helpWindow != null) {
        this.hideHelpWindow();
    } else {
        this.helpIcon.prompt = null;
        this.formulaField.stopHover();

        var _this = this;
        this.helpWindow = isc.Window.create({
            title: this.helpWindowTitle.evalDynamicString(this, { builderType: this.builderTypeText }),
            showMinimizeButton: false,
            showMaximizeButton: false,
            isModal: false,
            closeClick : function () {
                _this.hideHelpWindow();
            },
            headerIconProperties: {
                src: "[SKIN]actions/help.png"
            },

            items: [isc.Label.create({
                contents: this.getHelpText(),
                canSelectText: true, // for copy/paste of function names
                padding: 10
            })]
        });
        // stay on top of the modal mask created by the calling window so that e.g. moving or
        // clicking on the calling window doesn't push the help window behind the click mask
        // (making it unreachable)
        this.helpWindow.observe(window, "bringToFront", "observer.bringToFront()");

        if (window) {
            var neededSpace = window.getRight()+width;
            var overflow = neededSpace-isc.Page.getWidth();
            if (overflow > 0) {
                window.setLeft(window.getLeft()-overflow);
                left = window.getRight();
            }
        }
        this.helpWindow.resizeTo(width, height);
        this.helpWindow.moveTo(left, top);
        this.helpWindow.show();
        if (window) {

        }
    }
},

hideHelpWindow : function () {
    if (this.helpWindow) {
        this.helpWindow.destroy();
        this.helpWindow = null;
    }
    this.helpIcon.prompt = this.getHelpText();
    this.formulaField.stopHover();
},

// Internal method that provides the default help-text when hovering over the helpIcon
getHoverText : function () {
    var output = isc.SB.create();

    output.append("<b>", this.helpTextIntro, "</b> <P>");
    output.append("<ul>");
    var index = isc.MathFunction.getRegisteredFunctionIndex(),
        functions = this.mathFunctions
    ;

    if (functions && functions.length > 0) {
        for (var i=0; i< functions.length; i++) {
            var item = index[functions[i]];
            output.append("<li> <b>", item.name, ": </b> ", item.description, "<p>");
            output.append("<i>usage: ", item.usage, "</i> </li>");
        }
    }
    output.append("</ul>");

    return output.toString();
},

// Get an array of those fields available for use in the formula
// (based on visibility and numeric type)
getAvailableFields : function () {
    if (this.availableFields) return this.availableFields;

    var availableFields = this.availableFields = [],
        currentField = this.field,
        fields = this.getFields(),
        j=0;

    if (!fields) return availableFields;

    for (var i = 0; i < fields.getLength(); i++) {
        var item = isc.addProperties({}, fields.get(i)),
            type = item.type;

        item.originalOrder = i;

        if (currentField) {
            if (currentField.name == item.name) continue;
            // Disallow circular
            // formula fields (one refers to another, which refers back to it!)
            if (item.userFormula) {
                var usedVar = false,
                    vars = item.userFormula.formulaVars || {};
                for (var fieldKey in vars) {
                    if (vars[fieldKey] == currentField.name) {
                        usedVar = true;
                        break;
                    }
                }

                if (usedVar) continue;
            }
        }

        if (item.userFormula ||
            isc.SimpleType.inheritsFrom(type, "integer") ||
            isc.SimpleType.inheritsFrom(type, "float"))
        {
            item.mappingKey = isc.FormulaBuilder.mappingKeyForIndex(j++);
            if (!item.title) item.title = isc.DataSource.getAutoTitle(item.name);
            availableFields.add(item);
        }
    }

    // now, move field mappingKeys around according to those keys already used in the formula
    var vars = currentField && currentField.userFormula ?
                currentField.userFormula.formulaVars : {}
    ;


    var badVars = [];
    for (var key in vars) {
        var mappedField = availableFields.find("mappingKey", key),
            actualField = availableFields.find("name", vars[key])
        ;
        if (actualField == null) { badVars.add(key); continue; }
        // if there's a field already associated with the key bound to the actual field,
        // give that field the key we originally wanted to assign to the actual field
        if (mappedField) mappedField.mappingKey = actualField.mappingKey;
        actualField.mappingKey = key;
    }
    if (badVars.length > 0) {
        var missingMarker = this.component && this.component.missingFormulaFieldValue || "-";
        isc.FormulaBuilder.remapBadVars(badVars, vars, currentField.userFormula, missingMarker);
    }

    isc.FormulaBuilder.sortFields(availableFields, true);
    isc.FormulaBuilder.applyHeaderSpanTitles(this, availableFields);
    return availableFields;
},

// Get an array of used-fields from those fields available for use in the formula
getUsedFields : function (up) {

    var usedFields = [],
        formula = this.getValue(),
        availableFields = this.getAvailableFields().duplicate();

    if (!formula) return usedFields;

    isc.FormulaBuilder.sortFields(availableFields, !!up);

    for (var i = 0; i < availableFields.length; i++) {
        var item = availableFields.get(i);
        if (isc.FormulaBuilder.fieldIsUsed(formula, item.mappingKey, this.allowEscapedKeys)) {
            usedFields.add(item);
        }
    }
    return usedFields;
},

getCompleteValueObject : function () {
    var usedFields = this.getUsedFields(),
        func = this.generateFunction(),
        sortFunc = function (record,field,context) {
            return func(record,context);
        },
        properties = { sortNormalizer: sortFunc, _generatedFormulaFunc: func,
            type: this.fieldType,
            userFormula : { text: this.getValue(), formulaVars: {} }
        },
        fieldIdProperty = this.getFieldIdProperty();


    if (this.allowEscapedKeys) properties.userFormula.allowEscapedKeys = true;

    for (var i=0; i<usedFields.length; i++) {
        var item = usedFields.get(i);
        properties.userFormula.formulaVars[item.mappingKey] = item[fieldIdProperty];
    }

    return properties;
},

getBasicValueObject : function () {
    var usedFields = this.getUsedFields(),
        userFormula = { text: this.getValue(), formulaVars: {} },
        fieldIdProperty = this.getFieldIdProperty();

    if (this.allowEscapedKeys) userFormula.allowEscapedKeys = true;

    for (var i=0; i<usedFields.length; i++) {
        var item = usedFields.get(i);
        userFormula.formulaVars[item.mappingKey] = item[fieldIdProperty];
    }

    return userFormula;
},

//> @method formulaBuilder.getUpdatedFieldObject()
// Returns the entire property-set for the updated field, including title and formula-related
// properties
//
// @return (Field) The original field along with the updated title and formula
// @group formulaFields
// @visibility external
//<
getUpdatedFieldObject : function () {
    return isc.addProperties( this.field,
        { title: this.getTitle() },
        this.getCompleteValueObject()
    );
},

//> @method formulaBuilder.testFunction()
// Test the formula by generating it's function and trying to run it.
// @return (string) result of the function
// @group formulaFields
// @visibility external
//<
testFunction : function () {
    var result = this.getClass().testFunction(this.field, this.getBasicValueObject(),
        this.component,
        this.getFields(),
        this.testRecord
    );

    var testMessage = "",
        errorText = result.errorText || this.defaultErrorText;

    if (result.failedGeneration || result.failedExecution) {
        testMessage = this.invalidBuilderPrompt.evalDynamicString(this, {
                        builderType: this.builderTypeText,
                        errorText: errorText
                    });
    } else if (result.emptyTestValue) {
        testMessage = this.invalidBlankPrompt.evalDynamicString(this, {
                        builderType: this.builderTypeText
                    });
    } else {
        testMessage = this.validBuilderPrompt.evalDynamicString(this, {
                        builderType: this.builderTypeText
                    });
    }

    this.setTestMessage(testMessage);
    this.setSamplePrompt(this.getSamplePrompt(result));

    return result;
},

//> @method formulaBuilder.getTestRecord()
// Gets the +link{formulaBuilder.testRecord, test record} for this formula.
// @return (Object) the +link{formulaBuilder.testRecord, testRecord} for this formula
// @group formulaFields
// @visibility external
//<
getTestRecord : function () {
    if (this.testRecord) return this.testRecord;
    return this.getClass().getTestRecord(this.component, this.getAvailableFields());
},

setTestMessage: function (message) {
    this.messageLabel.setContents(message);
},

setSamplePrompt: function (message) {
    this.sampleLabel.setContents("<center>"+message+"</center>");
},

// Create a function to wrap a calculation
//      * script local vars for all used fields
//      * script local vars for all mapped MathFunctions
//      * return the result of the formula
generateFunction : function () {
    return this.getClass().generateFunction(this.getBasicValueObject(), this.getUsedFields(),
        this.component);
},

//> @method formulaBuilder.saveAddAnother()
// Call to finish working, test the formula and call
// +link{FormulaBuilder.fireOnClose(), fireOnClose()}.  If the formula saves ok, don't close
// the builder but instead reset it, ready to add a new formula-field.
//
// @group formulaFields
// @visibility external
//<
saveAddAnother : function () {
    this.restartBuilder = true;
    this.save();
},

//> @attr formulaBuilder.warnDuplicateTitles (string : null : IRWA)
// Should the user be showed a warning when the entered Title value already exists?
// @visibility external
//<

//> @attr formulaBuilder.warnDuplicateTitlesMessage (HTMLString : "Another field already has the title '${fieldTitle}'.  Continue anyway?" : IRWA)
// The message to display when warnDuplicateTitles is true
// This is a dynamic string - text within <code>&#36;{...}</code> will be evaluated as JS code
// when the message is displayed.
// @group i18nMessages
// @visibility external
//<
warnDuplicateTitlesMessage : "Another field already has the title '${fieldTitle}'.  Continue anyway?",

fieldTitleIsUnique : function (title) {
    var allFields = this.component ? this.component.getAllFields() : null,
        fields = allFields ? allFields.findAll({"title": title}) : null,
        isUnique = true
    ;

    if (fields && fields.length > 0) {
        for (var i=0; i<fields.length; i++) {
            if (fields[i].name != this.field.name) {
                isUnique = false;
                break;
            }
        }
    }

    return isUnique;
},

//> @method formulaBuilder.save()
// Call to finish working, test the formula and call
// +link{FormulaBuilder.fireOnClose(), fireOnClose()}.  Called automatically
// when the Save button is clicked.
//
// @group formulaFields
// @visibility external
//<
save : function () {
    var result = this.testFunction();

    if (this.warnDuplicateTitles && !this.duplicateTitleAccepted) {
        var fieldTitle = this.getTitle();
        if (!this.fieldTitleIsUnique(fieldTitle)) {
            var msg = this.warnDuplicateTitlesMessage.evalDynamicString(this, {
                fieldTitle: fieldTitle
            });

            var _this = this;

            isc.confirm(msg,
                function (value) {
                    if (value) {
                        _this.duplicateTitleAccepted = true;
                        _this.delayCall("save");
                    } else {
                        _this.restartBuilder = false;
                    }
                }
            );
            return null;
        }
    }

    delete this.duplicateTitleAccepted;

    if (result.emptyTestValue) {
        isc.warn(this.invalidBlankPrompt.evalDynamicString(this, { builderType: this.builderTypeText }));
        return;
    } else if (result.failedGeneration || result.failedExecution) {
        isc.warn(this.invalidGeneratedFunctionPrompt.evalDynamicString(this, { builderType: this.builderTypeText }));
        return;
    }

    this.completeEditing(false);
},

// call this to finish working with the builder
completeEditing : function (cancelled, ignoreSaveCheck) {
    this.cancelled = cancelled;
    if (cancelled) {
        if (this.editMode && !ignoreSaveCheck) {
            if (this.getValue() != this.initialValue) {
                var _this = this;
                var message = this.saveConfirmationPrompt.evalDynamicString(this, { builderType: this.builderTypeText });
                isc.confirm(message,
                    function (shouldSave) {
                        if (shouldSave) {
                            _this.save();
                        } else {
                            _this.completeEditing(true, true);
                        }
                    }
                );
                return;
            }
        }
    }
    if (this.helpWindow) this.hideHelpWindow();

    if (this.availableFields) {
        // restore the original field-order
        isc.FormulaBuilder.sortFields(this.availableFields, true);
        this.availableFields.clearProperty("originalOrder");
    }

    this.fireOnClose();
},

//>    @method    formulaBuilder.fireOnClose()    (A)
// Override to execute a callback function when the Formula is Cancelled or Saved.
//
// @group formulaFields
// @visibility external
//<
fireOnClose : function () {},

//> @attr formulaBuilder.autoTest (boolean : true : IRWA)
// When set to true, automatically tests the formula by calling
// +link{formulaBuilder.testFunction(), testFunction()} whenever typing into the
// +link{formulaBuilder.formulaField, formulaField} pauses.
// <P>
// The default is true.
//
// @group formulaFields
// @visibility external
//<
autoTest : true,

//> @attr formulaBuilder.autoTestDelay (integer : 200 : IRWA)
// When +link{formulaBuilder.autoTest} is true, this property indicates the delay in
// milliseconds between a user pausing and +link{formulaBuilder.testFunction(), testFunction()}
// being called.
// <P>
// The default is 200 milliseconds.
//
// @group formulaFields
// @visibility external
//<
autoTestDelay : 200,

//> @attr formulaBuilder.testRecord (Record : null : IRA)
// Record to use when testing the formula dynamically (if +link{formulaBuilder.autoTest} is enabled) or when
// showing samples of formula output.
// <P>
// If not specified, the selected record in the component that launched the FormulaBuilder will
// be used, or if there's no selection, the first visible row, or with no component, a dummy
// data row derived automatically from the provided DataSource.
//
// @group formulaFields
// @visibility external
//<

//> @attr formulaBuilder.samplePrompt (HTMLString : "<nobr>For record: ${title}</nobr><br><nobr>Output: ${output}</nobr>" : IRWA)
// This is a dynamic string - text within <code>&#36;{...}</code> will be evaluated as JS code
// when the message is displayed.
// <P>
// Default value returns <P>
// <code>
// <i>For Record: + the value of the rows title-field <br>
// Output: + the result of he generated function<br>
// </i>
// </code>
// @group i18nMessages
// @visibility external
//<
samplePrompt : "<nobr>For Record: ${title}</nobr><br><nobr>Output: ${output}</nobr>",

//> @method formulaBuilder.getSamplePrompt()
// Evaluates and returns the dynamic +link{formulaBuilder.samplePrompt} string which is
// displayed beneath the formulaField and updated when typing pauses.
//
// @param (TestFunctionResult) The return value from a call to testFunction().
// @return (string) Caption displaying dynamic row-title and the result of the formula
// @group i18nMessages
// @visibility external
//<
getSamplePrompt : function (result) {
    // if there's no DS, we can't use getTitleField() - instead, just get the first key from
    // the record object and use the value of record[firstKey]

    var titleField = this.dataSource ? this.dataSource.getTitleField() : isc.firstKey(result.record),
        output = result.result != null ? result.result :
            this.invalidBuilderPrompt.evalDynamicString(
                this,
                { builderType: this.builderTypeText,
                  errorText:result.errorText || this.defaultErrorText }),
        title = result.record[titleField];
    return this.samplePrompt.evalDynamicString(this, { title: title, output: output });
}

});

isc.FormulaBuilder.addClassMethods({

// 0 = A, 1 = B, etc.
mappingKeyForIndex : function (index) {
    // Use ascii table for range of A-Z, then repeat as AA, AB, etc.
    // A = 65, Z = 90
    var key = "",
        outerDiv = Math.floor(index/(26*27)),
        innerVal = index % (26*26),
        div = Math.floor( innerVal /26);

    if (outerDiv >= 1) key+= String.fromCharCode(65+(outerDiv-1));

    if (div >= 1) {
        if (outerDiv >= 1) {
            key += String.fromCharCode(65+(div-1));
            key += String.fromCharCode(65+(index-(26*27))%26);
        } else {
            key += String.fromCharCode(65+(div-1));
            key += String.fromCharCode(65+index%26);
        }
    } else {
        if (outerDiv >=1) {
            key += String.fromCharCode(65);
        }
        key += String.fromCharCode(65+index%26);
    }

    return key;
},

getFieldIdProperty : function (component) {
    return component ? component.fieldIdProperty : "name";
},

applyHeaderSpanTitles : function (builder, fields, spans, paramTitle) {
    if (!builder.showHeaderSpanTitles) return;
    spans = spans || builder.headerSpans;
    if (!spans) return;
    for (var i = 0; i < spans.length; i++) {
        var title = paramTitle || "";
        var span = spans[i];
        title += span.title + builder.spanTitleSeparator;
        if (span.spans) {
            this.applyHeaderSpanTitles(builder, fields, span.spans, title);
        } else if (span.fields) {  // ASSERT: Should always be one or the other
            for (var j = 0; j < span.fields.length; j++) {
                var fieldName = span.fields[j];
                for (var k = 0; k < fields.length; k++) {
                    if (fields[k].name == fieldName) {
                        fields[k].title = title + fields[k].title;
                        break;
                    }
                }
            }
        }
    }
},




sortFields : function (fields, direction) {
    fields.sortByProperty("mappingKey", direction,
        function (item, propertyName, context) {
            var result = item[propertyName];
            if      (result.length == 1) result = '99' + result;
            else if (result.length == 2) result = '9'  + result;
            return result;
        });
},


handleKeyExp : function (text, key, mode, replace) {
    switch(mode) {
    case "braced":
        if (replace) return text.replaceAll("#{" + key + "}", replace);
        else         return text.indexOf   ("#{" + key + "}") >= 0;
    case "escaped":
        var regex = new RegExp("#" + key + "(?=$|[^A-Z]+)", "g");
        if (replace) return text.replace(regex, replace);
        else         return regex.test(text);
    default:
    case "simple":
        var regex = new RegExp("(^|[^A-Z]+)" + key + "(?=$|[^A-Z]+)", "g");
        if (replace) return text.replace(regex, "$1" + replace);
        else         return regex.test(text);
    }
},

fieldIsUsed : function (formula, mappingKey, allowEscapedKeys) {
    if (!formula || !mappingKey) return false;

    if (this.handleKeyExp(formula, mappingKey)) return true;

    if (!allowEscapedKeys) return false;

    return this.handleKeyExp(formula, mappingKey, "escaped") ||
           this.handleKeyExp(formula, mappingKey, "braced");
},

// Get an array of those fields used in the formula-string
getFieldDetailsFromValue : function (formula, vars, fields, component, searchOption) {
    var used = isc.shallowClone(vars),
        fieldIdProperty = this.getFieldIdProperty(component),
        fieldDetails = { usedFields: [], missingFields: [] }
    ;
    for (var key in used) {
        var item = used[key],
            isUsed = this.fieldIsUsed(formula, key, searchOption),
            fieldID = fields.findIndex(fieldIdProperty, item);

        if (!fields[fieldID]) {
            if (!isUsed) {
                isc.logWarn("Field " + item + " is not in the list of available fields.  " +
                    "However, it is not used in the formula either - ignoring.");
                delete vars[key];
            } else {
                isc.logWarn("Field " + item + " is not in the list of available-fields");
                fieldDetails.missingFields.add(item);
            }
        } else {
            var field = isc.addProperties({}, fields[fieldID]);
            field.mappingKey = key;
            fieldDetails.usedFields.add(field);
        }
    }

    return fieldDetails;
},

// Test the formula by generating it's function and trying to run it
testFunction : function (field, userFormula, component, usedFields, testRecord) {
    var result = {};
    try {
        result.component = component;
        result.record = this.getTestRecord(component, usedFields, testRecord);
        if (!userFormula.text || userFormula.text == "") {
            result.emptyTestValue = true;
            return result;
        }
        result.jsFunction = this.generateFunction(userFormula, usedFields, component);
        result.result = result.jsFunction(result.record, component);
    } catch (err) {
        if (!result.jsFunction) result.failedGeneration = true;
        result.failedExecution = true;
        result.errorText = err.message;
    }
    return result;
},

getTestRecord : function (component, fields, testRecord) {
    var fieldIdProperty = this.getFieldIdProperty(component),
        record;

    if (testRecord) return testRecord;

    if (component) {
        record = component.getSelectedRecord();

        if (!record) {
            if (component.body) {
                var visibleRows = component.body.getVisibleRows();
                record = visibleRows ? component.getRecord(visibleRows[0]) : component.data.get(0);
            } else {
                record = component.data.get(0);
            }
        }
    }
    if (!record && fields) {
        // no data to use, build a dummy record from the passed fields
        record = {};
        for (var i = 0; i < fields.length; i++) {
            var item = fields.get(i);

            if (item.userFormula) {
                item._generatedFormulaFunc =
                    isc.FormulaBuilder.generateFunction(item.userFormula, fields, component);
                var func = item._generatedFormulaFunc;
                item.sortNormalizer = function (record, field, context) {
                    return func(record, context);
                };
            }

            if (item._generatedFormulaFunc) {
                // this is a formula - get the value of its _generatedFormulaFunc()
                isc.DataSource.setPathValue(record, item[fieldIdProperty], null,
                    item._generatedFormulaFunc(record, component));
            } else if (item.type)
                if (isc.SimpleType.inheritsFrom(item.type, "integer") ||
                    isc.SimpleType.inheritsFrom(item.type, "float"))
                {
                    isc.DataSource.setPathValue(record, item[fieldIdProperty], null, 1);
                } else {
                    isc.DataSource.setPathValue(record, item[fieldIdProperty], null, item[fieldIdProperty]);
                }
            else {
                isc.DataSource.setPathValue(record, item[fieldIdProperty], null, item[fieldIdProperty]);
           }
        }
    }
    return record;
},


// Creates a function to wrap the calculation of a formula.  userFormula contains the
// properties held in field.userFormula.

generateFunction : function (userFormula, fields, component, allowNonNumeric, catchErrors) {

    // Default to using a try...catch block to catch errors
    // Note there are 2 cases we need to catch
    // - Syntax error in the function. We have to catch this here as the "new Function()"
    //   call will fail
    // - Logic error in the function. We catch that within the function itself
    if (catchErrors == null) catchErrors = true;

    var output = isc.SB.create(),
        formula = userFormula.text,
        fieldIdProperty = this.getFieldIdProperty(component),
        fieldDetails = this.getFieldDetailsFromValue(formula, userFormula.formulaVars, fields,
                                                     component, userFormula.allowEscapedKeys),
        usedFields = fieldDetails.usedFields,
        missingFields = fieldDetails.missingFields
    ;

    isc.FormulaBuilder.sortFields(usedFields, false);

    if (missingFields.length == 0) {
        output.append("var nullVars = [];\n");

        if (usedFields.length > 0) {
            // script local vars for record-values
            for (var i = 0; i < usedFields.length; i++) {
                var item = usedFields.get(i),
                    mappingKey = item.mappingKey;
                // The array of field objects available here should have the 'mappingKey'
                // and fieldIdProperty set.

                var fieldName = item[fieldIdProperty],
                    pathCode = "isc.DataSource.getPathValue(record,'" + fieldName + "', field)"
                ;


                var skipInSummary = item.userFormula && component &&
                    !component.shouldApplyUserFormulaAfterSummary(item)

                // Code to extract the appropriate value from the record based on
                // fieldName.
                // Note that if possible we use the DBC._getFieldValue() method -- this
                // handles dataPaths, and "getAtomicType()" logic if present.
                // If we don't have a component, back off to getPathValue() which
                // will still navigate nested objects using dataPath

                output.append("var ");
                output.append(
                    "field=component==null?null:component.getField('",fieldName,"');",
                    "if (field==null && component && component.completeFields)",
                        "field=component.completeFields.find('name','",fieldName,"');",
                    "var ", mappingKey,
                    // use getPathValue so we can handle being passed a dataPath to navigate a
                    // nested structure. Used by the RulesEngine / populate rule code-path.
                    //"=isc.DataSource.getPathValue(record,'", fieldName, "', field)\n;"

                    item.userFormula ? "=component" +
                        (skipInSummary ? "&&!component._isSummaryRecord(record)" : "") +
                                  "?component.getFormulaFieldValue(field, record):" + pathCode
                        : item.userSummary ?
                        "=component?component.getSummaryFieldValue(field, record):" + pathCode
                        : "=" + pathCode,
                    "\n;"
                );

                output.append("if (", mappingKey, " == null || (component && ",
                    mappingKey, " == component.badFormulaResultValue) || (!component && ",
                    mappingKey, " == '.')) nullVars.add('", mappingKey, "');");

                if (userFormula.allowEscapedKeys) {
                    formula = this.handleKeyExp(formula, mappingKey, "escaped", mappingKey);
                    formula = this.handleKeyExp(formula, mappingKey, "braced", mappingKey);
                }
            }
            output.append("\n");
        }

        // script local vars for MathFunction-pointers
        var functions = isc.MathFunction.getRegisteredFunctions();
        if (functions && functions.length > 0) {
            output.append("var functions=isc.MathFunction.getRegisteredFunctionIndex(),\n");
            for (var i = 0; i < functions.length; i++) {
                var item = functions.get(i);
                output.append("        ");
                output.append(item.name, "=", "functions.", item.name, ".jsFunction");
                output.append(i == functions.length - 1 ? ";" : ",", "\n");
            }
            output.append("\n");
        }

        if (catchErrors) {
            output.append("try{\n");
        }

        // If NaN, use badFormulaResultValue
        output.append("var value=" , formula , ";");

        if (catchErrors) {
            var errorMessage = "Attempt to evaluate formulaFunction " + formula +
                " failed. Error message:";

            output.append("\n} catch (e) { (component||isc).logWarn(",
                                errorMessage.asSource(true)," + e.message); }\n");

        }

        output.append(
            (allowNonNumeric ? null :

                "if (!isFinite(value) || nullVars.length > 0) " +
                "return (component && component.badFormulaResultValue) || '.'; "),
            "return value;");
    } else {
        this.logWarn("Formula failed due to missing fields: " + missingFields.join(", ") + ".");
        var result = (component && component.badFormulaResultValue) || ".";
        if (result) result = "'" + result + "'";
        output.append("return ", result, ";");
    }

    // return the wrapped function
    var content = output.toString();
    //this.logWarn("content\n:" + content);
    var func;
    if (catchErrors) {
        try {
            func = isc._makeFunction("record,component", content);
        } catch (e) {
            this.logWarn("Error attempting to convert formula text '" + formula +
                "' to a function:" + e.message);
            func = isc._makeFunction("record,component", "return null;");
        }
    } else {
        func = isc._makeFunction("record,component", content);
    }

    return func;

},

remapBadVars : function (badVars, vars, formula, missingMarker) {
    for (var i = 0; i < badVars.length; i++) {
        var key = badVars[i],
            text = formula.text || "";
        formula.text = this.handleKeyExp(text, key, "simple", missingMarker);
        if (formula.allowEscapedKeys) {
            formula.text = this.handleKeyExp(formula.text, key, "escaped", missingMarker);
            formula.text = this.handleKeyExp(formula.text, key, "braced",  missingMarker);
        }
        isc.logWarn("Formula variable " + key + " refers to missing field " + vars[key]);
        delete vars[key];
    }
}

});

// -----------------------------------------------------------------------------------------

//> @class SummaryBuilder
// Shows an interface allowing a user to create or edit fields by typing simple
// format-strings into a text field.  The format-strings can include the values of other fields
// and additional text as required.
// <P>
// Available values for the format-string are determined by the DataSource fields, and are given
// simple single-letter aliases (such as "A", "B", ...) similar to column names in Excel.
// The set of available values is shown in the +link{formulaBuilder.fieldKey} as a simple
// mapping between the +link{dataSourceField.title,field title} and it's short name.
// <P>
// To include a field in the format-string, prefix it with a hash sign (#).
//
// @treeLocation Client Reference/Data Binding
// @group summaryFields
// @visibility external
//<
isc.ClassFactory.defineClass("SummaryBuilder", "FormulaBuilder");

isc.SummaryBuilder.addProperties({
// attributes

//> @attr summaryBuilder.builderTypeText (String : "Summary" : IR)
// Indicates whether to use "summary" or some other keyword in various captions and text
//
// @group i18nMessages
// @visibility external
//<
builderTypeText: "Summary",

fieldType:"text",

//> @attr summaryBuilder.dataSource (DataSource or ID : null : IRW)
// @include formulaBuilder.dataSource
// @group summaryFields
// @visibility external
//<

//> @attr summaryBuilder.dataSources (Array of DataSource : null : IR)
// @include formulaBuilder.dataSources
// @group summaryFields
// @visibility rules
//<

//> @attr summaryBuilder.fields (Array of Field : null : IRW)
// DataSource providing the available fields for the SummaryBuilder.
// <P>
// By default the SummaryBuilder will include all fields.  Set +link{summaryBuilder.fields} to
// override this.
//
// @group summaryFields
// @visibility external
//<

//> @attr summaryBuilder.editMode (boolean : false : IR)
// @include formulaBuilder.editMode
// @group summaryFields
// @visibility external
//<

//> @attr summaryBuilder.field (Field : null : IR)
// @include formulaBuilder.field
// @group summaryFields
// @visibility external
//<

//> @attr summaryBuilder.formulaField (AutoChild TextItem : null : IR)
// @include formulaBuilder.formulaField
// @group summaryFields
// @visibility external
//<

//> @attr summaryBuilder.titleField (AutoChild TextItem : null : IR)
// @include formulaBuilder.titleField
// @group summaryFields
// @visibility external
//<

//> @attr summaryBuilder.showHelpIcon (boolean : true : IR)
// @include formulaBuilder.showHelpIcon
// @group summaryFields
// @visibility external
//<

//> @attr summaryBuilder.helpIcon (AutoChild FormItemIcon : null : IRA)
// @include formulaBuilder.helpIcon
// @group summaryFields
// @visibility external
//<

//> @attr summaryBuilder.showAutoHideCheckBox (boolean : true : IR)
// @include formulaBuilder.showAutoHideCheckBox
// @group summaryFields
// @visibility external
//<

//> @attr summaryBuilder.autoHideCheckBox (AutoChild TextItem : null : IR)
// @include formulaBuilder.autoHideCheckBox
// @group summaryFields
// @visibility external
//<

//> @attr summaryBuilder.testRecord (Record : null : IRA)
// Record to use when showing sample output for the format string.
// <P>
// If not specified, the selected record in the component that launched the SummaryBuilder will
// be used, or if there's no selection, the first visible row, or with no component, a dummy
// data row derived automatically from the provided DataSource.
//
// @group formulaFields
// @visibility external
//<

//> @attr summaryBuilder.autoHideCheckBoxLabel (String : "Auto hide fields used in summary" : IRW)
// Text label for the checkbox that allows the user to automatically hide the
// fields used in the summary format.
//
// @group i18nMessages
// @visibility external
//<
autoHideCheckBoxLabel: "Auto hide fields used in Summary",

//> @attr summaryBuilder.helpTextIntro (String : "The following functions are available:" : IR)
// Text that appears in the hover from the +link{helpIcon}, as a pre-amble to the list of
// available format-tokens.
//
// @group i18nMessages
// @visibility external
//<
helpTextIntro: "Building Summary Columns",

// when true, allow #AA syntax for multi-char keys, as well as #{AA}
allowBasicMultiCharKeys: false

});

isc.SummaryBuilder.addMethods({

//> @method summaryBuilder.setSummary()
// Call to set the format-string in this SummaryBuilder.
// <P>
// Note that calling setSummary() will update the UI, generate the summary's function and
// test it automatically.
//
// @param (String) The new format-string for the summary
// @group formulaFields
// @visibility external
//<
setSummary : function (newValue) {
    this.setValue(newValue);
},

//> @method summaryBuilder.getHelpText()
// Call to retrieve the text the SummaryBuilder would show by default for help, or override to
// provide alternate help text.
//
// @return (String) By default, the results of getHoverText()
// @group summaryFields
// @visibility external
//<

// set initialValue and then call this.Super to do the internal work
setInitialValue : function () {
    if (this.editMode && this.field.userSummary) {
        this.initialValue = this.field.userSummary.text;
        if (this.field.userSummary.allowBasicMultiCharKeys) {
            this.allowBasicMultiCharKeys = this.field.userSummary.allowBasicMultiCharKeys;
        }
    }
    this.initialValue = this.initialValue || "";
    this.setValue(this.initialValue);
},

getUniqueFieldName : function () {
    return this.getNewUniqueFieldName("summaryField");
},

// Override providing help-text specific to building Summary columns
getHoverText : function () {
    var output = isc.SB.create(),
            record = this.getTestRecord(),
            fieldIdProperty = this.getFieldIdProperty(),
            fieldA = this.getFieldFromMappingKey("A"),
            fieldAName = fieldA[fieldIdProperty],
            fieldATitle = fieldA ? fieldA.title || fieldA.name : null,
            fieldB = this.getFieldFromMappingKey("B"),
            fieldBName = fieldB ? fieldB[fieldIdProperty] : null,
            fieldBTitle = fieldB ? fieldB.title || fieldB.name : null
        ;

    output.append("<b>", this.helpTextIntro, "</b> <P>");
    output.append("Summary columns are user-created fields that combine dynamic-values " +
        "from other fields in the current record with static text specified by the user.<P>");
    output.append("Dynamic-values are specified by prefixing a mapping-key from the table " +
        "opposite with #");
    if (this.getFields().length > 26) output.append(", or by using #{key} when the key " +
        "is 2 or more characters long,");
    output.append(" and everything else is copied directly into the output.<P>");

    if (this.dataSource) {
        output.append("For example, in the current DataSource, key <b>A</b> maps to field <i>",
            fieldATitle,"</i> and <b>B</b> is <i>", !fieldB ? "missing" : fieldBTitle, "</i>.<P>");
        output.append("So, if we enter the Summary format-string as:<P>",
            "<i>#A is relative to #B</i><P>",
            "then example output using the current data would look like:<P>");

        if (record) {
            var fieldAValue, fieldBValue;
            if (!fieldB) fieldBValue = "{missing}";
            if (!this.component) {
                fieldAValue = record[fieldAName];
                if (fieldB) fieldBValue = record[fieldBName];
            } else {
                var component = this.component,

                    includeHilitesA =
                        component.shouldIncludeHiliteInSummaryField("sample",fieldAName);
                fieldAValue  = includeHilitesA ?
                        component.getStandaloneFieldValue(record, fieldAName) :
                        component.getFormattedValue(record, fieldAName,
                                 component.getRawValue(record, fieldAName));
                if (fieldB) {
                    var includeHilitesB =
                            component.shouldIncludeHiliteInSummaryField("sample",fieldBName);
                    fieldBValue  = includeHilitesB ?
                            component.getStandaloneFieldValue(record, fieldBName) :
                            component.getFormattedValue(record, fieldBName,
                                     component.getRawValue(record, fieldBName));
                 }
            }
            output.append("<i>", fieldAValue, " is relative to ", fieldBValue, "</i><P>");
        }
    }

    return output.toString();
},

// When clicking on a record to insert a key value, always escape it.
insertEscapedKeys: true,

// Get an array of those fields available for use in the summary based on visibility.
getAvailableFields : function () {
    if (this.availableFields) return this.availableFields;

    var availableFields = this.availableFields = [],
        currentField = this.field,
        fields = this.getFields();

    if (!fields) return availableFields;

    for (var i = 0, j = 0; i < fields.getLength(); i++) {
        var item = isc.addProperties({}, fields.get(i));

        if (currentField) {
            if (currentField.name == item.name) continue;
            if (item.userSummary) {
                var usedVar = false,
                    vars = item.userSummary.summaryVars || {};
                for (var fieldKey in vars) {
                    if (vars[fieldKey] == currentField.name) {
                        usedVar = true;
                        break;
                    }
                }
                if (usedVar) continue;
            }
        }

        item.originalOrder = i;

        //if (!item.userSummary) {
            item.mappingKey = isc.FormulaBuilder.mappingKeyForIndex(j++);
            if (!item.title) item.title = isc.DataSource.getAutoTitle(item.name);
            availableFields.add(item);
        //}
    }

    // now, move field mappingKeys around according to those keys already used in the formula
    var vars = currentField && currentField.userSummary ?
                currentField.userSummary.summaryVars : {}
    ;


    var badVars = [];
    for (var key in vars) {
        var mappedField = availableFields.find("mappingKey", key),
            actualField = availableFields.find("name", vars[key])
        ;
        if (actualField == null) { badVars.add(key); continue; }
        // if there's a field already associated with the key bound to the actual field,
        // give that field the key we originally wanted to assign to the actual field
        if (mappedField) mappedField.mappingKey = actualField.mappingKey;
        actualField.mappingKey = key;
    }
    if (badVars.length > 0) {
        var missingMarker = this.component && this.component.missingSummaryFieldValue || "-";
        isc.SummaryBuilder.remapBadVars(badVars, vars, currentField.userSummary, missingMarker);
    }

    isc.FormulaBuilder.sortFields(availableFields, true);
    isc.SummaryBuilder.applyHeaderSpanTitles(this, availableFields);
    return availableFields;
},

// Get an array of used-fields from those fields available for use in the Summary
getUsedFields : function (up) {

    var usedFields = [],
        formula = this.getValue(),
        allowBasicMultiCharKeys = this.allowBasicMultiCharKeys,
        availableFields = this.getAvailableFields().duplicate();


    isc.FormulaBuilder.sortFields(availableFields, !!up);

    for (var i = 0; i < availableFields.length; i++) {
        var item = availableFields.get(i);

        if (isc.SummaryBuilder.fieldIsUsed(formula, item.mappingKey, allowBasicMultiCharKeys)) {
            usedFields.add(item);
        }
    }
    return usedFields;
},

// return the complete set of properties for the builder-type, including functions
getCompleteValueObject : function () {
    var usedFields = this.getUsedFields(),
        func = this.generateFunction(),
        fieldIdProperty = this.getFieldIdProperty(),
        sortFunc = function (record,field,context) {
            return func(record,context);
        },
        properties = { sortNormalizer: sortFunc, _generatedSummaryFunc: func,
            type: this.fieldType,
            userSummary : { text: this.getValue() }
        };

    if (this.allowBasicMultiCharKeys) properties.userSummary.allowBasicMultiCharKeys = true;

    if (usedFields && usedFields.length > 0) {
        properties.userSummary.summaryVars = {};
        for (var i = 0; i < usedFields.length; i++) {
            var item = usedFields.get(i);
            properties.userSummary.summaryVars[item.mappingKey] =
                item[fieldIdProperty];
        }
    }

    return properties;
},

// return the basic set of properties for the builder-type, excluding field-title and functions
getBasicValueObject : function () {
    var usedFields = this.getUsedFields(),
        fieldIdProperty = this.getFieldIdProperty(),
        userSummary = { text: this.getValue(), summaryVars: {} };

    if (this.allowBasicMultiCharKeys) userSummary.allowBasicMultiCharKeys = true;

    for (var i=0; i<usedFields.length; i++) {
        var item = usedFields.get(i);
        userSummary.summaryVars[item.mappingKey] = item[fieldIdProperty];
    }

    return userSummary;
},

// Call the ClassMethod to generate the function for this Format
generateFunction : function (){
    return this.getClass().generateFunction(this.getBasicValueObject(), this.getUsedFields(),
        this.component
    );
},

initWidget: function(){
    this.Super("initWidget", arguments);
}

//>    @method    summaryBuilder.fireOnClose()    (A)
// Override to execute a callback function when the Format is Cancelled or Saved.
//
// @group summaryFields
// @visibility external
//<

//> @method summaryBuilder.save()
// @include formulaBuilder.save
// @visibility external
//<

});

isc.SummaryBuilder.addClassMethods({

// Test the format-string by generating it's function and trying to run it.
// "userSummary" is the properties in field.userSummary
testFunction : function (field, userSummary, component, usedFields, testRecord){
    var result = {},
        fieldIdProperty = this.getFieldIdProperty(component);
    try {
        result.component = component;
        result.record = this.getTestRecord(component, usedFields, testRecord);
        if (userSummary.text == "") {
            result.emptyTestValue = true;
            return result;
        }
        result.jsFunction = this.generateFunction(userSummary, usedFields, component);
        result.result = result.jsFunction(result.record, field[fieldIdProperty], component);

    } catch (err) {
        if (!result.jsFunction) result.failedGeneration = true;
        result.failedExecution = true;
        result.errorText = err.message;
    }
    return result;
},

fieldIsUsed : function (summary, mappingKey, allowBasicMultiCharKeys) {
    if (!summary || !mappingKey) return false;

    if (this.handleKeyExp(summary, mappingKey, "braced")) return true;

    if (mappingKey.length > 1 && !allowBasicMultiCharKeys) return false;

    return this.handleKeyExp(summary, mappingKey, "escaped");
},

// Create a function to produce a summary-value according to the format-string supplied
generateFunction : function (userSummary, fields, component) {
    var output = isc.SB.create(),
        format = userSummary.text,
        fieldIdProperty = this.getFieldIdProperty(component),
        allowBasicMultiCharKeys = userSummary.allowBasicMultiCharKeys,
        fieldDetails = this.getFieldDetailsFromValue(format, userSummary.summaryVars, fields,
                                                     component, allowBasicMultiCharKeys),
        usedFields = fieldDetails.usedFields,
        missingFields = fieldDetails.missingFields
    ;

    isc.FormulaBuilder.sortFields(usedFields, false);

    if (usedFields.length > 0) {
        // script local vars for record-values
        for (var i = 0; i < usedFields.length; i++) {
            var item = usedFields.get(i),
                fieldName = item[fieldIdProperty],
                pathCode = "isc.DataSource.getPathValue(record,'" + fieldName + "')"
            ;
            output.append(
                "var includeHilites = component ? ",
                    // "fieldName" local var within the generated function is the name of the
                    // summary field itself (passed to the function as a param)
                    // "fieldName" var within this for... loop is the name of the used
                    // field, so we quote that to supply it as a hardcoded string within
                    // the generated function.
                    "component.shouldIncludeHiliteInSummaryField(fieldName,'",fieldName,"') :",
                    "false;\n"
            );

            output.append("var ");
            output.append(
                item.mappingKey,
                "= (component ?",
                    "(includeHilites ? ",
                        "component.getStandaloneFieldValue(record, '", fieldName, "') :",
                        "component.getFormattedValue(record, '",fieldName,
                             "',component.getRawValue(record,'",fieldName,"'))) :",
                // No component:
                // use getPathValue so we can handle being passed a dataPath to navigate a
                // nested structure. Used by the RulesEngine / populate rule code-path.
                //" : isc.DataSource.getPathValue(record,'", fieldName, "'));"
                item.userFormula ?
                    "component?component.getFormulaFieldValue(field, record):" + pathCode
                    : item.userSummary ?
                        "component?component.getSummaryFieldValue(field, record):" + pathCode
                    : pathCode,
                ")\n"
                );
            output.append(";\n");

            // first replace tokens in the format #{key}, then in the format #key
            var replaceText = "'+" + item.mappingKey + "+'";
            format = this.handleKeyExp(format, item.mappingKey, "braced", replaceText);
            if (item.mappingKey.length == 1 || userSummary.allowBasicMultiCharKeys) {
                format = this.handleKeyExp(format, item.mappingKey, "escaped", replaceText);
            }
        }
        output.append("\n");
    }

    // Replace disallowed field aliases with component.missingSummaryFieldValue
    var missingMarker = component && component.missingSummaryFieldValue || "-";
    format = format.replace(/#(\{[A-Z][A-Z]?\}|[A-Z])/g, missingMarker);
    if (userSummary.allowBasicMultiCharKeys) {
        format = format.replace(/#([A-Z][A-Z]?)/g, missingMarker);
    }

    // ensure the format-string is properly formed following field-token replacement
    if (format.substr(0, 2) == "'+") { // usedField starts the string (strip leading '+)
        format = format.substr(2);
    } else if (format.substr(0, 1) != "'") { // otherwise start the string (prefix with ')
        format = "'" + format;
    }
    if (format.substr(format.length - 2) == "+'") { // usedField ends the string (strip trailing +')
        format = format.substr(0, format.length - 2);
    } else if (format.substr(format.length - 1) != "'") { // otherwise terminate the string
        format = format + "'";
    }

    output.append("return ", format, ";");

    // return the wrapped function
    var content = output.toString(),
        func = isc._makeFunction("record,fieldName,component", content);

    return func;
},

remapBadVars : function (badVars, vars, summary, missingMarker) {
    for (var i = 0 ; i < badVars.length; i++) {
        var key = badVars[i],
            text = summary.text || "";
        summary.text = this.handleKeyExp(text, key, "braced", missingMarker);
        if (key.length == 1 || summary.allowBasicMultiCharKeys) {
            summary.text = this.handleKeyExp(summary.text, key, "escaped", missingMarker);
        }
        isc.logWarn("Summary variable " + key + " refers to missing field " + vars[key]);
        delete vars[key];
    }
}

});







// Hilite Rule
// ---------------------------------------------------------------------------------------
// Interface for defining and editing a single grid hilite Rule

//>    @class HiliteRule
// A widget for editing the criteria of a single +link{class:DataBoundComponent} hilite.
// The default implementation presents a series of +link{class:FormItem, formItems}
// for selecting the various elements of a simple criterion and a foreground or background
// color.  To specify more complex criteria, specify both foreground and background colors or
// to apply the hilite to multiple fields, you can create an
// +link{class:AdvancedHiliteEditor, advanced hilite rule}.
// <P>
// <i><b>Important Note:</b> this class should not be used directly - it is exposed purely for
// +link{group:i18nMessages, i18n reasons.}</i>
//
// @treeLocation Client Reference/Grids/ListGrid
// @visibility external
//<
isc.defineClass("HiliteRule", "HLayout");

isc.HiliteRule.addProperties({
    height: 1,
    overflow: "visible",

    //> @attr hiliteRule.clause (AutoChild FilterClause : null : IR)
    // AutoChild +link{class:FilterClause} displaying the +link{class:FormItem, formItems} used to
    // specify the criteria for this HiliteRule.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>hiliteRule.clauseProperties</code>.
    //
    // @visibility external
    //<
    clauseConstructor: "FilterClause",

    clauseDefaults: {
        fieldPickerWidth: "*",
        overflow: "visible",

        clauseDefaults: {
            // don't disallow editing of canEdit:false, or canFilter:false fields.
            canEditField:function () {
                return true;
            }
        },
        operatorPickerWidth: 140,
        valueItemWidth: 130,
        excludeNonFilterableFields: false,

        // If a field has a specified displayField with no explicit optionDataSource,
        // pick up the rootDataSource (set up in DBC.editHilites())
        getDefaultOptionDataSource : function (field) {
            if (this.creator && this.creator.rootDataSource) {
                return this.creator.rootDataSource;
            }
            var ds = this.dataSource;
            // Always avoid attempting to fetch against the hiliteCriteria DS
            if (ds && ds.isHiliteCriteriaDS) return null;
            return ds;
        }
    },

    //> @attr hiliteRule.hiliteForm (AutoChild DynamicForm : null : IR)
    // AutoChild +link{class:DynamicForm} displaying the +link{class:FormItem, formItems} used to
    // specify the hiliting properties of this rule.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>hiliteRule.hiliteFormProperties</code>.
    //
    // @visibility external
    //<
    hiliteFormDefaults: {
        _constructor: "DynamicForm",
        numCols: 6,
        colWidths: [60, 60, 60, 60, 60, 40],
        width: 300
    },

    //> @attr hiliteRule.colorFieldTitle (string : "Color" : IR)
    // The title for the Color picker field.
    //
    // @group i18nMessages
    // @visibility external
    //<
    colorFieldTitle: "Color",

    advancedClauseLayoutDefaults: {
        _constructor: "HLayout",
        height: 1,
        overflow: "visible"
    },

    //> @attr hiliteRule.advancedClauseLabel (AutoChild Label : null : IR)
    // AutoChild +link{class:Label} displaying the human-readable description of an advanced
    // hilite-rule.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>hiliteRule.advancedClauseLabelProperties</code>.
    //
    // @visibility external
    //<
    advancedClauseLabelDefaults: {
        _constructor: "Label",
        autoParent: "advancedClauseLayout",
        width: "*",
        overflow: "hidden",
        height: 18,
        valign: "center",
        wrap: false,
        padding: 1
    },

    //> @attr hiliteRule.advancedClauseEditButton (AutoChild ImgButton : null : IR)
    // AutoChild +link{class:ImgButton} displayed by an advanced hilite-rule and used to open
    // it for editing in an +link{class:AdvancedHiliteEditor, advanced hilite editor}.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>hiliteRule.advancedClauseEditButtonProperties</code>.
    //
    // @visibility external
    //<
    advancedClauseEditButtonDefaults: {
        _constructor: "ImgButton",
        autoParent: "advancedClauseLayout",
        width: 18, height: 18, layoutAlign: "center",
        src: "[SKINIMG]/actions/edit.png",
        showRollOver:false, showDown:false, showDisabled:false,
        click: function () { this.creator.editAdvancedRule(); }
    },

    //> @attr HiliteRule.showRemoveButton (boolean : true : IR)
    // If true, show a +link{hiliteRule.removeButton, button} for this HiliteRule, allowing it
    // to be removed.
    //
    // @visibility external
    //<
    showRemoveButton:true,

    //> @attr hiliteRule.removeButtonPrompt (string : "Remove" : IR)
    // The hover prompt text for the +link{hiliteRule.removeButton, remove button}.
    //
    // @group i18nMessages
    // @visibility external
    //<
    removeButtonPrompt: "Remove",

    //> @attr hiliteRule.removeButton (AutoChild ImgButton : null : IR)
    // The Hilite removal ImgButton that appears before this Hilite if +link{showRemoveButton}
    // is set.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>hiliteRule.removeButtonProperties</code>.
    //
    // @visibility external
    //<
    removeButtonDefaults : {
        _constructor:isc.ImgButton,
        width:18, height:18, layoutAlign:"center",
        src:"[SKIN]/actions/remove.png",
        showRollOver:false, showDown:false, showDisabled:false,
        hoverWidth:80,
        click: function () { this.creator.remove(); }
    },

    //> @attr hiliteRule.foregroundColorTitle (string : "Text" : IR)
    // The +link{FormItem.title,title} of the 'Text' color picker.
    //
    // @group i18nMessages
    // @visibility external
    //<
    foregroundColorTitle: "Text",

    //> @attr hiliteRule.backgroundColorTitle (string : "Background" : IR)
    // The +link{FormItem.title,title} of the 'Background' color picker.
    //
    // @group i18nMessages
    // @visibility external
    //<
    backgroundColorTitle: "Background",

    //> @attr hiliteRule.iconFieldTitle (string : "Icon" : IR)
    // The +link{FormItem.title,title} of the 'Icon' picker.
    //
    // @group i18nMessages
    // @visibility external
    //<
    iconFieldTitle: "Icon",


    //> @attr hiliteRule.iconField (AutoChild SelectItem : null : IR)
    // The 'Icon' picker field.
    // <p>
    // +link{iconFieldTitle} is a +link{group:autoChildUsage,passthrough} for the picker's
    // +link{FormItem.title,title}.
    //<
    iconFieldDefaults: {
        type: "SelectItem",
        width: "*",
        defaultValue: "",
        // Always allow a blank value ... we'll add others
        valueMap: {"": ""},
        valueIcons: {"": "[SKINIMG]/blank.gif"},
        // Disable by default -- we'll enable if a real choice is added
        disabled: true,
        showIf : function () {
            return !this.isDisabled();
        }
    }
});

isc.HiliteRule.addMethods({

    initWidget: function () {

        if (!this.isAdvanced && this.hilite) {
            var criteria = this.hilite.criteria;
            if (criteria && criteria.criteria && isc.isAn.Array(criteria.criteria))
                // the criterion we were passed is really an advancedCriteria - switch on
                // this.isAdvanced so we show an appropriate UI
                this.isAdvanced = true;
        }
        if (isc.isA.String(this.dataSource)) this.dataSource = isc.DS.getDataSource(this.dataSource);

        // make sure cssText and textColor/backgroundColor attributes are in sync
        if (this.hilite) this.checkHiliteProperties(this.hilite);

        if (this.isAdvanced) {
            // need to show a removeButton, label and editButton here instead of an isc.FilterClause
            var description = isc.DataSource.getAdvancedCriteriaDescription(
                this.hilite.criteria,
                this.dataSource
            );
            var missingField = (description.indexOf(isc.FilterBuilder.missingFieldPrompt) >= 0);

            this.membersMargin = 2;
            this.addAutoChild("advancedClauseLayout");
            this.addAutoChild("removeButton",
                {
                    autoParent: "advancedClauseLayout"
                }
            );
            this.addAutoChild("advancedClauseLabel",
                {
                    contents: description,
                    prompt: description,
                    disabled: missingField ? true : false
                }
            );
            this.addAutoChild("advancedClauseEditButton", { disabled: missingField ? true : false });
        } else {
            var dsField = this.dataSource.getField(this.fieldName);
            if (dsField == null) dsField = this.dataSource.getFieldForDataPath(this.fieldName);
            var missingField = (dsField == null);
            this.addAutoChild("clause",
                {
                    dataSource: this.dataSource,
                    fieldDataSource: this.fieldDataSource,
                    field: this.dataSource.getField(this.fieldName),
                    fieldName: this.fieldName,
                    criterion: this.hilite ? this.hilite.criteria : null,
                    showRemoveButton: this.showRemoveButton,
                    missingField: missingField,
                    remove : function () {
                        this.creator.remove();
                    }
                }
            );
            this.addMember(this.clause);
            this.addAutoChild("hiliteForm", { disabled: missingField ? true : false,
                items: [
                    { name: "textColor", title: this.foregroundColorTitle, type: "ColorItem",
                        width: "*"
                    },
                    { name: "backgroundColor", title: this.backgroundColorTitle,
                        type: "ColorItem", width: "*"
                    }
                ]
            });
            if (this.hilite) {
                this.hiliteForm.setValues(
                    {
                        textColor: this.hilite.textColor,
                        backgroundColor: this.hilite.backgroundColor
                    }
                );
            }

            var field = this.clause.field;
            if (field && field.type == "boolean") {
                var item = this.hiliteForm.getItem("textColor");
                item.disable();
            }

            var hiliteIconItem = isc.addProperties({}, this.iconFieldDefaults, this.iconFieldProperties, {
                name: "icon",
                title: this.iconFieldTitle
            });

            // Add hiliteIcons to valueMap
            if (this.hiliteIcons) {
                this.hiliteIcons.map(function (icon) {
                    hiliteIconItem.disabled = false;
                    // Map all values to "", to show the icon only. There may be a more elegant
                    // way to do this with pickListProperties.
                    hiliteIconItem.valueMap[icon] = "";
                    // This actually provides the icon for the select item
                    hiliteIconItem.valueIcons[icon] = icon;
                });
            }

            // Add this.hilite.icon to valueMap if not already there
            var icon = this.hilite ? this.hilite.icon : null;
            if (icon && !hiliteIconItem.valueMap[icon]) {
                hiliteIconItem.disabled = false;
                hiliteIconItem.valueMap[icon] = "";
                hiliteIconItem.valueIcons[icon] = icon;
            }

            this.hiliteForm.addItem(hiliteIconItem);
            this.hiliteForm.setValue("icon", this.hilite ? this.hilite.icon : "");

            this.addMember(this.hiliteForm);
            this.iconField = this.hiliteForm.getField("icon");
        }
    },

    checkHiliteProperties : function (hilite) {
        if (!hilite) return;

        if (hilite.cssText) {
            // the hilite has cssText - ensure it coincides with the direct textColor and
            //  backgroundColor attributes
            var cssElements = hilite.cssText.split(";");

            for (var i=0; i<cssElements.length; i++) {
                var item = cssElements[i],
                    parts = item.split(":")
                ;

                if (parts[0] == "textColor" && !hilite.textColor)
                    hilite.textColor = parts[1];
                else if (parts[0] == "backgroundColor" && !hilite.backgroundColor)
                    hilite.backgroundColor = parts[1];
            }
        } else if (hilite.textColor || hilite.backgroundColor) {
            // no cssText but color attributes are set - build cssText now
            hilite.cssText = "";
            if (hilite.textColor)
                hilite.cssText += "color:" + hilite.textColor + ";";
            if (hilite.backgroundColor)
                hilite.cssText += "background-color:" + hilite.backgroundColor + ";";

            //alert(hilite.cssText);
        }
    },

//> @method hiliteRule.remove()
// Remove this HiliteRule.  Default implementation calls markForDestroy().
//
// @visibility external
//<
    remove : function () {
        this.markForDestroy();
    },

//> @method hiliteRule.getHilite()
// Return the hilite definition being edited, including criteria and hilite properties.
//
// @return (Hilite) the hilite
// @visibility external
//<
    getHilite : function () {

        if (this.isAdvanced) {
            // externally edited in advanced editor
            return this.hilite;
        }

        if (this.missingField) return this.hilite;

        var hilite = this.hilite =
                isc.addProperties(this.hilite || {}, { fieldName: this.fieldName }),
            icon = this.hiliteForm.getValue("icon"),
            textColorValue = this.hiliteForm.getValue("textColor"),
            backgroundColorValue = this.hiliteForm.getValue("backgroundColor"),
            criterion = this.clause.getCriterion()
        ;

        if (!criterion && !icon && !textColorValue && !backgroundColorValue) {
            return null;
        }

        // always mark the criteria as advanced, since it always will be
        if (criterion && !criterion._constructor) criterion._constructor = "AdvancedCriteria";
        hilite.criteria = criterion;
        hilite.icon = icon;

        // Note: we need to be idempotent because we're scribbling on a cached object, so
        // multiple calls to getHilite() with e.g. the user twiddling the foreground/background
        // pulldown in between need to return correct data, so be careful here.
        if (textColorValue != null || backgroundColorValue != null) hilite.cssText = "";
        if (textColorValue != null) {
            hilite.textColor = textColorValue;
            hilite.cssText += "color:" + textColorValue + ";";
        }
        if (backgroundColorValue != null) {
            hilite.backgroundColor = backgroundColorValue;
            hilite.cssText += "background-color:" + backgroundColorValue + ";";
        }

        if (this.hilite && this.hilite.id) hilite.id = this.hilite.id;

        return hilite;
    },

//> @method hiliteRule.editAdvancedRule()
// Show an +link{class:AdvancedHiliteEditor} to edit this advanced rule.
//
// @visibility hiliteEditor
//<
    editAdvancedRule : function () {
        var callback = this.getID()+".editAdvancedRuleReply(hilite)";
        this.advancedHiliteDialog = isc.Window.create({
            title: isc.AdvancedHiliteEditor.getInstanceProperty("title"),
            width: Math.round(isc.Page.getWidth()/2),
            height: 1,
            isModal: true,
            showModalMask: true,
            showResizer: true,
            autoSize: true,
            autoCenter: true,
            items: [
                isc.AdvancedHiliteEditor.create({
                    width: "100%", height: "100%",
                    dataSource: this.fieldDataSource ? null : this.dataSource,
                    fieldDataSource: this.fieldDataSource,
                    // inherit rootDataSource - used as default optionDataSource
                    rootDataSource:this.rootDataSource,
                    hilite: this.hilite,
                    hiliteIcons: this.hiliteIcons,
                    hiliteCanReplaceValue: this.hiliteCanReplaceValue,
                    hiliteReplaceValueFieldTitle: this.hiliteReplaceValueFieldTitle,
                    callback: callback
                })
            ]
        });

        this.advancedHiliteDialog.show();
    },

    editAdvancedRuleReply : function (hilite) {
        this.advancedHiliteDialog.hide();
        this.advancedHiliteDialog.markForDestroy();

        if (hilite) {

            this.hilite = hilite;

            var description = isc.DataSource.getAdvancedCriteriaDescription(
                this.hilite.criteria,
                this.dataSource
            );

            this.advancedClauseLabel.setContents(description);
            this.advancedClauseLabel.setPrompt(description);
        }
    }

});


// Hilite Editor
// ---------------------------------------------------------------------------------------
// Interface for defining and editing grid hilites

//>    @class HiliteEditor
// A widget for defining and editing a set of +link{class:HiliteRule, hilite rules} for use by
// +link{class:DataBoundComponent, dataBoundComponents}.  Presents a list of available fields
// and allows editing of simple hilites directly and more complex hilites via
// +link{class:AdvancedHiliteEditor}s.
// <P>
// <i><b>Important Note:</b> this class should not be used directly - it is exposed purely for
// +link{group:i18nMessages, i18n reasons.}</i>
//
// @treeLocation Client Reference/Grids/ListGrid
// @visibility external
//<
isc.defineClass("HiliteEditor", "VLayout");

isc.HiliteEditor.addProperties({

    membersMargin: 5,

    mainLayoutDefaults : {
        _constructor:"HLayout",
        membersMargin: 5,
        overflow: "visible"
    },

    fieldLayoutDefaults: {
        _constructor: "VLayout",
        autoParent: "mainLayout",
        width: 1,
        overflow: "visible"
    },

    //> @attr hiliteEditor.fieldList (AutoChild ListGrid : null : IR)
    // AutoChild +link{class:ListGrid} showing the list of fields to create hilites for.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>hiliteEditor.fieldListProperties</code>.
    //
    // @visibility external
    //<
    fieldListDefaults: {
        _constructor: "ListGrid",
        height: "*",
        autoFitData: "both",
        autoFitMaxWidth: 300,
        autoFitMaxRecords: 5,
        selectionType: "none",
        showRollOver: true,
        width: 1,
        autoFitFieldWidths: true,
        autoFitWidthApproach: "both",
        autoFitExpandField: "title",
        autoParent: "fieldLayout",
        showHoverComponents: true,
        detailField: "prompt",
        hoverMode: "detailField",
        canHover: true,
        recordClick : function (grid, record) {
            this.creator.addRule(record);
        }
    },

    //> @attr hiliteEditor.availableFieldsColumnTitle (string : "Available Fields" : IR)
    // The title for the 'Available Fields' column in the
    // +link{hiliteEditor.fieldList, fieldList}.
    //
    // @group i18nMessages
    // @visibility external
    //<
    availableFieldsColumnTitle: "Available Fields",

    ruleLayoutDefaults : {
        _constructor:"VLayout",
        membersMargin: 1,
        padding: 1,
        overflow: "auto",
        autoParent: "mainLayout"
    },

    //> @attr hiliteEditor.hiliteIcons (Array of SCImgURL : null : IRW)
    // Specifies a list of icons that can be used in +link{Hilite,hilites}.
    // <P>
    // <code>hiliteIcons</code> should be specified as an Array of +link{SCImgURL}.
    // When present, +link{class:HiliteRule}s
    // will offer the user a drop down for picking one of these icons.
    // <P>
    // If the user picks an icon, the created hiliting rule will have +link{hilite.icon} set to
    // the chosen icon.  +link{listGridField.hiliteIconPosition} controls where the icon will
    // appear for that field -- the default is that it appears in front of the normal cell content.
    //
    // @visibility external
    //<

    //> @attr hiliteEditor.hiliteRule (AutoChild HiliteRule : null : IR)
    // AutoChild +link{class:HiliteRule} used to create new simple hilites.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>hiliteEditor.hiliteRuleProperties</code>.
    //
    // @visibility external
    //<
    hiliteRuleDefaults: {
        _constructor: "HiliteRule"
    },

    hiliteButtonsDefaults : {
        layoutAlign: "right",
        _constructor:"HLayout",
        membersMargin:8, height:1
    },

    hiliteButtonsSpacerDefaults : {
        _constructor: "LayoutSpacer",
        width: "*",
        autoParent: "hiliteButtons"
    },

    //> @attr hiliteEditor.addAdvancedRuleButton (AutoChild StatefulCanvas : null : IR)
    // AutoChild +link{class:IButton} that opens an +link{AdvancedHiliteEditor} to create a new
    // advanced rule.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>hiliteEditor.addAdvancedRuleButtonProperties</code>.
    //
    // @visibility external
    //<
    addAdvancedRuleButtonDefaults: {
        _constructor: "IAutoFitButton",
        autoParent: "hiliteButtons",
        align: "center",
        height: 22,
        click: function () {
            this.creator.addAdvancedRule();
        }
    },

    //> @attr hiliteEditor.addAdvancedRuleButtonTitle (string : "Add Advanced Rule" : IR)
    // The title text for the +link{hiliteEditor.addAdvancedRuleButton, add advanced rule}
    // button.
    //
    // @group i18nMessages
    // @visibility external
    //<
    addAdvancedRuleButtonTitle: "Add Advanced Rule",

    //> @attr hiliteEditor.saveButton (AutoChild StatefulCanvas : null : IR)
    // AutoChild +link{class:ImgButton} that saves the hilites in this editor and fires the
    // +link{hiliteEditor.callback, callback}.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>hiliteEditor.saveButtonProperties</code>.
    //
    // @visibility external
    //<
    saveButtonDefaults : {
        _constructor:"IAutoFitButton",
        autoParent:"hiliteButtons",
        click : function () {
            this.creator.saveHilites();
        }
    },

    //> @attr hiliteEditor.saveButtonTitle (string : "Save" : IR)
    // The title text for the +link{hiliteEditor.saveButton, saveButton}.
    //
    // @group i18nMessages
    // @visibility external
    //<
    saveButtonTitle: "Save",

    //> @attr hiliteEditor.cancelButton (AutoChild StatefulCanvas : null : IR)
    // AutoChild +link{class:ImgButton} that cancels this editor without saving
    // any changes, firing the +link{hiliteEditor.callback, callback} with a null
    // parameter.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>hiliteEditor.cancelButtonProperties</code>.
    //
    // @visibility external
    //<
    cancelButtonDefaults : {
        _constructor:"IAutoFitButton",
        autoParent:"hiliteButtons",
        click : function () {
            this.creator.completeEditing();
        }
    },

    //> @attr hiliteEditor.cancelButtonTitle (string : "Cancel" : IR)
    // The title text for the +link{hiliteEditor.cancelButton, cancel button}.
    //
    // @group i18nMessages
    // @visibility external
    //<
    cancelButtonTitle: "Cancel",

    // overall layout
    // ---------------------------------------------------------------------------------------
    defaultWidth:900, defaultHeight:300

    //> @attr hiliteEditor.callback (Callback : null : IR)
    // The callback to fire when +link{hiliteEditor.saveHilites} is called.
    //
    // @visibility external
    //<
});

isc.HiliteEditor.addMethods({

    initWidget : function () {

        this.Super("initWidget", arguments);
        this.addAutoChildren(["mainLayout", "fieldLayout"]);
        this.addAutoChild("fieldList", { fields: [
            { name: "name", showIf: "false" },
            { name: "title", title: this.availableFieldsColumnTitle}
        ]});

        this.addAutoChildren(["ruleLayout", "hiliteButtons", "hiliteButtonsSpacer"]);

        this.addAutoChild("addAdvancedRuleButton", { title: this.addAdvancedRuleButtonTitle});
        this.addAutoChild("saveButton", { title: this.saveButtonTitle});
        this.addAutoChild("cancelButton", { title: this.cancelButtonTitle});

        this.setDataSource(this.dataSource);

        this.setHilites(this.hilites);
    },

    setHiliteIcons : function (hiliteIcons) {
        this.hiliteIcons = hiliteIcons;
    },

    setDataSource : function (ds) {
        this.dataSource = ds;
        if (this.fieldDataSource && !this.fieldDataSource._autoDerived) {
            this.setupFieldList();
        } else if (this.dataSource) {
            this.getClientOnlyFieldDS();
        } else {
            this.logWarn("No DataSource present, can't edit hilites");
        }
        this.fieldList.markForRedraw();
    },

    setFieldDataSource : function (ds) {
        this.fieldDataSource = ds;
        this.setupFieldList();
    },

    // override point - if showFieldList is false, override this method to set up data for
    // whatever replacement list is provided
    setupFieldList : function () {
        this.fieldList.showFilterEditor = true;
        this.fieldList.setDataSource(this.fieldDataSource);
        this.fieldList.setFields([
            { name: "name", showIf: "false" },
            { name: "title", title: this.availableFieldsColumnTitle },
            { name: "type", showIf: "false" },
            { name: "showInSimpleEditor", showIf: "false" }
        ]);
        var crit = { _constructor: "AdvancedCriteria", operator: "or",
            criteria: [
                { fieldName: "showInSimpleEditor", operator: "equals", value: true },
                { fieldName: "showInSimpleEditor", operator: "isNull" }
            ]
        };
        this.fieldList.fetchData(crit);
    },

    // This dataSource is used to pick fields to which hilights can be applied
    getClientOnlyFieldDS : function () {
        var sourceFields = isc.getValues(this.dataSource.getFields());
        var fields = [];
        for (var i = 0; i < sourceFields.length; i++) {
            var field = sourceFields[i];
            // Skip hidden fields or inherited fields

            if (!field.hidden && field.canHilite != false &&
                    this.dataSource.fields[field.name] != null
                )
            {
                if (field.summaryTitle != null) {
                     field = isc.addProperties({}, field, {title: field.summaryTitle});
                }
                fields.add(field);
            }
        }
        this.fieldDataSource = isc.DataSource.create({
            _autoDerived:true,
            // Hang a flag on this DS to simplify debugging
            isClientOnlyFieldDS:true,
            fields: [
                { name: "name", showIf: "false" },
                { name: "title", title: this.availableFieldsColumnTitle },
                { name: "type", showIf: "false" },
                { name: "showInSimpleEditor", type: "boolean", showIf: "false" }
            ],
            cacheData: fields,
            clientOnly: true
        });

        this.setupFieldList();
    },

    //> @method hiliteEditor.addRule()
    // Adds a new HiliteRule for a passed record.
    //
    // @param record (Record) the record containing the name or datapath of the field to which
    //                        this hilite applies
    // @visibility hiliteEditor
    //<
    addRule : function (record) {
        var newRule = this.createAutoChild("hiliteRule", {
            // pass the rootDataSource through, if present, so
            // fields with a specified displayField and no explicit optionDataSource
            // can retrieve options
            rootDataSource:this.rootDataSource,
            width: "100%",

            fieldName: record.dataPath || record.name,
            dataSource: this.dataSource,
            fieldDataSource: this.fieldDataSource,
            hiliteIcons: this.hiliteIcons
        });

        this.showNewHilite(newRule);
    },

    //> @method hiliteEditor.removeRule()
    // Removes the passed +link{class:HiliteRule, HiliteRule} from this editor.
    //
    // @param hiliteRule (HiliteRule) the hiliteRule to remove
    // @visibility external
    //<
    removeRule : function (hiliteRule) {
        if (isc.isA.Number(hiliteRule)) hiliteRule = this.ruleLayout.getMember(hiliteRule);
        if (!isc.isA.HiliteRule(hiliteRule)) return;

        this.ruleLayout.members.remove(hiliteRule);
        hiliteRule.destroy();
    },

    showNewHilite : function (newRule) {
        this.ruleLayout.addMember(newRule);
    },

    //> @method hiliteEditor.addAdvancedRule()
    // Shows a +link{class:AdvancedHiliteEditor, dialog} to add a new Advanced
    // +link{class:HiliteRule,HiliteRule}.
    //
    // @visibility hiliteEditor
    //<
    addAdvancedRule : function () {
        var callback = this.getID()+".addAdvancedRuleReply(hilite)";
        this.advancedHiliteDialog = isc.Window.create({
            title: isc.AdvancedHiliteEditor.getInstanceProperty("title"),
            width: Math.round(isc.Page.getWidth()/2),
            height: 1,
            isModal: true,
            showModalMask: true,
            showResizer: true,
            canDragResize: true,
            autoSize: true,
            autoCenter: true,
            items: [
                isc.AdvancedHiliteEditor.create({
                    width: "100%", height: "100%",
                    // inherit rootDataSource (used for default optionDataSource)
                    rootDataSource: this.rootDataSource,
                    // for the advanced editor, we want to show inherited fields as well local
                    // fields because the user may need to specify criteria using fields that
                    // are not defined in the immediate DS (but that target a visible locally
                    // defined field with a hilite).  So only pass the fieldDataSource to the
                    // AdvancedHiliteEditor if one was explicitly passed to us as opposed to
                    // one we auto-derived
                    dataSource: this.fieldDataSource && !this.fieldDataSource._autoDerived ? null : this.dataSource,
                    fieldDataSource: this.fieldDataSource,
                    hiliteIcons: this.hiliteIcons,
                    hiliteCanReplaceValue: this.hiliteCanReplaceValue,
                    hiliteReplaceValueFieldTitle: this.hiliteReplaceValueFieldTitle,
                    callback: callback
                })
            ]
        });

        this.advancedHiliteDialog.show();
    },

    addAdvancedRuleReply : function (hilite) {
        this.advancedHiliteDialog.hide();
        this.advancedHiliteDialog.markForDestroy();

        if (!hilite) return;

        var newRule = this.createAutoChild("hiliteRule", {
            rootDataSource:this.rootDataSource,
            width: "100%",
            isAdvanced: true,
            dataSource: this.dataSource,
            fieldDataSource: this.fieldDataSource,
            fieldName: hilite.fieldName,
            hilite : hilite,
            hiliteIcons: this.hiliteIcons,
            hiliteCanReplaceValue: this.hiliteCanReplaceValue,
            hiliteReplaceValueFieldTitle: this.hiliteReplaceValueFieldTitle
        });

        this.showNewHilite(newRule);
    },

    //> @method hiliteEditor.clearHilites()
    // Clear all Hilites from the editor.
    //
    // @visibility external
    //<
    clearHilites : function () {
        for (var i = this.ruleLayout.members.length-1; i >= 0; i--)
            this.removeRule(this.ruleLayout.getMember(i));
    },

    //> @method hiliteEditor.setHilites()
    // Initialize this editor with a set of Hilites.
    //
    // @param hilites (Array of Hilite) the array of hilite objects to apply
    // @visibility external
    //<
    setHilites : function (hilites) {

        hilites = this.hilites = hilites || [];

        this._loadedHilites = this.hilites.duplicate();

        for (var i=0; i<hilites.length; i++) {
            var hilite = hilites[i],
                css = hilite.cssText ? hilite.cssText.replaceAll(" ", "") : null,
                cssArray = css ? css.split(";") : [""],
                fgColor = hilite.textColor,
                bgColor = hilite.backgroundColor
            ;

            if (css) {
                for (var j=0; j<cssArray.length; j++) {
                    var setting = cssArray[j],
                        subArray = setting && setting.length > 0 ? setting.split(":") : []
                    ;
                    if (subArray && subArray[0] == "background-color" && !bgColor) {
                        hilite.backgroundColor = subArray[1];
                    }
                    if (subArray && subArray[0] == "color" && !fgColor) {
                        hilite.textColor = subArray[1];
                    }
                }
            }

            var missingField = false;
            if (this.dataSource) {
                var desc = isc.DataSource.getCriterionDescription(hilite.criteria, this.dataSource);
                missingField = desc.contains(isc.FilterBuilder.missingFieldPrompt);
            }

            var newRule = this.createAutoChild("hiliteRule",
                {
                    rootDataSource:this.rootDataSource,
                    fieldName: hilite.fieldName,
                    hilite: hilite,
                    dataSource: this.dataSource,
                    fieldDataSource: this.fieldDataSource,
                    missingField: missingField,
                    hiliteIcons: this.hiliteIcons
                }
            );

            this.showNewHilite(newRule);
        }
    },

    //> @method hiliteEditor.saveHilites()
    // Save the set of Hilites and fire the +link{hiliteEditor.callback, callback}.
    //
    // @param callback (Callback) the function to call when saving is complete
    // @visibility external
    //<
    saveHilites : function (callback) {
        var hilites = this.getHilites();
        if ((!hilites || hilites.length == 0) &&
                (!this._loadedHilites || this._loadedHilites.length == 0)) return;
        this.completeEditing(hilites);
    },

    getHilites : function () {
        var rules = this.ruleLayout.members,
            hilites = []
        ;

        for (var i = 0; i < rules.length; i++) {
            var rule = rules[i],
                hilite = rule.getHilite();
            if (hilite == null) continue;
            hilites.add(hilite);
        }

        return hilites;
    },

    getHiliteState : function () {
        var hilites = this.getHilites();
        if (hilites == null || hilites.length == 0) return null;
        return "(" + isc.JSON.encode(hilites, {dateFormat:"logicalDateConstructor"}) + ")";
    },

    completeEditing : function (hilites) {
        if (this.logIsInfoEnabled()) this.logInfo("returning hilites: " + isc.echoFull(hilites));
        if (this.callback) this.fireCallback(this.callback, "hilites", [hilites]);
    }
});


//>    @class AdvancedHiliteEditor
// A widget for editing a single, advanced +link{class:HiliteRule, hilite rule} for use by
// +link{class:DataBoundComponent, dataBoundComponents}.  Where a simple hilite provides
// configuration of a single criterion and either foreground or background color for
// application to a single field, an advanced hilite can specify more complex criteria which can
// both test and affect multiple fields and allow both background and foreground colors to
// be specified in a single rule.
// <P>
// <i><b>Important Note:</b> this class should not be used directly - it is exposed purely for
// +link{group:i18nMessages, i18n reasons.}</i>
//
// @treeLocation Client Reference/Grids/ListGrid
// @visibility external
//<
isc.defineClass("AdvancedHiliteEditor", "VStack");

isc.AdvancedHiliteEditor.addProperties({
    // editor for advanced  highlights
    // ---------------------------------------------------------------------------------------

    padding: 10,
    membersMargin: 10,

    //> @attr advancedHiliteEditor.filterBuilder (AutoChild FilterBuilder : null : IR)
    // AutoChild +link{class:FilterBuilder} for configuring the criteria for this Hilite.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>advancedHiliteEditor.filterBuilderProperties</code>.
    //
    // @visibility external
    //<
    filterBuilderDefaults : {
        _constructor:"FilterBuilder",
        isGroup:true,
        padding:8,
        maxHeight: 200,
        overflow: "visible"
    },

    //> @attr advancedHiliteEditor.filterGroupTitle (string : "Filter" : IR)
    // The title for the Filter group.
    //
    // @group i18nMessages
    // @visibility external
    //<
    filterGroupTitle: "Filter",

    //> @attr advancedHiliteEditor.hiliteIcons (Array of SCImgURL : null : IRW)
    // Specifies a list of icons that can be used in hilites.
    // <P>
    // <code>hiliteIcons</code> should be specified as an Array of +link{SCImgURL}.
    // When present, +link{class:HiliteRule, hilite rules}
    // will offer the user a drop down for picking one of these icons.
    // <P>
    // If the user picks an icon, the created hiliting rule will have +link{hilite.icon} set to
    // the chosen icon.  +link{listGridField.hiliteIconPosition} controls where the icon will
    // appear for that field -- the default is that it appears in front of the normal cell content.
    //
    // @visibility external
    //<

    //> @attr advancedHiliteEditor.hiliteForm (AutoChild DynamicForm : null : IR)
    // AutoChild +link{class:DynamicForm} for configuring the details of this Hilite.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>advancedHiliteEditor.hiliteFormProperties</code>.
    //
    // @visibility external
    //<
    hiliteFormDefaults : {
        _constructor:"DynamicForm",
        isGroup:true,
        extraSpace:4,
        padding:8,
        width:"100%",
        numCols: 8,
        colWidths:[200,150,100,150,100,150,40,50]
    },

    //> @attr advancedHiliteEditor.appearanceGroupTitle (string : "Appearance" : IR)
    // The title for the Appearance group.
    //
    // @group i18nMessages
    // @visibility external
    //<
    appearanceGroupTitle: "Appearance",

    //> @attr advancedHiliteEditor.targetFieldsItemTitle (string : "Target Field(s)" : IR)
    // The title for the Target Field(s) picker.
    //
    // @group i18nMessages
    // @visibility external
    //<
    targetFieldsItemTitle: "Target Field(s)",

    hiliteButtonsDefaults : {
        _constructor:isc.HLayout,
        membersMargin:8, height:1
    },

    hiliteButtonsSpacerDefaults : {
        _constructor: "LayoutSpacer",
        width: "*",
        autoParent: "hiliteButtons"
    },

    //> @attr advancedHiliteEditor.saveButton (AutoChild StatefulCanvas : null : IR)
    // AutoChild +link{class:ImgButton} that accepts this Hilite and fires the
    // +link{advancedHiliteEditor.callback, callback}.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>advancedHiliteEditor.saveButtonProperties</code>.
    //
    // @visibility external
    //<
    saveButtonDefaults : {
        _constructor:"IAutoFitButton",
        autoParent:"hiliteButtons",
        click : function () {
            this.creator.saveHilite();
        }
    },

    //> @attr advancedHiliteEditor.saveButtonTitle (string : "Save" : IR)
    // The title text for the +link{advancedHiliteEditor.saveButton,saveButton}.
    //
    // @group i18nMessages
    // @visibility external
    //<
    saveButtonTitle: "Save",

    //> @attr advancedHiliteEditor.cancelButton (AutoChild StatefulCanvas : null : IR)
    // AutoChild +link{class:ImgButton} that cancels this AdvancedHiliteEditor without saving
    // any changes, firing the +link{advancedHiliteEditor.callback, callback} with a null
    // parameter.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>advancedHiliteEditor.cancelButtonProperties</code>.
    //
    // @visibility external
    //<
    cancelButtonDefaults : {
        _constructor:"IAutoFitButton",
        autoParent:"hiliteButtons",
        click : function () {
            this.creator.cancelEditing();
        }
    },

    //> @attr advancedHiliteEditor.cancelButtonTitle (string : "Cancel" : IR)
    // The title text for the +link{advancedHiliteEditor.cancelButton,cancelButton}.
    //
    // @group i18nMessages
    // @visibility external
    //<
    cancelButtonTitle: "Cancel",

    // overall layout
    // ---------------------------------------------------------------------------------------
    defaultWidth:900, defaultHeight:600,
    visibilityMode:"multiple",

    //> @attr advancedHiliteEditor.invalidHilitePrompt (string : "Enter at least one rule, a color or icon, and a target field, or press 'Cancel' to abandon changes." : IR)
    // The message to show when the user clicks "Save" without entering any criteria.
    //
    // @group i18nMessages
    // @visibility external
    //<
    invalidHilitePrompt: "Enter at least one rule, a color or icon, and a target field, or press 'Cancel' to abandon changes.",

    //> @attr advancedHiliteEditor.title (string : "Advanced Highlight Editor" : IR)
    // The title text shown in the header bar of this editor's dialog.
    //
    // @group i18nMessages
    // @visibility external
    //<
    title: "Advanced Highlight Editor",

    //> @attr advancedHiliteEditor.callback (Callback : null : IR)
    // The callback to fire when the +link{advancedHiliteEditor.saveButton,saveButton} is clicked.
    //
    // @visibility external
    //<

    iconFieldDefaults: isc.HiliteRule.getInstanceProperty("iconFieldDefaults")
});

// Copy i18nMessages from HiliteRule.
// This is deferred until after the locale has loaded.
isc.defer("isc.AdvancedHiliteEditor.addProperties({" +
"foregroundColorTitle: isc.HiliteRule.getInstanceProperty('foregroundColorTitle')," +
"backgroundColorTitle: isc.HiliteRule.getInstanceProperty('backgroundColorTitle')," +
"iconFieldTitle: isc.HiliteRule.getInstanceProperty('iconFieldTitle')" +
"})");

isc.AdvancedHiliteEditor.addMethods({

    initWidget : function () {
        this.Super("initWidget", arguments);

        var ds = this.getDataSource(),
            _this = this;

        this.addAutoChild("filterBuilder",
            { groupTitle: this.filterGroupTitle,
              dataSource: ds, fieldDataSource: this.fieldDataSource,

              // Override the defaultOptionDataSource logic on every FilterClause and
              // nested FilterClause created inside subclauses to avoid fetching against
              // the special hilite criteria DS.
              inheritedClauseProperties : {
                rootDataSource:this.rootDataSource,
                // If a field has a specified displayField with no explicit optionDataSource,
                // pick up the rootDataSource (set up in DBC.editHilites())
                getDefaultOptionDataSource : function (field) {

                    if (this.rootDataSource) {
                        return this.rootDataSource;
                    }
                    var ds = this.Super("getDefaultOptionDataSource", arguments);
                    // Always avoid attempting to fetch against the hiliteCriteria DS
                    if (ds && ds.isHiliteCriteriaDS) return null;
                    return ds;

                }
              },
              fieldNameChanged : function (filterClause) {
                  this.Super("fieldNameChanged", arguments);
                  _this.fieldChosen(filterClause.getFieldName(true));
              }
             }
        );

        var hiliteIconItem = isc.addProperties({}, this.iconFieldDefaults, this.iconFieldProperties, {
            name: "icon",
            title: this.iconFieldTitle
        });

        // Add hiliteIcons to valueMap
        if (this.hiliteIcons) {
            this.hiliteIcons.map(function (icon) {
                hiliteIconItem.disabled = false;
                // Map all values to "", to show the icon only. There may be a more elegant
                // way to do this with pickListProperties.
                hiliteIconItem.valueMap[icon] = "";
                // This actually provides the icon for the select item
                hiliteIconItem.valueIcons[icon] = icon;
            });
        }

        // Add this.hilite.icon to valueMap if not already there
        var icon = this.hilite ? this.hilite.icon : null;
        if (icon && !hiliteIconItem.valueMap[icon]) {
            hiliteIconItem.disabled = false;
            hiliteIconItem.valueMap[icon] = "";
            hiliteIconItem.valueIcons[icon] = icon;
        }


        var items = [
            {title:this.targetFieldsItemTitle, name:"fieldName", multiple:true, allowMultiSelect: true,
             type:"select"
            },
            {title:this.foregroundColorTitle, name:"textColor", type:"color" },
            {title:this.backgroundColorTitle, name:"backgroundColor", type:"color" },
            hiliteIconItem
        ];
        var hiliteFormProperties = { groupTitle: this.appearanceGroupTitle };
        if (this.hiliteCanReplaceValue) {
            hiliteFormProperties.numCols = 10;
            hiliteFormProperties.colWidths = [200,150,100,150,150,150,100,150,40,50];
            if (this.hiliteReplaceValueFieldTitle == null) {
                this.hiliteReplaceValueFieldTitle =
                    isc.ListGrid.getInstanceProperty("hiliteReplaceValueFieldTitle");
            }
            items.addAt({title:this.hiliteReplaceValueFieldTitle, name:"replacementValue", type:"text" }, 2);
        }

        var hiliteForm = this.addAutoChild("hiliteForm", hiliteFormProperties);
        this.iconField = hiliteForm.getField("icon");

        if (this.fieldDataSource) {
            items[0] = isc.addProperties({}, items[0], {
                valueField: "name",
                displayField: "title",
                optionDataSource: this.fieldDataSource,
                optionCriteria: { _constructor: "AdvancedCriteria", operator: "or",
                    criteria: [
                        { fieldName: "showInSimpleEditor", operator: "equals", value: true },
                        { fieldName: "showInSimpleEditor", operator: "isNull" }
                    ]
                }
            });
            delete items[0].defaultDynamicValue;
            this.hiliteForm.addItems(items);
        } else {
            var fieldNames = this.fieldNames || ds.getFieldNames(),
                fieldMap = this.fieldMap = {};
            for (var i = 0; i < fieldNames.length; i++) {
                var fieldName = fieldNames[i],
                    field = ds.getField(fieldName),
                    fieldTitle = field.title;
                if (field.hidden) continue;
                fieldTitle = fieldTitle ? fieldTitle : fieldName;
                fieldMap[fieldName] = fieldTitle;
            }
            this.fieldMap = fieldMap;
            items[0].valueMap = fieldMap;
            this.hiliteForm.addItems(items);
        }

        this.addAutoChildren(["hiliteButtons", "hiliteButtonsSpacer"]);
        this.addAutoChild("saveButton", { title: this.saveButtonTitle });
        this.addAutoChild("cancelButton", { title: this.cancelButtonTitle });

        this.addMembers([this.filterBuilder, this.hiliteForm, this.hiliteButtons]);

        if (this.hilite != null) {
            // we're editing an existing hilite
            this.filterBuilder.setCriteria(this.hilite.criteria);
            this.hiliteForm.editRecord(this.hilite);
        }

    },

    // the first time a field is chosen when defining criteria, default the target field to
    // that field.
    fieldChosen : function (fieldName) {
        if (fieldName && this.hiliteForm.getValue("fieldName") == null) {
            this.hiliteForm.setValue("fieldName", fieldName);
        }
    },

    //> @method advancedHiliteEditor.saveHilite()
    // Save changes and fire the +link{advancedHiliteEditor.callback, callback}.
    //
    // @visibility external
    //<
    saveHilite : function () {
        this.hiliteForm.setValue("criteria", this.filterBuilder.getCriteria());
        var hilite = this.hiliteForm.getValues();

        if (hilite.criteria.criteria == null || hilite.criteria.criteria.length == 0 ||
            (!hilite.textColor && !hilite.backgroundColor && !hilite.icon && !hilite.replacementValue) ||
            hilite.fieldName == null)
        {
            isc.say(this.invalidHilitePrompt);
            return;
        }

        var cssText = "";

        if (hilite.textColor && hilite.textColor != "") {
            cssText += "color:"+hilite.textColor+";";
        }
        if (hilite.backgroundColor && hilite.backgroundColor != "") {
            cssText += "background-color:"+hilite.backgroundColor+";";
        }

        hilite.cssText = cssText;
        if (this.hilite && this.hilite.id) hilite.id = this.hilite.id;

        this.completeEditing(hilite);
    },

    //> @method advancedHiliteEditor.cancelEditing()
    // Discard changes and fire the +link{advancedHiliteEditor.callback, callback} with a null
    // parameter.
    //
    // @visibility external
    //<
    cancelEditing : function () {
        this.completeEditing(null);
    },

    completeEditing : function (result) {
        if (this.callback) this.fireCallback(this.callback, ["hilite"], [result]);
    }
});







//> @object GroupSpecifier
// A Javascript object defining the details of a single group operation.
//
// @treeLocation Client Reference/Data Binding
// @visibility external
//<

//> @attr groupSpecifier.property (String : null : IR)
// The property name, eg a +link{ListGridField, field name}, to which this groupSpecifier applies.
//
// @visibility external
//<

//> @attr groupSpecifier.grouping (String : null : IR)
// The grouping in which this specifier should group.
//
// @visibility external
//<

//> @attr groupSpecifier.normalizer (Function : null : IR)
// A normalizer function which this groupSpecifier will use to group.
//
// @visibility external
//<

//> @attr groupSpecifier.context (DataBoundComponent : null : IR)
// A DataBoundComponent providing the context for the group-normalizer.
//
// @visibility external
//<


//> @class MultiGroupPanel
// A widget that allows the user to set up complex grouping arrangements by defining a group of
// +link{GroupSpecifier}s.
// <P>
// Each +link{GroupSpecifier} applies to a single property and grouping - so, for instance, in
// a grid with two columns, <code>Nationhood</code> and <code>Country</code>, you could group first
// by <code>Nationhood</code> with its selected groupingMode and then by <code>Country</code> with its selected groupingMode.
//
// @treeLocation Client Reference/Data Binding
// @visibility external
//<
isc.defineClass("MultiGroupPanel", "Layout");

isc.MultiGroupPanel.addProperties({
    vertical: true,
    overflow: "visible",

    //> @attr multiGroupPanel.fields (Array of Field : null : IR)
    // The list of fields which the user can choose to group by.
    // @visibility external
    //<

    // i18n text constants
    //> @attr multiGroupPanel.addLevelButtonTitle (String : "Add Level" : IR)
    // The title-text to appear on the addLevelButton
    // @visibility external
    // @group i18nMessages
    //<
    addLevelButtonTitle: "Add Level",
    //> @attr multiGroupPanel.deleteLevelButtonTitle (String : "Delete Level" : IR)
    // The title-text to appear on the deleteLevelButton
    // @visibility external
    // @group i18nMessages
    //<
    deleteLevelButtonTitle: "Delete Level",
    //> @attr multiGroupPanel.copyLevelButtonTitle (String : "Copy Level" : IR)
    // The title-text to appear on the copyLevelButton
    // @visibility external
    // @group i18nMessages
    //<
    copyLevelButtonTitle: "Copy Level",

    //> @attr multiGroupPanel.invalidListPrompt (HTMLString : "Columns may only be used once: '${title}' is used multiple times." : IR)
    // This is a dynamic string - text within <code>&#36;{...}</code> will be evaluated as JS code
    // when the message is displayed.
    // <P>
    // Default value returns <P>
    // <code>
    // <i>Columns may only be used once: <code>[some field's title]</code> is used multiple times</i>
    // </code>
    // @visibility external
    // @group i18nMessages
    //<
    invalidListPrompt: "Columns may only be used once: '${title}' is used multiple times.",

    //> @attr multiGroupPanel.propertyFieldTitle (String : "Column" : IR)
    // The title-text to appear in the header of the "property" field.
    // @visibility external
    // @group i18nMessages
    //<
    propertyFieldTitle: "Column",

    //> @attr multiGroupPanel.groupingFieldTitle (String : "Grouping" : IR)
    // The title-text to appear in the header of the "grouping" field.
    // @visibility external
    // @group i18nMessages
    //<
    groupingFieldTitle: "Grouping",

    //> @attr multiGroupPanel.firstGroupLevelTitle (String : "Group by" : IR)
    // The title-text to appear in the first column for the first group-level.
    // @visibility external
    // @group i18nMessages
    //<
    firstGroupLevelTitle: "Group by",

    //> @attr multiGroupPanel.otherGroupLevelTitle (String : "Then by" : IR)
    // The title-text to appear in the first column for all group-levels other than the first.
    // @visibility external
    // @group i18nMessages
    //<
    otherGroupLevelTitle: "Then by",

    topLayoutDefaults: {
        _constructor: "HLayout",
        overflow: "visible",
        height: 22,
        align: "left",
        membersMargin: 5,
        extraSpace: 5
    },

    //> @attr multiGroupPanel.addLevelButton (AutoChild IButton : null : RA)
    // Automatically generated +link{class:IButton} providing a mechanism for adding new levels
    // to the group configuration.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>multiGroupPanel.addLevelButtonProperties</code> and
    // <code>multiGroupPanel.addLevelButtonDefaults</code>.
    //
    // @visibility external
    //<
    addLevelButtonDefaults: {
        _constructor: "IButton",
        icon: "[SKINIMG]actions/add.png",
        autoFit: true,
        height: 22,
        showDisabled: false,
        autoParent: "topLayout",
        click: "this.creator.addLevel()"
    },

    //> @attr multiGroupPanel.deleteLevelButton (AutoChild IButton : null : RA)
    // Automatically generated +link{class:IButton} providing a mechanism for deleting levels
    // from the group configuration.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>multiGroupPanel.deleteLevelButtonProperties</code> and
    // <code>multiGroupPanel.deleteLevelButtonDefaults</code>.
    //
    // @visibility external
    //<
    deleteLevelButtonDefaults: {
        _constructor: "IButton",
        icon: "[SKINIMG]actions/remove.png",
        autoFit: true,
        height: 22,
        showDisabled: false,
        autoParent: "topLayout",
        click: "this.creator.deleteSelectedLevel()"
    },

    //> @attr multiGroupPanel.copyLevelButton (AutoChild IButton : null : RA)
    // Automatically generated +link{class:IButton} providing a mechanism for duplicating levels
    // in the group configuration.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>multiGroupPanel.copyLevelButtonProperties</code> and
    // <code>multiGroupPanel.copyLevelButtonDefaults</code>.
    //
    // @visibility external
    //<
    copyLevelButtonDefaults: {
        _constructor: "IButton",
        icon: "[SKINIMG]RichTextEditor/copy.png",
        autoFit: true,
        height: 22,
        showDisabled: false,
        autoParent: "topLayout",
        click: "this.creator.copySelectedLevel()"
    },

    //> @attr multiGroupPanel.levelUpButtonTitle (String : "Move Level Up" : IR)
    // The hover-prompt for the Level Up button.
    // @visibility external
    // @group i18nMessages
    //<
    levelUpButtonTitle: "Move Level Up",

    //> @attr multiGroupPanel.levelUpButton (AutoChild ImgButton : null : RA)
    // Automatically generated +link{class:ImgButton} providing a mechanism for moving existing
    // group-levels up in the group configuration.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>multiGroupPanel.levelUpButtonProperties</code> and
    // <code>multiGroupPanel.levelUpButtonDefaults</code>.
    //
    // @visibility external
    //<
    levelUpButtonDefaults: {
        _constructor: "ImgButton",
        //src: "[SKINIMG]common/arrow_up.gif",
        src: "[SKINIMG]TransferIcons/up.png",
        height: 22,
        width: 24,
        imageType: "center",
        showDisabled: false,
        showRollOver: false,
        showDown: false,
        showFocused: false,
        autoParent: "topLayout",
        click: "this.creator.moveSelectedLevelUp();"
    },

    //> @attr multiGroupPanel.levelDownButtonTitle (String : "Move Level Down" : IR)
    // The hover-prompt for the Level Down button.
    // @visibility external
    // @group i18nMessages
    //<
    levelDownButtonTitle: "Move Level Down",

    //> @attr multiGroupPanel.levelDownButton (AutoChild ImgButton : null : RA)
    // Automatically generated +link{class:ImgButton} providing a mechanism for moving existing
    // group-levels down in the group configuration.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>multiGroupPanel.levelDownButtonProperties</code> and
    // <code>multiGroupPanel.levelDownButtonDefaults</code>.
    //
    // @visibility external
    //<
    levelDownButtonDefaults: {
        _constructor: "ImgButton",
        //src: "[SKINIMG]common/arrow_down.gif",
        src: "[SKINIMG]TransferIcons/down.png",
        height: 22,
        width: 24,
        imageType: "center",
        showDisabled: false,
        showRollOver: false,
        showDown: false,
        showFocused: false,
        autoParent: "topLayout",
        click: "this.creator.moveSelectedLevelDown()"
    },

    //> @attr multiGroupPanel.optionsGrid (AutoChild ListGrid : null : IR)
    // Automatically generated +link{class:ListGrid} allowing the user to configure a set of
    // +link{GroupSpecifier}s.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>multiGroupPanel.optionsGridProperties</code> and
    // <code>multiGroupPanel.optionsGridDefaults</code>.
    //
    // @visibility external
    //<
    optionsGridDefaults: {
        _constructor: "ListGrid",
        width:"100%",
        height: "*",
        canSort: false,
        canReorderFields: false,
        canResizeFields: false,
        canEdit: true,
        canEditNew: true,
        selectionType: "single",
        selectionProperty: "_selection_1",
        canEditCell : function (rowNum, colNum) {
            var record = this.getRecord(rowNum);
            if (colNum == 2) {
                if (!this.creator.verifyGroupingModeAvailableField(record, rowNum)) return false;
            }
            // use default rules for all other fields
            return this.Super("canEditCell", arguments);
        },
        recordDoubleClick : function (viewer, record, recordNum, field, fieldNum, value, rawValue) {
            this.creator.getGroupingModeAvailableField(record, recordNum);
        },
        defaultFields: [
            { name: "groupSequence", title: "&nbsp;", showTitle: false, canEdit: false, width: 80, canHide: false,
                showDefaultContextMenu: false,
                formatCellValue : function (value, record, rowNum, colNum, grid) {
                    return rowNum == 0 ? grid.creator.firstGroupLevelTitle :
                        grid.creator.otherGroupLevelTitle;
                }
            },
            { name: "property", title: " ", type: "select",
                defaultToFirstOption: true,
                showDefaultContextMenu: false,
                changed:"item.grid.creator.getGroupingModeAvailableField(null, null)"
            },
            { name: "grouping",  title: " ", width: 100,
                showDefaultContextMenu: false,
                defaultToFirstOption: true,
                type: "text", editorType: "select",
                displayField: "groupingTitle",
                changed : function (form, item, value) {
                    var vm = item.valueMap || item.editorValueMap;
                    form.setValue("groupingTitle", vm[value]);
                }
            },
            { name: "groupingTitle",  showIf: "false", showDefaultContextMenu: false }
        ],
        recordClick : function (viewer, record, recordNum) {
            this.creator.setButtonStates();
        },
        bodyKeyPress: function (event) {
            if (event.keyName == "Delete" && this.anySelected()) this.removeSelectedData();
            else this.Super("bodyKeyPress", arguments);
        },
        extraSpace: 5
    },

    propertyFieldNum: 1,
    directionFieldNum: 2,

    topAutoChildren: ["topLayout", "addLevelButton", "deleteLevelButton", "copyLevelButton"]

    //> @attr multiGroupPanel.initialGrouping (Array: null : IR)
    // The initial group configuration to show in the
    // +link{multiGroupPanel.optionsGrid, optionsGrid}.
    //
    // @visibility external
    //<

    //> @attr multiGroupPanel.maxLevels (number : null : IR)
    // The maximum number of levels of grouping that can be applied.  Since each group-property or
    // field-name can be used only once in a given multi-group operation, if no maxLevels value
    // or a value larger than the total number of available properties is specified, it will
    // default to the total number of available properties.
    //
    // @visibility external
    //<

});

isc.MultiGroupPanel.addProperties({
    //> @method multiGroupPanel.getNumLevels()
    // Return the number of levels of grouping that have been configured.
    //
    // @return (number) The number of levels of grouping that have been configured
    // @visibility external
    //<
    getNumLevels : function () {
        return this.optionsGrid.data.length;
    },

    //> @method multiGroupPanel.getGroup()
    // Returns all configured grouping levels, as an array of +link{GroupSpecifier}s.
    //
    // @return (Array of GroupSpecifier) the GroupSpecifiers for all configured grouping levels
    // @visibility external
    //<
    getGroup : function () {
        var grid = this.optionsGrid,
            data = isc.shallowClone(grid.data),
            editRowNum = grid.getEditRow(),
            editValues = isc.isA.Number(editRowNum) ? grid.getEditValues(editRowNum) : null
        ;
        if (editValues) data[editRowNum] = isc.addProperties(data[editRowNum], editValues);
        return this.getGroupSpecifiers(data);
    },

    //> @method multiGroupPanel.validate()
    // Validate that no two +link{GroupSpecifier}s group on the same
    // +link{groupSpecifier.property, property}.
    //
    // @return (boolean) True if validation succeeds, false if any property is used twice
    // @visibility external
    //<
    validate : function () {
        var grid = this.optionsGrid,
            data = grid.data,
            specifiers = []
        ;

        for (var i = 0; i<data.length; i++) {
            var item = data.get(i);
            if (specifiers.contains(item.property)) {
                var _this = this,
                    title = this.optionsGrid.getField("property").valueMap[item.property],
                    message = this.invalidListPrompt.evalDynamicString(this, { title: title });
                isc.warn(message,
                    function () {
                        _this.recordFailedValidation(item, i);
                    }
                );
                return false;
            }
            specifiers.add(item.property);
        }

        return true;
    },

    recordFailedValidation : function (record) {
        var grid = this.optionsGrid,
            recordIndex = (isc.isA.Number(record) ? record : grid.getRecordIndex(record)),
            record = (!isc.isA.Number(record) ? record : grid.data.get(record))
        ;
        grid.selectSingleRecord(record);
        grid.startEditing(recordIndex, 1);
    },

    getGroupSpecifiers : function (data) {
        return this.optionsGrid.removeSelectionMarkers(data);
    },

    setInitialGrouping : function (data) {
        // update the initial grouping rows - the grid relies on a groupingTitle value which is
        // derived from the appropriate groupingModes valueMap
        for (var i=0; i<data.length; i++) {
            var record = data[i];
            if (record.grouping) {
                var field = this.fields.find("name", record.property),
                    modes = this._getFieldGroupingModes(field) || {}
                ;
                record.groupingTitle = modes[record.grouping];
            }
        }
        this.optionsGrid.setData(data);
    },

    initWidget : function () {
        this.Super("initWidget", arguments);

        // store the maxLevels for use with runtime calls to setFields()
        this._maxLevels = this.maxLevels;

        this.addAutoChildren(this.topAutoChildren);

        this.addAutoChild("levelUpButton", { prompt: this.levelUpButtonTitle });
        this.addAutoChild("levelDownButton", { prompt: this.levelDownButtonTitle });

        this.addAutoChild("optionsGrid");
        this.setGroupFields();
        this.setGroupingMode();
        this.setButtonTitles();

        this.addMember(this.topLayout);
        this.addMember(this.optionsGrid);

        this.setButtonStates();

        if (this.initialGrouping) this.setInitialGrouping(this.initialGrouping);
        else this.addLevel();
    },

    setButtonTitles : function (enable) {
        if (this.addLevelButton) this.addLevelButton.setTitle(this.addLevelButtonTitle);
        if (this.deleteLevelButton) this.deleteLevelButton.setTitle(this.deleteLevelButtonTitle);
        if (this.copyLevelButton) this.copyLevelButton.setTitle(this.copyLevelButtonTitle);
    },

    setButtonStates : function () {
        var numLevels = this.getNumLevels(),
            maxLevels = this.maxLevels,
            grid = this.optionsGrid,
            anySelected = grid.anySelected(),
            selectedIndex = grid.getRecordIndex(grid.getSelectedRecord())
        ;
        if (this.addLevelButton) this.addLevelButton.setDisabled(numLevels >= maxLevels);
        if (this.deleteLevelButton) this.deleteLevelButton.setDisabled(!anySelected);
        if (this.copyLevelButton) this.copyLevelButton.setDisabled(!anySelected || numLevels >= maxLevels);
        if (this.levelUpButton) this.levelUpButton.setDisabled(!anySelected || selectedIndex == 0);
        if (this.levelDownButton) this.levelDownButton.setDisabled(!anySelected || selectedIndex == numLevels-1);
    },

    // support setting the fields array after creation-time
    setFields : function (fields) {
        if (isc.DataSource && isc.isA.DataSource(fields)) fields = isc.getValues(fields.getFields());
        this.fields = fields;
        this.setGroupFields();
        this.optionsGrid.refreshFields();
        this.setButtonStates();
    },

    setGroupFields : function () {
        var fields = [];

        this.optionsGrid.getField("property").title = this.propertyFieldTitle;

        if (!this.fields) return;

        for (var i=0; i<this.fields.length; i++) {
            var field = this.fields[i];
            if (this.optionsGrid._canGroupByField(field)) fields.add(field);
        }
        this.fields = fields;
        var grid = this.optionsGrid,
            fieldMap = this.fields ? this.fields.getValueMap(grid.fieldIdProperty, "title") :
                { none: "No fields" },
            keyCount = isc.getKeys(fieldMap).length
        ;

        for (var key in fieldMap) {
            // if there's no title, use DS.getAutoTitle() (!value seems to detect empty strings
            // too, but checking it seperately just to be safe)
            if (isc.DataSource && (!fieldMap[key] || isc.isAn.emptyString(fieldMap[key])))
                fieldMap[key] = isc.DataSource.getAutoTitle(key);
        }

        this.optionsGrid.setValueMap("property", fieldMap);
        if (!this._maxLevels || this.maxLevels > keyCount) this.maxLevels = keyCount;
    },

    setGroupingMode : function () {
        var field = this.optionsGrid.getField("grouping");
        field.title = this.groupingFieldTitle;
        field.valueMap = this.getGroupingModeFirstItem();
    },

    getGroupingModeFirstItem : function () {
        var groupingField = this.optionsGrid.getField("grouping");
        var field = this.fields[0],
            valueMap = this._getFieldGroupingModes(field)
        ;
        //groupingField.valueMap = valueMap;
        groupingField.editorValueMap = this._getFieldGroupingModes(field);
    },

    _getFieldGroupingModes : function (field) {
        if (!field) return;
        if (!this._fieldGroupingModes) this._fieldGroupingModes = {};
        var modes = this._fieldGroupingModes[field.name];

        if (!modes) {
            modes = field.groupingModes ? field.groupingModes :
                (!field.getGroupValue ? ( field._simpleType ? (field._simpleType.getGroupingModes ?
                                                           field._simpleType.getGroupingModes() :
                                                           field._simpleType.groupingModes) : false )
                : false)
            ;
            this._fieldGroupingModes[field.name] = modes;
        }

        return modes;
    },

    getGroupingModeAvailableField : function (record, rowNum) {
        var grid = this.optionsGrid,
            editRowNum = grid.getEditRow(),
            editValues = isc.isA.Number(editRowNum) ? grid.getEditValues(editRowNum) : null
        ;
        var recordNum = (rowNum == null)?editRowNum:rowNum;
        var propertyName = (record == null)?editValues.property:record.property;
        for (var i=0; i<this.fields.length; i++) {
            var field = this.fields[i];
            if (propertyName == field.name) {
                var groupingModes = this._getFieldGroupingModes(field);
                if (groupingModes != null) {
                    grid.setValueMap("grouping", groupingModes);
                    if (rowNum == null) {
                        if (field.groupingMode != null) {
                            grid.setEditValue(recordNum, 2, field.groupingMode);
                        } else if (field.defaultGroupingMode != null) {
                            grid.setEditValue(recordNum, 2, field.defaultGroupingMode);
                        } else {
                            grid.setEditValue(recordNum, 2, groupingModes[0]);
                        }
                        grid.refreshCell(recordNum,2);
                    }
                } else {
                    grid.setEditorValueMap("grouping", null);
                    grid.setEditValue(recordNum, 2, null);
                    grid.refreshCell(recordNum,2);
                }
                break;
            }
        }
    },

    verifyGroupingModeAvailableField : function (record, rowNum) {
        var grid = this.optionsGrid,
            editValues = isc.isA.Number(rowNum) ? grid.getEditValues(rowNum) : null
        ;
        var propertyName = (typeof editValues.property === 'undefined')?record.property:editValues.property;
        for (var i=0; i<this.fields.length; i++) {
            var field = this.fields[i];
            if (propertyName == field.name) {
                var groupingModes = this._getFieldGroupingModes(field);
                if (groupingModes != null) {
                    var size = isc.getKeys(groupingModes).length;
                    if (size == 1) {
                        return false;
                    } else {
                        return true;
                    }
                } else {
                    return false;
                }
            }
        }
        return true;
    },

    addLevel : function () {
        var grid = this.optionsGrid,
            selectedIndex = grid.getRecordIndex(grid.getSelectedRecord()),
            columnField = grid.getField("property"),
            rowNum = selectedIndex >=0 ? selectedIndex+1 : grid.data.length,
            record = {
                property: isc.firstKey(columnField.valueMap)
            }
        ;

        grid.data.addAt(record, rowNum);
        this.editRecord(rowNum);
        this.setButtonStates();
        this.fireChangeEvent();
        this.getGroupingModeAvailableField(record,null);
    },

    deleteSelectedLevel : function () {
        var grid = this.optionsGrid,
            selectedIndex = grid.getRecordIndex(grid.getSelectedRecord())
        ;
        if (selectedIndex >= 0) {
            grid.data.removeAt(selectedIndex);
            this.setButtonStates();
            this.fireChangeEvent();
        }
    },

    copySelectedLevel : function () {
        var grid = this.optionsGrid,
            editRowNum = grid.getEditRow(),
            selectedIndex = grid.getRecordIndex(grid.getSelectedRecord()),
            record = isc.shallowClone(grid.getRecord(selectedIndex))
        ;
        if (selectedIndex >= 0) {
            grid.data.addAt(record, selectedIndex+1);
            this.editRecord(selectedIndex+1);
            this.setButtonStates();
            this.fireChangeEvent();
            this.getGroupingModeAvailableField(record,null);
            grid.setEditValue(selectedIndex+1, 2, record.grouping);
            grid.refreshCell(selectedIndex+1,2);
        }
    },

    editRecord : function (rowNum) {
        // select and edit a record
        this.optionsGrid.selectSingleRecord(rowNum);
        this.optionsGrid.startEditing(rowNum, this.propertyFieldNum);
    },

    moveSelectedLevelUp : function () {
        var grid = this.optionsGrid,
            selectedIndex = grid.getRecordIndex(grid.getSelectedRecord())
        ;
        if (selectedIndex>0) {
            grid.data.slide(selectedIndex, selectedIndex-1);
            this.optionsGrid.selectSingleRecord(selectedIndex-1);
            this.setButtonStates();
            this.fireChangeEvent();
        }
    },

    moveSelectedLevelDown : function () {
        var grid = this.optionsGrid,
            selectedIndex = grid.getRecordIndex(grid.getSelectedRecord())
        ;
        if (selectedIndex >= 0 && selectedIndex < grid.data.length-1) {
            grid.data.slide(selectedIndex, selectedIndex+1);
            this.optionsGrid.selectSingleRecord(selectedIndex+1);
            this.setButtonStates();
            this.fireChangeEvent();
        }
    },

    fireChangeEvent : function () {
        this.groupChanged(isc.shallowClone(this.getGroup()));
    },

    //> @method multiGroupPanel.groupChanged()
    // Fired whenever the group configuration changes.  The single parameter is an array of
    // +link{GroupSpecifier}s that represent the list of group-levels as they appear after
    // whatever change has occurred.
    //
    // @param groupLevels (Array of GroupSpecifier) The current group configuration, after any changes
    // @visibility external
    //<
    groupChanged : function (groupLevels) {
    }

});




//> @class MultiGroupDialog
// A dialog that allows the user to set up complex grouping arrangements by defining a group of
// +link{GroupSpecifier}s.
// <P>
// Each +link{GroupSpecifier} applies to a single property and grouping - so, for instance, in
// a grid with two columns, <code>Nationhood</code> and <code>Country</code>, you could group first
// by <code>Nationhood</code> with its selected groupingMode and then by <code>Country</code> with its selected groupingMode.
// <P>
// <i><b>Important Note:</b> this class should not be used directly - it is exposed purely for
// +link{group:i18nMessages, i18n reasons.}</i>
//
// @treeLocation Client Reference/Data Binding
// @visibility external
//<
isc.defineClass("MultiGroupDialog", "Window");

isc.MultiGroupDialog.addClassMethods({
    //> @classMethod multiGroupDialog.askForGrouping()
    // Launches a MultiGroupDialog and obtains a group-definition from the user.
    //
    // @param fieldSource (Array of Field or DataSource or DataBoundComponent) A source for Fields which the user can choose to group by
    // @param initialGrouping (Array) The initial group definition.
    // @param callback (Callback) Called when the user defines and accepts one or more
    // +link{GroupSpecifier}s.  Single parameter <code>groupLevels</code> is an Array of
    // GroupSpecifier or null if the user cancelled the dialog.
    // @visibility external
    //<
    askForGrouping : function (fieldSource, initialGrouping, callback) {
        var fields = isc.isAn.Array(fieldSource) ? fieldSource :
                isc.DataSource && isc.isA.DataSource(fieldSource) ? isc.getValues(fieldSource.getFields()) :
                isc.isA.DataBoundComponent(fieldSource) ? fieldSource.getFields() : null
        ;
        if (!fields) return;
        var grouping = [];
        if ((typeof initialGrouping === 'undefined') || (initialGrouping == null)){
            grouping = initialGrouping;
        } else {
            for (var i = 0; i < initialGrouping.length; i++) {
                var groupField = initialGrouping[i];
                for (var ii = 0; ii < fields.length; ii++) {
                    var field = fields[ii];
                    if (groupField == field.name) {
                        var record = {
                            property: field.name,
                            grouping: field.groupingMode
                        };
                        grouping.add(record);
                    }
                }
            }
        }
        var props = {
            autoDraw:true,
            fields: fields,
            initialGrouping: grouping,
            callback: callback
        };
        isc.MultiGroupDialog.create(props);
    }
});

isc.MultiGroupDialog.addProperties({
    isModal: true,
    width: 500,
    height: 250,
    vertical: true,
    autoCenter: true,
    showMinimizeButton: false,

    mainLayoutDefaults: {
        _constructor: "VLayout",
        width: "100%",
        height: "100%",
        layoutMargin: 5
    },

    multiGroupPanelDefaults: {
        _constructor: "MultiGroupPanel",
        width: "100%",
        height: "*",
        autoParent: "mainLayout"
    },

    // i18n text constants - passthrough to this.multiGroupPanel
    //> @attr multiGroupDialog.title (String : "Group" : IR)
    // The title-text to appear in this Dialog's Header-bar.
    //
    // @visibility external
    // @group i18nMessages
    //<
    title: "Group",

    //> @attr multiGroupDialog.addLevelButtonTitle (String : "Add Level" : IR)
    // The title-text to appear on the addLevelButton.
    // <P>
    // Note, this is a passthrough property which, when set, is passed through to the
    // +link{class:MultiGroupPanel, MultiGroupPanel} contained in this dialog.  You only need to
    // consider the properties on the MultiGroupPanel for i18n.
    //
    // @visibility external
    // @group i18nMessages
    //<

    //> @attr multiGroupDialog.deleteLevelButtonTitle (String : "Delete Level" : IR)
    // The title-text to appear on the deleteLevelButton
    // <P>
    // Note, this is a passthrough property which, when set, is passed through to the
    // +link{class:MultiGroupPanel, MultiGroupPanel} contained in this dialog.  You only need to
    // consider the properties on the MultiGroupPanel for i18n.
    //
    // @visibility external
    // @group i18nMessages
    //<

    //> @attr multiGroupDialog.copyLevelButtonTitle (String : "Copy Level" : IR)
    // The title-text to appear on the copyLevelButton
    // <P>
    // Note, this is a passthrough property which, when set, is passed through to the
    // +link{class:MultiGroupPanel, MultiGroupPanel} contained in this dialog.  You only need to
    // consider the properties on the MultiGroupPanel for i18n.
    //
    // @visibility external
    // @group i18nMessages
    //<

    //> @attr multiGroupDialog.levelUpButtonTitle (String : "Move Level Up" : IR)
    // The hover-prompt for the Level Up button.
    // @visibility external
    // @group i18nMessages
    //<

    //> @attr multiGroupDialog.levelDownButtonTitle (String : "Move Level Down" : IR)
    // The hover-prompt for the Level Down button.
    // @visibility external
    // @group i18nMessages
    //<

    //> @attr multiGroupDialog.invalidListPrompt (HTMLString : "Columns may only be used once: '${title}' is used multiple times." : IR)
    // This is a dynamic string - text within <code>&#36;{...}</code> will be evaluated as JS code
    // when the message is displayed.
    // <P>
    // Default value returns <P>
    // <code>
    // <i>Columns may only be used once: <code>[some field's title]</code> is used multiple times</i>
    // </code>
    // <P>
    // Note, this is a passthrough property which, when set, is passed through to the
    // +link{class:MultiGroupPanel, MultiGroupPanel} contained in this dialog.  You only need to
    // consider the properties on the MultiGroupPanel for i18n.
    //
    // @visibility external
    // @group i18nMessages
    //<

    //> @attr multiGroupDialog.propertyFieldTitle (String : "Column" : IR)
    // The title-text to appear in the header of the "property" field.
    // <P>
    // Note, this is a passthrough property which, when set, is passed through to the
    // +link{class:MultiGroupPanel, MultiGroupPanel} contained in this dialog.  You only need to
    // consider the properties on the MultiGroupPanel for i18n.
    //
    // @visibility external
    // @group i18nMessages
    //<

    //> @attr multiGroupDialog.groupingFieldTitle (String : "Grouping" : IR)
    // The title-text to appear in the header of the "property" field.
    // <P>
    // Note, this is a passthrough property which, when set, is passed through to the
    // +link{class:MultiGroupPanel, MultiGroupPanel} contained in this dialog.  You only need to
    // consider the properties on the MultiGroupPanel for i18n.
    //
    // @visibility external
    // @group i18nMessages
    //<

    //> @attr multiGroupDialog.initialGrouping (Array : null : IR)
    // The initial group configuration to show in the
    // +link{multiGroupPanel.optionsGrid, optionsGrid}.
    // <P>
    // Note, this is a passthrough property which, when set, is passed through to the
    // +link{class:MultiGroupPanel, MultiGroupPanel} contained in this dialog.
    //
    // @visibility external
    //<

    //> @attr multiGroupDialog.maxLevels (number : null : IR)
    // The maximum number of levels of grouping that can be applied.  Since each group-property or
    // field-name can be used only once in a given multi-group operation, if no maxLevels value
    // or a value larger than the total number of available properties is specified, it will
    // default to the total number of available properties.
    // <P>
    // Note, this is a passthrough property which, when set, is passed through to the
    // +link{class:MultiGroupPanel, MultiGroupPanel} contained in this dialog.
    //
    // @visibility external
    //<


    //> @attr multiGroupDialog.applyButtonTitle (String : "Apply" : IR)
    // The title-text to appear on the applyButton
    // @visibility external
    // @group i18nMessages
    //<
    applyButtonTitle: "Apply",
    //> @attr multiGroupDialog.cancelButtonTitle (String : "Cancel" : IR)
    // The title-text to appear on the cancelButton
    // @visibility external
    // @group i18nMessages
    //<
    cancelButtonTitle: "Cancel",

    bottomLayoutDefaults: {
        _constructor: "HLayout",
        width: "100%",
        height: 22,
        align: "right",
        membersMargin: 5,
        autoParent: "mainLayout"
    },

    //> @attr multiGroupDialog.applyButton (AutoChild IButton : null : RA)
    // Automatically generated +link{class:IButton} providing the mechanism for accepting
    // the current group configuration.  Fires the passed callback with a single parameter,
    // groupLevels, representing the current group configuration as an array of
    // +link{GroupSpecifier}s.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>multiGroupPanel.applyButtonProperties</code> and
    // <code>multiGroupPanel.applyButtonDefaults</code>.
    //
    // @visibility external
    //<
    applyButtonDefaults: {
        _constructor: "IButton",
        autoFit: true,
        height: 22,
        autoParent: "bottomLayout",
        click: "this.creator.apply()"
    },

    //> @attr multiGroupDialog.cancelButton (AutoChild IButton : null : RA)
    // Automatically generated +link{class:IButton} providing the mechanism for closing this
    // Dialog without accepting the current group configuration.  The passed callback is fired
    // with a single null parameter, indicating that the operation was cancelled.
    // <P>
    // This component is an +link{type:AutoChild} and as such may be customized via
    // <code>multiGroupPanel.cancelButtonProperties</code> and
    // <code>multiGroupPanel.cancelButtonDefaults</code>.
    //
    // @visibility external
    //<
    cancelButtonDefaults: {
        _constructor: "IButton",
        autoFit: true,
        height: 22,
        autoParent: "bottomLayout",
        click: "this.creator.cancel()"
    },

    bottomAutoChildren: ["bottomLayout", "applyButton", "cancelButton"]

    //> @attr multiGroupDialog.addLevelButton (AutoChild IButton : null : RA)
    // @include multiGroupPanel.addLevelButton
    //<

    //> @attr multiGroupDialog.deleteLevelButton (AutoChild IButton : null : RA)
    // @include multiGroupPanel.deleteLevelButton
    //<

    //> @attr multiGroupDialog.copyLevelButton (AutoChild IButton : null : RA)
    // @include multiGroupPanel.copyLevelButton
    //<

    //> @attr multiGroupDialog.levelUpButton (AutoChild ImgButton : null : RA)
    // @include multiGroupPanel.levelUpButton
    //<

    //> @attr multiGroupDialog.levelDownButton (AutoChild ImgButton : null : RA)
    // @include multiGroupPanel.levelDownButton
    //<

    //> @attr multiGroupDialog.fields (Array of Field : null : IR)
    // @include multiGroupPanel.fields
    //<

    //> @attr multiGroupDialog.optionsGrid (AutoChild ListGrid : null : IR)
    // @include multiGroupPanel.optionsGrid
    //<

});

isc.MultiGroupDialog.addProperties({
    initWidget : function () {
        this.Super("initWidget", arguments);

        // copy the unset properties
        this.copyUnsetProperties();

        this.addAutoChild("mainLayout");
        this.addAutoChild("multiGroupPanel", this.getPassthroughProperties());

        this.addAutoChildren(this.bottomAutoChildren);
        this.addItem(this.mainLayout);
        // grab a local copy of the panel's optionsGrid
        this.optionsGrid = this.multiGroupPanel.optionsGrid;
        // setup the button-states
        this.setButtonStates();
    },

    _passthroughs: [ "fields", "initialGrouping", "maxLevels", "invalidListPrompt",
        // autoChildren & i18nMessages
        "addLevelButtonTitle", "addLevelButtonDefaults", "addLevelButtonProperties",
        "deleteLevelButtonTitle", "deleteLevelButtonDefaults", "deleteLevelButtonProperties",
        "levelUpButtonTitle", "levelDownButtonTitle",
        "copyLevelButtonTitle", "copyLevelButtonDefaults", "copyLevelButtonProperties",
        // grid properties and titles
        "optionsGridDefaults", "optionsGridProperties",
        "firstGroupLevelTitle", "propertyFieldTitle", "groupingFieldTitle", "otherGroupLevelTitle"
    ],

    getPassthroughProperties : function () {
        var propNames = this._passthroughs,
            props = {};

        for (var i = 0; i < propNames.length; i++) {
            var name = propNames[i];
            if (this[name] != null) props[name] = this[name];
        }
        return props;
    },

    _unsetPropertiesPanel: [ "addLevelButtonTitle", "deleteLevelButtonTitle", "levelUpButtonTitle", "levelDownButtonTitle",
        "copyLevelButtonTitle", "firstGroupLevelTitle", "propertyFieldTitle",
        "groupingFieldTitle", "otherGroupLevelTitle", "invalidListPrompt"
    ],

    _unsetPropertiesDialog: [ "applyButtonTitle", "cancelButtonTitle", "title"
    ],

    copyUnsetProperties : function () {
        var propNames = this._unsetPropertiesPanel,
            props = {};
        for (var i = 0; i < propNames.length; i++) {
            var name = propNames[i];
            if (isc.MultiGroupPanel.getInstanceProperty(name) == null) {
                props[name] = isc.MultiSortPanel.getInstanceProperty(name);
            }
        }
        isc.MultiGroupPanel.addProperties(props);

        propNames = this._unsetPropertiesDialog;
        props = {};
        for (var i = 0; i < propNames.length; i++) {
            var name = propNames[i];
            if (isc.MultiGroupDialog.getInstanceProperty(name) == null) {
                props[name] = isc.MultiSortDialog.getInstanceProperty(name);
            }
        }
        isc.MultiGroupDialog.addProperties(props);
    },

    setButtonStates : function () {
        this.multiGroupPanel.setButtonStates();
        this.applyButton.setTitle(this.applyButtonTitle);
        this.cancelButton.setTitle(this.cancelButtonTitle);
    },

    //> @method multiGroupDialog.getNumLevels()
    // @include multiGroupPanel.getNumLevels
    //<
    getNumLevels : function () {
        return this.multiGroupPanel.getNumLevels();
    },

    //> @method multiGroupDialog.getGroup()
    // @include multiGroupPanel.getGroup
    //<
    getGroup : function () {
        return this.multiGroupPanel.getGroup();
    },

    //> @method multiGroupDialog.validate()
    // @include multiGroupPanel.validate
    //<
    validate : function () {
        return this.multiGroupPanel.validate();
    },

    closeClick : function () {
        this.cancel();
        return false;
    },

    cancel : function () {
        this.hide();
        this.markForDestroy();
    },

    apply : function () {
        // end the current edit, if there is one
        if (this.optionsGrid.getEditRow() != null) this.optionsGrid.endEditing();
        if (!this.validate()) return;
        if (this.callback) {
            // get the array of GroupSpecifiers and fire the callback is one was provided
            var specifiers = isc.shallowClone(this.getGroup());
            this.fireCallback(this.callback, "groupingMode", [specifiers]);
        }
        this.hide();
        this.markForDestroy();
    }

});


isc._debugModules = (isc._debugModules != null ? isc._debugModules : []);isc._debugModules.push('Grids');isc.checkForDebugAndNonDebugModules();isc._moduleEnd=isc._Grids_end=(isc.timestamp?isc.timestamp():new Date().getTime());if(isc.Log&&isc.Log.logIsInfoEnabled('loadTime'))isc.Log.logInfo('Grids module init time: ' + (isc._moduleEnd-isc._moduleStart) + 'ms','loadTime');delete isc.definingFramework;}else{if(window.isc && isc.Log && isc.Log.logWarn)isc.Log.logWarn("Duplicate load of module 'Grids'.");}

/*

  SmartClient Ajax RIA system
  Version v9.1p_2021-05-18/LGPL Deployment (2021-05-18)

  Copyright 2000 and beyond Isomorphic Software, Inc. All rights reserved.
  "SmartClient" is a trademark of Isomorphic Software, Inc.

  LICENSE NOTICE
     INSTALLATION OR USE OF THIS SOFTWARE INDICATES YOUR ACCEPTANCE OF
     ISOMORPHIC SOFTWARE LICENSE TERMS. If you have received this file
     without an accompanying Isomorphic Software license file, please
     contact licensing@isomorphic.com for details. Unauthorized copying and
     use of this software is a violation of international copyright law.

  DEVELOPMENT ONLY - DO NOT DEPLOY
     This software is provided for evaluation, training, and development
     purposes only. It may include supplementary components that are not
     licensed for deployment. The separate DEPLOY package for this release
     contains SmartClient components that are licensed for deployment.

  PROPRIETARY & PROTECTED MATERIAL
     This software contains proprietary materials that are protected by
     contract and intellectual property law. You are expressly prohibited
     from attempting to reverse engineer this software or modify this
     software for human readability.

  CONTACT ISOMORPHIC
     For more information regarding license rights and restrictions, or to
     report possible license violations, please contact Isomorphic Software
     by email (licensing@isomorphic.com) or web (www.isomorphic.com).

*/

