<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>design/CollectionView State Charts.graffle</filename>
    </added>
    <added>
      <filename>frameworks/datastore/tests/models/record/core_methods.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/mixins/collection_row_delegate.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/content.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/deleteSelection.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/deselect.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/itemViewForContentIndex.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/layerIdFor.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/length.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/mouse.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/nowShowing.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/reload.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/select.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/selectNextItem.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/selection.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/collection/ui_diagram.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/list/rowDelegate.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/list/rowHeightForContentIndex.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/list/rowOffsetForContentIndex.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/list/ui_outline.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/list/ui_row_heights.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/list/ui_simple.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/tests/views/stacked/ui_comments.js</filename>
    </added>
    <added>
      <filename>frameworks/desktop/views/stacked.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/controllers/tree.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/mixins/collection_content.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/mixins/tree_item_content.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/private/tree_item_observer.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/controllers/array/array_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/controllers/array/enum_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/controllers/array/null_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/controllers/array/single_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/controllers/object/empty_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/controllers/object/multiple_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/controllers/object/single_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/controllers/object/single_enumerable_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/controllers/tree/outline_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/mixins/string.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/private/tree_item_observer/flat_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/private/tree_item_observer/group_case.js</filename>
    </added>
    <added>
      <filename>frameworks/foundation/tests/private/tree_item_observer/outline_case.js</filename>
    </added>
    <added>
      <filename>frameworks/runtime/tests/core/console.js</filename>
    </added>
    <added>
      <filename>frameworks/runtime/tests/system/selection_set/copy.js</filename>
    </added>
    <added>
      <filename>frameworks/runtime/tests/system/selection_set/isEqual.js</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -61,3 +61,4 @@ config :standard_theme,
 %w(tests docs).each do |app_target|
   config app_target, :required =&gt; [:desktop], :theme =&gt; :standard_theme
 end
+ 
\ No newline at end of file</diff>
      <filename>Buildfile</filename>
    </modified>
    <modified>
      <diff>@@ -14,36 +14,61 @@
 */
 TestRunner.Target = SC.Record.extend(
 /** @scope TestRunner.Target.prototype */ {
+
+  primaryKey: &quot;name&quot;,
   
-  /** Load the tests for the target. */
-  tests: function() {
-    this.refreshTests();
-    return this._tests;
-  }.property().cacheable(),
+  /**
+    The target name.  This is the primary key
+    
+    @property
+  */
+  name: SC.Record.attr(String),
   
   /**
-    Loads the targets from the server.  When the targets have loaded, adds 
-    them to the store and then sets the local content.
+    The kind of target.  Usually is one of &quot;app&quot; or &quot;framework&quot;.
+    
+    @property
   */
-  refreshTests: function() {
-    if (!this._tests) this._tests = [] ;
-    SC.Request.getUrl(this.get('link_tests'))
-      .notify(this, 'testsDidRefresh').set('isJSON', YES).send();
-  },
-  
-  testsDidRefresh: function(request) {
-    var json = request.get('response'), len = json.length, idx;
-    for(idx=0;idx&lt;len;idx++) json[idx].guid = json[idx].url ; // patch
-    var tests = SC.Store.updateRecords(json, SC.Store, TestRunner.Test);
-    tests = tests.sort(function(a,b) { 
-      a = a.get('filename'); 
-      b = b.get('filename');
-      return (a&lt;b) ? -1 : (a&gt;b) ? 1 : 0;
-    });
+  kind: SC.Record.attr(String),
 
-    this.propertyWillChange('tests');
-    this._tests = tests ;
-    this.propertyDidChange('tests');
-  }
+  /**
+    Link to the document index json
 
+    @property
+  */
+  docsUrl: SC.Record.attr(String, { key: &quot;link_docs&quot; }),
+  
+  /**
+    Link to the test index json
+    
+    @property
+  */
+  testsUrl: SC.Record.attr(String, { key: &quot;link_tests&quot; }),
+  
+  /**
+    Link to the target itself.  This is only useful for applications.
+    
+    @property
+  */
+  rootUrl: SC.Record.attr(String, { key: &quot;link_root&quot; }),
+  
+  
+  /**
+    The parent target, if there is one.  This is a reference to the primary
+    key.
+    
+    @property
+  */
+  parent: SC.Record.hasOne(&quot;TestRuner.Target&quot;),
+  
+  /**
+    The display name for the target.  Computed by taking the last part of the
+    target name.
+  */
+  displayName: function() {
+    var name = this.get('name').split('/');
+    name = name[name.length-1] || '(none)';
+    return name.titleize();
+  }.property('name').cacheable()
+  
 }) ;</diff>
      <filename>apps/tests/models/target.js</filename>
    </modified>
    <modified>
      <diff>@@ -27,19 +27,18 @@ SC.DataSource = SC.Object.extend( /** SC.DataSource.prototype */ {
     be anything you want.
     
     If your data source subclass can handle the fetch you should override this 
-    method to return one out of three possible values:
-    1. SC.Array with storeKeys:
-       You can return it immediately or return an empty array and populate it 
-       dynamically later once the result set has arrived with .replace() .
-    2. SC.SparseArray:
-       The data source will be consulted to dynamically populate the contents 
-       of the array as it is requested.
-    3. SC.Query:
-       With this option, the record array returned from the store will 
-       automatically update when records are added, changed, or removed from 
-       the store. This is ideal if want to delegate to your store the job of 
-       keeping the record arrays up to date instead of consulting the 
-       data source every time.
+    method to return a SC.Array with storeKeys. You can return it immediately 
+    or return an empty array and populate it dynamically later once the result 
+    set has arrived with .replace() . You can also return a SC.SparseArray, 
+    where data source will be consulted to dynamically populate the contents 
+    of the array as it is requested.
+    
+    Note that if you gave an SC.Query as the fetchKey to findAll() on the store
+    you do not have to return anything from this method as you are from then on 
+    delegating the responsibility to keep the record array updated to the store.
+    The fetch() method in that case merely functions as a notification 
+    mechanism where you get the opportunity to fetch more data from a backend
+    based on the SC.Query.
     
     On return, the Store will write your result set in an SC.RecordArray 
     instance, which will monitor your array for changes and then maps those
@@ -53,19 +52,29 @@ SC.DataSource = SC.Object.extend( /** SC.DataSource.prototype */ {
     will call this method to load the required record to then materialize it.
     
     @param {SC.Store} store the requesting store
-    @param {Object} fetchKey key describing the request, may be SC.Record or
-        SC.Record.STORE_KEYS if invoked from store.retrieveRecords
+    @param {Object|SC.Query} fetchKey key describing the request, may be 
+      SC.Record or SC.Record.STORE_KEYS if invoked from store.retrieveRecords.
+      Could also be an SC.Query if that was passed in to findAll()
     @param {Hash} params optional additonal fetch params. storeKeys if invoked
-        from store.retrieveRecords
-    @returns {SC.Array|SC.Query} result set with storeKeys. In case of SC.Array
+      from store.retrieveRecords
+    @returns {SC.Array} result set with storeKeys. In case of SC.Array
       it may be sparse.
   */
   fetch: function(store, fetchKey, params) {
     return null;  
   },
   
-  retrieveRecords: function(store, storeKeys) {
-    return this._handleEach(store, storeKeys, this.retrieveRecord);  
+  /**
+    Called when store needs a set of storeKeys
+    
+    @param {SC.Store} store the requesting store
+    @param {Array} storeKeys
+    @param {Array} ids - optional
+    @returns 
+  */
+  
+  retrieveRecords: function(store, storeKeys, ids) {
+    return this._handleEach(store, storeKeys, this.retrieveRecord, ids);  
   },
   
   /**
@@ -103,9 +112,6 @@ SC.DataSource = SC.Object.extend( /** SC.DataSource.prototype */ {
     return (cret === uret === dret) ? (cret || uret || dret) : SC.MIXED_STATE ;
   },
   
-  
-  
-  
   /**
     Invoked by the store whenever it needs to cancel one or more records that
     are currently in-flight.  If any of the storeKeys match records you are
@@ -185,10 +191,12 @@ SC.DataSource = SC.Object.extend( /** SC.DataSource.prototype */ {
   /** @private
     invokes the named action for each store key.  returns proper value
   */
-  _handleEach: function(store, storeKeys, action) {
+  _handleEach: function(store, storeKeys, action, ids) {
     var len = storeKeys.length, idx, ret, cur;
+    if(!ids) ids = [];
+    
     for(idx=0;idx&lt;len;idx++) {
-      cur = action.call(this, store, storeKeys[idx]);
+      cur = action.call(this, store, storeKeys[idx], ids[idx]);
       if (ret === undefined) {
         ret = cur ;
       } else if (ret === YES) {
@@ -220,7 +228,15 @@ SC.DataSource = SC.Object.extend( /** SC.DataSource.prototype */ {
     return NO ;
   },
 
-  retrieveRecord: function(store, storeKey) {
+  /**
+    Called from retrieveRecords() to retrieve a single record.
+    
+    @param {SC.Store} store the requesting store
+    @param {Array} storeKey key to retrieve
+    @param {String} id the id to retrieve
+    @returns {Boolean} YES if handled
+  */
+  retrieveRecord: function(store, storeKey, id) {
     return NO ;
   },
 </diff>
      <filename>frameworks/datastore/data_sources/data_source.js</filename>
    </modified>
    <modified>
      <diff>@@ -39,9 +39,10 @@ SC.FixturesDataSource = SC.DataSource.extend( {
     @returns {SC.Array} result set with storeKeys.  
   */  
   fetch: function(store, fetchKey, params) {
+    
     var ret = [], dataHashes, i, storeKey, hashes= [];
     if (!(fetchKey === SC.Record || SC.Record.hasSubclass(fetchKey))) {
-      return null ;
+      return ret ;
     }
     dataHashes = this.fixturesFor(fetchKey);
     for(i in dataHashes){</diff>
      <filename>frameworks/datastore/data_sources/fixtures.js</filename>
    </modified>
    <modified>
      <diff>@@ -214,7 +214,7 @@ SC.Record = SC.Object.extend(
   /**
     This will return the raw attributes that you can edit directly.  If you 
     make changes to this hash, be sure to call beginEditing() before you get
-    the attributes and endEditing() aftwards.
+    the attributes and endEditing() afterwards.
   
     @returns {Object} the current attributes of the receiver
   **/
@@ -260,14 +260,14 @@ SC.Record = SC.Object.extend(
   
   normalize: function(includeNull) {
     
-    var primaryKey = this.primaryKey, dataHash = {}, recordId = this.get('id');
-    var store = this.get('store'), storeKey = this.get('storeKey'), attrValue, isRecord;
+    var primaryKey = this.primaryKey, dataHash = {}, recordId = this.get('id'), 
+      recHash, store = this.get('store'), storeKey = this.get('storeKey'), 
+      attrValue, isRecord, defaultVal;
     
     dataHash[primaryKey] = recordId;
     
     for(var key in this) {
-      // make sure property is a record attribute. if record attribute is a class (SC.Record)
-      // do not add to hash unless includeNull argument is true.
+      // make sure property is a record attribute.
       if(this[key] &amp;&amp; this[key]['typeClass']) {
         isRecord = SC.typeOf(this[key].typeClass())==='class';
 
@@ -276,19 +276,27 @@ SC.Record = SC.Object.extend(
           if(attrValue || includeNull) dataHash[key] = attrValue;
         }
         else if(isRecord) {
+          recHash = store.readDataHash(storeKey);
 
-          if(SC.typeOf(this.get(key))===SC.T_OBJECT) {
-            // if relationships are present
-            var ids = this.get(key).getEach('id');
-            dataHash[key] = ids.get('length')&gt;1 ? ids : ids[0];
+          if(recHash[key]) {
+            // write value already there
+            dataHash[key] = recHash[key];
           }
           else {
-            // otherwise write default
-            dataHash[key] = (this.get(key).get) ? this.get(key).get('id') : this.get(key);
+            // or write default
+            defaultVal = this[key].get('defaultValue');
+            if(SC.typeOf(defaultVal)===SC.T_FUNCTION) {
+              // computed default value
+              dataHash[key] = defaultVal();
+            }
+            else {
+              // plain value
+              dataHash[key] = defaultVal;
+            }
           }
-          
         }
-        else if(includeNull) {
+        
+        if(includeNull &amp;&amp; dataHash[key]===undefined) {
           dataHash[key] = null;
         }
         
@@ -330,9 +338,33 @@ SC.Record = SC.Object.extend(
   // PRIVATE
   //
   
+  /** @private
+    Creates string representation of record, with status.
+    
+    @returns {String}
+  */
+  
   toString: function() {
     var attrs = this.get('attributes');
-    return &quot;%@(%@)&quot;.fmt(this.constructor.toString(), SC.inspect(attrs));
+    return &quot;%@(%@) %@&quot;.fmt(this.constructor.toString(), SC.inspect(attrs), this.statusString());
+  },
+  
+  /** @private
+    Creates string representation of record, with status.
+    
+    @returns {String}
+  */
+  
+  statusString: function() {
+    var ret = [], status = this.status();
+    
+    for(prop in SC.Record) {
+      if(prop.match(/[A-Z_]$/) &amp;&amp; SC.Record[prop]===status) {
+        ret.push(prop);
+      }
+    }
+    
+    return ret.join(&quot; &quot;);
   }
       
 }) ;
@@ -473,6 +505,21 @@ SC.Record.mixin( /** @scope SC.Record */ {
     
     return ret ;
   },
+  
+  /**
+    Given a primaryKey value for the record, returns the associated
+    storeKey.  As opposed to storeKeyFor() however, this method
+    will NOT generate a new storeKey but returned undefined.
+    
+    @param {String} id a record id
+    @returns {Number} a storeKey.
+  */
+  storeKeyExists: function(id) {
+    var storeKeys = this.storeKeysById(),
+        ret       = storeKeys[id];
+    
+    return ret ;
+  },
 
   /** 
     Returns a record with the named ID in store.</diff>
      <filename>frameworks/datastore/models/record.js</filename>
    </modified>
    <modified>
      <diff>@@ -195,9 +195,13 @@ SC.RecordAttribute = SC.Object.extend(
       if (SC.none(value) &amp;&amp; (value = this.get('defaultValue'))) {
         if (typeof value === SC.T_FUNCTION) {
           value = this.defaultValue(record, key, this);
+          // write default value so it doesn't have to be executed again
+          
+          if(record.attributes()) record[attrKey] = value;
         }
       } else value = this.toType(record, key, value);
     }
+    
     return value ;
   },
 </diff>
      <filename>frameworks/datastore/models/record_attribute.js</filename>
    </modified>
    <modified>
      <diff>@@ -133,9 +133,11 @@ SC.NestedStore = SC.Store.extend(
     @returns {SC.Store} receiver
   */
   commitChanges: function(force) {
-    var pstore = this.get('parentStore');
-    pstore.commitChangesFromNestedStore(this, this.chainedChanges, force);
-    this.reset(); // clear out custom changes 
+    if (this.get('hasChanges')) {
+      var pstore = this.get('parentStore');
+      pstore.commitChangesFromNestedStore(this, this.chainedChanges, force);
+      this.reset(); // clear out custom changes
+    }
     return this ;
   },
 
@@ -177,7 +179,7 @@ SC.NestedStore = SC.Store.extend(
     this.discardChanges();
     
     var parentStore = this.get('parentStore');
-    if (parentStore) parentStore.willDestroyChildStore(this);
+    if (parentStore) parentStore.willDestroyNestedStore(this);
     
     sc_super();  
     return this ;</diff>
      <filename>frameworks/datastore/system/nested_store.js</filename>
    </modified>
    <modified>
      <diff>@@ -219,9 +219,9 @@ SC.RecordArray = SC.Object.extend(SC.Enumerable, SC.Array,
   */
   _storeKeysContentDidChange: function(target, key, value, rev) {
     this._records = null ; // clear cache
-    
     // if this record array is based on a queryKey reapply the
     // the query before setting the storeKeys to ensure it always conforms
+    
     if(SC.instanceOf(this.queryKey, SC.Query)) {
       this.storeKeys = SC.Query.containsStoreKeys(this.queryKey, value, this.store);
       this.notifyPropertyChange('length');</diff>
      <filename>frameworks/datastore/system/record_array.js</filename>
    </modified>
    <modified>
      <diff>@@ -95,12 +95,17 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
       store.commitChanges().destroy();
     }}}
     
+    @param {Hash} attrs optional attributes to set on new store
     @returns {SC.NestedStore} new nested store chained to receiver
   */
-  chain: function() {
-    var ret = SC.NestedStore.create({ parentStore: this }) ; 
-    var nested = this.nestedStores;
-    if (!nested) nested =this.nestedStores = [];
+  chain: function(attrs) {
+    if (!attrs) attrs = {};
+    attrs.parentStore = this;
+    
+    var ret    = SC.NestedStore.create(attrs),
+        nested = this.nestedStores;
+        
+    if (!nested) nested = this.nestedStores = [];
     nested.push(ret);
     return ret ;
   },
@@ -119,6 +124,18 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
     return this ;
   },
 
+  /**
+    Used to determine if a nested store belongs directly or indirectly to the
+    receiver.
+    
+    @param {SC.Store} store store instance
+    @returns {Boolean} YES if belongs
+  */
+  hasNestedStore: function(store) {
+    while(store &amp;&amp; (store !== this)) store = store.get('parentStore');
+    return store === this ;
+  },
+
   // ..........................................................
   // SHARED DATA STRUCTURES 
   // 
@@ -537,44 +554,43 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
     
     // OK, no locking issues.  So let's just copy them changes. 
     // get local reference to values.
-    var len = changes.length, i, storeKey;
-    var my_dataHashes, my_statuses, my_editables, my_revisions;
-    var ch_dataHashes, ch_statuses, ch_revisions;
+    var len = changes.length, i, storeKey, myDataHashes, myStatuses, 
+      myEditables, myRevisions, chDataHashes, chStatuses, chRevisions;
+    
+    myRevisions  = this.revisions ;
+    myDataHashes = this.dataHashes;
+    myStatuses   = this.statuses;
+    myEditables  = this.editables ;
     
-    my_revisions  = this.revisions ;
-    my_dataHashes = this.dataHashes;
-    my_statuses   = this.statuses;
-    my_editables  = this.editables ;
-
     // setup some arrays if needed
-    if (!my_editables) my_editables = this.editables = [] ;
+    if (!myEditables) myEditables = this.editables = [] ;
     
-    ch_dataHashes = nestedStore.dataHashes;
-    ch_revisions  = nestedStore.revisions ;
-    ch_statuses   = nestedStore.statuses;
+    chDataHashes = nestedStore.dataHashes;
+    chRevisions  = nestedStore.revisions ;
+    chStatuses   = nestedStore.statuses;
 
     SC.RunLoop.begin();
     for(i=0;i&lt;len;i++) {
       storeKey = changes[i];
 
       // now copy changes
-      my_dataHashes[storeKey] = ch_dataHashes[storeKey];
-      my_statuses[storeKey]   = ch_statuses[storeKey];
-      my_revisions[storeKey]  = ch_revisions[storeKey];
+      myDataHashes[storeKey] = chDataHashes[storeKey];
+      myStatuses[storeKey]   = chStatuses[storeKey];
+      myRevisions[storeKey]  = chRevisions[storeKey];
       
-      my_editables[storeKey] = 0 ; // always make dataHash no longer editable
+      myEditables[storeKey] = 0 ; // always make dataHash no longer editable
       
       this._notifyRecordPropertyChange(storeKey, NO);
     }
     SC.RunLoop.end();
     
     // add any records to the changelog for commit handling
-    var my_changelog = this.changelog, ch_changelog = nestedStore.changelog;
-    if (ch_changelog) {
-      if (!my_changelog) my_changelog = this.changelog = SC.Set.create();
-      my_changelog.addEach(ch_changelog);
+    var myChangelog = this.changelog, chChangelog = nestedStore.changelog;
+    if (chChangelog) {
+      if (!myChangelog) myChangelog = this.changelog = SC.Set.create();
+      myChangelog.addEach(chChangelog);
     }  
-    this.changelog=my_changelog;
+    this.changelog = myChangelog;
     
     return this ;
   },
@@ -647,28 +663,28 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
     
     h2. Query Keys
     
-    The kind of query key you pass is generally determined by the type of 
-    persistent stores you hook up for your application.  Most stores, however,
-    will accept an SC.Record subclass as the query key.  This will return 
+    The kind of fetchKey you pass is generally determined by the type of 
+    persistent stores you hook up for your application. Most stores, however,
+    will accept an SC.Record subclass as the fetchKey. It is up to your data source
+    to figure out which storeKeys to return based on the fetchKey. This will return 
     a RecordArray matching all instances of that class as is relevant to your
     application, for instance: findAll(MyApp.MyModel)
     
-    You can also pass an SC.Query object as your queryKey, for instance:
+    You can also pass an SC.Query object as your fetchKey, for instance:
     var q = SC.Query.create({ recordType: MyApp.MyModel, 
       conditions: &quot;firstName = 'John'&quot;, orderBy: &quot;lastName ASC&quot;});
     var records = MyApp.store.findAll(q);
     
-    In your dataSource fetch() method you can return either a store key 
-    array, a sparse array, or an SC.Query object.
-    
-    If an SC.Query is returned from the data source, the record array created in
+    If an SC.Query is given as fetchKey, the record array created in
     findAll() will automatically update when records are added, changed, 
-    or removed from the store.
+    or removed from the store. When a fetchKey is given, you do not have to 
+    return anything from the data source as you are from then on delegating
+    the responsibility to keep the record array updated to the store.
     
     Once you retrieve a RecordArray, you can filter the results even further
     by using the filter() method, which may issue even more specific requests.
     
-    @param {Object|SC.Query} queryKey key describing the type of records to 
+    @param {Object|SC.Query} fetchKey key describing the type of records to 
       fetch or a predefined SC.Query object
     @param {Hash} params optional additional parameters to pass along to the
       data source
@@ -676,38 +692,29 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
       within a given record array
     @returns {SC.RecordArray} matching set or null if no server handled it
   */
-  findAll: function(queryKey, params, recordArray) { 
-    var _store = this, source = this.get('dataSource'), ret, storeKeys, 
+  findAll: function(fetchKey, params, recordArray) { 
+    var _store = this, source = this.get('dataSource'), ret = [], storeKeys, 
       sourceRet, cacheKey;
     
     if(recordArray) {
       // giving a recordArray will circumvent the data source
       // typically happens when chaining findAll statements
-      storeKeys = SC.Query.containsRecords(queryKey, recordArray, _store);
+      storeKeys = SC.Query.containsRecords(fetchKey, recordArray, _store);
     }
     else if (source) {
-      // call fetch() on the data source. It can respond with either
-      // a storeKey array, a sparse array or a SC.Query object.
-      sourceRet = source.fetch.call(source, this, queryKey, params);
-      
-      if(SC.typeOf(sourceRet) === SC.T_ARRAY) {
+      // call fetch() on the data source.
+      sourceRet = source.fetch.call(source, this, fetchKey, params);
+      if(SC.typeOf(sourceRet)===SC.T_ARRAY) {
         storeKeys = sourceRet;
       }
-      else if(SC.instanceOf(sourceRet, SC.Query)) {
-        queryKey = sourceRet;
-      }
-      else {
-        throw(&quot;Data source fetch() has to return array or SC.Query object&quot;);
-      }
-      
     }
     
     // if SC.Query returned from data source or no data source was given 
-    if(!storeKeys &amp;&amp; SC.instanceOf(queryKey, SC.Query)) {
-      storeKeys = SC.Query.containsStoreKeys(queryKey, null, _store);
+    if(!storeKeys &amp;&amp; SC.instanceOf(fetchKey, SC.Query)) {
+      storeKeys = SC.Query.containsStoreKeys(fetchKey, null, _store);
     }
     
-    if(storeKeys) ret = this.recordArrayFromStoreKeys(storeKeys, queryKey, _store);
+    ret = this.recordArrayFromStoreKeys(storeKeys, fetchKey, _store);
     
     return ret ;
   },
@@ -717,22 +724,23 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
     cache if records are already cached, if not store them for reuse.
     
     @param {Array} storeKeys added to returned record array
+    @param {Object|SC.Query} fetchKey
     @param {SC.Store} _store
     @returns {SC.RecordArray} matching set or null if no server handled it
   */
   
-  recordArrayFromStoreKeys: function(storeKeys, queryKey, _store) {
+  recordArrayFromStoreKeys: function(storeKeys, fetchKey, _store) {
     var ret, isQuery, cacheKey;
     
     // if an array was provided, see if a wrapper already exists for 
     // this store.  Otherwise create it
-    cacheKey = SC.keyFor('__records__', SC.guidFor(storeKeys));
+    cacheKey = SC.keyFor('__records__', [SC.guidFor(storeKeys), SC.guidFor(fetchKey)].join('_'));
     ret = this[cacheKey];
     if (!ret) {
-      ret = SC.RecordArray.create({store: _store, queryKey: queryKey, storeKeys: storeKeys});
+      ret = SC.RecordArray.create({store: _store, queryKey: fetchKey, storeKeys: storeKeys});
       // store reference to record array if SC.Query so we can notify it
       // when store changes
-      if(SC.instanceOf(queryKey, SC.Query)) {
+      if(SC.instanceOf(fetchKey, SC.Query)) {
         if (!this.recordArraysWithQuery) this.recordArraysWithQuery = [];
         this.recordArraysWithQuery.push(ret);
       }
@@ -928,7 +936,7 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
     } else status = K.DESTROYED_DIRTY ;
     
     // remove the data hash, set new status
-    this.removeDataHash(storeKey, status);
+    this.writeStatus(storeKey, status);
     this.dataHashDidChange(storeKey);
 
     // add/remove change log
@@ -1013,7 +1021,6 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
     
     // record data hash change
     this.dataHashDidChange(storeKey, null);
-    
     // record in changelog
     changelog = this.changelog ;
     if (!changelog) changelog = this.changelog = SC.Set.create() ;
@@ -1077,8 +1084,8 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
     instance itself using materializeRecord()
     
     @param {SC.Record|Array} recordTypes class or array of classes
-    @param {Array} ids ids to destroy
-    @param {Array} storeKeys (optional) store keys to destroy
+    @param {Array} ids ids to retrieve
+    @param {Array} storeKeys (optional) store keys to retrieve
     @returns {Array} storeKeys to be retrieved
   */
   retrieveRecords: function(recordTypes, ids, storeKeys, _isRefresh) {
@@ -1138,12 +1145,12 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
     
     // now retrieve storekeys from dataSource
     if (source) {
-      var ok = source.retrieveRecords.call(source, this, ret);
+      var ok = source.retrieveRecords.call(source, this, ret, ids);
       if (ok === NO) ret.length = 0; // could not find.
     }
     return ret ;
   },
-
+  
   _TMP_RETRIEVE_ARRAY: [],
   
   /**
@@ -1232,6 +1239,10 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
     if(recordTypes===undefined &amp;&amp; ids===undefined &amp;&amp; storeKeys===undefined){
       storeKeys=this.changelog;
     }
+    
+    // if no storeKeys or ids at this point, return
+    if(!storeKeys &amp;&amp; !ids) return;
+    
     len = (storeKeys === undefined) ? ids.length : storeKeys.length;
     
     for(idx=0;idx&lt;len;idx++) {
@@ -1662,7 +1673,8 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
   },
   
   /**
-    Given a recordType and primaryKey, find the storeKey.
+    Given a recordType and primaryKey, find the storeKey. If the primaryKey 
+    has not been assigned a storeKey yet, it will be added.
     
     @param {SC.Record} recordType the record type
     @param {String} primaryKey the primary key
@@ -1673,6 +1685,18 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
   },
   
   /**
+    Given a primaryKey value for the record, returns the associated
+    storeKey.  As opposed to storeKeyFor() however, this method
+    will NOT generate a new storeKey but returned undefined.
+    
+    @param {String} id a record id
+    @returns {Number} a storeKey.
+  */
+  storeKeyExists: function(recordType, primaryKey) {
+    return recordType.storeKeyExists(primaryKey);
+  },
+  
+  /**
     Finds all storeKeys of a certain record type in this store
     and returns an array.
     
@@ -1681,7 +1705,7 @@ SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
   */
   
   storeKeysFor: function(recordType) {
-    var recType, ret = [];
+    var recType, ret = [], storeKey;
     if(!this.statuses) return;
     
     for(storeKey in SC.Store.recordTypesByStoreKey) {</diff>
      <filename>frameworks/datastore/system/store.js</filename>
    </modified>
    <modified>
      <diff>@@ -40,10 +40,13 @@ module(&quot;SC.Record normalize method&quot;, {
       
       // test toMany relationships
       relatedToMany: SC.Record.toMany('MyApp.Foo')
-      
+ 
     });
     
-    MyApp.Bar = SC.Record.extend({});
+    MyApp.Bar = SC.Record.extend({
+      // test toOne relationships
+      relatedTo: SC.Record.toOne('MyApp.Bar', { defaultValue: '1' })
+    });
     
     storeKeys = MyApp.store.loadRecords(MyApp.Foo, [
       { 
@@ -92,7 +95,7 @@ test(&quot;normalizing a pre-populated record&quot; ,function() {
   
   ok(sameValue, 'hash value of firstName after normalizing is 123 string');
   ok(sameValue, 'hash value of relatedTo should be 1');
-  ok(computedValues.indexOf(relatedToComputed)!==-1, 'hash value of relatedTo should be foo1, foo2 or foo3');
+  ok(computedValues.indexOf(relatedToComputed)!==-1, 'hash value of relatedToComputed should be either foo1, foo2 or foo3');
   
   equals(rec.get('firstName'), '123', 'get value of firstName after normalizing is 123 string');
   
@@ -163,3 +166,41 @@ test(&quot;normalizing a new record with toMany should reflect id in data hash&quot; ,func
   equals(newRecord.get('relatedToMany').get('length'), 2, 'number of relatedToMany is still 2');
   
 });
+
+test(&quot;normalizing a new record with toOne that has broken relationship&quot; ,function() {
+
+  var recHash = { 
+    guid: 'foo5', 
+    firstName: &quot;Andrew&quot;,
+    relatedTo: 'foo10' // does not exist
+  };
+
+  var newRecord = MyApp.store.createRecord(MyApp.Foo, recHash);
+  MyApp.store.commitRecords();
+  
+  equals(newRecord.attributes()['relatedTo'], 'foo10', 'should be foo10');
+  
+  newRecord.normalize();
+  
+  equals(newRecord.attributes()['relatedTo'], 'foo10', 'should remain foo10');
+  
+});
+
+test(&quot;normalizing a new record with toOne with relationship to wrong recordType&quot; ,function() {
+
+  var recHash = { 
+    guid: 'bar1', 
+    firstName: &quot;Andrew&quot;,
+    relatedTo: 'foo1' // does exist but wrong recordType
+  };
+
+  var newRecord = MyApp.store.createRecord(MyApp.Bar, recHash);
+  MyApp.store.commitRecords();
+  
+  equals(newRecord.attributes()['relatedTo'], 'foo1', 'should be foo1');
+  
+  newRecord.normalize();
+  
+  equals(newRecord.attributes()['relatedTo'], 'foo1', 'should remain foo1');
+  
+});</diff>
      <filename>frameworks/datastore/tests/models/record/normalize.js</filename>
    </modified>
    <modified>
      <diff>@@ -19,11 +19,11 @@ module(&quot;SC.Query evaluation of records&quot;, {
     
     // load some data
     MyApp.store.loadRecords(MyApp.Foo, [
-      { guid: 1, firstName: &quot;John&quot;, lastName: &quot;Doe&quot; },
-      { guid: 2, firstName: &quot;Jane&quot;, lastName: &quot;Doe&quot; },
-      { guid: 3, firstName: &quot;Emily&quot;, lastName: &quot;Parker&quot;, bornIn: 1975 },
-      { guid: 4, firstName: &quot;Johnny&quot;, lastName: &quot;Cash&quot; },
-      { guid: 5, firstName: &quot;Bert&quot;, lastName: &quot;Berthold&quot; }
+      { guid: 1, firstName: &quot;John&quot;, lastName: &quot;Doe&quot;, married: true },
+      { guid: 2, firstName: &quot;Jane&quot;, lastName: &quot;Doe&quot;, married: false },
+      { guid: 3, firstName: &quot;Emily&quot;, lastName: &quot;Parker&quot;, bornIn: 1975, married: true },
+      { guid: 4, firstName: &quot;Johnny&quot;, lastName: &quot;Cash&quot;, married: true },
+      { guid: 5, firstName: &quot;Bert&quot;, lastName: &quot;Berthold&quot;, married: true }
     ]);
     
     rec1 = MyApp.store.find(MyApp.Foo,1);
@@ -70,4 +70,13 @@ test(&quot;should handle undefined record properties correctly&quot;, function() {
   ok(q.contains(rec2) == true, 'record without bornIn set should match');
   
 }); 
+
+test(&quot;should handle boolean correctly&quot;, function() {
+  
+  q.conditions = &quot;married = true&quot;;
+  q.parseQuery();
+  ok(q.contains(rec1) == true, 'record with married set should match');
+  ok(q.contains(rec2) == false, 'record without married set should not match');
+  
+});
   </diff>
      <filename>frameworks/datastore/tests/system/query/evaluation_of_records.js</filename>
    </modified>
    <modified>
      <diff>@@ -19,6 +19,11 @@ module(&quot;SC.Query querying findAll on a store&quot;, {
       
       fetch: function(store, fetchKey, params) {
         return this.storeKeys;
+      },
+      
+      destroyRecord: function(store, storeKey){
+        store.dataSourceDidDestroy(storeKey);
+        return YES;
       }
       
     });
@@ -30,11 +35,11 @@ module(&quot;SC.Query querying findAll on a store&quot;, {
     MyApp.Faa = SC.Record.extend({});
     
     var records = [
-      { guid: 1, firstName: &quot;John&quot;, lastName: &quot;Doe&quot; },
-      { guid: 2, firstName: &quot;Jane&quot;, lastName: &quot;Doe&quot; },
-      { guid: 3, firstName: &quot;Emily&quot;, lastName: &quot;Parker&quot;, bornIn: 1975 },
-      { guid: 4, firstName: &quot;Johnny&quot;, lastName: &quot;Cash&quot; },
-      { guid: 5, firstName: &quot;Bert&quot;, lastName: &quot;Berthold&quot; }
+      { guid: 1, firstName: &quot;John&quot;, lastName: &quot;Doe&quot;, married: true },
+      { guid: 2, firstName: &quot;Jane&quot;, lastName: &quot;Doe&quot;, married: false },
+      { guid: 3, firstName: &quot;Emily&quot;, lastName: &quot;Parker&quot;, bornIn: 1975, married: true },
+      { guid: 4, firstName: &quot;Johnny&quot;, lastName: &quot;Cash&quot;, married: true },
+      { guid: 5, firstName: &quot;Bert&quot;, lastName: &quot;Berthold&quot;, married: true }
     ];
     
     // load some data
@@ -42,20 +47,6 @@ module(&quot;SC.Query querying findAll on a store&quot;, {
     // for sanity check, load two record types
     MyApp.store.loadRecords(MyApp.Faa, records);
     
-    // 
-    // now set up a second store with data source that returns SC.Query
-    // 
-    MyApp.DataSource2 = SC.DataSource.create({
-      // just return fetchKey which will be SC.Query
-      fetch: function(store, fetchKey, params) {
-        return fetchKey;
-      }
-    });
-    MyApp.store2 = SC.Store.create().from(MyApp.DataSource2);
-    MyApp.DataSource2.storeKeys = MyApp.store2.loadRecords(MyApp.Foo, records);
-    // for sanity check, load two record types
-    MyApp.store2.loadRecords(MyApp.Faa, records);
-    
   }
 });
 
@@ -64,6 +55,15 @@ module(&quot;SC.Query querying findAll on a store&quot;, {
 // RECORD PROPERTIES
 // 
 
+test(&quot;should find records based on boolean&quot;, function() {
+  
+  var q = SC.Query.create({recordType: MyApp.Foo, conditions:&quot;married=true&quot;});
+  
+  var records = MyApp.store.findAll(q);
+  equals(records.get('length'), 4, 'record length should be 4');
+  
+});
+
 test(&quot;should find records based on query string&quot;, function() {
   
   var q = SC.Query.create({recordType: MyApp.Foo, conditions:&quot;firstName = 'John'&quot;});
@@ -85,11 +85,11 @@ test(&quot;should find records based on SC.Query&quot;, function() {
 
 test(&quot;should find records based on SC.Query without recordType&quot;, function() {
   
-  var q = SC.Query.create({conditions:&quot;firstName = 'Jane'&quot;});
+  var q = SC.Query.create({conditions:&quot;lastName = 'Doe'&quot;});
   
   var records = MyApp.store.findAll(q);
   equals(records.get('length'), 2, 'record length should be 2');
-  equals(records.objectAt(0).get('firstName'), 'Jane', 'name should be Jane');
+  equals(records.objectAt(0).get('firstName'), 'John', 'name should be John');
   equals(records.objectAt(1).get('firstName'), 'Jane', 'name should be Jane');
 
 });
@@ -174,7 +174,7 @@ test(&quot;SC.Query returned from fetchRecords() should return result set&quot;, function(
   
   var q = SC.Query.create({recordType: MyApp.Foo, conditions:&quot;firstName = 'John'&quot;});
   
-  var records = MyApp.store2.findAll(q);
+  var records = MyApp.store.findAll(q);
   equals(records.get('length'), 1, 'record length should be 1');
   equals(records.objectAt(0).get('firstName'), 'John', 'name should be John');
 
@@ -184,7 +184,7 @@ test(&quot;Loading records after SC.Query is returned in fetchRecords() should show u
   
   var q = SC.Query.create({recordType: MyApp.Foo, conditions:&quot;firstName = 'John'&quot;});
   
-  var records = MyApp.store2.findAll(q);
+  var records = MyApp.store.findAll(q);
   equals(records.get('length'), 1, 'record length should be 1');
   equals(records.objectAt(0).get('firstName'), 'John', 'name should be John');
   
@@ -194,7 +194,7 @@ test(&quot;Loading records after SC.Query is returned in fetchRecords() should show u
     { guid: 22, firstName: &quot;Barbara&quot;, lastName: &quot;Jones&quot; }
   ];
   
-  MyApp.store2.loadRecords(MyApp.Foo, recordsToLoad);
+  MyApp.store.loadRecords(MyApp.Foo, recordsToLoad);
   
   equals(records.get('length'), 3, 'record length should be 3');
   
@@ -208,14 +208,14 @@ test(&quot;Loading records after getting empty record array based on SC.Query should
   
   var q = SC.Query.create({recordType: MyApp.Foo, conditions:&quot;firstName = 'Maria'&quot;});
   
-  var records = MyApp.store2.findAll(q);
+  var records = MyApp.store.findAll(q);
   equals(records.get('length'), 0, 'record length should be 0');
   
   var recordsToLoad = [
     { guid: 20, firstName: &quot;Maria&quot;, lastName: &quot;Johnson&quot; }
   ];
   
-  MyApp.store2.loadRecords(MyApp.Foo, recordsToLoad);
+  MyApp.store.loadRecords(MyApp.Foo, recordsToLoad);
   
   equals(records.get('length'), 1, 'record length should be 1');
   
@@ -227,10 +227,10 @@ test(&quot;Changing a record should make it show up in RecordArrays based on SC.Query
   
   var q = SC.Query.create({recordType: MyApp.Foo, conditions:&quot;firstName = 'Maria'&quot;});
   
-  var records = MyApp.store2.findAll(q);
+  var records = MyApp.store.findAll(q);
   equals(records.get('length'), 0, 'record length should be 0');
   
-  var record = MyApp.store2.find(MyApp.Foo, 1);
+  var record = MyApp.store.find(MyApp.Foo, 1);
   record.set('firstName', 'Maria');
   
   equals(records.get('length'), 1, 'record length should be 1');
@@ -243,10 +243,11 @@ test(&quot;Deleting a record should make the RecordArray based on SC.Query update acc
   
   var q = SC.Query.create({recordType: MyApp.Foo, conditions:&quot;firstName = 'John'&quot;});
   
-  var records = MyApp.store2.findAll(q);
+  var records = MyApp.store.findAll(q);
   equals(records.get('length'), 1, 'record length should be 1');
   
-  MyApp.store2.destroyRecord(MyApp.Foo, 1);
+  MyApp.store.destroyRecord(MyApp.Foo, 1);
+  MyApp.store.commitRecords();
   
   equals(records.get('length'), 0, 'record length should be 0');
   
@@ -312,11 +313,11 @@ test(&quot;Using orderBy in SC.Query and loading more records to the store&quot;, function
   
   var q = SC.Query.create({recordType: MyApp.Foo, orderBy:&quot;firstName ASC&quot;});
   
-  var records = MyApp.store2.findAll(q);
+  var records = MyApp.store.findAll(q);
   equals(records.get('length'), 5, 'record length should be 5');
   equals(records.objectAt(0).get('firstName'), 'Bert', 'name should be Bert');
   
-  MyApp.store2.loadRecords(MyApp.Foo, [
+  MyApp.store.loadRecords(MyApp.Foo, [
     { guid: 11, firstName: &quot;Anna&quot;, lastName: &quot;Petterson&quot; }
   ]);
   
@@ -349,10 +350,10 @@ test(&quot;Chaining findAll() queries and loading more records&quot;, function() {
   var q = SC.Query.create({recordType: MyApp.Foo, conditions:&quot;lastName='Doe'&quot;});
   var q2 = SC.Query.create({recordType: MyApp.Foo, conditions:&quot;firstName='John'&quot;});
   
-  var records = MyApp.store2.findAll(q).findAll(q2);
+  var records = MyApp.store.findAll(q).findAll(q2);
   equals(records.get('length'), 1, 'record length should be 1');
   
-  MyApp.store2.loadRecords(MyApp.Foo, [
+  MyApp.store.loadRecords(MyApp.Foo, [
     { guid: 11, firstName: &quot;John&quot;, lastName: &quot;Doe&quot; }
   ]);
   </diff>
      <filename>frameworks/datastore/tests/system/query/find_all.js</filename>
    </modified>
    <modified>
      <diff>@@ -9,7 +9,7 @@
 var SC = SC || {} ; 
 
 SC.mapDisplayNames = function(obj, level, path, seen) {
-  if (!SC.browser.safari) return ; 
+  if (!SC.browser.safari) return ;
   if (obj === undefined) obj = window;
   if (level === undefined) level = 0;
   if (path === undefined) path = [];
@@ -22,7 +22,7 @@ SC.mapDisplayNames = function(obj, level, path, seen) {
   path[loc] = '';
   
   for(var key in obj) {
-    if (!obj.hasOwnProperty(key)) continue ;
+    if (obj.hasOwnProperty &amp;&amp; !obj.hasOwnProperty(key)) continue ;
     if (!isNaN(Number(key))) continue ; // skip array indexes
     if (key === &quot;constructor&quot;) continue ;
     if (key === &quot;superclass&quot;) continue ;</diff>
      <filename>frameworks/debug/core.js</filename>
    </modified>
    <modified>
      <diff>@@ -94,7 +94,7 @@ SC.PageDesignController = SC.Object.extend({
   // INTERNAL SUPPORT
   // 
   init: function() {
-    this.designers = new SC.Set();
+    this.designers = SC.Set.create();
     this.sel = [];
     sc_super();
   }</diff>
      <filename>frameworks/designer/controllers/page_design.js</filename>
    </modified>
    <modified>
      <diff>@@ -9,7 +9,7 @@
 }
 
 .sc-list-item-view.sel {
-	background-color: #666; 
+	background-color: #888; 
 	color: white ;
 }
 
@@ -31,6 +31,14 @@
 	right: 4px;
 }
 
+.sc-list-item-view .sc-outline {
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+}
+
 .sc-list-item-view.has-icon label,
 .sc-list-item-view.has-checkbox label {
 	left: 24px;
@@ -42,6 +50,44 @@
 
 /* @end */
 
+/* @group Disclosure */
+
+.sc-list-item-view img.disclosure {
+	position: absolute ;
+	left: -11px;
+	top: 50%;
+	margin-top: -6px;
+	width: 14px;
+	height: 14px;
+	background-image: static_url('images/sc-theme-sprite.png') ;
+	background-position: -16px -1025px ;
+}
+
+.sc-list-item-view img.disclosure.open {
+	background-position: 0px -1008px ;
+}
+
+.sc-list-item-view img.disclosure.active {
+	background-position: -0px -1041px ;
+}
+
+.sc-list-item-view img.disclosure.open.active {
+	background-position: -16px -1008px ;
+}
+
+.sc-list-item-view.disabled img.disclosure,
+.sc-list-item-view.disabled img.disclosure.active {
+	background-position: -16px -1041px ;
+}
+
+.sc-list-item-view.disabled img.disclosure.open,
+.sc-list-item-view.disabled img.disclosure.open.active {
+	background-position: 0px -1024px ;
+}
+
+
+/* @end */
+
 /* @group Icon */
 
 .sc-list-item-view img.icon {</diff>
      <filename>frameworks/desktop/english.lproj/list_item.css</filename>
    </modified>
    <modified>
      <diff>@@ -6,39 +6,32 @@
 // ==========================================================================
 
 /**
-  Indicates that the collection view expects to accept a drop ON the specified
-  item.
-*/
-SC.DROP_ON = 0x01 ;
-
-/**
-  Indicates that the collection view expects to accept a drop BEFORE the 
-  specified item.
-*/
-SC.DROP_BEFORE = 0x02 ;
-
-/**
-  Indicates that the collection view want's to know which operations would 
-  be allowed for either drop operation.
-*/
-SC.DROP_ANY = 0x03 ;
-
-/**
   @namespace
 
-  A Collection View Delegate is consulted by a SC.CollectionView's to control
-  certain behaviors such as selection control and drag and drop behaviors.
+  A Collection View Delegate is consulted by a SC.CollectionView's to make
+  policy decisions about certain behaviors such as selection control and
+  drag and drop.  If you need to control other aspects of your data, you may
+  also want to add the SC.CollectionContent mixin.
   
-  To act as a Collection Delegate, the object should be set as the delegate
-  property of the collection view and should implement one or more of the
-  methods below.
+  To act as a Collection Delegate, just apply this mixin to your class.  You
+  must then set the &quot;delegate&quot; property on the CollectionView to your object.
   
-  You can also choose to mixin this delegate to get suitable default 
-  implementations of these methods.
+  Alternatively, if no delegate is set on a CollectionView, but the content 
+  implements this mixin, the content object will be used as the delegate 
+  instead.
+  
+  If you set an ArrayController or its arrangedObjects property as the content
+  of a CollectionView, the ArrayController will automatically act as the 
+  delegate for the view.
   
   @since SproutCore 1.0
 */
 SC.CollectionViewDelegate = {
+
+  /**
+    Used to detect the mixin by SC.CollectionView
+  */
+  isCollectionViewDelegate: YES,
   
   /**
     This method will be called anytime the collection view is about to
@@ -48,9 +41,9 @@ SC.CollectionViewDelegate = {
     selected objects that cannot be selected.  The default implementation of
     this method simply returns the proposed selection.
     
-    @param view {SC.CollectionView} the collection view
-    @param sel {Array} Proposed array of selected objects.
-    @returns The actual array allowed or null if no change is allowed.
+    @param {SC.CollectionView} view the collection view
+    @param {SC.IndexSet} sel Proposed array of selected objects.
+    @returns {SC.IndexSet} Actual allow selection index set
   */
   collectionViewSelectionForProposedSelection: function(view, sel) {
     return sel ;
@@ -188,39 +181,71 @@ SC.CollectionViewDelegate = {
     This method is only called if canDeleteContent is YES on the collection
     view.
     
-    @param view {SC.CollectionView} the collection view
-    @param item {Array} proposed array  of items to delete.
-    @returns {Array} items allowed to delete or null.
+    @param {SC.CollectionView} view the collection view
+    @param {SC.IndexSet} indexes proposed index set of items to delete.
+    @returns {SC.IndexSet} index set allowed to delete or null.
   */
-  collectionViewShouldDeleteContent: function(view, items) { return items; },
+  collectionViewShouldDeleteIndexes: function(view, indexes) { 
+    return indexes; 
+  },
   
   /**
     Called by the collection view to actually delete the selected items.
     
-    The default behavior will use standard array operators to remove the 
-    items from the content array.  You can implement this method to provide
-    your own deletion method.
+    The default behavior will use standard array operators to delete the 
+    indexes from the array.  You can implement this method to provide your own 
+    deletion method.
     
-    If you simply want to controls the items to be deleted, you should instead
+    If you simply want to control the items to be deleted, you should instead
     implement collectionViewShouldDeleteItems().  This method will only be 
-    called if canDeleteContent is YES and collectionViewShouldDeleteContent()
-    returns a non-empty array.
+    called if canDeleteContent is YES and collectionViewShouldDeleteIndexes()
+    returns a non-empty index set
     
-    @param view {SC.CollectionView} the view collection view
-    @param items {Array} the items to delete
-    @returns {Boolean} YES if the operation succeeded, NO otherwise.
+    @param {SC.CollectionView} view collection view
+    @param {SC.IndexSet} indexes the items to delete
+    @returns {Boolean} YES if the deletion was a success.
   */
-  collectionViewDeleteContent: function(view, items) { return NO; },
+  collectionViewDeleteContent: function(view, content, indexes) {
+    if (!content) return NO ;
+    
+    if (SC.typeOf(content.destroyAt) === SC.T_FUNCTION) {
+      content.destroyAt(indexes);
+      return YES ;
+      
+    } else if (SC.typeOf(content.removeAt) === SC.T_FUNCTION) {
+      content.removeAt(indexes);
+      return YES;
+      
+    } else return NO ;
+  },
   
   /**
-    Called by the collection when attempting to select an item.
+    Called by the collection when attempting to select an item.  Return the 
+    actual indexes you want to allow to be selected.  Return null to disallow
+    the change.  The default allows all selection.
+    
+    @param {SC.CollectionView} view the view collection view
+    @param {SC.IndexSet} indexes the indexes to be selected
+    @param {Boolean} extend YES if the indexes will extend existing sel
+    @returns {SC.IndexSet} allowed index set
+  */
+  collectionViewShouldSelectIndexes: function (view, indexes, extend) { 
+    return indexes; 
+  },
+  
+  /**
+    Called by the collection when attempting to deselect an item.  Return the 
+    actual indexes you want to allow to be deselected.  Return null to 
+    disallow the change.  The default allows all selection.
     
-    The default implementation always returns YES.
+    Note that you should not modify the passed in IndexSet.  clone it instead.
     
-    @param view {SC.CollectionView} the view collection view
-    @param item {Object} the item to be selected
-    @returns {Boolean} YES to alow, NO to prevent it
+    @param {SC.CollectionView} view the view collection view
+    @param {SC.IndexSet} indexes the indexes to be selected
+    @returns {SC.IndexSet} allowed index set
   */
-  collectionViewShouldSelectItem: function (view, item) { return YES; }
+  collectionViewShouldDeselectIndexes: function (view, indexes) { 
+    return indexes; 
+  }
   
 };</diff>
      <filename>frameworks/desktop/mixins/collection_view_delegate.js</filename>
    </modified>
    <modified>
      <diff>@@ -193,8 +193,15 @@ SC.AlertPane = SC.PanelPane.extend({
     @property {SC.ButtonView}
   */
   buttonThree: SC.outlet('contentView.childViews.2.childViews.0'),
+
+  /**
+    The view for the button 3 (Extra) wrapper.
+    
+    @property {SC.View}
+  */
+  buttonThreeWrapper: SC.outlet('contentView.childViews.2'),
   
-  layout: { centerX: 0, width: 452, top: 100 },
+  layout: { centerX: 0, width: 502, top: 55 },
 
   /** @private - internal view that is actually displayed */
   contentView: SC.View.extend({
@@ -220,7 +227,7 @@ SC.AlertPane = SC.PanelPane.extend({
       }),
 
       SC.View.extend({
-        layout: { bottom: 13, height: 24, right: 18, width: 416 },
+        layout: { bottom: 13, height: 24, right: 18, width: 466 },
         childViews: ['cancelButton', 'okButton'],
         classNames: ['textAlignRight'],
         cancelButton : SC.ButtonView.extend({
@@ -250,6 +257,7 @@ SC.AlertPane = SC.PanelPane.extend({
       
       SC.View.extend({
         layout: { bottom: 13, height: 24, left: 18, width: 150 },
+        isVisible: NO,
         childViews: [
           SC.ButtonView.extend({
             useStaticLayout: YES,
@@ -340,7 +348,13 @@ SC.AlertPane.show = function(message, description, caption, button1Title, button
   for(var idx=0;idx&lt;3;idx++) {
     button = ret.get(buttonKeys[idx]);
     title = args[idx + 3];
-    if (title) button.set('title', title).set('isVisible', YES);
+    if (title) {
+      button.set('title', title).set('isVisible', YES);
+      if (idx==2) {
+        button_wrapper = ret.get('buttonThreeWrapper');
+        button_wrapper.set('isVisible', YES);
+      }
+    }
   }
   var show = ret.append() ; // make visible.
   ret.adjust('height', ret.childViews[0].$().height()) ;</diff>
      <filename>frameworks/desktop/panes/alert.js</filename>
    </modified>
    <modified>
      <diff>@@ -179,18 +179,37 @@ SC.MenuPane = SC.PickerPane.extend(
   */
   previousSelectedMenuItem : null,
 
-  /*
+  /**
     The anchor for this Menu
 
     @type ButtonView/MenuItemView
   */
   anchor: null,
   
+  /** @private
+
+    Array of Display Items which is produced by displayItems function
+  */
   displayItemsArray: null,
   
+  /**
+    Set of Menu Item Views created from items array
+    
+    @type SC.Array
+  */
   menuItemViews: [],
 
+  /** 
+    Example view which will be used to create the Menu Items
+    
+    @default SC.MenuItemView
+    @type SC.View
+  */
+  exampleView: SC.MenuItemView,
+  
   /**
+    @private
+    
     Overwrite the popup function of the pickerPane
   */
   popup: function(anchorViewOrElement, preferMatrix) {
@@ -200,9 +219,9 @@ SC.MenuPane = SC.PickerPane.extend(
     this.set('anchor',anchorViewOrElement);
     this.set('preferType',SC.PICKER_MENU) ;
     if(preferMatrix) this.set('preferMatrix',preferMatrix) ;
-    this.positionPane() ;
     this.endPropertyChanges();
     this.append() ;
+    this.positionPane() ;
   },
   
   /**
@@ -212,7 +231,6 @@ SC.MenuPane = SC.PickerPane.extend(
     @type {String}
   */
   displayItems: function() {
-
     var items = this.get('items') ;
     var loc = this.get('localize') ;
     var keys = null,itemType, cur ;
@@ -221,7 +239,7 @@ SC.MenuPane = SC.PickerPane.extend(
     var idx, item ;
     var fetchKeys = SC._menu_fetchKeys ;
     var fetchItem = SC._menu_fetchItem ;
-    var menuHeight = this.get('menuHeight')||0 ;
+    var menuHeight = 0 ;
     // loop through items and collect data
     for (idx = 0; idx &lt; max; ++idx) {
       item = items.objectAt(idx) ;
@@ -251,7 +269,7 @@ SC.MenuPane = SC.PickerPane.extend(
                                               isEnabled: cur[2], icon: cur[3], 
                                               isSeparator: cur[4], action: cur[5],
                                               isCheckbox: cur[6], isShortCut: cur[7],
-                                              menuItemNumber: cur[9], isBranch: cur[8],
+                                              menuItemNumber: idx, isBranch: cur[8],
                                               itemHeight: cur[9], subMenu: cur[10], 
                                               keyEquivalent: cur[11], target:cur[12] }) ;                         
       }
@@ -298,30 +316,55 @@ SC.MenuPane = SC.PickerPane.extend(
     }
     // collect some data 
     var items = this.get('displayItems') ;
-    var ret = sc_super();
+    //var ret = sc_super();
     // regenerate the buttons only if the new display items differs from the
-    // last cached version of it needsFirstDisplay is YES.
+    // last cached version of it.
 
     var last = this.get('_menu_displayItems') ;
     if (firstTime || (items !== last)) {
       if(!this.get('isEnabled') || !this.get('contentView')) return ;
-      var contentView = this.get('contentView');
       var menuHeight = this.get('menuHeight');
       this.set('_menu_displayItems',items) ; // save for future
       context.addStyle('text-align', 'center') ;
-      var itemWidth = this.get('itemWidth');
+      var itemWidth = this.get('itemWidth') ;
       if (SC.none(itemWidth)) {
         itemWidth = this.get('layout').width || 100;
-        this.set('itemWidth',itemWidth); 
+        this.set('itemWidth',itemWidth) ; 
       }
       this.renderChildren(context,items) ;
+      context.push(&quot;&lt;div class='top-left-edge'&gt;&lt;/div&gt;&quot;,
+       &quot;&lt;div class='top-edge'&gt;&lt;/div&gt;&quot;,
+       &quot;&lt;div class='top-right-edge'&gt;&lt;/div&gt;&quot;,
+       &quot;&lt;div class='right-edge'&gt;&lt;/div&gt;&quot;,
+       &quot;&lt;div class='bottom-right-edge'&gt;&lt;/div&gt;&quot;,
+       &quot;&lt;div class='bottom-edge'&gt;&lt;/div&gt;&quot;,
+       &quot;&lt;div class='bottom-left-edge'&gt;&lt;/div&gt;&quot;,
+       &quot;&lt;div class='left-edge'&gt;&lt;/div&gt;&quot;);
+    }
+    else {
+      this.get('menuItemViews').forEach( function(menuItemView) { 
+        menuItemView.updateLayer();
+      }, this) ;
     }
-    context.addStyle('height',this.menuHeight) ;
     if (SC.BENCHMARK_MENU_PANE_RENDER) SC.Benchmark.end(bkey) ;
-    return ret;
+    //return ret ;
   },
 
   /**
+    This method is used to observe the menuHeight and set the layout accordingly
+    and position the pane.
+    
+    @observes menuHeight
+  */
+  menuHeightObserver: function() {
+    var height = this.layout.height ;
+    var menuHeight = this.get('menuHeight') ; 
+    if( height !== menuHeight) {
+      this.adjust('height',menuHeight).updateLayout() ;
+    }
+  }.observes('menuHeight'),
+  
+  /**
     Actually generates the menu HTML for the display items.  This method 
     is called the first time a view is constructed and any time the display
     items change thereafter.  This will construct the HTML but will not set
@@ -347,10 +390,10 @@ SC.MenuPane = SC.PickerPane.extend(
       var itemSubMenu = item.get('subMenu') ;
       var itemHeight = item.get('itemHeight') ;
       var itemKeyEquivalent = item.get('keyEquivalent') ;
+      var itemTarget = item.get('target') ;
       var itemWidth = this.get('itemWidth') ;
-      var itemTarget = this.get('itemTarget') ;
       var itemView = this.createChildView(
-        SC.MenuItemView, {
+        this.exampleView, {
           owner : itemView,
           displayDelegate : itemView,
           parentPane: this,
@@ -359,35 +402,34 @@ SC.MenuPane = SC.PickerPane.extend(
           contentValueKey : 'title',
           contentIconKey : 'icon',
           contentCheckboxKey : 'checkbox',
-          contentIsBranchKey :'branchItem',  
+          contentIsBranchKey : 'branchItem',  
           isSeparatorKey : 'separator',
-          shortCutKey :'shortCut',  
+          shortCutKey : 'shortCut',  
           action : itemAction,
           target : itemTarget,
-          contentTargetKey:'target',
-          layout : { top:0, left:0, width:itemWidth, height:itemHeight, centerX:0, centerY:0},
+          layout : { top: 0, left: 0, width: itemWidth, height: itemHeight },
           isEnabled : itemIsEnabled,
           itemHeight : itemHeight,
           itemWidth : itemWidth,
           keyEquivalent : itemKeyEquivalent,
           content : SC.Object.create({
-          title : itemTitle,
-          value : itemValue,
-          icon : itemIcon,
-          separator : isSeparator,
-          action : itemAction,
-          checkbox : isCheckbox,
-          shortCut : isShortCut,
-          branchItem : isBranch,
-          subMenu : itemSubMenu
-        }),
+            title : itemTitle,
+            value : itemValue,
+            icon : itemIcon,
+            separator : isSeparator,
+            action : itemAction,
+            checkbox : isCheckbox,
+            shortCut : isShortCut,
+            branchItem : isBranch,
+            subMenu : itemSubMenu
+          }),
         rootElementPath : [menuItemNumber]
       });
       context = context.begin(itemView.get('tagName')) ;
       itemView.prepareContext(context, YES) ;
       context = context.end() ;
-      menuItemViews.push(itemView);
-      this.set('menuItemViews',menuItemViews);
+      menuItemViews.push(itemView) ;
+      this.set('menuItemViews',menuItemViews) ;
     }
   },
   
@@ -428,22 +470,29 @@ SC.MenuPane = SC.PickerPane.extend(
     @returns {Boolean}  YES if handled, NO otherwise
   */
   performKeyEquivalent: function(keyString,evt) {
-    if(!this.get('isEnabled')) return YES ;
-    var items = this.get('displayItemsArray') ;
-    if(!items) return;
-    var len = items.length ;
-    var menuItems = this.get('menuItemViews');
+    var items, len, menuItems, item, keyEquivalent, 
+        action, isEnabled, target;
+    
+    if(!this.get('isEnabled')) return NO ;
+
+    items = this.get('displayItemsArray') ;
+    if (!items) return NO;
+
+    len = items.length ;
+    menuItems = this.get('menuItemViews');
     for(var idx=0; idx&lt;len; ++idx) {
-      var item = items[idx] ;
-      var keyEquivalent = item.get('keyEquivalent') ;
-      var action = item.get('action') ;
-      var isEnabled = item.get('isEnabled') ;
-      var target = item.get('target') || this ;
+      item          = items[idx] ;
+      keyEquivalent = item.get('keyEquivalent') ;
+      action        = item.get('action') ;
+      isEnabled     = item.get('isEnabled') ;
+      target        = item.get('target') || this ;
       if(keyEquivalent == keyString &amp;&amp; isEnabled) {
-        if(menuItems &amp;&amp; menuItems[idx]) menuItems[idx].triggerAction(evt);
+        if(menuItems &amp;&amp; menuItems[idx]) {
+          return menuItems[idx].triggerAction(evt);
+        }
       }
     }
-    return YES ;
+    return NO ;
   },
   
   //Mouse and Key Events
@@ -561,7 +610,26 @@ SC.MenuPane = SC.PickerPane.extend(
     }
     if(!this.clickInside(f, evt)) this.remove();
     return YES;
+  },
+  
+  /** 
+    Get the Menu Item based on the key,value passed
+    @params {String} key 
+    @params {String} value 
+    
+    @returns SC.MenuItemView
+  */
+  getMenuItem: function(key,value) {
+    var displayItems = this.get('displayItemsArray') ;
+    var menuItemViews = this.get('menuItemViews') ;
+    if(displayItems &amp;&amp; menuItemViews) {
+      var idx = displayItems.get(key).indexOf(value);
+      if(idx !== -1) return menuItemViews[idx];
+      else return null;
+    }
+    else return null;
   }
+  
 });
 
 SC._menu_fetchKeys = function(k) {</diff>
      <filename>frameworks/desktop/panes/menu.js</filename>
    </modified>
    <modified>
      <diff>@@ -273,10 +273,10 @@ SC.Drag = SC.Object.extend(
     var pane = dv.get('pane') ;
     var pv = dv.get('parentView') ;
     var clippingFrame = dv.get('clippingFrame') ;
-    
     // convert to global cooridinates
-    var f = pv.convertFrameToView(clippingFrame, null) ;
-    var pf = pane.get('frame') ;
+    //var f = pv ? pv.convertFrameToView(clippingFrame, null) : clippingFrame ;
+    var f = pv ? pv.convertFrameToView(dv.get('frame'), null) : dv.get('frame') ;
+    var pf = pane ? pane.get('frame') : {x:0, y: 0};
     
     dv.adjust({
       top: f.y + pf.y,
@@ -284,11 +284,22 @@ SC.Drag = SC.Object.extend(
       width: f.width,
       height: f.height
     });
+    //get frame in global cords after pane adjustment
+    var dvf = dv.get('frame');
+    
+    var origin = f;//pv.convertFrameToView(dv.get('frame'), null) ;
     
-    var origin = f ; // dv.convertFrameToView(dv.get('frame'), null) ;
-    var pointer = { x: this.event.pageX, y: this.event.pageY } ;
-    this.ghostOffset = { x: (pointer.x-origin.x), y: (pointer.y-origin.y) } ;
+    // console.log(&quot;clipping Frame x: %@ y: %@ &quot;.fmt(clippingFrame.x, clippingFrame.y));
+    // console.log(&quot;dvf x: %@ y: %@ &quot;.fmt(dvf.x, dvf.y));
+    // 
+    // console.log(&quot;f x: %@ y: %@ &quot;.fmt(f.x, f.y));
+    // console.log(&quot;loc x: %@ loc: %@ &quot;.fmt(loc.x, loc.y));
+    // console.log(&quot;pf x: %@ pf: %@ &quot;.fmt(pf.x, pf.y));
     
+    this.ghostOffset = { x: (loc.x-origin.x), y: (loc.y-origin.y) } ;
+    // console.log(&quot;ghost Offset x: %@ pf: %@ &quot;.fmt(this.ghostOffset.x, this.ghostOffset.y));
+    
+    this.mouseGhostOffset = {x: loc.x - (dvf.x), y: loc.y - (dvf.y)}
     // position the ghost view
     this._positionGhostView(evt) ;
     
@@ -400,7 +411,7 @@ SC.Drag = SC.Object.extend(
       // notify last drop target that the drag exited, to allow it to cleanup
       if (target &amp;&amp; target.dragExited) target.dragExited(this, evt) ;
     } catch (e) {
-      onsole.log('Exception in SC.Drag.mouseUp(target.dragExited): %@'.fmt(e)) ;
+      //onsole.log('Exception in SC.Drag.mouseUp(target.dragExited): %@'.fmt(e)) ;
     }
     
     // notify all drop targets that the drag ended
@@ -434,8 +445,8 @@ SC.Drag = SC.Object.extend(
       classNames:['sc-ghost-view'],
       layout: { top: frame.y, left: frame.x, width: frame.width, height: frame.height },
       owner: this,
-      render: function(context, firstTime) {
-        if (firstTime) context.push(that.dragView.$().html()) ;
+      didCreateLayer: function() {
+        this.get('layer').appendChild(that.dragView.get('layer').cloneNode(true));
       }
     });
     
@@ -814,4 +825,4 @@ SC.Drag.mixin(
     delete this._scrollableViews[SC.guidFor(target)] ;  
   }
   
-});
+});
\ No newline at end of file</diff>
      <filename>frameworks/desktop/system/drag.js</filename>
    </modified>
    <modified>
      <diff>@@ -217,7 +217,7 @@ SC.RootResponder = SC.RootResponder.extend(
   */ 
   attemptKeyEquivalent: function(evt) {
     var ret = null ;
-    
+
     // keystring is a method name representing the keys pressed (i.e 
     // 'alt_shift_escape')
     var keystring = evt.commandCodes()[0];
@@ -420,7 +420,7 @@ SC.RootResponder = SC.RootResponder.extend(
         return evt.hasCustomEventHandling ;
       }
     }
-    return YES; // allow normal processing...
+    return this.sendEvent('keyDown', evt) ; // allow normal processing...
   },
   
   /** @private</diff>
      <filename>frameworks/desktop/system/root_responder.js</filename>
    </modified>
    <modified>
      <diff>@@ -1,39 +1,197 @@
-// ========================================================================
-// SC.CollectionView selectPreviousItem Unit Test
-// ========================================================================
-/*globals module test ok isObj equals expects */
-
-var array, collectionView ; // global variables
-
-module(&quot;CollectionView selectPreviousItem&quot;, {
-	
-	setup: function() {
-		array = [{ item:1 }, { item:2 }, { item:3 }, { item:4 }, { item:5 }] ;
-		collectionView = SC.CollectionView.create({ content: array }) ;
-	}
-	
-}) ;
-
-test(&quot;should select previous item stepby numberOfItems&quot;, function() {
-
-	// test stepby numberOfItems for selectPreviousItem
-	[1,2,3].map(function(n) {
-		for (var idx = array.length - 1; idx &gt;= 0; idx--) {
-			// set current selection to array idx
-			collectionView.set('selection', [collectionView.get('content').get(idx)]) ;
-			var c = collectionView.get('selection')[0].item;
-			
-			// perform selectPreviousItem with numberOfItems
-			collectionView.selectPreviousItem(false, n) ;
-
-			if (idx &gt;= n) {
-				// idx is &gt;= stepby numberOfItems, a selectPreviousItem will be equal to current idx minus stepby numberOfItems
-				equals(collectionView.selection[0].item, array[idx-n].item, 'selection(%@ of 5) should step back by n(%@) items'.fmt(c,n)) ;
-			} else {
-				// idx is &lt; stepby numberOfItems, a selectPreviousItem will be equal to current idx
-				equals(collectionView.selection[0].item, array[idx].item, 'selection(%@ of 5) should try to step back by n(%@) items'.fmt(c,n)) ;
-			}
-		} ;
-	}) ;
-
-}) ;
+// ==========================================================================
+// Project:   SproutCore - JavaScript Application Framework
+// Copyright: &#169;2006-2009 Sprout Systems, Inc. and contributors.
+//            portions copyright @2009 Apple, Inc.
+// License:   Licened under MIT license (see license.js)
+// ==========================================================================
+
+var view ;
+var content = &quot;1 2 3 4 5 6 7 8 9 10&quot;.w().map(function(x) {
+  return SC.Object.create({ title: x });
+});
+
+module(&quot;SC.CollectionView.selectPreviousItem&quot;, {
+  setup: function() {
+    view = SC.CollectionView.create({
+      content: content
+    });
+  }
+});
+
+// ..........................................................
+// BASIC OPERATIONS
+//
+
+test(&quot;selectPreviousItem(extend=undefined, numberOfItems=undefined)&quot;, function() {
+  var sel = SC.SelectionSet.create().add(content,4),
+      expected = SC.SelectionSet.create().add(content,3),
+      actual;
+      
+  view.set('selection', sel);
+  view.selectPreviousItem();
+  
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'should select previous to %@ (expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+});
+
+test(&quot;selectPreviousItem(extend=NO, numberOfItems=undefined)&quot;, function() {
+  var sel = SC.SelectionSet.create().add(content,4),
+      expected = SC.SelectionSet.create().add(content,3),
+      actual;
+      
+  view.set('selection', sel);
+  view.selectPreviousItem(NO);
+  
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'should select previous to %@ (expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+});
+
+test(&quot;selectPreviousItem(extend=YES, numberOfItems=undefined)&quot;, function() {
+  var sel = SC.SelectionSet.create().add(content,4),
+      expected = SC.SelectionSet.create().add(content,3,2),
+      actual;
+      
+  view.set('selection', sel);
+  view.selectPreviousItem(YES);
+  
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'should extend to previous of %@ (expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+});
+
+test(&quot;selectPreviousItem(extend=YES, numberOfItems=2)&quot;, function() {
+  var sel = SC.SelectionSet.create().add(content,4),
+      expected = SC.SelectionSet.create().add(content,2,3),
+      actual;
+      
+  view.set('selection', sel);
+  view.selectPreviousItem(YES,2);
+  
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'should extend to previous of %@ (expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+});
+
+// ..........................................................
+// ANCHOR CASES
+// 
+
+test(&quot;anchor test&quot;, function() {
+  var sel = SC.SelectionSet.create().add(content,2,3),
+      expected, actual;
+      
+  view.set('selection', sel);
+
+  // TRY 1: Set anchor
+  view.selectPreviousItem(YES); // first one sets the anchor
+  expected = SC.SelectionSet.create().add(content,2,2); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 1: should reduce end of selection (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  sel = actual;
+  
+  // TRY 2: further reduce selection
+  view.selectPreviousItem(YES); 
+  expected = SC.SelectionSet.create().add(content,2,1); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 2: should reduce end of selection again (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  sel = actual;
+  
+  // TRY 3: selection as only anchor, start to increase top
+  view.selectPreviousItem(YES); 
+  expected = SC.SelectionSet.create().add(content,1,2); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 3: should extend selection at top (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  sel = actual;
+
+  // TRY 4: further expand selection from top
+  view.selectPreviousItem(YES); 
+  expected = SC.SelectionSet.create().add(content,0,3); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 4: should extend selection at top again (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  sel = actual;
+  
+  // TRY 5: at top; can no longer expand selection
+  view.selectPreviousItem(YES); 
+  expected = SC.SelectionSet.create().add(content,0,3); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 5: selection it at top, cannot extend further (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  sel = actual;
+
+  // TRY 6: try again just to be sure
+  view.selectPreviousItem(YES); 
+  expected = SC.SelectionSet.create().add(content,0,3); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 6: multiple calls when already at top do nothing (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  sel = actual;
+  
+});
+
+test(&quot;anchor test 2&quot;, function() {
+  var sel = SC.SelectionSet.create().add(content,4,5),
+      expected, actual;
+      
+  view.set('selection', sel);
+
+  // TRY 1: Set anchor
+  view.selectPreviousItem(YES); // first one sets the anchor
+  expected = SC.SelectionSet.create().add(content,4,4); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 1: should reduce end of selection (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  sel = actual;
+  
+  // TRY 2: further reduce selection
+  view.selectPreviousItem(YES); 
+  expected = SC.SelectionSet.create().add(content,4,3); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 2: should reduce end of selection again (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  sel = actual;
+  
+  // TRY 3: don't extend.  jumps to previous item and resets selection
+  view.selectPreviousItem(NO); 
+  expected = SC.SelectionSet.create().add(content,3,1); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 3: not extending clears selection and anchor (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  sel = actual;
+
+  // TRY 4: now should expand from top with new selection
+  view.selectPreviousItem(YES); 
+  expected = SC.SelectionSet.create().add(content,2,2); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 4: should expand from top of new selection (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  sel = actual;
+  
+  // TRY 5: at top; can no longer expand selection
+  view.selectPreviousItem(YES); 
+  expected = SC.SelectionSet.create().add(content,1,3); 
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'TRY 5: should further expand top (sel: %@ expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+  
+});
+
+
+// ..........................................................
+// EDGE CASES
+// 
+
+test(&quot;selectPreviousItem() when selection is 0..0&quot;, function() {
+  var sel = SC.SelectionSet.create().add(content,0),
+      expected = SC.SelectionSet.create().add(content,0),
+      actual;
+      
+  view.set('selection', sel);
+  view.selectPreviousItem();
+  
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'should should not change from previous of %@ (expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+});
+
+test(&quot;selectPreviousItem(YES) when selection is 0..1&quot;, function() {
+  var sel = SC.SelectionSet.create().add(content,0,2),
+      expected = SC.SelectionSet.create().add(content,0,2),
+      actual;
+      
+  view.set('selection', sel);
+  view._selectionAnchor = 1 ; // fake anchor
+  view.selectPreviousItem(YES);
+  
+  actual = view.get('selection');
+  ok(expected.isEqual(actual), 'should should not change from previous of %@ (expected: %@ actual: %@)'.fmt(sel, expected, actual));  
+});</diff>
      <filename>frameworks/desktop/tests/views/collection/selectPreviousItem.js</filename>
    </modified>
    <modified>
      <diff>@@ -94,6 +94,20 @@ var pane = SC.ControlTestPane.design({ height: 32 })
     content: SC.Object.create({ title: &quot;List Item&quot;, count: 10 }),
     contentValueKey: &quot;title&quot;,
     contentUnreadCountKey:  &quot;count&quot;
+  }))
+  
+  .add(&quot;outline - 1&quot;, SC.ListItemView.design({ 
+    content: SC.Object.create({ title: &quot;List Item&quot; }),
+    contentValueKey: &quot;title&quot;,
+    contentUnreadCountKey:  &quot;count&quot;,
+    outlineLevel: 1
+  }))
+  
+  .add(&quot;outline - 2&quot;, SC.ListItemView.design({ 
+    content: SC.Object.create({ title: &quot;List Item&quot; }),
+    contentValueKey: &quot;title&quot;,
+    contentUnreadCountKey:  &quot;count&quot;,
+    outlineLevel: 2
   })) ;
 
 pane.show();
@@ -110,7 +124,6 @@ window.pane = pane ;
 function basic(view, sel, disabled) {
   var cq = view.$();
   ok(cq.hasClass('sc-list-item-view'), 'should have sc-list-item-view class');
-  ok(cq.hasClass('sc-collection-item'), 'should have sc-collection-item class');
   
   equals(cq.hasClass('sel'), !!sel, 'expect sel class');
   equals(cq.hasClass('disabled'), !!disabled, 'expect disabled class');
@@ -218,6 +231,22 @@ test('count', function() {
   count(pane.view('count - 10'), 10);
 });
 
+test(&quot;outline - 1&quot;, function() {
+  var v = pane.view('outline - 1'),
+      indent = v.get('outlineIndent');
+  ok(indent&gt;0, 'precond - outlineIndent property should be &gt; 0 (actual: %@)'.fmt(indent));
+  
+  equals(v.$('.sc-outline').css('left'), indent*1 + &quot;px&quot;, 'sc-outline div should be offset by outline ammount');
+});
+
+test(&quot;outline - 2&quot;, function() {
+  var v = pane.view('outline - 2'),
+      indent = v.get('outlineIndent');
+  ok(indent&gt;0, 'precond - outlineIndent property should be &gt; 0 (actual: %@)'.fmt(indent));
+  
+  equals(v.$('.sc-outline').css('left'), indent*2 + &quot;px&quot;, 'sc-outline div should be offset by outline ammount');
+});
+
 // ..........................................................
 // EDITING CONTENT
 // </diff>
      <filename>frameworks/desktop/tests/views/list_item.js</filename>
    </modified>
    <modified>
      <diff>@@ -8,13 +8,6 @@
 sc_require('mixins/collection_view_delegate') ;
 sc_require('views/list_item');
 
-SC.BENCHMARK_UPDATE_CHILDREN = YES ;
-SC.BENCHMARK_RENDER = YES ;
-SC.ENABLE_COLLECTION_PARTIAL_RENDER = YES ;
-SC.DEBUG_PARTIAL_RENDER = NO ;
-SC.SANITY_CHECK_PARTIAL_RENDER = YES ;
-SC.VALIDATE_COLLECTION_CONSISTANCY = NO ;
-
 /**
   Special drag operation passed to delegate if the collection view proposes
   to perform a reorder event.
@@ -29,8 +22,12 @@ SC.HORIZONTAL_ORIENTATION = 'horizontal';
 /** Selection points should be selected using vertical orientation. */
 SC.VERTICAL_ORIENTATION = 'vertical' ;
 
+SC.BENCHMARK_RELOAD = NO ;
+
 /**
   @class 
+
+  TODO: Document SC.CollectionView
   
   Renders a collection of views from a source array of model objects.
    
@@ -46,13 +43,15 @@ SC.VERTICAL_ORIENTATION = 'vertical' ;
   property if you want to monitor selection. (be sure to set the isEnabled
   property to allow selection.)
   
-  @extends SC.ClassicView
+  @extends SC.View
   @extends SC.CollectionViewDelegate
-  
+  @extends SC.CollectionContent
+  @since SproutCore 0.9
 */
-SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
-/** @scope SC.CollectionView.prototype */ 
-{
+SC.CollectionView = SC.View.extend(
+  SC.CollectionViewDelegate,
+  SC.CollectionContent,
+/** @scope SC.CollectionView.prototype */ {
   
   classNames: ['sc-collection-view'],
   
@@ -74,57 +73,62 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     Usually you will want to bind this property to a controller property 
     that actually contains the array of objects you to display.
     
-    @type SC.Array
+    @type {SC.Array}
   */
-  content: [],
+  content: null,
   
   /** @private */
   contentBindingDefault: SC.Binding.multiple(),
   
   /**
-    The array of currently selected objects.  
-    
-    This array should contain the currently selected content objects.  It is 
-    modified automatically by the collection view when the user changes the 
-    selection on the collection.
+    The current length of the content.
     
-    Any item views representing content objects in this array will have their 
-    isSelected property set to YES automatically.
+    @property
+    @type {Numer}
+  */
+  length: 0,
+  
+  /**
+    The set of indexes that are currently tracked by the collection view.
+    This property is used to determine the range of items the collection view
+    should monitor for changes.
     
-    The CollectionView can deal with selection arrays that contain content 
-    objects that do not belong to the content array itself.  Sometimes this 
-    will happen if you share the same selection across multiple collection 
-    views.
+    The default implementation of this property returns an index set covering
+    the entire range of the content.  It changes automatically whenever the
+    length changes.
     
-    Usually you will want to bind this property to a controller property that 
-    actually manages the selection for your display.
+    Note that the returned index set for this property will always be frozen.
+    To change the nowShowing index set, you must create a new index set and 
+    apply it.
     
-    @type Array
+    @property
+    @type {SC.IndexSet}
   */
-  selection: [],
-  
-  /** @private */
-  selectionBindingDefault: SC.Binding.multiple(),
+  nowShowing: function() {
+    var ret = this.computeNowShowing();
+    return ret ? ret.frozenCopy() : null;
+  }.property('length', 'clippingFrame').cacheable(),
   
   /**
-    Delegate used to implement fine-grained control over collection view 
-    behaviors.
+    Indexes of selected content objects.  This SC.SelectionSet is modified 
+    automatically by the collection view when the user changes the selection 
+    on the collection.
     
-    You can assign a delegate object to this property that will be consulted
-    for various decisions regarding drag and drop, selection behavior, and
-    even rendering.  The object you place here must implement some or all of
-    the SC.CollectionViewDelegate mixin.
+    Any item views representing content objects in this set will have their 
+    isSelected property set to YES automatically.
+    
+    @type {SC.SelectionSet}
   */
-  delegate: null,
+  selection: null,
   
   /** 
     Allow user to select content using the mouse and keyboard.
     
     Set this property to NO to disallow the user from selecting items. If you 
-    have items in your selection property, they will still be reflected
+    have items in your selectedIndexes property, they will still be reflected
     visually.
     
-    @type Boolean
+    @type {Boolean}
   */
   isSelectable: YES,
   
@@ -139,7 +143,7 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     the collection view will also be not selectable or editable, regardless of 
     the settings for isEditable &amp; isSelectable.
     
-    @type Boolean
+    @type {Boolean}
   */
   isEnabled: YES,
   
@@ -154,7 +158,7 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     the user will not be able to reorder, add, or delete items regardless of 
     the canReorderContent and canDeleteContent and isDropTarget properties.
     
-    @type Boolean
+    @type {Boolean}
   */
   isEditable: YES,
   
@@ -168,7 +172,7 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     If you also accept drops, this will allow the user to drop items into 
     specific points in the list.  Otherwise items will be added to the end.
     
-    @type Boolean
+    @type {Boolean}
   */
   canReorderContent: NO,
   
@@ -181,7 +185,7 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     If true the user will be allowed to delete selected items using the delete
     key.  Otherwise deletes will not be permitted.
     
-    @type Boolean
+    @type {Boolean}
   */
   canDeleteContent: NO,
   
@@ -195,7 +199,7 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     cause it to be registered as a drop target, activating the other drop 
     machinery.
     
-    @type Boolean
+    @type {Boolean}
   */
   isDropTarget: NO,
   
@@ -206,7 +210,7 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     click behavior.  Command modifiers will be ignored and instead clicking
     once will select an item and clicking on it again will deselect it.
     
-    @type Boolean
+    @type {Boolean}
   */
   useToggleSelection: NO,
   
@@ -274,6 +278,26 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     @property {String}
   */
   contentExampleViewKey: null,
+
+  /**
+    The view class to use when creating new group item views.
+    
+    The collection view will automatically create an instance of the view 
+    class you set here for each item in its content array.  You should provide 
+    your own subclass for this property to display the type of content you 
+    want.
+    
+    @property {SC.View}
+  */
+  groupExampleView: SC.ListItemView,
+  
+  /**
+    If set, this key will be used to get the example view for a given
+    content object.  The groupExampleView property will be ignored.
+    
+    @property {String}
+  */
+  contentGroupExampleViewKey: null,
   
   /**
     Invoked when the user double clicks on an item (or single clicks of 
@@ -311,19 +335,6 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
   */
   target: null,
   
-  /**
-    Set to YES whenever the content needs to update its children.  If you 
-    set this property, it will cause the view to update its children at the
-    end of the runloop or the next time it becomes visible.
-    
-    Generally you will not need to change this property.  Instead you should
-    call methods such as contentPropertyDidChange() or updateChildren()
-    directly instead.
-    
-    @property {Boolean}
-  */
-  isDirty: NO,
-  
   /** 
     Property on content items to use for display.
     
@@ -350,18 +361,26 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
   acceptsFirstResponder: NO,
   
   /**
-    If your layout uses a grid or horizontal-based layout, then make sure this 
-    property is always up to date with the current number of items per row.  
-    
-    The CollectionView will use this property to support keyboard navigation 
-    using the arrow keys.
+    Enables observing content property changes.  Set this property if you 
+    want to deal with property changes on content objects directly in the 
+    collection view instead of delegating change observing to individual item
+    views.
     
-    If your collection view is simply a vertical list of items then you do not 
-    need to change this property.
+    @type {Boolean}
+  */
+  observeContentProperties: NO,
+  
+  /** 
+    The insertion orientation.  This is used to determine which
+    dimension we should pay attention to when determining insertion point for
+    a mouse click.
     
-    @property {Number}
+    {{{
+      SC.HORIZONTAL_ORIENTATION: look at the X dimension only
+      SC.VERTICAL_ORIENTATION: look at the Y dimension only
+    }}}
   */
-  itemsPerRow: 1,
+  insertionOrientation: SC.HORIZONTAL_ORIENTATION,
   
   // ..........................................................
   // SUBCLASS METHODS
@@ -381,21 +400,6 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
   computeLayout: function() { return null; },
   
   /**
-    Override to return the range of items to render for a given frame.
-    
-    You can override this method to implement support for incremenetal 
-    rendering.  The range you return here will be used to limit the number of 
-    actual item views that are created by the collection view.
-    
-    If you do not want to support incremental rendering, just return null.
-    
-    @param {Rect} frame The frame you should use to determine the range.
-    @returns {Range} A hash that indicates the range of content objects to 
-      render.  ({ start: X, length: Y }) 
-  */  
-  contentRangeInFrame: function(frame) { return null; },
-  
-  /**
     Override to compute the layout of the itemView for the content at the 
     specified index.  This layout will be applied to the view just before it
     is rendered.
@@ -404,210 +408,592 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
       itemView
     @returns {Hash} a view layout
   */
-  itemViewLayoutAtContentIndex: function(contentIndex) {
-    throw &quot;itemViewLayoutAtContentIndex must be implemented&quot;;
+  layoutForContentIndex: function(contentIndex) {
+    return null ;
+  },
+  
+  /**
+    Override to return an IndexSet with the indexes that are at least 
+    partially visible in the passed rectangle.  This method is used by the 
+    default implementation of computeNowShowing() to determine the new 
+    nowShowing range after a scroll.
+    
+    Override this method to implement incremental rendering.
+    
+    The default simply returns the current content length.
+    
+    @param {Rect} rect the visible rect
+    @returns {SC.IndexSet} now showing indexes
+  */
+  contentIndexesInRect: function(rect) {
+    return SC.IndexSet.create(0, this.get('length'));    
+  },
+  
+  /**
+    Compute the nowShowing index set.  The default implementation simply 
+    returns the full range.  Override to implement incremental rendering.
+    
+    You should not normally call this method yourself.  Instead get the 
+    nowShowing property.
+    
+    @returns {SC.IndexSet} new now showing range
+  */
+  computeNowShowing: function() {
+    var r = this.contentIndexesInRect(this.get('clippingFrame')),
+        content = SC.makeArray(this.get('content')),
+        len     = content.get('length');
+         
+    // default show all.
+    if (!r) r = SC.IndexSet.create(0, len);
+
+    // make sure the index set doesn't contain any indexes greater than the
+    // actual content.
+    if (r.get('max') &gt; len) r.remove(len, r.get('max')-len);
+    
+    return r ;
+  },
+  
+  /** 
+    Override to show the insertion point during a drag.
+    
+    Called during a drag to show the insertion point.  Passed value is the
+    item view that you should display the insertion point before.  If the
+    passed value is null, then you should show the insertion point AFTER that
+    last item view returned by the itemViews property.
+    
+    Once this method is called, you are guaranteed to also recieve a call to
+    hideInsertionPoint() at some point in the future.
+    
+    The default implementation of this method does nothing.
+    
+    @param itemView {SC.ClassicView} view the insertion point should appear directly before. If null, show insertion point at end.
+    @param dropOperation {Number} the drop operation.  will be SC.DROP_BEFORE or SC.DROP_ON
+    
+    @returns {void}
+  */
+  showInsertionPoint: function(itemView, dropOperation) {
+    return (dropOperation === SC.DROP_BEFORE) ? this.showInsertionPointBefore(itemView) : this.hideInsertionPoint() ;
   },
   
+  /**
+    Override to hide the insertion point when a drag ends.
+    
+    Called during a drag to hide the insertion point.  This will be called 
+    when the user exits the view, cancels the drag or completes the drag.  It 
+    will not be called when the insertion point changes during a drag.
+    
+    You should expect to receive one or more calls to 
+    showInsertionPointBefore() during a drag followed by at least one call to 
+    this method at the end.  Your method should not raise an error if it is 
+    called more than once.
+    
+    @returns {void}
+  */
+  hideInsertionPoint: function() {},
+  
   // ..........................................................
-  // CONTENT CHANGES
+  // DELEGATE SUPPORT
   // 
   
-  /** @private
-    Whenever content array changes, start observing the [] property.  Also 
-    call the contentPropertyDidChange handler.
+  
+  /**
+    Delegate used to implement fine-grained control over collection view 
+    behaviors.
+    
+    You can assign a delegate object to this property that will be consulted
+    for various decisions regarding drag and drop, selection behavior, and
+    even rendering.  The object you place here must implement some or all of
+    the SC.CollectionViewDelegate mixin.
+    
+    If you do not supply a delegate but the content object you set implements 
+    the SC.CollectionViewDelegate mixin, then the content will be 
+    automatically set as the delegate.  Usually you will work with a 
+    CollectionView in this way rather than setting a delegate explicitly.
+    
+    @type {SC.CollectionViewDelegate}
   */
-  _collection_contentDidChange: function() {
-    var content = this.get('content') ;
-    if (content === this._content) return this; // nothing to do
+  delegate: null,
+  
+  /**
+    The delegate responsible for handling selection changes.  This property
+    will be either the delegate, content, or the collection view itself, 
+    whichever implements the SC.CollectionViewDelegate mixin.
     
-    var func = this._collection_contentPropertyDidChange ;
+    @property
+    @type {Object}
+  */
+  selectionDelegate: function() {
+    var del = this.get('delegate'), content = this.get('content');
+    return this.delegateFor('isCollectionViewDelegate', del, content);  
+  }.property('delegate', 'content').cacheable(),
+  
+  /**
+    The delegate responsible for providing additional display information 
+    about the content.  If you bind a collection view to a controller, this
+    the content will usually also be the content delegate, though you 
+    could implement your own delegate if you prefer.
     
-    // remove old observer, add new observer
-    if (this._content) this._content.removeObserver('[]', this, func) ;
-    if (content) content.addObserver('[]', this, func) ;
+    @property
+    @type {Object}
+  */
+  contentDelegate: function() {
+    var del = this.get('delegate'), content = this.get('content');
+    return this.delegateFor('isCollectionContent', del, content);
+  }.property('delegate', 'content').cacheable(),
+  
+  // ..........................................................
+  // CONTENT CHANGES
+  // 
+  
+  /**
+    Called whenever the content array or an item in the content array or a
+    property on an item in the content array changes.  Reloads the appropriate
+    item view when the content array itself changes or calls 
+    contentPropertyDidChange() if a property changes.
+    
+    Normally you will not call this method directly though you may override
+    it if you need to change the way changes to observed ranges are handled.
+    
+    @param {SC.Array} content the content array generating the change
+    @param {Object} object the changed object
+    @param {String} key the changed property or '[]' or an array change
+    @param {SC.IndexSet} indexes affected indexes or null for all items
+    @returns {void}
+  */
+  contentRangeDidChange: function(content, object, key, indexes) {
+    if (!object &amp;&amp; (key === '[]')) {
+      this.reload(indexes); // note: if indexes == null, reloads all
+    } else {
+      this.contentPropertyDidChange(object, key, indexes);
+    }
+  },
+
+  /**
+    Called whenever a property on an item in the content array changes.  This
+    is only called if you have set observesContentProperties to YES.
     
-    // cache
-    this._content = content;
-    this._contentPropertyRevision = null ;
+    Override this property if you want to do some custom work whenever a 
+    property on a content object changes.
+
+    The default implementation does nothing.
     
-    // trigger property change handler...
-    var rev = (content) ? content.propertyRevision : -1 ;
-    this._collection_contentPropertyDidChange(this, '[]', content, rev) ; 
-  }.observes('content'),
+    @param {Object} target the object that changed
+    @param {String} key the property that changed value
+    @param {SC.IndexSet} indexes the indexes in the content array affected
+    @returns {void}
+  */
+  contentPropertyDidChange: function(target, key, indexes) {
+    // Default Does Nothing
+  },
   
-  /** @private
-    Called whenever the content array or any items in the content array 
-    changes. mark view as dirty.
-  */
-  _collection_contentPropertyDidChange: function(target, key, value, rev) {    
-    if (!this._updatingContent &amp;&amp; (!rev || (rev != this._contentPropertyRevision))) {
-      this._contentPropertyRevision = rev ;
-      this._updatingContent = true ;
-      this.contentPropertyDidChange(target, key);
-      this._updatingContent = false ;
+  /**
+    Called whenever the view needs to updates it's contentRangeObserver to 
+    reflect the current nowShowing index set.  You will not usually call this
+    method yourself but you may override it if you need to provide some 
+    custom range observer behavior.
+
+    Note that if you do implement this method, you are expected to maintain
+    the range observer object yourself.  If a range observer has not been
+    created yet, this method should create it.  If an observer already exists
+    this method should udpate it.
+    
+    When you create a new range observer, the oberver must eventually call
+    contentRangeDidChange() for the collection view to function properly.
+    
+    If you override this method you probably also need to override 
+    destroyRangeObserver() to cleanup any existing range observer.
+    
+    @returns {void}
+  */
+  updateContentRangeObserver: function() {
+    var nowShowing = this.get('nowShowing'),
+        observer   = this._cv_contentRangeObserver,
+        content    = this.get('content');
+    
+    if (!content) return ; // nothing to do
+    
+    if (observer) {
+      content.updateRangeObserver(observer, nowShowing);
+    } else {
+      var func = this.contentRangeDidChange,
+          deep = this.get('observeContentProperties');
+      
+      observer = content.addRangeObserver(nowShowing, this, func, null, deep);      
+      this._cv_contentRangeObserver = observer ;
     }
+    
   },
   
   /**
-    Invoked whenever a the content array changes.  The default implementation
-    will possibly recompute the view's layout size and the marks it as dirty
-    so that it can update its children.
-  */
-  contentPropertyDidChange: function(target, key) {
-    this.adjust(this.computeLayout()) ;
-    this.set('isDirty', YES) ;
-    this.invalidateNowShowingRange() ;
-    return this ;
+    Called whever the view needs to invalidate the current content range 
+    observer.  This is called whenever the content array changes.  You will 
+    not usually call this method yourself but you may override it if you 
+    provide your own range observer behavior.
+
+    Note that if you override this method you should probably also override
+    updateRangeObserver() to create or update a range oberver as needed.
+    
+    @returns {void}
+  */
+  removeContentRangeObserver: function() {
+    var content  = this.get('content'),
+        observer = this._cv_contentRangeObserver ;
+        
+    if (observer) {
+      if (content) content.removeRangeObserver(observer);
+      this._cv_contentRangeObserver = null ;
+    }
+  },
+    
+  /**
+    Called whenever the content length changes.  This will invalidate the 
+    length property of the view itself causing the nowShowing to recompute
+    which will in turn update the UI accordingly.
+    
+    @returns {void}
+  */
+  contentLengthDidChange: function() {
+    var content = this.get('content');
+    this.set('length', content ? content.get('length') : 0);
   },
   
   /** @private
-    Anytime isDirty changes to YES or our visibility in window changes,
-    schedule a full update.
-  */
-  _collection_isDirtyDidChange: function() {
-    // don't test isVisibleInWindow here for a 10% perf gain
-    if (this.get('isDirty')) {
-      // using invokeOnce here doubles rendering speed!
-      this.invokeOnce(this.displayDidChange) ;
+    Whenever content property changes to a new value:
+    
+     - remove any old observers 
+     - setup new observers (maybe wait until end of runloop to do this?)
+     - recalc height/reload content
+     - set content as delegate if delegate was old content
+     - reset selection
+     
+    Whenever content array mutates:
+    
+     - possibly stop observing property changes on objects, observe new objs
+     - reload effected item views
+     - update layout for receiver
+  */
+  _cv_contentDidChange: function() {
+    var content = this.get('content'),
+        lfunc   = this.contentLengthDidChange ;
+
+    if (content === this._content) return this; // nothing to do
+
+    // cleanup old content
+    this.removeContentRangeObserver();
+    if (this._content) {
+      this._content.removeObserver('length', this, lfunc);
+    }
+    
+    // cache
+    this._content = content;
+    
+    // add new observers - range observer will be added lazily
+    if (content) {
+      content.addObserver('length', this, lfunc);
     }
-  }.observes('isDirty', 'isVisibleInWindow'),
+    
+    // notify all items changed
+    this.contentLengthDidChange();
+    this.contentRangeDidChange(content, null, '[]', null);
+    
+  }.observes('content'),
   
   // ..........................................................
-  // SELECTION CHANGES
+  // ITEM VIEWS
   // 
   
   /** @private
-    Whenever selection array changes, start observing the [] property.  Also 
-    set childrenNeedFullUpdate to YES, which will trigger an update.
+  
+    The indexes that need to be reloaded.  Must be one of YES, NO, or an
+    SC.IndexSet.
+  
   */
-  _collection_selectionDidChange: function() {
-    var selection = this.get('selection') ;
-    if (selection === this._selection) return this; // nothing to do
+  _invalidIndexes: NO,
+  
+  /** 
+    Regenerates the item views for the content items at the specified indexes.
+    If you pass null instead of an index set, regenerates all item views.
     
-    var func = this._collection_selectionPropertyDidChange ;
+    This method is called automatically whenever the content array changes in
+    an observable way, but you can call its yourself also if you need to 
+    refresh the collection view for some reason.
     
-    // remove old observer, add new observer
-    if (this._selection) this._selection.removeObserver('[]', this, func) ;
-    if (selection) selection.addObserver('[]', this, func) ;
+    Note that if the length of the content is shorter than the child views
+    and you call this method, then the child views will be removed no matter
+    what the index.
     
-    // cache
-    this._selection = selection;
-    this._selectionPropertyRevision = null ;
+    @param {SC.IndexSet} indexes
+    @returns {SC.CollectionView} receiver
+  */
+  reload: function(indexes) {
+    var invalid = this._invalidIndexes ;
+    if (indexes &amp;&amp; invalid !== YES) {
+      if (invalid) invalid.add(indexes);
+      else invalid = this._invalidIndexes = indexes.clone();
+
+    } else this._invalidIndexes = YES ; // force a total reload
     
-    // trigger property change handler...
-    var rev = (selection) ? selection.propertyRevision : -1 ;
-    this._collection_selectionPropertyDidChange(this, '[]', selection, rev) ; 
-  }.observes('selection'),
+    if (this.get('isVisibleInWindow')) this.invokeOnce(this.reloadIfNeeded);
+    
+    return this ;
+  },
+
+  /** 
+    Invoked once per runloop to actually reload any needed item views.
+    You can call this method at any time to actually force the reload to
+    happen immediately if any item views need to be reloaded.
+    
+    Note that this method will also invoke two other callback methods if you
+    define them on your subclass:
+    
+    - *willReload()* is called just before the items are reloaded
+    - *didReload()* is called jsut after items are reloaded
+    
+    You can use these two methods to setup and teardown caching, which may
+    reduce overall cost of a reload.  Each method will be passed an index set
+    of items that are reloaded or null if all items are reloaded.
+    
+    @returns {SC.CollectionView} receiver
+  */
+  reloadIfNeeded: function() {
+    var invalid = this._invalidIndexes;
+    if (!invalid || !this.get('isVisibleInWindow')) return this ; // delay
+    this._invalidIndexes = NO ;
+    
+    var content = this.get('content'),
+        len     = content ? content.get('length'): 0,
+        layout  = this.computeLayout(),
+        bench   = SC.BENCHMARK_RELOAD,
+        nowShowing = this.get('nowShowing'),
+        itemViews  = this._sc_itemViews,
+        containerView = this.get('containerView') || this,
+        views, idx, cvlen, view, childViews ;
+
+    // if the set is defined but it contains the entire nowShowing range, just
+    // replace
+    if (invalid.isIndexSet &amp;&amp; invalid.contains(nowShowing)) invalid = YES ;
+    if (this.willReload) this.willReload(invalid === YES ? null : invalid);
+
+    // if an index set, just update indexes
+    if (invalid.isIndexSet) {
+      childViews = containerView.get('childViews');
+      cvlen = childViews.get('length');
+      
+      if (bench) {
+        SC.Benchmark.start(bench=&quot;%@#reloadIfNeeded (Partial)&quot;.fmt(this),YES);
+      }
+      
+      invalid.forEach(function(idx) {
+        
+        // get the existing item view, if there is one
+        var existing = itemViews ? itemViews[idx] : null;
+        
+        // if nowShowing, then reload the item view.
+        if (nowShowing.contains(idx)) {
+          view = this.itemViewForContentIndex(idx, YES);
+          if (existing &amp;&amp; existing.parentView === containerView) {
+            containerView.replaceChild(view, existing);
+          } else {
+            containerView.appendChild(view);
+          }
+          
+        // if not nowShowing, then remove the item view if needed
+        } else if (existing &amp;&amp; existing.parentView === containerView) {
+          containerView.removeChild(existing);
+        }
+      },this);
+
+      if (bench) SC.Benchmark.end(bench);
+      
+    // if set is NOT defined, replace entire content with nowShowing
+    } else {
+
+      if (bench) {
+        SC.Benchmark.start(bench=&quot;%@#reloadIfNeeded (Full)&quot;.fmt(this),YES);
+      }
+
+      views = [];
+      nowShowing.forEach(function(idx) {
+        views.push(this.itemViewForContentIndex(idx, YES));
+      }, this);
+
+      // below is an optimized version of:
+      //this.replaceAllChildren(views);
+      containerView.beginPropertyChanges();
+      containerView.destroyLayer().removeAllChildren();
+      containerView.set('childViews', views); // quick swap
+      containerView.replaceLayer();
+      containerView.endPropertyChanges();
+      
+      if (bench) SC.Benchmark.end(bench);
+      
+    }
+    
+    // adjust my own layout if computed
+    if (layout) this.adjust(layout);
+    if (this.didReload) this.didReload(invalid === YES ? null : invalid);
+    
+    
+    return this ;
+  },
+  
+  displayProperties: 'isFirstResponder isEnabled isActive'.w(),
   
   /** @private
-    Called whenever the content array or any items in the selection array 
-    changes.  update children if this is a new property revision.
-  */
-  _collection_selectionPropertyDidChange: function(target, key, value, rev) {    
-    if (!this._updatingSelection &amp;&amp; (!rev || (rev != this._selectionPropertyRevision))) {
-      this._selectionPropertyRevision = rev ;
-      this._updatingSelection = true ;
-      this.selectionPropertyDidChange(target, key);
-      this._updatingSelection = false ;
-    }
+    If we're asked to render the receiver view for the first time but the 
+    child views still need to be added, go ahead and add them.
+  */
+  render: function(context, firstTime) {
+    if (firstTime &amp;&amp; this._needsReload) this.reloadIfNeeded ;
+    
+    // add classes for other state.
+    context.setClass('focus', this.get('isFirstResponder'));
+    context.setClass('disabled', !this.get('isEnabled'));
+    context.setClass('active', this.get('isActive'));
+
+    return sc_super();
   },
+    
+
+  _TMP_ATTRS: {},
+  _COLLECTION_CLASS_NAMES: ['sc-collection-item'],
   
   /**
-    Invoked whenever a the selection array changes.  The default 
-    implementation will possibly recompute the view's layout size and the 
-    marks it as dirty so that it can update its children.
-  */
-  selectionPropertyDidChange: function(target, key) {
-    this.adjust(this.computeLayout()) ;
-    this.set('isDirty', YES) ;
-    this.invalidateNowShowingRange() ;
-    return this ;
+    Returns the item view for the content object at the specified index. Call
+    this method instead of accessing child views directly whenever you need 
+    to get the view associated with a content index.
+
+    Although this method take two parameters, you should almost always call
+    it with just the content index.  The other two parameters are used 
+    internally by the CollectionView.
+    
+    If you need to change the way the collection view manages item views
+    you can override this method as well.  If you just want to change the
+    default options used when creating item views, override createItemView()
+    instead.
+  
+    Note that if you override this method, then be sure to implement this 
+    method so that it uses a cache to return the same item view for a given
+    index unless &quot;force&quot; is YES.  In that case, generate a new item view and
+    replace the old item view in your cache with the new item view.
+
+    @param {Number} idx the content index
+    @param {Boolean} rebuild internal use, do not use
+    @returns {SC.View} instantiated view
+  */
+  itemViewForContentIndex: function(idx, rebuild) {
+    // return from cache if possible
+    var content   = this.get('content'),
+        itemViews = this._sc_itemViews,
+        item = content.objectAt(idx),
+        del  = this.get('contentDelegate'),
+        groupIndexes = del.contentGroupIndexes(this, content),
+        isGroupView = NO,
+        key, ret, E, layout, layerId;
+
+    // use cache if available
+    if (!itemViews) itemViews = this._sc_itemViews = [] ;
+    if (!rebuild &amp;&amp; (ret = itemViews[idx])) return ret ; 
+
+    // otherwise generate...
+    
+    // first, determine the class to use
+    isGroupView = groupIndexes &amp;&amp; groupIndexes.contains(idx);
+    if (isGroupView) isGroupView = del.contentIndexIsGroup(this, content,idx);
+    if (isGroupView) {
+      key  = this.get('contentGroupExampleViewKey');
+      if (key &amp;&amp; item) E = item.get(key);
+      if (!E) E = this.get('groupExampleView');
+
+    } else {
+      key  = this.get('contentExampleViewKey');
+      if (key &amp;&amp; item) E = item.get(key);
+      if (!E) E = this.get('exampleView');
+    }
+
+    // collect some other state
+    var attrs = this._TMP_ATTRS;
+    attrs.contentIndex = idx;
+    attrs.content      = item ;
+    attrs.owner        = attrs.displayDelegate = this;
+    attrs.parentView   = this.get('containerView') || this ;
+    attrs.page         = this.page ;
+    attrs.layerId      = this.layerIdFor(idx, item);
+    attrs.isEnabled    = del.contentIndexIsEnabled(this, content, idx);
+    attrs.isSelected   = del.contentIndexIsSelected(this, content, idx);
+    attrs.outlineLevel = del.contentIndexOutlineLevel(this, content, idx);
+    attrs.disclosureState = del.contentIndexDisclosureState(this, content, idx);
+    attrs.isGroupView  = isGroupView;
+    attrs.isVisibleInWindow = this.isVisibleInWindow;
+    attrs.classNames = this._COLLECTION_CLASS_NAMES;
+    
+    layout = this.layoutForContentIndex(idx);
+    if (layout) {
+      attrs.layout = layout;
+    } else {
+      delete attrs.layout ;
+    }
+    
+    ret = this.createItemView(E, idx, attrs);
+    itemViews[idx] = ret ;
+    return ret ;
   },
   
-  // ..........................................................
-  // NOW SHOWING RANGE
-  // 
+  _TMP_LAYERID: [],
   
   /**
-    The currently visible range.  This is invalidated anytime the clipping
-    frame changes or anytime the view is resized.  This in turn may cause
-    the collection view to do a 'fast' revalidation of its content.
+    Primitive to instantiate an item view.  You will be passed the class 
+    and a content index.  You can override this method to perform any other
+    one time setup.
+
+    Note that item views may be created somewhat frequently so keep this fast.
+
+    *IMPORTANT:* The attrs hash passed is reused each time this method is 
+    called.   If you add properties to this hash be sure to delete them before
+    returning from this method.
+    
+    @param {Class} exampleClass example view class
+    @param {Number} idx the content index
+    @param {Hash} attrs expected attributes
+    @returns {SC.View} item view instance
+  */ 
+  createItemView: function(exampleClass, idx, attrs) {
+    return exampleClass.create(attrs);
+  },
+
+  /**
+    Generates a layerId for the passed index and item.  Usually the default
+    implementation is suitable.
     
-    @property {Range}
+    @param {Number} idx the content index
+    @returns {String} layer id, must be suitable for use in HTML id attribute
   */
-  nowShowingRange: function() {
-    // console.log(this.get('clippingFrame'));
-    var r = this.contentRangeInFrame(this.get('clippingFrame')),
-        content = SC.makeArray(this.get('content')),
-        len     = content.get('length');
-         
-    if (!r) r = { start: 0, length: len } ; // default - show all
-     
-    // make sure the range isn't greater than the content length 
-    r.length = Math.min(SC.maxRange(r), len) - r.start ;
-    return r ;
-  }.property('content', 'clippingFrame').cacheable(),
+  layerIdFor: function(idx) {  
+    var ret = this._TMP_LAYERID;
+    ret[0] = SC.guidFor(this);
+    ret[1] = idx;
+    return ret.join('-');
+  },
   
   /**
-    Call this method if the nowShowingRange should be recalculated for some
-    reason.  Usually the nowShowingRange will invalidate and recalculate on 
-    its own but you can force the property to need an update if you 
-    prefer.
+    Extracts the content index from the passed layerID.  If the layer id does
+    not belong to the receiver or if no value could be extracted, returns NO.
     
-    @returns {SC.CollectionView} receiver
+    @param {String} id the layer id
   */
-  invalidateNowShowingRange: function() {
-    this.notifyPropertyChange('nowShowingRange') ;
-    return this ;
-  },
-  
-  /** @private
-    Observer triggers whenever the nowShowingRange changes.  If the range has
-    actually changed and we are on screen, then schedule fast update. 
-    Otherwise, just mark as dirty.
-  */
-  nowShowingRangeDidChange: function() {
-    var range = this.get('nowShowingRange') ;
-    var old = this._collection_nowShowingRange ;
-    if (!old || !SC.rangesEqual(range, old)) {
-      this._collection_nowShowingRange = range ;
-      if (this.get('isVisibleInWindow')) this.displayDidChange() ;
-      else this.set('isDirty', YES);
-    }
-  }.observes('nowShowingRange'),
-  
-  // ..........................................................
-  // ITEM VIEWS
-  // 
-  
-  itemViewAtContentIndex: function(contentIndex) {
-    var range = this.get('nowShowingRange') ;
-    var itemView = this.createExampleView() ;
-    var key, content = SC.makeArray(this.get('content')) ;
-    var selection = SC.makeArray(this.get('selection')) ;
-    content = content.objectAt(contentIndex) ;
-    if (!content) return null ;
-    
-    var guids = this._itemViewGuids, guid;
-    if (!guids) this._itemViewGuids = guids = {};
-    
-    // use cache of item view guids to avoid creating temporary objects
-    guid = SC.guidFor(content);
-    if (!(key = guids[guid])) {
-      key = guids[guid] = SC.guidFor(this)+'_'+guid;
-    }
+  contentIndexForLayerId: function(id) {
+    if (!id || !(id = id.toString())) return null ; // nothing to do
     
-    itemView.set('content', content) ;
-    itemView.layerId = key ; // NOTE: cannot use .set here, layerId is RO
-    itemView.set('isVisible', SC.valueInRange(contentIndex, range)) ;
-    itemView.set('isSelected', (selection.indexOf(content) == -1) ? NO : YES) ;
+    var base = this._baseLayerId;
+    if (!base) base = this._baseLayerId = SC.guidFor(this)+&quot;-&quot;;
     
-    // NOTE: *must* set the layout silently...
-    itemView.layout = this.itemViewLayoutAtContentIndex(contentIndex) ;
-    itemView.set('parentView', this) ;
-    return itemView ;
+    // no match
+    if ((id.length &lt;= base.length) || (id.indexOf(base) !== 0)) return null ; 
+    var ret = Number(id.slice(id.lastIndexOf('-')+1));
+    return isNaN(ret) ? null : ret ;
   },
   
+
   /** 
     Find the first content item view for the passed event.
     
@@ -623,133 +1009,362 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
   */
   itemViewForEvent: function(evt) {
     var responder = this.getPath('pane.rootResponder') ;
-    
     if (!responder) return null ; // fast path
     
-    // need to materialize an itemView under the mouse if possible
-    var baseGuid = SC.guidFor(this) ;
-    var baseGuidLen = baseGuid.length ;
-    var element = evt.target ;
-    var elementId = element.id.slice(0, baseGuidLen) ;
-    while (elementId !== baseGuid) {
-      element = element.parentNode ;
-      if (!element) return null ; // didn't find it!
-      elementId = element.id.slice(0, baseGuidLen) ;
+    var base    = SC.guidFor(this) + '-',
+        baseLen = base.length,
+        element = evt.target,
+        layer   = this.get('layer'),
+        contentIndex = null,
+        id, itemView, ret ;
+        
+    // walk up the element hierarchy until we find this or an element with an
+    // id matching the base guid (i.e. a collection item)
+    while (element &amp;&amp; element !== document &amp;&amp; element !== layer) {
+      id = element ? element.getAttribute('id') : null ;
+      if (id &amp;&amp; (contentIndex = this.contentIndexForLayerId(id)) !== null) {
+          break;
+      }
+      element = element.parentNode ; 
     }
     
-    if (element.id.length === baseGuidLen) {
-      return null ; // we found ourself, so we're not over a child view
+    // no matching element found? 
+    if (contentIndex===null || (element === layer)) {
+      element = layer = null; // avoid memory leaks 
+      return null;    
     }
     
     // okay, found the DOM node for the view, go ahead and create it
-    // first, find the content...
-    var contentGuid = element.id.slice(baseGuidLen+1) ;
-    var nowShowingRange = this.get('nowShowingRange') ;
-    var content = SC.makeArray(this.get('content')) ;
-    var idx = SC.minRange(nowShowingRange) ;
-    var max = SC.maxRange(nowShowingRange) ;
-    var c = content.objectAt(idx) ;
-    while (SC.guidFor(c) !== contentGuid) {
-      idx++ ;
-      if (idx &gt; max) return null ; // couldn't find the content...
-      c = content.objectAt(idx) ;
+    // first, find the contentIndex
+    if (contentIndex &gt;= this.get('length')) {
+      throw &quot;layout for item view %@ was found when item view does not exist (%@)&quot;.fmt(id, this);
     }
     
-    // then create the view for that content
-    var itemView = this.createExampleView() ;
-    var selection = SC.makeArray(this.get('selection')) ;
-    itemView.set('content', c) ;
-    itemView.layerId = element.id ; // cannot use .set here, layerId is RO
-    SC.View.views[itemView.layerId] = itemView ; // register for event handling
-    itemView.set('isVisible', SC.valueInRange(idx, nowShowingRange)) ;
-    itemView.set('isSelected', (selection.indexOf(c) == -1) ? NO : YES) ;
-    
-    // NOTE: *must* set the layout silently...
-    itemView.layout = this.itemViewLayoutAtContentIndex(idx) ;
-    itemView.set('parentView', this) ;
-    
-    // prevent normal, non-materialized view behavior
-    // TODO: isMaterialized should do this automatically in SC.View
-    itemView.layerLocationNeedsUpdate = NO ;
-    itemView.childViewsNeedLayout = NO ;
-    itemView.layerNeedsUpdate = NO ;
-    
-    // NOTE: still have to search for view, because itemView could contain
-    // nested views, and the mouseDown should go to them first...
-    var view = responder.targetViewForEvent(evt) ;
-    if (!view) return null ; // workaround for error on IE8, see Ticket #169
-    
-    // work up the view hierarchy to find a match...
-    do {
-      // item clicked was the ContainerView itself... i.e. the user clicked 
-      // outside the child items nothing to return...
-      if (view == this) return null ;
-      
-      // sweet!... the view is not only in the collection, but it says we can 
-      // hit it. hit it and quit it... 
-      if (!view.hitTest || view.hitTest(evt)) return view ;
-    } while (view = view.get('parentView')) ;
-    
-    // nothing was found... 
-    return null ;
+    return this.itemViewForContentIndex(contentIndex, NO);
   },
   
-  createExampleView: function(content) {
-    var exampleViewKey = this.get('contentExampleViewKey') ;
-    var ExampleView = (exampleViewKey) ?
-      content.get(exampleViewKey) :
-      this.get('exampleView') ;
-    
-    if (ExampleView) {
-      return ExampleView.create({
-        classNames: ['sc-collection-item'],
-        owner: this,
-        displayDelegate: this,
-        parentView: this,
-        isVisible: YES,
-        isMaterialized: YES
-      });
-    } else throw &quot;You must define an exampleView class to render collection items with&quot; ;
+  // ..........................................................
+  // DISCLOSURE SUPPORT
+  // 
+  
+  /**
+    Expands any items in the passed selection array that have a disclosure
+    state.
+    
+    @param {SC.IndexSet} indexes the indexes to expand
+    @returns {SC.CollectionView} receiver
+  */
+  expand: function(indexes) {
+    if (!indexes) return this; // nothing to do
+    var del     = this.get('contentDelegate'),
+        content = this.get('content');
+        
+    indexes.forEach(function(i) { 
+      var state = del.contentIndexDisclosureState(this, content, i);
+      if (state === SC.BRANCH_CLOSED) del.contentIndexExpand(this,content,i);
+    }, this);
+    return this;
+  },
+
+  /**
+    Collapses any items in the passed selection array that have a disclosure
+    state.
+    
+    @param {SC.IndexSet} indexes the indexes to expand
+    @returns {SC.CollectionView} receiver
+  */
+  collapse: function(indexes) {
+    if (!indexes) return this; // nothing to do
+    var del     = this.get('contentDelegate'),
+        content = this.get('content');
+        
+    indexes.forEach(function(i) { 
+      var state = del.contentIndexDisclosureState(this, content, i);
+      if (state === SC.BRANCH_OPEN) del.contentIndexCollapse(this,content,i);
+    }, this);
+    return this;
   },
   
-  // ......................................
-  // SELECTION
-  //
+  // ..........................................................
+  // SELECTION SUPPORT
+  // 
   
+  /** @private 
+
+    Called whenever the selection object is changed to a new value.  Begins
+    observing the selection for changes.
+    
+  */
+  _cv_selectionDidChange: function() {  
+    var sel  = this.get('selection'),
+        last = this._cv_selection,
+        func = this._cv_selectionContentDidChange;
+        
+    if (sel === last) return this; // nothing to do
+    if (last) last.removeObserver('[]', this, func);
+    if (sel) sel.addObserver('[]', this, func);
+    
+    this._cv_selection = sel ;
+    this._cv_selectionContentDidChange();
+  }.observes('selection'),
+
   /** @private
-    Finds the smallest index of a content object in the selected array.
+  
+    Called whenever the selection object or its content changes.  This will
+    repaint any items that changed their selection state.
+  
   */
-  _indexOfSelectionTop: function() {
-    var content = this.get('content');
-    var sel = this.get('selection');
-    if (!content || !sel) return - 1;
-    
-    // find the first item in the selection
-    var contentLength = content.get('length') ;
-    var indexOfSelected = contentLength ; var idx = sel.length ;
-    while(--idx &gt;= 0) {
-      var curIndex = content.indexOf(sel[idx]) ;
-      if ((curIndex &gt;= 0) &amp;&amp; (curIndex &lt; indexOfSelected)) indexOfSelected = curIndex ;
+  _cv_selectionContentDidChange: function() {
+    var sel  = this.get('selection'),
+        last = this._cv_selindexes, // clone of last known indexes
+        content = this.get('content'),
+        diff ;
+
+    // save new last
+    this._cv_selindexes = sel ? sel.frozenCopy() : null;
+
+    // determine which indexes are now invalid
+    if (last) last = last.indexSetForSource(content, NO);
+    if (sel) sel = sel.indexSetForSource(content, NO);
+    
+    if (sel &amp;&amp; last) diff = sel.without(last).add(last.without(sel));
+    else diff = sel || last;
+
+    if (diff &amp;&amp; diff.get('length')&gt;0) this.reloadSelectionIndexes(diff);
+  },
+  
+  /** @private
+    Contains the current item views that need their selection to be repainted.
+    This may be either NO, YES, or an IndexSet.
+  */
+  _invalidSelection: NO,
+  
+  /**
+    Called whenever the selection changes.  The passed index set will contain
+    any affected indexes including those indexes that were previously 
+    selected and now should be deselected.
+    
+    Pass null to reload the selection state for all items.
+    
+    @param {SC.IndexSet} indexes affected indexes
+    @returns {SC.CollectionView} reciever
+  */
+  reloadSelectionIndexes: function(indexes) {
+    var invalid = this._invalidSelection ;
+    if (indexes &amp;&amp; (invalid !== YES)) {
+      if (invalid) invalid.add(indexes)
+      else invalid = this._invalidSelection = indexes.copy();
+
+    } else this._invalidSelection = YES ; // force a total reload
+    
+    if (this.get('isVisibleInWindow')) {
+      this.invokeOnce(this.reloadSelectionIndexesIfNeeded);
+    } 
+    
+    return this ;
+  },
+
+  /**
+    Reloads the selection state if needed on any dirty indexes.  Normally this
+    will run once at the end of the runloop, but you can force the item views
+    to reload their selection immediately by calling this method.
+    
+    You can also override this method if needed to change the way the 
+    selection is reloaded on item views.  The default behavior will simply
+    find any item views in the nowShowing range that are affected and 
+    modify them.
+    
+    @returns {SC.CollectionView} receiver
+  */
+  reloadSelectionIndexesIfNeeded: function() {
+    var invalid = this._invalidSelection;
+    if (!invalid || !this.get('isVisibleInWindow')) return this ; 
+
+    var nowShowing = this.get('nowShowing'),
+        reload     = this._invalidIndexes,
+        content    = this.get('content'),
+        sel        = this.get('selection');
+    
+    this._invalidSelection = NO; // reset invalid
+    
+    // fast path.  if we are going to reload everything anyway, just forget
+    // about it.  Also if we don't have a nowShowing, nothing to do.
+    if (reload === YES || !nowShowing) return this ;
+    
+    // if invalid is YES instead of index set, just reload everything 
+    if (invalid === YES) invalid = nowShowing;
+
+    // if we will reload some items anyway, don't bother
+    if (reload &amp;&amp; reload.isIndexSet) invalid = invalid.without(reload);
+
+    // iterate through each item and set the isSelected state.
+    invalid.forEach(function(idx) {
+      if (!nowShowing.contains(idx)) return; // not showing
+      var view = this.itemViewForContentIndex(idx);
+      if (view) view.set('isSelected', sel ? sel.contains(content, idx) : NO);
+    },this);
+    
+    return this ;
+  },
+  
+  /** 
+    Selection primitive.  Selects the passed IndexSet of items, optionally 
+    extending the current selection.  If extend is NO or not passed then this
+    will replace the selection with the passed value.  Otherwise the indexes
+    will be added to the current selection.
+    
+    @param {Number|SC.IndexSet} indexes index or indexes to select
+    @param extend {Boolean} optionally extend the selection
+    @returns {SC.CollectionView} receiver
+  */
+  select: function(indexes, extend) {
+
+    var content = this.get('content'),
+        del     = this.get('selectionDelegate'),
+        sel;
+
+    // normalize
+    if (SC.typeOf(indexes) === SC.T_NUMBER) {
+      indexes = SC.IndexSet.create(indexes, 1);
     }
+
+    // if we are passed an empty index set or null, clear the selection.
+    if (indexes &amp;&amp; indexes.get('length')&gt;0) {
+      // give the delegate a chance to alter the items
+      indexes = del.collectionViewShouldSelectIndexes(this, indexes, extend);
+      if (!indexes || indexes.get('length')===0) return this; // nothing to do
     
-    return (indexOfSelected &gt;= contentLength) ? -1 : indexOfSelected ;
+    } else indexes = null;
+
+    // build the selection object, merging if needed
+    if (extend &amp;&amp; (sel = this.get('selection'))) sel = sel.copy();
+    else sel = SC.SelectionSet.create();
+    if (indexes) sel.add(content, indexes);
+
+    // give delegate one last chance
+    sel = del.collectionViewSelectionForProposedSelection(this, sel);
+    if (!sel) sel = SC.SelectionSet.create(); // empty
+    
+    // if we're not extending the selection, clear the selection anchor
+    this._selectionAnchor = null ;
+    this.set('selection', sel.freeze()) ;  
+    return this;
+  },
+  
+  /** 
+    Primtive to remove the indexes from the selection.  
+    
+    @param {Number|SC.IndexSet} indexes index or indexes to select
+    @returns {SC.CollectionView} receiver
+  */
+  deselect: function(indexes) {
+
+    var sel     = this.get('selection'),
+        content = this.get('content'),
+        del     = this.get('selectionDelegate');
+        
+    if (!sel || sel.get('length')===0) return this; // nothing to do
+        
+    // normalize
+    if (SC.typeOf(indexes) === SC.T_NUMBER) {
+      indexes = SC.IndexSet.create(indexes, 1);
+    }
+
+    // give the delegate a chance to alter the items
+    indexes = del.collectionViewShouldDeselectIndexes(this, indexes) ;
+    if (!indexes || indexes.get('length')===0) return this; // nothing to do
+
+    // now merge change - note we expect sel &amp;&amp; indexes to not be null
+    sel = sel.copy().remove(content, indexes);
+    sel = del.collectionViewSelectionForProposedSelection(this, sel);
+    if (!sel) sel = SC.SelectionSet.create(); // empty
+
+    this.set('selection', sel.freeze()) ;
+    return this ;
   },
   
   /** @private
-    Finds the largest index of a content object in the selection array.
+   Finds the next selectable item, up to content length, by asking the
+   delegate. If a non-selectable item is found, the index is skipped. If
+   no item is found, selection index is returned unmodified.
+
+   Return value will always be in the range of the bottom of the current 
+   selection index and the proposed index.   
+   
+   @param {Number} proposedIndex the desired index to select
+   @param {Number} bottom optional bottom of selection use as fallback
+   @returns {Number} next selectable index. 
   */
-  _indexOfSelectionBottom: function() {
-    var content = this.get('content');
-    var sel = this.get('selection');
-    if (!content || !sel) return - 1;
+  _findNextSelectableItemFromIndex: function(proposedIndex, bottom) {
     
-    var indexOfSelected = -1 ; var idx = sel.length ;
-    while(--idx &gt;= 0) {
-      var curIndex = content.indexOf(sel[idx]) ;
-      if (curIndex &gt; indexOfSelected) indexOfSelected = curIndex ;
+    var lim     = this.get('length'),
+        range   = SC.IndexSet.create(), 
+        content = this.get('content'),
+        del     = this.get('selectionDelegate'),
+        ret, sel ;
+
+    // fast path
+    if (del.collectionViewShouldSelectIndexes === this.collectionViewShouldSelectIndexes) {
+      return proposedIndex;
+    }
+
+    // loop forwards looking for an index that is allowed by delegate
+    // we could alternatively just pass the whole range but this might be 
+    // slow for the delegate
+    while (proposedIndex &lt; lim) {
+      range.add(proposedIndex);
+      ret = del.collectionViewShouldSelectIndexes(this, range);
+      if (ret &amp;&amp; ret.get('length') &gt;= 1) return proposedIndex ;
+
+      range.remove(proposedIndex);
+      proposedIndex++;      
     }
+
+    // if nothing was found, return top of selection
+    if (bottom === undefined) {
+      sel = this.get('selection');
+      bottom = sel ? sel.get('max') : -1 ;
+    }
+    return bottom ;
+  },
+  
+  /** @private
+   Finds the previous selectable item, up to the first item, by asking the
+   delegate. If a non-selectable item is found, the index is skipped. If
+   no item is found, selection index is returned unmodified.
+   
+   @param {Integer} proposedIndex the desired index to select
+   @returns {Integer} the previous selectable index. This will always be in the range of the top of the current selection index and the proposed index.
+   @private
+  */
+  _findPreviousSelectableItemFromIndex: function(proposedIndex, top) {
+    var range   = SC.IndexSet.create(), 
+        content = this.get('content'),
+        del     = this.get('selectionDelegate'),
+        ret ;
     
-    return (indexOfSelected &lt; 0) ? -1 : indexOfSelected ;
+    // fast path
+    if (del.collectionViewShouldSelectIndexes === this.collectionViewShouldSelectIndexes) {
+      return proposedIndex;
+    }
+
+    // loop backwards looking for an index that is allowed by delegate
+    // we could alternatively just pass the whole range but this might be 
+    // slow for the delegate
+    while (proposedIndex &gt;= 0) {
+      range.add(proposedIndex);
+      ret = del.collectionViewShouldSelectIndexes(this, range);
+      if (ret &amp;&amp; ret.get('length') &gt;= 1) return proposedIndex ;
+      range.remove(proposedIndex);
+      proposedIndex--;      
+    }
+
+    // if nothing was found, return top of selection
+    if (top === undefined) {
+      var sel = this.get('selection');
+      top = sel ? sel.get('min') : -1 ;
+    }
+    return top ;
   },
   
   /**
@@ -763,23 +1378,24 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
       instead of replaced.  Defaults to false.
     @param numberOfItems {Integer} (Optional) The number of previous to be 
       selected.  Defaults to 1
-    @returns {void}
+      @returns {SC.CollectionView} receiver
   */
   selectPreviousItem: function(extend, numberOfItems) {
     if (SC.none(numberOfItems)) numberOfItems = 1 ;
     if (SC.none(extend)) extend = false ;
     
-    var content  = this.get('content');
-    var contentLength = content.get('length') ;
+    var sel     = this.get('selection'),
+        content = this.get('content');
+    if (sel) sel = sel.indexSetForSource(content, NO);
     
+    var selTop    = sel ? sel.get('min') : -1,
+        selBottom     = sel ? sel.get('max')-1 : -1,
+        anchor        = this._selectionAnchor;
+    if (SC.none(anchor)) anchor = selTop;
+
     // if extending, then we need to do some fun stuff to build the array
-    var selTop, selBottom, anchor ;
     if (extend) {
-      selTop = this._indexOfSelectionTop() ;
-      selBottom = this._indexOfSelectionBottom() ;
-      anchor = SC.none(this._selectionAnchor) ? selTop : this._selectionAnchor ;
-      this._selectionAnchor = anchor ;
-      
+
       // If the selBottom is after the anchor, then reduce the selection
       if (selBottom &gt; anchor) {
         selBottom = selBottom - numberOfItems ;
@@ -795,7 +1411,7 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
       
     // if not extending, just select the item previous to the selTop
     } else {
-      selTop = this._findPreviousSelectableItemFromIndex(this._indexOfSelectionTop() - numberOfItems);
+      selTop = this._findPreviousSelectableItemFromIndex(selTop - numberOfItems);
       if (selTop &lt; 0) selTop = 0 ;
       selBottom = selTop ;
       anchor = null ;
@@ -803,19 +1419,14 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     
     var scrollToIndex = selTop ;
     
-    // now build array of new items to select
-    var items = [] ;
-    while(selTop &lt;= selBottom) {
-      items[items.length] = content.objectAt(selTop++) ;
-    }
+    // now build new selection
+    sel = SC.IndexSet.create(selTop, selBottom+1-selTop);
     
     // ensure that the item is visible and set the selection
-    if (items.length &gt; 0) {
-      this.scrollToItemViewAtContentIndex(scrollToIndex) ;
-      this.selectItems(items) ;
-    }
-    
+    this.scrollToContentIndex(scrollToIndex) ;
+    this.select(sel) ;
     this._selectionAnchor = anchor ;
+    return this ;
   },
   
   /**
@@ -828,204 +1439,142 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
       instead of replaced.  Defaults to false.
     @param numberOfItems {Integer} (Optional) The number of items to be 
       selected.  Defaults to 1.
-    @returns {void}
+    @returns {SC.CollectionView} receiver
   */
   selectNextItem: function(extend, numberOfItems) {
     if (SC.none(numberOfItems)) numberOfItems = 1 ;
     if (SC.none(extend)) extend = false ;
-    
-    var content  = this.get('content');
-    var contentLength = content.get('length') ;
-    
+
+    var sel     = this.get('selection'),
+        content = this.get('content');
+    if (sel) sel = sel.indexSetForSource(content, NO);
+    
+    var selTop    = sel ? sel.get('min') : -1,
+        selBottom = sel ? sel.get('max')-1 : -1,
+        anchor    = this._selectionAnchor,
+        lim       = this.get('length');
+        
+    if (SC.none(anchor)) anchor = selTop;
+
     // if extending, then we need to do some fun stuff to build the array
-    var selTop, selBottom, anchor ;
     if (extend) {
-      selTop = this._indexOfSelectionTop() ;
-      selBottom = this._indexOfSelectionBottom() ;
-      anchor = SC.none(this._selectionAnchor) ? selTop : this._selectionAnchor ;
-      this._selectionAnchor = anchor ;
       
       // If the selTop is before the anchor, then reduce the selection
       if (selTop &lt; anchor) {
         selTop = selTop + numberOfItems ;
         
-      // otherwise, select the next item after the top 
+      // otherwise, select the next item after the bottom 
       } else {
-        selBottom = this._findNextSelectableItemFromIndex(selBottom + numberOfItems);
+        selBottom = this._findNextSelectableItemFromIndex(selBottom + numberOfItems, selBottom);
       }
       
       // Ensure we are not out of bounds
-      if (selBottom &gt;= contentLength) selBottom = contentLength-1;
+      if (selBottom &gt;= lim) selBottom = lim-1;
       if (selTop &gt; selBottom) selTop = selBottom ;
       
     // if not extending, just select the item next to the selBottom
     } else {
-      selBottom = this._findNextSelectableItemFromIndex(this._indexOfSelectionBottom() + numberOfItems);
+      selBottom = this._findNextSelectableItemFromIndex(selBottom + numberOfItems, selBottom);
       
-      if (selBottom &gt;= contentLength) selBottom = contentLength-1;
+      if (selBottom &gt;= lim) selBottom = lim-1;
       selTop = selBottom ;
       anchor = null ;
     }
     
     var scrollToIndex = selBottom ;
     
-    // now build array of new items to select
-    var items = [] ;
-    while(selTop &lt;= selBottom) {
-      items[items.length] = content.objectAt(selTop++) ;
-    }
+    // now build new selection
+    sel = SC.IndexSet.create(selTop, selBottom-selTop+1);
     
     // ensure that the item is visible and set the selection
-    if (items.length &gt; 0) {
-      this.scrollToItemViewAtContentIndex(scrollToIndex) ;
-      this.selectItems(items) ;
-    }
-    
+    this.scrollToContentIndex(scrollToIndex) ;
+    this.select(sel) ;
     this._selectionAnchor = anchor ;
+    return this ;
   },
-  
-  /**
-    Scroll the rootElement (if needed) to ensure that the item is visible.
-    @param {Integer} contentIndex The index of the item to scroll to
-    @returns {void}
-  */
-  scrollToItemViewAtContentIndex: function(contentIndex) {
-    var itemView = this.itemViewAtContentIndex(contentIndex) ;
-    if (itemView) this.scrollToItemView(itemView) ;
-  },
-  
+    
   /**
-    Scroll the rootElement (if needed) to ensure that the item is visible.
-    @param {SC.ClassicView} view The item view to scroll to
-    @returns {void}
-  */
-  scrollToItemView: function(view) {
-    // find first scrollable view.
-    var scrollable = this ;
-    while(scrollable &amp;&amp; (scrollable != SC.window) &amp;&amp; (!scrollable.get('isScrollable'))) {
-      scrollable = scrollable.get('parentNode') ;
-    }
-    if (!scrollable || (scrollable == SC.window)) return ; // no scrollable!
-    scrollable.scrollToVisible(view) ;
-  },
-  
-  /** 
-    Selects the passed array of items, optionally extending the
-    current selection.
+    Deletes the selected content if canDeleteContent is YES.  This will invoke 
+    delegate methods to provide fine-grained control.  Returns YES if the 
+    deletion was possible, even if none actually occurred.
     
-    @param items {Array} The item or items to select.
-    @param extendSelection {Boolean} If true, extends the selection instead of 
-      replacing it.
+    @returns {Boolean} YES if deletion is possible.
   */
-  selectItems: function(items, extendSelection) {
-    var base = (extendSelection) ? this.get('selection') : [] ;
-    var sel = [] ;
-    
-    items = SC.makeArray(items) ;
-    for (var i = 0, len = items.length; i &lt; len; i++) {
-      if (this.invokeDelegateMethod(this.delegate, 'collectionViewShouldSelectItem', this, items[i])) {
-        sel.push(items[i]);
-      }
-    }
+  deleteSelection: function() {
     
-    var set = SC.Set.create(base), obj ;
-    set.addEach(sel) ; // sel contains selectable items
-    sel.length = 0 ; // we don't want duplicates in the selection, so clear it
-    while (obj = set.pop()) sel.push(obj) ; // now fill it back up..
+    // perform some basic checks...
+    if (!this.get('canDeleteContent')) return NO;  
+
+    var sel     = this.get('selection'),
+        content = this.get('content'),
+        del     = this.get('selectionDelegate'),
+        indexes = sel&amp;&amp;content ? sel.indexSetForSource(content, NO) : null;
+        
+    if (!content || !indexes || indexes.get('length') === 0) return NO ;
     
-    // if we're not extending the selection, clear the selection anchor
-    this._selectionAnchor = null ;
-    this.set('selection', sel) ;  
-  },
-  
-  /** 
-    Removes the items from the selection.
-  */
-  deselectItems: function(items) {
-    var base = SC.makeArray(this.get('selection')) ;
-    var sel = [] ;
+    // let the delegate decide what to actually delete.  If this returns an
+    // empty index set or null, just do nothing.
+    indexes = del.collectionViewShouldDeleteIndexes(this, indexes);
+    if (!indexes || indexes.get('length') === 0) return NO ;
     
-    items = SC.makeArray(items) ;
+    // now have the delegate (or us) perform the deletion. The default 
+    // delegate implementation just uses standard SC.Array methods to do the
+    // right thing.
+    del.collectionViewDeleteContent(this, this.get('content'), indexes);
     
-    var set = SC.Set.create(base), obj ;
-    set.removeEach(items) ;
-    while (obj = set.pop()) sel.push(obj) ;
+    // also, fix up the selection by removing the actual items we removed
+    // set selection directly instead of calling select() since we are just
+    // fixing up the selection.
+    sel = this.get('selection').copy().remove(content, indexes);
+    this.set('selection', sel.freeze());
     
-    this.set('selection', sel) ;
+    return YES ;
   },
   
+  // ..........................................................
+  // SCROLLING
+  // 
+  
   /**
-    Deletes the selected content if canDeleteContent is YES.  
-    
-    This will invoke delegate methods to provide fine-grained control.
+    Scroll the rootElement (if needed) to ensure that the item is visible.
     
-    @returns {Boolean} YES if deletion is possible, even if none actually occurred.
+    @param {Number} contentIndex The index of the item to scroll to
+    @returns {SC.CollectionView} receiver
   */
-  deleteSelection: function() {
-    // console.log('deleteSelection called on %@'.fmt(this));
-    // perform some basic checks...
-    if (!this.get('canDeleteContent')) return NO;  
-    var sel = SC.makeArray(this.get('selection'));
-    if (!sel || sel.get('length') === 0) return NO ;
-    
-    // let the delegate decide what to actually delete.  If this returns an
-    // empty array or null, just do nothing.
-    sel = this.invokeDelegateMethod(this.delegate, 'collectionViewShouldDeleteContent', this, sel) ;
-    sel = SC.makeArray(sel) ; // ensure this is an array
-    if (!sel || sel.get('length') === 0) return YES ;
-    
-    // now have the delegate (or us) perform the deletion.  The collection
-    // view implements a default version of this method.
-    this.invokeDelegateMethod(this.delegate, 'collectionViewDeleteContent', this, sel) ;
-    return YES ;
+  scrollToContentIndex: function(contentIndex) {
+    var itemView = this.itemViewForContentIndex(contentIndex) ;
+    if (itemView) this.scrollToItemView(itemView) ;
+    return this; 
   },
-
+  
   /**
-    Default implementation of the delegate method.
-    
-    This method will delete the passed items from the content array using 
-    standard array methods.  This is often suitable if you are using an
-    array controller or a real array for your content.
-    
-    @param view {SC.CollectionView} this
-    @param sel {Array} the items to delete
-    @returns {Boolean} YES if the deletion was a success.
-  */
-  collectionViewDeleteContent: function(view, sel) {
-    
-    // get the content.  Bail if this cannot be used as an array.
-    var content = this.get('content') ; 
-    if (!content) return NO;  // nothing to do
-    
-    // determine the method to use
-    var hasDestroyObject = SC.typeOf(content.destroyObject) === SC.T_FUNCTION ;
-    var hasRemoveObject = SC.typeOf(content.removeObject) === SC.T_FUNCTION ;
-    if (!hasDestroyObject &amp;&amp; !hasRemoveObject) return NO; // nothing to do
-    
-    // suspend property notifications and remove the objects...
-    if (content.beginPropertyChanges) content.beginPropertyChanges();
-    var idx = sel.get('length') ;
-    while(--idx &gt;= 0) {
-      var item = sel.objectAt(idx) ;
-      if (hasDestroyObject) {
-        content.destroyObject(item);
-      } else {
-        content.removeObject(item);
-      }
-    }
-    // begin notifying again...
-    if (content.endPropertyChanges) content.endPropertyChanges() ;
+    Scroll the rootElement (if needed) to ensure that the item is visible.
+
+    @param {SC.View} view The item view to scroll to
+    @returns {SC.CollectionView} receiver
+  */
+  scrollToItemView: function(view) {
     
-    return YES ; // done!
+    // TODO: Implement scrollToItemView
+    console.warn(&quot;SC.CollectionView#scrollToItemView() is not yet implemented in 1.0&quot;);
+    return this ; 
+    
+    // find first scrollable view.
+    // var scrollable = this ;
+    // while(scrollable &amp;&amp; (scrollable != SC.window) &amp;&amp; (!scrollable.get('isScrollable'))) {
+    //   scrollable = scrollable.get('parentNode') ;
+    // }
+    // if (!scrollable || (scrollable == SC.window)) return ; // no scrollable!
+    // scrollable.scrollToVisible(view) ;
   },
-  
-  // ......................................
-  // EVENT HANDLING
-  //
+
+  // ..........................................................
+  // KEYBOARD EVENTS
+  // 
   
   /** @private */
   keyDown: function(evt) {
-    // console.log('keyDown called on %@'.fmt(this));
+    console.log('keyDown called on %@'.fmt(this));
     return this.interpretKeyEvents(evt) ;
   },
   
@@ -1036,8 +1585,9 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     Handle select all keyboard event.
   */
   selectAll: function(evt) {
-    var content = (this.get('content') || []).slice() ;
-    this.selectItems(content, NO) ;
+    var content = this.get('content'),
+        sel = content ? SC.IndexSet.create(0, content.get('length')) : null;
+    this.select(sel, NO) ;
     return YES ;
   },
   
@@ -1045,7 +1595,6 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     Handle delete keyboard event.
   */
   deleteBackward: function(evt) {
-    // console.log('deleteBackward called on %@ with evt %@'.fmt(this, evt));
     return this.deleteSelection() ;
   },
   
@@ -1053,7 +1602,6 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     Handle delete keyboard event.
   */
   deleteForward: function(evt) {
-    // console.log('deleteForward called on %@ with evt %@'.fmt(this, evt));
     return this.deleteSelection() ;
   },
   
@@ -1117,6 +1665,38 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     return true ;
   },
 
+  
+  
+  /** @private
+    if content value is editable and we have one item selected, then edit.
+    otherwise, invoke action.
+  */
+  insertNewline: function() {
+    var sel, itemView;
+    if (this.get('contentValueIsEditable')) {
+      sel = this.get('selection') ;
+      if (sel &amp;&amp; sel.get('length') === 1) {
+        itemView = this.itemViewForContent(sel.objectAt(0)) ;
+        if (itemView &amp;&amp; itemView.beginEditing) {
+          this.scrollToItemView(itemView) ;
+          itemView.beginEditing() ;
+        }
+      }
+      
+    // invoke action!
+    } else {
+      sel = this.get('selection') ;
+      itemView = (sel &amp;&amp; sel.get('length') === 1) ? this.itemViewForContent(sel.objectAt(0)) : null ;
+      this._cv_action(itemView, null) ;
+    }
+    
+    return YES ; // always handle
+  },
+
+  // ..........................................................
+  // MOUSE EVENTS
+  // 
+  
   /** @private
     Handles mouse down events on the collection view or on any of its 
     children.
@@ -1129,8 +1709,6 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     @returns {Boolean} Usually YES.
   */
   mouseDown: function(ev) {
-    // console.log('%@.mouseDown(%@)'.fmt(this, ev));
-    // console.log(ev.originalEvent);
     
     // When the user presses the mouse down, we don't do much just yet.
     // Instead, we just need to save a bunch of state about the mouse down
@@ -1140,21 +1718,21 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     this._mouseDownEvent = ev ;
     
     // Toggle selection only triggers on mouse up.  Do nothing.
-    if (this.useToggleSelection) return true;
+    if (this.get('useToggleSelection')) return true;
     
-    // Make sure that saved mouseDown state is always reset in case we do
-    // not get a paired mouseUp. (Only happens if subclass does not call us 
-    // like it should)
-    this._mouseDownAt = this._shouldSelect = this._shouldDeselect =
-      this._shouldReselect = this._refreshSelection = false;
-      
-    // debugger ;
     // find the actual view the mouse was pressed down on.  This will call
     // hitTest() on item views so they can implement non-square detection
     // modes. -- once we have an item view, get its content object as well.
-    var mouseDownView = (this._mouseDownView = this.itemViewForEvent(ev));
-    var mouseDownContent = 
-      (this._mouseDownContent = mouseDownView ? mouseDownView.get('content') : null);
+    var itemView      = this.itemViewForEvent(ev),
+        content       = this.get('content'),
+        contentIndex  = itemView ? itemView.get('contentIndex') : -1, 
+        info, anchor ;
+        
+    info = this.mouseDownInfo = {
+      itemView: itemView,
+      contentIndex: contentIndex,
+      at: Date.now()
+    };
       
     // become first responder if possible.
     this.becomeFirstResponder() ;
@@ -1163,290 +1741,202 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     
     // recieved a mouseDown on the collection element, but not on one of the 
     // childItems... unless we do not allow empty selections, set it to empty.
-    if (!mouseDownView) {
-      if (this.get('allowDeselectAll')) this.selectItems([], false);
-      return true ;
+    if (!itemView) {
+      if (this.get('allowDeselectAll')) this.select(null, false);
+      return YES ;
     }
     
     // collection some basic setup info
-    var selection  = this.get('selection') || [] ;
-    var isSelected = (selection.indexOf(mouseDownContent) !== -1) ;
-    var modifierKeyPressed = ev.ctrlKey || ev.metaKey ;
-    if (mouseDownView.checkboxView &amp;&amp; (SC.Event.element(ev) == ev.checkboxView.rootElement)) {
-      modifierKeyPressed = true ;
-    }
-    this._modifierKeyPressed = modifierKeyPressed ;  
+    var sel = this.get('selection'), isSelected, modifierKeyPressed;
+    if (sel) sel = sel.indexSetForSource(content, NO);
     
-    this._mouseDownAt = Date.now();
+    isSelected = sel ? sel.contains(contentIndex) : NO;
+    info.modifierKeyPressed = modifierKeyPressed = ev.ctrlKey || ev.metaKey ;
     
     // holding down a modifier key while clicking a selected item should 
     // deselect that item...deselect and bail.
     if (modifierKeyPressed &amp;&amp; isSelected) {
-      this._shouldDeselect = mouseDownContent;
+      info.shouldDeselect = contentIndex &gt;= 0;
+
     // if the shiftKey was pressed, then we want to extend the selection
     // from the last selected item
-    } else if (ev.shiftKey &amp;&amp; selection.get('length') &gt; 0) {
-      selection = this._findSelectionExtendedByShift(selection, mouseDownContent) ;
-      this.selectItems(selection) ;
+    } else if (ev.shiftKey &amp;&amp; sel &amp;&amp; sel.get('length') &gt; 0) {
+      sel = this._findSelectionExtendedByShift(sel, contentIndex);
+      anchor = this._selectionAnchor ; 
+      this.select(sel) ;
+      this._selectionAnchor = anchor; //save the anchor
       
     // If no modifier key was pressed, then clicking on the selected item 
     // should clear the selection and reselect only the clicked on item.
     } else if (!modifierKeyPressed &amp;&amp; isSelected) {
-      this._shouldReselect = mouseDownContent;
+      info.shouldReselect = contentIndex &gt;= 0;
       
     // Otherwise, if selecting on mouse down,  simply select the clicked on 
     // item, adding it to the current selection if a modifier key was pressed.
     } else {
-      if (this.get(&quot;selectOnMouseDown&quot;)){
-         this.selectItems(mouseDownContent, modifierKeyPressed);
-      } else this._shouldSelect = mouseDownContent ;
+      if (this.get(&quot;selectOnMouseDown&quot;)) {
+        this.select(contentIndex, modifierKeyPressed);
+      } else {
+        info.shouldSelect = contentIndex &gt;= 0 ;
+      }
     }
     
     // saved for extend by shift ops.
-    this._previousMouseDownContent = mouseDownContent;
+    info.previousContentIndex = contentIndex;
     
-    return true;
+    return YES;
   },
   
   /** @private */
   mouseUp: function(ev) {
     
-    var canAct = this.get('actOnSelect') ;
-    var view = this.itemViewForEvent(ev) ;
-    var content, selection;
+    var canAct = this.get('actOnSelect'),
+        view   = this.itemViewForEvent(ev),
+        info   = this.mouseDownInfo,
+        idx    = info.contentIndex,
+        contentIndex, sel, isSelected, canEdit, itemView;
     
-    if (this.useToggleSelection) {
+    if (this.get('useToggleSelection')) {
       if (!view) return ; // do nothing when clicked outside of elements
       
       // determine if item is selected. If so, then go on.
-      selection = this.get('selection') || [] ;
-      content = (view) ? view.get('content') : null ;
-      var isSelected = selection.include(content) ;
-      if (isSelected) {
-        this.deselectItems([content]) ;
-      } else this.selectItems([content],true) ;
+      sel = this.get('selection') ;
+      contentIndex = (view) ? view.get('contentIndex') : -1 ;
+      isSelected = sel &amp;&amp; sel.include(contentIndex) ;
+
+      if (isSelected) this.deselect(contentIndex) ;
+      else this.select(contentIndex, YES) ;
       
     } else {
-      content = (view) ? view.get('content') : null ;
+      contentIndex = (view) ? view.get('contentIndex') : -1 ;
       
       // this will be set if the user simply clicked on an unselected item and 
       // selectOnMouseDown was NO.
-      if (this._shouldSelect) this.selectItems(this._shouldSelect, this._modifierKeyPressed);
+      if (info.shouldSelect) this.select(idx, info.modifierKeyPressed);
       
       // This is true if the user clicked on a selected item with a modifier
       // key pressed.
-      if (this._shouldDeselect) this.deselectItems(this._shouldDeselect);
+      if (info.shouldDeselect) this.deselect(idx);
       
       // This is true if the user clicked on a selected item without a 
       // modifier-key pressed.  When this happens we try to begin editing 
       // on the content.  If that is not allowed, then simply clear the 
       // selection and reselect the clicked on item.
-      if (this._shouldReselect) {
+      if (info.shouldReselect) {
         
         // - contentValueIsEditable is true
-        var canEdit = this.get('contentValueIsEditable') ;
+        canEdit = this.get('contentValueIsEditable') ;
         
         // - the user clicked on an item that was already selected
         // - is the only item selected
         if (canEdit) {
-          var sel = this.get('selection') ;
-          canEdit = sel &amp;&amp; (sel.get('length') === 1) &amp;&amp; (sel.objectAt(0) === this._shouldReselect) ;
+          sel = this.get('selection') ;
+          canEdit = sel &amp;&amp; (sel.get('length') === 1) &amp;&amp; sel.contains(idx);
         }
         
         // - the item view responds to contentHitTest() and returns YES.
         // - the item view responds to beginEditing and returns YES.
         if (canEdit) {
-          var itemView = this.itemViewForContent(this._shouldReselect) ;
+          itemView = this.itemViewForContent(idx) ;
           canEdit = itemView &amp;&amp; (!itemView.contentHitTest || itemView.contentHitTest(ev)) ;
           canEdit = (canEdit &amp;&amp; itemView.beginEditing) ? itemView.beginEditing() : NO ;
         }
         
         // if cannot edit, just reselect
-        if (!canEdit) this.selectItems(this._shouldReselect,false) ;
+        if (!canEdit) this.select(idx, false) ;
       }
       
       this._cleanupMouseDown() ;
     }
     
     this._mouseDownEvent = null ;
-    if (canAct) this._action(ev, view) ;
+    if (canAct) this._cv_action(ev, view) ;
     
-    return false;  // bubble event to allow didDoubleClick to be called...
+    return NO;  // bubble event to allow didDoubleClick to be called...
   },
   
   /** @private */
   _cleanupMouseDown: function() {
-    this._mouseDownAt = this._shouldDeselect = this._shouldReselect = this._refreshSelection = this._shouldSelect = false;
-    this._mouseDownEvent = this._mouseDownContent = this._mouseDownView = null ;
+    this._mouseDownEvent = null;
+    this.mouseDownInfo = null;
   },
   
   /** @private */
   mouseMoved: function(ev) {
-    var view = this.itemViewForEvent(ev) ;
+    var view = this.itemViewForEvent(ev), 
+        last = this._lastHoveredItem ;
+
     // handle hover events.
-    if(this._lastHoveredItem &amp;&amp; ((view === null) || (view != this._lastHoveredItem)) &amp;&amp; this._lastHoveredItem.mouseOut) {
-      this._lastHoveredItem.mouseOut(ev); 
+    if (view !== last) {
+      if (last &amp;&amp; last.mouseOut) last.mouseOut(ev);
+      if (view &amp;&amp; view.mouseOver) view.mouseOver(ev);
     }
     this._lastHoveredItem = view ;
-    if (view &amp;&amp; view.mouseOver) view.mouseOver(ev) ;
+
+    if (view &amp;&amp; view.mouseMoved) view.mouseMoved(ev);
+    return YES;
   },
   
   /** @private */
   mouseOut: function(ev) {
-  
     var view = this._lastHoveredItem ;
     this._lastHoveredItem = null ;
-    if (view &amp;&amp; view.didMouseOut) view.didMouseOut(ev) ;
+    if (view &amp;&amp; view.mouseOut) view.mouseOut(ev) ;
+    return YES ;
   },
   
   /** @private */
   doubleClick: function(ev) {
     var view = this.itemViewForEvent(ev) ;
     if (view) {
-      this._action(view, ev) ;
+      this._cv_action(view, ev) ;
       return true ;
     } else return false ;
   },
   
   /** @private */
-  _findSelectionExtendedByShift: function(selection, mouseDownContent) {
-    var content = this.get('content');
-    
-    // bounds of the collection...
-    var contentLowerBounds = 0;
-    var contentUpperBounds = (content.get('length') - 1);
-    
-    var selectionBeginIndex = content.indexOf(selection[0]) ;
-    var selectionEndIndex = content.indexOf(selection[selection.length-1]) ;
+  _findSelectionExtendedByShift: function(sel, contentIndex) {
     
-    var previousMouseDownIndex = content.indexOf(this._previousMouseDownContent);
-    // _previousMouseDownContent couldn't be found... either it hasn't been set yet or the record has been deleted by the user
-    // fall back to the first selected item.
-    if (previousMouseDownIndex == -1) previousMouseDownIndex = selectionBeginIndex;
-    
-    var currentMouseDownIndex = content.indexOf(mouseDownContent);
-    // sanity check...
-    if (currentMouseDownIndex == -1) throw &quot;Unable to extend selection to an item that's not in the content array!&quot;;
+    // fast path.  if we don't have a selection, just select index
+    if (!sel || sel.get('length')===0) {
+      return SC.IndexSet.create(contentIndex);
+    }
     
+    // if we do have a selection, then figure out how to extend it.
+    var content = this.get('content'),
+        lim     = content.get('length')-1,
+        min     = sel.get('min'),
+        max     = sel.get('max')-1,
+        info    = this.mouseDownInfo,
+        anchor  = this._selectionAnchor ;
+    if (SC.none(anchor)) anchor = -1;
+
     // clicked before the current selection set... extend it's beginning...
-    if (currentMouseDownIndex &lt; selectionBeginIndex) {
-      selectionBeginIndex = currentMouseDownIndex;
-    }
+    if (contentIndex &lt; min) {
+      min = contentIndex;
+      if (anchor&lt;0) this._selectionAnchor = anchor = max; //anchor at end
     
     // clicked after the current selection set... extend it's ending...
-    if (currentMouseDownIndex &gt; selectionEndIndex) {
-      selectionEndIndex = currentMouseDownIndex;
-    }
+    } else if (contentIndex &gt; max) {
+      max = contentIndex;
+      if (anchor&lt;0) this._selectionAnchor = anchor = min; // anchor at start
     
     // clicked inside the selection set... need to determine where the last
     // selection was and use that as an anchor.
-    if ((currentMouseDownIndex &gt; selectionBeginIndex) &amp;&amp; (currentMouseDownIndex &lt; selectionEndIndex)) {
-      if (currentMouseDownIndex === previousMouseDownIndex) {
-        selectionBeginIndex = currentMouseDownIndex;
-        selectionEndIndex   = currentMouseDownIndex;
-      } else if (currentMouseDownIndex &gt; previousMouseDownIndex) {
-        selectionBeginIndex = previousMouseDownIndex;
-        selectionEndIndex   = currentMouseDownIndex;
-      } else if (currentMouseDownIndex &lt; previousMouseDownIndex){
-        selectionBeginIndex = currentMouseDownIndex;
-        selectionEndIndex   = previousMouseDownIndex;
-      }
-    }
-    
-    // slice doesn't include the last index passed... silly..
-    selectionEndIndex++;
-    
-    // shouldn't need to sanity check that the selection is in bounds due to 
-    // the indexOf checks above...I'll have faith that indexOf hasn't lied to 
-    // me...
-    return content.slice(selectionBeginIndex, selectionEndIndex);
-  },
-  
-  /** @private
-   Finds the next selectable item, up to content length, by asking the
-   delegate. If a non-selectable item is found, the index is skipped. If
-   no item is found, selection index is returned unmodified.
-   
-   @param {Integer} proposedIndex the desired index to select
-   @returns {Integer} the next selectable index. This will always be in the range of the bottom of the current selection index and the proposed index.
-   @private
-  */
-  _findNextSelectableItemFromIndex: function(proposedIndex) {
-    var content = this.get('content');
-    var contentLength = content.get('length');
-    var bottom = this._indexOfSelectionTop();
-    
-    while (proposedIndex &lt; contentLength &amp;&amp;
-      this.invokeDelegateMethod(this.delegate, 'collectionViewShouldSelectItem', this, content.objectAt(proposedIndex)) === NO) {
-      proposedIndex++;
-    }
-    return (proposedIndex &lt; contentLength) ? proposedIndex : bottom;
-  },
-  
-  /** @private
-   Finds the previous selectable item, up to the first item, by asking the
-   delegate. If a non-selectable item is found, the index is skipped. If
-   no item is found, selection index is returned unmodified.
-   
-   @param {Integer} proposedIndex the desired index to select
-   @returns {Integer} the previous selectable index. This will always be in the range of the top of the current selection index and the proposed index.
-   @private
-  */
-  _findPreviousSelectableItemFromIndex: function(proposedIndex) {
-    var content = this.get('content');
-    var contentLength = content.get('length');
-    var top = this._indexOfSelectionTop();
-    
-    while (proposedIndex &gt;= 0 &amp;&amp;
-           this.invokeDelegateMethod(this.delegate, 'collectionViewShouldSelectItem', this, content.objectAt(proposedIndex)) === NO) {
-      proposedIndex--;
-    }
-    return (proposedIndex &gt;= 0) ? proposedIndex : top ;
-  },
-  
-  /** @private
-    if content value is editable and we have one item selected, then edit.
-    otherwise, invoke action.
-  */
-  insertNewline: function() {
-    var sel, itemView;
-    if (this.get('contentValueIsEditable')) {
-      sel = this.get('selection') ;
-      if (sel &amp;&amp; sel.get('length') === 1) {
-        itemView = this.itemViewForContent(sel.objectAt(0)) ;
-        if (itemView &amp;&amp; itemView.beginEditing) {
-          this.scrollToItemView(itemView) ;
-          itemView.beginEditing() ;
-        }
-      }
+    } else if (contentIndex &gt;= min &amp;&amp; contentIndex &lt;= max) {
+      if (anchor&lt;0) this._selectionAnchor = anchor = min; //anchor at start
       
-    // invoke action!
-    } else {
-      sel = this.get('selection') ;
-      itemView = (sel &amp;&amp; sel.get('length') === 1) ? this.itemViewForContent(sel.objectAt(0)) : null ;
-      this._action(itemView, null) ;
+      if (contentIndex === anchor) min = max = contentIndex ;
+      else if (contentIndex &gt; anchor) {
+        min = anchor;
+        max = contentIndex ;
+      } else if (contentIndex &lt; anchor) {
+        min = contentIndex;
+        max = anchor ;
+      }
     }
-    
-    return YES ; // always handle
-  },
-  
-  // ......................................
-  // FIRST RESPONDER
-  // 
-  
-  /** @private
-    Called whenever the collection becomes first responder. 
-    Adds the focused class to the element.
-  */
-  didBecomeFirstResponder: function() {
-    // console.log('didBecomeFirstResponder called on %@'.fmt(this));
-    this.$().addClass('focus') ;
-  },
-  
-  /** @private */
-  willLoseFirstResponder: function() {
-    // console.log('willLoseFirstResponder called on %@'.fmt(this));
-    this.$().removeClass('focus');
+
+    return SC.IndexSet.create(min, max - min + 1);
   },
   
   // ......................................
@@ -1508,6 +1998,9 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     - a mouse down event was saved by the mouseDown method.
   */
   mouseDragged: function(ev) {
+    
+    var del = this.delegateFor('isCollectionViewDelegate', this.delegate, this.get('content'));
+    
     // if the mouse down event was cleared, there is nothing to do; return.
     if (this._mouseDownEvent === null) return YES ;
     
@@ -1515,7 +2008,7 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     if ((Date.now() - this._mouseDownAt) &lt; 123) return YES ;
     
     // OK, they must be serious, decide if a drag will be allowed.
-    if (this.invokeDelegateMethod(this.delegate, 'collectionViewShouldBeginDrag', this)) {
+    if (del.collectionViewShouldBeginDrag(this)) {
       
       // First, get the selection to drag.  Drag an array of selected
       // items appearing in this collection, in the order of the 
@@ -1884,35 +2377,29 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
   collectionViewShouldBeginDrag: function(view) {
     return this.get('canReorderContent') ;
   },
+
   
-  /**
-    If some state changes that causes the row height for a range of rows 
-    then you should call this method to notify the view that it needs to
-    recalculate the row heights for the collection.
+  dragViewFor: function(dragContent) {
+    // console.log('%@.dragViewFor(dragContent=%@)'.fmt(this, dragContent)) ;
+    var view = SC.View.create() ;
+    var layer = this.get('layer').cloneNode(false) ;
     
-    Anytime your content array changes, the rows are invalidated 
-    automatically so you only need to use this for cases where your rows
-    heights may change without changing the content array itself.
+    view.set('parentView', this) ;
+    view.set('layer', layer) ;
     
-    If all rows heights have changed, you can pass null to invalidate the
-    whole range.
+    var ary = dragContent, content = SC.makeArray(this.get('content')) ;
+    for (var idx=0, len=ary.length; idx&lt;len; idx++) {
+      var itemView = this.itemViewAtContentIndex(content.indexOf(ary[idx])) ;
+      if (itemView) layer.appendChild(itemView.get('layer').cloneNode(true)) ;
+    }
     
-    @param {Range} range or null.
-    @returns {SC.CollectionView} reciever
-  */
-  rowHeightsDidChangeInRange: function(range) {},
+    return view ;
+  },
+
+  // ..........................................................
+  // INSERTION POINT
+  // 
   
-  /** 
-    The insertion orientation.  This is used to determine which
-    dimension we should pay attention to when determining insertion point for
-    a mouse click.
-    
-    {{{
-      SC.HORIZONTAL_ORIENTATION: look at the X dimension only
-      SC.VERTICAL_ORIENTATION: look at the Y dimension only
-    }}}
-  */
-  insertionOrientation: SC.HORIZONTAL_ORIENTATION,
   
   /**
     Get the preferred insertion point for the given location, including 
@@ -1997,77 +2484,21 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     return ret;
   },
   
-  /** 
-    Override to show the insertion point during a drag.
-    
-    Called during a drag to show the insertion point.  Passed value is the
-    item view that you should display the insertion point before.  If the
-    passed value is null, then you should show the insertion point AFTER that
-    last item view returned by the itemViews property.
-    
-    Once this method is called, you are guaranteed to also recieve a call to
-    hideInsertionPoint() at some point in the future.
-    
-    The default implementation of this method does nothing.
-    
-    @param itemView {SC.ClassicView} view the insertion point should appear directly before. If null, show insertion point at end.
-    @param dropOperation {Number} the drop operation.  will be SC.DROP_BEFORE or SC.DROP_ON
-    
-    @returns {void}
-  */
-  showInsertionPoint: function(itemView, dropOperation) {
-    return (dropOperation === SC.DROP_BEFORE) ? this.showInsertionPointBefore(itemView) : this.hideInsertionPoint() ;
-  },
-  
-  /**
-    @deprecated
-    
-    Show the insertion point during a drag before the named item view.
-    
-    This method has been deprecated in favor of the more generic 
-    showInsertionPoint() which can be used to show drops occurring both on
-    and before an itemView.  If you do not implement showInsertionPoint() 
-    yourself, the default implementation will call this method whenever the
-    drop operation is SC.DROP_BEFORE.
-    
-    @param itemView {SC.ClassicView} the item view to show before.
-    @returns {void}
-  */
-  showInsertionPointBefore: function(itemView) {},
-  
-  /**
-    Override to hide the insertion point when a drag ends.
-    
-    Called during a drag to hide the insertion point.  This will be called 
-    when the user exits the view, cancels the drag or completes the drag.  It 
-    will not be called when the insertion point changes during a drag.
-    
-    You should expect to receive one or more calls to 
-    showInsertionPointBefore() during a drag followed by at least one call to 
-    this method at the end.  Your method should not raise an error if it is 
-    called more than once.
-    
-    @returns {void}
-  */
-  hideInsertionPoint: function() {},
-  
-  dragViewFor: function(dragContent) {
-    // console.log('%@.dragViewFor(dragContent=%@)'.fmt(this, dragContent)) ;
-    var view = SC.View.create() ;
-    var layer = this.get('layer').cloneNode(false) ;
-    
-    view.set('parentView', this) ;
-    view.set('layer', layer) ;
-    
-    var ary = dragContent, content = SC.makeArray(this.get('content')) ;
-    for (var idx=0, len=ary.length; idx&lt;len; idx++) {
-      var itemView = this.itemViewAtContentIndex(content.indexOf(ary[idx])) ;
-      if (itemView) layer.appendChild(itemView.get('layer').cloneNode(true)) ;
+  // ..........................................................
+  // INTERNAL SUPPORT
+  // 
+
+  /** @private - when we become visible, reload if needed. */
+  _cv_isVisibleInWindowDidChange: function() {
+    if (this.get('isVisibleInWindow')) {
+      if (this._invalidIndexes) this.invokeOnce(this.reloadIfNeeded);
+      if (this._invalidSelection) {
+        this.invokeOnce(this.reloadSelectionIndexesIfNeeded);
+      } 
     }
-    
-    return view ;
-  },
-  
+  }.observes('isVisibleInWindow'),
+
+
   /**
     Default delegate method implementation, returns YES if isSelectable
     is also true.
@@ -2076,16 +2507,56 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     return this.get('isSelectable') ;
   },
   
-  // ......................................
-  // INTERNAL
-  //
+  _TMP_DIFF1: SC.IndexSet.create(),
+  _TMP_DIFF2: SC.IndexSet.create(),
+  
+  /** @private
+  
+    Whenever the nowShowing range changes, update the range observer on the 
+    content item and instruct the view to reload any indexes that are not in
+    the previous nowShowing range.
+
+  */
+  _cv_nowShowingDidChange: function() {
+    var nowShowing  = this.get('nowShowing'),
+        last        = this._lastNowShowing,
+        diff, diff1, diff2;
+
+    // find the differences between the two
+    // NOTE: reuse a TMP IndexSet object to avoid creating lots of objects
+    // during scrolling
+    if (last &amp;&amp; nowShowing &amp;&amp; (last !== nowShowing)) {
+      diff1 = this._TMP_DIFF1.add(last).remove(nowShowing);
+      diff2 = this._TMP_DIFF2.add(nowShowing).remove(last);
+      diff = diff1.add(diff2);
+    } else diff = last || nowShowing ;
+
+    // if nowShowing has actually changed, then update
+    if (diff &amp;&amp; diff.get('length') &gt; 0) {
+      this._lastNowShowing = nowShowing ? nowShowing.frozenCopy() : null ;
+      this.updateContentRangeObserver();
+      this.reload(diff);
+    }
+    
+    // cleanup tmp objects
+    if (diff1) diff1.clear();
+    if (diff2) diff2.clear();
+    
+  }.observes('nowShowing'),
+  
+  init: function() {
+     sc_super();
+     this._lastNowShowing = this.get('nowShowing').clone();
+     if (this.content) this._cv_contentDidChange();
+     if (this.selection) this._cv_selectionDidChange();
+  },
   
   /** @private
     Perform the action.  Supports legacy behavior as well as newer style
     action dispatch.
   */
-  _action: function(view, evt) {
-    // console.log('_action invoked on %@ with view %@, evt %@'.fmt(this, view, evt));
+  _cv_action: function(view, evt) {
+    // console.log('_cv_action invoked on %@ with view %@, evt %@'.fmt(this, view, evt));
     var action = this.get('action');
     var target = this.get('target') || null;
     // console.log('action %@, target %@'.fmt(action, target));
@@ -2114,4 +2585,5 @@ SC.CollectionView = SC.View.extend(SC.CollectionViewDelegate,
     }
   }
   
+  
 });</diff>
      <filename>frameworks/desktop/views/collection.js</filename>
    </modified>
    <modified>
      <diff>@@ -6,6 +6,7 @@
 // ==========================================================================
 
 sc_require('views/collection');
+sc_require('mixins/collection_row_delegate');
 
 /** @class
   
@@ -18,13 +19,10 @@ sc_require('views/collection');
   the ListView to optimize its rendering.
   
   h2. Variable Row Heights
-  
-  ListView now supports variable row heights 
-  The ListView adds support for a single delegate method:
-  
-  {{{
-    collectionViewRowHeightForContent()
-  }}}
+
+  Normally you set the row height through the rowHeight property.  You can 
+  also support custom row heights by implementing the 
+  contentCustomRowHeightIndexes property to return an index set.
   
   h2. Using ListView with Very Large Data Sets
   
@@ -53,655 +51,334 @@ sc_require('views/collection');
   cheat for very long data sets to make rendering more efficient?)
   
   @extends SC.CollectionView
+  @extends SC.CollectionRowDelegate
   @since SproutCore 1.0
 */
 SC.ListView = SC.CollectionView.extend(
+  SC.CollectionRowDelegate,
 /** @scope SC.ListView.prototype */ {
   
   classNames: ['sc-list-view'],
-  
-  /**
-    The default layout for the list view simply fills the entire parentView.
-  */
-  layout: { left: 0, right: 0, top: 0, bottom: 0 },
-  
+
   acceptsFirstResponder: YES,
-  
+
   // ..........................................................
-  // ROW HEIGHT SUPPORT
+  // COLLECTION ROW DELEGATE SUPPORT
   // 
   
-  /** 
-    The common row height for list view items.
-    
-    If you set this property, then the ListView will be able to use this
-    property to perform absolute layout of its children and to minimize t
-    number of actual views it has to create.
-    
-    The value should be an integer expressed in pixels.
-    
-    You can alternatively set either the rowHeightKey or implement
-    the collectionViewHeightForRowAtContentIndex() delegate method.
-  */
-  rowHeight: 20,
   
   /**
-    If set, this key will be used to calculate the row height for a given
-    content object.
+    Returns the current collectionRowDelegate.  This property will recompute
+    everytime the content changes.
   */
-  rowHeightKey: null,
-  
-  /**
-    This optional delegate method will be called for each item in your 
-    content, giving you a chance to decide what row height to use for the
-    content at the named index.
-    
-    The default version will return either the fixed rowHeight you 
-    specified or will lookup the row height on the content object using the
-    rowHeightKey.
-    
-    @params {SC.CollectionView} the requesting collection view
-    @params {Number} the index into the content
-    @returns {Number} rowHeight
-  */
-  collectionViewHeightForRowAtContentIndex: function(collectionView, contentIndex) {
-    // console.log('collectionViewHeightForRowAtContentIndex invoked in %@ with index %@'.fmt(this, contentIndex));
-    // console.log('rowHeightKey is %@'.fmt(this.get('rowHeightKey')));
-    // just test for presence of a rowHeightKey..to implement fast path...
-    if (!this.rowHeightKey) return this.get('rowHeight');
-    var key = this.get('rowHeightKey'), content = this.get('content'), rowHeight;
-    if (content) content = content.objectAt(contentIndex);
-    rowHeight = content ? content.get(key) : this.get('rowHeight');
-    // console.log('content.get(key) is %@'.fmt(content ? content.get(key) : undefined));
-    return rowHeight ;
-  },
-  
-  /**
-    If some state changes that causes the row height for a range of rows 
-    then you should call this method to notify the view that it needs to
-    recalculate the row heights for the collection.
-    
-    Anytime your content array changes, the rows are invalidated 
-    automatically so you only need to use this for cases where your rows
-    heights may change without changing the content array itself.
-    
-    If all rows heights have changed, you can pass null to invalidate the
-    whole range.
-    
-    @param {Range} range or null.
-    @returns {SC.CollectionView} reciever
+  rowDelegate: function() {
+    var del     = this.delegate,
+        content = this.get('content');
+    return this.delegateFor('isCollectionRowDelegate', del, content);
+  }.property('delegate', 'content').cacheable(),
+  
+  /** @private 
+    Whenever the rowDelegate changes, begin observing important properties
   */
-  rowHeightsDidChangeInRange: function(range) {
-    if (this.get('hasUniformRowHeights')) return ; // nothing to do...
-    
-    // console.log('rowHeightsDidChangeInRange called on %@ with range %@'.fmt(this, $I(range)));
-    // if no range is passed, just wipe the cached...
-    if (!range) {
-      this._list_rowOffsets = this._list_rowHeights = null ;
-      
-    // otherwise, truncate the array of rowOffsets so that everything after
-    // the start of this range will be recalc'd.  For cached rowHeights,
-    // set to undefined unless max range exceeds length, in which case you
-    // just truncate.
-    } else {
-      var min = Math.max(0,range.start) ;
-      var offsets = this._list_rowOffsets, heights = this._list_rowHeights;
-      if (offsets) offsets.length = min ;
-      if (heights) {
-        var max = SC.maxRange(range); 
-        if (max &gt;= heights.length) {
-          heights.length = min ;
-        } else {
-          while(min&lt;max) heights[min++] = undefined ;
-        }
-      }
+  _sclv_rowDelegateDidChange: function() {
+    var last = this._sclv_rowDelegate,
+        del  = this.get('rowDelegate'),
+        func = this._sclv_rowHeightDidChange,
+        func2 = this._sclv_customRowHeightIndexesDidChange;
+        
+    if (last === del) return this; // nothing to do
+    this._sclv_rowDelegate = del; 
+
+    // last may be null on a new object
+    if (last) {
+      last.removeObserver('rowHeight', this, func);
+      last.removeObserver('customRowHeightIndexes', this, func2);
     }
     
-    // now update the layout...
-    this.adjust(this.computeLayout());
-    
-    // force recomputation of contentRangeInFrame
-    this.notifyPropertyChange('content');
-    
-    // and notify that nowShowingRange may have changed...
-    this.invalidateNowShowingRange() ;
-  },
-  
-  /**
-    Set to YES if your list view should have uniform row heights.  This will
-    enable an optimization that avoids inspecting actual content objects 
-    when calculating the size of the view.
+    if (!del) {
+      throw &quot;Internal Inconsistancy: ListView must always have CollectionRowDelegate&quot;;
+    }
     
-    The default version of this property is set to YES unless you set a 
-    delegate or a rowHeightKey.
-  */
-  hasUniformRowHeights: YES,
-  // function(key, value) {
-  //     if (value !== undefined) this._list_hasUniformRowHeights = value ;
-  //     value = this._list_hasUniformRowHeights;
-  //     return SC.none(value) ? !((this.delegate &amp;&amp; this.delegate.collectionViewHeightForRowAtContentIndex) || this.rowHeightKey) : value ;
-  //   }.property('delegate', 'rowHeightKey').cacheable(),
-  
-  /**
-    Calculates the offset for the row at the specified index.  Based on the 
-    current setting this may compute the row heights for previous items or 
-    it will simply do some math...
+    del.addObserver('rowHeight', this, func);
+    del.addObserver('customRowHeightIndexes', this, func2);
+    this._sclv_rowHeightDidChange()._sclv_customRowHeightIndexesDidChange();
+    return this ;
+  }.observes('rowDelegate'),
+
+  /** @private 
+    called whenever the rowHeight changes.  If the property actually changed
+    then invalidate all row heights.
   */
-  offsetForRowAtContentIndex: function(contentIndex) {
-    if (contentIndex === 0) return 0 ;
-    
-    // do some simple math if we have uniform row heights...
-    if (this.get('hasUniformRowHeights')) {
-      return this.get('rowHeight') * contentIndex ;
-      
-    // otherwise, use the rowOffsets cache...
-    } else {
-      // get caches
-      var offsets = this._list_rowOffsets;
-      if (!offsets) offsets = this._list_rowOffsets = [] ;
-      
-      // OK, now try the fast path...if undefined, loop backwards until we
-      // find an offset that IS cached...
-      var len = offsets.length, cur = contentIndex, height, ret;
-      
-      // get the cached offset.  Note that if the requested index is longer 
-      // than the length of the offsets cache, then just assume the value is
-      // undefined.  We don't want to accidentally read an old value...
-      if (contentIndex &lt; len) {
-        ret = offsets[cur];
-      } else {
-        ret = undefined ;
-        cur = len; // start search at current end of offsets...
-      }
-      
-      // if the cached value was undefined, loop backwards through the offsets
-      // hash looking for a cached value to start from
-      while((cur&gt;0) &amp;&amp; (ret===undefined)) ret = offsets[--cur];
-      
-      // now, work our way forward, building the cache of offsets.  Use
-      // cached heights...
-      if (ret===undefined) ret = offsets[cur] = 0 ;
-      while (cur &lt; contentIndex) {
-        // get height...recache if needed....
-        height = this._list_heightForRowAtContentIndex(cur) ;
-        
-        // console.log('index %@ has height %@'.fmt(cur, height));
+  _sclv_rowHeightDidChange: function() {
+    var del = this.get('rowDelegate'),
+        height = del.get('rowHeight'), 
+        indexes;
         
-        // add to ret and save in cache
-        ret = ret + height ;
-        
-        cur++; // go to next offset
-        offsets[cur] = ret ;
-      }
-      
-      return ret ;
-    }
-  },
-  
-  /**
-    Calculates the height for the row at content index.  This method will
-    perform some simple math if hasUniformRowHeights is enabled.  Otherwise
-    it will consult the collection view delegate to compute the row heights.
-  */
-  heightForRowAtContentIndex: function(contentIndex) {
-    if (this.get('hasUniformRowHeights')) {
-      return this.get('rowHeight') ;
-    } else return this._list_heightForRowAtContentIndex(contentIndex);
+    if (height === this._sclv_rowHeight) return this; // nothing to do
+    this._sclv_rowHeight = height;
+
+    indexes = SC.IndexSet.create(0, this.get('length'));
+    this.rowHeightDidChangeForIndexes(indexes);
+    return this ;
   },
-  
-  /** @private
-    By-passes the uniform row heights check.  Makes offsetForRow... a little
-    faster.
+
+  /** @private 
+    called whenever the customRowHeightIndexes changes.  If the property 
+    actually changed then invalidate affected row heights.
   */
-  _list_heightForRowAtContentIndex: function(contentIndex) {
-    // console.log('_list_heightForRowAtContentIndex invoked on %@ with index %@'.fmt(this, index));
-    var heights = this._list_rowHeights;
-    if (!heights) heights = this._list_rowHeights = [] ;
+  _sclv_customRowHeightIndexesDidChange: function() {
+    var del     = this.get('rowDelegate'),
+        indexes = del.get('customRowHeightIndexes'), 
+        last    = this._sclv_customRowHeightIndexes,
+        func    = this._sclv_customRowHeightIndexesContentDidChange;
+        
+    // nothing to do
+    if ((indexes===last) || (last &amp;&amp; last.isEqual(indexes))) return this;
+
+    // if we were observing the last index set, then remove observer
+    if (last &amp;&amp; this._sclv_isObservingCustomRowHeightIndexes) {
+      last.removeObserver('[]', this, func);
+    }
     
-    var height = (contentIndex &lt; heights.length) ?
-      heights[contentIndex] :
-      undefined ;
-    if (height===undefined) {
-      height = heights[contentIndex] = this.invokeDelegateMethod(this.delegate, 'collectionViewHeightForRowAtContentIndex', this, contentIndex) || 0 ;
+    // only observe new index set if it exists and it is not frozen.
+    if (this._sclv_isObservingCustomRowHeightIndexes = indexes &amp;&amp; !indexes.get('isFrozen')) {
+      indexes.addObserver('[]', this, func);
     }
     
-    return height ;
+    this._sclv_customRowHeightIndexesContentDidChange();
+    return this ;
+  },
+
+  /** @private
+    Called whenever the customRowHeightIndexes set is modified.
+  */
+  _sclv_customRowHeightIndexesContentDidChange: function() {
+    var del     = this.get('rowDelegate'),
+        indexes = del.get('customRowHeightIndexes'), 
+        last    = this._sclv_customRowHeightIndexes, 
+        changed;
+
+    // compute the set to invalidate.  the union of cur and last set
+    if (indexes &amp;&amp; last) {
+      changed = indexes.copy().add(last);
+    } else changed = indexes || last ;
+    this._sclv_customRowHeightIndexes = indexes ? indexes.frozenCopy() : null; 
+
+    // invalidate
+    this.rowHeightDidChangeForIndexes(changed);
+    return this ;
   },
   
   // ..........................................................
-  // RENDERING
+  // ROW PROPERTIES
   // 
   
-  render: function(context, firstTime) {
-    if (SC.BENCHMARK_RENDER) {
-      var bkey = '%@.render'.fmt(this) ;
-      SC.Benchmark.start(bkey);
-    }
-    this.beginPropertyChanges() ; // avoid sending notifications
-    
-    var content = SC.makeArray(this.get('content')) ;
-    var selection = SC.makeArray(this.get('selection'));
-    var oldRange = this._oldNowShowingRange ;
-    var range = SC.cloneRange(this.get('nowShowingRange')) ;
-    this._oldNowShowingRange = SC.cloneRange(range) ;
-    var key, itemView = this.createExampleView(content), c ;
-    var range2 ; // only used if the old range fits inside the new range
-    var idx, end, childId, maxLen ;
-    
-    // keep track of children we've got rendered
-    var childSet = this._childSet ;
-    if (!childSet) childSet = this._childSet = [] ;
+  /**
+    Returns the top offset for the specified content index.  This will take
+    into account any custom row heights and group views.
     
-    if (SC.ENABLE_COLLECTION_PARTIAL_RENDER) {
-      // used for santity checks during debugging
-      if (SC.SANITY_CHECK_PARTIAL_RENDER) var maxLen = range.length ;
-      
-      if (SC.DEBUG_PARTIAL_RENDER) {
-        console.log('oldRange = ') ;
-        console.log(oldRange) ;
-        console.log('range = ') ;
-        console.log(range) ;
-      }
-      
-      // if we're dirty, redraw everything visible
-      // (selection changed, content changed, etc.)
-      if (this.get('isDirty') || firstTime) {
-        childSet.length = 0 ; // full render
-        
-      // else, only redaw objects we haven't previously drawn
-      } else if (oldRange) {
-        // ignore ranges that don't overlap above..
-        if (range.start &gt;= oldRange.start + oldRange.length) {
-          childSet.length = 0 ; // full render
-        
-        // and below...
-        } else if (range.start + range.length &lt;= oldRange.start) {
-          childSet.length = 0 ; // full render
-        
-        // okay, the ranges do overlap. are they equal?
-        } else if (SC.rangesEqual(oldRange, range)) {
-          range = SC.EMPTY_RANGE ; // nothing to render
-        
-        // nope, is the old range inside the new range?
-        } else if (range.start &lt;= oldRange.start &amp;&amp; range.start + range.length &gt;= oldRange.start + oldRange.length) {
-          // need to render two ranges...all pre-existing views are valid
-          context.updateMode = SC.MODE_APPEND ;
-          range2 = { start: oldRange.start + oldRange.length, length: (range.start + range.length) - (oldRange.start + oldRange.length) } ;
-          range.length = oldRange.start - range.start ;
-        
-        // nope, is the new range inside the old range?
-        } else if (range.start &gt;= oldRange.start &amp;&amp; range.start + range.length &lt;= oldRange.start + oldRange.length) {        
-          // need to remove unused childNodes at both ends, start with bottom...
-          idx = oldRange.start ;
-          end = range.start ;
-          while (idx &lt; end) {
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('looping on bottom range');
-            childId = childSet[idx] ;
-            if (childId) context.remove(childId) ;
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('deleting content at index %@'.fmt(idx));
-            delete childSet[idx] ;
-            ++idx ;
-          }
-        
-          // now remove unused childNodes at the top of the range...
-          idx = range.start + range.length ;
-          end = oldRange.start + oldRange.length ;
-          while (idx &lt; end) {
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('looping on top range');
-            childId = childSet[idx] ;
-            if (childId) context.remove(childId) ;
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('deleting content at index %@'.fmt(idx));
-            delete childSet[idx] ;
-            ++idx ;
-          }
-        
-          range = SC.EMPTY_RANGE ; // nothing to render
-        
-        // nope, is the new range lower than the old range?
-        } else if (range.start &lt; oldRange.start) {
-          context.updateMode = SC.MODE_APPEND ;
-        
-          // need to remove unused childNodes at the top of the old range
-          idx = range.start + range.length ;
-          end = oldRange.start + oldRange.length ;
-          while (idx &lt; end) {
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('looping on top only');
-            childId = childSet[idx] ;
-            if (childId) context.remove(childId) ;
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('deleting content at index %@'.fmt(idx));
-            delete childSet[idx] ;
-            ++idx ;
-          }
-        
-          range.length = Math.min(range.length, oldRange.start - range.start) ;
-        
-        // nope, so the new range is higher than the old range
-        } else {
-          context.updateMode = SC.MODE_APPEND ;
-        
-          // need to remove unused childNodes at the bottom of the old range
-          idx = oldRange.start ;
-          end = range.start ;
-          while (idx &lt; end) {
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('looping on bottom only');
-            childId = childSet[idx] ;
-            if (childId) context.remove(childId) ;
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('deleting content at index %@'.fmt(idx));
-            delete childSet[idx] ;
-            ++idx ;
-          }
+    @param {Number} idx the content index
+    @returns {Number} the row offset
+  */
+  rowOffsetForContentIndex: function(idx) {
+    if (idx === 0) return 0 ; // fastpath
+
+    var del       = this.get('rowDelegate'),
+        rowHeight = del.get('rowHeight'),
+        ret, custom, cache, delta, max, content ;
         
-          end = range.start + range.length ;
-          range.start = oldRange.start + oldRange.length ;
-          range.length = end - range.start ;
-        }
-      }
-    
-      if (SC.SANITY_CHECK_PARTIAL_RENDER) {
-        if (range.length &lt; 0) throw &quot;range.length is &quot; + range.length ;
-        if (range.length &gt; maxLen) throw &quot;range.length is &quot; + range.length + ', max length is ' + maxLen ;
-        if (range.start &lt; 0) throw &quot;range.start is &quot; + range.start ;
-        if (range2) {
-          if (range2.length &lt; 0) throw &quot;range2.length is &quot; + range2.length ;
-          if (range2.length &gt; maxLen) throw &quot;range2.length is &quot; + range2.length + ', max length is ' + maxLen ;
-          if (range2.start &lt; 0) throw &quot;range2.start is &quot; + range2.start ;
-        }
+    ret = idx * rowHeight;
+    if (del.customRowHeightIndexes &amp;&amp; (custom=del.get('customRowHeightIndexes'))) {
+      
+      // prefill the cache with custom rows.
+      cache = this._sclv_offsetCache;
+      if (!cache) {
+        cache = this._sclv_offsetCache = [];
+        delta = max = 0 ;
+        custom.forEach(function(idx) {
+          delta += this.rowHeightForContentIndex(idx)-rowHeight;
+          cache[idx+1] = delta;
+          max = idx ;
+        }, this);
+        this._sclv_max = max+1;
       }
-    
-      if (SC.DEBUG_PARTIAL_RENDER) {
-        console.log('rendering = ') ;
-        console.log(range) ;
-        if (range2) {
-          console.log('also rendering = ') ;
-          console.log(range2) ;
+      
+      // now just get the delta for the last custom row before the current 
+      // idx.
+      delta = cache[idx];
+      if (delta === undefined) {
+        delta = cache[idx] = cache[idx-1];
+        if (delta === undefined) {
+          max = this._sclv_max;
+          if (idx &lt; max) max = custom.indexBefore(idx)+1;
+          delta = cache[idx] = cache[max] || 0;
         }
       }
+
+      ret += delta ;
     }
     
-    idx = SC.maxRange(range) ;
-    
-    var baseKey = SC.guidFor(this) + '_' ;
-    var guids = this._itemViewGuids, guid;
-    if (!guids) this._itemViewGuids = guids = {};
-    
-    // TODO: Use SC.IndexSet, not separate ranges, once it's ready.
-    // This will also make it possible to do partial updates during content
-    // and selection changes. Now we always do a full update.
-    
-    while (--idx &gt;= range.start) {
-      c = content.objectAt(idx) ;
-      if (SC.DEBUG_PARTIAL_RENDER) console.log('rendering content(%@) at index %@'.fmt(c.unread, idx));
-      
-      // use cache of item view guids to avoid creating temporary objects
-      guid = SC.guidFor(c);
-      if (!(key = guids[guid])) key = guids[guid] = baseKey+guid;
-      
-      itemView.set('content', c) ;
-      itemView.set('isSelected', (selection.indexOf(c) == -1) ? NO : YES) ;
-      itemView.layerId = key ; // cannot use .set, layerId is RO
-      if (SC.SANITY_CHECK_PARTIAL_RENDER &amp;&amp; childSet[idx]) throw key + '(' + c.unread + ')'+ ' at index ' + idx ; // should not re-render a child in the index!
-      childSet[idx] = key ;
-      itemView.adjust(this.itemViewLayoutAtContentIndex(idx)) ;
-      context = context.begin(itemView.get('tagName')) ;
-      itemView.prepareContext(context, YES) ;
-      context = context.end() ;
-    }
+    return ret ;
+  },
+  
+  /**
+    Returns the row height for the specified content index.  This will take
+    into account custom row heights and group rows.
     
-    if (range2) {
-      idx = SC.maxRange(range2) ;
-      while (--idx &gt;= range2.start) {
-        c = content.objectAt(idx) ;
-        if (SC.DEBUG_PARTIAL_RENDER) console.log('rendering content(%@) at index %@'.fmt(c.unread, idx));
-        
-        // use cache of item view guids to avoid creating temporary objects
-        guid = SC.guidFor(c);
-        if (!(key = guids[guid])) key = guids[guid] = baseKey+guid;
-        
-        itemView.set('content', c) ;
-        itemView.set('isSelected', (selection.indexOf(c) == -1) ? NO : YES) ;
-        itemView.layerId = key ; // cannot use .set, layerId is RO
-        if (SC.SANITY_CHECK_PARTIAL_RENDER &amp;&amp; childSet[idx]) throw key + '(' + c.unread + ')'+ ' at index ' + idx ; // should not re-render a child in the index!
-        childSet[idx] = key ;
-        itemView.adjust(this.itemViewLayoutAtContentIndex(idx)) ;
-        context = context.begin(itemView.get('tagName')) ;
-        itemView.prepareContext(context, YES) ;
-        context = context.end() ;
+    @param {Number} idx content index
+    @returns {Number} the row height
+  */
+  rowHeightForContentIndex: function(idx) {
+    var del = this.get('rowDelegate'),
+        ret, cache, content, indexes;
+    
+    if (del.customRowHeightIndexes &amp;&amp; (indexes=del.get('customRowHeightIndexes'))) {
+      cache = this._sclv_heightCache ;
+      if (!cache) {
+        cache = this._sclv_heightCache = [];
+        content = this.get('content');
+        indexes.forEach(function(idx) {
+          cache[idx] = del.contentIndexRowHeight(this, content, idx);
+        }, this);
       }
-    }
+      
+      ret = cache[idx];
+      if (ret === undefined) ret = del.get('rowHeight');
+    } else ret = del.get('rowHeight');
     
-    if (SC.DEBUG_PARTIAL_RENDER) console.log('******************************') ;
+    return ret ;
+  },
+  
+  /**
+    Call this method whenever a row height has changed in one or more indexes.
+    This will invalidate the row height cache and reload the content indexes.
+    Pass either an index set or a single index number.
+
+    This method is called automatically whenever you change the rowHeight
+    or customRowHeightIndexes properties on the collectionRowDelegate.
+    
+    @param {SC.IndexSet|Number} indexes 
+    @returns {SC.ListView} receiver
+  */  
+  rowHeightDidChangeForIndexes: function(indexes) {
+    var len     = this.get('length');
+
+    // clear any cached offsets
+    this._sclv_heightCache = this._sclv_offsetCache = null;
     
-    this.set('isDirty', NO);
-    this.endPropertyChanges() ;
-    if (SC.BENCHMARK_RENDER) SC.Benchmark.end(bkey);    
+    // find the smallest index changed; invalidate everything past it
+    if (indexes &amp;&amp; indexes.isIndexSet) indexes = indexes.get('min');
+    this.reload(SC.IndexSet.create(indexes, len-indexes));
+    return this ;
   },
   
   // ..........................................................
-  // SUBCLASS SUPPORT
+  // SUBCLASS IMPLEMENTATIONS
   // 
   
-  insertionOrientation: SC.VERTICAL_ORIENTATION,
-  
-  /** @private
-    Overrides default CollectionView method to compute the minimim height
-    of the list view.
+  /**
+    The layout for a ListView is computed from the total number of rows 
+    along with any custom row heights.
   */
   computeLayout: function() {
-    var content = this.get('content') ;
-    var rows = (content) ? content.get('length') : 0 ;
-    
-    // use this cached layout hash to avoid allocing memory...
-    var ret = this._cachedLayoutHash ;
-    if (!ret) ret = this._cachedLayoutHash = {};
-    
-    // set minHeight
-    ret.minHeight = this.offsetForRowAtContentIndex(rows);
-    return ret; 
+    // default layout
+    var ret = this._sclv_layout;
+    if (!ret) ret = this._sclv_layout = {};
+    ret.minHeight = this.rowOffsetForContentIndex(this.get('length'))+4;
+    return ret ;
   },
   
-  /** @private
-    Calculates the visible content range in the specified frame.  If 
-    uniform rows are set, this will use some simple math.  Otherwise it will
-    compute all row offsets leading up to the frame.
+  /**
+  
+    Computes the layout for a specific content index by combining the current
+    row heights.
+  
   */
-  contentRangeInFrame: function(frame) {
-    // console.log('contentRangeInFrame invoked on %@ with frame {%@, %@, %@, %@}'.fmt(this, frame.x, frame.y, frame.width, frame.height));
-    var min, max, ret, rowHeight ;
-    var minY = SC.minY(frame), maxY = SC.maxY(frame);
-    // use some simple math...
-    if (this.get('hasUniformRowHeights')) {
-      rowHeight = this.get('rowHeight') || 20 ;
-      min = Math.max(0,Math.floor(minY / rowHeight)-1) ;
-      max = Math.ceil(maxY / rowHeight) ;
-      
-      var content = this.get('content') ;
-      min = Math.min(min, content.get('length')) ;
-      max = Math.min(max, content.get('length')) ;
-      
-      // convert to range...
-      ret = { start: min, length: max - min } ;
-      
-    // otherwise, get the cached row offsets...
-    } else {
-      content = this.get('content');
-      var len = (content ? content.get('length') : 0), offset = 0;
-      
-      // console.log('contentRangeInFrame content length is %@'.fmt(len));
-      
-      // console.log('minY = ' + minY) ;
-      // console.log('maxY = ' + maxY) ;
-      
-      min = null; 
-      max = 0;
-      do {
-        offset = this.offsetForRowAtContentIndex(max); // add offset.
-        // console.log('offset is now %@'.fmt(offset));
-        if ((min===null) &amp;&amp; (offset &gt;= minY)) min = max; // set min
-        max++ ;
-      } while (max&lt;len &amp;&amp; offset &lt; maxY);
-      
-      // console.log('min = ' + min) ;
-      // console.log('max = ' + max) ;
-      
-      // convert to range...
-      ret = { start: Math.max(min-1, 0), length: Math.min(max - min + 2, len) } ;
-    }
-    
-    // console.log('contentRangeInFrame is {%@, %@}'.fmt(ret.start, ret.length));
-    return ret ;
+  layoutForContentIndex: function(contentIndex) {
+    return {
+      top:    this.rowOffsetForContentIndex(contentIndex),
+      height: this.rowHeightForContentIndex(contentIndex),
+      left:   0, 
+      right:  0
+    };
   },
   
-  /** @private */
-  itemViewLayoutAtContentIndex: function(contentIndex) {
-    // console.log('%@.itemViewLayoutAtContentIndex(%@)'.fmt(this, contentIndex));
-    var layout = { left: 0, right: 0 } ;
+  /**
+    Override to return an IndexSet with the indexes that are at least 
+    partially visible in the passed rectangle.  This method is used by the 
+    default implementation of computeNowShowing() to determine the new 
+    nowShowing range after a scroll.
     
-    // set top &amp; height...
-    layout.top = this.offsetForRowAtContentIndex(contentIndex) ;
-    layout.height = this.heightForRowAtContentIndex(contentIndex) ;
+    Override this method to implement incremental rendering.
     
-    return layout ;
-  },
-  
-  insertionPointClass: SC.View.extend({
-    emptyElement: '&lt;div&gt;&lt;span class=&quot;anchor&quot;&gt;&lt;/span&gt;&lt;/div&gt;',
-    classNames: ['sc-list-insertion-point'],
-    layout: { top: -6, height: 2, left: 4, right: 2 }
-  }),
-  
-  // TODO refactor code, remove duplication
-  showInsertionPoint: function(itemView, dropOperation) {
-    if (!itemView) {
-      // show insertion point below final itemView
-      var content = this.get('content') ;
-      content = content.objectAt(content.get('length')-1) ;
-      itemView = this.itemViewForContent(content) ;
-      
-      if (!itemView) return ;
-      
-      var f = itemView.get('frame') ;
-      var top = f.y, height = f.height ;
-      
-      if (!this._insertionPointView) {
-        this._insertionPointView = this.insertionPointClass.create() ;
-      }
+    The default simply returns the current content length.
     
-      var insertionPoint = this._insertionPointView ;
-      if (insertionPoint.get('parentView') !== itemView.get('parentView')) {
-        itemView.get('parentView').appendChild(insertionPoint) ;
-      }
-      
-      insertionPoint.adjust({ top: top + height }) ;
-      return ;
+    @param {Rect} rect the visible rect
+    @returns {SC.IndexSet} now showing indexes
+  */
+  contentIndexesInRect: function(rect) {
+    var rowHeight = this.get('rowDelegate').get('rowHeight'),
+        top       = SC.minY(rect),
+        bottom    = SC.maxY(rect),
+        height    = rect.height,
+        len       = this.get('length'),
+        offset, start, end;
+    
+    // estimate the starting row and then get actual offsets until we are 
+    // right.
+    start = (top - (top % rowHeight)) / rowHeight;
+    offset = this.rowOffsetForContentIndex(start);
+    
+    // go backwards until top of row is before top edge
+    while(start&gt;0 &amp;&amp; offset&gt;=top) {
+      start--;
+      offset -= this.rowHeightForContentIndex(start);
     }
     
-    // if drop on, then just add a class...
-    if (dropOperation === SC.DROP_ON) {
-      if (itemView !== this._dropOnInsertionPoint) {
-        this.hideInsertionPoint() ;
-        itemView.$().addClass('drop-target') ;
-        this._dropOnInsertionPoint = itemView ;
-      }
-      
-    } else {
-      if (this._dropOnInsertionPoint) {
-        this._dropOnInsertionPoint.$().removeClass('drop-target') ;
-        this._dropOnInsertionPoint = null ;
-      }
-      
-      if (!this._insertionPointView) {
-        this._insertionPointView = this.insertionPointClass.create() ;
-      }
-      
-      insertionPoint = this._insertionPointView ;
-      if (insertionPoint.get('parentView') !== itemView.get('parentView')) {
-        itemView.get('parentView').appendChild(insertionPoint) ;
-      }
-      
-      var frame = itemView.get('frame') ;
-      insertionPoint.adjust({ top: itemView.get('frame').y }) ;
+    // go forwards until bottom of row is after top edge
+    offset += this.rowHeightForContentIndex(start);
+    while(start&lt;len &amp;&amp; offset&lt;top) {
+      offset += this.rowHeightForContentIndex(start);
+      start++ ;
     }
+    if (start&lt;0) start = 0;
+    if (start&gt;=len) start=len;
     
-  },
-  
-  hideInsertionPoint: function() {
-    var insertionPoint = this._insertionPointView ;
-    if (insertionPoint) insertionPoint.removeFromParent() ;
-    
-    if (this._dropOnInsertionPoint) {
-      this._dropOnInsertionPoint.removeClassName('drop-target') ;
-      this._dropOnInsertionPoint = null ;
-    }
-  },
-  
-  // We can do this much faster programatically using the rowHeight
-  insertionIndexForLocation: function(loc, dropOperation) {
-    // console.log('insertionIndexForLocation called on %@'.fmt(this));
-    var f = this.get('clippingFrame') ;
-    var sf = f ; // FIXME this.get('scrollFrame') ;
-    var retOp = SC.DROP_BEFORE ;
     
-    // find the rowHeight and offset to work with
-    var offset = loc.y - f.y - sf.y ;
-    var rowOffset, rowHeight, idx ;
+    // estimate the final row and then get the actual offsets until we are 
+    // right. - look at the offset of the _following_ row
+    end = start + ((height - (height % rowHeight)) / rowHeight) ;
+    if (end &gt; len) end = len;
+    offset = this.rowOffsetForContentIndex(end);
     
-    // do some simple math if we have uniform row heights...
-    if (this.get('hasUniformRowHeights')) {
-      rowHeight = this.get('rowHeight') || 0 ;
-      idx = Math.floor(offset / rowHeight) ;
-      rowOffset = idx * rowHeight ;
-    // otherwise, use the rowOffsets cache...
-    } else {
-      // get caches
-      var offsets = this._list_rowOffsets;
-      if (!offsets) offsets = this._list_rowOffsets = [] ;
-      
-      // console.log('offset of pointer is %@'.fmt(offset));
-      // console.log('offsets are %@'.fmt(offsets.join(', ')));
-      
-      // OK, now try the fast path...if undefined, loop backwards until we
-      // find an offset that IS cached...
-      var len = offsets.length, cur = len, ret;
-      
-      // if the cached value was undefined, loop backwards through the offsets
-      // hash looking for a cached value to start from
-      while (cur&gt;0) {
-        ret = offsets[--cur];
-        if (ret &lt; offset) break ;
-      }
-      
-      rowOffset = offset[cur] ;
-      rowHeight = this._list_heightForRowAtContentIndex(cur) ;
-      
-      // console.log('rowHeight is %@'.fmt(rowHeight));
-      
-      idx = cur ;
+    // walk backwards until top of row is before or at bottom edge
+    while(end&gt;=start &amp;&amp; offset&gt;=bottom) {
+      end-- ;
+      offset -= this.rowHeightForContentIndex(end);
     }
     
-    // find the percent through the row...
-    var percentage = ((offset - rowOffset) / rowHeight) ;
-    
-    // console.log('percentage is %@'.fmt(percentage));
-    
-    // if the dropOperation is SC.DROP_ON and we are in the center 60%
-    // then return the current item.
-    if (dropOperation === SC.DROP_ON) {
-      if (percentage &gt; 0.80) idx++ ;
-      if ((percentage &gt;= 0.20) &amp;&amp; (percentage &lt;= 0.80)) {
-        retOp = SC.DROP_ON;
-      }
-    } else {
-      if (percentage &gt; 0.45) idx++ ;
+    // go forwards until bottom of row is after bottom edge
+    offset += this.rowHeightForContentIndex(end);
+    while(end&lt;len &amp;&amp; offset&lt;=bottom) {
+      offset += this.rowHeightForContentIndex(end);
+      end++ ;
     }
     
-    if (idx !== this._idx || retOp !== this._retOp) {
-      // console.log('insertionIndex is %@, op is %@'.fmt(idx, retOp));
-      this._idx = idx ;
-      this._retOp = retOp ;
-    }
+    end++; // end should be after start
+    if (end&lt;start) end = start;
+    if (end&gt;len) end = len ;
     
-    // console.log('[ret, retOp] is [%@, %@]'.fmt(ret, retOp));
-    return [idx, retOp] ;
-  }
+    // convert to IndexSet and return
+    return SC.IndexSet.create(start, end-start);
+  },
+  
+  // ..........................................................
+  // INTENRAL SUPPORT
+  // 
+
+  init: function() {
+    sc_super();
+    this._sclv_rowDelegateDidChange();
+  }  
   
 });</diff>
      <filename>frameworks/desktop/views/list.js</filename>
    </modified>
    <modified>
      <diff>@@ -5,8 +5,6 @@
 // License:   Licened under MIT license (see license.js)
 // ==========================================================================
 
-sc_require('mixins/collection_item');
-
 SC.LIST_ITEM_ACTION_CANCEL = 'sc-list-item-cancel-action';
 SC.LIST_ITEM_ACTION_REFRESH = 'sc-list-item-cancel-refresh';
 SC.LIST_ITEM_ACTION_EJECT = 'sc-list-item-cancel-eject';
@@ -25,14 +23,14 @@ SC.LIST_ITEM_ACTION_EJECT = 'sc-list-item-cancel-eject';
   @extends SC.View
   @extends SC.Control
   @extends SC.InlineEditorDelegate
-  @extends SC.CollectionItem
   @extends SC.Editable
+  @extends SC.StaticLayout
   @since SproutCore 1.0
 */
 SC.ListItemView = SC.View.extend(
+    SC.StaticLayout,
     SC.Control,
     SC.InlineEditorDelegate,
-    SC.CollectionItem,
 /** @scope SC.ListItemView.prototype */ {
   
   classNames: ['sc-list-item-view'],
@@ -120,6 +118,24 @@ SC.ListItemView = SC.View.extend(
   */
   isEditing: NO,
   
+  /**
+    Indent to use when rendering a list item with an outline level &gt; 0.  The
+    left edge of the list item will be indented by this amount for each 
+    outline level.
+  */
+  outlineIndent: 16,
+  
+  /**
+    Outline level for this list item.  Usually set by the collection view.
+  */
+  outlineLevel: 0,
+  
+  /**
+    Disclosure state for this list item.  Usually set by the collection view
+    when the list item is created.
+  */
+  disclosureState: SC.LEAF_NODE,
+  
   contentPropertyDidChange: function() {
     if (this.get('isEditing')) this.discardEditing() ;
     this.displayDidChange();
@@ -135,15 +151,29 @@ SC.ListItemView = SC.View.extend(
     @returns {void}
   */
   render: function(context, firstTime) {
-    var content = this.get('content') ;
-    var del = this.displayDelegate ;
-    var key, value ;
+    var content = this.get('content'),
+        del     = this.displayDelegate,
+        level   = this.get('outlineLevel'),
+        indent  = this.get('outlineIndent'),
+        key, value, working ;
+    
+    // outline level wrapper
+    working = context.begin(&quot;div&quot;).addClass(&quot;sc-outline&quot;);
+    if (level&gt;=0 &amp;&amp; indent&gt;0) working.addStyle(&quot;left&quot;, indent*(level+1));
+    
+    // handle disclosure triangle
+    value = this.get('disclosureState');
+    if (value !== SC.LEAF_NODE) {
+      this.renderDisclosure(working, value);
+      context.addClass('has-disclosure');
+    }
+    
     
     // handle checkbox
     key = this.getDelegateProperty('contentCheckboxKey', del) ;
     if (key) {
       value = content ? (content.get ? content.get(key) : content[key]) : NO ;
-      this.renderCheckbox(context, value);
+      this.renderCheckbox(working, value);
       context.addClass('has-checkbox');
     }
     
@@ -152,7 +182,7 @@ SC.ListItemView = SC.View.extend(
       key = this.getDelegateProperty('contentIconKey', del) ;
       value = (key &amp;&amp; content) ? (content.get ? content.get(key) : content[key]) : null ;
       
-      this.renderIcon(context, value);
+      this.renderIcon(working, value);
       context.addClass('has-icon');
     }
     
@@ -161,18 +191,18 @@ SC.ListItemView = SC.View.extend(
     value = (key &amp;&amp; content) ? (content.get ? content.get(key) : content[key]) : content ;
     if (value &amp;&amp; SC.typeOf(value) !== SC.T_STRING) value = value.toString();
     if (this.get('escapeHTML')) value = SC.RenderContext.escapeHTML(value);
-    this.renderLabel(context, value);
+    this.renderLabel(working, value);
     
     // handle unread count
     key = this.getDelegateProperty('contentUnreadCountKey', del) ;
     value = (key &amp;&amp; content) ? (content.get ? content.get(key) : content[key]) : null ;
-    if (!SC.none(value) &amp;&amp; (value !== 0)) this.renderCount(context, value) ;
+    if (!SC.none(value) &amp;&amp; (value !== 0)) this.renderCount(working, value) ;
     
     // handle action 
     key = this.getDelegateProperty('listItemActionProperty', del) ;
     value = (key &amp;&amp; content) ? (content.get ? content.get(key) : content[key]) : null ;
     if (value) {
-      this.renderAction(context, value);
+      this.renderAction(working, value);
       context.addClass('has-action');
     }
     
@@ -180,9 +210,35 @@ SC.ListItemView = SC.View.extend(
     if (this.getDelegateProperty('hasContentBranch', del)) {
       key = this.getDelegateProperty('contentIsBranchKey', del);
       value = (key &amp;&amp; content) ? (content.get ? content.get(key) : content[key]) : NO ;
-      this.renderBranch(context, value);
+      this.renderBranch(working, value);
       context.addClass('has-branch');
     }
+    
+    context = working.end();
+  },
+  
+  /**
+    Adds a disclosure triangle with the appropriate display to the content.
+    This method will only be called if the disclosure state of the view is
+    something other than SC.LEAF_NODE.
+
+    @param {SC.RenderContext} context the render context
+    @param {Boolean} state YES, NO or SC.MIXED_STATE
+    @returns {void}
+  */
+  renderDisclosure: function(context, state) {
+    var key = (state === SC.BRANCH_OPEN) ? &quot;open&quot; : &quot;closed&quot;,
+        cache = this._scli_disclosureHtml,
+        html, tmp;
+        
+    if (!cache) cache = this.constructor.prototype._scli_disclosureHtml = {};
+    html = cache[key];
+
+    if (!html) {
+      html = cache[key] = '&lt;img src=&quot;%@&quot; class=&quot;disclosure button %@&quot; /&gt;'.fmt(SC.BLANK_IMAGE_URL, key);
+    }
+    
+    context.push(html);
   },
   
   /**
@@ -195,20 +251,31 @@ SC.ListItemView = SC.View.extend(
     @returns {void}
   */
   renderCheckbox: function(context, state) {
-    context = context.begin('a').attr('href', 'javascript:;')
-      .classNames(SC.CheckboxView.prototype.classNames);
     
-    // set state on html
-    if (state === SC.MIXED_STATE) {
-      context.addClass('mixed');
-    } else context.setClass('sel', state);
+    var key = (state === SC.MIXED_STATE) ? &quot;mixed&quot; : state ? &quot;sel&quot; : &quot;nosel&quot;,
+        cache = this._scli_checkboxHtml,
+        html, tmp;
+        
+    if (!cache) cache = this.constructor.prototype._scli_checkboxHtml = {};
+    html = cache[key];
     
-    // now add inner content.  note we do not add a real checkbox because
-    // we don't want to have to setup a change observer on it.
-    context.push('&lt;img src=&quot;', SC.BLANK_IMAGE_URL, '&quot; class=&quot;button&quot; /&gt;');
+    if (!html) {
+      tmp = SC.RenderContext('a').attr('href', 'javascript:;')
+        .classNames(SC.CheckboxView.prototype.classNames);
+
+      // set state on html
+      if (state === SC.MIXED_STATE) tmp.addClass('mixed');
+      else tmp.setClass('sel', state);
+
+      // now add inner content.  note we do not add a real checkbox because
+      // we don't want to have to setup a change observer on it.
+      tmp.push('&lt;img src=&quot;', SC.BLANK_IMAGE_URL, '&quot; class=&quot;button&quot; /&gt;');
+
+      // apply edit
+      html = cache[key] = tmp.join();
+    }
     
-    // apply edit
-    context.end();
+    context.push(html);
   },
   
   /** 
@@ -329,6 +396,15 @@ SC.ListItemView = SC.View.extend(
   },
   
   /** @private 
+    Returns YES if the list item has a disclosure triangle and the event 
+    occurred inside of it.
+  */
+  _isInsideDisclosure: function(evt) {
+    if (this.get('disclosureSate')===SC.LEAF_NODE) return NO;
+    return this._isInsideElementWithClassName('disclosure', evt);
+  },
+  
+  /** @private 
   mouseDown is handled only for clicks on the checkbox view or or action
   button.
   */
@@ -339,43 +415,80 @@ SC.ListItemView = SC.View.extend(
       this._isMouseDownOnCheckbox = YES ;
       this._isMouseInsideCheckbox = YES ;
       return YES ; // listItem should handle this event
-    }  
+
+    } else if (this._isInsideDisclosure(evt)) {
+      this._addDisclosureActiveState();
+      this._isMouseDownOnDisclosure = YES;
+      this._isMouseInsideDisclosure = YES ;
+      return YES;
+    }
+    
     return NO ; // let the collection view handle this event
   },
   
   mouseUp: function(evt) {
-   var ret= NO ;
-   // if mouse was down in checkbox -- then handle mouse up, otherwise 
-   // allow parent view to handle event.
-   if (this._isMouseDownOnCheckbox) {
+    var ret= NO, del, checkboxKey, content, state, idx, set;
+    
+    // if mouse was down in checkbox -- then handle mouse up, otherwise 
+    // allow parent view to handle event.
+    if (this._isMouseDownOnCheckbox) {
    
-     // update only if mouse inside on mouse up...
-     if (this._isInsideCheckbox(evt)) {
-       var del = this.displayDelegate ;
-       var checkboxKey = this.getDelegateProperty('contentCheckboxKey', del) ;
-       var content = this.get('content') ;
-       if (content &amp;&amp; content.get) {
-         var value = content.get(checkboxKey) ;
-         value = (value === SC.MIXED_STATE) ? YES : !value ;
-         content.set(checkboxKey, value) ; // update content
-         this.displayDidChange(); // repaint view...
-       }
-     }
+      // update only if mouse inside on mouse up...
+      if (this._isInsideCheckbox(evt)) {
+        del = this.displayDelegate ;
+        checkboxKey = this.getDelegateProperty('contentCheckboxKey', del);
+        content = this.get('content') ;
+        if (content &amp;&amp; content.get) {
+          var value = content.get(checkboxKey) ;
+          value = (value === SC.MIXED_STATE) ? YES : !value ;
+          content.set(checkboxKey, value) ; // update content
+          this.displayDidChange(); // repaint view...
+        }
+      }
+ 
+      this._removeCheckboxActiveState() ;
+      ret = YES ;
+    
+    // if mouse as down on disclosure -- handle mosue up.  otherwise pass on
+    // to parent.
+    } else if (this._isMouseDownOnDisclosure) {
+      if (this._isInsideDisclosure(evt)) {
+        state = this.get('disclosureState');
+        idx   = this.get('contentIndex');
+        set   = idx ? SC.IndexSet.create(idx) : null;
+        del = this.get('displayDelegate');
+        
+        if (state === SC.BRANCH_OPEN) {
+          if (set &amp;&amp; del &amp;&amp; del.collapse) del.collapse(set);
+          else this.set('disclosureState', SC.BRANCH_CLOSED);
+          this.displayDidChange();
+          
+        } else if (state === SC.BRANCH_CLOSED) {
+          if (set &amp;&amp; del &amp;&amp; del.expand) del.expand(set);
+          else this.set('disclosureState', SC.BRANCH_OPEN);
+          this.displayDidChange();
+        }
+      }
      
-     this._removeCheckboxActiveState() ;
-     ret = YES ;
-   } 
+      this._removeDisclosureActiveState();
+      ret = YES ;
+    }
    
-   // clear cached info
-   this._isMouseInsideCheckbox = this._isMouseDownOnCheckbox = NO ;
-   return ret ;
+    // clear cached info
+    this._isMouseInsideCheckbox = this._isMouseDownOnCheckbox = NO ;
+    this._isMouseDownOnDisclosure = this._isMouseInsideDisclosure = NO ;
+    return ret ;
   },
   
   mouseExited: function(evt) {
    if (this._isMouseDownOnCheckbox) {
      this._removeCheckboxActiveState() ;
      this._isMouseInsideCheckbox = NO ;
-   }  
+     
+   } else if (this._isMouseDownOnDisclosure) {
+     this._removeDisclosureActiveState();
+     this._isMouseInsideDisclosure = NO ;
+   }
    return NO ;
   },
   
@@ -383,7 +496,11 @@ SC.ListItemView = SC.View.extend(
    if (this._isMouseDownOnCheckbox) {
      this._addCheckboxActiveState() ;
      this._isMouseInsideCheckbox = YES ;
-   }  
+     
+   } else if (this._isMouseDownOnDisclosure) {
+     this._addDisclosureActiveState();
+     this._isMouseInsideDisclosure = YES;
+   }
    return NO ;
   },
   
@@ -395,6 +512,15 @@ SC.ListItemView = SC.View.extend(
   _removeCheckboxActiveState: function() {
    this.$('.sc-checkbox-view').removeClass('active');
   },
+
+  _addDisclosureActiveState: function() {
+   var enabled = this.get('isEnabled');
+   this.$('img.disclosure').setClass('active', enabled);
+  },
+  
+  _removeDisclosureActiveState: function() {
+   this.$('img.disclosure').removeClass('active');
+  },
   
   /**
   Returns true if a click is on the label text itself to enable editing.</diff>
      <filename>frameworks/desktop/views/list_item.js</filename>
    </modified>
    <modified>
      <diff>@@ -14,7 +14,7 @@ SC.BENCHMARK_MENU_ITEM_RENDER = YES ;
   @extends SC.ButtonView
   @since SproutCore 1.0
 */
-SC.MenuItemView = SC.ButtonView.extend(
+SC.MenuItemView = SC.ButtonView.extend( SC.ContentDisplay,
 /** @scope SC.MenuItemView.prototype */{
   classNames: ['sc-menu-item'],
   tagName: 'div',
@@ -167,8 +167,8 @@ SC.MenuItemView = SC.ButtonView.extend(
   */
   displayProperties: ['contentValueKey', 'contentIconKey', 'shortCutKey',
                   'contentIsBranchKey','isCheckboxChecked','itemHeight',
-                   'subMenu'],
-
+                   'subMenu','isEnabled','content'],
+  contentDisplayProperties: 'title value icon separator action checkbox shortcut branchItem subMenu'.w(),
   /**
     Fills the passed html-array with strings that can be joined to form the
     innerHTML of the receiver element.  Also populates an array of classNames
@@ -183,7 +183,6 @@ SC.MenuItemView = SC.ButtonView.extend(
       var bkey = '%@.render'.fmt(this) ;
       SC.Benchmark.start(bkey) ;
     }
-	
     var content = this.get('content') ;
     var del = this.displayDelegate ;
     var key, val ;
@@ -193,7 +192,9 @@ SC.MenuItemView = SC.ButtonView.extend(
     var itemHeight = this.get('itemHeight') || 20 ;
     this.set('itemWidth',itemWidth) ;
     this.set('itemHeight',itemHeight) ;
-   
+    
+    if(!this.get('isEnabled'))
+      context.addClass('disabled') ;
     //handle seperator    
     ic = context.begin('a').attr('href', 'javascript: ;') ;   
     key = this.getDelegateProperty('isSeparatorKey', del) ;
@@ -201,9 +202,10 @@ SC.MenuItemView = SC.ButtonView.extend(
     if (val) {
       ic = ic.begin('span').addClass('separator') ;
       ic = ic.end() ;
+      if (SC.BENCHMARK_MENU_ITEM_RENDER) SC.Benchmark.end(bkey) ;
       return ;
     } else {
-      //handle checkbox
+      // handle checkbox
       key = this.getDelegateProperty('contentCheckboxKey', del) ;
       if (key) {
         val = content ? (content.get ? content.get(key) : content[key]) : NO ;</diff>
      <filename>frameworks/desktop/views/menu_item.js</filename>
    </modified>
    <modified>
      <diff>@@ -137,7 +137,7 @@ SC.ScrollerView = SC.View.extend({
   
   didCreateLayer: function() {
     // console.log('%@.didCreateLayer called'.fmt(this));
-    var callback = this._sc_scroller_armScrollTimer ;
+    var callback = this._sc_scroller_scrollDidChange ;
     SC.Event.add(this.$(), 'scroll', this, callback) ;
     
     // set scrollOffset first time
@@ -157,22 +157,28 @@ SC.ScrollerView = SC.View.extend({
   
   willDestroyLayer: function() {
     // console.log('%@.willDestroyLayer()'.fmt(this));
-    var callback = this._sc_scroller_armScrollTimer ;
+    var callback = this._sc_scroller_scrollDidChange ;
     SC.Event.remove(this.$(), 'scroll', this, callback) ;
   },
-  
+
+  // after 50msec, fire event again
   _sc_scroller_armScrollTimer: function() {
     if (!this._sc_scrollTimer) {
       SC.RunLoop.begin();
       var method = this._sc_scroller_scrollDidChange ;
-      this._sc_scrollTimer = this.invokeLater(method, 1) ;
+      this._sc_scrollTimer = this.invokeLater(method, 50) ;
       SC.RunLoop.end();
     }
   },
   
   _sc_scroller_scrollDidChange: function() {
-    // console.log('%@._sc_scroller_scrollDidChange called'.fmt(this));
-    this._sc_scrollTimer = null ; // clear so we can fire again
+    
+    var now = Date.now(), last = this._sc_lastScroll;
+    if (last &amp;&amp; (now-last)&lt;50) return this._sc_scroller_armScrollTimer();
+    this._sc_scrollTimer = null; 
+    this._sc_lastScroll = now;
+
+    SC.RunLoop.begin();
     
     if (!this.get('isEnabled')) return ; // nothing to do.
     
@@ -188,6 +194,8 @@ SC.ScrollerView = SC.View.extend({
     }
     
     this.set('value', loc + this.get('minimum')) ;
+    
+    SC.RunLoop.end();
   },
   
   /** @private */</diff>
      <filename>frameworks/desktop/views/scroller.js</filename>
    </modified>
    <modified>
      <diff>@@ -12,1106 +12,30 @@ SC.BENCHMARK_SOURCE_LIST_VIEW = YES ;
 
 /** @class
   
-  Displays a source list like the source list in iTunes.
+  Displays a source list like the source list in iTunes.  SourceList views
+  are very similar to ListView's but come preconfigured with the correct
+  appearance and default behaviors of a source list.
   
   @extends SC.ListView
-  @author Charles Jolley
-  @author Erich Ocean
-  @version 1.0
-  @since 0.9
+  @since SproutCore 1.0
 */
 SC.SourceListView = SC.ListView.extend(
 /** @scope SC.SourceListView.prototype */ {
-  
-  /**
-    The view class to use when displaying item views in groups.
-    
-    If the groupBy property is not null, then the collection view will create
-    an instance of this view class with the item views that belong to the 
-    group as child nodes for each distinct group value it encounters.
-    
-    If groupBy is null, then this property will not be used.  The default 
-    class provided here simply displays the group value in an H1 tag.
-    
-    @property {SC.View}
-  */
-  exampleGroupView: SC.SourceListGroupView,
-  
-  // ..........................................................
-  // GROUP HEIGHT SUPPORT
-  // 
-  
-  /**
-    Set to YES if your list view should have uniform group heights.  This will
-    enable an optimization that avoids inspecting actual group objects 
-    when calculating the size of the view.
-    
-    The default version of this property is set to YES unless you set a 
-    delegate or a rowHeightKey.
-  */
-  hasUniformGroupHeights: YES,
-  
-  /** 
-    The common group height for source list group views.
-    
-    If you set this property, then the ListView will be able to use this
-    property to perform absolute layout of its children and to minimize t
-    number of actual views it has to create.
-    
-    The value should be an integer expressed in pixels.
-    
-    You can alternatively set either the groupHeightKey or implement
-    the collectionViewHeightForGroupAtGroupIndex() delegate method.
-  */
-  groupHeight: 32,
-  
-  /**
-    If set, this key will be used to calculate the row height for a given
-    content object.
-  */
-  groupHeightKey: null,
-  
-  /**
-    This optional delegate method will be called for each group in your 
-    content groups, giving you a chance to decide what group height to use for
-    the group at the named groupIndex.
-    
-    The default version will return either the fixed groupHeight you 
-    specified or will lookup the group height on the content's groups object 
-    using the groupHeightKey.
-    
-    @params {SC.CollectionView} the requesting collection view
-    @params {Number} groupIndex the index into the group array
-    @returns {Number} groupHeight
-  */
-  collectionViewHeightForGroupAtIndex: function(collectionView, groupIndex) {
-    // console.log('%@.collectionViewHeightForGroupAtIndex(collectionView=%@, groupIndex=%@)'.fmt(this, collectionView, groupIndex));
-    if (!this.groupHeightKey) return this.get('groupHeight') ;
-    
-    var group, groups = this.getPath('content.groups') ;
-    if (groups) group = groups.objectAt(groupIndex) ;
-    
-    return group ?
-      group.get(this.get('groupHeightKey')) :
-      this.get('groupHeight') ;
-  },
-  
-  /**
-    Calculates the offset for the row at the specified index, taking into 
-    account the height of the groupView for that contentIndex.  Based on the 
-    current setting this may compute the group heights of groups for previous 
-    items or it will simply do some math...
-  */
-  offsetForRowAtContentIndex: function(contentIndex) {
-    // get normal contentIndex offset
-    var ret = sc_super() ;
-    
-    // now account for group view heights, using simple math if possible...
-    if (this.get('hasUniformGroupHeights')) {
-      var groupHeight = this.get('groupHeight') ;
-      
-      var groups = this.getPath('content.groups') || [] ;
-      var group, itemRange, len = groups.get('length') ;
-      for (var idx=0; idx&lt;len; ++idx) {
-        group = groups.objectAt(idx) ;
-        ret += groupHeight ;
-        if (SC.valueInRange(contentIndex, group.itemRange)) break ;
-      }
-    
-    // otherwise, use the groupOffsets cache...
-    } else {
-      // find the index of the group for contentIndex
-      var groupIndex = -1 ;
-      var groups = this.getPath('content.groups') || [] ;
-      var group, itemRange, len = groups.get('length') ;
-      for (var idx=0; idx&lt;len; ++idx) {
-        group = groups.objectAt(idx) ;
-        if (SC.valueInRange(contentIndex, group.itemRange)) {
-          groupIndex = idx ;
-          break ;
-        }
-      }
-      
-      // if we can't find the group, assume all groups
-      // (this resuts in the correct height when the offset of one-past-the-
-      // last-contentIndex is requested)
-      if (groupIndex === -1) groupIndex = len ;
-      
-      // get caches
-      var offsets = this._source_list_groupOffsets;
-      if (!offsets) offsets = this._source_list_groupOffsets = [] ;
-      
-      // OK, now try the fast path...if undefined, loop backwards until we
-      // find an offset that IS cached...
-      var len2 = offsets.length, cur = groupIndex, height, ret2;
-      
-      // get the cached offset.  Note that if the requested index is longer 
-      // than the length of the offsets cache, then just assume the value is
-      // undefined.  We don't want to accidentally read an old value...
-      if (groupIndex &lt; len2) {
-        ret2 = offsets[cur];
-      } else {
-        ret2 = undefined ;
-        cur = len2; // start search at current end of offsets...
-      }
-      
-      // if the cached value was undefined, loop backwards through the offsets
-      // hash looking for a cached value to start from
-      while ((cur&gt;0) &amp;&amp; (ret2===undefined)) ret2 = offsets[--cur] ;
-      
-      // now, work our way forward, building the cache of offsets.  Use
-      // cached heights...
-      if (ret2===undefined) ret2 = offsets[cur] = 0 ;
-      while (cur &lt; groupIndex) {
-        // get height...recache if needed....
-        height = this._source_list_heightForGroupAtIndex(cur) ;
-        
-        // console.log('index %@ has height %@'.fmt(cur, height));
-        
-        // add to ret2 and save in cache
-        ret2 = ret2 + height ;
-        
-        cur++; // go to next offset
-        offsets[cur] = ret2 ;
-      }
-      
-      // now update contentIndex offset
-      ret += ret2 ;
-      
-      // also add in the offset for the contentIndex's own group as well,
-      // if it really has a group...
-      if (groupIndex != len) {
-        ret += this._source_list_heightForGroupAtIndex(groupIndex) ;
-      }
-    }
-    
-    return ret ;
-  },
-  
+
   /**
-    Calculates the height for the group at groupIndex.  This method will
-    perform some simple math if hasUniformGroupHeights is YES.  Otherwise,
-    it will consult the collection view delegate to compute the group heights.
-  */
-  heightForGroupAtIndex: function(groupIndex) {
-    if (this.get('hasUniformGroupHeights')) {
-      return this.get('groupHeight') ;
-    } else return this._source_list_heightForGroupAtIndex(groupIndex) ;
-  },
-  
-  /** @private
-    By-passes the uniform row heights check.  Makes offsetForRow... a little
-    faster.
-  */
-  _source_list_heightForGroupAtIndex: function(groupIndex) {
-    // console.log('%@._source_list_heightForGroupAtIndex(groupIndex=%@)'.fmt(this, groupIndex)) ;
-    var heights = this._source_list_groupHeights;
-    if (!heights) heights = this._source_list_groupHeights = [] ;
-    
-    var height = (groupIndex &lt; heights.length) ?
-      heights[groupIndex] :
-      undefined ;
-    if (height===undefined) {
-      height = heights[groupIndex] = this.invokeDelegateMethod(this.delegate, 'collectionViewHeightForGroupAtIndex', this, groupIndex) || 0 ;
-    }
-    
-    // console.log('groupIndex=%@, height=%@'.fmt(groupIndex, height));
-    
-    return height ;
-  },
-  
-  // ..........................................................
-  // RENDERING
-  // 
-  
-  createExampleGroupView: function() {
-    var ExampleGroupView = this.get('exampleGroupView') ;
-    
-    if (ExampleGroupView) {
-      return ExampleGroupView.create({
-        classNames: ['sc-collection-group'],
-        owner: this,
-        displayDelegate: this,
-        parentView: this,
-        isVisible: YES,
-        isMaterialized: YES
-      });
-    } else throw &quot;You must define an exampleGroupView class to render collection groups with&quot; ;
-  },
-  
-  render: function(context, firstTime) {
-    var isDirty = this.get('isDirty') ; // sc_super() will clear this...
-    sc_super() ; // render content items
-    
-    // render groups
-    if (SC.BENCHMARK_RENDER) {
-      var bkey = '%@.render(groups)'.fmt(this) ;
-      SC.Benchmark.start(bkey);
-    }
-    this.beginPropertyChanges() ; // avoid sending notifications
-    
-    var content = SC.makeArray(this.get('content')) ;
-    var groups = content.get('groups') || [] ;
-    var oldRange = this._oldNowShowingGroupRange ;
-    var range = SC.cloneRange(this.get('nowShowingRange')) ;
-    this._oldNowShowingGroupRange = SC.cloneRange(range) ;
-    var key, groupView = this.createExampleGroupView(), c, g ;
-    var range2 ; // only used if the old range fits inside the new range
-    var idx, end, groupId, groupIndex ;
-    
-    // keep track of groups we've got rendered
-    var groupSet = this._groupSet ;
-    if (!groupSet) groupSet = this._groupSet = [] ;
-    
-    // figure out which groups should be rendered
-    var newGroupSet = [] ;
-    idx = range.start ;
-    end = range.start + range.length ;
-    while (idx &lt; end) {
-      // is the group for this contentIndex currently being rendered?
-      groupIndex = this.groupIndexForContentIndex(idx) ;
-      if (groupIndex &gt;= 0) {
-        newGroupSet.push(groupIndex) ;
-      }
-      ++idx ;
-    }
-    
-    if (SC.ENABLE_COLLECTION_PARTIAL_RENDER) {
-      // used for santity checks during debugging
-      if (SC.SANITY_CHECK_PARTIAL_RENDER) var maxLen = range.length ;
-      
-      if (SC.DEBUG_PARTIAL_RENDER) {
-        console.log('oldRange = ') ;
-        console.log(oldRange) ;
-        console.log('range = ') ;
-        console.log(range) ;
-      }
-      
-      // if we're dirty, redraw everything visible
-      // (selection changed, content changed, etc.)
-      if (isDirty) {
-        // console.log('doing a full render') ;
-        groupSet.length = 0 ; // full render
-        
-      // else, only redaw objects we haven't previously drawn
-      } else if (oldRange) {
-        // ignore ranges that don't overlap above..
-        if (range.start &gt;= oldRange.start + oldRange.length) {
-          // console.log('doing a full render') ;
-          groupSet.length = 0 ; // full render
-          
-        // and below...
-        } else if (range.start + range.length &lt;= oldRange.start) {
-          // console.log('doing a full render') ;
-          groupSet.length = 0 ; // full render
-          
-        // okay, the ranges do overlap. are they equal?
-        } else if (SC.rangesEqual(oldRange, range)) {
-          range = SC.EMPTY_RANGE ; // nothing to render
-          
-        // nope, is the old range inside the new range?
-        } else if (range.start &lt;= oldRange.start &amp;&amp; range.start + range.length &gt;= oldRange.start + oldRange.length) {
-          // need to render two ranges...all pre-existing views are valid
-          context.updateMode = SC.MODE_APPEND ;
-          range2 = { start: oldRange.start + oldRange.length, length: (range.start + range.length) - (oldRange.start + oldRange.length) } ;
-          range.length = oldRange.start - range.start ;
-          
-        // nope, is the new range inside the old range?
-        } else if (range.start &gt;= oldRange.start &amp;&amp; range.start + range.length &lt;= oldRange.start + oldRange.length) {        
-          // need to remove unused childNodes at both ends, start with bottom...
-          idx = oldRange.start ;
-          end = range.start ;
-          while (idx &lt; end) {
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('looping on bottom range');
-            // is the group for this contentIndex currently being rendered?
-            groupIndex = this.groupIndexForContentIndex(idx) ;
-            if (groupIndex &gt;= 0 &amp;&amp; (newGroupSet.indexOf(groupIndex) === -1)) {
-              groupId = groupSet[groupIndex] ;
-              if (groupId) context.remove(groupId) ;
-              if (SC.DEBUG_PARTIAL_RENDER) console.log('deleting group at index %@'.fmt(groupIndex));
-              delete groupSet[groupIndex] ;
-            }
-            ++idx ;
-          }
-          
-          // now remove unused childNodes at the top of the range...
-          idx = range.start + range.length ;
-          end = oldRange.start + oldRange.length ;
-          while (idx &lt; end) {
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('looping on top range');
-            // is the group for this contentIndex currently being rendered?
-            groupIndex = this.groupIndexForContentIndex(idx) ;
-            if (groupIndex &gt;= 0 &amp;&amp; (newGroupSet.indexOf(groupIndex) === -1)) {
-              groupId = groupSet[groupIndex] ;
-              if (groupId) context.remove(groupId) ;
-              if (SC.DEBUG_PARTIAL_RENDER) console.log('deleting group at index %@'.fmt(groupIndex));
-              delete groupSet[groupIndex] ;
-            }
-            ++idx ;
-          }
-          
-          range = SC.EMPTY_RANGE ; // nothing to render
-          
-        // nope, is the new range lower than the old range?
-        } else if (range.start &lt; oldRange.start) {
-          context.updateMode = SC.MODE_APPEND ;
-          
-          // need to remove unused childNodes at the top of the old range
-          idx = range.start + range.length ;
-          end = oldRange.start + oldRange.length ;
-          while (idx &lt; end) {
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('looping on top only');
-            // is the group for this contentIndex currently being rendered?
-            groupIndex = this.groupIndexForContentIndex(idx) ;
-            if (groupIndex &gt;= 0 &amp;&amp; (newGroupSet.indexOf(groupIndex) === -1)) {
-              groupId = groupSet[groupIndex] ;
-              if (groupId) context.remove(groupId) ;
-              if (SC.DEBUG_PARTIAL_RENDER) console.log('deleting group at index %@'.fmt(groupIndex));
-              delete groupSet[groupIndex] ;
-            }
-            ++idx ;
-          }
-          
-          range.length = Math.min(range.length, oldRange.start - range.start) ;
-          
-        // nope, so the new range is higher than the old range
-        } else {
-          context.updateMode = SC.MODE_APPEND ;
-          
-          // need to remove unused childNodes at the bottom of the old range
-          idx = oldRange.start ;
-          end = range.start ;
-          while (idx &lt; end) {
-            if (SC.DEBUG_PARTIAL_RENDER) console.log('looping on bottom only');
-            // is the group for this contentIndex currently being rendered?
-            groupIndex = this.groupIndexForContentIndex(idx) ;
-            if (groupIndex &gt;= 0 &amp;&amp; (newGroupSet.indexOf(groupIndex) === -1)) {
-              groupId = groupSet[groupIndex] ;
-              if (groupId) context.remove(groupId) ;
-              if (SC.DEBUG_PARTIAL_RENDER) console.log('deleting group at index %@'.fmt(groupIndex));
-              delete groupSet[groupIndex] ;
-            }
-            ++idx ;
-          }
-          
-          end = range.start + range.length ;
-          range.start = oldRange.start + oldRange.length ;
-          range.length = end - range.start ;
-        }
-      }
-      
-      if (SC.SANITY_CHECK_PARTIAL_RENDER) {
-        if (range.length &lt; 0) throw &quot;range.length is &quot; + range.length ;
-        if (range.length &gt; maxLen) throw &quot;range.length is &quot; + range.length + ', max length is ' + maxLen ;
-        if (range.start &lt; 0) throw &quot;range.start is &quot; + range.start ;
-        if (range2) {
-          if (range2.length &lt; 0) throw &quot;range2.length is &quot; + range2.length ;
-          if (range2.length &gt; maxLen) throw &quot;range2.length is &quot; + range2.length + ', max length is ' + maxLen ;
-          if (range2.start &lt; 0) throw &quot;range2.start is &quot; + range2.start ;
-        }
-      }
-    
-      if (SC.DEBUG_PARTIAL_RENDER) {
-        console.log('rendering = ') ;
-        console.log(range) ;
-        if (range2) {
-          console.log('also rendering = ') ;
-          console.log(range2) ;
-        }
-      }
-    }
-    
-    idx = SC.maxRange(range) ;
-    
-    var baseKey = SC.guidFor(this) + '_' ;
-    var guids = this._groupViewGuids, guid;
-    if (!guids) this._groupViewGuids = guids = {};
-    
-    // TODO: Use SC.IndexSet, not separate ranges, once it's ready.
-    // This will also make it possible to do partial updates during content
-    // and selection changes. Now we always do a full update.
-    
-    var groupLayout ;
-    while (--idx &gt;= range.start) {
-      groupIndex = this.groupIndexForContentIndex(idx) ;
-      
-      if (groupIndex == -1) continue ;
-      // don't re-render already rendered group views
-      if (groupSet[groupIndex]) continue ;
-      
-      g = groups.objectAt(groupIndex) ;
-      
-      // use cache of group view guids to avoid creating temporary objects
-      guid = SC.guidFor(g);
-      if (!(key = guids[guid])) key = guids[guid] = baseKey+guid;
-      
-      // console.log('groupSet[groupIndex]=%@'.fmt(groupSet[groupIndex])) ;
-      
-      if (SC.DEBUG_PARTIAL_RENDER) console.log('rendering group(%@) at index %@'.fmt(g.get('value'), idx));
-      
-      groupSet[groupIndex] = key ;
-      groupView.set('content', g) ;
-      groupView.layerId = key ; // cannot use .set, layerId is RO
-      
-      // get the layout for the group's first itemView
-      groupLayout = this.itemViewLayoutAtContentIndex(g.get('itemRange').start) ;
-      
-      // derive the correct groupView layout
-      groupLayout.height = this.heightForGroupAtIndex(groupIndex) ;
-      groupLayout.top -= groupLayout.height ;
-      
-      // console.log(groupLayout) ;
-      
-      groupView.adjust(groupLayout) ;
-      context = context.begin(groupView.get('tagName')) ;
-      groupView.prepareContext(context, YES) ;
-      context = context.end() ;
-    }
-    
-    if (range2) {
-      idx = SC.maxRange(range2) ;
-      while (--idx &gt;= range2.start) {
-        groupIndex = this.groupIndexForContentIndex(idx) ;
-        if (groupIndex == -1) continue ;
-        
-        g = groups.objectAt(groupIndex) ;
-        
-        // use cache of group view guids to avoid creating temporary objects
-        guid = SC.guidFor(g);
-        if (!(key = guids[guid])) key = guids[guid] = baseKey+guid;
-        
-        // don't re-render already rendered group views
-        if (groupSet[groupIndex]) continue ;
-        
-        if (SC.DEBUG_PARTIAL_RENDER) console.log('rendering group(%@) at index %@'.fmt(g.get('value'), idx));
-        
-        groupSet[groupIndex] = key ;
-        groupView.set('content', g) ;
-        groupView.layerId = key ; // cannot use .set, layerId is RO
-        
-        // get the layout for the group's first itemView
-        groupLayout = this.itemViewLayoutAtContentIndex(g.get('itemRange').start) ;
-        
-        // derive the correct groupView layout
-        groupLayout.height = this.heightForGroupAtIndex(groupIndex) ;
-        groupLayout.top -= groupLayout.height ;
-        groupView.adjust(groupLayout) ;
-        context = context.begin(groupView.get('tagName')) ;
-        groupView.prepareContext(context, YES) ;
-        context = context.end() ;
-      }
-    }
-    
-    if (SC.DEBUG_PARTIAL_RENDER) console.log('******************************') ;
-    
-    this.endPropertyChanges() ;
-    if (SC.BENCHMARK_RENDER) SC.Benchmark.end(bkey);
-  },
-  
-  /** @private
-    Calculates the visible content range in the specified frame.  If 
-    uniform rows are set, this will use some simple math.  Otherwise it will
-    compute all row offsets leading up to the frame.
+    Add class name to HTML for styling.
   */
-  contentRangeInFrame: function(frame) {
-    // console.log('contentRangeInFrame invoked on %@ with frame {%@, %@, %@, %@}'.fmt(this, frame.x, frame.y, frame.width, frame.height));
-    var min, max, ret, rowHeight ;
-    var minY = SC.minY(frame), maxY = SC.maxY(frame);
-    // use some simple math...
-    // if (this.get('hasUniformRowHeights') &amp;&amp; this.get('hasUniformGroupHeights')) {
-    //   rowHeight = this.get('rowHeight') || 20 ;
-    //   min = Math.max(0,Math.floor(minY / rowHeight)-1) ;
-    //   max = Math.ceil(maxY / rowHeight) ;
-    //   
-    //   // now take into acccount the groups...
-    //   
-    //   var content = this.get('content') ;
-    //   min = Math.min(min, content.get('length')) ;
-    //   max = Math.min(max, content.get('length')) ;
-    //   
-    //   // convert to range...
-    //   ret = { start: min, length: max - min } ;
-    //   
-    // // otherwise, get the cached row offsets...
-    // } else {
-      var content = this.get('content');
-      var len = (content ? content.get('length') : 0), offset = 0;
-      
-      // console.log('contentRangeInFrame content length is %@'.fmt(len));
-      
-      // console.log('minY = ' + minY) ;
-      // console.log('maxY = ' + maxY) ;
-      
-      min = null; 
-      max = 0;
-      do {
-        offset = this.offsetForRowAtContentIndex(max); // add offset.
-        // console.log('offset is now %@'.fmt(offset));
-        if ((min===null) &amp;&amp; (offset &gt;= minY)) min = max; // set min
-        max++ ;
-      } while (max&lt;len &amp;&amp; offset &lt; maxY);
-      
-      // console.log('min = ' + min) ;
-      // console.log('max = ' + max) ;
-      
-      // convert to range...
-      ret = { start: Math.max(min-1, 0), length: Math.min(max - min + 2, len) } ;
-    // }
-    // 
-    // var contentIdx = ret.start ;
-    // var group, groups = this.getPath('content.groups') || [] ;
-    // for (var idx=0, len=groups.length; idx&lt;len: ++idx) {
-    //   group = groups[idx] ;
-    //   if (SC.valueInRange(contentIdx, group.get('itemRange'))) {
-    //     
-    //   }
-    // }
-    
-    // console.log('contentRangeInFrame is {%@, %@}'.fmt(ret.start, ret.length));
-    return ret ;
-  },
-  
-  // /**
-  //   An array containing the contracted groups, if any, by ground index for the
-  //   current content array.
-  //   
-  //   @readOnly
-  //   @property
-  //   @type SC.Array
-  // */
-  // contractedGroups: function(key, val) {
-  //   if (val) {
-  //     throw &quot;The SC.SourceListView contractedGroups property is read-only.&quot; ;
-  //   }
-  //   
-  //   var ary = this._contractedGroups ;
-  //   if (!ary) ary = this._contractedGroups = [] ;
-  //   
-  //   // All groups default to &quot;expanded&quot;, so an empty array reflects this.
-  //   return ary ;
-  // }.property(),
-  // 
-  // /** @private
-  //   Called by groups when their expansion property changes.
-  // */
-  // groupDidChangeExpansion: function(group, isExpanded) {
-  //   if (!group) return ;
-  //   
-  //   var groups = this.getPath('content.groups') || [] ;
-  //   var groupIndex = groups.indexOf(group) ;
-  //   
-  //   if (groupIndex &gt;= 0) {
-  //     var ary = this._contractedGroups ;
-  //     if (!ary) ary = this._contractedGroups = [] ;
-  //     
-  //     // only change our display if isExpanded changes
-  //     if (isExpanded &amp;&amp; ary[groupIndex]) {
-  //       delete ary[groupIndex] ; // saves memory vs. storing NO
-  //       this.displayDidChange() ;
-  //     } else if (!ary[groupIndex]) {
-  //       ary[groupIndex] = YES ;
-  //       this.displayDidChange() ;
-  //     }
-  //   }
-  // },
+  classNames: ['sc-source-list'],
   
   /**
-    Expands the index into a range of content objects that have the same
-    group value.
-    
-    This method searches backward and forward through your content array for  
-    objects that have the same group value as the object at the index you 
-    pass in.  You can use this method when implementing layoutGroupView to 
-    determine the range of the content that belongs to the group.  
-    
-    Since this method simply searches through the content array, it is really
-    only suitable for content arrays of a few hundred items or less.  If you
-    expect to have a larger size of content array, then you may need to do
-    something custom in your data model to calculate this range in less time.
-    
-    @param {Number} contentIndex index of a content object
-    @returns {Range} a range of objects
+    Default row height for source list items is larger.
   */
-  groupRangeForContentIndex: function(contentIndex) {
-    // var content = SC.makeArray(this.get('content')) ; // assume an array
-    // var len = content.get('length') ;
-    // var groupBy = this.get('groupBy') ;
-    // if (!groupBy) return { start: 0, length: len } ;
-    // 
-    // var min = contentIndex, max = contentIndex ;
-    // var cur = content.objectAt(contentIndex) ;
-    // var groupValue = (cur) ? cur.get(groupBy) : null ;
-    // var curGroupValue ;
-    // 
-    // // find first item at bottom that does not match.  add one to get start
-    // while(--min &gt;= 0) {
-    //   cur = content.objectAt(min) ;
-    //   curGroupValue = (cur) ? cur.get(groupBy) : null ;
-    //   if (curGroupValue !== groupValue) break ;
-    // }
-    // min++ ;
-    // 
-    // // find first item at top that does not match.  keep value to calc range
-    // while(++max &lt; len) {
-    //   cur = content.objectAt(max) ;
-    //   curGroupValue = (cur) ? cur.get(groupBy) : null ;
-    //   if (curGroupValue !== groupValue) break ;
-    // }
-    // 
-    // return { start: min, length: max-min } ;
-  },
-  
+  rowHeight: 24,
+
   /**
-    Determines the group value at the specified content index.  Returns null
-    if grouping is disabled.
-    
-    @param {Number} contentIndex
-    @returns {Number} group index.
+    By default source lists should not select on mouse down since you will
+    often want to drag an item instead of selecting it.
   */
-  groupIndexForContentIndex: function(contentIndex) {
-    var groupIndex = -1 ;
-    var groups = this.getPath('content.groups') || [] ;
-    var group, itemRange, len = groups.get('length') ;
-    for (var idx=0; idx&lt;len; ++idx) {
-      group = groups.objectAt(idx) ;
-      if (SC.valueInRange(contentIndex, group.itemRange)) {
-        groupIndex = idx ;
-        break ;
-      }
-    }
-    return groupIndex ;
-  },
-  
-  // emptyElement: '&lt;div class=&quot;sc-source-list-view&quot;&gt;&lt;/div&gt;',
-  // 
-  // /**
-  //   name of property on the content object to use for the source list text.
-  // */
-  // contentValueKey: null,
-  // 
-  // /**
-  //   Set to YES if you want the content value to be editable.
-  // */
-  // contentValueIsEditable: NO,
-  // 
-  // /**
-  //   Allows reordering without modifiying the selection
-  // */
-  // 
-  // selectOnMouseDown: NO,
-  // 
-  // /**
-  //   Set to YES if you want source list items to display an icon.
-  //   
-  //   If this property is set, list items will leave space for a display 
-  //   icon to the left of the text label.  To actually display an icon in that
-  //   space, you will also need to set the contenIconUrlProperty or the 
-  //   contentIconClassNameProperty or both.
-  // */
-  // hasContentIcon: NO,
-  // 
-  // /**
-  //   Set if YES if you want the source list to display a branch arrow.
-  //   
-  //   If this property is set, list items will leave space on the right edge
-  //   to display a branch arrow, indicating the user can click on the item to
-  //   reveal a menu or another level of content. 
-  //   
-  //   To actually display a branch arrow, you must also set the 
-  //   contentIsBranchKey.
-  // */
-  // hasContentBranch: NO,
-  // 
-  // /**
-  //   Name of the content object property that contains the icon .
-  //   
-  //   This is the *name* of the property you want the list items to inspect
-  //   on content objects to retrieve an icon image URL.  For example, if you
-  //   set this property to 'icon', then the icon displayed for each item will
-  //   be the URL returned by content.get('icon').
-  //   
-  //   The value of this property must be either a URL or a CSS class name.  If
-  //   you use a CSS class name, then the image src will be set to a blank 
-  //   image and the class name will be applied automatically so you can use 
-  //   spriting.  If a URL is returned it will be set as the src property on
-  //   the image tag.
-  // */
-  // contentIconKey: null,
-  // 
-  // /**
-  //   Name of content object property that contains the unread count.
-  //   
-  //   The unread count is used to indicate to a user when an item in the 
-  //   source list contains items that need their attention.  If the unread 
-  //   count on a content object is a non-zero number, it will be displayed on
-  //   the right side of the list item.
-  //   
-  //   This is the *name* of the property you want the list item to inspect
-  //   on content objects to receive the unread count for the item.  For example,
-  //   if you set this property to &quot;unread&quot;, then the unread count will be
-  //   the value returned by content.get('unread').
-  //   
-  //   If you do not want to use unread counts, leave this property to null.
-  // */
-  // contentUnreadCountKey: null,
-  // 
-  // /**
-  //   Name of the content object property that contains the branch state.
-  //   
-  //   If an item is a branch, then a branch arrow will be displayed at the
-  //   right edge indicating that clicking on the item will reveal another
-  //   level or content or possibly a popup menu.
-  //   
-  //   To display the branch, you must also set hasContentBranch to YES.
-  //   
-  //   This is the *name* of the property you want the list item to inspect 
-  //   on the content objects to retrieve the branch state.  For example, if
-  //   you set this property to &quot;isBranch&quot;, then the branch state will be the
-  //   value returned by content.get('isBranch').
-  // */
-  // contentIsBranchKey: null,
-  // 
-  // /**
-  //   Key that contains the group name.
-  //   
-  //   If set, the title shown in the group label will be the value returned
-  //   by this property on the group object.
-  // */
-  // groupTitleKey: null,
-  // 
-  // /**
-  //   Key that contains group visibility.
-  //   
-  //   If set, the group label will display a disclosure triangle matching the
-  //   value of this property.
-  // */
-  // groupVisibleKey: null,
-  
-  /** 
-    The common row height for list view items.
-    
-    The value should be an integer expressed in pixels.
-  */
-  rowHeight: 32,
-  
-  // /**
-  //   Source list view items are usually list item views.  You can override 
-  //   this if you wish.
-  // */
-  // exampleView: SC.ListItemView,
-  // 
-  // /**
-  //   The standard group view provided by source list view generally 
-  //   provides all the functionality you need.
-  // */
-  // exampleGroupView: SC.SourceListGroupView,
-  // 
-  // 
-  // // .......................................
-  // // LAYOUT METHODS
-  // //
-  // 
-  // // whenever updateChildren is called with a deep method, flush the
-  // // cached group rows to make sure we get an accurate count.
-  // updateChildren: function(deep) {
-  //   if (deep) this._groupRows = null ;
-  //   return sc_super() ;  
-  // },
-  // 
-  // // determines if the group at the specified content index is visible or
-  // // not.  This will look either at a property on the group or on the
-  // // SourceListGroupView.
-  // groupAtContentIndexIsVisible: function(contentIndex) {
-  //   
-  //   if (!this.get('groupBy')) return YES; // no grouping
-  //   
-  //   // get the group value and try to find a matching view, which may
-  //   // or may not exist yet.
-  //   var groupValue = this.groupValueAtContentIndex(contentIndex) ;
-  //   var groupView = this.groupViewForGroupValue(groupValue) ;
-  //   
-  //   // if the groupView exists, use that.  The visible state is stored here
-  //   // in case the group does not actually support storing its own visibility.
-  //   // ignore groupView if it does not support isGroupVisible
-  //   var ret = YES ;
-  //   if (groupView) ret = groupView.get('isGroupVisible') ;
-  //   
-  //   // otherwise try to get from the group itself.
-  //   if (((ret === undefined) || (ret === null) || !groupView) &amp;&amp; groupValue &amp;&amp; groupValue.get) {
-  //     var key = this.get('groupVisibleKey') ;
-  //     if (key) ret = !!groupValue.get(key) ;
-  //   }
-  //   
-  //   // if the above methods failed for some reason, just leave the group visible
-  //   if ((ret === undefined) || (ret === null)) ret = YES ;
-  //   
-  //   return ret ;
-  // },
-  // 
-  // // calculates the number of rows consumed by each group.  stores a hash of
-  // // contentIndexes and rows. 
-  // computedGroupRows: function() {
-  //   if (this._groupRows) return this._groupRows;
-  //   
-  //   var loc = 0 ; 
-  //   var content = Array.from(this.get('content')) ;  
-  //   var max = content.get('length') ;
-  //   
-  //   var ret = {} ;
-  //   while(loc &lt; max) {
-  //     var range = this.groupRangeForContentIndex(loc) ;
-  //     var isVisible = this.groupAtContentIndexIsVisible(range.start) ;
-  //     ret[range.start] = (isVisible) ? range.length : 0 ;
-  //     
-  //     // add a header row space if neede
-  //     var groupValue = this.groupValueAtContentIndex(range.start) ;
-  //     if (groupValue != null) ret[range.start]++ ;
-  //     
-  //     loc = (range.length &lt;= 0) ? max : SC.maxRange(range) ;
-  //   }
-  //   
-  //   return this._groupRows = ret ;
-  // },
-  // 
-  // // Returns the number of rows in the specified range.
-  // countRowsInRange: function(range) {
-  //   var groupRows = this.computedGroupRows() ;
-  //   var max = SC.maxRange(range) ;
-  //   var loc = SC.minRange(range) ;
-  //   var ret = 0 ;
-  //   
-  //   while(loc &lt; max) {
-  //     var range = this.groupRangeForContentIndex(loc) ;
-  //     loc = (range.length &lt;= 0) ? max : SC.maxRange(range) ;
-  //     ret += groupRows[range.start] || (range+1);
-  //   }
-  //   return ret ;
-  // },
-  // 
-  // computeFrame: function() {
-  //   var content = this.get('content') ;
-  //   var rowHeight = this.get('rowHeight') || 20 ;
-  //   
-  //   // find number of groups.  
-  //   var rows = this.countRowsInRange({ start: 0, length: content.get('length') });
-  //   if (rows &lt;= 0) rows = 0 ;
-  //   
-  //   // get parent width
-  //   var parent = this.get('parentNode') ;
-  //   var f = (parent) ? parent.get('innerFrame') : { width: 100, height: 100 };
-  //   f.x = f.y = 0;
-  //   f.height = Math.max(f.height, rows * rowHeight) ;
-  //   return f ;
-  // },
-  // 
-  // // disable incremental rendering for now
-  // contentRangeInFrame: function(frame) {
-  //   var content =this.get('content') ;
-  //   var len = (content) ? content.get('length') : 0 ;
-  //   var ret = { start: 0, length: len } ;
-  //   return ret ;
-  // },
-  // 
-  // /** @private */
-  // adjustItemViewLayoutAtContentIndex: function(itemView, contentIndex, firstLayout) {
-  //   if (SC.BENCHMARK_SOURCE_LIST_VIEW) {
-  //     SC.Benchmark.start('SC.SourceListView.adjustItemViewLayoutAtContentIndex') ;
-  //   }
-  //   
-  //   // if itemView's group is not visible, then just set to invisible.
-  //   if (!this.groupAtContentIndexIsVisible(contentIndex)) {
-  //     itemView.set('isVisible', false) ;
-  //   } else {
-  //     
-  //     // if item was not visible, make it visible.  Also force layout.
-  //     if (!itemView.get('isVisible')) {
-  //       firstLayout = YES ;        
-  //       itemView.set('isVisible', true) ;
-  //     }
-  //     
-  //     var rowHeight = this.get('rowHeight') || 0 ;
-  //     
-  //     // layout relative to top of group.  Leave open row for title
-  //     if(this.get(&quot;groupBy&quot;))
-  //     {
-  //       
-  //       var range = this.groupRangeForContentIndex(contentIndex) ;
-  //       contentIndex = (contentIndex - range.start) ;
-  //       
-  //       var groupValue = this.groupValueAtContentIndex(range.start) ;
-  //       if (groupValue != null) contentIndex++ ;
-  //     }
-  //     
-  //     var f = { 
-  //       x: 0, 
-  //       y: contentIndex*rowHeight,
-  //       height: rowHeight, 
-  //       width: this.get('innerFrame').width 
-  //     } ;
-  //     
-  //     if (firstLayout || !SC.rectsEqual(itemView.get('frame'), f)) {
-  //       itemView.set('frame', f) ;      
-  //     }
-  //     
-  //   }
-  //   
-  //   if (SC.BENCHMARK_SOURCE_LIST_VIEW) {
-  //     SC.Benchmark.end('SC.SourceListView.layoutItemViewsFor') ;
-  //   }
-  // },
-  // 
-  // layoutGroupView: function(groupView, groupValue, contentIndexHint, firstLayout) {
-  //   
-  //   if (SC.BENCHMARK_SOURCE_LIST_VIEW) {
-  //     SC.Benchmark.start('SC.SourceListView.adjustItemViewLayoutAtContentIndex') ;
-  //   }
-  //   
-  //   //console.log('layoutGroupView', groupValue) ;
-  //   
-  //   // find the range this group will belong to
-  //   var range = this.groupRangeForContentIndex(contentIndexHint) ;
-  //   var isVisible = this.groupAtContentIndexIsVisible(range.start) ;
-  //   
-  //   var priorRows = this.countRowsInRange({ start: 0, length: range.start }) ;
-  //   var rowHeight = this.get('rowHeight') || 0 ;
-  //   var parentView = groupView.get('parentView') || this ;
-  //   var rows = (isVisible) ? range.length : 0 ;
-  //   if (groupValue != null) rows++ ;
-  //   
-  //   var f = { 
-  //     x: 0, 
-  //     y: priorRows*rowHeight,
-  //     height: rowHeight * rows, 
-  //     width: (parentView || this).get('innerFrame').width 
-  //   } ;
-  //   
-  //   if (firstLayout || !SC.rectsEqual(groupView.get('frame'), f)) {
-  //     groupView.set('frame', f) ;      
-  //   }
-  //   
-  //   if (SC.BENCHMARK_SOURCE_LIST_VIEW) {
-  //     SC.Benchmark.end('SC.SourceListView.layoutGroupView') ;    
-  //   }
-  // },
-  // 
-  // // .......................................
-  // // INSERTION POINT METHODS
-  // //
-  // 
-  // insertionOrientation: SC.VERTICAL_ORIENTATION,
-  // 
-  // insertionPointClass: SC.View.extend({
-  //   emptyElement: '&lt;div class=&quot;list-insertion-point&quot;&gt;&lt;span class=&quot;anchor&quot;&gt;&lt;/span&gt;&lt;/div&gt;'
-  // }),
-  // 
-  // showInsertionPoint: function(itemView, dropOperation) {
-  //   if (!itemView) return ;
-  //   
-  //   // if drop on, then just add a class...
-  //   if (dropOperation === SC.DROP_ON) {
-  //     if (itemView !== this._dropOnInsertionPoint) {
-  //       this.hideInsertionPoint() ;
-  //       itemView.addClassName('drop-target') ;
-  //       this._dropOnInsertionPoint = itemView ;
-  //     }
-  //     
-  //   } else {
-  //     
-  //     if (this._dropOnInsertionPoint) {
-  //       this._dropOnInsertionPoint.removeClassName('drop-target') ;
-  //       this._dropOnInsertionPoint = null ;
-  //     }
-  //     
-  //     if (!this._insertionPointView) {
-  //       this._insertionPointView = this.insertionPointClass.create() ;
-  //     }
-  //     
-  //     var insertionPoint = this._insertionPointView ;
-  //     var f = this.calculateInsertionPointFrame(itemView);
-  //     insertionPoint.set('frame', f) ;
-  //     
-  //     if (insertionPoint.parentNode != itemView.parentNode) {
-  //       itemView.parentNode.appendChild(insertionPoint) ;
-  //     }
-  //   }
-  //   
-  // },
-  // 
-  // /**
-  //   This is the default frame for the insertion point.  Override this method 
-  //   if your insertion point's styling needs to be customized, or if you need 
-  //   more control of the insertion point's positioning (i.e., heirarchical 
-  //   placement)
-  // */
-  // calculateInsertionPointFrame: function(itemView) {
-  //   return { height: 0, x: 8, y: itemView.get('frame').y, width: itemView.owner.get('frame').width };
-  // },
-  // 
-  // hideInsertionPoint: function() {
-  //   var insertionPoint = this._insertionPointView ;
-  //   if (insertionPoint) insertionPoint.removeFromParent() ;
-  //   
-  //   if (this._dropOnInsertionPoint) {
-  //     this._dropOnInsertionPoint.removeClassName('drop-target') ;
-  //     this._dropOnInsertionPoint = null ;
-  //   }
-  // },
-  // 
-  // // We can do this much faster programatically using the rowHeight
-  // insertionIndexForLocation: function(loc, dropOperation) {  
-  //   var f = this.get('innerFrame') ;
-  //   var sf = this.get('scrollFrame') ;
-  //   var rowHeight = this.get('rowHeight') || 0 ;
-  //   var headerRowCount = (this.get(&quot;groupBy&quot;)) ? 1 : 0;
-  //   
-  //   // find the offset to work with.
-  //   var offset = loc.y - f.y - sf.y ;
-  //   
-  //   var ret = -1; // the return value
-  //   var retOp = SC.DROP_BEFORE ;
-  //   
-  //   // search groups until we find one that matches
-  //   var top = 0 ;
-  //   var idx = 0 ;
-  //   while((ret&lt;0) &amp;&amp; (range = this.groupRangeForContentIndex(idx)).length&gt;0){
-  //     var max = top + ((range.length+headerRowCount) * rowHeight) ;
-  //     
-  //     // the offset is within the group, find the row in the group.  Remember
-  //     // that the top row is actually the label, so we should return -1 if 
-  //     // we hit there.
-  //     if (max &gt;= offset) {
-  //       offset -= top ;
-  //         
-  //       ret = Math.floor(offset / rowHeight) ;
-  //       
-  //       // find the percent through the row...
-  //       var percentage = (offset / rowHeight) - ret ;
-  //       
-  //       // if the dropOperation is SC.DROP_ON and we are in the center 60%
-  //       // then return the current item.
-  //       if (dropOperation === SC.DROP_ON) {
-  //         if (percentage &gt; 0.80) ret++ ;
-  //         if ((percentage &gt;= 0.20) &amp;&amp; (percentage &lt;= 0.80)) {
-  //           retOp = SC.DROP_ON;
-  //         }
-  //       } else {
-  //         if (percentage &gt; 0.45) ret++ ;
-  //       }
-  //       
-  //       // handle dropping on top row...
-  //       if (ret &lt; headerRowCount) return [-1, SC.DROP_BEFORE] ; // top row!
-  //       
-  //       // convert to index
-  //       ret = (ret - headerRowCount) + idx ;
-  //       
-  //     // we are not yet within the group, go on to the next group.
-  //     } else {
-  //       idx += range.length ;
-  //       top = max ;
-  //     }
-  //   }
-  //   
-  //   return [ret, retOp] ;
-  // }
-  
+  selectOnMouseDown: NO
+
 });</diff>
      <filename>frameworks/desktop/views/source_list.js</filename>
    </modified>
    <modified>
      <diff>@@ -9,150 +9,95 @@ require('mixins/selection_support');
 
 /**
   @class
+
+  An ArrayController provides a way for you to publish an array of objects
+  for CollectionView or other controllers to work with.  To work with an 
+  ArrayController, set the content property to the array you want the 
+  controller to manage.  Then work directly with the controller object as if
+  it were the array itself.
   
-  An Array Controller provides a way to project the contents of an array
-  out to a view.  You can use this object any place you might use an
-  array.  Changes to the array will not propogate to the content array
-  until you call commitChanges().
-  
+  When you want to display an array of objects in a CollectionView, bind the
+  &quot;arrangedObjects&quot; of the array controller to the CollectionView's &quot;content&quot;
+  property.  This will automatically display the array in the collection view.
+
   @extends SC.Controller
   @extends SC.Array
   @extends SC.SelectionSupport
   @author Charles Jolley
-  @author Erich Ocean
-  @version 1.0
   @since SproutCore 1.0
 */
 SC.ArrayController = SC.Controller.extend(SC.Array, SC.SelectionSupport,
 /** @scope SC.ArrayController.prototype */ {
-  
-  /**
-    If YES the will return controllers for content objects.
-    
-    If you want to use an array controller to edit an array contents directly
-    but you do not want to wrap the values of the array in controller objects
-    then you should set this property to NO.
-    
-    @type Boolean
-  */
-  useControllersForContent: NO,
-  
-  /**
-    Provides compatibility with collection controllers.
-    
-    @property
-    @type SC.ArrayController
-  */
-  arrangedObjects: function() { return this; }.property('content'),
+
+  // ..........................................................
+  // PROPERTIES
+  // 
   
   /**
     The content array managed by this controller.  
     
-    In general you can treat an instance of ArrayController as if it were 
-    the array held in this property.  Any changes you make to the controller
-    that are not specifically implemented in the controller will pass through
-    to the Array.
+    You can set the content of the ArrayController to any object that 
+    implements SC.Array or SC.Enumerable.  If you set the content to an object
+    that implements SC.Enumerable only, you must also set the orderBy property
+    so that the ArrayController can order the enumerable for you.
     
-    Also if you set commitsChangesImmediately to NO, the controller will
-    buffer changes against this.
+    If you set the content to a non-enumerable and non-array object, then the
+    ArrayController will wrap the item in an array in an attempt to normalize
+    the result.
     
     @type SC.Array
   */
   content: null,
-  
-  /** @private */
-  contentBindingDefault: SC.Binding.multiple(),
-  
+
   /**
-    Set to YES if the controller has any content, even an empty array.
+    Makes the array editable or not.  If this is set to NO, then any attempts
+    at changing the array content itself will throw an exception.
     
     @property
     @type Boolean
   */
-  hasContent: function() {
-    return !SC.none(this.get('content')) ;
-  }.property('content'),
+  isEditable: YES,
   
   /**
-    Property key to use to group objects.
+    Used to sort the array.
     
-    If groupBy is set to a non-null value, then the array controller will
-    automatically partion the content array into groups based on the value of 
-    the passed property key.  The exampleGroup will be used to create the
-    objects in the groups array.
+    If you set this property to a key name, array of key names, or a function,
+    then then ArrayController will automatically reorder your content array
+    to match the sort order.  (If you set a function, the function will be
+    used to sort).
+
+    Normally, you should only use this property if you set the content of the
+    controller to an unordered enumerable such as SC.Set or SC.SelectionSet.
+    In this case the orderBy property is required in order for the controller
+    to property order the content for display.
     
-    If this property is set, you MUST ensure the items in the content array 
-    are already sorted by the group key.  Otherwise groups might appear more 
-    than once.
+    If you set the content to an array, it is usually best to maintain the 
+    array in the proper order that you want to display things rather than 
+    using this method to order the array since it requires an extra processing
+    step.  You can use this orderBy property, however, for displaying smaller 
+    arrays of content.
     
-    @type String
-  */
-  groupByKey: null,
-  
-  /**
-    When grouping, create objects using this class. SC.ArrayController will 
-    set 'value' to the value of the groupByKey, and 'itemRange' to an 
-    SC.Range of the item indexes in the group, and 'owner' to this array
-    controller.
+    Note that you can only to use addObject() to insert new objects into an
+    array that is ordered.  You cannot manually reorder or insert new objects
+    into specific locations because the order is managed by this property 
+    instead.
     
-    @type Class
-  */
-  exampleGroup: SC.Object,
-  
-  /**
-    The groups, if any, for the current content array.
+    If you pass a function, it should be suitable for use in compare().
     
-    @readOnly
     @property
-    @type SC.Array
+    @type String|Array|Function
   */
-  groups: function(key, val) {
-    if (val) throw &quot;The SC.ArrayController groups property is read-only.&quot; ;
-    
-    var groupByKey = this.get('groupByKey') ;
-    if (!groupByKey) return SC.EMPTY_ARRAY ; // no groups
+  orderBy: null,
     
-    var content = this.get('content') ;
-    if (!content || content.get('length') === 0) {
-      return SC.EMPTY_ARRAY ; // no groups...
-    }
-    
-    // okay, calculate the groups...
-    var previousGroup, currentGroup, groupStart = 0 ;
-    var obj, groups = [], GroupClass = this.get('exampleGroup') ;
-    
-    // initialize the discover groups loop
-    previousGroup = content.objectAt(0).get(groupByKey) ;
-    
-    // discover groups...
-    for (var idx=0, len=content.get('length'); idx&lt;len; ++idx) {
-      obj = content.objectAt(idx) ;
-      currentGroup = obj.get(groupByKey) ;
-      
-      // did our group change?
-      if (previousGroup !== currentGroup) {
-        // save the current group
-        groups.push(GroupClass.create({
-          value: previousGroup,
-          itemRange: { start: groupStart, length: idx - groupStart },
-          owner: this
-        }));
-        
-        // and move on to the next group
-        previousGroup = currentGroup ;
-        groupStart = idx ;
-      }
-    }
-    
-    // close the last and final group
-    groups.push(GroupClass.create({
-      value: previousGroup,
-      itemRange: { start: groupStart, length: idx - groupStart },
-      owner: this
-    }));
+  /**
+    Set to YES if you want the controller to wrap non-enumerable content    
+    in an array and publish it.  Otherwise, it will treat single content like 
+    null content.
     
-    return groups ;
-  }.property('content', 'groupByKey').cacheable(),
+    @property
+    @type Boolean
+  */
+  allowsSingleContent: YES,
   
   /**
     Set to YES if you want objects removed from the array to also be
@@ -166,353 +111,364 @@ SC.ArrayController = SC.Controller.extend(SC.Array, SC.SelectionSupport,
     @type {Boolean}
   */
   destroyOnRemoval: NO,
-  
+
   /**
-    Defines the default class to use when creating new content. 
-    
-    This property should either contains a class or a string that resolves
-    to a class that responds to the newRecord() method.
+    Returns an SC.Array object suitable for use in a CollectionView.  
+    Depending on how you have your ArrayController configured, this property
+    may be one of several different values.  
     
-    @type {Class}
+    @property
+    @type SC.Array
   */
-  exampleContentObject: null,
+  arrangedObjects: function() {
+    return this;
+  }.property().cacheable(),
   
   /**
-    Creates a new record instance and adds it to the end of the current array.
-    
-    This method works just like insertNewObjectAt() but always appends.
+    Computed property indicates whether or not the array controller can 
+    remove content.  You can delete content only if the content is not single
+    content and isEditable is YES.
     
-    @param attributes {Hash} optional hash of attributes to pass to the new obejct.
-    @param objectType {Class} optional class of object to create.
-    @returns {Object} the newly created object (also added to the array)
+    @property
+    @type Boolean
   */
-  newObject: function(attributes, objectType) {
-    return this.insertNewObjectAt(null, attributes, objectType) ;
-  },
+  canRemoveContent: function() {
+    var content = this.get('content'), ret;
+    ret = !!content &amp;&amp; this.get('isEditable') &amp;&amp; this.get('hasContent');
+    if (ret) {
+      return !content.isEnumerable || 
+             (SC.typeOf(content.removeObject) === SC.T_FUNCTION);
+    } else return NO ;
+  }.property('content', 'isEditable', 'hasContent'),
   
   /**
-    Creates a new content object and inserts it at the passed index or appends
-    it at the end of the array if you pass null.
-    
-    This method takes an optional hash of attributes which will be set on
-    the new record.  You can also pass an optional objectType.  If you do 
-    not pass the objectType, you must instead set the exampleContentObject to 
-    the class of the object you want to use.  The object can be of any type 
-    but it must respond to the newRecord() method.
-    
-    Objects created using this method will be destroyed automatically if you
-    have set commitsChangesImmediately to false and call discardChanges().
-    
-    @param index {Number} the index to insert at or null to append.
-    @param attributes {Hash} optional hash of attributes to pass to the new obejct.
-    @param objectType {Class} optional class of object to create.
-    @returns {Object} the newly created object (also added to the array)
-  */
-  insertNewObjectAt: function(index, attributes, objectType) {
-    
-    // compute the objectType
-    if (!objectType) objectType = this.get('exampleContentObject') ;
-    if (SC.typeOf(objectType) === SC.T_STRING) {
-      objectType = SC.objectForPropertyPath(objectType) ;
-    }
-    if (SC.none(objectType)) {
-      throw &quot;Invalid object type was provided&quot; ;
-    }
-    
-    if (SC.typeOf(objectType.newObject) !== SC.T_FUNCTION) {
-      throw &quot;content object type does not support newRecord()&quot; ;
-    }
+    Computed property indicates whether you can reorder content.  You can
+    reorder content as long a the controller isEditable and the content is a
+    real SC.Array-like object.  You cannot reorder content when orderBy is
+    non-null.
     
-    // Create a new object...
-    var obj = objectType.newObject(attributes) ;
-    if (!this._createdObjects) this._createdObjects = [] ;
-    this._createdObjects.push(obj) ; // save for discard...
+    @property
+    @type Boolean
+  */
+  canReorderContent: function() {
+    var content = this.get('content'), ret;
+    ret = !!content &amp;&amp; this.get('isEditable') &amp;&amp; !this.get('orderBy');
+    return ret &amp;&amp; !!content.isSCArray;
+  }.property('content', 'isEditable', 'orderBy'),
+  
+  /**
+    Computed property insides whether you can add content.  You can add 
+    content as long as the controller isEditable and the content is not a 
+    single object.
     
-    // Add to array.
-    if (index) {
-      this.insertAt(index, obj) ;
-    } else this.pushObject(obj) ;
+    Note that the only way to simply add object to an ArrayController is to
+    use the addObject() or pushObject() methods.  All other methods imply 
+    reordering and will fail.
     
-    return obj ;
-  },
+    @property
+    @type Boolean
+  */
+  canAddContent: function() {
+    var content = this.get('content'), ret ;
+    ret = content &amp;&amp; this.get('isEditable') &amp;&amp; content.isEnumerable;
+    if (ret) {
+      return (SC.typeOf(content.addObject) === SC.T_FUNCTION) || 
+             (SC.typeOf(content.pushObject) === SC.T_FUNCTION); 
+    } else return NO ;
+  }.property('content', 'isEditable'),
   
-  /** @private
-    Watches changes to the content property updates the contentClone.
+  /**
+    Set to YES if the controller has valid content that can be displayed,
+    even an empty array.  Returns NO if the content is null or not enumerable
+    and allowsSingleContent is NO.
     
-    @observes content
+    @property
+    @type Boolean
   */
-  _contentDidChange: function() {
-    // console.log('%@._contentDidChange()'.fmt(this));
-    var content = this.get('content') ;
-    if (content === this._content) return ; // nothing to do
-    
-    var func = this._contentPropertyDidChange ;
+  hasContent: function() {
+    var content = this.get('content');
+    return !!content &amp;&amp; 
+           (!!content.isEnumerable || !!this.get('allowsSingleContent'));
+  }.property('content', 'allowSingleContent'),
+
+  // ..........................................................
+  // METHODS
+  // 
+  
+  /**
+    Adds an object to the array.  If the content is ordered, this will add the 
+    object to the end of the content array.  The content is not ordered, the
+    location depends on the implementation of the content.
     
-    // remove old observer, add new observer, and trigger content property change
-    if (this._content &amp;&amp; this._content.removeObserver) {
-      this._content.removeObserver('[]', this, func) ;
-    } 
+    If the source content does not support adding an object, then this method 
+    will throw an exception.
     
-    if (content &amp;&amp; content.addObserver) {
-      content.addObserver('[]', this, func) ;
-    }
+    @param {Object} object the object to add
+    @returns {SC.ArrayController} receiver
+  */
+  addObject: function(object) {
+    if (!this.get('canAddContent')) throw &quot;%@ cannot add content&quot;.fmt(this);
     
-    this._content = content; //cache
-    this._contentPropertyRevision = null ;
+    var content = this.get('content');
+    if (content.isSCArray) content.pushObject(object);
+    else if (content.addObject) content.addObject(object);
+    else throw &quot;%@.content does not support addObject&quot;.fmt(this);
     
-    var rev = (content) ? content.propertyRevision : -1 ;
-    this._contentPropertyDidChange(content, '[]', content, rev) ; 
-  }.observes('content'),
-  
-  _contentPropertyDidChange: function(target, key, value, rev) {  
-    // console.log('%@._contentPropertyDidChange(target=%@, key=%@, value=%@, rev=%@)'.fmt(this, target, key, value, rev));
-    if (!this._updatingContent &amp;&amp; (!rev || (rev != this._contentPropertyRevision))) {
-      this._contentPropertyRevision = rev ;
-      
-      this._updatingContent = true ;
-      
-      this.beginPropertyChanges();
-      this.contentCloneReset();
-      this.enumerableContentDidChange() ;
-      this.notifyPropertyChange('length') ;
-      this.updateSelectionAfterContentChange();
-      this.endPropertyChanges() ;
-      
-      this._updatingContent = false ;
-    }
+    return this;
   },
   
   /**
-    The array content that (when committed) will be merged back into the 
-    content property. All array methods will take place on this object.
+    Removes the passed object from the array.  If the underyling content 
+    is a single object, then this simply sets the content to null.  Otherwise
+    it will call removeObject() on the content.
     
-    @property
-    @type SC.Array
-  */
-  contentClone: null,
-  
-  /** @private
-    Clones the content property into the contentClone property.
-  */
-  contentCloneReset: function() {
-    this._changelog = [];
-    this.set('contentClone', null);
-  },
-
-  /**
-   SC.Array interface implementation.
-   
-   @param idx {Number} Starting index in the array to replace.  If idx &gt;= 
-     length, then append to the end of the array.
-   
-   @param amt {Number} Number of elements that should be removed from the 
-     array, starting at *idx*.
-   
-   @param objects {Array} An array of zero or more objects that should be 
-     inserted into the array at *idx* 
-  */
-  replace: function(idx, amt, objects) {
-    var content = this.get('content') ;
-    
-    // in case the passed objects are controllers, convert to source objects.
-    var copyIdx = objects.length ;
-    var sourceObjects = objects ;
-    if (copyIdx &gt; 0) {
-      sourceObjects = [] ;
-      while(--copyIdx &gt;= 0) {
-        sourceObjects[copyIdx] = this._sourceObjectFor(objects[copyIdx]) ;
-      }
-    }
+    Also, if destroyOnRemoval is YES, this will actually destroy the object.
     
-    // create clone of content array if needed
-    var contentClone = this.get('contentClone') ;
-    if (!contentClone) {
-      this.set('contentClone', contentClone = SC.$A(content)) ;
+    @param {Object} object the object to remove
+    @returns {SC.ArrayController} receiver
+  */
+  removeObject: function(object) {
+    if (!this.get('canRemoveContent')) {
+      throw &quot;%@ cannot remove content&quot;.fmt(this);
     }
     
-    // now, record the removed objects.  This may be used later.
-    if (this.get('destroyOnRemoval')) {
-      if (!this._deletions) this._deletions = [] ;
-      for (var i=0; i &lt; amt; i++) {
-        this._deletions.push(content.objectAt(idx + i));
-      }
+    var content = this.get('content');
+    if (content.isEnumerable) content.removeObject(object);
+    else {
+      this.set('content', null);
+      this.enumerableContentDidChange();
     }
     
-    // and record additions
-    if (!this._changelog) this._changelog = []; 
-    this._changelog.push({ idx: idx, amt: amt, objects: sourceObjects });
-    
-    // then actually perform the edit on the contentClone
-    contentClone.replace(idx, amt, sourceObjects);
-    
-    this.editorDidChange() ;
-    this.enumerableContentDidChange();
-    this.updateSelectionAfterContentChange();
-    
-    return this;
+    if (this.get('destroyOnRemoval') &amp;&amp; object.destroy) object.destroy();
+    return this; 
   },
   
+  // ..........................................................
+  // SC.ARRAY SUPPORT
+  // 
+
   /**
-    SC.Array interface implimentation.
-    @param idx {Number} The index of the item to return.  If idx exceeds the 
-      current length, return null.
+    Compute the length of the array based on the observable content
   */
-  objectAt: function(idx) {
-    var obj = this._getSourceContent() ;
-    obj = (obj &amp;&amp; obj.objectAt) ? obj.objectAt(idx) : null;
-    return this._objectControllerFor(obj) ;
-  },
+  length: function() {
+    var content = this._scac_observableContent();
+    return content ? content.get('length') : 0;
+  }.property().cacheable(),
+
   /**
-    SC.Array interface implimentation.
-    @property
-    @type {integer}
+    Returns the object at the specified index based on the observable content
   */
-  length: function( key, value ) {
-    var ret = this._getSourceContent() ;
-    return (ret &amp;&amp; ret.get) ? (ret.get('length') || 0) : 0 ;
-  }.property(),
+  objectAt: function(idx) {
+    var content = this._scac_observableContent();
+    return content ? content.objectAt(idx) : undefined ;    
+  },
   
   /**
-    Returns the index in the array of the specified object.
-    
-    This can handle both controller wrapper objects and source content objects.
+    Forwards a replace on to the content, but only if reordering is allowed.
   */
-  indexOf: function(obj) {
-    return this._getSourceContent().indexOf(this._sourceObjectFor(obj)) ;
+  replace: function(start, amt, objects) {
+
+    // check for various conditions before a replace is allowed
+    if (!objects || objects.get('length')===0) {
+      if (!this.get('canRemoveContent')) {
+        throw &quot;%@ cannot remove objects from the current content&quot;.fmt(this);
+      }
+    } else if (!this.get('canReorderContent')) {
+      throw &quot;%@ cannot add or reorder the current content&quot;.fmt(this);
+    }    
+    
+    // if we can do this, then just forward the change.  This should fire
+    // updates back up the stack, updating rangeObservers, etc.
+    var content = this.get('content'); // note: use content, not observable
+    if (content) content.replace(start, amt, objects);
+    return this; 
   },
   
-  _getSourceContent: function() {
-    return this.get('contentClone') || this.get('content') || [];
+  // ..........................................................
+  // INTERNAL SUPPORT
+  // 
+  
+  init: function() {
+    sc_super();
+    this._scac_contentDidChange();
   },
   
-  /** @private */
-  performCommitChanges: function() {
-    var content = this.get('content');
-    var ret     = true;
-    var idx ;
+  /** @private
+    Cached observable content property.  Set to NO to indicate cache is 
+    invalid.
+  */
+  _scac_cached: NO,
+  
+  /**
+    @private
     
-    // cannot commit changes to null content.  Return an error.
-    if (!content) {
-      return SC.$error(&quot;No Content&quot;);
-    }
+    Returns the current array this controller is actually managing.  Usually
+    this should be the same as the content property, but sometimes we need to
+    generate something different because the content is not a regular array.
     
-    if (content.beginPropertyChanges) content.beginPropertyChanges();
+    Passing YES to the force parameter will force this value to be recomputed.
+  
+    @returns {SC.Array} observable or null
+  */
+  _scac_observableContent: function() {
+    var ret = this._scac_cached;
+    if (ret !== NO) return ret;
     
-    // apply all the changes made to the clone
-    if (this._changelog) {
-      var changelog = this._changelog ;
-      var max = changelog.length;
-      for(idx=0;idx&lt;max;idx++) {
-        var change = changelog[idx];
-        content.replace(change.idx, change.amt, change.objects) ;
-      }
-      this._changelog.length = 0 ; // reset changelog
-    }
+    var content = this.get('content'),
+        orderBy, func, t, len;
     
-    // finally, destroy any removed objects if necessary.  Make 
-    // sure the objects have not been re-added before doing this.
-    if (this.get('destroyOnRemoval') &amp;&amp; this._deletions &amp;&amp; this._deletions.length&gt;0) {
-      idx = this._deletions.length;
-      while(--idx &gt;= 0) {
-        var obj = this._deletions[idx] ;
-        if (obj &amp;&amp; obj.destroy &amp;&amp; (content.indexOf(obj) &lt; 0)) {
-          obj.destroy() ; 
-        }
-      }
-      this._deletions.length = 0; // clear array
+    // empty content
+    if (SC.none(content)) return this._scac_cached = [];
+
+    // wrap non-enumerables
+    if (!content.isEnumerable) {
+      ret = this.get('allowsSingleContent') ? [content] : [];
+      return (this._scac_cached = ret);
+    } 
+    
+    // no-wrap
+    orderBy = this.get('orderBy');
+    if (!orderBy) {
+      if (content.isSCArray) return (this._scac_cached = content) ;
+      else throw &quot;%@.orderBy is required for unordered content&quot;.fmt(this);     
     }
     
-    // changes commited, clear any created objects from the internal array
-    if (this._createdObjects) this._createdObjects.length = 0 ;
+    // all remaining enumerables must be sorted.
+    
+    // build array - then sort it
+    switch(SC.typeOf(orderBy)) {
+    case SC.T_STRING:
+      orderBy = [orderBy];
+      break;
+    case SC.T_FUNCTION:
+      func = orderBy ;
+      break;
+    case SC.T_ARRAY:
+      break;
+    default:
+      throw &quot;%@.orderBy must be Array, String, or Function&quot;.fmt(this);
+    }
     
-     // finish commiting changes.
-    if (content.endPropertyChanges) content.endPropertyChanges();
-    if (content.commitChanges) ret = content.commitChanges();
+    len = orderBy.get('length');
     
-    if (SC.$ok(ret)) {
-      this.contentCloneReset();
-      this.editorDidClearChanges();
+    // generate comparison function if needed - use orderBy
+    if (!func) {
+      func = function(a,b) {
+        var idx=0, status=0, key, aValue, bValue;
+        for(idx=0;(idx&lt;len)&amp;&amp;(status===0);idx++) {
+          key = orderBy.objectAt(idx);
+        
+          if (!a) aValue = a ;
+          else if (a.isObservable) aValue = a.get(key);
+          else aValue = a[key];
+
+          if (!b) bValue = b ;
+          else if (b.isObservable) bValue = b.get(key);
+          else bValue = b[key];
+        
+          status = SC.compare(aValue, bValue);
+        }
+        return status ; 
+      };
     }
+
+    ret = [];
+    content.forEach(function(o) { ret.push(o); });
+    ret.sort(func);
     
-    return ret;
+    func = null ; // avoid memory leaks
+    return (this._scac_cached = ret) ;
   },
   
-  /** @private */
-  performDiscardChanges: function() {
-    this.contentCloneReset();
-    this.editorDidClearChanges();
-    
-    // if any objects were created before the commit, destroy the objects 
-    // and reset the array.
-    if (this._createdObjects &amp;&amp; this._createdObjects.length &gt; 0) {
-      var idx = this._createdObjects.length ;
-      while(--idx &gt;= 0) {
-        var obj = this._createdObjects[idx] ;
-        if (SC.typeOf(obj.destroy) === SC.T_FUNCTION) obj.destroy() ;
-      }
-      this._createdObjects.length = 0 ;
+  /**
+    Whenever content changes, setup and teardown observers on the content
+    as needed.
+  */
+  _scac_contentDidChange: function() {
+
+    this._scac_cached = NO; // invalidate observable content
+    
+    var cur    = this.get('content'),
+        orders = !!this.get('orderBy'),
+        last   = this._scac_content,
+        oldlen = this._scac_length || 0,
+        ro     = this._scac_rangeObserver,
+        func   = this._scac_rangeDidChange,
+        efunc  = this._scac_enumerableDidChange,
+        newlen;
+        
+    if (last === cur) return this; // nothing to do
+
+    // teardown old observer
+    if (last) {
+      if (ro &amp;&amp; last.isSCArray) last.removeRangeObserver(ro);
+      else if (last.isEnumerable) last.removeObserver('[]', this, efunc);
     }
     
-    return true;
-  },
+    ro = null;
+    
+    // save new cached values 
+    this._scac_cached = NO;
+    this._scac_content = cur ;
+    
+    // setup new observers
+    // also, calculate new length.  do it manually instead of using 
+    // get(length) because we want to avoid computed an ordered array.
+    if (cur) {
+      if (!orders &amp;&amp; cur.isSCArray) ro = cur.addRangeObserver(null,this,func);
+      else if (cur.isEnumerable) cur.addObserver('[]', this, efunc);
+      newlen = cur.isEnumerable ? cur.get('length') : 1; 
+    } else newlen = SC.none(cur) ? 0 : 1;
+
+    this._scac_rangeObserver = ro;
+    
+
+    // finally, notify enumerable content has changed.
+    this._scac_length = newlen;
+    this.enumerableContentDidChange(0, newlen, newlen - oldlen);
+    this.updateSelectionAfterContentChange();
+  }.observes('content'),
   
-  /** @private
-    Returns the object controller for a source value.
+  /**
+    Whenever enumerable content changes, need to regenerate the 
+    observableContent and notify that the range has changed.  
+    
+    IMPORTANT: Assumes content is not null and is enumerable
   */
-  _objectControllerFor: function(obj) {
-    if (!this.useControllersForContent) return obj;
-    
-    var controllers = (this._objControllers = this._objControllers || {}) ;
-    var guid = SC.guidFor(obj) ;
-    var ret = controllers[guid] ;
-    if (!ret) {
-      ret = controllers[guid] = this.controllerForValue(obj) ;
-      if (ret) ret.__isArrayController = true ;
-    }
-    return ret ;
+  _scac_enumerableDidChange: function() {
+    var content = this.get('content'), // use content directly
+        newlen  = content.get('length'),
+        oldlen  = this._scac_length;
+        
+    this._scac_length = newlen;
+    this.beginPropertyChanges();
+    this._scac_cached = NO; // invalidate
+    this.enumerableContentDidChange(0, newlen, newlen-oldlen);
+    this.endPropertyChanges();
+    this.updateSelectionAfterContentChange();
   },
   
-  /** @private
-    Returns the source object for the passed value.  If the passed value is a 
-    controller, this will map back to the sourceo object.  Otherwise the 
-    object itself will be returned.
+  /**
+    Whenever array content changes, need to simply forward notification.
+    
+    Assumes that content is not null and is SC.Array.
   */
-  _sourceObjectFor: function(obj) {
-    return (obj &amp;&amp; obj.kindOf &amp;&amp; obj.kindOf(SC.Controller)) ?
-      obj.get('content') :
-      obj ;
-  },
-  
-  /** @private */
-  init: function() {
-    sc_super() ;
-    if (this.get('content')) this._contentDidChange() ;
-  },
-  
-  sort: function(callback) {
-    var content = this.get('content') ;
-    if (content &amp;&amp; content.sort) content.sort(callback) ;
-    return this ; // we're still the content...
-  },
-  
-  forEach: function(callback, target) {
-    if (typeof callback !== &quot;function&quot;) throw new TypeError() ;
-    var content = this.get('content') ;
-    if (!content) return this ;
-    
-    var len = (content.get) ? content.get('length') : content.length ;
-    if (target === undefined) target = null;
-    
-    var last = null ;
-    var context = SC.Enumerator._popContext();
-    for(var idx=0;idx&lt;len;idx++) {
-      var next = content.nextObject(idx, last, context) ;
-      callback.call(target, next, idx, this);
-      last = next ;
+  _scac_rangeDidChange: function(array, objects, key, indexes) {
+    if (key !== '[]') return ; // nothing to do
+    
+    var content = this.get('content');
+    this._scac_length = content.get('length');
+    this._scac_cached = NO; // invalidate
+    
+    // if array length has changed, just notify every index from min up
+    if (indexes) {
+      this.beginPropertyChanges();
+      indexes.forEachRange(function(start, length) {
+        this.enumerableContentDidChange(start, length, 0);
+      }, this);
+      this.endPropertyChanges();
+      this.updateSelectionAfterContentChange();
     }
-    last = null ;
-    context = SC.Enumerator._pushContext(context);
-    return this ;
   }
   
 });</diff>
      <filename>frameworks/foundation/controllers/array.js</filename>
    </modified>
    <modified>
      <diff>@@ -13,7 +13,7 @@
   an editing context.
   
   In general you will not use this class, but you can use a subclass such
-  as ObjectController, CollectionController, or ArrayController.
+  as ObjectController, TreeController, or ArrayController.
   
   h2. EDITING CONTEXTS
   
@@ -27,291 +27,19 @@
   provide this capability.
   
   @extends SC.Object
+  @since SproutCore 1.0
 */
 SC.Controller = SC.Object.extend(
 /** @scope SC.Controller.prototype */ {
   
   /**
-    The controller will set this property to true whenever there are 
-    changes that need to be committed.  This property is true whenever
-    the controller itself has uncommitted changes or when any dependent
-    editors have uncommitted changes.  In your own subclass, call 
-    this.objectDidChange(this) to register changes.
+    Makes a controller editable or not editable.  The SC.Controller class 
+    itself does not do anything with this property but subclasses will 
+    respect it when modifying content.
     
+    @property
     @type Boolean
   */
-  hasChanges: NO,
-  
-  /**
-    This is the controller's parent controller usually.  The controller will
-    notify this controller when its changes are committed or discarded.
-    
-    @type SC.Controller
-  */
-  context: null,
-  
-  /**
-    If this is NO, then the controller will only commit changes when you
-    explicitly call commitChanges.  Otherwise it will commit them
-    immediately.  You usually want this set to NO.  It is initially set to
-    YES for compatibility.
-    
-    @type Boolean
-  */
-  commitChangesImmediately: YES,
-  
-  /**
-    If the controller has uncommitted changes, call this method to 
-    commit them.  This method will commit the changes for any dependent
-    editors as well.  This will return true if the commit completed and 
-    false or an error object if it failed.
-  */
-  commitChanges: function() {
-    this._commitTimeout = null ; // clear timeout 
-    var ret = this._canCommitChanges() ;
-    if (!$ok(ret)) return ret ;
-    return this._performCommitChanges() ;
-  },
-  
-  /**
-    If this controller has uncommitted changes that you do not want to keep,
-    call this method to discard them.  This method will also discard 
-    changes for any dependent editors as well.
-  */
-  discardChanges: function() {
-    var ret = this._canDiscardChanges() ;
-    if (!$ok(ret)) return ret ;
-    return this._performDiscardChanges() ;
-  },
-  
-  /**
-    This method will return an appropriate controller object for the 
-    value of the property you name.  This will return one of:
-    
-    &lt;table&gt;
-    &lt;tr&gt; &lt;th&gt;Value Type&lt;/th&gt;        &lt;th&gt;Returns&lt;/th&gt; &lt;/tr&gt;
-    &lt;tr&gt; &lt;td&gt;Array-compatible&lt;/td&gt;  &lt;td&gt;SC.ArrayController&lt;/td&gt;&lt;/tr&gt;
-    &lt;tr&gt; &lt;td&gt;SC.Collection&lt;/td&gt;     &lt;td&gt;SC.CollectionController&lt;/td&gt;&lt;/tr&gt;
-    &lt;tr&gt; &lt;td&gt;Kind of SC.Object&lt;/td&gt; &lt;td&gt;SC.ObjectController&lt;/td&gt;&lt;/tr&gt;
-    &lt;tr&gt; &lt;td&gt;other&lt;/td&gt;             &lt;td&gt;value&lt;/td&gt;&lt;/tr&gt;
-    &lt;/table&gt;
-    
-    This is a helper method used by subclasses to create the appropriate 
-    type of controller.
-  */
-  controllerForValue: function(value) {
-    var ret = null ;
-    switch(SC.typeOf(value)) {
-      case SC.T_OBJECT:
-        if (value.kindOf(SC.Collection)) {
-          ret = SC.CollectionController ;          
-        } else ret = SC.ObjectController ;
-        break ;
-      case SC.T_ARRAY:
-        ret = SC.ArrayController ;
-        break ;
-      default:
-        ret = null ;
-    }
-    
-    return (ret) ? ret.create({ content: value, context: this }) : value ;
-  },
-  
-  /**
-    Call this method whenever you have uncommitted changes.  This will
-    handle notifying your parent context as well.
-    
-    @param {SC.Controller} editor 
-      This is the object that has uncommitted changes.  Normally you should 
-      not pass a value.  If you do pass an object, then that object will  
-      become a dependent editor of the receiver.
-  */
-  editorDidChange: function(editor) {
-    if (!editor) editor = this ; // set default value
-    
-    // if this is another editor, add it to the list of editors that need
-    // to be notified of a change.
-    if (editor != this) {
-      if (!this._dirtyEditors) this._dirtyEditors = SC.Set.create();
-      this._dirtyEditors.add(editor) ;
-    } else {
-      this._hasLocalChanges = YES ;
-    }
-    if (!this.get('hasChanges')) {
-      this.set('hasChanges', YES) ;
-      
-      // if we have a parent context notify them
-      if (this.context) {
-        this.context.editorDidChange(this) ; 
-        
-      // otherwise, if commit changes immediately is true, schedule commit.
-      // commit is only done once per cycle so that at least all the
-      // changes you might make at one time will be batched.
-      } else if (this.get('commitChangesImmediately')) {
-        if (!this._commitTimeout) {
-          this._commitTimeout = this.commitChanges.invokeLater(this);
-        }
-      }
-    }
-  },
-  
-  /**
-    Call this method when your object no longer has uncommitted changes.
-    This will clear your hasChanges property and notify your parent context.
-    This is called automatically whenever changes are committed or discarded
-    on your controller.
-  */  
-  editorDidClearChanges: function(editor) {
-    if (!editor) editor = this ; // set default value
-    
-    if (editor != this) {
-      // if we are currently clearing changes, then we will clean up the
-      // hasChanges state and dirtyeditors in bulk when this is all done.
-      // so do nothing.
-      if (this._clearingChanges) return ;
-      if (this._dirtyEditors) this._dirtyEditors.remove(editor) ;
-    } else {
-      this._hasLocalChanges = NO ;
-    }
-    
-    // _dirtyEditors may be undefined so use !! to force this to a bool value.
-    var hasChanges = !!(this._hasLocalChanges || (this._dirtyEditors &amp;&amp; this._dirtyEditors.length &gt; 0)) ;
-    
-    if (this.get('hasChanges') != hasChanges) {
-      this.set('hasChanges', hasChanges) ;
-      if (this.context) this.context.editorDidClearChanges(editor) ;
-    }
-  },
-  
-  /**
-    Override this method to determine if your controller can commit the
-    changes.  This should validate your changes.  Return false or an error
-    object if you cannot commit the change.  This method will not be called
-    unless hasChanges is true and all your dependent editors are return
-    true as well.
-  */  
-  canCommitChanges: function() {
-    return YES ;
-  },
-  
-  /**
-    Override this method to actually commit the changes for your controller.
-    This will only be called if all controllers indicate that they can
-    commit.  Return true if you succeeded or false or an error if you failed.
-  */
-  performCommitChanges: function() {
-    return SC.$error('performCommitChanges is not implemented') ;
-  },
-  
-  /**
-    Override this method to determine if your controller can discard the 
-    changes it has built up.  This method will not be called unless you
-    have set hasChanges to true.  Return false or an error object if you
-    cannot discard the change.
-  */
-  canDiscardChanges: function() {
-    return YES ;
-  },
-  
-  /**
-    Override this method to actually discard the changes for your controller.
-    This will only be called if all controllers indicate that they can discard
-    their changes.  Return true if you succeed or false or an error if you 
-    failed.
-  */
-  performDiscardChanges: function() {
-    return SC.$error('performDiscardChanges is not implemented');
-  },
-  
-  // ....................................
-  // PRIVATE
-  
-  /** @private */
-  init: function() {
-    sc_super() ;
-    this._contextObserver() ;
-  },
-  
-  /** @private */
-  _contextObserver: function() {
-    if (this.context) {
-      // inherit the parent contexts inherit property
-      this.commitChangesImmediately = this.context.commitChangesImmediately ;
-    }
-  }.observes('context'),
-  
-  /** @private */
-  _canCommitChanges: function() {
-    if (!this.get('hasChanges')) return NO ;
-    
-    // validate editors.
-    var ret = YES ;
-    if (this._dirtyEditors) {
-      ret = this._dirtyEditors.invokeWhile(YES, '_canCommitChanges') ;
-      if (!$ok(ret)) return ret ;
-    }
-    
-    // then validate receiver
-    return this.canCommitChanges() ;
-  },
-  
-  /** @private */
-  _performCommitChanges: function() {
-    if (!this.get('hasChanges')) return true ;
-    
-    // first commit any editors.  If not successful, return. otherwise,
-    // clear editors.
-    var ret = YES ;
-    if (this._dirtyEditors) {
-      this._clearingChanges = YES ;
-      ret = this._dirtyEditors.invokeWhile(YES, '_performCommitChanges') ;
-      this._clearingChanges = NO ;
-      
-      if ($ok(ret)) {
-        this._dirtyEditors = null ;
-      } else return ret ;
-    }
-    
-    // now commit changes for the receiver.
-    ret = this.performCommitChanges() ;
-    if ($ok(ret)) this.editorDidClearChanges() ;
-    return ret ;
-  },
-  
-  /** @private */
-  _canDiscardChanges: function() {
-    if (!this.get('hasChanges')) return false ;
-    // validate editors.
-    var ret = YES ;
-    if (this._dirtyEditors) {
-      ret = this._dirtyEditors.invokeWhile(YES, '_canDiscardChanges') ;
-      if (!$ok(ret)) return ret ;
-    }
-    
-    // then validate receiver
-    return this.canDiscardChanges() ;
-  },
-  
-  /** @private */
-  _performDiscardChanges: function() {
-    if (!this.get('hasChanges')) return YES ;
-    
-    // first discard changes for any editors.  If not successful, return. 
-    // otherwise, clear editors.
-    var ret = YES ;
-    if (this._dirtyEditors) {
-      this._clearingChanges = YES ;
-      ret = this._dirtyEditors.invokeWhile(YES, '_performDiscardChanges') ;
-      this._clearingChanges = NO ;
-      if ($ok(ret)) {
-        this._dirtyEditors = null ;
-      } else return ret ;
-    }
-    
-    // now discard changes for the receiver.
-    ret = this.performDiscardChanges() ;
-    if ($ok(ret)) this.editorDidClearChanges() ;
-    return ret ;
-  }
+  isEditable: YES
   
 });</diff>
      <filename>frameworks/foundation/controllers/controller.js</filename>
    </modified>
    <modified>
      <diff>@@ -47,375 +47,277 @@ require('controllers/controller') ;
   instead of an array.
   
   @extends SC.Controller
+  @since SproutCore 1.0
 */
 SC.ObjectController = SC.Controller.extend(
 /** @scope SC.ObjectController.prototype */ {
-  
-  // ...............................
+
+  // ..........................................................
   // PROPERTIES
-  //
+  // 
   
   /**
-    set this to some value and the object controller will project 
-    its properties.
+    Set to the object you want this controller to manage.  The object should
+    usually be a single value; not an array or enumerable.  If you do supply
+    an array or enumerable with a single item in it, the ObjectController
+    will manage that single item.
+
+    Usually your content object should implement the SC.Observable mixin, but
+    this is not required.  All SC.Object-based objects support SC.Observable
+    
+    @property
+    @type Object
   */
   content: null,
-  contentBindingDefault: SC.Binding.multiple(),
 
   /**
-    This will be set to true if the object currently does not have any
-    content.  You might use this to disable any controls attached to the
-    controller.
+    If YES, then setting the content to an enumerable or an array with more 
+    than one item will cause the Controller to attempt to treat the array as
+    a single object.  Use of get(), for example, will get every property on
+    the enumerable and return it.  set() will set the property on every item
+    in the enumerable. 
+    
+    If NO, then setting content to an enumerable with multiple items will be
+    treated like setting a null value.  hasContent will be NO.
     
     @type Boolean
   */
-  hasNoContent: true,
-  
+  allowsMultipleContent: NO,
+
   /**
-    This will be set to true if the content is a single object or an array 
-    with a single item.  You can use this to disabled your UI.
+    Becomes YES whenever this object is managing content.  Usually this means
+    the content property contains a single object or an array or enumerable
+    with a single item.  Array's or enumerables with multiple items will 
+    normally make this property NO unless allowsMultipleContent is YES.
     
+    @property
     @type Boolean
   */
-  hasSingleContent: false, 
+  hasContent: function() {
+    return !SC.none(this.get('observableContent'));
+  }.property('observableContent'),
   
   /**
-    This will be set to true if the content is an array with multiple objects 
-    in it.
+    Makes a controller editable or not editable.  The SC.Controller class 
+    itself does not do anything with this property but subclasses will 
+    respect it when modifying content.
     
+    @property
     @type Boolean
   */
-  hasMultipleContent: false,
-
-  /**
-    Set to true if the controller has any content, even an empty array.
-  */
-  hasContent: function() {
-    return this.get('content') ;
-  }.property('content'),
-
+  isEditable: YES,
+  
   /**
-    Set this property to true and multiple content will be treated like a null 
-    value. This will only impact use of get() and set().
+    Primarily for internal use.  Normally you should not access this property 
+    directly.  
     
-    @type Boolean
+    Returns the actual observable object proxied by this controller.  Usually 
+    this property will mirror the content property.  In some cases - notably 
+    when setting content to an enumerable, this may return a different object.
+    
+    Note that if you set the content to an enumerable which itself contains
+    enumerables and allowsMultipleContent is NO, this will become null.
+    
+    @property
+    @type Object
   */
-  allowsMultipleContent: true,
-  
+  observableContent: function() {
+    var content = this.get('content'),
+        len, allowsMultiple;
+        
+    // if enumerable, extract the first item or possibly become null
+    if (content &amp;&amp; content.isEnumerable) {
+      len = content.get('length');
+      allowsMultiple = this.get('allowsMultipleContent');
+      
+      if (len === 1) content = content.firstObject();
+      else if (len===0 || !allowsMultiple) content = null;
+      
+      // if we got some new content, it better not be enum also...
+      if (content &amp;&amp; !allowsMultiple &amp;&amp; content.isEnumerable) content=null;
+    }
+    
+    return content;
+  }.property('content', 'allowsMultipleContent').cacheable(),
+
+  // ..........................................................
+  // METHODS
+  // 
+
   /**
-    Override this method to destroy the selected object. 
+    Override this method to destroy the selected object.
     
     The default just passes this call onto the content object if it supports
-    it, and then sets the content to null.
+    it, and then sets the content to null.  
+    
+    Unlike most calls to destroy() this will not actually destroy the 
+    controller itself; only the the content.  You continue to use the 
+    controller by setting the content to a new value.
+    
+    @returns {SC.ObjectController} receiver
   */
   destroy: function() {
-    var content = this.get('content') ;
-    if (content &amp;&amp; SC.typeOf(content.destroy) === SC.T_FUNCTION) content.destroy();
+    var content = this.get('observableContent') ;
+    if (content &amp;&amp; SC.typeOf(content.destroy) === SC.T_FUNCTION) {
+      content.destroy();
+    } 
     this.set('content', null) ;  
+    return this;
   },
   
-  // ...............................
-  // INTERNAL SUPPORT
-  //
-  
   /**
-    When this controller commits changes, it will copy its changed values
-    to the content object and then call &quot;commitChanges&quot; on the content
-    object if that object implements the method.
-  */
-  performCommitChanges: function() {
+    Invoked whenever any property on the content object changes.  
+
+    The default implementation will simply notify any observers that the 
+    property has changed.  You can override this method if you need to do 
+    some custom work when the content property changes.
     
-    var content = this.get('content') ;
-    var ret = true ;
-    var key, loc;
+    If you have set the content property to an enumerable with multiple 
+    objects and you set allowsMultipleContent to YES, this method will be 
+    called anytime any property in the set changes.
 
-    // empty arrays are treated like null values, arrays.len=1 treated like 
-    // single objects.
-    var isArray = false ;
-    if (SC.isArray(content)) {
-      var len = this._lengthFor(content) ;
-      if (len === 0) {
-        content = null ; 
-      } else if (len === 1) {
-        content = this._objectAt(0, content) ;
-      } else if (this.get('allowsMultipleContent')) {
-        isArray = true ;
-      } else content = null ;
-    }
+    If all properties have changed on the content or if the content itself 
+    has changed, this method will be called with a key of &quot;*&quot;.
     
-    if (!this._changes) this._changes = {} ;
+    @param {Object} target the content object
+    @param {String} key the property that changes
+    @returns {void}
+  */
+  contentPropertyDidChange: function(target, key) {
+    if (key === '*') this.allPropertiesDidChange();
+    else this.notifyPropertyChange(key);
+  },
+  
+  /** 
+    Called whenver you try to get/set an unknown property.  The default 
+    implementation will pass through to the underlying content object but 
+    you can override this method to do some other kind of processing if 
+    needed.
+  */
+  unknownProperty: function(key,value) {
+    
+    // avoid circular references
+    if (key==='content') {
+      if (value !== undefined) this.content = value;
+      return this.content;
+    }
     
-    // cannot commit changes to empty content.  Return an error.
-    if (!content) {
-      return SC.$error(&quot;No Content&quot;) ;
+    // for all other keys, just pass through to the observable object if 
+    // there is one.  Use getEach() and setEach() on enumerable objects.
+    var content = this.get('observableContent'), loc, cur, isSame;
+    if (content===null || content===undefined) return undefined; // empty
 
-    // if content is an array, then loop through each item in the array and
-    // get the changed values.
-    } else if (isArray) {
-      
-      loc = this._lengthFor(content) ;
-      while(--loc &gt;= 0) {
-        var object = this._objectAt(loc, content) ;
-        if (!object) continue ;
-        
-        if (object.beginPropertyChanges) object.beginPropertyChanges(); 
-        
-        // loop through all the keys in changes and get the values...
-        for(key in this._changes) {
-          if (!this._changes.hasOwnProperty(key)) continue ;
-          var value = this._changes[key];
-          
-          // if the value is an array, get the idx matching the content
-          // object.  Otherwise, just use the value of the item.
-          if(SC.isArray(value)) {
-            value = this._objectAt(loc, value) ;
+    // getter...
+    if (value === undefined) {
+      if (content.isEnumerable) {
+        value = content.getEach(key);
+
+        // iterate over array to see if all values are the same. if so, then
+        // just return that value
+        loc = value.get('length');
+        if (loc&gt;0) {
+          isSame = YES;
+          cur = value.objectAt(0);
+          while((--loc &gt; 0) &amp;&amp; isSame) {
+            if (cur !== value.objectAt(loc)) isSame = NO ;
           }
-          
-          if (object.set) {
-            object.set(key,value) ;
-          } else object[key] = value ;
-        }
+          if (isSame) value = cur;
+        } else value = undefined; // empty array.
 
-        if (object.endPropertyChanges) object.endPropertyChanges() ;
-        if (object.commitChanges) ret = object.commitChanges() ;
-      }
+      } else value = (content.isObservable) ? content.get(key) : content[key];
       
-    // if the content is not an array, then just loop through each changed
-    // value and copy it to the object.
+    // setter
     } else {
-      
-      if (content.beginPropertyChanges) content.beginPropertyChanges() ;
-      
-      // save the set of changes to apply them.  Nothing should clear it but
-      // just in case.
-      var changes = this._changes ;
-      for(key in changes) {
-        if (!changes.hasOwnProperty(key)) continue;
-        
-        var oldValue = content.get ? content.get(key) : content[key];
-        var newValue = changes[key];
-        
-        if (SC.none(oldValue) &amp;&amp; newValue === '') newValue = null;
-        if (newValue != oldValue) {
-          if (content.set) {
-            content.set('isDirty', YES);
-          } else {
-            content.isDirty=YES;
-          } 
-        }
-        
-        if (content.set) {
-          content.set(key, newValue);
-        } else {
-          content[key] = newValue;
-        }
+      if (!this.get('isEditable')) {
+        throw &quot;%@.%@ is not editable&quot;.fmt(this,key);
       }
       
-      if (content.endPropertyChanges) content.endPropertyChanges() ;
-      if (content.commitChanges) ret = content.commitChanges() ;
-    }
-    
-    // if commit was successful, dump changes hash and clear editor.
-    if (SC.$ok(ret)) {
-      this._changes = {} ;
-      //this._valueControllers = {};
-      this.editorDidClearChanges() ;
+      if (content.isEnumerable) content.setEach(key, value);
+      else if (content.isObservable) content.set(key, value);
+      else content[key] = value;
     }
     
-    return ret ;
-  },
-  
-  /** @private */
-  performDiscardChanges: function() { 
-    this._changes = {};
-    this._valueControllers = {};
-    this.editorDidClearChanges();
-    this.allPropertiesDidChange();
-    return true ;
+    return value;
   },
   
-  /** @private */
-  unknownProperty: function(key,value)
-  {
-    if (key == &quot;content&quot;)
-    {
-      // FOR CONTENT KEY:
-      // avoid circular references.  If you try to set content, just save the
-      // value. The propertyObserver will be triggered below to do the rest of
-      // the setup as needed.
-      if (!(value === undefined)) this[key] = value;
-      return this[key];
-    } 
-    else 
-    {
-      // FOR ALL OTHER KEYS:
-      // Save the value in our temporary hash and note the changes in the 
-      // editor.
+  // ...............................
+  // INTERNAL SUPPORT
+  //
 
-      if (!this._changes) this._changes = {} ; 
-      if (!this._valueControllers) this._valueControllers = {}; 
-      
-      if (value !== undefined)
-      {
-        // for changes, save in _changes hash and note that a change is required.
-        this._changes[key] = value;
-        if (this._valueControllers[key])
-        {
-          this._valueControllers[key] = null;
-        }
-        // notifying observers regarless if a controller had been created since they're lazy loaded
-        this.propertyWillChange(key + &quot;Controller&quot;);
-        this.propertyDidChange(key + &quot;Controller&quot;);
-        this.editorDidChange();
-      }
-      else
-      {
-        // are we requesting the controller for a value?
-        if (key.slice(key.length-10,key.length) == &quot;Controller&quot;)
-        {
-          // the actual value...
-          key = key.slice(0,-10);
-          if ( !this._valueControllers[key] )
-          {
-            this._valueControllers[key] = this.controllerForValue(this._getValueForPropertyKey(key));
-          }
-          value = this._valueControllers[key];
-        }
-        else
-        {
-          // otherwise, get the value.
-          // first check the _changes hash, then check the content object.
-          value = this._getValueForPropertyKey(key);
-        }
-      }
-      return value;
-    }
+  /** @private - setup observer on init if needed. */
+  init: function() {
+    sc_super();
+    if (this.get('observableContent')) this._scoc_contentDidChange();
   },
-  
-  _getValueForPropertyKey: function( key )
-  {
-    // first check the changes hash for a uncommited value...
-    var value = this._changes[key];
-    // sweet, no need to proceed.
-    if ( value !== undefined ) return value;
 
-    // ok, we'll need to get the value from the content object
-    var obj = this.get('content');
-    // no content object... return null.
-    if (!obj) return null;
+  /** 
+    @private
+    
+    Called whenever the observable content property changes.  This will setup
+    observers on the content if needed.
+  */
+  _scoc_contentDidChange: function() {
+    var last = this._scoc_observableContent,
+        cur  = this.get('observableContent'),
+        func = this.contentPropertyDidChange,
+        efunc= this._scoc_enumerableContentDidChange;
 
-    if (SC.isArray(obj))
-    {
-      value = [];
-      var len = this._lengthFor(obj);
-      if (len &gt; 1)
-      {
-        // if content is an array with more than one item, collect
-        // content from array.
-        if (this.get('allowsMultipleContent')) {
-          for(var idx=0; idx &lt; len; idx++) {
-            var item = this._objectAt(idx, obj) ;
-            value.push(item ? (item.get ? item.get(key) : item[key]) : null) ;
-          }
-        } else {
-          value = null;
-        }
-      }
-      else if (len == 1)
-      {
-        // if content is array with one item, collect from first obj.
-        obj = this._objectAt(0,obj) ;
-        value = obj.get ? obj.get(key) : obj[key] ;
-      }
-      else
-      {
-        // if content is empty array, act as if null.
-        value = null;
-      }
-    }
-    else
-    {
-      // content is a single item. Just get the property.
-      value = obj.get ? obj.get(key) : obj[key] ;
+    if (last === cur) return this; // nothing to do
+    
+    this._scoc_observableContent = cur; // save old content
+    
+    // stop observing last item -- if enumerable stop observing set
+    if (last) {
+      if (last.isEnumerable) last.removeObserver('[]', this, efunc);
+      else if (last.isObservable) last.removeObserver('*', this, func);
     }
-    return value;
-  },
-
-  _lastContentPropertyRevision: 0,
-  
-  /** @private */
-  _contentDidChange: function(target,key,value,propertyRevision) {
     
-    // handle changes to the content...
-    if ((value = this.get('content')) != this._content) {
+    if (cur) {
+      if (cur.isEnumerable) cur.addObserver('[]', this, efunc);
+      else if (cur.isObservable) cur.addObserver('*', this, func);
+    }
 
-      if (this.get('hasChanges')) {
-        // if we have uncommitted changes, then discard the changes or raise
-        // an exception.
-        var er = this.discardChanges() ;
-        if (!SC.$ok(er)) throw(er) ;
-      } else {
-        // no changes, but we want to ensure that we flush the cache 
-        // of any SC.Controllers we have for the content
-        this._valueControllers = {} ;
-      }
-      
-      // get the handler method
-      var f = this._contentPropertyDidChange ;
-      
-      // stop listening to old content.
-      if (this._content) {
-        if (SC.isArray(this._content)) {
-          this._content.invoke('removeObserver', '*', this, f) ;
-        } else if (this._content.removeObserver) {
-          this._content.removeObserver('*', this, f) ;
-        }
-      }
-      
-      // start listening for changes on the new content object.
-      this._content = value ;
-      if (value) {
-        if (SC.isArray(value)) {
-          value.invoke('addObserver', '*', this, f) ;
-        } else if (value.addObserver) {
-          value.addObserver('*', this, f) ;
-        }
-      }
+    // notify!
+    if ((last &amp;&amp; last.isEnumerable) || (cur &amp;&amp; cur.isEnumerable)) {
+      this._scoc_enumerableContentDidChange();
+    } else this.contentPropertyDidChange(cur, '*');
 
-      // determine the content type.
-      var count = !value ? 0 : (SC.isArray(value) ? this._lengthFor(value) : 1) ;
-      
-      // New content is configured, update controller stats
-      this.beginPropertyChanges() ;
-      this.set('hasNoContent',count === 0) ;
-      this.set('hasSingleContent',count === 1) ;
-      this.set('hasMultipleContent',count &gt; 1) ;
-
-      // notify everyone that everything is different now.
-      this.allPropertiesDidChange() ;
-      this.endPropertyChanges() ;
-    }
-  }.observes('content'),
+  }.observes(&quot;observableContent&quot;),
   
-  // invoked when properties on the content object change.  Just forward
-  // to controller.
-  _contentPropertyDidChange: function(target,key,value, propertyRevision) {
-    this._changeFromContent = true ;
-    if (key === '*') {
-      this.allPropertiesDidChange() ;
-    } else {
-      this.propertyWillChange(key) ;
-      this.propertyDidChange(key,value) ;
+  /** @private
+    Called when observed enumerable content has changed.  This will teardown
+    and setup observers on the enumerable content items and then calls 
+    contentPropertyDidChange().  This method may be called even if the new
+    'cur' is not enumerable but the last content was enumerable.
+  */
+  _scoc_enumerableContentDidChange: function() {
+    var cur  = this.get('observableContent'),
+        set  = this._scoc_observableContentItems,
+        func = this.contentPropertyDidChange;
+    
+    // stop observing each old item
+    if (set) {
+      set.forEach(function(item) {
+        if (item.isObservable) item.removeObserver('*', this, func);
+      }, this);
+      set.clear();
     }
-    this._changeFromContent = false ;
-  },
-  
-  _lengthFor: function(obj) {
-    return (obj.get ? obj.get('length') : obj.length) || 0;
-  },
+    
+    // start observing new items if needed
+    if (cur &amp;&amp; cur.isEnumerable) {
+      if (!set) set = SC.Set.create();
+      cur.forEach(function(item) {
+        if (set.contains(item)) return ; // nothing to do
+        set.add(item);
+        if (item.isObservable) item.addObserver('*', this, func);
+      }, this); 
+    } else set = null;
+    
+    this._scoc_observableContentItems = set; // save for later cleanup
   
-  _objectAt: function(idx, obj) {
-    return obj.objectAt ? obj.objectAt(idx) : (obj.get ? obj.get(idx) : obj[idx]) ;
+    // notify
+    this.contentPropertyDidChange(cur, '*');
+    return this ;
   }
-      
+        
 }) ;</diff>
      <filename>frameworks/foundation/controllers/object.js</filename>
    </modified>
    <modified>
      <diff>@@ -5,6 +5,31 @@
 // License:   Licened under MIT license (see license.js)
 // ==========================================================================
 
+/**
+  Indicates that the collection view expects to accept a drop ON the specified
+  item.
+*/
+SC.DROP_ON = 0x01 ;
+
+/**
+  Indicates that the collection view expects to accept a drop BEFORE the 
+  specified item.
+*/
+SC.DROP_BEFORE = 0x02 ;
+
+/**
+  Indicates that the collection view expects to accept a drop AFTER the
+  specified item.  This is treated just like SC.DROP_BEFORE is most views
+  except for tree lists.
+*/
+SC.DROP_AFTER = 0x04 ;
+
+/**
+  Indicates that the collection view want's to know which operations would 
+  be allowed for either drop operation.
+*/
+SC.DROP_ANY = 0x07 ;
+
 SC.mixin(/** @scope SC */ {
   
   /**</diff>
      <filename>frameworks/foundation/core.js</filename>
    </modified>
    <modified>
      <diff>@@ -152,9 +152,17 @@ SC.ControlTestPane.add = function(label, view, attrs) {
 SC.ControlTestPane.standardSetup = function() {
   var pane = this ;
   return {
-    setup: function() { pane._pane = pane.create(); },
+    setup: function() { 
+      SC.RunLoop.begin();
+      pane._pane = pane.create(); 
+      SC.RunLoop.end();
+    },
+    
     teardown: function() {
+      SC.RunLoop.begin();
       if (pane._pane) pane._pane.remove();
+      SC.RunLoop.end();
+      
       pane._pane = null ;
     }
   } ;
@@ -177,6 +185,10 @@ SC.ControlTestPane.view = function(viewKey) {
 */
 SC.ControlTestPane.show = function() {
   var pane = this ;
-  test(&quot;show control test pane&quot;, function() { pane._showPane = pane.create(); });
+  test(&quot;show control test pane&quot;, function() { 
+    SC.RunLoop.begin();
+    pane._showPane = pane.create(); 
+    SC.RunLoop.end();
+  });
 };
 </diff>
      <filename>frameworks/foundation/debug/control_test_pane.js</filename>
    </modified>
    <modified>
      <diff>@@ -37,10 +37,25 @@ SC.browser = (function() {
 
 SC.setupBodyClassNames = function() {
   var el = document.body ;
+  var browser, platform, shadows, borderRad, classNames;
   if (!el) return ;
-  var browser = SC.browser.current ;
-  var platform = (SC.browser.windows) ? 'windows' : (SC.browser.mac) ? 'mac' : 'other-platform' ;
-  var classNames = (el.className) ? el.className.split(' ') : [] ;
+  browser = SC.browser.current ;
+  platform = (SC.browser.windows) ? 'windows' : (SC.browser.mac) ? 'mac' : 'other-platform' ;
+  
+  shadows = (document.documentElement.style.MozBoxShadow !== undefined) || 
+                (document.documentElement.style.webkitBoxShadow !== undefined) ||
+                (document.documentElement.style.oBoxShadow !== undefined) ||
+                (document.documentElement.style.boxShadow !== undefined);
+  
+  borderRad = (document.documentElement.style.MozBorderRadius !== undefined) || 
+              (document.documentElement.style.webkitBorderRadius !== undefined) ||
+              (document.documentElement.style.oBorderRadius !== undefined) ||
+              (document.documentElement.style.borderRadius !== undefined);
+  
+  
+  classNames = (el.className) ? el.className.split(' ') : [] ;
+  if(shadows) classNames.push('box-shadow');
+  if(borderRad) classNames.push('border-rad');
   classNames.push(browser) ;
   classNames.push(platform) ;
   if (SC.browser.mobileSafari) classNames.push('mobile-safari') ;
@@ -50,4 +65,4 @@ SC.setupBodyClassNames = function() {
 &lt;% end %&gt;
 &lt;% content_for('setup_body_class_names') do %&gt;
 if (SC.setupBodyClassNames) SC.setupBodyClassNames() ;
-&lt;% end -%&gt;  
\ No newline at end of file
+&lt;% end -%&gt;
\ No newline at end of file</diff>
      <filename>frameworks/foundation/english.lproj/bootstrap.rhtml</filename>
    </modified>
    <modified>
      <diff>@@ -3,3 +3,94 @@
 	font-size: 13px;
 	line-height: 1.2
 }
+
+
+/* @group SC.Benchmark */
+
+.sc-benchmark-graph
+{
+  z-index: 100000; 
+  background-color: white;  
+  position: absolute; 
+  top: 0px; 
+  left:0px; 
+  bottom:0px; 
+  overflow: auto;
+  right: 0px;
+}
+
+.sc-benchmark-title
+{
+  font-size: 13px; 
+  font-weight: bold; 
+  position: absolute; 
+  left: 0px; 
+  top: 5px;  
+}
+
+.sc-benchmark-emphasis
+{
+  font-weight: bold; 
+}
+
+.sc-benchmark-top
+{
+  height:20px;
+  left:0px;
+  top: 25px;
+  background-color: #ccc;
+  opacity: 0.4;
+  position: relative;
+  border-top: 1px solid #aaa;
+  border-bottom: 1px solid #aaa;
+}
+
+.sc-benchmark-tick
+{
+  width: 1px; 
+  height: 2000px;
+  position: absolute; 
+  top: 25px; 
+  background-color: #ccc;
+}
+
+.sc-benchmark-tick-label
+{
+  width: 40px; 
+  position: absolute; 
+  top: 27px; 
+  font-size: 10px;
+  margin-left: 2px;
+  white-space: nowrap; 
+}
+
+.sc-benchmark-bar
+{
+  height:16px; 
+  position: absolute; 
+  font-size: 10px; 
+  white-space: nowrap; 
+  border: 1px solid #CC6633; 
+  background-color: #FFCC33; 
+  -moz-border-radius: 2px;
+  -webkit-border-radius: 2px;
+  line-height: 14px;
+/*  -webkit-box-shadow: 1px 1px 2px #333;*/
+/*  text-shadow: #fff 1px 1px 1px;*/
+}
+
+.sc-benchmark-row
+{
+  position: absolute;
+  left: 0px;
+  height: 30px;
+}
+
+.sc-benchmark-row.even
+{
+  opacity: 0.4;
+  background-color: #ddd; 
+}
+
+
+/* @end */
\ No newline at end of file</diff>
      <filename>frameworks/foundation/english.lproj/core.css</filename>
    </modified>
    <modified>
      <diff>@@ -204,7 +204,7 @@ SC.Button = {
       // tree)
       return this.triggerAction(evt);
     }
-    return YES;
+    return NO; // did not handle it; keep searching
   },
 
   /**</diff>
      <filename>frameworks/foundation/mixins/button.js</filename>
    </modified>
    <modified>
      <diff>@@ -33,92 +33,11 @@
 */
 SC.SelectionSupport = {
   
-  /** 
-    Call this method whenever your source content changes to ensure the 
-    selection always remains up-to-date and valid.
-  */
-  updateSelectionAfterContentChange: function() {
-    var objects = SC.makeArray(this.get('arrangedObjects')) ;
-    var currentSelection = SC.makeArray(this.get('selection')) ;
-    var sel = [] ;
-    
-    // the new selection is the current selection that exists in 
-    // arrangedObjects or an empty selection if selection is not allowed.
-    var max = currentSelection.get('length') ;
-    if (this.get('allowsSelection')) {
-      for(var idx=0;idx&lt;max;idx++) {
-        var obj = currentSelection.objectAt(idx) ;
-        if (objects.indexOf(obj) &gt;= 0) sel.push(obj) ;
-      }
-    }
-    
-    // if the new selection is a multiple selection, get the first object
-    var selectionLength = sel.get('length') ;
-    if ((selectionLength &gt; 1) &amp;&amp; !this.get('allowsMultipleSelection')) {
-      sel = [sel.objectAt(0)] ;
-    }
-    
-    // if the selection is empty, select the first item.
-    if ((selectionLength == 0) &amp;&amp; !this.get('allowsEmptySelection')) {
-      if (objects.get('length') &gt; 0) sel = [objects.objectAt(0)] ;
-    }
-    
-    // update the selection.
-    this.set('selection', sel) ;
-  },
+  // ..........................................................
+  // PROPERTIES
+  // 
   
-  /**
-    @property
-    @type SC.Array
-    
-    Returns the set of content objects the selection should be a part of.
-    Selections in general may contain objects outside of this content, but
-    this set will be used when enforcing items such as no empty selection.
-    
-    The default version of this property returns the receiver.
-  */
-  arrangedObjects: function() { return this; }.property(),
-  
-  /**
-    @property
-    @type SC.Set
-    
-    This is the current selection.  You can make this selection and another
-    controller's selection work in concert by binding them together. You
-    generally have a master selection that relays changes TO all the others.
-  */
-  selection: function(key, value) {
-    if (value !== undefined) {
-      // are we even allowing selection at all? If not, return early...
-      if (!this.get('allowsSelection')) return this._selection ;
-      
-      value = SC.makeArray(value) ; // always force to an array
-      
-      // ok, new decide if the *type* of seleciton is allowed...
-      switch (value.get('length')) {
-        case 0:
-          // check to see if we're attemting to set an empty array
-          // if that's not allowed, set to the first available item in 
-          // arrangedObjects
-          if (!this.get('allowsEmptySelection')) {
-            var objects = this.get('arrangedObjects') ;
-            if (objects.get('length') &gt; 0) value = [objects.objectAt(0)];
-          }
-          this._selection = value ;
-          break;
-        case 1:
-          this._selection = value;
-          break;
-        default:
-          // fall through for &gt;= 2, only allow if configured for multi-select
-          this._selection = this.get('allowsMultipleSelection') ?
-            value :
-            this._selection ;
-          break;
-      }
-    }
-    else return this._selection ;
-  }.property(),
+  hasSelectionSupport: YES,
   
   /**
     If YES, selection is allowed. Default is YES.
@@ -142,6 +61,57 @@ SC.SelectionSupport = {
   allowsEmptySelection: YES,
   
   /**
+    This is the current selection.  You can make this selection and another
+    controller's selection work in concert by binding them together. You
+    generally have a master selection that relays changes TO all the others.
+    
+    @property
+    @type SC.SelectionSet
+  */
+  selection: function(key, value) {
+    var content, empty;
+    
+    if (value !== undefined) {
+      
+      // are we even allowing selection at all?  Also, must be enumerable
+      if (this.get('allowsSelection') &amp;&amp; value &amp;&amp; value.isEnumerable) {
+        
+        // ok, new decide if the *type* of selection is allowed...
+        switch (value.get('length')) {
+          
+          // check to see if we're attempting to set an empty array
+          // if that's not allowed, set to the first available item in 
+          // arrangedObjects
+          case 0:
+            empty   = this.get('allowsEmptySelection');
+            content = this.get('arrangedObjects');
+            if (empty &amp;&amp; content &amp;&amp; content.get('length')&gt;0) {
+                value = SC.SelectionSet.create().add(content, 0).freeze();
+            } else value = null ;
+            break;
+            
+          // single items are always allows
+          case 1:
+            break;
+
+          // fall through for &gt;= 2, only allow if configured for multi-select
+          default:
+            if (!this.get('allowsMultipleSelection')) value = null;
+            break;
+        }
+      } else value = null;
+      
+      // always make selection into something then save
+      if (!value) value = SC.SelectionSet.EMPTY;
+      this._scsel_selection = value;
+      
+    // read only mode
+    } else return this._scsel_selection ;
+    
+  }.property('arrangedObjects', 'allowsEmptySelection', 
+      'allowsMultipleSelection', 'allowsSelection').cacheable(),
+  
+  /**
     YES if the receiver currently has a non-zero selection.
     
     @property Boolean
@@ -149,6 +119,43 @@ SC.SelectionSupport = {
   hasSelection: function() {
     var sel = this.get('selection') ;
     return !!sel &amp;&amp; (sel.get('length') &gt; 0) ;
-  }.property('selection')
+  }.property('selection').cacheable(),
+  
+  // ..........................................................
+  // METHODS
+  // 
+
+  /** 
+    Call this method whenever your source content changes to ensure the 
+    selection always remains up-to-date and valid.
+  */
+  updateSelectionAfterContentChange: function() {
+    var content = this.get('arrangedObjects'),
+        sel     = this.get('selection'),
+        indexes, len, max, ret;
+    if (!sel) return  this; // nothing to do
+    
+    // remove from the sel any items selected beyond the length of the new
+    // arrangedObjects
+    indexes = content? sel.indexSetForSource(content, NO) : null;
+    len     = content ? content.get('length') : 0;
+    max     = indexes ? indexes.get('max') : 0;
+    if (max &gt; len) ret = sel.copy().remove(content, len, max-len);
+
+    if (!this.get('allowsSelection')) ret = SC.SelectionSet.EMPTY;
+
+    if (!this.get('allowsMultipleSelection') &amp;&amp; (ret||sel).get('length')&gt;1){
+      indexes = content ? (ret||sel).indexSetForSource(content, NO) : null;
+      ret = SC.SelectionSet.create();
+      if (indexes) ret.add(content, indexes.get('min'));
+    }
+  
+    if (!this.get('allowsEmptySelection') &amp;&amp; (ret || sel).get('length')===0) {
+      if (content) ret = SC.SelectionSet.create().add(content, 0);
+    }
   
+    if (ret) this.set('selection', ret);
+    return this ;
+  }
+    
 };</diff>
      <filename>frameworks/foundation/mixins/selection_support.js</filename>
    </modified>
    <modified>
      <diff>@@ -224,10 +224,22 @@ SC.String = {
     return this.replace(/^\s+|\s+$/g,&quot;&quot;);
   },
   
-  /** Splits the string into words, separated by spaces.  */
-  w: function() { return this.split(' '); }
+  /**
+    Splits the string into words, separated by spaces. Empty strings are
+    removed from the results.
     
-} ;
+    @returns {Array} an array of non-empty strings
+  */
+  w: function() { 
+    var ary = [], ary2 = this.split(' '), len = ary2.length ;
+    for (var idx=0; idx&lt;len; ++idx) {
+      var str = ary2[idx] ;
+      if (str.length !== 0) ary.push(str) ; // skip empty strings
+    }
+    return ary ;
+  }
+  
+};
 
 SC.String.strip = SC.String.trim; // convenience alias.
 </diff>
      <filename>frameworks/foundation/mixins/string.js</filename>
    </modified>
    <modified>
      <diff>@@ -378,37 +378,38 @@ SC.Pane = SC.View.extend({
     
     @param {SC.RootResponder} rootResponder
     @returns {SC.Pane} receiver
-
+    
   */
-  append: function() { 
+  append: function() {
     return this.appendTo(document.body) ;
   },
-
+  
   /**
     Removes the pane from the document.  This will remove the
     DOM node and deregister you from the document window.
   */
   remove: function() {
-    if (!this.get('isVisibleInWindow')) return this; // nothing to do
-
+    if (!this.get('isVisibleInWindow')) return this ; // nothing to do
+    if (!this.get('isPaneAttached')) return this ; // nothing to do
+    
     // remove layer...
     var dom = this.get('layer') ;
-    if (dom.parentNode) dom.parentNode.removeChild(dom);
-    dom = null;
+    if (dom.parentNode) dom.parentNode.removeChild(dom) ;
+    dom = null ;
     
     // remove from the RootResponder also
     var responder = this.rootResponder ;
     if (this.get('isKeyPane')) responder.makeKeyPane(null) ; // orders matter, remove keyPane first
-    if (this.get('isMainPane')) responder.makeMainPane(null);
-    responder.panes.remove(this);
+    if (this.get('isMainPane')) responder.makeMainPane(null) ;
+    responder.panes.remove(this) ;
     this.rootResponder = responder = null ;
-
-    // clean up some of my own properties    
+    
+    // clean up some of my own properties 
     this.set('isPaneAttached', NO) ;
     this.parentViewDidChange() ;
   },
-
-  /** 
+  
+  /**
     Inserts the pane into the DOM as the last child of the passed DOM element. 
     You can pass in either a CoreQuery object or a selector, which will be 
     converted to a CQ object.  You can optionally pass in the rootResponder </diff>
      <filename>frameworks/foundation/panes/pane.js</filename>
    </modified>
    <modified>
      <diff>@@ -5,6 +5,8 @@
 // License:   Licened under MIT license (see license.js)
 // ==========================================================================
 
+sc_require('core') ;
+ 
 /** @namespace SC.Benchmark
 
   This bit of meta-programming magic can install a benchmark handler on any
@@ -21,7 +23,20 @@
   Benchmark does not require anything other than the date.js class.  It also
   does not rely on SC.Object so that you can benchmark code in that area as
   well.
-
+  
+  The benchmark has three types of reports.
+  
+  report(): Returns an abbreviated list with just the durations of the bench. 
+            Also, it averages multiple runs. Everything is reported on the top
+            level only.
+            
+  timelineReport(): Returns an list of benchmarks and sub-benchmarks. If the
+                    the globalStartTime is set, then it will show relative
+                    time from that time.
+  
+  timelineChart(): Displays a chart of all the benchmarks (not sub-benchmarks)
+                   relative to the first time capture or to the globalStartTime.
+                   Hide this by calling hideChart()
 */
 SC.Benchmark = {
 
@@ -41,47 +56,81 @@ SC.Benchmark = {
   */
   enabled: YES,
   
-/**
-   This hash stores collected stats.  It contains key value pairs.  The value
-   will be a hash with the following properties:
+  /**
+     This hash stores collected stats.  It contains key value pairs.  The value
+     will be a hash with the following properties:
    
-  * * *runs*: the number of times this stat has run
-  * * *amt*: the total time consumed by this (divide by runs to get avg)
-  * * *name*: an optional longer name you assigned to the stat key.  Set this  using name().
-  * * *_starts*: this array is used internally. 
-*/
+    * * *runs*: the number of times this stat has run
+    * * *amt*: the total time consumed by this (divide by runs to get avg)
+    * * *name*: an optional longer name you assigned to the stat key.  Set this  using name().
+    * * *_starts*: this array is used internally.
+    * * *_times*: this array is used internally.
+  */
   stats: {},
 
   /**
+    If set, one can tell when the benchmark is started relatively to the global start time.
+  
+    @type Integer
+  */
+  globalStartTime: null,
+
+   /**
     Call this method at the start of whatever you want to collect.
-    if topLevelOnly is passed, then recursive calls to the start will be 
-    ignored and only the top level call will be benchmarked.
+    If a parentKey is passed, then you will attach the stat to the parent, otherwise 
+    it will be on the top level. If topLevelOnly is passed, then recursive calls 
+    to the start will be ignored and only the top level call will be benchmarked.
     
     @param key {String} A unique key that identifies this benchmark.  All calls to start/end with the same key will be groups together.
+    @param parentKey {String} A unique key that identifies the parent benchmark.  All calls to start/end with the same key will be groups together.
     @param topLevelOnly {Boolean} If true then recursive calls to this method with the same key will be ignored.  
     @param time {Integer} Only pass if you want to explicitly set the start time.  Otherwise the start time is now.
   */
-  start: function(key, topLevelOnly, time) {
+  start: function(key, parentKey, time, topLevelOnly) {
     if (!this.enabled) return ;
-    var stat = this._statFor(key) ;
+
+    var start = (time || Date.now());
+
+    if(parentKey)
+    {
+      var stat = this._subStatFor(key, parentKey) ;
+    }
+    else
+    {
+      var stat = this._statFor(key) ;
+    }
     
-    if (topLevelOnly &amp;&amp; stat._starts.length &gt; 0) {
+    if (topLevelOnly &amp;&amp; stat._starts.length &gt; 0) 
+    {
       stat._starts.push('ignore') ;
-    } else {
-      stat._starts.push(time || Date.now()) ;
     }
+    else
+    {
+      stat._starts.push(start) ;
+    }
+    stat._times.push({start: start, _subStats: {}});
+    
+    return key;
   },
-  
+
   /**
     Call this method at the end of whatever you want to collect.  This will
     save the collected benchmark.
     
     @param key {String} The benchmark key you used when you called start()
+    @param parentKey {String} The benchmark parent key you used when you called start()
     @param time {Integer} Only pass if you want to explicitly set the end time.  Otherwise start time is now.
   */
-  end: function(key, time, runs) {
+  end: function(key, parentKey, time) {
     if (!this.enabled) return ;
-    var stat = this._statFor(key) ;
+    if(parentKey)
+    {
+      var stat = this._subStatFor(key, parentKey) ;
+    }
+    else
+    {
+      var stat = this._statFor(key) ;
+    }
     var start = stat._starts.pop() ;
     if (!start) {
       console.log('SC.Benchmark &quot;%@&quot; ended without a matching start.  No information was saved.'.fmt(key));
@@ -91,28 +140,41 @@ SC.Benchmark = {
     // top level only.
     if (start == 'ignore') return ; 
     
-    stat.amt += (time || Date.now()) - start ;
-    stat.runs += (runs || 1) ;
+    var end = (time || Date.now()) ;
+    var dur = end - start;
+
+    stat._times[stat._times.length-1].end = end;
+    stat._times[stat._times.length-1].dur = dur;
+
+    stat.amt += dur ;
+    stat.runs++ ;
     
     if (this.verbose) this.log(key) ;
   },
+  
+  /* 
+    Set the inital global start time.
+  */
+  setGlobalStartTime: function(time)
+  {
+    this.globalStartTime = time;
+  },
 
   /**
     This is a simple way to benchmark a function.  The function will be 
     run with the name you provide the number of times you indicate.  Only the
     function is a required param.
   */  
-  bench: function(func, key, reps, context) {
+  bench: function(func, key, reps) {
     if (!key) key = &quot;bench%@&quot;.fmt(this._benchCount++) ;
     if (!reps) reps = 1 ;
     var ret ;
     
-    var runs = reps; 
-    SC.Benchmark.start(key) ;
     while(--reps &gt;= 0) {
-      ret = func(context);
+      var timeKey = SC.Benchmark.start(key) ;
+      ret = func();
+      SC.Benchmark.end(timeKey) ; 
     }
-    SC.Benchmark.end(key, null, runs) ; 
     
     return ret ;
   },
@@ -124,11 +186,11 @@ SC.Benchmark = {
   install: function(object,method, topLevelOnly) {
     
     // vae the original method.
-    var __func = (object['b__' + method] = object[method]) ;
+    var __func = object['b__' + method] = object[method] ;
     
     // replace with this helper.
     object[method] = function() {
-      var key = '%@(%@)'.fmt(method, SC.$A(arguments).join(', ')) ;
+      var key = '%@(%@)'.fmt(method, $A(arguments).join(', ')) ;
       SC.Benchmark.start(key, topLevelOnly) ;
       var ret = __func.apply(this, arguments) ;
       SC.Benchmark.end(key) ;
@@ -155,27 +217,133 @@ SC.Benchmark = {
   report: function(key) {
     if (key) return this._genReport(key) ;
     var ret = [] ;
-    
-    // find the longest stat name...
-    var maxLen = 0 ;
-    for(key in this.stats) {
+    for(var key in this.stats) {
       if (!this.stats.hasOwnProperty(key)) continue ;
-      if (key.length &gt; maxLen) maxLen = key.length;
+      ret.push(this._genReport(key)) ;
+    }
+    return ret.join(&quot;\n&quot;) ;
+  },
+
+  /**
+    Generate a human readable benchmark report. Pass in appName if you desire.
+
+    @param {string} application name.
+  */
+  timelineReport: function(appName) 
+  {
+    appName = (appName) ? 'SproutCore Application' : appName;
+    var ret = [appName, 'User-Agent: %@'.fmt(navigator.userAgent), 'Report Generated: %@ (%@)'.fmt(new Date().toString(), Date.now()), ''] ;
+
+    var chart = this._compileChartData(true);
+    for(var i=0; i&lt;chart.length; i++)
+    {
+      if(chart[i][4])
+      {
+        ret.push(this._timelineGenSubReport(chart[i]));
+      }
+      else
+      {
+        ret.push(this._timelineGenReport(chart[i]));
+      }
     }
+    return ret.join(&quot;\n&quot;) ;
+  },
+
+  /**
+    Generate a human readable benchmark chart. Pass in appName if you desire.
+
+  */
+  timelineChart: function(appName) {
     
-    // now gen report...
-    var keys = [] ;
-    for(key in this.stats) {
-      if (!this.stats.hasOwnProperty(key)) continue ;
-      keys.push(key) ;
+    // Hide the chart if there is an existing one.
+    this.hideChart();
+    
+    // Compile the data.
+    var chart = this._compileChartData(false);
+    var chartLen = chart.length;
+    
+    // Return if there is nothing to draw.
+    if(chartLen == 0) return;
+    
+    // Get the global start of the graph.
+    var gStart = (this.globalStartTime) ? this.globalStartTime : chart[0][1];
+    var maxDur = chart[chartLen-1][2]-gStart;
+    var maxHeight = 50+chartLen*30;
+    var incr = Math.ceil(maxDur/200)+1;
+    var maxWidth = incr*50;
+    
+    // Create the basic graph element.
+    var graph = document.createElement('div');
+    graph.className = 'sc-benchmark-graph';
+    document.body.appendChild(graph);
+
+    // Set the title.
+    var title = document.createElement('div');
+    title.innerHTML = ((appName) ? appName : 'SproutCore Application') + (' - Total Captured Time: ' + maxDur +' ms - Points Captured: ' + chartLen) + ' [&lt;a href=&quot;javascript:SC.Benchmark.hideChart();&quot;&gt;Hide Chart&lt;/a&gt;]';
+    title.className = 'sc-benchmark-title'; 
+    graph.appendChild(title);
+
+
+    var topBox = document.createElement('div');
+    topBox.className = 'sc-benchmark-top'; 
+    topBox.style.width = maxWidth + 'px';
+    graph.appendChild(topBox);
+
+    // Draw the tick marks.
+    for(var i=0;i&lt;incr; i++)
+    {
+      var tick = document.createElement('div');
+      tick.className = 'sc-benchmark-tick';
+      tick.style.left = (i*50)+'px';
+      tick.style.height = maxHeight+'px';
+      var tickLabel = document.createElement('div');
+      tickLabel.className = 'sc-benchmark-tick-label';
+      tickLabel.style.left = (i*50)+'px';
+      tickLabel.innerHTML = i*200+&quot; ms&quot;;
+      graph.appendChild(tick);
+      graph.appendChild(tickLabel);
     }
-    keys = keys.sort() ;
-    var idx, max = keys.length ;
-    for(idx=0;idx&lt;max;idx++) {
-      ret.push(this._genReport(keys[idx], maxLen)) ;
+    
+    // For each item in the chart, print it out on the screen.
+    for(var i=0;i&lt;chartLen; i++)
+    {
+    	var row = document.createElement('div');
+    	row.style.top = (75+(i*30))+'px';
+    	row.style.width = maxWidth+'px';
+    	row.className = (i%2==0) ? 'sc-benchmark-row even' : 'sc-benchmark-row';
+    	graph.appendChild(row);
+
+      var div = document.createElement('div');
+      var start = chart[i][1];
+      var end = chart[i][2];
+      var duration = chart[i][3];
+      
+      div.innerHTML = '&amp;nbsp;' + (chart[i][0] + &quot; &lt;span class='sc-benchmark-emphasis'&gt;&quot; + duration + 'ms&lt;/span&gt;');
+      
+      div.className = 'sc-benchmark-bar';
+      div.style.cssText = 'left:'+ (((start-gStart)/4))+'px; width: '+((duration/4))+
+                          'px; top: '+(53+(i*30))+'px;';
+      div.title = &quot;start: &quot; + (start-gStart) + &quot; ms, end: &quot; + (end-gStart) + ' ms, duration: ' + duration + ' ms';
+      graph.appendChild(div);
+    }
+
+    // Save the graph.
+    this._graph = graph;
+  },
+  
+  /*
+    Hide chart.
+    
+  */
+  hideChart: function()
+  {
+    if(this._graph) {
+      try{ 
+        document.body.removeChild(this._graph);
+      }catch(e){};
     }
-    return ret.join(&quot;\n&quot;) ;
   },
+  
 
   /**
     This method is just like report() except that it will log the results to
@@ -200,38 +368,116 @@ SC.Benchmark = {
   },
   
   // PRIVATE METHODS
+
+  // @private
+  
+  // Generates, sorts, and returns the array of all the data that has been captured.
+  _compileChartData: function(showSub)
+  {
+    var chart = [];
+    for(var key in this.stats) 
+    {
+      var stat = this.stats[key];
+      for(var i=0; i&lt;stat._times.length; i++)
+      {
+        var st = stat._times[i];
+        var dispKey = (stat._times.length &gt; 1) ? (i+1)+' - '+key : key;
+        chart.push([dispKey, st.start, st.end, st.dur, false]);
+        if(showSub)
+        {
+          var subStats = st._subStats;
+          for(var k in subStats) 
+          {
+           
+            var subStat = subStats[k];
+            for(var j=0; j&lt;subStat._times.length; j++)
+            {
+              var s = subStat._times[j];
+              var dispKey = (subStat._times.length &gt; 1) ? (j+1)+' - '+k : k;
+              chart.push([dispKey, s.start, s.end, s.dur, true]);
+         
+            }
+          }
+        }
+      }
+    }
+    
+    chart.sort(function(a,b)
+    {
+      if(a[1] &lt; b[1]) 
+      {
+        return -1;
+      }
+      else if(a[1] == b[1])
+      {
+        if(a[3] &amp;&amp; !b[3]) return -1;
+        if(!a[3] &amp;&amp; b[3]) return 1;
+        return 0;
+      }
+      return 1;
+    });
+
+    return chart;
+  },
   
-  _genReport: function(key, nameLength) {
+  // Generate the traditional report show multiple runs averaged.
+  _genReport: function(key) {
     var stat = this._statFor(key) ;
-    var avg = (stat.runs &gt; 0) ? (Math.floor(stat.amt * 100000 / stat.runs) / 100000) : 0 ;
+    var avg = (stat.runs &gt; 0) ? (Math.floor(stat.amt * 1000 / stat.runs) / 1000) : 0 ;
      
-    // Generate the name, adding padding spaces if needed.
-    var name = (stat.name || key)  ;
-    nameLength = (nameLength) ? nameLength : 0; 
-    if (nameLength &gt; name.length) {
-      var toJoin = [name] ;
-      nameLength -= name.length ;
-      while(--nameLength &gt;= 0) toJoin.push(' ') ;
-      name = toJoin.join('') ;
+    return 'BENCH %@ msec: %@ (%@x)'.fmt(avg, (stat.name || key), stat.runs) ;  
+  },
+
+  // Generate the report in the form of at time line. This returns the parent.
+  _timelineGenReport: function(val) 
+  {
+    if(this.globalStartTime)
+    {
+      return 'BENCH start: %@ msec, duration: %@ msec,  %@'.fmt((val[1]-this.globalStartTime), val[3], val[0]) ;  
+    } 
+    else
+    {
+      return 'BENCH duration: %@ msec, %@'.fmt( val[3],  val[0]) ;  
     }
-    
-    return 'BENCH | %@1 | avg: %@4 msec | total: %@2 msec | reps: %@3x '.fmt(name, stat.amt, stat.runs, avg) ;  
   },
   
-  // @private
+  // Generate the report in the form of at time line. This returns the children.
+  _timelineGenSubReport: function(val) 
+  {
+    if(this.globalStartTime)
+    {
+      return '   CHECKPOINT BENCH start: %@ msec, duration: %@ msec,  %@'.fmt((val[1]-this.globalStartTime), val[3], val[0]) ;  
+    } 
+    else
+    {
+      return '   CHECKPOINT BENCH duration: %@ msec, %@'.fmt( val[3], val[0]) ;  
+    }
+  },
+  
+  // returns a stats hash for the named key and parent key.  If the hash does not exist yet,
+  // creates it.
+  _subStatFor: function(key, parentKey) {
+    var parentTimeLen = this.stats[parentKey]._times.length;
+    if(parentTimeLen == 0) return;
+    var parentSubStats = this.stats[parentKey]._times[this.stats[parentKey]._times.length-1]._subStats;
+    var ret = parentSubStats[key] ;
+    if (!ret) ret = parentSubStats[key] = {
+      runs: 0, amt: 0, name: key, _starts: [], _times: []      
+    };
+    return ret ;
+  },
+
   // returns a stats hash for the named key.  If the hash does not exist yet,
   // creates it.
   _statFor: function(key) {
     var ret = this.stats[key] ;
-    if (!ret) {
-      ret = this.stats[key] = {
-        runs: 0, amt: 0, name: key, _starts: []      
-      };
-    }
+    if (!ret) ret = this.stats[key] = {
+      runs: 0, amt: 0, name: key, _starts: [], _times: []      
+    };
     return ret ;
   },
   
-  reset: function() { this.stats = {} ;  /*debugger;*/},
+  reset: function() { this.stats = {} ; },
   
   // This is private, but it is used in some places, so we are keeping this for
   // compatibility.
@@ -242,3 +488,5 @@ SC.Benchmark = {
   _benchCount: 1
   
 } ;
+
+SC.Benchmark = SC.Benchmark;
\ No newline at end of file</diff>
      <filename>frameworks/foundation/system/benchmark.js</filename>
    </modified>
    <modified>
      <diff>@@ -77,8 +77,8 @@ SC.Event = function(originalEvent) {
   
   // normalize wheelDelta, wheelDeltaX, &amp; wheelDeltaY for Safari
   if (SC.browser.safari &amp;&amp; originalEvent.wheelDelta!==undefined) {
-    this.wheelDelta = this.wheelDeltaY = 0-(originalEvent.wheelDeltaY || originalEvent.wheelDelta)/120;
-    this.wheelDeltaX = 0-(originalEvent.wheelDeltaX||0)/120 ;
+    this.wheelDelta = this.wheelDeltaY = 0-(originalEvent.wheelDeltaY || originalEvent.wheelDelta);
+    this.wheelDeltaX = 0-(originalEvent.wheelDeltaX||0) ;
     
   // normalize wheelDelta for Firefox
   // note that we multiple the delta on FF to make it's acceleration more 
@@ -339,18 +339,18 @@ SC.mixin(SC.Event, /** @scope SC.Event */ {
     @returns {Hash} simulated event object
   */
   simulateEvent: function(elem, eventType, attrs) {
-    var ret = {
+    var ret = SC.Event.create({
       type: eventType,
       target: elem,
       preventDefault: function(){ this.cancelled = YES; },
-      stopPropagation: function(){ this.bubble = NO; },
+      stopPropagation: function(){ this.bubbles = NO; },
       allowDefault: function() { this.hasCustomEventHandling = YES; },
       timeStamp: Date.now(),
-      bubble: (this.NO_BUBBLE.indexOf(eventType)&lt;0) ,
+      bubbles: (this.NO_BUBBLE.indexOf(eventType)&lt;0),
       cancelled: NO,
       normalized: YES
-    } ;
-    if (attrs) SC.mixin(ret, attrs);
+    });
+    if (attrs) SC.mixin(ret, attrs) ;
     return ret ;
   },
   
@@ -407,15 +407,15 @@ SC.mixin(SC.Event, /** @scope SC.Event */ {
       event = this.simulateEvent(elem, eventType) ;
       args.unshift(event) ;
     }
-
+    
     event.type = eventType ;
-
+    
     // Trigger the event - bubble if enabled
     var current = elem;
     do {
       ret = SC.Event.handle.apply(current, args);
       current = (current===document) ? null : (current.parentNode || document);
-    } while(!ret &amp;&amp; event.bubble &amp;&amp; current);    
+    } while(!ret &amp;&amp; event.bubbles &amp;&amp; current);    
     current = null ;
 
     // Handle triggering native .onfoo handlers</diff>
      <filename>frameworks/foundation/system/event.js</filename>
    </modified>
    <modified>
      <diff>@@ -43,7 +43,13 @@ SC.Request = SC.Object.extend({
   
   send: function(body) {
     var request = this ; 
-    if(body) request.set('body', body) ;
+    if(body) {
+      if(this.get('isJSON')) {
+        body=SC.json.encode(body);
+        if(body===undefined) console.error('There was an error encoding to JSON');
+      }
+      request.set('body', body) ;
+    }
     SC.Request.manager.sendRequest(request) ;
     return request ;
   },
@@ -63,10 +69,10 @@ SC.Request = SC.Object.extend({
     }
     
     if (this.get(&quot;isJSON&quot;)) {
-        var source = response.responseText ;
-        var json = SC.json.decode(source) ;
-        //TODO cache this value?
-        return json ;
+      var source = response.responseText ;
+      var json = SC.json.decode(source) ;
+      //TODO cache this value?
+      return json ;
     }
     
     if(response.responseXML) return response.responseXML ;</diff>
      <filename>frameworks/foundation/system/request.js</filename>
    </modified>
    <modified>
      <diff>@@ -112,7 +112,7 @@ SC.mixin( /** @scope SC */ {
   
   /** Return the point that will center the frame Y within the passed frame. */
   centerY: function(innerFrame, outerFrame) {
-    return (outerFrame.width - innerFrame.width) /2  ;
+    return (outerFrame.height - innerFrame.height) /2  ;
   },
   
   /** Check if the given point is inside the rect. */</diff>
      <filename>frameworks/foundation/system/utils.js</filename>
    </modified>
    <modified>
      <diff>@@ -87,3 +87,43 @@ test(&quot;Test Synchronous GET Request, auto-deserializing JSON&quot;, function() {
   ok(SC.$ok(contents), 'contents should not be an error (contents = %@)'.fmt(contents));
   if (SC.$ok(contents)) same(contents, {&quot;message&quot;: &quot;Yay!&quot;}, 'contents should have message') ;
 });
+
+
+test(&quot;Test if Request body is being auto-serializing to JSON&quot;, function() {
+  request.set(&quot;isAsynchronous&quot;, false);
+  request.set(&quot;isJSON&quot;, true);
+  var objectToPost={&quot;content&quot;: &quot;garbage&quot;};
+  
+  request.send(objectToPost);
+  var jsonEncoded = request.get('body');
+  var contents = request.get(&quot;response&quot;);
+  
+  equals(jsonEncoded, '{&quot;content&quot;:&quot;garbage&quot;}', &quot;The json object passed in send should be encoded and set as the body&quot;);
+});
+
+
+test(&quot;Test Multiple Asynchronous GET Request - two immediate, and two in serial&quot;, function() {
+  var requestCount = 3;
+  var responseCount = 0;
+
+  var observer = function(response) {
+    responseCount++;
+    if(requestCount&lt;=7) {
+      SC.Request.getUrl(url).addObserver(&quot;response&quot;, observer).send();
+      requestCount++;
+    }
+  };
+  
+  
+  SC.Request.getUrl(url).notify(this, observer).send();
+  SC.Request.getUrl(url).notify(this, observer).send();
+  SC.Request.getUrl(url).notify(this, observer).send();
+  
+  stop() ; // stops the test runner
+  setTimeout( function(){
+    equals(requestCount, 8, &quot;requestCount should be 4&quot;);
+    equals(responseCount, 6, &quot;responseCount should be 4&quot;);
+    window.start() ; // starts the test runne
+  }, 3000);
+});
+</diff>
      <filename>frameworks/foundation/tests/system/request.js</filename>
    </modified>
    <modified>
      <diff>@@ -30,7 +30,7 @@ SC.FieldView = SC.View.extend(SC.Control, SC.Validatable,
 
   /**
     The raw value of the field itself.  This is computed from the 'value'
-    propery by passing it through any validator you might have set.  This is 
+    property by passing it through any validator you might have set.  This is 
     the value that will be set on the field itself when the view is updated.
     
     @property {String}
@@ -57,7 +57,7 @@ SC.FieldView = SC.View.extend(SC.Control, SC.Validatable,
   
   /**
     Override to set the actual value of the field.
-
+    
     The default implementation will simple copy the newValue to the value
     attribute of any input tags in the receiver view.  You can override this
     method to provide specific functionality needed by your view.
@@ -70,7 +70,7 @@ SC.FieldView = SC.View.extend(SC.Control, SC.Validatable,
     this.$input().val(newValue);
     return this ;
   },
-
+  
   /**
     Override to retrieve the actual value of the field.
     
@@ -104,7 +104,6 @@ SC.FieldView = SC.View.extend(SC.Control, SC.Validatable,
     @returns {Boolean|SC.Error} result of validation.
   */
   fieldValueDidChange: function(partialChange) {
-
     // collect the field value and convert it back to a value
     var fieldValue = this.getFieldValue();
     var value = this.objectForFieldValue(fieldValue, partialChange);
@@ -180,7 +179,7 @@ SC.FieldView = SC.View.extend(SC.Control, SC.Validatable,
   
   // ACTIONS
   // You generally do not need to override these but they may be used.
-
+  
   /**
     Called to perform validation on the field just before the form 
     is submitted.  If you have a validator attached, this will get the
@@ -196,7 +195,7 @@ SC.FieldView = SC.View.extend(SC.Control, SC.Validatable,
   
   // OVERRIDE IN YOUR SUBCLASS
   // Override these primitives in your subclass as required.
-
+  
   /**
     Allow the browser to do its normal event handling for the mouse down
     event.  But first, set isActive to YES.
@@ -209,7 +208,7 @@ SC.FieldView = SC.View.extend(SC.Control, SC.Validatable,
     evt.allowDefault(); 
     return YES; 
   },
-
+  
   /** @private
     Remove the active class on mouseOut if mouse is down.
   */  
@@ -218,7 +217,7 @@ SC.FieldView = SC.View.extend(SC.Control, SC.Validatable,
     evt.allowDefault();
     return YES;
   },
-
+  
   /** @private
     If mouse was down and we renter the button area, set the active state again.
   */  
@@ -227,7 +226,7 @@ SC.FieldView = SC.View.extend(SC.Control, SC.Validatable,
     evt.allowDefault();
     return YES;
   },
-
+  
   _field_isMouseDown: NO,
   
   /** @private</diff>
      <filename>frameworks/foundation/views/field.js</filename>
    </modified>
    <modified>
      <diff>@@ -284,7 +284,7 @@ SC.View = SC.Object.extend(SC.Responder, SC.DelegateSupport,
       } else {
         var that = this;
         this.set('layerNeedsUpdate', YES);
-        this.invokeOnce(function() { that.updateLayerIfNeeded(YES) });
+        this.invokeOnce(function() { that.updateLayerIfNeeded(YES); });
       }
       
       // if we were firstResponder, resign firstResponder also if no longer
@@ -584,9 +584,11 @@ SC.View = SC.Object.extend(SC.Responder, SC.DelegateSupport,
   */
   findLayerInParentLayer: function(parentLayer) {
     var layerId = this.get('layerId') ;
+    var node, i, ilen,found, elem;
     
     // first, let's try the fast path...
-    var elem = document.getElementById(layerId) ;
+    if(parentLayer.getElementById) elem = parentLayer.getElementById(layerId) ;
+    else elem = document.getElementById(layerId) ;
     
     // TODO: use code generation to only really do this check on IE
     if (SC.browser.msie &amp;&amp; elem &amp;&amp; elem.id !== layerId) elem = null ;
@@ -600,21 +602,25 @@ SC.View = SC.Object.extend(SC.Responder, SC.DelegateSupport,
     // if no element was found the fast way, search down the parentLayer for
     // the element.  This code should not be invoked very often.  Usually a
     // DOM element will be discovered by the first method above.
+    // This code uses a BFS algorithm as is expected to find the layer right 
+    // below the parent.
     if (!elem) {
       elem = parentLayer.firstChild ;
-      while (elem &amp;&amp; (elem.id !== layerId)) {
-        // try to get first child or next sibling if no children
-        var next = elem.firstChild || elem.nextSibling ;
-        
-        // if no next sibling, then get next sibling of parent.  Walk up 
-        // until we find parent with next sibling or find ourselves back at
-        // the beginning.
-        while (!next &amp;&amp; elem &amp;&amp; ((elem = elem.parentNode) !== parentLayer)) {
-          next = elem.nextSibling ;
+      var q=[];
+      q.push(parentLayer);
+      while(q.length!==0){
+        node=q[0];
+        q.shift();
+        if(node.id==layerId){
+          found=true;
+          elem=node;
+          break;
+        } 
+        for(i=0, ilen=node.childNodes.length; i&lt;ilen; i++){
+          q.push(node.childNodes[i]);
         }
-        
-        elem = next ;
       }
+      if(!found) elem=null;  
     }
     
     return elem;
@@ -808,9 +814,7 @@ SC.View = SC.Object.extend(SC.Responder, SC.DelegateSupport,
     @returns {SC.View} receiver
   */
   replaceLayer: function() {
-    if (!this.get('layer')) return this; // nothing to do
     this.destroyLayer();
-
     this.set('layerLocationNeedsUpdate', YES) ;
     this.invokeOnce(this.updateLayerLocationIfNeeded) ;
   },
@@ -1108,7 +1112,7 @@ SC.View = SC.Object.extend(SC.Responder, SC.DelegateSupport,
     @returns {Boolean}
   */
   performKeyEquivalent: function(keystring, evt) {
-    var ret = null,
+    var ret = NO,
         childViews = this.get('childViews'),
         len = childViews.length,
         idx = -1 ;</diff>
      <filename>frameworks/foundation/views/view.js</filename>
    </modified>
    <modified>
      <diff>@@ -27,7 +27,7 @@ var NO = false ;
 // prevent a console.log from blowing things up if we are on a browser that
 // does not support it
 if (typeof console === 'undefined') {
-  var console = console || window.console || {} ;
+  window.console = {} ;
   console.log = console.info = console.warn = console.error = function(){};
 }
 
@@ -270,7 +270,7 @@ SC.mixin(/** @scope SC */ {
     var guidKey = this.guidKey ;
     if (obj[guidKey]) return obj[guidKey] ;
 
-    switch(this.typeOf(obj)) {
+    switch(typeof obj) {
       case SC.T_NUMBER:
         return (this._numberGuids[obj] = this._numberGuids[obj] || (&quot;nu&quot; + obj));
       case SC.T_STRING:
@@ -328,7 +328,7 @@ SC.mixin(/** @scope SC */ {
     @returns {String} the hash code for this instance.
   */
   hashFor: function(obj) {
-    return (obj &amp;&amp; obj.hash &amp;&amp; SC.typeOf(obj.hash) === SC.T_FUNCTION) ? obj.hash() : this.guidFor(obj) ;
+    return (obj &amp;&amp; obj.hash &amp;&amp; (typeof obj.hash === SC.T_FUNCTION)) ? obj.hash() : this.guidFor(obj) ;
   },
     
   /**
@@ -383,31 +383,32 @@ SC.mixin(/** @scope SC */ {
         if (v&lt;w) return -1;
         if (v&gt;w) return 1;
         return 0;
-        break;
+
       case SC.T_STRING:
         if (v.localeCompare(w)&lt;0) return -1;
         if (v.localeCompare(w)&gt;0) return 1;
         return 0;
-        break;
+
       case SC.T_ARRAY:
         var l = Math.min(v.length,w.length);
         var r = 0;
         var i = 0;
-        while (r==0 &amp;&amp; i &lt; l) {
+        while (r===0 &amp;&amp; i &lt; l) {
           r = arguments.callee(v[i],w[i]);
-          if ( r != 0 ) return r;
+          if ( r !== 0 ) return r;
           i++;
-        };
+        }
+        
         // all elements are equal now
         // shorter array should be ordered first
         if (v.length &lt; w.length) return -1;
         if (v.length &gt; w.length) return 1;
         // arrays are equal now
         return 0;
-        break;
+
       default:
         return 0;
-    };
+    }
   },
   
   // ..........................................................</diff>
      <filename>frameworks/runtime/core.js</filename>
    </modified>
    <modified>
      <diff>@@ -16,14 +16,14 @@ SC.ArraySuite.define(function(T) {
   // ..........................................................
   // MODULE: isDeep = YES 
   // 
-  module(T.desc(&quot;RangeObserver Methods - isDeep YES&quot;), {
+  module(T.desc(&quot;RangeObserver Methods&quot;), {
     setup: function() {
       expected = T.objects(10);
       array = T.newObject(expected);
 
       observer = T.observer();
       rangeObserver = array.addRangeObserver(SC.IndexSet.create(2,3), 
-                observer, observer.rangeDidChange, null, YES);
+                observer, observer.rangeDidChange, null, NO);
       
     },
     
@@ -36,48 +36,50 @@ SC.ArraySuite.define(function(T) {
     ok(rangeObserver &amp;&amp; rangeObserver.isRangeObserver, 'returns a range observer object');
   });
 
-  // ..........................................................
-  // EDIT PROPERTIES
+  // NOTE: Deep Property Observing is disabled for SproutCore 1.0
+  //
+  // // ..........................................................
+  // // EDIT PROPERTIES
+  // // 
+  //
+  // test(&quot;editing property on object in range should fire observer&quot;, function() {
+  //   var obj = array.objectAt(3);
+  //   obj.set('foo', 'BAR');
+  //   observer.expectRangeChange(array, obj, 'foo', SC.IndexSet.create(3));
+  // });
   // 
+  // test(&quot;editing property on object outside of range should NOT fire observer&quot;, function() {
+  //   var obj = array.objectAt(0);
+  //   obj.set('foo', 'BAR');
+  //   equals(observer.callCount, 0, 'observer should not fire');
+  // });
+  // 
+  // 
+  // test(&quot;updating property after changing observer range&quot;, function() {
+  //   array.updateRangeObserver(rangeObserver, SC.IndexSet.create(8,2));
+  //   observer.callCount = 0 ;// reset b/c callback should happen here
+  // 
+  //   var obj = array.objectAt(3);
+  //   obj.set('foo', 'BAR');
+  //   equals(observer.callCount, 0, 'modifying object in old range should not fire observer');
+  //   
+  //   obj = array.objectAt(9);
+  //   obj.set('foo', 'BAR');
+  //   observer.expectRangeChange(array, obj, 'foo', SC.IndexSet.create(9));
+  //   
+  // });
+  // 
+  // test(&quot;updating a property after removing an range should not longer update&quot;, function() {
+  //   array.removeRangeObserver(rangeObserver);
+  // 
+  //   observer.callCount = 0 ;// reset b/c callback should happen here
+  // 
+  //   var obj = array.objectAt(3);
+  //   obj.set('foo', 'BAR');
+  //   equals(observer.callCount, 0, 'modifying object in old range should not fire observer');
+  //   
+  // });
 
-  test(&quot;editing property on object in range should fire observer&quot;, function() {
-    var obj = array.objectAt(3);
-    obj.set('foo', 'BAR');
-    observer.expectRangeChange(array, obj, 'foo', SC.IndexSet.create(3));
-  });
-  
-  test(&quot;editing property on object outside of range should NOT fire observer&quot;, function() {
-    var obj = array.objectAt(0);
-    obj.set('foo', 'BAR');
-    equals(observer.callCount, 0, 'observer should not fire');
-  });
-  
-  
-  test(&quot;updating property after changing observer range&quot;, function() {
-    array.updateRangeObserver(rangeObserver, SC.IndexSet.create(8,2));
-    observer.callCount = 0 ;// reset b/c callback should happen here
-
-    var obj = array.objectAt(3);
-    obj.set('foo', 'BAR');
-    equals(observer.callCount, 0, 'modifying object in old range should not fire observer');
-    
-    obj = array.objectAt(9);
-    obj.set('foo', 'BAR');
-    observer.expectRangeChange(array, obj, 'foo', SC.IndexSet.create(9));
-    
-  });
-  
-  test(&quot;updating a property after removing an range should not longer update&quot;, function() {
-    array.removeRangeObserver(rangeObserver);
-
-    observer.callCount = 0 ;// reset b/c callback should happen here
-
-    var obj = array.objectAt(3);
-    obj.set('foo', 'BAR');
-    equals(observer.callCount, 0, 'modifying object in old range should not fire observer');
-    
-  });
-  
   // ..........................................................
   // REPLACE
   // 
@@ -196,14 +198,14 @@ SC.ArraySuite.define(function(T) {
   // REMOVING
   // 
   
-  test(&quot;removeAt IN range fires observer with index set covering edit to end of array&quot;, function() {
-    var set     = SC.IndexSet.create(3,array.get('length')-4);
+  test(&quot;removeAt IN range fires observer with index set covering edit to end of array plus delta&quot;, function() {
+    var set     = SC.IndexSet.create(3,array.get('length')-3);
     array.removeAt(3);
     observer.expectRangeChange(array, null, '[]', set);
   });
 
-  test(&quot;removeAt BEFORE range fires observer with index set covering edit to end of array&quot;, function() {
-    var set     = SC.IndexSet.create(0,array.get('length')-1);
+  test(&quot;removeAt BEFORE range fires observer with index set covering edit to end of array plus delta&quot;, function() {
+    var set     = SC.IndexSet.create(0,array.get('length'));
     array.removeAt(0);
     observer.expectRangeChange(array, null, '[]', set);
   });
@@ -216,8 +218,114 @@ SC.ArraySuite.define(function(T) {
   
   
   
+  // ..........................................................
+  // MODULE: No explicit range
+  // 
+  module(T.desc(&quot;RangeObserver Methods - No explicit range&quot;), {
+    setup: function() {
+      expected = T.objects(10);
+      array = T.newObject(expected);
+
+      observer = T.observer();
+      rangeObserver = array.addRangeObserver(null, observer, 
+                          observer.rangeDidChange, null, NO);
+      
+    },
+    
+    teardown: function() {
+      T.destroyObject(array);
+    }
+  });
   
+  test(&quot;returns RangeObserver object&quot;, function() {
+    ok(rangeObserver &amp;&amp; rangeObserver.isRangeObserver, 'returns a range observer object');
+  });
+
+  // ..........................................................
+  // REPLACE
+  // 
+
+  test(&quot;replacing object in range fires observer with index set covering only the effected item&quot;, function() {
+    array.replace(2, 1, T.objects(1));
+    observer.expectRangeChange(array, null, '[]', SC.IndexSet.create(2,1));
+  });
+
+  test(&quot;replacing at start of array&quot;, function() {
+    array.replace(0, 1, T.objects(1));
+    observer.expectRangeChange(array, null, '[]', SC.IndexSet.create(0,1));
+  });
+
+  test(&quot;replacing object at end of array&quot;, function() {
+    array.replace(9, 1, T.objects(1));
+    observer.expectRangeChange(array, null, '[]', SC.IndexSet.create(9,1));
+  });
+
+  test(&quot;removing range should no longer fire observers&quot;, function() {
+    array.removeRangeObserver(rangeObserver);
+    
+    observer.callCount = 0 ;
+    array.replace(2, 1, T.objects(1));
+    equals(observer.callCount, 0, 'observer should not fire');
+
+    observer.callCount = 0 ;
+    array.replace(0, 1, T.objects(1));
+    equals(observer.callCount, 0, 'observer should not fire');
+
+    observer.callCount = 0 ;
+    array.replace(9, 1, T.objects(1));
+    equals(observer.callCount, 0, 'observer should not fire');
+  });
+
+  // ..........................................................
+  // GROUPED CHANGES
+  // 
   
+  test(&quot;grouping property changes should notify observer only once at end with single IndexSet&quot;, function() {
+    
+    array.beginPropertyChanges();
+    array.replace(2, 1, T.objects(1));
+    array.replace(4, 1, T.objects(1));
+    array.endPropertyChanges();
+    
+    var set = SC.IndexSet.create().add(2).add(4); // both edits
+    observer.expectRangeChange(array, null, '[]', set);
+  });
+
+  // ..........................................................
+  // INSERTING
+  // 
+  
+  test(&quot;insertAt in range fires observer with index set covering edit to end of array&quot;, function() {
+    var newItem = T.objects(1)[0],
+        set     = SC.IndexSet.create(3,array.get('length')-2);
+        
+    array.insertAt(3, newItem);
+    observer.expectRangeChange(array, null, '[]', set);
+  });
+
+  test(&quot;adding object fires observer&quot;, function() {
+    var newItem = T.objects(1)[0];
+    var set = SC.IndexSet.create(array.get('length'));
+
+    array.pushObject(newItem);
+    observer.expectRangeChange(array, null, '[]', set);
+  });
+  
+  // ..........................................................
+  // REMOVING
+  // 
+  
+  test(&quot;removeAt fires observer with index set covering edit to end of array&quot;, function() {
+    var set     = SC.IndexSet.create(3,array.get('length')-3);
+    array.removeAt(3);
+    observer.expectRangeChange(array, null, '[]', set);
+  });
+
+  test(&quot;popObject fires observer with index set covering removed range&quot;, function() {
+    var set = SC.IndexSet.create(array.get('length')-1);
+    array.popObject();
+    observer.expectRangeChange(array, null, '[]', set);
+  });
   
   
   // ..........................................................</diff>
      <filename>frameworks/runtime/debug/test_suites/array/rangeObserver.js</filename>
    </modified>
    <modified>
      <diff>@@ -7,8 +7,8 @@
 
 // note: SC.Observable also enhances array.  make sure we are called after
 // SC.Observable so our version of unknownProperty wins.
-sc_require('mixins/observable') ;
-sc_require('mixins/enumerable') ;
+sc_require('mixins/observable');
+sc_require('mixins/enumerable');
 sc_require('system/range_observer');
 
 SC.OUT_OF_RANGE_EXCEPTION = &quot;Index out of range&quot; ;
@@ -35,59 +35,63 @@ SC.OUT_OF_RANGE_EXCEPTION = &quot;Index out of range&quot; ;
   contents in a KVO-friendly way.  You can also be notified whenever the 
   membership if an array changes by changing the syntax of the property to
   .observes('*myProperty.[]') .
-
+  
   To support SC.Array in your own class, you must override two
   primitives to use it: replace() and objectAt().  
-
+  
   Note that the SC.Array mixin also incorporates the SC.Enumerable mixin.  All
   SC.Array-like objects are also enumerable.
-
+  
   @extends SC.Enumerable
   @since SproutCore 0.9.0
 */
 SC.Array = {
-
-/**
-  @field {Number} length
   
-  Your array must support the length property.  your replace methods should
-  set this property whenever it changes.
-*/
-  // length: 0,
+  /**
+    Walk like a duck - use isSCArray to avoid conflicts
+  */
+  isSCArray: YES,
   
-/**
-  This is one of the primitves you must implement to support SC.Array.  You 
-  should replace amt objects started at idx with the objects in the passed 
-  array.  You should also call this.enumerableContentDidChange() ;
+  /**
+    @field {Number} length
+    
+    Your array must support the length property.  Your replace methods should
+    set this property whenever it changes.
+  */
+  // length: 0,
   
-  @param {Number} idx 
-    Starting index in the array to replace.  If idx &gt;= length, then append to 
-    the end of the array.
-
-  @param {Number} amt 
-    Number of elements that should be removed from the array, starting at 
-    *idx*.
-
-  @param {Array} objects 
-    An array of zero or more objects that should be inserted into the array at 
-    *idx* 
-*/
+  /**
+    This is one of the primitves you must implement to support SC.Array.  You 
+    should replace amt objects started at idx with the objects in the passed 
+    array.  You should also call this.enumerableContentDidChange() ;
+    
+    @param {Number} idx 
+      Starting index in the array to replace.  If idx &gt;= length, then append to 
+      the end of the array.
+      
+    @param {Number} amt 
+      Number of elements that should be removed from the array, starting at 
+      *idx*.
+      
+    @param {Array} objects 
+      An array of zero or more objects that should be inserted into the array at 
+      *idx* 
+  */
   replace: function(idx, amt, objects) {
     throw &quot;replace() must be implemented to support SC.Array&quot; ;
   },
-
-/**
-  This is one of the primitives you must implement to support SC.Array.  
-  Returns the object at the named index.  If your object supports retrieving 
-  the value of an array item using get() (i.e. myArray.get(0)), then you do
-  not need to implement this method yourself.
-  
-  @param {Number} idx
-    The index of the item to return.  If idx exceeds the current length, 
-    return null.
-*/
-  objectAt: function(idx)
-  {
+  
+  /**
+    This is one of the primitives you must implement to support SC.Array.  
+    Returns the object at the named index.  If your object supports retrieving 
+    the value of an array item using get() (i.e. myArray.get(0)), then you do
+    not need to implement this method yourself.
+    
+    @param {Number} idx
+      The index of the item to return.  If idx exceeds the current length, 
+      return null.
+  */
+  objectAt: function(idx) {
     if (idx &lt; 0) return undefined ;
     if (idx &gt;= this.get('length')) return undefined;
     return this.get(idx);
@@ -95,7 +99,7 @@ SC.Array = {
   
   /**
     @field []
-
+    
     This is the handler for the special array content property.  If you get
     this property, it will return this.  If you set this property it a new 
     array, it will replace the current content.
@@ -109,13 +113,13 @@ SC.Array = {
     return this ;
   }.property(),
   
-/**  
-  This will use the primitive replace() method to insert an object at the 
-  specified index.
-  
-  @param {Number} idx index of insert the object at.
-  @param {Object} object object to insert
-*/
+  /**
+    This will use the primitive replace() method to insert an object at the 
+    specified index.
+    
+    @param {Number} idx index of insert the object at.
+    @param {Object} object object to insert
+  */
   insertAt: function(idx, object) {
     if (idx &gt; this.get('length')) throw SC.OUT_OF_RANGE_EXCEPTION ;
     this.replace(idx,0,[object]) ;
@@ -129,22 +133,22 @@ SC.Array = {
     
     If you pass a single index or a start and length that is beyond the 
     length this method will throw an SC.OUT_OF_RANGE_EXCEPTION
-  
+    
     @param {Number|SC.IndexSet} start index, start of range, or index set
     @param {Number} length length of passing range
     @returns {Object} receiver
   */
   removeAt: function(start, length) {
-
+    
     var delta = 0, // used to shift range
         empty = [];
     
     if (typeof start === SC.T_NUMBER) {
-
+      
       if ((start &lt; 0) || (start &gt;= this.get('length'))) {
         throw SC.OUT_OF_RANGE_EXCEPTION;
       }
-
+      
       // fast case
       if (length === undefined) {
         this.replace(start,1,empty);
@@ -298,14 +302,14 @@ SC.Array = {
     @param {Object} target object to invoke on change
     @param {String|Function} method the method to invoke
     @param {Object} context optional context
-    @param {Boolean} isDeep set to YES to observe object properties
     @returns {SC.RangeObserver} range observer
   */
-  addRangeObserver: function(indexes, target, method, context, isDeep) {
+  addRangeObserver: function(indexes, target, method, context) {
     var rangeob = this._array_rangeObservers;
-    if (!rangeob) rangeob = this._array_rangeObservers = SC.Set.create() ;
-
+    if (!rangeob) rangeob = this._array_rangeObservers = SC.CoreSet.create() ;
+    
     var C = this.rangeObserverClass ;
+    var isDeep = NO; //disable this feature for now
     var ret = C.create(this, indexes, target, method, context, isDeep) ;
     rangeob.add(ret);
     
@@ -350,7 +354,7 @@ SC.Array = {
     if (rangeob) rangeob.remove(rangeObserver) ; // clear
     return ret ;
   },
-
+  
   /**
     Updates observers with content change.  To support range observers, 
     you must pass three change parameters to this method.  Otherwise this
@@ -373,6 +377,7 @@ SC.Array = {
       if (delta === undefined) delta = 0 ;
       if (delta !== 0 || amt === undefined) {
         length = this.get('length') - start ;
+        if (delta&lt;0) length -= delta; // cover removed range as well
       } else {
         length = amt ;
       }
@@ -483,7 +488,7 @@ if (!Array.prototype.lastIndexOf) {
 // because working with arrays are so common.
 (function() {
   SC.mixin(Array.prototype, {
-
+    
     // primitive for array support.
     replace: function(idx, amt, objects) {
       if (this.isFrozen) throw SC.FROZEN_ERROR ;
@@ -493,7 +498,7 @@ if (!Array.prototype.lastIndexOf) {
         var args = [idx, amt].concat(objects) ;
         this.splice.apply(this,args) ;
       }
-
+      
       // if we replaced exactly the same number of items, then pass only the
       // replaced range.  Otherwise, pass the full remaining array length 
       // since everything has shifted
@@ -501,7 +506,7 @@ if (!Array.prototype.lastIndexOf) {
       this.enumerableContentDidChange(idx, amt, len - amt) ;
       return this ;
     },
-  
+    
     // If you ask for an unknown property, then try to collect the value
     // from member items.
     unknownProperty: function(key, value) {
@@ -519,11 +524,11 @@ if (!Array.prototype.lastIndexOf) {
   if (!indexOf || (indexOf === SC.Array.indexOf)) {
     Array.prototype.indexOf = function(object, startAt) {
       var idx, len = this.length;
-
+      
       if (startAt === undefined) startAt = 0;
       else startAt = (startAt &lt; 0) ? Math.ceil(startAt) : Math.floor(startAt);
       if (startAt &lt; 0) startAt += len;
-
+      
       for(idx=startAt;idx&lt;len;idx++) {
         if (this[idx] === object) return idx ;
       }
@@ -535,11 +540,11 @@ if (!Array.prototype.lastIndexOf) {
   if (!lastIndexOf || (lastIndexOf === SC.Array.lastIndexOf)) {
     Array.prototype.lastIndexOf = function(object, startAt) {
       var idx, len = this.length;
-
+      
       if (startAt === undefined) startAt = len-1;
       else startAt = (startAt &lt; 0) ? Math.ceil(startAt) : Math.floor(startAt);
       if (startAt &lt; 0) startAt += len;
-
+      
       for(idx=startAt;idx&gt;=0;idx--) {
         if (this[idx] === object) return idx ;
       }
@@ -547,5 +552,4 @@ if (!Array.prototype.lastIndexOf) {
     };
   }
   
-})() ;
-
+})();</diff>
      <filename>frameworks/runtime/mixins/array.js</filename>
    </modified>
    <modified>
      <diff>@@ -9,7 +9,7 @@ require('private/observer_set') ;
 
 /*globals logChange */
 
-SC.LOG_OBSERVING = NO ;
+SC.LOG_OBSERVERS = NO ;
 
 /**
   @namespace 
@@ -137,6 +137,9 @@ SC.LOG_OBSERVING = NO ;
 */
 SC.Observable = {
 
+  /** walk like that ol' duck */
+  isObservable: YES,
+  
   /**
     Determines whether observers should be automatically notified of changes
     to a key.
@@ -408,7 +411,7 @@ SC.Observable = {
     this._kvo_revision = (this._kvo_revision || 0) + 1; 
     var level = this._kvo_changeLevel || 0,
         cachedep, idx, dfunc, cache, func,
-        log = SC.LOG_OBSERVING &amp;&amp; !(this.LOG_OBSERVING===NO);
+        log = SC.LOG_OBSERVERS &amp;&amp; !(this.LOG_OBSERVING===NO);
 
     if (this._kvo_cacheable &amp;&amp; (cache = this._kvo_cache)) {
 
@@ -443,7 +446,7 @@ SC.Observable = {
     var suspended ;
     if ((level &gt; 0) || (suspended=SC.Observers.isObservingSuspended)) {
       var changes = this._kvo_changes ;
-      if (!changes) changes = this._kvo_changes = SC.Set.create() ;
+      if (!changes) changes = this._kvo_changes = SC.CoreSet.create() ;
       changes.add(key) ;
       
       if (suspended) {
@@ -566,7 +569,7 @@ SC.Observable = {
     // there are dependent keys, so we need to do the work to find out if 
     // any of them or their dependent keys are cached.
     queue = cached[key] = [];
-    seen  = SC._TMP_SEEN_SET = (SC._TMP_SEEN_SET || SC.Set.create());
+    seen  = SC._TMP_SEEN_SET = (SC._TMP_SEEN_SET || SC.CoreSet.create());
     seen.add(key);
     this._kvo_addCachedDependents(queue, keys, dependents, seen);
     seen.clear(); // reset
@@ -591,9 +594,9 @@ SC.Observable = {
       this._kvo_cloned[kvoKey] = YES ;
       
     // if item does exist but has not been cloned, then clone it.  Note
-    // that all types must implement slice().0
+    // that all types must implement copy().0
     } else if (!this._kvo_cloned[kvoKey]) {
-      ret = this[kvoKey] = ret.slice();
+      ret = this[kvoKey] = ret.copy();
       this._kvo_cloned[kvoKey] = YES; 
     }
     
@@ -686,7 +689,7 @@ SC.Observable = {
       if (target === this) target = null ; // use null for observers only.
       kvoKey = SC.keyFor('_kvo_observers', key);
       this._kvo_for(kvoKey, SC._ObserverSet).add(target, method, context);
-      this._kvo_for('_kvo_observed_keys', SC.Set).add(key) ;
+      this._kvo_for('_kvo_observed_keys', SC.CoreSet).add(key) ;
     }
 
     if (this.didAddObserver) this.didAddObserver(key, target, method);
@@ -742,7 +745,7 @@ SC.Observable = {
         observers = this._kvo_for(kvoKey) ;
         observers.remove(target, method) ;
         if (observers.targets &lt;= 0) {
-          this._kvo_for('_kvo_observed_keys', SC.Set).remove(key);
+          this._kvo_for('_kvo_observed_keys', SC.CoreSet).remove(key);
         }
       }
     }
@@ -894,7 +897,7 @@ SC.Observable = {
     
     SC.Observers.flush(this) ; // hookup as many observers as possible.
 
-    var log = SC.LOG_OBSERVING &amp;&amp; !(this.LOG_OBSERVING===NO) ;
+    var log = SC.LOG_OBSERVERS &amp;&amp; !(this.LOG_OBSERVING===NO) ;
     var observers, changes, dependents, starObservers, idx, keys, rev ;
     var members, membersLength, member, memberLoc, target, method, loc, func ;
     var context, spaces, cache ;
@@ -918,7 +921,7 @@ SC.Observable = {
       
       // save the current set of changes and swap out the kvo_changes so that
       // any set() calls by observers will be saved in a new set.
-      if (!changes) changes = SC.Set.create() ;
+      if (!changes) changes = SC.CoreSet.create() ;
       this._kvo_changes = null ;
 
       // Add the passed key to the changes set.  If a '*' was passed, then
@@ -926,7 +929,7 @@ SC.Observable = {
       // once finished, clear the key so the loop will end.
       if (key === '*') {
         changes.add('*') ;
-        changes.addEach(this._kvo_for('_kvo_observed_keys', SC.Set));
+        changes.addEach(this._kvo_for('_kvo_observed_keys', SC.CoreSet));
 
       } else if (key) changes.add(key) ;
 </diff>
      <filename>frameworks/runtime/mixins/observable.js</filename>
    </modified>
    <modified>
      <diff>@@ -67,7 +67,7 @@ SC.Observers = {
   // need to start observing.
   addPendingRangeObserver: function(observer) {
     var ro = this.rangeObservers;
-    if (!ro) ro = this.rangeObservers = SC.Set.create();
+    if (!ro) ro = this.rangeObservers = SC.CoreSet.create();
     ro.add(observer);
     return this ;
   },
@@ -117,7 +117,7 @@ SC.Observers = {
   },
   
   isObservingSuspended: 0,
-  _pending: SC.Set.create(),
+  _pending: SC.CoreSet.create(),
   
   objectHasPendingChanges: function(obj) {
     this._pending.add(obj) ; // save for later
@@ -134,7 +134,7 @@ SC.Observers = {
     var pending ;
     if(--this.isObservingSuspended &lt;= 0) {
       pending = this._pending ;
-      this._pending = SC.Set.create() ;
+      this._pending = SC.CoreSet.create() ;
       
       var idx, len = pending.length;
       for(idx=0;idx&lt;len;idx++) {</diff>
      <filename>frameworks/runtime/private/observer_queue.js</filename>
    </modified>
    <modified>
      <diff>@@ -27,7 +27,7 @@ SC._ObserverSet = {
     // get the set of methods
     var methods = this[targetGuid] ;
     if (!methods) {
-      methods = this[targetGuid] = SC.Set.create() ;
+      methods = this[targetGuid] = SC.CoreSet.create() ;
       methods.target = target ;
       methods.isTargetSet = YES ; // used for getMembers().
       this.targets++ ;</diff>
      <filename>frameworks/runtime/private/observer_set.js</filename>
    </modified>
    <modified>
      <diff>@@ -12,7 +12,7 @@ require('system/object') ;
   the console.  This should be disabled in production code.  Note that you
   can also enable this from the console or temporarily.
 */
-SC.LOG_BINDING_NOTIFICATIONS = NO ;
+SC.LOG_BINDINGS = NO ;
 
 /**
   Performance paramter.  This will benchmark the time spent firing each 
@@ -483,10 +483,10 @@ SC.Binding = {
     this._transformedBindingValue = v;
   },
   
-  _connectQueue: SC.Set.create(),
-  _alternateConnectQueue: SC.Set.create(),
-  _changeQueue: SC.Set.create(),
-  _alternateChangeQueue: SC.Set.create(),
+  _connectQueue: SC.CoreSet.create(),
+  _alternateConnectQueue: SC.CoreSet.create(),
+  _changeQueue: SC.CoreSet.create(),
+  _alternateChangeQueue: SC.CoreSet.create(),
   _changePending: NO,
 
   /**
@@ -502,7 +502,7 @@ SC.Binding = {
     SC.Observers.suspendPropertyObserving();
 
     var didFlush = NO ;
-    var log = SC.LOG_BINDING_NOTIFICATIONS ;
+    var log = SC.LOG_BINDINGS ;
     
     // connect any bindings
     var queue, binding ;
@@ -554,7 +554,7 @@ SC.Binding = {
     var v = this._bindingValue ;
     var tv = this._transformedBindingValue ;
     var bench = SC.BENCHMARK_BINDING_NOTIFICATIONS ;
-    var log = SC.LOG_BINDING_NOTIFICATIONS ; 
+    var log = SC.LOG_BINDINGS ; 
     
     // the from property value will always be the binding value, update if 
     // needed.</diff>
      <filename>frameworks/runtime/system/binding.js</filename>
    </modified>
    <modified>
      <diff>@@ -198,7 +198,7 @@ SC.IndexSet = SC.mixin({},
     index--; // start with previous index
     
     var content = this._content, 
-        max     = this.get('max');
+        max     = this.get('max'),
         start   = this.rangeStartForIndex(index);
     if (!content) return null;
 
@@ -399,8 +399,6 @@ SC.IndexSet = SC.mixin({},
       
       if (!content) return this; // nothing to do
 
-      //console.log(start.inspect());
-      
       cur = 0 ;
       next = content[0];
       while(next !== 0) {
@@ -1134,3 +1132,4 @@ SC.IndexSet = SC.mixin({},
 }) ;
 
 SC.IndexSet.slice = SC.IndexSet.copy = SC.IndexSet.clone ;
+SC.IndexSet.EMPTY = SC.IndexSet.create().freeze();</diff>
      <filename>frameworks/runtime/system/index_set.js</filename>
    </modified>
    <modified>
      <diff>@@ -359,12 +359,14 @@ SC.mixin(SC.Object, /** @scope SC.Object @static */ {
 // DEFAULT OBJECT INSTANCE
 // 
 SC.Object.prototype = {
-
+  
+  _kvo_enabled: YES,
+  
   /** @private
     This is the first method invoked on a new instance.  It will first apply
     any added properties to the new instance and then calls the real init()
     method.
-
+    
     @param {Array} extensions an array-like object with hashes to apply.
     @returns {Object} receiver
   */
@@ -374,23 +376,23 @@ SC.Object.prototype = {
     for(idx=0;idx&lt;len;idx++) SC._object_extend(this, extensions[idx]) ;
     SC.generateGuid(this) ; // add guid
     this.init() ; // call real init
-
+    
     // Call 'initMixin' methods to automatically setup modules.
     var inits = this.initMixin; len = (inits) ? inits.length : 0 ;
     for(idx=0;idx &lt; len; idx++) inits[idx].call(this);
-
+    
     return this ; // done!
   },
-
-  /** 
+  
+  /**
     You can call this method on an object to mixin one or more hashes of 
     properties on the receiver object.  In addition to simply copying 
     properties, this method will also prepare the properties for use in 
     bindings, computed properties, etc.
-
+    
     If you plan to use this method, you should call it before you call
     the inherited init method from SC.Object or else your instance may not 
-    function properly.  
+    function properly.
     
     h2. Example
     
@@ -398,7 +400,7 @@ SC.Object.prototype = {
       // dynamically apply a mixin specified in an object property
       var MyClass = SC.Object.extend({
          extraMixin: null,
-
+         
          init: function() {
            this.mixin(this.extraMixin);
            sc_super();
@@ -488,7 +490,7 @@ SC.Object.prototype = {
   respondsTo: function( methodName ) {
     return !!(SC.typeOf(this[methodName]) === SC.T_FUNCTION);
   },
-
+  
   /**
     Attemps to invoked the named method, passing the included two arguments.  
     Returns NO if the method is either not implemented or if the handler 
@@ -597,7 +599,11 @@ SC.Object.prototype = {
   /** @private */
   toString: function() {
     if (!this._object_toString) {
-      this._object_toString = &quot;%@:%@&quot;.fmt(SC._object_className(this.constructor), SC.guidFor(this));
+      // only cache the string if the klass name is available
+      var klassName = SC._object_className(this.constructor) ;
+      var string = &quot;%@:%@&quot;.fmt(klassName, SC.guidFor(this));
+      if (klassName) this._object_toString = string ;
+      else return string ;
     } 
     return this._object_toString ;
   },</diff>
      <filename>frameworks/runtime/system/object.js</filename>
    </modified>
    <modified>
      <diff>@@ -48,7 +48,7 @@ SC.RangeObserver = /** SC.RangeObserver.prototype */ {
   create: function(source, indexSet, target, method, context, isDeep) {
     var ret = SC.beget(this);
     ret.source = source;
-    ret.indexes = indexSet;
+    ret.indexes = indexSet ? indexSet.frozenCopy() : null;
     ret.target = target;
     ret.method = method;
     ret.context = context ;
@@ -91,7 +91,9 @@ SC.RangeObserver = /** SC.RangeObserver.prototype */ {
     @returns {SC.RangeObserver} receiver
   */
   update: function(source, indexSet) {
-    this.indexes = indexSet ;
+    if (this.indexes &amp;&amp; this.indexes.isEqual(indexSet)) return this ;
+    
+    this.indexes = indexSet ? indexSet.frozenCopy() : null ;
     this.endObserving().beginObserving();
     return this;
   },
@@ -107,7 +109,7 @@ SC.RangeObserver = /** SC.RangeObserver.prototype */ {
     if (!this.isDeep) return this; // nothing to do
     
     var observing = this.observing;
-    if (!observing) observing = this.observing = SC.Set.create();
+    if (!observing) observing = this.observing = SC.CoreSet.create();
     
     // cache iterator function to keep things fast
     var func = this._beginObservingForEach;
@@ -226,7 +228,8 @@ SC.RangeObserver = /** SC.RangeObserver.prototype */ {
     @returns {SC.RangeObserver} receiver
   */
   rangeDidChange: function(changes) {
-    if (!changes || this.indexes.intersects(changes)) {
+    var indexes = this.indexes;
+    if (!changes || !indexes || indexes.intersects(changes)) {
       this.endObserving(); // remove old observers
       this.method.call(this.target, this.source, null, '[]', changes, this.context);
       this.beginObserving(); // setup new ones
@@ -249,7 +252,7 @@ SC.RangeObserver = /** SC.RangeObserver.prototype */ {
       
     // lazily convert index to IndexSet.  
     if (index &amp;&amp; !index.isIndexSet) {
-      index = this[guid] = SC.IndexSet.create(index);
+      index = this[guid] = SC.IndexSet.create(index).freeze();
     }
     
     if (context) {</diff>
      <filename>frameworks/runtime/system/range_observer.js</filename>
    </modified>
    <modified>
      <diff>@@ -50,7 +50,10 @@ SC.RunLoop = SC.Object.extend(/** @scope SC.RunLoop.prototype */ {
     @returns {SC.RunLoop} receiver
   */
   beginRunLoop: function() {
-    this._start = new Date().getTime() ; // can't use Date.now() in runtime 
+    this._start = new Date().getTime() ; // can't use Date.now() in runtime
+    if (SC.LOG_BINDINGS || SC.LOG_OBSERVERS) {
+      console.log(&quot;-- SC.RunLoop.beginRunLoop at %@&quot;.fmt(this._start));
+    } 
     return this ; 
   },
   
@@ -64,18 +67,26 @@ SC.RunLoop = SC.Object.extend(/** @scope SC.RunLoop.prototype */ {
     @returns {SC.RunLoop} receiver
   */
   endRunLoop: function() {
-    
     // at the end of a runloop, flush all the delayed actions we may have 
     // stored up.  Note that if any of these queues actually run, we will 
     // step through all of them again.  This way any changes get flushed
     // out completely.
     var didChange ;
+
+    if (SC.LOG_BINDINGS || SC.LOG_OBSERVERS) {
+      console.log(&quot;-- SC.RunLoop.endRunLoop ~ flushing application queues&quot;);
+    } 
     
     do {
       didChange = this.flushApplicationQueues() ;
       if (!didChange) didChange = this._flushinvokeLastQueue() ; 
     } while(didChange) ;
     this._start = null ;
+
+    if (SC.LOG_BINDINGS || SC.LOG_OBSERVERS) {
+      console.log(&quot;-- SC.RunLoop.endRunLoop ~ End&quot;);
+    } 
+    
     return this ; 
   },
   </diff>
      <filename>frameworks/runtime/system/run_loop.js</filename>
    </modified>
    <modified>
      <diff>@@ -287,7 +287,7 @@ SC.SelectionSet = SC.Object.extend(SC.Enumerable, SC.Freezable, SC.Copyable, {
    Clones the set into a new set.  
   */
   clone: function() {
-    var ret  = this.constructor.create(),
+    var ret  = this.constructor.create({ length: this.length }),
         sets = this._sets,
         len  = sets ? sets.length : 0 ,
         idx, set;
@@ -296,7 +296,7 @@ SC.SelectionSet = SC.Object.extend(SC.Enumerable, SC.Freezable, SC.Copyable, {
       sets = ret._sets = sets.slice();
       for(idx=0;idx&lt;len;idx++) {
         if (!(set = sets[idx])) continue ;
-        set = sets[idx] = set.clone();
+        set = sets[idx] = set.copy();
         ret[SC.guidFor(set.source)] = idx;
       }
     }
@@ -304,6 +304,23 @@ SC.SelectionSet = SC.Object.extend(SC.Enumerable, SC.Freezable, SC.Copyable, {
     return ret ;
   },
   
+  /**
+    @private 
+    
+    Freezing a SelectionSet also freezes its internal sets.
+  */
+  freeze: function() {
+    if (this.isFrozen) return this ;
+    var sets = this._sets,
+        loc  = sets ? sets.length : 0,
+        set ;
+        
+    while(--loc &gt;= 0) {
+      if (set = sets[loc]) set.freeze();
+    }
+    return sc_super();
+  },
+  
   // ..........................................................
   // ITERATORS
   // 
@@ -318,8 +335,11 @@ SC.SelectionSet = SC.Object.extend(SC.Enumerable, SC.Freezable, SC.Copyable, {
   
   firstObject: function() {
     if (this.get('length')===0) return undefined;
-    var sets = this._sets;
-    return sets ? sets[0].firstObject() : undefined;
+    var sets = this._sets, 
+        set  = sets ? sets[0] : null,
+        src  = set ? set.source : null,
+        idx  = set ? set.firstObject() : -1;
+    return (src &amp;&amp; idx&gt;=0) ? src.objectAt(idx) : undefined;
   }.property(),
   
   /**
@@ -464,3 +484,5 @@ SC.SelectionSet = SC.Object.extend(SC.Enumerable, SC.Freezable, SC.Copyable, {
 });
 
 SC.SelectionSet.prototype.copy = SC.SelectionSet.prototype.clone;
+SC.SelectionSet.EMPTY = SC.SelectionSet.create().freeze();
+</diff>
      <filename>frameworks/runtime/system/selection_set.js</filename>
    </modified>
    <modified>
      <diff>@@ -10,6 +10,16 @@ sc_require('mixins/observable') ;
 sc_require('mixins/freezable');
 sc_require('mixins/copyable');
 
+// IMPORTANT NOTE:  This file actually defines two classes: 
+// SC.Set is a fully observable set class documented below. 
+// SC._CoreSet is just like SC.Set but is not observable.  This is required
+// because SC.Observable is built on using sets and requires sets without 
+// observability.
+//
+// We use pointer swizzling below to swap around the actual definitions so 
+// that the documentation will turn out right.  (The docs should only 
+// define SC.Set - not SC._CoreSet)
+
 /**
   @class 
 
@@ -26,10 +36,10 @@ sc_require('mixins/copyable');
 
   h1. Creating a Set
 
-  You can create a set like you would most objects using SC.Set.create() or
-  new SC.Set().  Most new sets you create will be empty, but you can also
-  initialize the set with some content by passing an array or other enumerable
-  of objects to the constructor.
+  You can create a set like you would most objects using SC.Set.create().  
+  Most new sets you create will be empty, but you can also initialize the set 
+  with some content by passing an array or other enumerable of objects to the 
+  constructor.
 
   Finally, you can pass in an existing set and the set will be copied.  You
   can also create a copy of a set by calling SC.Set#clone().
@@ -75,26 +85,58 @@ sc_require('mixins/copyable');
   object's _guid but if you implement the hash() method on the object, it will
   use the return value from that method instead.
 
-  @extends Object
   @extends SC.Enumerable 
   @extends SC.Observable
-  @since SproutCore 0.9.15
+  @extends SC.Copyable
+  @extends SC.Freezable
+
+  @since SproutCore 1.0
 */
-SC.Set = function(items) {
-  this.initObservable();
-  if (items &amp;&amp; items.length &gt; 0) {
-    var idx = items.get ? items.get('length') : items.length ;
-    if (items.objectAt) {
-      while(--idx &gt;= 0) this.add(items.objectAt(idx)) ;
-    } else {
-      while(--idx &gt;= 0) this.add(items[idx]) ;
+SC.Set = SC.mixin({}, 
+  SC.Enumerable, 
+  SC.Observable, 
+  SC.Freezable, 
+  SC.Observable,
+/** @scope SC.Set.prototype */ {
+
+  /** 
+    Creates a new set, with the optional array of items included in the 
+    return set.
+
+    @param {SC.Enumerable} items items to add
+    @return {SC.Set}
+  */
+  create: function(items) {
+    var ret, idx, pool = SC.Set._pool, isObservable = this.isObservable;
+    if (!isObservable &amp;&amp; items===undefined &amp;&amp; pool.length&gt;0) ret = pool.pop();
+    else {
+      ret = SC.beget(this);
+      if (isObservable) ret.initObservable();
+      
+      if (items &amp;&amp; items.isEnumerable &amp;&amp; items.get('length')&gt;0) {
+
+        ret.isObservable = NO; // suspend change notifications
+        
+        // arrays and sets get special treatment to make them a bit faster
+        if (items.isSCArray) {
+          idx = items.get ? items.get('length') : items.length;
+          while(--idx&gt;=0) ret.add(items.objectAt(idx));
+        
+        } else if (items.isSet) {
+          idx = items.length;
+          while(--idx&gt;=0) ret.add(items[idx]);
+          
+        // otherwise use standard SC.Enumerable API
+        } else items.forEach(function(i) { ret.add(i); }, this);
+        
+        ret.isObservable = isObservable;
+      }
     }
-  }
-  return this ;
-} ;
-
-SC.Set.prototype = {
-
+    return ret ;
+  },
+  
+  isSet: YES,
+  
   /**
     This property will change as the number of objects in the set changes.
 
@@ -128,6 +170,27 @@ SC.Set.prototype = {
     var idx = this[SC.hashFor(obj)] ;
     return (!SC.none(idx) &amp;&amp; (idx &lt; this.length) &amp;&amp; (this[idx]===obj)) ;
   },
+  
+  /**
+    Returns YES if the passed object is also a set that contains the same 
+    objects as the receiver.
+  
+    @param {SC.Set} obj the other object
+    @returns {Boolean}
+  */
+  isEqual: function(obj) {
+    // fail fast
+    if (!obj || !obj.isSet || (obj.get('length') !== this.get('length'))) {
+      return NO ;
+    }
+    
+    var loc = this.get('length');
+    while(--loc&gt;=0) {
+      if (!obj.contains(this[loc])) return NO ;
+    }
+    
+    return YES;
+  },
 
   /**
     Call this method to add an object. performs a basic add.
@@ -146,27 +209,39 @@ SC.Set.prototype = {
     var guid = SC.hashFor(obj) ;
     var idx = this[guid] ;
     var len = this.length ;
-    if (SC.none(idx) || (idx &gt;= len) || (this[idx] !== obj)) {
+    if ((idx===null || idx===undefined) || (idx &gt;= len) || (this[idx]!==obj)){
       this[len] = obj ;
       this[guid] = len ;
       this.length = len+1;
     }
     
+    if (this.isObservable) this.enumerableContentDidChange();
+    
     return this ;
   },
 
   /**
-    Add all the items in the passed array.
+    Add all the items in the passed array or enumerable
   */
   addEach: function(objects) {
     if (this.isFrozen) throw SC.FROZEN_ERROR;
+    if (!objects || !objects.isEnumerable) {
+      throw &quot;%@.addEach must pass enumerable&quot;.fmt(this);
+    }
 
-    var idx = objects.get('length') ;
-    if (objects.objectAt) {
+    var idx, isObservable = this.isObservable ;
+    
+    if (isObservable) this.beginPropertyChanges();
+    if (objects.isSCArray) {
+      idx = objects.get('length');
       while(--idx &gt;= 0) this.add(objects.objectAt(idx)) ;
-    } else {
-      while(--idx &gt;= 0) this.add(objects[idx]) ;
-    }
+    } else if (objects.isSet) {
+      idx = objects.length;
+      while(--idx&gt;=0) this.add(objects[idx]);
+      
+    } else objects.forEach(function(i) { this.add(i); }, this);
+    if (isObservable) this.endPropertyChanges();
+    
     return this ;
   },  
 
@@ -201,6 +276,7 @@ SC.Set.prototype = {
 
     // reduce the length
     this.length = len-1;
+    if (this.isObservable) this.enumerableContentDidChange();
     return this ;
   },
 
@@ -221,12 +297,22 @@ SC.Set.prototype = {
   */
   removeEach: function(objects) {
     if (this.isFrozen) throw SC.FROZEN_ERROR;
-    var idx = objects.get('length') ;
-    if (objects.objectAt) {
-      while(--idx &gt;= 0) this.remove(objects.objectAt(idx)) ;
-    } else {
-      while(--idx &gt;= 0) this.remove(objects[idx]) ;
+    if (!objects || !objects.isEnumerable) {
+      throw &quot;%@.addEach must pass enumerable&quot;.fmt(this);
     }
+
+    var idx, isObservable = this.isObservable ;
+    
+    if (isObservable) this.beginPropertyChanges();
+    if (objects.isSCArray) {
+      idx = objects.get('length');
+      while(--idx &gt;= 0) this.remove(objects.objectAt(idx)) ;
+    } else if (objects.isSet) {
+      idx = objects.length;
+      while(--idx&gt;=0) this.remove(objects[idx]);
+    } else objects.forEach(function(i) { this.remove(i); }, this);
+    if (isObservable) this.endPropertyChanges();
+    
     return this ;
   },  
 
@@ -234,7 +320,7 @@ SC.Set.prototype = {
    Clones the set into a new set.  
   */
   clone: function() {
-    return SC.Set.create(this);    
+    return this.constructor.create(this);    
   },
 
   /**
@@ -242,42 +328,49 @@ SC.Set.prototype = {
   */
   destroy: function() {
     this.isFrozen = NO ; // unfreeze to return to pool
-    SC.Set._pool.push(this.clear());
+    if (!this.isObservable) SC.Set._pool.push(this.clear());
     return this;
   },
   
   // .......................................
   // PRIVATE 
   //
-  _each: function(iterator) {
-    var len = this.length ;
-    for(var idx=0;idx&lt;len;idx++) iterator(this[idx]) ;
+
+  // optimized
+  forEach: function(iterator, target) {
+    var len = this.length;
+    if (!target) target = this ;
+    for(var idx=0;idx&lt;len;idx++) iterator.call(target, this[idx], idx, this);
+    return this ;
   },
 
   toString: function() {
-    return &quot;SC.Set&lt;%@&gt;&quot;.fmt(SC.$A(this)) ;
-  }  
+    var len = this.length, idx, ary = [];
+    for(idx=0;idx&lt;len;idx++) ary[idx] = this[idx];
+    return &quot;SC.Set&lt;%@&gt;&quot;.fmt(ary.join(',')) ;
+  },
+  
+  // the pool used for non-observable sets
+  _pool: [],
+  
+  isObservable: YES
 
-} ;
+}) ;
 
-// Make this enumerable and observable
-SC.mixin(SC.Set.prototype, SC.Enumerable, SC.Observable, SC.Freezable, SC.Observable) ;
+SC.Set.constructor = SC.Set;
 
-SC.Set.prototype.slice = SC.Set.prototype.copy = SC.Set.prototype.clone ;
+// Make SC.Set look a bit more like other enumerables
+SC.Set.copy = SC.Set.clone ;
+SC.Set.push = SC.Set.unshift = SC.Set.add ;
+SC.Set.shift = SC.Set.pop ;
 
-SC.Set.prototype.push = SC.Set.prototype.unshift = SC.Set.prototype.add ;
-SC.Set.prototype.shift = SC.Set.prototype.pop ;
+// add generic add/remove enumerable support
+SC.Set.addObject = SC.Set.add ;
+SC.Set.removeObject = SC.Set.remove;
 
 SC.Set._pool = [];
 
-/**
-  To create a set, pass an array of items instead of a hash.
-*/
-SC.Set.create = function(items) { 
-  var pool;
-  if (items === undefined &amp;&amp; (pool = SC.Set._pool).length&gt;0) {
-    return pool.pop();
-  } else {
-    return new SC.Set(items);  
-  }
-};
+// CoreSet is like Set but not observable
+SC.CoreSet = SC.beget(SC.Set);
+SC.CoreSet.isObservable = NO ;
+SC.CoreSet.constructor = SC.CoreSet;</diff>
      <filename>frameworks/runtime/system/set.js</filename>
    </modified>
    <modified>
      <diff>@@ -15,5 +15,5 @@ test(&quot;should be able to resolve an object on the window&quot;, function() {
   // verify we can resolve our binding path
   same(SC.objectForPropertyPath('myGlobal'), { test: 'this '}) ;
   
-  delete window.myGlobal ;
+  window.myGlobal =null ;
 });</diff>
      <filename>frameworks/runtime/tests/core/objectForPropertyPath.js</filename>
    </modified>
    <modified>
      <diff>@@ -39,7 +39,7 @@ function validate(set, expected, defaultSource) {
   }
 }
 // ..........................................................
-// BASIC ADDS
+// BASIC REMOVES
 // 
 
 test(&quot;Removed indexes for single source&quot;, function() {</diff>
      <filename>frameworks/runtime/tests/system/selection_set/remove.js</filename>
    </modified>
    <modified>
      <diff>@@ -22,23 +22,12 @@ module(&quot;creating SC.Set instances&quot;, {
   
 });
 
-test(&quot;new SC.Set() should create empty set&quot;, function() {
-  var set = new SC.Set() ;
+test(&quot;SC.Set.create() should create empty set&quot;, function() {
+  var set = SC.Set.create() ;
   equals(set.length, 0) ;
 });
 
-test(&quot;new SC.Set([1,2,3]) should create set with three items in them&quot;, function() {
-  var set = new SC.Set([a,b,c]) ;
-  equals(set.length, 3) ;
-  equals(set.contains(a), YES) ;
-  equals(set.contains(b), YES) ;
-  equals(set.contains(c), YES) ;
-});
-
-test(&quot;SC.Set.create() is an alias for new SC.Set()&quot;, function() {
-  var set = SC.Set.create() ;
-  equals(set.length, 0) ;
-  
+test(&quot;SC.Set.create([1,2,3]) should create set with three items in them&quot;, function() {
   var set = SC.Set.create([a,b,c]) ;
   equals(set.length, 3) ;
   equals(set.contains(a), YES) ;
@@ -46,7 +35,7 @@ test(&quot;SC.Set.create() is an alias for new SC.Set()&quot;, function() {
   equals(set.contains(c), YES) ;
 });
 
-test(&quot;new SC.Set() should accept anything that implements SC.Array&quot;, function() {
+test(&quot;SC.Set.create() should accept anything that implements SC.Array&quot;, function() {
   var arrayLikeObject = SC.Object.create(SC.Array, {
     _content: [a,b,c],
     length: 3,
@@ -60,16 +49,6 @@ test(&quot;new SC.Set() should accept anything that implements SC.Array&quot;, function()
   equals(set.contains(c), YES) ;
 });
 
-test(&quot;new SC.Set() should accept anything that looks like an array, even if it does not implement SC.Array&quot;, function() {
-  var arrayLikeObject = { length: 3, '0': a, '1': b, '2': c } ;
-  
-  var set = SC.Set.create(arrayLikeObject) ;
-  equals(set.length, 3) ;
-  equals(set.contains(a), YES) ;
-  equals(set.contains(b), YES) ;
-  equals(set.contains(c), YES) ;
-});
-
 var set ; // global variables
 
 // The tests below also end up testing the contains() method pretty </diff>
      <filename>frameworks/runtime/tests/system/set.js</filename>
    </modified>
    <modified>
      <diff>@@ -271,6 +271,7 @@ CoreTest.Plan = {
       assertions: []
     };
     
+    var msg;
     var name = desc ;
     if (this.currentModule) name = this.currentModule + &quot; module: &quot; + name;
     
@@ -292,7 +293,8 @@ CoreTest.Plan = {
         setup.call(this);
         working.setup_end = this.now();
       } catch(e) {
-        this.error(&quot;Setup exception on &quot; + name + &quot;: &quot; + e.message);
+        msg = (e &amp;&amp; e.toString) ? e.toString() : &quot;(unknown error)&quot;;
+        this.error(&quot;Setup exception on &quot; + name + &quot;: &quot; + msg);
       }
     });
     
@@ -302,11 +304,13 @@ CoreTest.Plan = {
         this.warn(&quot;Test not yet implemented: &quot; + name);
       } else {
         try {
+          if (CoreTest.trace) console.log(&quot;run: &quot; + name);
           this.working.test_begin = this.now();
           func.call(this);
           this.working.test_end = this.now();
         } catch(e) {
-          this.error(&quot;Died on test #&quot; + (this.working.assertions.length + 1) + &quot;: &quot; + e.message);
+          msg = (e &amp;&amp; e.toString) ? e.toString() : &quot;(unknown error)&quot;;
+          this.error(&quot;Died on test #&quot; + (this.working.assertions.length + 1) + &quot;: &quot; + msg);
         }
       }
     });
@@ -318,7 +322,8 @@ CoreTest.Plan = {
         teardown.call(this);
         this.working.teardown_end = this.now();
       } catch(e) {
-        this.error(&quot;Teardown exception on &quot; + name + &quot;: &quot; + e.message);
+        msg = (e &amp;&amp; e.toString) ? e.toString() : &quot;(unknown error)&quot;;
+        this.error(&quot;Teardown exception on &quot; + name + &quot;: &quot; + msg);
       }
     });
     
@@ -331,7 +336,8 @@ CoreTest.Plan = {
           this.reset();
           this.working.total_end = this.working.reset_end = this.now();
         } catch(ex) {
-          this.error(&quot;Reset exception on &quot; + name + &quot;: &quot; + ex.message) ;
+          msg = (ex &amp;&amp; ex.toString) ? ex.toString() : &quot;(unknown error)&quot;;
+          this.error(&quot;Reset exception on &quot; + name + &quot;: &quot; + msg) ;
         }
       }
       </diff>
      <filename>frameworks/testing/system/plan.js</filename>
    </modified>
    <modified>
      <diff>@@ -17,12 +17,15 @@ sc_require('system/plan');
 
   @since SproutCore 1.0
 */
+
+
 CoreTest.Runner = {
   
   /**
     The CoreTest plan.  If not set, a default plan will be created.
   */
   plan: null,
+  errors: null,
   
   create: function() {
     var len = arguments.length,
@@ -90,7 +93,9 @@ CoreTest.Runner = {
     this.flush();
     
     var result = this.report.find('.testresult .status');
-    var str = CoreTest.fmt('&lt;span&gt;Completed %@ tests in %@ msec. &lt;/span&gt;&lt;span class=&quot;total&quot;&gt;%@&lt;/span&gt; total assertions: ', r.tests, r.runtime, r.total);
+    var str = CoreTest.fmt('&lt;span&gt;Completed %@ tests in %@ msec. &lt;/span&gt;'
+              +'&lt;span class=&quot;total&quot;&gt;%@&lt;/span&gt; total assertions: ', r.tests, 
+              r.runtime, r.total);
     
     if (r.passed &gt; 0) {
       str += CoreTest.fmt('&amp;nbsp;&lt;span class=&quot;passed&quot;&gt;%@ passed&lt;/span&gt;', r.passed);
@@ -101,23 +106,28 @@ CoreTest.Runner = {
     }
 
     if (r.errors &gt; 0) {
-      str += CoreTest.fmt('&amp;nbsp;&lt;span class=&quot;errors&quot;&gt;%@ error%@&lt;/span&gt;', r.errors, (r.errors !== 1 ? 's' : ''));
+      str += CoreTest.fmt('&amp;nbsp;&lt;span class=&quot;errors&quot;&gt;%@ error%@&lt;/span&gt;', 
+            r.errors, (r.errors !== 1 ? 's' : ''));
     }
 
     if (r.warnings &gt; 0) {
-      str += CoreTest.fmt('&amp;nbsp;&lt;span class=&quot;warnings&quot;&gt;%@ warnings%@&lt;/span&gt;', r.warnings, (r.warnings !== 1 ? 's' : ''));
+      str += CoreTest.fmt('&amp;nbsp;&lt;span class=&quot;warnings&quot;&gt;%@ warnings%@&lt;/span&gt;',
+            r.warnings, (r.warnings !== 1 ? 's' : ''));
     }
 
     // if all tests passed, disable hiding them.  if some tests failed, hide
     // them by default.
+    this.errors.push('&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;');
     if ((r.failed + r.errors + r.warnings) &gt; 0) {
       this.hidePassedTestsDidChange(); // should be checked by default
     } else {
       this.report.find('.hide-passed').addClass('disabled')
         .find('input').attr('disabled', true);
+      this.errors.length = 0;
     }     
     if(CoreTest.showUI) Q$('.core-test').css(&quot;right&quot;, &quot;360px&quot;);
     result.html(str);
+    CoreTest.errors=this.errors.join('');
   },
   
   planDidRecord: function(plan, module, test, assertions, timings) {
@@ -137,23 +147,33 @@ CoreTest.Runner = {
     var logstr = this.logstr ;
     var errors =this.errors;
     if (!logstr) logstr = this.logstr = [];
-    if (!errors) errors = this.errors = [];
-    logstr.push(CoreTest.fmt('&lt;tr class=&quot;test %@&quot;&gt;&lt;th class=&quot;desc&quot; colspan=&quot;2&quot;&gt;%@ (&lt;span class=&quot;passed&quot;&gt;%@&lt;/span&gt;, &lt;span class=&quot;failed&quot;&gt;%@&lt;/span&gt;, &lt;span class=&quot;errors&quot;&gt;%@&lt;/span&gt;, &lt;span class=&quot;warnings&quot;&gt;%@&lt;/span&gt;)&lt;/th&gt;&lt;/tr&gt;', clean, name, s.passed, s.failed, s.errors, s.warnings));
+    if (!this.errors) this.errors = ['&lt;table style=&quot;border:1px solid&quot;&gt;&lt;thead&gt;'+
+          '&lt;tr&gt;&lt;th class=&quot;desc&quot;&gt;'+navigator.userAgent+'&lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;/tr&gt;'+
+          '&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;'];
+    logstr.push(CoreTest.fmt('&lt;tr class=&quot;test %@&quot;&gt;&lt;th class=&quot;desc&quot; colspan=&quot;2&quot;&gt;'+
+          '%@ (&lt;span class=&quot;passed&quot;&gt;%@&lt;/span&gt;, &lt;span class=&quot;failed&quot;&gt;%@&lt;/span&gt;,'+
+          ' &lt;span class=&quot;errors&quot;&gt;%@&lt;/span&gt;, &lt;span class=&quot;warnings&quot;&gt;%@&lt;/span&gt;)'+
+          '&lt;/th&gt;&lt;/tr&gt;', clean, name, s.passed, s.failed, s.errors, s.warnings));
     if(s.failed&gt;0 || s.errors&gt;0){
-      errors.push(CoreTest.fmt('&lt;tr class=&quot;test %@&quot;&gt;&lt;th class=&quot;desc&quot; colspan=&quot;2&quot;&gt;%@ (&lt;span class=&quot;passed&quot;&gt;%@&lt;/span&gt;, &lt;span class=&quot;failed&quot;&gt;%@&lt;/span&gt;, &lt;span class=&quot;errors&quot;&gt;%@&lt;/span&gt;, &lt;span class=&quot;warnings&quot;&gt;%@&lt;/span&gt;)&lt;/th&gt;&lt;/tr&gt;', clean, name, s.passed, s.failed, s.errors, s.warnings));  
+      this.errors.push(CoreTest.fmt('&lt;tr class=&quot;test %@&quot;&gt;'+
+          '&lt;th style=&quot;background:grey; color:white&quot; class=&quot;desc&quot; colspan=&quot;2&quot;&gt;'+
+          '%@ (&lt;span class=&quot;passed&quot;&gt;%@&lt;/span&gt;, &lt;span class=&quot;failed&quot;&gt;%@&lt;/span&gt;'+
+          ', &lt;span class=&quot;errors&quot;&gt;%@&lt;/span&gt;, &lt;span class=&quot;warnings&quot;&gt;%@&lt;/span&gt;'+
+          ')&lt;/th&gt;&lt;/tr&gt;', clean, name, s.passed, s.failed, s.errors, s.warnings));  
     }
-    //debugger ;
-    //this.logq.append(q);
     
     len = assertions.length;
     for(idx=0;idx&lt;len;idx++) {
       cur = assertions[idx];
       clean = cur.result === CoreTest.OK ? 'clean' : 'dirty';
-      logstr.push(CoreTest.fmt('&lt;tr class=&quot;%@&quot;&gt;&lt;td class=&quot;desc&quot;&gt;%@&lt;/td&gt;&lt;td class=&quot;action %@&quot;&gt;%@&lt;/td&gt;&lt;/tr&gt;', clean, cur.message, cur.result, (cur.result || '').toUpperCase()));
-      if(!cur.result==='dirty'){
-        errors.push(CoreTest.fmt('&lt;tr class=&quot;%@&quot;&gt;&lt;td class=&quot;desc&quot;&gt;%@&lt;/td&gt;&lt;td class=&quot;action %@&quot;&gt;%@&lt;/td&gt;&lt;/tr&gt;', clean, cur.message, cur.result, (cur.result || '').toUpperCase()));
+      logstr.push(CoreTest.fmt('&lt;tr class=&quot;%@&quot;&gt;&lt;td class=&quot;desc&quot;&gt;%@&lt;/td&gt;'
+          +'&lt;td class=&quot;action %@&quot;&gt;%@&lt;/td&gt;&lt;/tr&gt;', clean, cur.message, cur.result, 
+          (cur.result || '').toUpperCase()));
+      if(clean=='dirty'){
+        this.errors.push(CoreTest.fmt('&lt;tr class=&quot;%@&quot;&gt;&lt;td class=&quot;desc&quot;&gt;%@&lt;/td&gt;'
+        +'&lt;td class=&quot;action %@&quot;&gt;%@&lt;/td&gt;&lt;/tr&gt;', clean, cur.message, cur.result,
+        (cur.result || '').toUpperCase()));
       }
-      //this.logq.append(q);
     }
     
     this.testCount++;</diff>
      <filename>frameworks/testing/system/runner.js</filename>
    </modified>
    <modified>
      <diff>@@ -51,5 +51,12 @@ if (!Array.prototype.uniq) {
 }
 
 if (!String.prototype.w) {
-  String.prototype.w = function w() { return this.split(' '); } ;
+  String.prototype.w = function w() { 
+    var ary = [], ary2 = this.split(' '), len = ary2.length ;
+    for (var idx=0; idx&lt;len; ++idx) {
+      var str = ary2[idx] ;
+      if (str.length !== 0) ary.push(str) ; // skip empty strings
+    }
+    return ary ;
+  };
 }
\ No newline at end of file</diff>
      <filename>frameworks/testing/utils.js</filename>
    </modified>
  </modified>
  <removed type="array">
    <removed>
      <filename>frameworks/desktop/mixins/collection_item.js</filename>
    </removed>
    <removed>
      <filename>frameworks/desktop/tests/views/list/methods.js</filename>
    </removed>
    <removed>
      <filename>frameworks/desktop/tests/views/list/ui.js</filename>
    </removed>
    <removed>
      <filename>frameworks/foundation/tests/controllers/array.js</filename>
    </removed>
    <removed>
      <filename>frameworks/foundation/tests/controllers/controller.js</filename>
    </removed>
    <removed>
      <filename>frameworks/foundation/tests/controllers/object.js</filename>
    </removed>
  </removed>
  <parents type="array">
    <parent>
      <id>e0a27edadd1481d777363d45658f15717b3fb903</id>
    </parent>
  </parents>
  <author>
    <name>Charles Jolley</name>
    <email>charles@sproutit.com</email>
  </author>
  <url>http://github.com/sproutit/sproutcore/commit/2b0ca5dbd25c2ca087133ab34720770a721342d9</url>
  <id>2b0ca5dbd25c2ca087133ab34720770a721342d9</id>
  <committed-date>2009-06-01T11:42:11-07:00</committed-date>
  <authored-date>2009-06-01T11:42:11-07:00</authored-date>
  <message>Merge Apple Postback 009

Squashed commit of the following:

commit 8f003b1522a791d917acab93ff999635ac5ea29f
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 27 22:13:14 2009 -0700

    ListView uses minHeight

commit 268469be9fd46594df4665c40567d3d49f754a61
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 27 22:12:54 2009 -0700

    SC.SelectionSet.firstObject() returns object

commit 6aedea096cea4c51e0de2d43101c874d199b32dd
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 27 22:12:42 2009 -0700

    make ArrayController observe content

commit 27f3f6e37295783e2371deba3039d635e5ba4775
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 27 20:35:13 2009 -0700

    Stop absorbing all them keyboard events.

commit d7c1654f72caf9177b8cb7fdf5abfd55b01d4957
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 27 17:47:32 2009 -0700

    Update SC.SelectionSupport - still not happy

commit 5367ce40ce767a1c94e9e9df7ef09249b2e150a5
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 27 17:47:14 2009 -0700

    Cleanup several minor typos - more tests pass

commit 692c9a46f9e69b682b400c468819e46e901d9214
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 27 11:35:30 2009 -0700

    Basic unit test for SC.TreeController

commit 4001dad8af859abd857f44a0a03e88e94f2c476e
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 27 09:55:17 2009 -0700

    Update unit test Target model.

commit 5a7972f46fb72780ff2e48852c15c45225622ab9
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 27 09:55:02 2009 -0700

    Fix typo in SC.TreeController

commit b9b2a40e237ac2ac7973258066dce5b0b85149d3
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 26 22:46:33 2009 -0700

    Initial cut at SC.TreeController

commit aec5999048adfb4d188595f73e00168cce318309
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 26 22:46:23 2009 -0700

    Further unit testing for SC.ArrayController

commit e98a4ecd78421d03fe3958f8aae0b19a47ee56e2
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 26 22:02:56 2009 -0700

    More unit tests for SC.ArrayController

commit 9aec252e4269da6051b16f108e1e6c8787fe3c6b
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 26 22:02:39 2009 -0700

    Better logging for unit test exceptions.

commit 5bbc49575a5392adb3f38224fed94232a74cb12a
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 26 22:02:28 2009 -0700

    Make SC.Set properly notify on changes

    SC.Set did not notify when you added or removed an item.  Added SC.CoreSet which does not do observing for use inside of the SC.Observable.  Otherwise you get infinite recursion.

commit cc37d19ea344b1adc62685ae3c2f92f8f79ce550
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 26 11:45:09 2009 -0700

    New unit tests + fixes for SC.ArrayController

commit 8e970598bc1a9f6c91d2248dd05197c819317da1
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 26 11:44:58 2009 -0700

    CoreTest.trace logs tests

commit 1294a6984fd4a316700682cc2ca193391abf6faa
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 26 11:44:38 2009 -0700

    Remove outdated SC.ArrayController tests

commit 2f8230043a3e8b91f5fbac51e022497fd3c3b412
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Fri May 22 17:20:48 2009 -0700

    Further work on ArrayController.

commit 7f33625cb7f3df7805835ee3dd551cd59c687e56
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Fri May 22 14:53:56 2009 -0700

    Further enhancements to SC.ArrayController

commit 9fe40bcc599183568f67d42ca692936aabb9f54c
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Fri May 22 14:53:37 2009 -0700

    Remove outdate sc_require

commit 2aa1e89ce0ec61e61afac5d02ad2acd02d3527bb
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Fri May 22 11:52:51 2009 -0700

    Initial cut at adding Sc.ArrayController

commit b3c063a3e3bd862b7e61777abc802aa92d3c5b08
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Fri May 22 08:47:35 2009 -0700

    Remove outdated tests for SC.Controller

commit d84fe16276d1f0502a4481ac9bfc124b21dfb10f
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Fri May 22 08:45:39 2009 -0700

    Delete outdate ObjectController tests

commit 5d9a704c7a5b0010b67c69774e3b3e30c3c93bc5
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Fri May 22 08:45:19 2009 -0700

    Finish unit testing SC.ObjectController

commit 816224947806f4717b378c57313bef8cd68774dd
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Thu May 21 23:10:09 2009 -0700

    Cleanup SC.ObjectController + some tests

commit b0d77d40e2298aa63233260de99db250b3fe6ef9
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Thu May 21 23:09:54 2009 -0700

    Move SC.TreeItemObserver et al to Foundation

    This will be used by the SC.TreeController

commit d74d4b5d3025f66312cc96f302329938918afc88
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 20 15:52:59 2009 -0700

    Improve perf slightly.

commit 7b6424dafe9734d219be07609b44f2d262ab3a84
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 20 11:38:42 2009 -0700

    Add support for expanding and collapsing

    Not unit tested yet.

commit ef589d1d8abbbcd5cd122828f897c203d883c233
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 20 08:11:08 2009 -0700

    Expand sample to 10,000 items

commit ae870037bd1b3ec9f4a61c4bd8b1a29ba278f6b2
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 20 08:06:42 2009 -0700

    Add basic outline unit test for ListView

commit 3ee6fbf8128ebe610b726d84590361c245b78caf
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Wed May 20 08:06:35 2009 -0700

    Add support for showing disclosure triangle in ListItem

commit e47328aa5913c56ed35c5cd63457cfa0e633aa31
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 19 23:27:23 2009 -0700

    SC._TreeItemObserver now supports SC.CollectionContent

commit f6730e2fc07d060eda3c24ea2339cf844174263f
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 19 22:19:24 2009 -0700

    Rename SC.CollectionContentDelgate -&gt; SC.CollectionContent

commit 890c65559790171c3e1e576597e97a63a0f1595f
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 19 22:02:43 2009 -0700

    Complete SC._TreeItemObserver class.

    Can now flatten arbitrary tree structures. Yes!

commit 3c2c007256c5f693ae01f522545476f6cef2d34f
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 19 11:23:52 2009 -0700

    Further improvements to SC._TreeItemObserver

    Now passes full battery of unit tests for grouped
    items including notifying a range observer when
    its content is changed.

    Still need to implement modifying the observer
    object and passing that through to content.

commit 7569c9b365a957d45c2f164217309b238b034911
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Tue May 19 11:22:55 2009 -0700

    Fixed: RangeObserver fires when removing object

    RangeObservers would not fire when removing an
    object at the end.  Updated unit tests to reflect
    fix.

commit b341eb774d239ab2c803e46e6a44c4590734627c
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Mon May 18 20:55:07 2009 -0700

    Further progress on SC.TreeItemObserver

commit 03bb197f4d704f0a277e9b1d6d59acae61b14546
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Mon May 18 11:13:53 2009 -0700

    Initial SC._TreeItemObserver class

    Includes basic unit tests without support for
    modifying the underlying array.

commit 3362805d04a65477b9dc3121bba47e33be1207b5
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Fri May 15 12:51:50 2009 -0700

    Add and fix unit tests for ListView

commit 02dd20391a118f2d31fd44e0e4d6c4d15d882252
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Fri May 15 12:51:39 2009 -0700

    Make sure SC.Scroller is always up to date

    If we don't respond to a scroll event, set a 50msec
    timer to ensure we eventually handle it.

commit c8a287ab8b2518f19addf67053704ae1f95aef79
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Thu May 14 22:13:11 2009 -0700

    Squash merge okito/collection work

commit 7e0036bab282f4d2fd0d7bef53caeb2ce0dcd185
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Wed May 27 16:39:58 2009 -0700

    Bug fixes to normalize() and commitRecords()

commit 78ad795ebf616d33339d552adba4c9e48c56666c
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Wed May 27 14:09:19 2009 -0700

    Code cleanup of ruby style JS

commit 97b1a96aa0f7539a23c82f8908351a245496a67d
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Wed May 27 11:22:04 2009 -0700

    Adding more query unit tests

commit 50c0909b4040d26124a7058079b8ba7aa6a101ea
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Tue May 26 16:53:49 2009 -0700

    Fix for IE8 in objectForPropertyPath unit test, changes in tables for sellenium tests.

commit 8dd3dd5168c5fe332ee73dd58583edf73de38485
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Tue May 26 13:53:40 2009 -0700

    Fix for Safari 3.2.3. It was failing when invoking the hasOwnProperty function with certain objects

commit 699146ecaa239e2fefcf9e13312f226dd47526bb
Author: Joshua Dickens &lt;schwa23@jdickens.apple.com&gt;
Date:   Thu Apr 30 15:17:02 2009 -0700

    Modified bootstrap detection code to add 'box-shadow' and 'border-rad' class names when browser supports those properties.

commit 678a5f6f558c833bc05de1f68120efed8cbcdb18
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Fri May 22 11:43:25 2009 -0700

    Fix SC.Record statusString unit test

commit d0be0e50d7a732a5de10b4f052d7ee4608cb7a31
Author: SantoshShan &lt;sshanbogue@apple.com&gt;
Date:   Thu May 21 18:04:51 2009 -0700

    Some more changes to improve the Menu View

commit e4063fe011de944adfe308fe16cda1b88624330e
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Thu May 21 16:44:31 2009 -0700

    null values not properly written in normalize()

commit 285957245e5336b0a7861a509ee4778ae4fa698b
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Thu May 21 16:33:15 2009 -0700

    Only write defaultValue returned if there is a data hash

commit 1ff1c8b519fc1c23e9f954267c6ad77aa8fed1c8
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Thu May 21 16:21:07 2009 -0700

    write default value derived from function so it doesnt have to be computed again

commit d84845a77c9c25312a18332bc41d1108c9a14537
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Thu May 21 14:53:32 2009 -0700

    Fixes to unit test, simplified findAll() and fetch() API and how it interacts with SC.Query

commit 26773f56978223fa502fcbcf51d363bac851d70b
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Wed May 20 18:14:59 2009 -0700

    Moving status of SC.Record to instance method

commit 347bf3db3a2539fefdea931e09f0a1b3a5b66c19
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 20 03:14:58 2009 -0700

    use updated parentStore.willDestroyNestedStore() API

commit 707df600a8e7f011e6b56ab0ec7f74ac4e738e28
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 20 03:14:22 2009 -0700

    only commit changes in a nested store if there are changes to commit

commit 7cd34b3099c96c4cc4abc88cc7447e9281c51b07
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 20 03:01:55 2009 -0700

    added unit tests for improved String.prototype.w() behavior

commit e1af139a7f486b8e443c741a185b48196719d3e2
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 20 02:52:56 2009 -0700

    make String.prototype.w() not return empty strings

commit a4526b7e111e5130ac7c7a69be2436faf3f82fd5
Author: teresa &lt;teresa.tsui@gmail.com&gt;
Date:   Thu May 21 11:32:06 2009 -0700

    update alert box layout

commit 15b517727826a46f14eeac5a424c3184d5952bd5
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Thu May 21 14:10:02 2009 -0700

    Console object fixed to be working in all browsers. Including unit test

commit 0b2340bccaffb296c0179364a94bdf31cc925439
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Thu May 21 13:18:30 2009 -0700

    Fix for centerY method in utils.js and new unit test to probe that the request queue works.

commit 667fbbadddde0e21085eb1d66d1f43bda1111a1f
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Wed May 20 17:34:29 2009 -0700

    Added functionality to SC.Request to auto-serialize  JSON when an object is passed to the send function

commit bbaa530d3d688500ea3286909940c56aecad16b4
Author: Peter Bergstrom &lt;pbergstr@Galactica.local&gt;
Date:   Tue May 19 20:46:18 2009 -0700

    Added correct license to file.

commit 381995c0cf96ca91da2cbefe3246caf65c594c4d
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Tue May 19 15:46:52 2009 -0700

    Adding ids to retrieveRecords() and fixture data source fix

commit 6126e22df47e8d2ff8f908e3cf359ca7a27fa114
Author: teresa &lt;teresa.tsui@gmail.com&gt;
Date:   Tue May 19 15:32:27 2009 -0700

    update alert buttonThreeWrapper isVisible so it won't block click through zone when button3 is null

commit d276574bdaad031bfec721ad71cfb4257d467a1b
Author: Peter Bergstrom &lt;pbergstr@Galactica.local&gt;
Date:   Mon May 18 09:30:54 2009 -0700

    Benchmarking.

commit 2a01557965c3ee6257f55087e9fe31e202359b43
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Mon May 18 17:40:29 2009 -0700

    BFS algorithm for findLayerInParentLayer

commit 24215c13bae7fb81204c83f3540b755a98107dd1
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Mon May 18 15:35:56 2009 -0700

    Errors formated in a table to be retrieved by sellenium

commit ca95d470806c9509496213d463b2443d19bf93d7
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Mon May 18 12:03:54 2009 -0700

    Changes to runner to generate table with only errors

commit 4345a288a152b22bad0252f7bf43cc7f66625a23
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Fri May 15 16:08:17 2009 -0700

    Added statusToString() method and cleaned up normalize()

commit 3a5f24978f1939de4ad16f52c6f800f3a61c3cdf
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Wed May 13 14:36:44 2009 -0700

    Added storeKeyExists() helper method and more normalize unit tests

commit 412e456809b78cdfe377b0fae26c42c9a7611f79
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Mon May 11 15:37:21 2009 -0700

    Fix to normalize writing toOne relationships

commit 12deee037b8c50cab3e1f18bd57e21cae29ca3ec
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Mon May 11 15:24:45 2009 -0700

    Fixes to normalize() and some more unit tests

commit d7d04adf87be647044ff9c9a2bfa5764630eb4a4
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Mon May 11 13:56:55 2009 -0700

    Better tests for computed default values

commit 4f04329a01be406022d15889dd94f570d7e65098
Author: Charles Jolley &lt;charles@sproutit.com&gt;
Date:   Thu May 14 11:31:34 2009 -0700

    Unit tests for SC.SelectionSet

    Specifically for isEqual() and copy()

commit c13504ccbdda636fc5d1f95fad75542e24148f48
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 13 23:19:38 2009 -0700

    remove initStatechart handling; this is not a part of SproutCore

commit b33c83a32bcf5f9bd815c39bf80231fc4a8c15dd
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 13 22:18:15 2009 -0700

    removed statechart framework, it's not part of SproutCore

commit b7aea228f8c5fd0ffb2c0d4c2c5b507f9fac788a
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 13 17:33:16 2009 -0700

    updated documentation

commit 757096417c48d711e4f2aa9cb7aa4f63df0700ac
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 13 17:26:30 2009 -0700

    implement (add|remove)StatechartObserver() methods on SC.Object

commit 31fcb4546a43bbb1b48378c145359099b2d59427
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 13 17:25:54 2009 -0700

    don't allow disptach to be called recursively

commit 889ec3d7e8ff216dc395c489a34d4b2a52a381ab
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 13 17:25:12 2009 -0700

    show the current state above the current event, to allow for more room

commit feff1342176269eba2972e3086bb19b17177dba5
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 13 17:24:46 2009 -0700

    only trace mouse* events when StachartDebugger.LOG_MOUSE_EVENTS is YES

commit dd2717b4ddca39b930716256b07cfa18c90de158
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 13 17:23:45 2009 -0700

    only removea pane if it already attached

commit 03235075ca65b3b7fe68710903673d86f7e6e41e
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 13 15:11:33 2009 -0700

    call statechart init if defined, but wait until SproutCore is ready

commit fda96704be1e6f32be44e3f4d0f9e960d3078475
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed May 13 14:50:52 2009 -0700

    monkeypatch createChildViews() in posing SC.MainPane from statechart/debug mode

commit 9152462d3c88e82ee5ba8cf77addccb2b382b03a
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sun May 10 19:56:55 2009 -0700

    brinhg in Eloqua changes to SC.Drag

commit e3d3de58e99812f032ddcada5f0b01c5ee332a54
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Thu May 7 00:05:39 2009 -0700

    remove testing-only button view in debug bar

commit 2afa4fa7a35720600e9e0e9471ef90e852d8314f
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Thu May 7 00:04:42 2009 -0700

    integrating open source statechart debug code

commit 5e390f4ecfd43890356e7221e2eb8bfc75ec2809
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sun Apr 26 00:09:07 2009 +0200

    define the StatechartDebugger variable as null when not present

commit 3d70347252048772e9805d11b0afd8f45f4ac773
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sun Apr 26 00:03:11 2009 +0200

    Revert &quot;refactored source tree, improved file and class names&quot;

    These files were not developed for/by Apple and should not have been checked
    in.

    This reverts commit 13f455be906e23494dffb658d883f6301c20715b.

commit 2027805c46826167a1a4c01c2b1d8394822d3474
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sat Apr 25 23:58:52 2009 +0200

    integrate statechart event delivery into SproutCore's responder system

commit 7a3e35ea23b78a88d22d50c8b00861b50b6f280a
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sat Apr 25 23:57:57 2009 +0200

    added hasStatechart: YES  property when initStatechart() runs

commit 05f6caf9d0e856f2a8fa34bb79f5532630e92c0a
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sat Apr 25 23:57:15 2009 +0200

    add initStatechart() call (if defined) to normal SC.Object.init() method

commit 8c4b67e0030bb1e976e89df3aaad03676eed73f6
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sat Apr 25 23:56:26 2009 +0200

    synthesized events should be actual SC.Event instances too

commit 8983e6540141540fee0e0a4858e20f972bc80120
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sat Apr 25 23:55:43 2009 +0200

    hack to get synthesized keyboard events to work; keydown() method seems broken (still)

commit 6760598f1d371db78ccd0f7ee2dfc2ebd9eafa4d
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Thu Apr 23 11:02:18 2009 +0200

    refactored source tree, improved file and class names

commit 151a35af6d688abeb455ad66b4deff8254d23abd
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed Apr 22 21:21:49 2009 +0200

    changed how state transitions are logged, made the state property (as specified by stateKey) KVO-compliant

commit a60fa0b3d2d9794c7aec0801563cb8c506e6cb76
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed Apr 22 13:34:02 2009 +0200

    don't cache object toString() until class name is avaliable

commit 80f2403c3486e0057a0ceb2bf6ccb249d3860e42
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed Apr 22 13:33:27 2009 +0200

    whitespace, documentation typos

commit fda0d7a1ba31df46645db45953f34721df48ccc0
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed Apr 22 13:31:51 2009 +0200

    added commented-out logging statements

commit 95981faa795315a30a0d069e9cf6a7e2413805c9
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed Apr 22 13:30:08 2009 +0200

    update to use StatechartDebugger when present

commit 113391e58be942798d8ae6d45c1efd203c29ea49
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed Apr 22 13:29:52 2009 +0200

    update to use StatechartDebugger when present

commit ed4a0c40a1af3e4a71362e70592cdec5cf60dd80
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Wed Apr 22 13:29:00 2009 +0200

    whitespace

commit fef2528393b603aff015f0f00262f49832e59303
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 22:30:32 2009 -0700

    added single-step support

commit 4b64e4673de6a06f7a3401b934bb11e168323895
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 22:18:48 2009 -0700

    made event tracing optional

commit e5f6891332cb6f77bde715a8980cfdb89f5344ec
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 22:15:40 2009 -0700

    moved tracing code to debug mode only, fixed filename typo in system/dispatch.js

commit 21004440645fe7f0177e40183434d50bc42d2607
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 22:04:10 2009 -0700

    added helper methed for general event handling

commit 5b88fe7a1255118a1f05fe29ae6a57eb29b69af2
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 16:05:33 2009 -0700

    more logging improvements

commit c8e9e0e2c8bc58eb8223f2e1de7e19df613924c0
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 15:58:00 2009 -0700

    improved statechart loggging; now showing depth via indentation

commit bfba4c9167cb8cd735ce0ff765dd345dce1b3ab6
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 15:38:37 2009 -0700

    updated dispatch() to use state transition helpers

commit 27ebcb55cc01065e967593dccb2adf5f07dfa3af
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 15:38:20 2009 -0700

    whitespace

commit 53f8eed8e2557d7e5a2b98c4ff7759f5aaf74108
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 15:19:01 2009 -0700

    updated initStatechart() to use state transition helpers

commit a57402ed3bda8c041a7fa26edbc4438d76d24269
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 15:18:46 2009 -0700

    added state transition helpers methods

commit ff2fda8a70266f261ffa054ce6ca58ddf4d0ef68
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 15:02:34 2009 -0700

    converted dispatch() to use state keys, not state functions

commit cdd82ad4862a62a7a934f2361ca4fd53aeffcf7b
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 14:45:17 2009 -0700

    converted initStatechart to use state keys, not state functions

commit b234473bba7c1356ed595facdc6f3bdfff0d0d93
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 14:43:45 2009 -0700

    whitespace

commit 7cb5039d142ab137b210502fadf17b39f715fc22
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Fri Apr 17 06:14:59 2009 -0700

    fixed final bug in statechart dispatch() method

commit d228cba57fc2cd3b523a7aeae9e0035db914ba64
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Tue Apr 14 23:05:58 2009 -0700

    fixeds for dispatch() function

commit a7d7e761816be597493be31ffc8315253d209eb8
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Tue Apr 14 12:10:47 2009 -0700

    debugged initStatechart() method; now working

commit 608fd09a586b0db82816e78c7bc9d7157458cc23
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Mon Apr 13 09:00:28 2009 -0700

    added sc_terminate() handler response

commit d2379371d40631a5a49052c2dc10db6e6da60248
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sun Apr 12 17:25:19 2009 -0700

    whitespace

commit 530899b7a7b213669979dbdc7200d23de0a29efc
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sun Apr 12 17:12:14 2009 -0700

    added initStatechart: method

commit 661e3cccee9b7553677297bf0729dc200c51a833
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sun Apr 12 14:06:07 2009 -0700

    missed a few places that were not using the stateKey

commit 3be0560eae3fc80322343f9998c6bcdf28d0a974
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sun Apr 12 14:04:34 2009 -0700

    reorganize code and comments to make algorithm clearer

commit e16e8f00f9e7374ad624d5d07cee4222ecb60864
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sun Apr 12 11:31:45 2009 -0700

    improved docs, removed done variable, code refactored

commit ed243ff4ddf6a48e417e473bb7e2c03b7877303d
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sat Apr 11 21:35:55 2009 -0700

    improved comments

commit 1842e30b36b959d4e0a0afcd910cf8a9d7f63ac4
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sat Apr 11 21:33:52 2009 -0700

    improved comments

commit 8e43649a9ad1cc3ad1bec2eaaa2cbc8d3eaed014
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sat Apr 11 21:31:37 2009 -0700

    cleaneup up documentation, added labeled break to remove if() checks

commit 46b1a49089265d0231f3e9598984a247767a2c90
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sat Apr 11 21:07:22 2009 -0700

    source cleanup, whitespace, added stateKey property

commit 77f7eb56c6634dc47d8402cf74b6ed6c37688ac3
Author: Erich Ocean &lt;erich.ocean@me.com&gt;
Date:   Sat Apr 11 19:10:00 2009 -0700

    first stab at a hierarchical event processor for SproutCore

commit 76a2452086bc1aa9defb68c006c8b76750bdd1f5
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Thu May 14 16:59:15 2009 -0700

    Fix for console. By just doing var console = console; the native console object will not work. I removed the var and it got fixed.

commit 3fdf4714fd81e898897f584b3257ff2aa11884f8
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Thu May 14 16:57:36 2009 -0700

    Fix for findLayerInParentLayer. I still have to replace the algorithm to a BFS search to make it faster. There is a bug in IE where the parentNode is incorrect in some leafs , even though the html is fine.

commit a81b4acc5206b4a22569e5c31e8aca6d5a7212a9
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Wed May 13 12:22:57 2009 -0700

    Failing unit test fixed

commit 43b920e2ac15d481f07e1ce4261abf09cd2ff506
Author: juanpin &lt;juanpin@mac.com&gt;
Date:   Wed May 13 12:16:15 2009 -0700

    Destroy record in the store was removing the datahash before the commit

commit fb8b40d601c7ad61b1a1e343c8ab8af2c6d75cc5
Author: Onar Vikingstad &lt;onar@onarvikingstad.apple.com&gt;
Date:   Wed May 13 14:36:44 2009 -0700

    Added storeKeyExists() helper method and more normalize unit tests</message>
  <tree>6d56135545edc491fa9a7a1c7385280a1c08bc4c</tree>
  <committer>
    <name>Charles Jolley</name>
    <email>charles@sproutit.com</email>
  </committer>
</commit>
