API  0.9.6
 All Classes Files Functions Variables Macros Groups Pages
CPSplitView.j
Go to the documentation of this file.
1 /*
2  * CPSplitView.j
3  * AppKit
4  *
5  * Created by Thomas Robinson.
6  * Copyright 2008, 280 North, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21  */
22 
23 #include "../Foundation/Foundation.h"
24 
25 
26 #define SPLIT_VIEW_MAYBE_POST_WILL_RESIZE() \
27  if ((_suppressResizeNotificationsMask & DidPostWillResizeNotification) === 0) \
28  { \
29  [self _postNotificationWillResize]; \
30  _suppressResizeNotificationsMask |= DidPostWillResizeNotification; \
31  }
32 
33 #define SPLIT_VIEW_MAYBE_POST_DID_RESIZE() \
34  if ((_suppressResizeNotificationsMask & ShouldSuppressResizeNotifications) !== 0) \
35  _suppressResizeNotificationsMask |= DidSuppressResizeNotification; \
36  else \
37  [self _postNotificationDidResize];
38 
39 #define SPLIT_VIEW_DID_SUPPRESS_RESIZE_NOTIFICATION() \
40  ((_suppressResizeNotificationsMask & DidSuppressResizeNotification) !== 0)
41 
42 #define SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(shouldSuppress) \
43  if (shouldSuppress) \
44  _suppressResizeNotificationsMask |= ShouldSuppressResizeNotifications; \
45  else \
46  _suppressResizeNotificationsMask = 0;
47 
48 CPSplitViewDidResizeSubviewsNotification = @"CPSplitViewDidResizeSubviewsNotification";
49 CPSplitViewWillResizeSubviewsNotification = @"CPSplitViewWillResizeSubviewsNotification";
50 
53 
57 
70 @implementation CPSplitView : CPView
71 {
72  id _delegate;
73  BOOL _isVertical;
74  BOOL _isPaneSplitter;
75 
76  int _currentDivider;
77  float _initialOffset;
78  CPDictionary _preCollapsePositions;
79 
80  CPString _originComponent;
81  CPString _sizeComponent;
82 
83  CPArray _DOMDividerElements;
84  CPString _dividerImagePath;
85  int _drawingDivider;
86 
87  CPString _autosaveName;
88  BOOL _shouldAutosave;
89  CGSize _shouldRestoreFromAutosaveUnlessFrameSize;
90 
91  BOOL _needsResizeSubviews;
92  int _suppressResizeNotificationsMask;
93 
94  CPArray _buttonBars;
95 }
96 
97 + (CPString)defaultThemeClass
98 {
99  return @"splitview";
100 }
101 
102 + (id)themeAttributes
103 {
104  return [CPDictionary dictionaryWithObjects:[1.0, 10.0, [CPColor grayColor]]
105  forKeys:[@"divider-thickness", @"pane-divider-thickness", @"pane-divider-color"]];
106 }
107 
108 /*
109  @ignore
110 */
111 + (void)initialize
112 {
113  if (self !== [CPSplitView class])
114  return;
115 
116  var bundle = [CPBundle bundleForClass:self];
117 
118  CPSplitViewHorizontalImage = CPImageInBundle("CPSplitView/CPSplitViewHorizontal.png", CGSizeMake(5.0, 10.0), bundle);
119  CPSplitViewVerticalImage = CPImageInBundle("CPSplitView/CPSplitViewVertical.png", CGSizeMake(10.0, 5.0), bundle);
120 }
121 
122 - (id)initWithFrame:(CGRect)aFrame
123 {
124  if (self = [super initWithFrame:aFrame])
125  {
126  _suppressResizeNotificationsMask = 0;
127  _preCollapsePositions = [CPMutableDictionary new];
128  _currentDivider = CPNotFound;
129 
130  _DOMDividerElements = [];
131  _buttonBars = [];
132 
133  _shouldAutosave = YES;
134 
135  [self _setVertical:YES];
136  }
137 
138  return self;
139 }
140 
145 - (float)dividerThickness
146 {
147  return [self currentValueForThemeAttribute:[self isPaneSplitter] ? @"pane-divider-thickness" : @"divider-thickness"];
148 }
149 
154 - (BOOL)isVertical
155 {
156  return _isVertical;
157 }
158 
163 - (void)setVertical:(BOOL)shouldBeVertical
164 {
165  if (![self _setVertical:shouldBeVertical])
166  return;
167 
168  // Just re-adjust evenly.
169  var frame = [self frame],
170  dividerThickness = [self dividerThickness];
171 
172  [self _postNotificationWillResize];
173 
174  var eachSize = ROUND((frame.size[_sizeComponent] - dividerThickness * (_subviews.length - 1)) / _subviews.length),
175  index = 0,
176  count = _subviews.length;
177 
178  if ([self isVertical])
179  {
180  for (; index < count; ++index)
181  [_subviews[index] setFrame:CGRectMake(ROUND((eachSize + dividerThickness) * index), 0, eachSize, frame.size.height)];
182  }
183  else
184  {
185  for (; index < count; ++index)
186  [_subviews[index] setFrame:CGRectMake(0, ROUND((eachSize + dividerThickness) * index), frame.size.width, eachSize)];
187  }
188 
189  [self setNeedsDisplay:YES];
190  [self _postNotificationDidResize];
191 
192 }
193 
194 - (BOOL)_setVertical:(BOOL)shouldBeVertical
195 {
196  var changed = (_isVertical != shouldBeVertical);
197 
198  _isVertical = shouldBeVertical;
199 
200  _originComponent = [self isVertical] ? "x" : "y";
201  _sizeComponent = [self isVertical] ? "width" : "height";
202  _dividerImagePath = [self isVertical] ? [CPSplitViewVerticalImage filename] : [CPSplitViewHorizontalImage filename];
203 
204  return changed;
205 }
206 
212 - (BOOL)isPaneSplitter
213 {
214  return _isPaneSplitter;
215 }
216 
222 - (void)setIsPaneSplitter:(BOOL)shouldBePaneSplitter
223 {
224  if (_isPaneSplitter == shouldBePaneSplitter)
225  return;
226 
227  _isPaneSplitter = shouldBePaneSplitter;
228 
229  if (_DOMDividerElements[_drawingDivider])
230  [self _setupDOMDivider];
231 
232  // The divider changes size when pane splitter mode is toggled, so the
233  // subviews need to change size too.
234  _needsResizeSubviews = YES;
235  [self setNeedsDisplay:YES];
236 }
237 
238 - (void)didAddSubview:(CPView)aSubview
239 {
240  _needsResizeSubviews = YES;
241 }
242 
248 - (BOOL)isSubviewCollapsed:(CPView)subview
249 {
250  return [subview frame].size[_sizeComponent] < 1 ? YES : NO;
251 }
252 
259 - (CGRect)rectOfDividerAtIndex:(int)aDivider
260 {
261  var frame = [_subviews[aDivider] frame],
262  rect = CGRectMakeZero();
263 
264  rect.size = [self frame].size;
265  rect.size[_sizeComponent] = [self dividerThickness];
266  rect.origin[_originComponent] = frame.origin[_originComponent] + frame.size[_sizeComponent];
267 
268  return rect;
269 }
270 
277 - (CGRect)effectiveRectOfDividerAtIndex:(int)aDivider
278 {
279  var realRect = [self rectOfDividerAtIndex:aDivider],
280  padding = 2;
281 
282  realRect.size[_sizeComponent] += padding * 2;
283  realRect.origin[_originComponent] -= padding;
284 
285  return realRect;
286 }
287 
288 - (void)drawRect:(CGRect)rect
289 {
290  var count = [_subviews count] - 1;
291 
292  while ((count--) > 0)
293  {
294  _drawingDivider = count;
295  [self drawDividerInRect:[self rectOfDividerAtIndex:count]];
296  }
297 }
298 
304 - (void)willRemoveSubview:(CPView)aView
305 {
306 #if PLATFORM(DOM)
307  var dividerToRemove = _DOMDividerElements.pop();
308 
309  // The divider may not exist if we never rendered out the DOM.
310  if (dividerToRemove)
311  CPDOMDisplayServerRemoveChild(_DOMElement, dividerToRemove);
312 #endif
313 
314  _needsResizeSubviews = YES;
315  [self setNeedsLayout];
316  [self setNeedsDisplay:YES];
317 }
318 
319 - (void)layoutSubviews
320 {
321  [self _adjustSubviewsWithCalculatedSize]
322 }
323 
328 - (void)drawDividerInRect:(CGRect)aRect
329 {
330 #if PLATFORM(DOM)
331  if (!_DOMDividerElements[_drawingDivider])
332  {
333  _DOMDividerElements[_drawingDivider] = document.createElement("div");
334 
335  _DOMDividerElements[_drawingDivider].style.position = "absolute";
336  _DOMDividerElements[_drawingDivider].style.backgroundRepeat = "repeat";
337 
338  CPDOMDisplayServerAppendChild(_DOMElement, _DOMDividerElements[_drawingDivider]);
339  }
340 
341  [self _setupDOMDivider];
342  CPDOMDisplayServerSetStyleLeftTop(_DOMDividerElements[_drawingDivider], NULL, _CGRectGetMinX(aRect), _CGRectGetMinY(aRect));
343  CPDOMDisplayServerSetStyleSize(_DOMDividerElements[_drawingDivider], _CGRectGetWidth(aRect), _CGRectGetHeight(aRect));
344 #endif
345 }
346 
347 - (void)_setupDOMDivider
348 {
349  if (_isPaneSplitter)
350  {
351  _DOMDividerElements[_drawingDivider].style.backgroundColor = "";
352  _DOMDividerElements[_drawingDivider].style.backgroundImage = "url('"+_dividerImagePath+"')";
353  }
354  else
355  {
356  _DOMDividerElements[_drawingDivider].style.backgroundColor = [[self currentValueForThemeAttribute:@"pane-divider-color"] cssString];
357  _DOMDividerElements[_drawingDivider].style.backgroundImage = "";
358  }
359 }
360 
361 - (void)viewWillDraw
362 {
363  [self _adjustSubviewsWithCalculatedSize];
364 }
365 
366 - (void)_adjustSubviewsWithCalculatedSize
367 {
368  if (!_needsResizeSubviews)
369  return;
370 
371  _needsResizeSubviews = NO;
372 
373  [self resizeSubviewsWithOldSize:[self _calculateSize]];
374 }
375 
376 - (CGSize)_calculateSize
377 {
378  var subviews = [self subviews],
379  count = subviews.length,
380  size = CGSizeMakeZero();
381 
382  if ([self isVertical])
383  {
384  size.width += [self dividerThickness] * (count - 1);
385  size.height = CGRectGetHeight([self frame]);
386  }
387  else
388  {
389  size.width = CGRectGetWidth([self frame]);
390  size.height += [self dividerThickness] * (count - 1);
391  }
392 
393  while (count--)
394  size[_sizeComponent] += [subviews[count] frame].size[_sizeComponent];
395 
396  return size;
397 }
398 
399 - (BOOL)cursorAtPoint:(CGPoint)aPoint hitDividerAtIndex:(int)anIndex
400 {
401  var frame = [_subviews[anIndex] frame],
402  startPosition = frame.origin[_originComponent] + frame.size[_sizeComponent],
403  effectiveRect = [self effectiveRectOfDividerAtIndex:anIndex],
404  buttonBar = _buttonBars[anIndex],
405  buttonBarRect = null,
406  additionalRect = null;
407 
408  if (buttonBar != null)
409  {
410  buttonBarRect = [buttonBar resizeControlFrame];
411  buttonBarRect.origin = [self convertPoint:buttonBarRect.origin fromView:buttonBar];
412  }
413 
414  if ([_delegate respondsToSelector:@selector(splitView:effectiveRect:forDrawnRect:ofDividerAtIndex:)])
415  effectiveRect = [_delegate splitView:self effectiveRect:effectiveRect forDrawnRect:effectiveRect ofDividerAtIndex:anIndex];
416 
417  if ([_delegate respondsToSelector:@selector(splitView:additionalEffectiveRectOfDividerAtIndex:)])
418  additionalRect = [_delegate splitView:self additionalEffectiveRectOfDividerAtIndex:anIndex];
419 
420  return CGRectContainsPoint(effectiveRect, aPoint) ||
421  (additionalRect && CGRectContainsPoint(additionalRect, aPoint)) ||
422  (buttonBarRect && CGRectContainsPoint(buttonBarRect, aPoint));
423 }
424 
425 - (CPView)hitTest:(CGPoint)aPoint
426 {
427  if ([self isHidden] || ![self hitTests] || !CGRectContainsPoint([self frame], aPoint))
428  return nil;
429 
430  var point = [self convertPoint:aPoint fromView:[self superview]],
431  count = [_subviews count] - 1;
432 
433  for (var i = 0; i < count; i++)
434  {
435  if ([self cursorAtPoint:point hitDividerAtIndex:i])
436  return self;
437  }
438 
439  return [super hitTest:aPoint];
440 }
441 
442 /*
443  Tracks the divider.
444  @param anEvent the input event
445 */
446 - (void)trackDivider:(CPEvent)anEvent
447 {
448  var type = [anEvent type];
449 
450  if (type == CPLeftMouseUp)
451  {
452  // We disabled autosaving during tracking.
453  _shouldAutosave = YES;
454 
455  if (_currentDivider != CPNotFound)
456  {
457  _currentDivider = CPNotFound;
458  [self _autosave];
459  [self _updateResizeCursor:anEvent];
460  }
461 
462  return;
463  }
464 
465  if (type == CPLeftMouseDown)
466  {
467  var point = [self convertPoint:[anEvent locationInWindow] fromView:nil],
468  count = [_subviews count] - 1;
469 
470  _currentDivider = CPNotFound;
471 
472  for (var i = 0; i < count; i++)
473  {
474  var frame = [_subviews[i] frame],
475  startPosition = frame.origin[_originComponent] + frame.size[_sizeComponent];
476 
477  if ([self cursorAtPoint:point hitDividerAtIndex:i])
478  {
479  if ([anEvent clickCount] == 2 &&
480  [_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)] &&
481  [_delegate respondsToSelector:@selector(splitView:shouldCollapseSubview:forDoubleClickOnDividerAtIndex:)])
482  {
483  var minPosition = [self minPossiblePositionOfDividerAtIndex:i],
484  maxPosition = [self maxPossiblePositionOfDividerAtIndex:i],
485  preCollapsePosition = [_preCollapsePositions objectForKey:"" + i] || 0;
486 
487  if ([_delegate splitView:self canCollapseSubview:_subviews[i]] && [_delegate splitView:self shouldCollapseSubview:_subviews[i] forDoubleClickOnDividerAtIndex:i])
488  {
489  if ([self isSubviewCollapsed:_subviews[i]])
490  [self setPosition:preCollapsePosition ? preCollapsePosition : (minPosition + (maxPosition - minPosition) / 2) ofDividerAtIndex:i];
491  else
492  [self setPosition:minPosition ofDividerAtIndex:i];
493  }
494  else if ([_delegate splitView:self canCollapseSubview:_subviews[i + 1]] && [_delegate splitView:self shouldCollapseSubview:_subviews[i + 1] forDoubleClickOnDividerAtIndex:i])
495  {
496  if ([self isSubviewCollapsed:_subviews[i + 1]])
497  [self setPosition:preCollapsePosition ? preCollapsePosition : (minPosition + (maxPosition - minPosition) / 2) ofDividerAtIndex:i];
498  else
499  [self setPosition:maxPosition ofDividerAtIndex:i];
500  }
501  }
502  else
503  {
504  _currentDivider = i;
505  _initialOffset = startPosition - point[_originComponent];
506 
507  // Don't autosave during a resize. We'll wait until it's done.
508  _shouldAutosave = NO;
509  [self _postNotificationWillResize];
510  }
511  }
512  }
513 
514  if (_currentDivider === CPNotFound)
515  return;
516  }
517 
518  else if (type == CPLeftMouseDragged && _currentDivider != CPNotFound)
519  {
520  var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
521 
522  [self setPosition:(point[_originComponent] + _initialOffset) ofDividerAtIndex:_currentDivider];
523  // Cursor might change if we reach a resize limit.
524  [self _updateResizeCursor:anEvent];
525  }
526 
527  [CPApp setTarget:self selector:@selector(trackDivider:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES];
528 }
529 
530 - (void)mouseDown:(CPEvent)anEvent
531 {
532  // FIXME: This should not trap events if not on a divider!
533  [self trackDivider:anEvent];
534 }
535 
536 - (void)viewDidMoveToWindow
537 {
538  // Enable split view resize cursors. Commented out pending CPTrackingArea implementation.
539  //[[self window] setAcceptsMouseMovedEvents:YES];
540 }
541 
542 - (void)mouseEntered:(CPEvent)anEvent
543 {
544  // Tracking code handles cursor by itself.
545  if (_currentDivider == CPNotFound)
546  [self _updateResizeCursor:anEvent];
547 }
548 
549 - (void)mouseMoved:(CPEvent)anEvent
550 {
551  if (_currentDivider == CPNotFound)
552  [self _updateResizeCursor:anEvent];
553 }
554 
555 - (void)mouseExited:(CPEvent)anEvent
556 {
557  if (_currentDivider == CPNotFound)
558  // FIXME: we should use CPCursor push/pop (if previous currentCursor != arrow).
560 }
561 
562 - (void)_updateResizeCursor:(CPEvent)anEvent
563 {
564  var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
565 
566  if ([anEvent type] === CPLeftMouseUp && ![[self window] acceptsMouseMovedEvents])
567  {
569  return;
570  }
571 
572  for (var i = 0, count = [_subviews count] - 1; i < count; i++)
573  {
574  // If we are currently tracking, keep the resize cursor active even outside of hit areas.
575  if (_currentDivider === i || (_currentDivider == CPNotFound && [self cursorAtPoint:point hitDividerAtIndex:i]))
576  {
577  var frameA = [_subviews[i] frame],
578  sizeA = frameA.size[_sizeComponent],
579  startPosition = frameA.origin[_originComponent] + sizeA,
580  frameB = [_subviews[i + 1] frame],
581  sizeB = frameB.size[_sizeComponent],
582  canShrink = [self _realPositionForPosition:startPosition - 1 ofDividerAtIndex:i] < startPosition,
583  canGrow = [self _realPositionForPosition:startPosition + 1 ofDividerAtIndex:i] > startPosition,
584  cursor = [CPCursor arrowCursor];
585 
586  if (sizeA === 0)
587  canGrow = YES; // Subview is collapsed.
588  else if (!canShrink &&
589  [_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)] &&
590  [_delegate splitView:self canCollapseSubview:_subviews[i]])
591  {
592  canShrink = YES; // Subview is collapsible.
593  }
594 
595  if (sizeB === 0)
596  {
597  // Right/lower subview is collapsed.
598  canGrow = NO;
599  // It's safe to assume it can always be uncollapsed.
600  canShrink = YES;
601  }
602  else if (!canGrow &&
603  [_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)] &&
604  [_delegate splitView:self canCollapseSubview:_subviews[i + 1]])
605  {
606  canGrow = YES; // Right/lower subview is collapsible.
607  }
608 
609  if (_isVertical && canShrink && canGrow)
610  cursor = [CPCursor resizeLeftRightCursor];
611  else if (_isVertical && canShrink)
612  cursor = [CPCursor resizeLeftCursor];
613  else if (_isVertical && canGrow)
614  cursor = [CPCursor resizeRightCursor];
615  else if (canShrink && canGrow)
616  cursor = [CPCursor resizeUpDownCursor];
617  else if (canShrink)
618  cursor = [CPCursor resizeUpCursor];
619  else if (canGrow)
620  cursor = [CPCursor resizeDownCursor];
621 
622  [cursor set];
623  return;
624  }
625  }
626 
628 }
629 
635 - (float)maxPossiblePositionOfDividerAtIndex:(int)dividerIndex
636 {
637  var frame = [_subviews[dividerIndex + 1] frame];
638 
639  if (dividerIndex + 1 < [_subviews count] - 1)
640  return frame.origin[_originComponent] + frame.size[_sizeComponent] - [self dividerThickness];
641  else
642  return [self frame].size[_sizeComponent] - [self dividerThickness];
643 }
644 
650 - (float)minPossiblePositionOfDividerAtIndex:(int)dividerIndex
651 {
652  if (dividerIndex > 0)
653  {
654  var frame = [_subviews[dividerIndex - 1] frame];
655 
656  return frame.origin[_originComponent] + frame.size[_sizeComponent] + [self dividerThickness];
657  }
658  else
659  return 0;
660 }
661 
662 - (int)_realPositionForPosition:(float)position ofDividerAtIndex:(int)dividerIndex
663 {
664  // not sure where this should override other positions?
665  if ([_delegate respondsToSelector:@selector(splitView:constrainSplitPosition:ofSubviewAt:)])
666  {
667  var proposedPosition = [_delegate splitView:self constrainSplitPosition:position ofSubviewAt:dividerIndex];
668 
669  // Silently ignore bad positions which could result from odd delegate responses. We don't want these
670  // bad results to go into the system and cause havoc with frame sizes as the split view tries to resize
671  // its subviews.
672  if (_IS_NUMERIC(proposedPosition))
673  position = proposedPosition;
674  }
675 
676  var proposedMax = [self maxPossiblePositionOfDividerAtIndex:dividerIndex],
677  proposedMin = [self minPossiblePositionOfDividerAtIndex:dividerIndex],
678  actualMax = proposedMax,
679  actualMin = proposedMin;
680 
681  if ([_delegate respondsToSelector:@selector(splitView:constrainMinCoordinate:ofSubviewAt:)])
682  {
683  var proposedActualMin = [_delegate splitView:self constrainMinCoordinate:proposedMin ofSubviewAt:dividerIndex];
684 
685  if (_IS_NUMERIC(proposedActualMin))
686  actualMin = proposedActualMin;
687  }
688 
689  if ([_delegate respondsToSelector:@selector(splitView:constrainMaxCoordinate:ofSubviewAt:)])
690  {
691  var proposedActualMax = [_delegate splitView:self constrainMaxCoordinate:proposedMax ofSubviewAt:dividerIndex];
692 
693  if (_IS_NUMERIC(proposedActualMax))
694  actualMax = proposedActualMax;
695  }
696 
697  var viewA = _subviews[dividerIndex],
698  viewB = _subviews[dividerIndex + 1],
699  realPosition = MAX(MIN(position, actualMax), actualMin);
700 
701  // Is this position past the halfway point to collapse?
702  if (position < proposedMin + (actualMin - proposedMin) / 2)
703  if ([_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)])
704  if ([_delegate splitView:self canCollapseSubview:viewA])
705  realPosition = proposedMin;
706 
707  // We can also collapse to the right.
708  if (position > proposedMax - (proposedMax - actualMax) / 2)
709  if ([_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)])
710  if ([_delegate splitView:self canCollapseSubview:viewB])
711  realPosition = proposedMax;
712 
713  return realPosition;
714 }
715 
721 - (void)setPosition:(float)position ofDividerAtIndex:(int)dividerIndex
722 {
723  // Any manual changes to the divider position should override anything we are restoring from
724  // autosave.
725  _shouldRestoreFromAutosaveUnlessFrameSize = nil;
726 
728  [self _adjustSubviewsWithCalculatedSize];
729 
730  var realPosition = [self _realPositionForPosition:position ofDividerAtIndex:dividerIndex],
731  viewA = _subviews[dividerIndex],
732  frameA = [viewA frame],
733  viewB = _subviews[dividerIndex + 1],
734  frameB = [viewB frame],
735  preCollapsePosition = 0,
736  preSize = frameA.size[_sizeComponent];
737 
738  frameA.size[_sizeComponent] = realPosition - frameA.origin[_originComponent];
739 
740  if (preSize !== 0 && frameA.size[_sizeComponent] === 0)
741  preCollapsePosition = preSize;
742 
743  if (preSize !== frameA.size[_sizeComponent])
744  {
746  [_subviews[dividerIndex] setFrame:frameA];
748  }
749 
750  preSize = frameB.size[_sizeComponent];
751 
752  var preOrigin = frameB.origin[_originComponent];
753  frameB.size[_sizeComponent] = frameB.origin[_originComponent] + frameB.size[_sizeComponent] - realPosition - [self dividerThickness];
754 
755  if (preSize !== 0 && frameB.size[_sizeComponent] === 0)
756  preCollapsePosition = frameB.origin[_originComponent];
757 
758  frameB.origin[_originComponent] = realPosition + [self dividerThickness];
759 
760  if (preSize !== frameB.size[_sizeComponent] || preOrigin !== frameB.origin[_originComponent])
761  {
763  [_subviews[dividerIndex + 1] setFrame:frameB];
765  }
766 
767  if (preCollapsePosition)
768  [_preCollapsePositions setObject:preCollapsePosition forKey:"" + dividerIndex];
769 
770  [self setNeedsDisplay:YES];
771 
773  [self _postNotificationDidResize];
774 
776 }
777 
778 - (void)setFrameSize:(CGSize)aSize
779 {
780  if (_shouldRestoreFromAutosaveUnlessFrameSize)
781  _shouldAutosave = NO;
782  else
783  [self _adjustSubviewsWithCalculatedSize];
784 
785  [super setFrameSize:aSize];
786 
787  if (_shouldRestoreFromAutosaveUnlessFrameSize)
788  _shouldAutosave = YES;
789 
790  [self setNeedsDisplay:YES];
791 }
792 
793 - (void)resizeSubviewsWithOldSize:(CPSize)oldSize
794 {
795  if ([_delegate respondsToSelector:@selector(splitView:resizeSubviewsWithOldSize:)])
796  {
797  [_delegate splitView:self resizeSubviewsWithOldSize:oldSize];
798  return;
799  }
800 
801  [self adjustSubviews];
802 }
803 
804 - (void)adjustSubviews
805 {
806  var count = [_subviews count];
807 
808  if (!count)
809  return;
810 
812  [self _postNotificationWillResize];
813 
814  var index = 0,
815  bounds = [self bounds],
816  boundsSize = bounds.size[_sizeComponent],
817  oldSize = [self _calculateSize],
818  dividerThickness = [self dividerThickness],
819  totalDividers = count - 1,
820  oldFlexibleSpace = 0,
821  totalSizablePanes = 0,
822  isSizableMap = {},
823  viewSizes = [],
824  delegateRespondsToShouldAdjust = [_delegate respondsToSelector:@selector(splitView:shouldAdjustSizeOfSubview:)];
825 
826  // What we want to do is to preserve non resizable sizes first, and then to preserve the ratio of size to available
827  // non fixed space for every other subview. E.g. assume fixed space was 20 pixels initially, view 1 was 20 and
828  // view 2 was 30 pixels, for a total of 70 pixels. Then the new total size becomes 140 pixels. Now we want the fixed
829  // space to still be 20 pixels, view 1 to be 48 pixels and view 2 to be 72 pixels. This way the relative size of
830  // view 1 to view 2 remains the same - view 1 was 66% of view 2 initially and after the resize view 1 is still
831  // 66% of view 2's size.
832  //
833  // For this calculation, we can consider the dividers themselves to also be fixed size areas - they should remain
834  // the same size before and after.
835 
836  // How much flexible size do we have in pre-resize pixels?
837  for (index = 0; index < count; ++index)
838  {
839  var view = _subviews[index],
840  isSizable = !delegateRespondsToShouldAdjust || [_delegate splitView:self shouldAdjustSizeOfSubview:view],
841  size = [view frame].size[_sizeComponent];
842 
843  isSizableMap[index] = isSizable;
844  viewSizes.push(size);
845 
846  if (isSizable)
847  {
848  oldFlexibleSpace += size;
849  totalSizablePanes++;
850  }
851  }
852 
853  // nonSizableSpace is the number of fixed pixels in pre-resize terms and the desired number post-resize.
854  var nonSizableSpace = oldSize[_sizeComponent] - oldFlexibleSpace,
855  newFlexibleSpace = boundsSize - nonSizableSpace,
856  remainingFixedPixelsToRemove = 0;
857 
858  if (newFlexibleSpace < 0)
859  {
860  remainingFixedPixelsToRemove = -newFlexibleSpace;
861  newFlexibleSpace = 0;
862  }
863 
864  var remainingFixedPanes = count - totalSizablePanes;
865 
866  for (index = 0; index < count; ++index)
867  {
868  var view = _subviews[index],
869  viewFrame = CGRectMakeCopy(bounds),
870  isSizable = isSizableMap[index],
871  targetSize = 0;
872 
873  // The last area must take up exactly the remaining space, fixed or not.
874  if (index + 1 === count)
875  targetSize = boundsSize - viewFrame.origin[_originComponent];
876  // Try to keep fixed size areas the same size.
877  else if (!isSizable)
878  {
879  var removedFixedPixels = MIN(remainingFixedPixelsToRemove / remainingFixedPanes, viewSizes[index]);
880  targetSize = viewSizes[index] - removedFixedPixels;
881  remainingFixedPixelsToRemove -= removedFixedPixels;
882  remainingFixedPanes--;
883  }
884  // (new size / flexible size available) == (old size / old flexible size available)
885  else if (oldFlexibleSpace > 0)
886  targetSize = newFlexibleSpace * viewSizes[index] / oldFlexibleSpace;
887  // oldFlexibleSpace <= 0 so all flexible areas were crushed. When we get space, allocate it evenly.
888  // totalSizablePanes cannot be 0 since isSizable.
889  else
890  targetSize = newFlexibleSpace / totalSizablePanes;
891 
892  targetSize = MAX(0, ROUND(targetSize));
893  viewFrame.size[_sizeComponent] = targetSize;
894  [view setFrame:viewFrame];
895  bounds.origin[_originComponent] += targetSize + dividerThickness;
896  }
897 
899 }
900 
966 - (void)setDelegate:(id)delegate
967 {
968  if ([_delegate respondsToSelector:@selector(splitViewDidResizeSubviews:)])
969  [[CPNotificationCenter defaultCenter] removeObserver:_delegate name:CPSplitViewDidResizeSubviewsNotification object:self];
970  if ([_delegate respondsToSelector:@selector(splitViewWillResizeSubviews:)])
971  [[CPNotificationCenter defaultCenter] removeObserver:_delegate name:CPSplitViewWillResizeSubviewsNotification object:self];
972 
973  _delegate = delegate;
974 
975  if ([_delegate respondsToSelector:@selector(splitViewDidResizeSubviews:)])
978  name:CPSplitViewDidResizeSubviewsNotification
979  object:self];
980  if ([_delegate respondsToSelector:@selector(splitViewWillResizeSubviews:)])
983  name:CPSplitViewWillResizeSubviewsNotification
984  object:self];
985 }
986 
1002 // FIXME Should be renamed to setButtonBar:ofDividerAtIndex:.
1003 - (void)setButtonBar:(CPButtonBar)aButtonBar forDividerAtIndex:(unsigned)dividerIndex
1004 {
1005  if (!aButtonBar)
1006  {
1007  _buttonBars[dividerIndex] = nil;
1008  return;
1009  }
1010 
1011  var view = [aButtonBar superview],
1012  subview = aButtonBar;
1013 
1014  while (view && view !== self)
1015  {
1016  subview = view;
1017  view = [view superview];
1018  }
1019 
1020  if (view !== self)
1021  [CPException raise:CPInvalidArgumentException
1022  reason:@"CPSplitView button bar must be a subview of the split view."];
1023 
1024  var viewIndex = [[self subviews] indexOfObject:subview];
1025 
1026  [aButtonBar setHasResizeControl:YES];
1027  [aButtonBar setResizeControlIsLeftAligned:dividerIndex < viewIndex];
1028 
1029  _buttonBars[dividerIndex] = aButtonBar;
1030 }
1031 
1032 - (void)_postNotificationWillResize
1033 {
1034  var userInfo = nil;
1035 
1036  if (_currentDivider !== CPNotFound)
1037  userInfo = [CPDictionary dictionaryWithObject:_currentDivider forKey:@"CPSplitViewDividerIndex"];
1038 
1039  [[CPNotificationCenter defaultCenter] postNotificationName:CPSplitViewWillResizeSubviewsNotification
1040  object:self
1041  userInfo:userInfo];
1042 }
1043 
1044 - (void)_postNotificationDidResize
1045 {
1046  var userInfo = nil;
1047 
1048  if (_currentDivider !== CPNotFound)
1049  userInfo = [CPDictionary dictionaryWithObject:_currentDivider forKey:@"CPSplitViewDividerIndex"];
1050 
1051  [[CPNotificationCenter defaultCenter] postNotificationName:CPSplitViewDidResizeSubviewsNotification
1052  object:self
1053  userInfo:userInfo];
1054 
1055 
1056  // TODO Cocoa always autosaves on "viewDidEndLiveResize". If Cappuccino adds support for this we
1057  // should do the same.
1058  [self _autosave];
1059 }
1060 
1066 - (void)setAutosaveName:(CPString)autosaveName
1067 {
1068  if (_autosaveName == autosaveName)
1069  return;
1070 
1071  _autosaveName = autosaveName;
1072 }
1073 
1079 - (CPString)autosaveName
1080 {
1081  return _autosaveName;
1082 }
1083 
1087 - (void)_autosave
1088 {
1089  if (_shouldRestoreFromAutosaveUnlessFrameSize || !_shouldAutosave || !_autosaveName)
1090  return;
1091 
1092  var userDefaults = [CPUserDefaults standardUserDefaults],
1093  autosaveName = [self _framesKeyForAutosaveName:[self autosaveName]],
1094  autosavePrecollapseName = [self _precollapseKeyForAutosaveName:[self autosaveName]],
1095  count = [_subviews count],
1096  positions = [CPMutableArray new],
1097  preCollapseArray = [CPMutableArray new];
1098 
1099  for (var i = 0; i < count; i++)
1100  {
1101  var frame = [_subviews[i] frame];
1102  [positions addObject:CPStringFromRect(frame)];
1103  [preCollapseArray addObject:[_preCollapsePositions objectForKey:"" + i]];
1104  }
1105 
1106  [userDefaults setObject:positions forKey:autosaveName];
1107  [userDefaults setObject:preCollapseArray forKey:autosavePrecollapseName];
1108 }
1109 
1116 - (void)_restoreFromAutosaveIfNeeded
1117 {
1118  if (_shouldRestoreFromAutosaveUnlessFrameSize && !_CGSizeEqualToSize([self frameSize], _shouldRestoreFromAutosaveUnlessFrameSize))
1119  {
1120  [self _restoreFromAutosave];
1121  }
1122 
1123  _shouldRestoreFromAutosaveUnlessFrameSize = nil;
1124 }
1125 
1129 - (void)_restoreFromAutosave
1130 {
1131  if (!_autosaveName)
1132  return;
1133 
1134  var autosaveName = [self _framesKeyForAutosaveName:[self autosaveName]],
1135  autosavePrecollapseName = [self _precollapseKeyForAutosaveName:[self autosaveName]],
1136  userDefaults = [CPUserDefaults standardUserDefaults],
1137  frames = [userDefaults objectForKey:autosaveName],
1138  preCollapseArray = [userDefaults objectForKey:autosavePrecollapseName];
1139 
1140  if (frames)
1141  {
1142  var dividerThickness = [self dividerThickness],
1143  position = 0;
1144 
1145  _shouldAutosave = NO;
1146 
1147  for (var i = 0, count = [frames count] - 1; i < count; i++)
1148  {
1149  var frame = CPRectFromString(frames[i]);
1150  position += frame.size[_sizeComponent];
1151 
1152  [self setPosition:position ofDividerAtIndex:i];
1153 
1154  position += dividerThickness;
1155  }
1156 
1157  _shouldAutosave = YES;
1158  }
1159 
1160  if (preCollapseArray)
1161  {
1162  _preCollapsePositions = [CPMutableDictionary new];
1163 
1164  for (var i = 0, count = [preCollapseArray count]; i < count; i++)
1165  [_preCollapsePositions setObject:preCollapseArray[i] forKey:i + ""];
1166  }
1167 }
1168 
1172 - (CPString)_framesKeyForAutosaveName:(CPString)theAutosaveName
1173 {
1174  if (!theAutosaveName)
1175  return nil;
1176 
1177  return @"CPSplitView Subview Frames " + theAutosaveName;
1178 }
1179 
1183 - (CPString)_precollapseKeyForAutosaveName:(CPString)theAutosaveName
1184 {
1185  if (!theAutosaveName)
1186  return nil;
1187 
1188  return @"CPSplitView Subview Precollapse Positions " + theAutosaveName;
1189 }
1190 
1191 @end
1192 
1193 var CPSplitViewDelegateKey = "CPSplitViewDelegateKey",
1194  CPSplitViewIsVerticalKey = "CPSplitViewIsVerticalKey",
1195  CPSplitViewIsPaneSplitterKey = "CPSplitViewIsPaneSplitterKey",
1196  CPSplitViewButtonBarsKey = "CPSplitViewButtonBarsKey",
1197  CPSplitViewAutosaveNameKey = "CPSplitViewAutosaveNameKey";
1198 
1199 @implementation CPSplitView (CPCoding)
1200 
1201 /*
1202  Initializes the split view by unarchiving data from \c aCoder.
1203  @param aCoder the coder containing the archived CPSplitView.
1204 */
1205 - (id)initWithCoder:(CPCoder)aCoder
1206 {
1207  // We need to restore this property before calling super's initWithCoder:.
1208  _autosaveName = [aCoder decodeObjectForKey:CPSplitViewAutosaveNameKey];
1209 
1210  /*
1211 
1212  It is common for the main window of a Cappuccino app window to be resized to match the browser
1213  window size at the end of the UI being loaded from a cib. But at decoding time (now) whatever
1214  window size was originally saved will be in place, so if we try to restore the autosaved divider
1215  positions now they might be constrained to the wrong positions due to the difference in frame size,
1216  and in addition they might move later when the window is resized.
1217 
1218  The workaround is to restore the position once now (so it's approximately correct during loading),
1219  and then once more in the next runloop cycle when any `setFullPlatformWindow` calls are done.
1220 
1221  (However if the frame size doesn't change before the next cycle, we should not restore the position
1222  again because that would overwrite any changes the app developer might have made in user code.)
1223 
1224  The other consideration is that any parent split views need to be restored before any child
1225  subviews, otherwise the parent restore will also change the positioning of the child.
1226 
1227  */
1228  if (_autosaveName)
1229  {
1230  // Schedule /before/ [super initWithCoder:]. This way this instance's _restoreFromAutosaveIfNeeded
1231  // will happen before that of any subviews loaded by [super initWithCoder:].
1232  [[CPRunLoop currentRunLoop] performSelector:@selector(_restoreFromAutosaveIfNeeded) target:self argument:nil order:0 modes:[CPDefaultRunLoopMode]];
1233  }
1234 
1235  self = [super initWithCoder:aCoder];
1236 
1237  if (self)
1238  {
1239  _suppressResizeNotificationsMask = 0;
1240  _preCollapsePositions = [CPMutableDictionary new];
1241 
1242  _currentDivider = CPNotFound;
1243  _shouldAutosave = YES;
1244 
1245  _DOMDividerElements = [];
1246 
1247  _buttonBars = [aCoder decodeObjectForKey:CPSplitViewButtonBarsKey] || [];
1248 
1249  [self setDelegate:[aCoder decodeObjectForKey:CPSplitViewDelegateKey]];
1250 
1251  _isPaneSplitter = [aCoder decodeBoolForKey:CPSplitViewIsPaneSplitterKey];
1252  [self _setVertical:[aCoder decodeBoolForKey:CPSplitViewIsVerticalKey]];
1253 
1254  if (_autosaveName)
1255  {
1256  [self _restoreFromAutosave];
1257  // Remember the frame size we had at this point so that we can restore again if it changes
1258  // before the next runloop cycle. See above notes.
1259  _shouldRestoreFromAutosaveUnlessFrameSize = [self frameSize];
1260  }
1261  }
1262 
1263  return self;
1264 }
1265 
1266 /*
1267  Archives this split view into the provided coder.
1268  @param aCoder the coder to which the button's instance data will be written.
1269 */
1270 - (void)encodeWithCoder:(CPCoder)aCoder
1271 {
1272  [super encodeWithCoder:aCoder];
1273 
1274  //FIXME how should we handle this?
1275  //[aCoder encodeObject:_buttonBars forKey:CPSplitViewButtonBarsKey];
1276 
1277  [aCoder encodeConditionalObject:_delegate forKey:CPSplitViewDelegateKey];
1278 
1279  [aCoder encodeBool:_isVertical forKey:CPSplitViewIsVerticalKey];
1280  [aCoder encodeBool:_isPaneSplitter forKey:CPSplitViewIsPaneSplitterKey];
1281 
1282  [aCoder encodeObject:_autosaveName forKey:CPSplitViewAutosaveNameKey];
1283 }
1284 
1285 @end