1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require('mixins/tree_item_content');
  9 sc_require('mixins/collection_content');
 10 
 11 /**
 12   @ignore
 13   @class
 14 
 15   A TreeNode is an internal class that will manage a single item in a tree
 16   when trying to display the item in a hierarchy.
 17 
 18   When displaying a tree of objects, a tree item object will be nested to
 19   cover every object that might have child views, ignoring those that will
 20   definitely not. (Any node which has children or may have children should
 21   advertise this by exposing an array at its treeItemChildrenKey property;
 22   any node which does not do so is assumed to be permanently childless, so
 23   we optimize by not observing it. In CS terms, nodes can implicitly
 24   advertise whether they are *leaves-or-branches* and should be observed,
 25   or are *permanently leaves*, and may remain unobserved.)
 26 
 27   TreeNode stores an array which contains either a number pointing to the
 28   next place in the array there is a child item or it contains a child item.
 29 
 30   @extends SC.Object
 31   @extends SC.Array
 32   @extends SC.CollectionContent
 33   @since SproutCore 1.0
 34 */
 35 SC.TreeItemObserver = SC.Object.extend(SC.Array, SC.CollectionContent, {
 36 
 37   //@if(debug)
 38   /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */
 39 
 40   /* @private */
 41   toString: function () {
 42     var item = this.get('item'),
 43       ret = sc_super();
 44 
 45     return item ? "%@:\n  ↳ %@".fmt(ret, item) : ret;
 46   },
 47 
 48   /* END DEBUG ONLY PROPERTIES AND METHODS */
 49   //@endif
 50 
 51   /** @private */
 52   _cachedItem: null,
 53 
 54   /** @private */
 55   _cachedDelegate: null,
 56 
 57   /**
 58     The node in the tree this observer will manage.  Set when creating the
 59     object.  If you are creating an observer manually, you must set this to
 60     a non-null value.
 61   */
 62   item: null,
 63 
 64   /**
 65     The controller delegate.  If the item does not implement the
 66     TreeItemContent method, delegate properties will be used to determine how
 67     to access the content.  Set automatically when a tree item is created.
 68 
 69     If you are creating an observer manually, you must set this to a non-null
 70     value.
 71   */
 72   delegate: null,
 73 
 74   /**
 75     The key used to retrieve children from the observed item. If a
 76     delegate exists, the key will be the value of the `treeItemChildrenKey`
 77     property of the delegate. Otherwise, the key will be `treeItemChildren`.
 78 
 79     @type String
 80     @default 'treeItemChildren'
 81   */
 82   treeItemChildrenKey: 'treeItemChildren',
 83 
 84   /**
 85     The key used to identify the expanded state of the observed item.
 86     If a delegate exists, the key will be the value of the `treeItemIsExpandedKey`
 87     property of the delegate. Otherwise, the key will be `treeItemIsExpanded`.
 88 
 89     @type String
 90     @default 'treeItemIsExpanded'
 91   */
 92   treeItemIsExpandedKey: 'treeItemIsExpanded',
 93 
 94   // ..........................................................
 95   // FOR NESTED OBSERVERS
 96   //
 97 
 98   /**
 99     The parent TreeItemObserver for this observer.  Must be set on create.
100   */
101   parentObserver: null,
102 
103   /**
104     The parent item for the observer item.  Computed automatically from the
105     parent.  If the value of this is null, then this is the root of the tree.
106   */
107   parentItem: function () {
108     var p = this.get('parentObserver');
109     return p ? p.get('item') : null;
110   }.property('parentObserver').cacheable(),
111 
112   /**
113     Index location in parent's children array.  If this is the root item
114     in the tree, should be null.
115   */
116   index: null,
117 
118   outlineLevel: 0,
119 
120   // ..........................................................
121   // EXTRACTED FROM ITEM
122   //
123 
124   /**
125     Array of child tree items.  Extracted from the item automatically on init.
126   */
127   children: null,
128 
129   /**
130     Disclosure state of this item.  Must be SC.BRANCH_OPEN or SC.BRANCH_CLOSED
131     If this is the root of a item tree, the observer will have children but
132     no parent or parent item.  IN this case the disclosure state is always
133     SC.BRANCH_OPEN.
134 
135     @property
136     @type Number
137   */
138   disclosureState: SC.BRANCH_OPEN,
139 
140   /**
141     IndexSet of children with branches.  This will ask the delegate to name
142     these indexes.  The default implementation will iterate over the children
143     of the item but a more optimized version could avoid touching each item.
144 
145     @property
146     @type SC.IndexSet
147   */
148   branchIndexes: function () {
149     var item = this.get('item'),
150         len, pitem, idx, children, ret;
151 
152     // no item - no branches
153     if (!item) return SC.IndexSet.EMPTY;
154 
155     // if item is treeItemContent then ask it directly
156     else if (item.isTreeItemContent) {
157       pitem  = this.get('parentItem');
158       idx    = this.get('index');
159       return item.treeItemBranchIndexes(pitem, idx);
160 
161     // otherwise, loop over children and determine disclosure state for each
162     } else {
163       children = this.get('children');
164       if (!children) return null; // no children - no branches
165       ret = SC.IndexSet.create();
166       len = children.get('length');
167       pitem = item; // save parent
168 
169       for (idx = 0; idx < len; idx++) {
170         item = children.objectAt(idx);
171         if (!item) continue;
172         if (!this._computeChildren(item, pitem, idx)) continue; // no children
173         if (this._computeDisclosureState(item, pitem, idx) !== SC.LEAF_NODE) {
174           ret.add(idx);
175         }
176       }
177 
178       return ret.get('length') > 0 ? ret : null;
179     }
180   }.property('children').cacheable(),
181 
182   /**
183     Returns YES if the item itself should be shown, NO if only its children
184     should be shown.  Normally returns YES unless the parentObject is null.
185   */
186   isHeaderVisible: function () {
187     return !!this.get('parentObserver');
188   }.property('parentObserver').cacheable(),
189 
190   /**
191     Get the current length of the tree item including any of its children.
192   */
193   length: 0,
194 
195   // ..........................................................
196   // SC.ARRAY SUPPORT
197   //
198 
199   /**
200     Get the object at the specified index.  This will talk the tree info
201     to determine the proper place.  The offset should be relative to the
202     start of this tree item.  Calls recursively down the tree.
203 
204     This should only be called with an index you know is in the range of item
205     or its children based on looking at the length.
206 
207     @param {Number} index
208     @param {Boolean} omitMaterializing
209     @returns {Object}
210   */
211   objectAt: function (index, omitMaterializing) {
212     var len   = this.get('length'),
213         item  = this.get('item'),
214         cache = this._objectAtCache,
215         cur   = index,
216         indexes, children;
217 
218     if (index >= len) return undefined;
219     if (this.get('isHeaderVisible')) {
220       if (index === 0) return item;
221       else cur--;
222     }
223     item = null;
224 
225     if (!cache) cache = this._objectAtCache = [];
226     if ((item = cache[index]) !== undefined) return item;
227 
228     children = this.get('children');
229     if (!children) return undefined; // no children - nothing to get
230 
231     // loop through branch indexes, reducing the offset until it matches
232     // something we might actually return.
233     indexes = this.get('branchIndexes');
234     if (indexes) {
235       indexes.forEach(function (i) {
236         if (item || (i > cur)) return; // past end - nothing to do
237 
238         var observer = this.branchObserverAt(i), len;
239         if (!observer) return; // nothing to do
240 
241         // if cur lands inside of this observer's length, use objectAt to get
242         // otherwise, just remove len from cur.
243         len = observer.get('length');
244         if (i + len > cur) {
245           item = observer.objectAt(cur - i, omitMaterializing);
246           cur = -1;
247         } else {
248           cur = cur - (len - 1);
249         }
250 
251       }, this);
252     }
253 
254     if (cur >= 0) item = children.objectAt(cur, omitMaterializing); // get internal if needed
255     cache[index] = item; // save in cache
256 
257     return item;
258   },
259 
260   /**
261     Implements SC.Array.replace() primitive.  For this method to succeed, the
262     range you replace must lie entirely within the same parent item, otherwise
263     this will raise an exception.
264 
265     ### The Operation Parameter
266 
267     Note that this replace method accepts an additional parameter "operation"
268     which is used when you try to insert an item on a boundary between
269     branches whether it should be inserted at the end of the previous group
270     after the group.  If you don't pass operation, the default is
271     SC.DROP_BEFORE, which is the expected behavior.
272 
273     Even if the operation is SC.DROP_AFTER, you should still pass the actual
274     index where you expect the item to be inserted.  For example, if you want
275     to insert AFTER the last index of an 3-item array, you would still call:
276 
277         observer.replace(3, 0, [object1 .. objectN], SC.DROP_AFTER)
278 
279     The operation is simply used to disambiguate whether the insertion is
280     intended to be AFTER the previous item or BEFORE the items you are
281     replacing.
282 
283     @param {Number} start the starting index
284     @param {Number} amt the number of items to replace
285     @param {SC.Array} objects array of objects to insert
286     @param {Number} operation either SC.DROP_BEFORE or SC.DROP_AFTER
287     @returns {SC.TreeItemObserver} receiver
288   */
289   replace: function (start, amt, objects, operation) {
290 
291     var cur = start,
292         observer = null,
293         indexes, len, max;
294 
295     if (operation === undefined) operation = SC.DROP_BEFORE;
296 
297     // adjust the start location based on branches, possibly passing on to an
298     // observer.
299     if (this.get('isHeaderVisible')) cur--; // exclude my own header item
300     if (cur < 0) throw new Error("Tree Item cannot replace itself");
301 
302     // remove branch lengths.  If the adjusted start location lands inside of
303     // another branch, then just let that observer handle it.
304     indexes = this.get('branchIndexes');
305     if (indexes) {
306       indexes.forEach(function (i) {
307         if (observer || (i >= cur)) return; // nothing to do
308         observer = this.branchObserverAt(i);
309         if (!observer) return; // nothing to do
310 
311         len = observer.get('length');
312 
313         // if this branch range is before the start loc, just remove it and
314         // go on.  If cur is somewhere inside of the range, then save to pass
315         // on.  Note use of operation to determine the ambiguous end op.
316         if ((i + len === cur) && operation === SC.DROP_AFTER) {
317           cur = cur - i;
318         } else if (i + len > cur) {
319           cur = cur - i; // put inside of nested range
320         } else {
321           cur = cur - (len - 1);
322           observer = null;
323         }
324       }, this);
325     }
326 
327     // if an observer was saved, pass on call.
328     if (observer) {
329       observer.replace(cur, amt, objects, operation);
330       return this;
331     }
332 
333     // no observer was saved, which means cur points to an index inside of
334     // our own range.  Now amt just needs to be adjusted to remove any
335     // visible branches as well.
336     max = cur + amt;
337     if (amt > 1 && indexes) { // if amt is 1 no need...
338       indexes.forEachIn(cur, indexes.get('max') - cur, function (i) {
339         if (i > max) return; // nothing to do
340         if (!(observer = this.branchObserverAt(i))) return; // nothing to do
341         len = observer.get('length');
342         max = max - (len - 1);
343       }, this);
344     }
345 
346     // get amt back out.  if amt is negative, it means that the range passed
347     // was not cleanly inside of this range.  raise an exception.
348     amt = max - cur;
349 
350     // ok, now that we are adjusted, get the children and forward the replace
351     // call on.  if there are no children, bad news...
352     var children = this.get('children');
353     if (!children) throw new Error("cannot replace() tree item with no children");
354 
355     if ((amt < 0) || (max > children.get('length'))) {
356       throw new Error("replace() range must lie within a single tree item");
357     }
358 
359     children.replace(cur, amt, objects, operation);
360 
361     // don't call enumerableContentDidChange() here because, as an observer,
362     // we should be notified by the children array itself.
363 
364     return this;
365   },
366 
367   /**
368     Called whenever the content for the passed observer has changed.  Default
369     version notifies the parent if it exists and updates the length.
370 
371     The start, amt and delta params should reflect changes to the children
372     array, not to the expanded range for the wrapper.
373   */
374   observerContentDidChange: function (start, amt, delta) {
375 
376     // clear caches
377     this.invalidateBranchObserversAt(start);
378     this._objectAtCache = this._outlineLevelCache = null;
379     this._disclosureStateCache = null;
380     this.notifyPropertyChange('branchIndexes');
381 
382     var oldlen = this.get('length'),
383         newlen = this._computeLength(),
384         parent = this.get('parentObserver'), set;
385 
386     // update length if needed
387     if (oldlen !== newlen) {
388       this.set('length', newlen);
389     }
390 
391     // if we have a parent, notify that parent that we have changed.
392     if (!this._notifyParent) return this; // nothing more to do
393 
394     if (parent) {
395       set = SC.IndexSet.create(this.get('index'));
396       parent._childrenRangeDidChange(parent.get('children'), null, '[]', set);
397 
398     // otherwise, note the enumerable content has changed.  note that we need
399     // to convert the passed change to reflect the computed range
400     } else {
401       if (oldlen === newlen) {
402         amt = this.expandChildIndex(start + amt);
403         start = this.expandChildIndex(start);
404         amt = amt - start;
405         delta = 0;
406 
407       } else {
408         start = this.expandChildIndex(start);
409         amt   = newlen - start;
410         delta = newlen - oldlen;
411       }
412 
413       var removedCount = amt;
414       var addedCount = delta + removedCount;
415       this.arrayContentDidChange(start, removedCount, addedCount);
416     }
417   },
418 
419   /**
420     Accepts a child index and expands it to reflect any nested groups.
421   */
422   expandChildIndex: function (index) {
423 
424     var ret = index;
425     if (this.get('isHeaderVisible')) index++;
426 
427     // fast path
428     var branches = this.get('branchIndexes');
429     if (!branches || branches.get('length') === 0) return ret;
430 
431     // we have branches, adjust for their length
432     branches.forEachIn(0, index, function (idx) {
433       ret += this.branchObserverAt(idx).get('length') - 1;
434     }, this);
435 
436     return ret; // add 1 for item header
437   },
438 
439   // ..........................................................
440   // SC.COLLECTION CONTENT SUPPORT
441   //
442 
443   /** SC.CollectionContent
444     Called by the collection view to return any group indexes.  The default
445     implementation will compute the indexes one time based on the delegate
446     treeItemIsGrouped
447   */
448   contentGroupIndexes: function (view, content) {
449     var ret;
450 
451     if (content !== this) return null; // only care about receiver
452 
453     // If this is not the root item, never do grouping
454     if (this.get('parentObserver')) return null;
455 
456     var item = this.get('item'), group, indexes, cur, padding;
457 
458     if (item && item.isTreeItemContent) group = item.get('treeItemIsGrouped');
459     else group = !!this.delegate.get('treeItemIsGrouped');
460 
461     // If grouping is enabled, build an index set with all of our local groups.
462     if (group) {
463       ret      = SC.IndexSet.create();
464       indexes  = this.get('branchIndexes');
465 
466       if (indexes) {
467         // Start at the minimum index, which is equal for the tree and flat array
468         cur = indexes.min();
469 
470         // Padding is the difference between the tree index and array index for the current tree index
471         padding = 0;
472         indexes.forEach(function (i) {
473           ret.add(i + padding, 1);
474 
475           var observer = this.branchObserverAt(i);
476           if (observer) {
477             padding += observer.get('length') - 1;
478             cur += padding;
479           }
480         }, this);
481       }
482     } else {
483       ret = null;
484     }
485 
486     return ret;
487   },
488 
489   /** SC.CollectionContent */
490   contentIndexIsGroup: function (view, content, idx) {
491     var indexes = this.contentGroupIndexes(view, content);
492     return indexes ? indexes.contains(idx) : NO;
493   },
494 
495   /**
496     Returns the outline level for the specified index.
497   */
498   contentIndexOutlineLevel: function (view, content, index) {
499     if (content !== this) return -1; // only care about us
500 
501     var cache = this._outlineLevelCache;
502     if (cache && (cache[index] !== undefined)) return cache[index];
503     if (!cache) cache = this._outlineLevelCache = [];
504 
505     var len   = this.get('length'),
506         cur   = index,
507         ret   = null,
508         indexes;
509 
510     if (index >= len) return -1;
511 
512     if (this.get('isHeaderVisible')) {
513       if (index === 0) {
514         cache[0] = this.get('outlineLevel') - 1;
515         return cache[0];
516       } else {
517         cur--;
518       }
519     }
520 
521     // loop through branch indexes, reducing the offset until it matches
522     // something we might actually return.
523     indexes = this.get('branchIndexes');
524     if (indexes) {
525       indexes.forEach(function (i) {
526         if ((ret !== null) || (i > cur)) return; // past end - nothing to do
527 
528         var observer = this.branchObserverAt(i), len;
529         if (!observer) return; // nothing to do
530 
531         // if cur lands inside of this observer's length, use objectAt to get
532         // otherwise, just remove len from cur.
533         len = observer.get('length');
534         if (i + len > cur) {
535           ret = observer.contentIndexOutlineLevel(view, observer, cur - i);
536           cur = -1;
537         } else {
538           cur = cur - (len - 1);
539         }
540 
541       }, this);
542     }
543 
544     if (cur >= 0) ret = this.get('outlineLevel'); // get internal if needed
545     cache[index] = ret; // save in cache
546     return ret;
547   },
548 
549   /**
550     Returns the disclosure state for the specified index.
551   */
552   contentIndexDisclosureState: function (view, content, index) {
553     if (content !== this) return -1; // only care about us
554 
555     var cache = this._disclosureStateCache;
556     if (cache && (cache[index] !== undefined)) return cache[index];
557     if (!cache) cache = this._disclosureStateCache = [];
558 
559     var len   = this.get('length'),
560         cur   = index,
561         ret   = null,
562         indexes;
563 
564     if (index >= len) return SC.LEAF_NODE;
565 
566     if (this.get('isHeaderVisible')) {
567       if (index === 0) {
568         cache[0] = this.get('disclosureState');
569         return cache[0];
570       } else {
571         cur--;
572       }
573     }
574 
575     // loop through branch indexes, reducing the offset until it matches
576     // something we might actually return.
577     indexes = this.get('branchIndexes');
578     if (indexes) {
579       indexes.forEach(function (i) {
580         if ((ret !== null) || (i > cur)) return; // past end - nothing to do
581 
582         var observer = this.branchObserverAt(i), len;
583         if (!observer) return; // nothing to do
584 
585         // if cur lands inside of this observer's length, use objectAt to get
586         // otherwise, just remove len from cur.
587         len = observer.get('length');
588         if (i + len > cur) {
589           ret  = observer.contentIndexDisclosureState(view, observer, cur - i);
590           cur  = -1;
591         } else {
592           cur = cur - (len - 1);
593         }
594 
595       }, this);
596     }
597 
598     if (cur >= 0) ret = SC.LEAF_NODE; // otherwise its a leaf node
599     cache[index] = ret; // save in cache
600     return ret;
601   },
602 
603   /**
604     Expands the specified content index.  This will search down until it finds
605     the branchObserver responsible for this item and then calls _collapse on
606     it.
607   */
608   contentIndexExpand: function (view, content, idx) {
609     var indexes, cur = idx, children, item;
610 
611     if (content !== this) return; // only care about us
612     if (this.get('isHeaderVisible')) {
613       if (idx === 0) {
614         this._expand(this.get('item'));
615         return;
616       } else {
617         cur--;
618       }
619     }
620 
621     indexes = this.get('branchIndexes');
622     if (indexes) {
623       indexes.forEach(function (i) {
624         if (i >= cur) return; // past end - nothing to do
625         var observer = this.branchObserverAt(i), len;
626         if (!observer) return;
627 
628         len = observer.get('length');
629         if (i + len > cur) {
630           observer.contentIndexExpand(view, observer, cur - i);
631           cur = -1; //done
632         } else {
633           cur = cur - (len - 1);
634         }
635 
636       }, this);
637     }
638 
639     // if we are still inside of the range then maybe pass on to a child item
640     if (cur >= 0) {
641       children = this.get('children');
642       item     = children ? children.objectAt(cur) : null;
643       if (item) this._expand(item, this.get('item'), cur);
644     }
645   },
646 
647   /**
648     Called to collapse a content index item if it is currently in an open
649     disclosure state.  The default implementation does nothing.
650 
651     @param {SC.CollectionView} view the collection view
652     @param {SC.Array} content the content object
653     @param {Number} idx the content index
654     @returns {void}
655   */
656   contentIndexCollapse: function (view, content, idx) {
657     var indexes, children, item, cur = idx;
658 
659     if (content !== this) return; // only care about us
660     if (this.get('isHeaderVisible')) {
661       if (idx === 0) {
662         this._collapse(this.get('item'));
663         return;
664       } else {
665         cur--;
666       }
667     }
668 
669     indexes = this.get('branchIndexes');
670     if (indexes) {
671       indexes.forEach(function (i) {
672         if (i >= cur) return; // past end - nothing to do
673         var observer = this.branchObserverAt(i), len;
674         if (!observer) return;
675 
676         len = observer.get('length');
677         if (i + len > cur) {
678           observer.contentIndexCollapse(view, observer, cur - i);
679           cur = -1; //done
680         } else {
681           cur = cur - (len - 1);
682         }
683 
684       }, this);
685     }
686 
687     // if we are still inside of the range then maybe pass on to a child item
688     if (cur >= 0) {
689       children = this.get('children');
690       item     = children ? children.objectAt(cur) : null;
691       if (item) this._collapse(item, this.get('item'), cur);
692     }
693   },
694 
695   // ..........................................................
696   // BRANCH NODES
697   //
698 
699   /**
700     Returns the branch item for the specified index.  If none exists yet, it
701     will be created.
702   */
703   branchObserverAt: function (index) {
704     var byIndex = this._branchObserversByIndex,
705         indexes = this._branchObserverIndexes,
706         ret, item, children;
707 
708     if (!byIndex) byIndex = this._branchObserversByIndex = [];
709     if (!indexes) {
710       indexes = this._branchObserverIndexes = SC.IndexSet.create();
711     }
712 
713     ret = byIndex[index];
714     if (ret) return ret; // use cache
715 
716     // no observer for this content exists, create one
717     children = this.get('children');
718     item   = children ? children.objectAt(index) : null;
719     if (!item) return null; // can't create an observer for a null item
720 
721     byIndex[index] = ret = SC.TreeItemObserver.create({
722       item:     item,
723       delegate: this.get('delegate'),
724       parentObserver:   this,
725       index:  index,
726       outlineLevel: this.get('outlineLevel') + 1
727     });
728 
729     indexes.add(index); // save for later invalidation
730     return ret;
731   },
732 
733   /**
734     Invalidates any branch observers on or after the specified index range.
735   */
736   invalidateBranchObserversAt: function (index) {
737     var byIndex = this._branchObserversByIndex,
738         indexes = this._branchObserverIndexes;
739 
740     if (!byIndex || byIndex.length <= index) return this; // nothing to do
741     if (index < 0) index = 0;
742 
743     // destroy any observer on or after the range
744     indexes.forEachIn(index, indexes.get('max') - index, function (i) {
745       var observer = byIndex[i];
746       if (observer) observer.destroy();
747     }, this);
748 
749     byIndex.length = index; // truncate to dump extra indexes
750 
751     return this;
752   },
753 
754   // ..........................................................
755   // INTERNAL METHODS
756   //
757 
758   /** @private */
759   _cleanUpCachedDelegate: function () {
760     var cachedDelegate = this._cachedDelegate;
761 
762     if (cachedDelegate) {
763       cachedDelegate.removeObserver('treeItemIsExpandedKey', this, this.treeItemIsExpandedKeyDidChange);
764       cachedDelegate.removeObserver('treeItemChildrenKey', this, this.treeItemChildrenKeyDidChange);
765       cachedDelegate.removeObserver('treeItemIsGrouped', this, this.treeItemIsGroupedDidChange);
766 
767       // Remove the delegate specific key observers from the cached item.
768       this._cleanUpCachedItem();
769 
770       // Reset the delegate specific keys.
771       this.set('treeItemChildrenKey', 'treeItemChildren');
772       this.set('treeItemIsExpandedKey', 'treeItemIsExpanded');
773 
774       // Remove the cache.
775       this._cachedDelegate = null;
776     }
777   },
778 
779   /** @private */
780   _cleanUpCachedItem: function () {
781     var cachedItem = this._cachedItem,
782       treeItemIsExpandedKey = this.get('treeItemIsExpandedKey'),
783       treeItemChildrenKey = this.get('treeItemChildrenKey');
784 
785     if (cachedItem) {
786       cachedItem.removeObserver(treeItemIsExpandedKey, this, this._itemIsExpandedDidChange);
787       cachedItem.removeObserver(treeItemChildrenKey, this, this._itemChildrenDidChange);
788 
789       // Remove the cache.
790       this._cachedItem = null;
791     }
792   },
793 
794   /** SC.Object.prototype.init */
795   init: function () {
796     sc_super();
797 
798     // Initialize the item and the delegate. Be sure to set up the delegate first,
799     // because it determines the keys to observe on the item.
800     this._delegateDidChange();
801     this._itemDidChange();
802 
803     this._notifyParent = YES; // avoid infinite loops
804   },
805 
806   /** SC.Object.prototype.destroy
807     Called just before a branch observer is removed.  Should stop any
808     observing and invalidate any child observers.
809   */
810   destroy: function () {
811     this.invalidateBranchObserversAt(0);
812     this._objectAtCache = null;
813     this._notifyParent = NO; // parent doesn't care anymore
814 
815     // Cleanup the observed item and delegate.
816     this._cleanUpCachedItem();
817     this._cleanUpCachedDelegate();
818 
819     var children = this._children,
820         ro = this._childrenRangeObserver;
821     if (children && ro) children.removeRangeObserver(ro);
822 
823     this.set('length', 0);
824 
825     sc_super();
826   },
827 
828   /** @private */
829   _itemDidChange: function () {
830     var item = this.get('item'),
831       treeItemChildrenKey,
832       treeItemIsExpandedKey;
833 
834     treeItemIsExpandedKey = this.get('treeItemIsExpandedKey');
835     treeItemChildrenKey = this.get('treeItemChildrenKey');
836 
837     // Cleanup the previous observed item.
838     this._cleanUpCachedItem();
839 
840     //@if(debug)
841     // Add some developer support to prevent broken behavior.
842     if (!item) { throw new Error("Developer Error: SC.TreeItemObserver: Item cannot be null and must be set on create."); }
843 
844     if (item.hasObserverFor(treeItemIsExpandedKey)) {
845       SC.warn("Developer Warning: SC.TreeItemObserver: Item '%@' appears to already be assigned to a tree item observer. This will cause strange behavior working with the item.".fmt(item));
846     }
847     //@endif
848 
849     item.addObserver(treeItemIsExpandedKey, this, this._itemIsExpandedDidChange);
850     item.addObserver(treeItemChildrenKey, this, this._itemChildrenDidChange);
851 
852     // Fire the observer functions once to initialize.
853     this.beginPropertyChanges();
854     this._itemIsExpandedDidChange();
855     this._itemChildrenDidChange();
856     this.endPropertyChanges();
857 
858     // Track the item so that when it changes we can clean-up.
859     this._cachedItem = item;
860   }.observes('item'),
861 
862   /** @private */
863   _itemIsExpandedDidChange: function () {
864     var state = this.get('disclosureState'),
865       item = this.get('item'),
866       next;
867 
868     next = this._computeDisclosureState(item);
869     if (state !== next) { this.set('disclosureState', next); }
870   },
871 
872   /** @private */
873   _itemChildrenDidChange: function () {
874     var children = this.get('children'),
875       item = this.get('item'),
876       next;
877 
878     next = this._computeChildren(item);
879     if (children !== next) { this.set('children', next); }
880   },
881 
882   /** @private
883     Called whenever the children or disclosure state changes.  Begins or ends
884     observing on the children array so that changes can propogate outward.
885   */
886   _childrenDidChange: function () {
887     var state = this.get('disclosureState'),
888       cur   = state === SC.BRANCH_OPEN ? this.get('children') : null,
889       last  = this._children,
890       ro    = this._childrenRangeObserver;
891 
892     if (last === cur) return this; //nothing to do
893 
894     if (ro) last.removeRangeObserver(ro);
895 
896     if (cur) {
897       this._childrenRangeObserver = cur.addRangeObserver(null, this, this._childrenRangeDidChange);
898     } else {
899       this._childrenRangeObserver = null;
900     }
901 
902     this._children = cur;
903     this._childrenRangeDidChange(cur, null, '[]', null);
904   }.observes("children", "disclosureState"),
905 
906   /** @private
907     Called anytime the actual content of the children has changed.  If this
908     changes the length property, then notifies the parent that the content
909     might have changed.
910   */
911   _childrenRangeDidChange: function (array, objects, key, indexes) {
912     var children = this.get('children'),
913       len = children ? children.get('length') : 0,
914       min = indexes ? indexes.get('min') : 0,
915       max = indexes ? indexes.get('max') : len,
916       old = this._childrenLen || 0;
917 
918     this._childrenLen = len; // save for future calls
919     this.observerContentDidChange(min, max - min, len - old);
920   },
921 
922   /** @private
923     Computes the current disclosure state of the item by asking the item or
924     the delegate.  If no pitem or index is passed, the parentItem and index
925     will be used.
926   */
927   _computeDisclosureState: function (item, pitem, index) {
928     var key;
929 
930     // no item - assume leaf node
931     if (!item || !this._computeChildren(item)) return SC.LEAF_NODE;
932 
933     // item implement TreeItemContent - call directly
934     else if (item.isTreeItemContent) {
935       if (pitem === undefined) pitem = this.get('parentItem');
936       if (index === undefined) index = this.get('index');
937       return item.treeItemDisclosureState(pitem, index);
938 
939     // otherwise get treeItemDisclosureStateKey from delegate
940     } else {
941       key = this.get('treeItemIsExpandedKey');
942       return item.get(key) ? SC.BRANCH_OPEN : SC.BRANCH_CLOSED;
943     }
944   },
945 
946   /** @private
947     Collapse the item at the specified index.  This will either directly
948     modify the property on the item or call the treeItemCollapse() method.
949   */
950   _collapse: function (item, pitem, index) {
951     var key;
952 
953     // no item - assume leaf node
954     if (!item || !this._computeChildren(item)) return this;
955 
956     // item implement TreeItemContent - call directly
957     else if (item.isTreeItemContent) {
958       if (pitem === undefined) pitem = this.get('parentItem');
959       if (index === undefined) index = this.get('index');
960       item.treeItemCollapse(pitem, index);
961 
962     // otherwise get treeItemDisclosureStateKey from delegate
963     } else {
964       key = this.get('treeItemIsExpandedKey');
965       item.setIfChanged(key, NO);
966     }
967 
968     return this;
969   },
970 
971   /** @private Each time the delegate changes, observe it for changes to its keys. */
972   _delegateDidChange: function () {
973     var delegate = this.get('delegate');
974 
975     // Clean up the previous observed delegate.
976     this._cleanUpCachedDelegate();
977 
978     if (delegate) {
979       delegate.addObserver('treeItemChildrenKey', this, this.treeItemChildrenKeyDidChange);
980       delegate.addObserver('treeItemIsExpandedKey', this, this.treeItemIsExpandedKeyDidChange);
981       delegate.addObserver('treeItemIsGrouped', this, this.treeItemIsGroupedDidChange);
982 
983       // Fire the observer functions once to initialize.
984       this.treeItemChildrenKeyDidChange();
985       this.treeItemIsExpandedKeyDidChange();
986       this.treeItemIsGroupedDidChange();
987     }
988 
989     // Re-initialize the item to match the new delegate.
990     this._itemDidChange();
991 
992     // Cache the previous delegate so we can clean up.
993     this._cachedDelegate = delegate;
994   }.observes('delegate'),
995 
996   /** @private
997     Expand the item at the specified index.  This will either directly
998     modify the property on the item or call the treeItemExpand() method.
999   */
1000   _expand: function (item, pitem, index) {
1001     var key;
1002 
1003     // no item - assume leaf node
1004     if (!item || !this._computeChildren(item)) return this;
1005 
1006     // item implement TreeItemContent - call directly
1007     else if (item.isTreeItemContent) {
1008       if (pitem === undefined) pitem = this.get('parentItem');
1009       if (index === undefined) index = this.get('index');
1010       item.treeItemExpand(pitem, index);
1011 
1012     // otherwise get treeItemDisclosureStateKey from delegate
1013     } else {
1014       key = this.get('treeItemIsExpandedKey');
1015       item.setIfChanged(key, YES);
1016     }
1017 
1018     return this;
1019   },
1020 
1021   /** @private
1022     Computes the children for the passed item.
1023   */
1024   _computeChildren: function (item) {
1025     var key;
1026 
1027     if (!item) { // no item - no children
1028       return null;
1029     } else if (item.isTreeItemContent) {  // item implements TreeItemContent - call directly
1030       return item.get('treeItemChildren');
1031     } else { // otherwise get treeItemChildrenKey from delegate
1032       key = this.get('treeItemChildrenKey');
1033       return item.get(key);
1034     }
1035   },
1036 
1037   /** @private
1038     Computes the length of the array by looking at children.
1039   */
1040   _computeLength: function () {
1041     var ret = this.get('isHeaderVisible') ? 1 : 0,
1042         state = this.get('disclosureState'),
1043         children = this.get('children'),
1044         indexes;
1045 
1046     // if disclosure is open, add children count + length of branch observers.
1047     if ((state === SC.BRANCH_OPEN) && children) {
1048       ret += children.get('length');
1049 
1050       indexes = this.get('branchIndexes');
1051       if (indexes) {
1052         indexes.forEach(function (idx) {
1053           var observer = this.branchObserverAt(idx);
1054           ret += observer.get('length') - 1;
1055         }, this);
1056       }
1057     }
1058     return ret;
1059   },
1060 
1061   /** @private */
1062   treeItemChildrenKeyDidChange: function () {
1063     var del = this.get('delegate'),
1064       key;
1065 
1066     key = del ? del.get('treeItemChildrenKey') : 'treeItemChildren';
1067     this.set('treeItemChildrenKey', key ? key : 'treeItemChildren');
1068   },
1069 
1070   /** @private */
1071   treeItemIsExpandedKeyDidChange: function () {
1072     var del = this.get('delegate'),
1073       key;
1074 
1075     key = del ? del.get('treeItemIsExpandedKey') : 'treeItemIsExpanded';
1076     this.set('treeItemIsExpandedKey', key ? key : 'treeItemIsExpanded');
1077   },
1078 
1079   /** @private */
1080   treeItemIsGroupedDidChange: function () {
1081     this.notifyPropertyChange('branchIndexes');
1082   }
1083 
1084 });
1085 
1086