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