1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2010 Sprout Systems, 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('views/split_divider');
  9 
 10 /**
 11   Prevents the view from getting resized when the SplitView is resized,
 12   or the user resizes or moves an adjacent child view.
 13 */
 14 SC.FIXED_SIZE = 'sc-fixed-size';
 15 
 16 /**
 17   Prevents the view from getting resized when the SplitView is resized
 18   (unless the SplitView has resized all other views), but allows it to
 19   be resized when the user resizes or moves an adjacent child view.
 20 */
 21 SC.RESIZE_MANUAL = 'sc-manual-size';
 22 
 23 /**
 24   Allows the view to be resized when the SplitView is resized or due to
 25   the user resizing or moving an adjacent child view.
 26 */
 27 SC.RESIZE_AUTOMATIC = 'sc-automatic-resize';
 28 
 29 
 30 /**
 31   @class
 32 
 33   SC.SplitView arranges multiple views side-by-side or on top of each
 34   other.
 35 
 36   By default, SC.SplitView sets `size` and `position` properties on the
 37   child views, leaving it up to the child view to adjust itself. For good
 38   default behavior, mix SC.SplitChild into your child views.
 39 
 40   SplitView can resize its children to fit (the default behavior),
 41   or resize itself to fit its children--allowing you to build column-
 42   based file browsers and the like. As one child (a divider, most likely)
 43   is moved, SplitView can move additional children to get them out of the way.
 44 
 45   Setting Up SplitViews
 46   =======================================
 47   You can set up a split view like any other view in SproutCore:
 48 
 49       SplitView.design({
 50         childViews: ['leftPanel', 'rightPanel'],
 51 
 52         leftPanel: SC.View.design(SC.SplitChild, {
 53           minimumSize: 200
 54         }),
 55 
 56         rightPanel: SC.View.design(SC.SplitChild, {
 57           // it is usually the right panel you want to resize
 58           // as the SplitView resizes:
 59           autoResizeStyle: SC.RESIZE_AUTOMATIC
 60         })
 61       })
 62 
 63   Dividers
 64   =======================================
 65   Dividers are automatically added between every child view.
 66 
 67   You can specify what dividers to create in two ways:
 68 
 69   - Set splitDividerView to change the default divider view class to use.
 70 
 71   - Override splitViewDividerBetween(splitView, view1, view2), either in
 72     your subclass of SC.SplitView or in a delegate, and return the divider
 73     view instance that should go between the two views.
 74 
 75   As far as SplitView is concerned, dividers are actually just ordinary
 76   child views. They usually have an autoResizeStyle of SC.FIXED_SIZE, and
 77   usually mixin SC.SplitThumb to relay mouse and touch events to the SplitView.
 78   To prevent adding dividers between dividers and views or dividers and dividers,
 79   SC.SplitView marks all dividers with an isSplitDivider property.
 80 
 81   If you do not want to use split dividers at all, or wish to set them up
 82   manually in your childViews array, set splitDividerView to null.
 83 
 84   @extends SC.View
 85   @author Alex Iskander
 86 */
 87 SC.SplitView = SC.View.extend({
 88   /**@scope SC.SplitView.prototype*/
 89 
 90   /**
 91     @type Array
 92     @default ['topLeftView', 'bottomRightView']
 93     @readonly
 94     @see SC.View#childViews
 95   */
 96   childViews: ['topLeftView', 'bottomRightView'],
 97 
 98   /**
 99     @type Array
100     @default ['sc-split-view']
101     @readonly
102     @see SC.View#classNames
103    */
104   classNames: ['sc-split-view'],
105 
106   /**
107     Used by the splitView computed property to find the nearest SplitView.
108 
109     @type Boolean
110     @default true
111     @readonly
112    */
113   isSplitView: YES,
114 
115   /**
116    The class of view to create for the divider views. Override this to use a subclass of
117    SC.SplitDividerView, or to implment your own.
118 
119    @type SC.View
120    @default SC.SplitDividerView
121   */
122   splitDividerView: SC.SplitDividerView,
123 
124   /**
125    Determines whether the SplitView should lay out its children
126    horizontally or vertically.
127 
128    Possible values:
129 
130      - SC.LAYOUT_HORIZONTAL: side-by-side
131      - SC.LAYOUT_VERTICAL: on top of each other
132 
133    @type LayoutDirection
134    @default SC.LAYOUT_HORIZONTAL
135   */
136   layoutDirection: SC.LAYOUT_HORIZONTAL,
137 
138   /**
139    * Determines whether the SplitView should attempt to resize its
140    * child views to fit within the SplitView's own frame (the default).
141    *
142    * If NO, the SplitView will decide its own size based on its children.
143    *
144    * @type Boolean
145    * @default true
146   */
147   shouldResizeChildrenToFit: YES,
148 
149   /**
150    * The cursor of the child view currently being dragged (if any).
151    * This allows the cursor to be used even if the user drags "too far",
152    * past the child's own boundaries.
153    *
154    * @type String
155    * @default null
156   */
157   splitChildCursorStyle: null,
158 
159   /** @private
160     Only occurs during drag, which only happens after render, so we
161     update directly.
162   */
163   _scsv_splitChildCursorDidChange: function() {
164     this.get('cursor').set('cursorStyle', this.get('splitChildCursorStyle'));
165   }.observes('splitChildCursorStyle'),
166 
167   /** @private */
168   init: function() {
169     // set up the SC.Cursor instance that this view and all the subviews
170     // will share.
171     this.cursor = SC.Cursor.create();
172     sc_super();
173   },
174 
175   // RENDERING
176   // Things like layoutDirection must be rendered as class names.
177   // We delegate to a render delegate.
178   //
179   displayProperties: ['layoutDirection'],
180   renderDelegateName: 'splitRenderDelegate',
181 
182   //
183   // UTILITIES
184   //
185   /**
186    * @private
187    * Returns either the width or the height of the SplitView's frame,
188    * depending on the value of layoutDirection. If layoutDirection is
189    * SC.LAYOUT_HORIZONTAL, this will return the SplitView's width; otherwise,
190    * the SplitView's height.
191    *
192    * @property
193    * @type {Number}
194   */
195   _frameSize: function(){
196     if (this.get('layoutDirection') === SC.LAYOUT_HORIZONTAL) {
197       return this.get('frame').width;
198     } else {
199       return this.get('frame').height;
200     }
201   }.property('frame', 'layoutDirection').cacheable(),
202 
203   /** @private */
204   viewDidResize: function () {
205     this.scheduleTiling();
206 
207     sc_super();
208   },
209 
210   /** @private */
211   layoutDirectionDidChange: function() {
212     // Schedule tiling.
213     this.scheduleTiling();
214     // Propagate to dividers.
215     var layoutDirection = this.get('layoutDirection'),
216         childViews = this.get('childViews'),
217         len = childViews ? childViews.get('length') : 0,
218         i, view;
219     for (i = 0; i < len; i++) {
220       view = childViews[i];
221       if (view.get('isSplitDivider')) view.setIfChanged('layoutDirection', layoutDirection);
222     }
223   }.observes('layoutDirection'),
224 
225   //
226   // PUBLIC CHILD VIEW ADJUSTMENT API
227   //
228   /**
229    * Attempts to adjust the position of a child view, such as a divider.
230    *
231    * The implementation for this may be overridden in the delegate method
232    * splitViewAdjustPositionForChild.
233    *
234    * You may use this method to automatically collapse the view by setting
235    * the view's position to the position of the next or previous view (accessible
236    * via the child's nextView and previousView properties and the
237    * getPositionForChild method).
238    *
239    * @param {SC.View} child The child to move.
240    * @param {Number} position The position to move the child to.
241    * @returns {Number} The position to which the child was actually moved.
242   */
243   adjustPositionForChild: function(child, position){
244     return this.invokeDelegateMethod(this.get('delegate'), 'splitViewAdjustPositionForChild', this, child, position);
245   },
246 
247   /**
248    * Returns the position within the split view for a child view,
249    * such as a divider. This position is not necessarily identical
250    * to the view's actual layout 'left' or 'right'; that position could
251    * be offset--for instance, to give a larger grab area to the divider.
252    *
253    * The implementation for this is in the delegate method
254    * splitViewGetPositionForChild.
255    *
256    * @param {SC.View} child The child whose position to find.
257    * @returns {Number} The position.
258   */
259   getPositionForChild: function(child){
260     return this.invokeDelegateMethod(this.get('delegate'), 'splitViewGetPositionForChild', this, child);
261   },
262 
263   //
264   // CHILD VIEW MANAGEMENT
265   //
266 
267   // When children are added and removed, we must re-run the setup process that
268   // sets the SplitView child properties such as nextView, previousView, etc.,
269   // and which adds dividers.
270   didAddChild: function() {
271     // we have to add a guard because _scsv_setupChildViews may add or remove
272     // dividers, causing this method to be called again uselessly.
273     // this is purely for performance. The guard goes here, rather than in
274     // setupChildViews, because of the invokeOnce.
275     if (this._scsv_settingUpChildViews) return;
276     this._scsv_settingUpChildViews = YES;
277 
278     this.invokeOnce('_scsv_setupChildViews');
279 
280     this._scsv_settingUpChildViews = NO;
281   },
282 
283   didRemoveChild: function() {
284     // we have to add a guard because _scsv_setupChildViews may add or remove
285     // dividers, causing this method to be called again uselessly.
286     // this is purely for performance. The guard goes here, rather than in
287     // setupChildViews, because of the invokeOnce.
288     if (this._scsv_settingUpChildViews) return;
289     this._scsv_settingUpChildViews = YES;
290 
291     this.invokeOnce('_scsv_setupChildViews');
292 
293     this._scsv_settingUpChildViews = NO;
294   },
295 
296   createChildViews: function() {
297     sc_super();
298 
299     if (this._scsv_settingUpChildViews) return;
300     this._scsv_settingUpChildViews = YES;
301 
302     this.invokeOnce('_scsv_setupChildViews');
303 
304     this._scsv_settingUpChildViews = NO;
305   },
306 
307   /**
308    * @private
309    * During initialization and whenever the child views change, SplitView needs
310    * to set some helper properties on the children and create any needed dividers.
311    *
312    * Note: If dividers are added, childViews changes, causing this to be called again;
313    * this is proper, because this updates the nextView, etc. properties appropriately.
314    *
315    * The helper properties are: previousView, nextView, viewIndex.
316   */
317   _scsv_setupChildViews: function() {
318     var del = this.get('delegate'),
319         layoutDirection = this.get('layoutDirection'),
320 
321         children = this.get('childViews').copy(), len = children.length, idx,
322         child, lastChild, lastNonDividerChild,
323 
324         oldDividers = this._scsv_dividers || {}, newDividers = {}, divider, dividerId;
325 
326     // loop through all children, keeping track of the previous child
327     // as we loop using the lastChild variable.
328     for (idx = 0; idx < len; idx++) {
329       child = children[idx];
330 
331       // do initial setup of things like autoResizeStyle:
332       if (!child.get('autoResizeStyle')) {
333         if (child.get('size') !== undefined) {
334           child.set('autoResizeStyle', SC.RESIZE_MANUAL);
335         } else {
336           child.set('autoResizeStyle', SC.RESIZE_AUTOMATIC);
337         }
338       }
339 
340       // we initialize the size first thing in case the size is empty (fill)
341       // if it is empty, the way we position the views would lead to inconsistent
342       // sizes. In addition, we will constrain all initial sizes so they'll be valid
343       // when/if we auto-resize them.
344       var size = this.invokeDelegateMethod(del, 'splitViewGetSizeForChild', this, child);
345       size = this.invokeDelegateMethod(del, 'splitViewConstrainSizeForChild', this, child, size);
346       this.invokeDelegateMethod(del, 'splitViewSetSizeForChild', this, child, size);
347 
348       child.previousView = lastChild;
349       child.nextView = undefined;
350       child.viewIndex = idx;
351 
352       if (lastChild) {
353         lastChild.nextView = child;
354       }
355 
356       if (lastNonDividerChild && !child.isSplitDivider) {
357         dividerId = SC.guidFor(lastNonDividerChild) + "-" + SC.guidFor(child);
358 
359         // Try to re-use an existing divider.
360         divider = oldDividers[dividerId];
361         if (!divider) {
362           divider = this.invokeDelegateMethod(del, 'splitViewDividerBetween', this, lastNonDividerChild, child);
363         }
364 
365         if (divider) {
366           divider.setIfChanged('isSplitDivider', YES);
367           divider.setIfChanged('layoutDirection', layoutDirection);
368 
369           newDividers[dividerId] = divider;
370 
371           if (oldDividers[dividerId]) {
372             delete oldDividers[dividerId];
373           } else {
374             this.insertBefore(divider, child);
375           }
376         }
377       }
378 
379 
380       lastChild = child;
381       if (!child.isSplitDivider) lastNonDividerChild = child;
382     }
383 
384     // finally, remove all dividers that we didn't keep
385     for (dividerId in oldDividers) {
386       oldDividers[dividerId].destroy();
387     }
388 
389     this._scsv_dividers = newDividers;
390 
391     // retile immediately.
392     this._scsv_tile();
393   },
394 
395   //
396   // BASIC LAYOUT CODE
397   //
398 
399   /**
400     Whether the SplitView needs to be re-laid out. You can change this by
401     calling scheduleTiling.
402   */
403   needsTiling: YES,
404 
405   /**
406     Schedules a retile of the SplitView.
407   */
408   scheduleTiling: function() {
409     this.set('needsTiling', YES);
410     this.invokeOnce('_scsv_tile');
411   },
412 
413   tileIfNeeded: function() {
414     if (!this.get('needsTiling')) return;
415     this._scsv_tile();
416   },
417 
418   /**
419    * @private
420    * Tiling is the simpler of two layout paths. Tiling lays out all of the
421    * children according to their size, and, if shouldResizeChildrenToFit is
422    * YES, attempts to resize the children to fit in the SplitView.
423    *
424    * It is called when the child views are initializing or have changed, and
425    * when the SplitView is resized.
426    *
427   */
428   _scsv_tile: function() {
429     var del = this.get('delegate');
430 
431     // LOGIC:
432     //
433     // - Call splitViewLayoutChildren delegate method to position views and
434     //   find total size.
435     //
436     // - If meant to automatically resize children to fit, run the
437     //   splitViewResizeChildrenToFit delegate method.
438     //
439     // - Call splitViewLayoutChildren again if splitViewResizeChildrenToFit was called.
440     //
441     // - If not meant to automatically resize children to fit, change the SplitView
442     //   size to match the total size of all children.
443 
444     var size, frameSize = this.get('_frameSize');
445 
446     size = this.invokeDelegateMethod(del, 'splitViewLayoutChildren', this);
447 
448     if (this.get('shouldResizeChildrenToFit') && size !== frameSize) {
449       this.invokeDelegateMethod(del, 'splitViewResizeChildrenToFit', this, size);
450       size = this.invokeDelegateMethod(del, 'splitViewLayoutChildren', this);
451     }
452 
453     if (!this.get('shouldResizeChildrenToFit')) {
454       if (this.get('layoutDirection') === SC.LAYOUT_HORIZONTAL) {
455         this.adjust('width', size);
456       } else {
457         this.adjust('height', size);
458       }
459     }
460 
461     this.set('needsTiling', NO);
462   },
463 
464   /**
465    * Lays out the children one next to each other or one on top of the other,
466    * based on their sizes. It returns the total size.
467    *
468    * You may override this method in a delegate.
469    *
470    * @param {SC.SplitView} splitView The SplitView whose children need layout.
471    * @returns {Number} The total size of all the SplitView's children.
472   */
473   splitViewLayoutChildren: function(splitView) {
474     var del = this.get('delegate');
475 
476     var children = this.get('childViews'), len = children.length, idx,
477         child, pos = 0;
478 
479     for (idx = 0; idx < len; idx++) {
480       child = children[idx];
481 
482       this.invokeDelegateMethod(del, 'splitViewSetPositionForChild', this, children[idx], pos);
483       pos += this.invokeDelegateMethod(del, 'splitViewGetSizeForChild', this, children[idx]);
484     }
485 
486     return pos;
487   },
488 
489   /**
490    * Attempts to resize the child views of the split view to fit in the SplitView's
491    * frame. So it may proportionally adjust the child views, the current size of the
492    * SplitView's content is passed.
493    *
494    * You may override this method in a delegate.
495    *
496    * @param {SC.SplitView} splitView The SC.SplitView whose children should be resized.
497    * @param {Number} contentSize The current not-yet-resized size of the SplitView's content.
498   */
499   splitViewResizeChildrenToFit: function(splitView, contentSize) {
500     var del = this.get('delegate');
501 
502     // LOGIC:
503     //
504     //   - 1) Size auto-resizable children in proportion to their existing sizes to attempt
505     //     to fit within the target size— auto-resizable views have autoResizeStyle set
506     //     to SC.RESIZE_AUTOMATIC.
507     //
508     //   - 2) Size non-auto-resizable children in proportion to their existing sizes—these
509     //     views will _not_ have an autoResizeStyle of SC.RESIZE_AUTOMATIC.
510     //
511 
512     var frameSize = this.get('_frameSize');
513     var children = this.get('childViews'), len = children.length, idx,
514         child, resizableSize = 0, nonResizableSize = 0, childSize;
515 
516     // To do this sizing while keeping things proportionate, the total size of resizable
517     // views and the total size of non-auto-resizable views must be calculated independently.
518     for (idx = 0; idx < len; idx++) {
519       child = children[idx];
520 
521       childSize = this.invokeDelegateMethod(del, 'splitViewGetSizeForChild', this, child);
522 
523       if (this.invokeDelegateMethod(del, 'splitViewShouldResizeChildToFit', this, child)) {
524         resizableSize += childSize;
525       } else {
526         nonResizableSize += childSize;
527       }
528     }
529 
530     var runningSize = contentSize;
531 
532     // we run through each twice: non-aggressively, then aggressively. This is controlled by providing
533     // a -1 for the outOfSize. This tells the resizing to not bother with proportions and just resize
534     // whatever it can.
535     runningSize = this._resizeChildrenForSize(runningSize, frameSize, YES, resizableSize);
536     runningSize = this._resizeChildrenForSize(runningSize, frameSize, YES, -1);
537     runningSize = this._resizeChildrenForSize(runningSize, frameSize, NO, nonResizableSize);
538     runningSize = this._resizeChildrenForSize(runningSize, frameSize, NO, -1);
539   },
540 
541   /**
542    * @private
543    * Utility method used by splitViewResizeChildrenToFit to do the proportionate
544    * sizing of child views.
545    *
546    * @returns {Number} The new runningSize.
547   */
548   _resizeChildrenForSize: function(runningSize, targetSize, useResizable, outOfSize) {
549     var del = this.get('delegate');
550 
551     var children = this.get('childViews'), idx, len = children.length, child;
552 
553     var diff = targetSize - runningSize;
554     for (idx = 0; idx < len; idx++) {
555       child = children[idx];
556 
557       var originalChildSize = this.invokeDelegateMethod(del, 'splitViewGetSizeForChild', this, child),
558           size = originalChildSize;
559 
560       var isResizable = this.invokeDelegateMethod(del, 'splitViewShouldResizeChildToFit', this, child);
561       if (isResizable === useResizable) {
562         // if outOfSize === -1 then we are aggressively resizing (not resizing proportionally)
563         if (outOfSize === -1) size += diff;
564         else size += (size / outOfSize) * diff;
565 
566         size = Math.round(size);
567 
568         size = this.invokeDelegateMethod(del, 'splitViewConstrainSizeForChild', this, child, size);
569         this.invokeDelegateMethod(del, 'splitViewSetSizeForChild', this, child, size);
570 
571 
572         // we remove the original child size—but we don't add it back.
573         // we don't add it back because the load is no longer shared.
574         if (outOfSize !== -1) outOfSize -= originalChildSize;
575       }
576 
577       // We modify the old size to account for our changes so we can keep a running diff
578       runningSize -= originalChildSize;
579       runningSize += size;
580       diff = targetSize - runningSize;
581     }
582 
583     return runningSize;
584   },
585 
586   /**
587    * Determines whether the SplitView should attempt to resize the specified
588    * child view when the SplitView's size changes.
589    *
590    * You may override this method in a delegate.
591    *
592    * @param {SC.SplitView} splitView The SplitView that owns the child.
593    * @param {SC.View} child The child view.
594    * @returns {Boolean}
595   */
596   splitViewShouldResizeChildToFit: function(splitView, child) {
597     return (
598       this.get('shouldResizeChildrenToFit')  &&
599       child.get('autoResizeStyle') === SC.RESIZE_AUTOMATIC
600     );
601   },
602 
603   /**
604    * Attempts to move a single child from its current position to
605    * a desired position.
606    *
607    * You may override the behavior on a delegate.
608    *
609    * @param {SC.SplitView} splitView The splitView whose child should be moved.
610    * @param {SC.View} child The child which should be moved.
611    * @param {Number} position The position to attempt to move the child to.
612    * @returns {Number} The final position of the child.
613   */
614   splitViewAdjustPositionForChild: function(splitView, child, position) {
615     // var del = this.get('delegate');
616     // Unlike tiling, the process of moving a child view is much more sophisticated.
617     //
618     // The basic sequence of events is simple:
619     //
620     //  - resize previous child
621     //  - resize the child itself to compensate for its movement if
622     //    child.compensatesForMovement is YES.
623     //  - adjust position of next child.
624     //
625     // As the process is recursive in both directions (resizing a child may attempt
626     // to move it if it cannot be resized further), adjusting one child view could
627     // affect many _if not all_ of the SplitView's children.
628     //
629     // For safety, sanity, and stability, the recursive chain-reactions only travel
630     // in one direction; for instance, resizing the previous view may attempt to adjust
631     // its position, but that adjustment will not propagate to views after it.
632     //
633     // This process, while powerful, has one complication: if you change a bunch of views
634     // before a view, and then _fail_ to move views after it, the views before must be
635     // moved back to their starting points. But if their positions were changed directly,
636     // this would be impossible.
637     //
638     // As such, the positions are not changed directly. Rather, the changes are written
639     // to a _plan_, and changes only committed once everything is finalized.
640     //
641     // splitViewAdjustPositionForChild is the entry point, and as such is responsible
642     // for triggering the creation of the plan, the needed modifications, and the
643     // finalizing of it.
644     var plan = this._scsv_createPlan();
645     var finalPosition = this._scsv_adjustPositionForChildInPlan(plan, child, position, child);
646     this._scsv_commitPlan(plan);
647 
648     return finalPosition;
649   },
650 
651   /**
652    * @private
653    * Creates a plan in which to prepare changes to the SplitView's children.
654    *
655    * A plan is an array with the same number of elements as the SplitView has children.
656    * Each element is a hash containing these properties:
657    *
658    * - child: the view the hash represents
659    * - originalPosition: the position before the planning process
660    * - position: the planned new position.
661    * - originalSize: the size before the planning process
662    * - size: the planned new size.
663    *
664    * The repositioning and resizing logic can, at any time, reset part of the plan
665    * to its original state, allowing layout processes to be run non-destructively.
666    * In addition, storing the original positions and sizes is more performant
667    * than looking them up each time.
668    *
669    * @returns {Plan}
670   */
671   _scsv_createPlan: function() {
672     var del = this.get('delegate'),
673         plan = [], children = this.get('childViews'), idx, len = children.length,
674         child, childPosition, childSize;
675 
676     for (idx = 0; idx < len; idx++) {
677       child = children[idx];
678       childPosition = this.invokeDelegateMethod(del, 'splitViewGetPositionForChild', this, child);
679       childSize = this.invokeDelegateMethod(del, 'splitViewGetSizeForChild', this, child);
680 
681       plan[idx] = {
682         child: child,
683         originalPosition: childPosition,
684         position: childPosition,
685         originalSize: childSize,
686         size: childSize
687       };
688     }
689 
690     return plan;
691   },
692 
693   /**
694     * @private
695     * Resets a range of the plan to its original settings.
696     *
697     * @param {Plan} plan The plan.
698     * @param {Number} first The first item in the range.
699     * @param {Number} last The last item in the range.
700    */
701    _scsv_resetPlanRange: function(plan, first, last) {
702      for (var idx = first; idx <= last; idx++) {
703        plan[idx].position = plan[idx].originalPosition;
704        plan[idx].size = plan[idx].originalSize;
705      }
706    },
707 
708   /**
709    * @private
710    * Commits the changes specified in the plan to the child views.
711    *
712    * @param {Plan} plan The plan with the changes.
713   */
714   _scsv_commitPlan: function(plan) {
715     var del = this.get('delegate'), len = plan.length, idx, item, end = 0;
716 
717     for (idx = 0; idx < len; idx++) {
718       item = plan[idx];
719       if (item.size !== item.originalSize) {
720         this.invokeDelegateMethod(del, 'splitViewSetSizeForChild', this, item.child, item.size);
721       }
722 
723       if (item.position !== item.originalPosition) {
724         this.invokeDelegateMethod(del, 'splitViewSetPositionForChild', this, item.child, item.position);
725       }
726 
727       end = item.position + item.size;
728     }
729 
730 
731     if (!this.get('shouldResizeChildrenToFit')) {
732       if (this.get('layoutDirection') === SC.LAYOUT_HORIZONTAL) {
733         this.adjust('width', end);
734       } else {
735         this.adjust('height', end);
736       }
737     }
738   },
739 
740   /**
741    * Moves the specified child view as close as it can to the specified
742    * position, saving all changes this causes into the plan.
743    *
744    * The "directness" of the action also comes into play. An action is direct if:
745    *
746    * - The child being modified is the originating child (the one being dragged, most likely)
747    * - The child is being _positioned_ as is immediately _after_ the originating child.
748    * - The child is being _sized_ and is immediately _before_ the originating child.
749    *
750    * This means that direct actions modify the originating child or the border between
751    * it and a sibling. Some child views don't like to accept indirect actions, as the
752    * indirect actions may confuse or annoy users in some cases.
753    *
754    * @param {Plan} plan The plan write changes to (and get some data from).
755    * @param {SC.View} child The child to move.
756    * @param {Number} position The position to attempt to move the child to.
757    * @param {Boolean} source The child from which the attempt to adjust originated—used
758    * to determine directness.
759    *
760    * @returns {Number} The final position of the child.
761   */
762   _scsv_adjustPositionForChildInPlan: function(plan, child, position, source) {
763     var del = this.get('delegate');
764 
765     if (
766       !child.get('allowsIndirectAdjustments') &&
767       source !== child && source !== child.previousView
768     ) {
769       return plan[child.viewIndex].position;
770     }
771 
772     // since the process is recursive, we need to prevent the processing from
773     // coming back in this direction.
774     if (child._splitViewIsAdjusting) {
775       return plan[child.viewIndex].position;
776     }
777 
778     child._splitViewIsAdjusting = YES;
779 
780     //
781     // STEP 1: attept to resize the previous child.
782     //
783     var previousChild = child.previousView, nextChild = child.nextView,
784         previousChildPosition, previousChildSize,
785         nextChildPosition, nextChildSize,
786         size = plan[child.viewIndex].size;
787 
788     if (previousChild && !previousChild._splitViewIsAdjusting) {
789       // we determine the size we would like it to be by subtracting its position
790       // from the position _we_ would like to have.
791       previousChildPosition = plan[previousChild.viewIndex].position;
792       previousChildSize = position - previousChildPosition;
793 
794       previousChildSize = this._scsv_adjustSizeForChildInPlan(
795         plan, previousChild, previousChildSize, source
796       );
797 
798       // the child may not have resized/moved itself all the way, so we will
799       // recalculate the target position based on how much it _was_ able to.
800       position = previousChildPosition + previousChildSize;
801     } else if (!previousChild) {
802       // if there is no previous child view, then this is the first view and
803       // as such _must_ be at 0.
804       position = 0;
805     }
806 
807     // further steps deal with children _after_ this one; these steps should
808     // not be performed if those children are already being taken care of.
809     if (nextChild && nextChild._splitViewIsAdjusting) {
810       child._splitViewIsAdjusting = NO;
811       plan[child.viewIndex].position = position;
812       return position;
813     }
814 
815 
816     //
817     // STEP 2: attempt to resize this view to avoid moving the next one.
818     // Only occurs if the view's settings tell it to compensate _and_ there is a
819     // next view to compensate for, or we are resizing to fit and there _is no_ next child.
820     //
821     if (child.get('compensatesForMovement') && nextChild) {
822       nextChildPosition = plan[nextChild.viewIndex].position;
823       size = this._scsv_adjustSizeForChildInPlan(plan, child, nextChildPosition - position);
824     } else if (!nextChild && this.get('shouldResizeChildrenToFit')) {
825       nextChildPosition = this.get('_frameSize');
826       size = this._scsv_adjustSizeForChildInPlan(plan, child, nextChildPosition - position);
827       position = nextChildPosition - size;
828     }
829 
830     // STEP 3: attempt to move the next child to account for movement of this one.
831     if (nextChild) {
832       nextChildPosition = position + size;
833       nextChildPosition = this._scsv_adjustPositionForChildInPlan(plan, nextChild, nextChildPosition, source);
834     }
835 
836     // if we were unable to position the next child, or there is no next
837     // child but we need to resize children to fit, we have to undo some
838     // of our previous work.
839     if (nextChildPosition && position !== nextChildPosition - size) {
840       position = nextChildPosition - size;
841 
842       // then, for whatever is left, we again resize the previous view, after throwing
843       // away the previous calculations.
844       if (previousChild && !previousChild._splitViewIsAdjusting) {
845         this._scsv_resetPlanRange(plan, 0, previousChild.viewIndex);
846         previousChildSize = position - plan[previousChild.viewIndex].position;
847         this._scsv_adjustSizeForChildInPlan(plan, previousChild, previousChildSize, child);
848       }
849 
850     }
851 
852 
853     plan[child.viewIndex].position = position;
854     child._splitViewIsAdjusting = NO;
855     return position;
856   },
857 
858   _scsv_adjustSizeForChildInPlan: function(plan, child, size, source) {
859     var del = this.get('delegate');
860 
861     if (
862       source &&
863       !child.get('allowsIndirectAdjustments') &&
864       source !== child && source !== child.nextView && source !== child.previousView
865     ) {
866       return plan[child.viewIndex].size;
867     }
868 
869     // First, see if resizing alone will do the job.
870     var actualSize = this.invokeDelegateMethod(del, 'splitViewConstrainSizeForChild', this, child, size);
871 
872     plan[child.viewIndex].size = actualSize;
873 
874     if (size === actualSize) return size;
875 
876     // if not, attempt to move the view.
877     var currentPosition = plan[child.viewIndex].position,
878         targetPosition = currentPosition + size - actualSize;
879 
880     var position = this._scsv_adjustPositionForChildInPlan(plan, child, targetPosition, source);
881 
882     // the result is the new right edge minus the old left edge—that is,
883     // the size we can pretend we are for the caller, now that we have
884     // resized some other views.
885     return position + actualSize - currentPosition;
886   },
887 
888   /**
889    * Returns a view instance to be used as a divider between two other views,
890    * or null if no divider should be used.
891    *
892    * The value of the 'splitDividerView' property will be instantiated. The default
893    * value of this property is 'SC.SplitDividerView'. If the value is null or undefined,
894    * null will be returned, and the SplitView will not automatically create dividers.
895    *
896    * You may override this method in a delegate.
897    *
898    * @param {SC.SplitView} splitView The split view that is hte parent of the
899    * two views.
900    * @param {SC.View} view1 The first view.
901    * @param {SC.View} view2 The second view.
902    * @returns {SC.View} The view instance to use as a divider.
903   */
904   splitViewDividerBetween: function(splitView, view1, view2){
905     if (!this.get('splitDividerView')) return null;
906 
907     return this.get('splitDividerView').create();
908   },
909 
910   /**
911    * Returns the current position for the specified child.
912    *
913    * You may override this in a delegate.
914    *
915    * @param {SC.SplitView} splitView The SplitView which owns the child.
916    * @param {SC.View} child The child.
917    * @returns Number
918   */
919   splitViewGetPositionForChild: function(splitView, child) {
920     return child.get('position');
921   },
922 
923   /**
924    * Sets the position for the specified child.
925    *
926    * You may override this in a delegate.
927    *
928    * @param {SC.SplitView} splitView The SplitView which owns the child.
929    * @param {SC.View} child The child.
930    * @param {Number} position The position to move the child to.
931   */
932   splitViewSetPositionForChild: function(splitView, child, position) {
933     child.set('position', position);
934   },
935 
936   /**
937    * Returns the current size for the specified child.
938    *
939    * You may override this in a delegate.
940    *
941    * @param {SC.SplitView} splitView The SplitView which owns the child.
942    * @param {SC.View} child The child.
943    * @returns Number
944   */
945   splitViewGetSizeForChild: function(splitView, child) {
946     var size = child.get('size');
947     if (SC.none(size)) return 100;
948 
949     return size;
950   },
951 
952   /**
953    * Sets the size for the specified child.
954    *
955    * You may override this in a delegate.
956    *
957    * @param {SC.SplitView} splitView The SplitView which owns the child.
958    * @param {SC.View} child The child.
959    * @param {Number} position The size to give the child.
960   */
961   splitViewSetSizeForChild: function(splitView, child, size) {
962     child.set('size', size);
963   },
964 
965   /**
966    * Returns the nearest valid size to a proposed size for a child view.
967    * By default, constrains the size to the range specified by the child's
968    * minimumSize and maximumSize properties, and returns 0 if the child
969    * has canCollapse set and the size is less than the child's collapseAtSize.
970    *
971    * You may override this in a delegate.
972    *
973    * @param {SC.SplitView} splitView The SplitView which owns the child.
974    * @param {SC.View} child The child.
975    * @param {Number} position The proposed size for the child.
976    * @returns Number
977   */
978   splitViewConstrainSizeForChild: function(splitView, child, size) {
979     if (child.get('autoResizeStyle') === SC.FIXED_SIZE) {
980       return this.invokeDelegateMethod(this.get('delegate'), 'splitViewGetSizeForChild', this, child);
981     }
982 
983     if (child.get('canCollapse')) {
984       var collapseAtSize = child.get('collapseAtSize');
985       if (collapseAtSize && size < collapseAtSize) return 0;
986     }
987 
988     var minSize = child.get('minimumSize') || 0;
989     if (minSize !== undefined && minSize !== null) size = Math.max(minSize, size);
990 
991     var maxSize = child.get('maximumSize');
992     if (maxSize !== undefined && maxSize !== null) size = Math.min(maxSize, size);
993 
994     return size;
995   }
996 });
997