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