API  0.9.8
 All Classes Files Functions Variables Typedefs 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 @global CPApp
27 
29 
30 @optional
31 - (BOOL)splitView:(CPSplitView)splitView canCollapseSubview:(CPView)subview;
32 - (BOOL)splitView:(CPSplitView)splitView shouldAdjustSizeOfSubview:(CPView)subview;
33 - (BOOL)splitView:(CPSplitView)splitView shouldCollapseSubview:(CPView)subview forDoubleClickOnDividerAtIndex:(CPInteger)dividerIndex;
34 - (CGRect)splitView:(CPSplitView)splitView additionalEffectiveRectOfDividerAtIndex:(CPInteger)dividerIndex;
35 - (CGRect)splitView:(CPSplitView)splitView effectiveRect:(CGRect)proposedEffectiveRect forDrawnRect:(CGRect)drawnRect ofDividerAtIndex:(CPInteger)dividerIndex;
36 - (float)splitView:(CPSplitView)splitView constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(CPInteger)dividerIndex;
37 - (float)splitView:(CPSplitView)splitView constrainMinCoordinate:(float)proposedMin ofSubviewAt:(CPInteger)dividerIndex;
38 - (float)splitView:(CPSplitView)splitView constrainSplitPosition:(float)proposedPosition ofSubviewAt:(CPInteger)dividerIndex;
39 - (void)splitView:(CPSplitView)splitView resizeSubviewsWithOldSize:(CGSize)oldSize;
40 - (void)splitViewDidResizeSubviews:(CPNotification)aNotification;
41 - (void)splitViewWillResizeSubviews:(CPNotification)aNotification;
42 
43 @end
44 
54 
55 #define SPLIT_VIEW_MAYBE_POST_WILL_RESIZE() \
56  if ((_suppressResizeNotificationsMask & DidPostWillResizeNotification) === 0) \
57  { \
58  [self _postNotificationWillResize]; \
59  _suppressResizeNotificationsMask |= DidPostWillResizeNotification; \
60  }
61 
62 #define SPLIT_VIEW_MAYBE_POST_DID_RESIZE() \
63  if ((_suppressResizeNotificationsMask & ShouldSuppressResizeNotifications) !== 0) \
64  _suppressResizeNotificationsMask |= DidSuppressResizeNotification; \
65  else \
66  [self _postNotificationDidResize];
67 
68 #define SPLIT_VIEW_DID_SUPPRESS_RESIZE_NOTIFICATION() \
69  ((_suppressResizeNotificationsMask & DidSuppressResizeNotification) !== 0)
70 
71 #define SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(shouldSuppress) \
72  if (shouldSuppress) \
73  _suppressResizeNotificationsMask |= ShouldSuppressResizeNotifications; \
74  else \
75  _suppressResizeNotificationsMask = 0;
76 
77 CPSplitViewDidResizeSubviewsNotification = @"CPSplitViewDidResizeSubviewsNotification";
78 CPSplitViewWillResizeSubviewsNotification = @"CPSplitViewWillResizeSubviewsNotification";
79 
83 
95 @implementation CPSplitView : CPView
96 {
97  id <CPSplitViewDelegate> _delegate;
98  BOOL _isVertical;
99  BOOL _isPaneSplitter;
100 
101  int _currentDivider;
102  float _initialOffset;
103  CPDictionary _preCollapsePositions;
104 
105  CPString _originComponent;
106  CPString _sizeComponent;
107 
108  CPArray _DOMDividerElements;
109  CPString _dividerImagePath;
110  int _drawingDivider;
111 
112  CPString _autosaveName;
113  BOOL _shouldAutosave;
114  CGSize _shouldRestoreFromAutosaveUnlessFrameSize;
115 
116  BOOL _needsResizeSubviews;
117  int _suppressResizeNotificationsMask;
118 
119  CPArray _buttonBars;
120 
121  unsigned _implementedDelegateMethods;
122 }
123 
124 + (CPString)defaultThemeClass
125 {
126  return @"splitview";
127 }
128 
129 + (CPDictionary)themeAttributes
130 {
131  return @{
132  @"divider-thickness": 1.0,
133  @"pane-divider-thickness": 10.0,
134  @"pane-divider-color": [CPColor grayColor],
135  @"horizontal-divider-color": [CPNull null],
136  @"vertical-divider-color": [CPNull null],
137  };
138 }
139 
140 - (id)initWithFrame:(CGRect)aFrame
141 {
142  if (self = [super initWithFrame:aFrame])
143  {
144  _suppressResizeNotificationsMask = 0;
145  _preCollapsePositions = [CPMutableDictionary new];
146  _currentDivider = CPNotFound;
147 
148  _DOMDividerElements = [];
149  _buttonBars = [];
150 
151  _shouldAutosave = YES;
152 
153  [self _setVertical:YES];
154  }
155 
156  return self;
157 }
158 
163 - (float)dividerThickness
164 {
165  return [self currentValueForThemeAttribute:[self isPaneSplitter] ? @"pane-divider-thickness" : @"divider-thickness"];
166 }
167 
172 - (BOOL)isVertical
173 {
174  return _isVertical;
175 }
176 
181 - (void)setVertical:(BOOL)shouldBeVertical
182 {
183  if (![self _setVertical:shouldBeVertical])
184  return;
185 
186  // Just re-adjust evenly.
187  var frame = [self frame],
188  dividerThickness = [self dividerThickness];
189 
190  [self _postNotificationWillResize];
191 
192  var eachSize = ROUND((frame.size[_sizeComponent] - dividerThickness * (_subviews.length - 1)) / _subviews.length),
193  index = 0,
194  count = _subviews.length;
195 
196  if ([self isVertical])
197  {
198  for (; index < count; ++index)
199  [_subviews[index] setFrame:CGRectMake(ROUND((eachSize + dividerThickness) * index), 0, eachSize, frame.size.height)];
200  }
201  else
202  {
203  for (; index < count; ++index)
204  [_subviews[index] setFrame:CGRectMake(0, ROUND((eachSize + dividerThickness) * index), frame.size.width, eachSize)];
205  }
206 
207  [self setNeedsDisplay:YES];
208  [self _postNotificationDidResize];
209 
210 }
211 
212 - (BOOL)_setVertical:(BOOL)shouldBeVertical
213 {
214  var changed = (_isVertical != shouldBeVertical);
215 
216  _isVertical = shouldBeVertical;
217 
218  _originComponent = [self isVertical] ? "x" : "y";
219  _sizeComponent = [self isVertical] ? "width" : "height";
220  _dividerImagePath = [self isVertical] ? [[self valueForThemeAttribute:@"vertical-divider-color"] filename] : [[self valueForThemeAttribute:@"horizontal-divider-color"] filename];
221 
222  return changed;
223 }
224 
230 - (BOOL)isPaneSplitter
231 {
232  return _isPaneSplitter;
233 }
234 
240 - (void)setIsPaneSplitter:(BOOL)shouldBePaneSplitter
241 {
242  if (_isPaneSplitter == shouldBePaneSplitter)
243  return;
244 
245  _isPaneSplitter = shouldBePaneSplitter;
246 
247  if (_DOMDividerElements[_drawingDivider])
248  [self _setupDOMDivider];
249 
250  // The divider changes size when pane splitter mode is toggled, so the
251  // subviews need to change size too.
252  _needsResizeSubviews = YES;
253  [self setNeedsDisplay:YES];
254 }
255 
256 - (void)didAddSubview:(CPView)aSubview
257 {
258  _needsResizeSubviews = YES;
259 }
260 
266 - (BOOL)isSubviewCollapsed:(CPView)subview
267 {
268  return [subview frame].size[_sizeComponent] < 1 ? YES : NO;
269 }
270 
277 - (CGRect)rectOfDividerAtIndex:(int)aDivider
278 {
279  var frame = [_subviews[aDivider] frame],
280  rect = CGRectMakeZero();
281 
282  rect.size = [self frame].size;
283  rect.size[_sizeComponent] = [self dividerThickness];
284  rect.origin[_originComponent] = frame.origin[_originComponent] + frame.size[_sizeComponent];
285 
286  return rect;
287 }
288 
295 - (CGRect)effectiveRectOfDividerAtIndex:(int)aDivider
296 {
297  var realRect = [self rectOfDividerAtIndex:aDivider],
298  padding = 2;
299 
300  realRect.size[_sizeComponent] += padding * 2;
301  realRect.origin[_originComponent] -= padding;
302 
303  return realRect;
304 }
305 
306 - (void)drawRect:(CGRect)rect
307 {
308  var count = [_subviews count] - 1;
309 
310  while ((count--) > 0)
311  {
312  _drawingDivider = count;
313  [self drawDividerInRect:[self rectOfDividerAtIndex:count]];
314  }
315 }
316 
322 - (void)willRemoveSubview:(CPView)aView
323 {
324 #if PLATFORM(DOM)
325  var dividerToRemove = _DOMDividerElements.pop();
326 
327  // The divider may not exist if we never rendered out the DOM.
328  if (dividerToRemove)
329  CPDOMDisplayServerRemoveChild(_DOMElement, dividerToRemove);
330 #endif
331 
332  _needsResizeSubviews = YES;
333  [self setNeedsLayout];
334  [self setNeedsDisplay:YES];
335 }
336 
337 - (void)layoutSubviews
338 {
339  [self _adjustSubviewsWithCalculatedSize]
340 }
341 
346 - (void)drawDividerInRect:(CGRect)aRect
347 {
348 #if PLATFORM(DOM)
349  if (!_DOMDividerElements[_drawingDivider])
350  {
351  _DOMDividerElements[_drawingDivider] = document.createElement("div");
352 
353  _DOMDividerElements[_drawingDivider].style.position = "absolute";
354  _DOMDividerElements[_drawingDivider].style.backgroundRepeat = "repeat";
355 
356  CPDOMDisplayServerAppendChild(_DOMElement, _DOMDividerElements[_drawingDivider]);
357  }
358 
359  [self _setupDOMDivider];
360  CPDOMDisplayServerSetStyleLeftTop(_DOMDividerElements[_drawingDivider], NULL, CGRectGetMinX(aRect), CGRectGetMinY(aRect));
361  CPDOMDisplayServerSetStyleSize(_DOMDividerElements[_drawingDivider], CGRectGetWidth(aRect), CGRectGetHeight(aRect));
362 #endif
363 }
364 
365 - (void)_setupDOMDivider
366 {
367  if (_isPaneSplitter)
368  {
369  _DOMDividerElements[_drawingDivider].style.backgroundColor = "";
370  _DOMDividerElements[_drawingDivider].style.backgroundImage = "url('"+_dividerImagePath+"')";
371  }
372  else
373  {
374  _DOMDividerElements[_drawingDivider].style.backgroundColor = [[self currentValueForThemeAttribute:@"pane-divider-color"] cssString];
375  _DOMDividerElements[_drawingDivider].style.backgroundImage = "";
376  }
377 }
378 
379 - (void)viewWillDraw
380 {
381  [self _adjustSubviewsWithCalculatedSize];
382 }
383 
384 - (void)_adjustSubviewsWithCalculatedSize
385 {
386  if (!_needsResizeSubviews)
387  return;
388 
389  _needsResizeSubviews = NO;
390 
391  [self resizeSubviewsWithOldSize:[self _calculateSize]];
392 }
393 
394 - (CGSize)_calculateSize
395 {
396  var subviews = [self subviews],
397  count = subviews.length,
398  size = CGSizeMakeZero();
399 
400  if ([self isVertical])
401  {
402  size.width += [self dividerThickness] * (count - 1);
403  size.height = CGRectGetHeight([self frame]);
404  }
405  else
406  {
407  size.width = CGRectGetWidth([self frame]);
408  size.height += [self dividerThickness] * (count - 1);
409  }
410 
411  while (count--)
412  size[_sizeComponent] += [subviews[count] frame].size[_sizeComponent];
413 
414  return size;
415 }
416 
417 - (BOOL)cursorAtPoint:(CGPoint)aPoint hitDividerAtIndex:(int)anIndex
418 {
419  var frame = [_subviews[anIndex] frame],
420  startPosition = frame.origin[_originComponent] + frame.size[_sizeComponent],
421  effectiveRect = [self effectiveRectOfDividerAtIndex:anIndex],
422  buttonBar = _buttonBars[anIndex],
423  buttonBarRect = null,
424  additionalRect = null;
425 
426  if (buttonBar != null)
427  {
428  buttonBarRect = [buttonBar resizeControlFrame];
429  buttonBarRect.origin = [self convertPoint:buttonBarRect.origin fromView:buttonBar];
430  }
431 
432  effectiveRect = [self _sendDelegateSplitViewEffectiveRect:effectiveRect forDrawnRect:effectiveRect ofDividerAtIndex:anIndex];
433  additionalRect = [self _sendDelegateSplitViewAdditionalEffectiveRectOfDividerAtIndex:anIndex];
434 
435  return CGRectContainsPoint(effectiveRect, aPoint) ||
436  (additionalRect && CGRectContainsPoint(additionalRect, aPoint)) ||
437  (buttonBarRect && CGRectContainsPoint(buttonBarRect, aPoint));
438 }
439 
440 - (CPView)hitTest:(CGPoint)aPoint
441 {
442  if ([self isHidden] || ![self hitTests] || !CGRectContainsPoint([self frame], aPoint))
443  return nil;
444 
445  var point = [self convertPoint:aPoint fromView:[self superview]],
446  count = [_subviews count] - 1;
447 
448  for (var i = 0; i < count; i++)
449  {
450  if ([self cursorAtPoint:point hitDividerAtIndex:i])
451  return self;
452  }
453 
454  return [super hitTest:aPoint];
455 }
456 
457 /*
458  Tracks the divider.
459  @param anEvent the input event
460 */
461 - (void)trackDivider:(CPEvent)anEvent
462 {
463  var type = [anEvent type];
464 
465  if (type == CPLeftMouseUp)
466  {
467  // We disabled autosaving during tracking.
468  _shouldAutosave = YES;
469 
470  if (_currentDivider != CPNotFound)
471  {
472  _currentDivider = CPNotFound;
473  [self _autosave];
474  [self _updateResizeCursor:anEvent];
475  }
476 
477  return;
478  }
479 
480  if (type == CPLeftMouseDown)
481  {
482  var point = [self convertPoint:[anEvent locationInWindow] fromView:nil],
483  count = [_subviews count] - 1;
484 
485  _currentDivider = CPNotFound;
486 
487  for (var i = 0; i < count; i++)
488  {
489  var frame = [_subviews[i] frame],
490  startPosition = frame.origin[_originComponent] + frame.size[_sizeComponent];
491 
492  if ([self cursorAtPoint:point hitDividerAtIndex:i])
493  {
494  if ([anEvent clickCount] == 2 &&
495  [self _delegateRespondsToSplitViewCanCollapseSubview] &&
496  [self _delegateRespondsToSplitViewshouldCollapseSubviewForDoubleClickOnDividerAtIndex])
497  {
498  var minPosition = [self minPossiblePositionOfDividerAtIndex:i],
499  maxPosition = [self maxPossiblePositionOfDividerAtIndex:i],
500  preCollapsePosition = [_preCollapsePositions objectForKey:"" + i] || 0;
501 
502  if ([self _sendDelegateSplitViewCanCollapseSubview:_subviews[i]] && [self _sendDelegateSplitViewShouldCollapseSubview:_subviews[i] forDoubleClickOnDividerAtIndex:i])
503  {
504  if ([self isSubviewCollapsed:_subviews[i]])
505  [self setPosition:preCollapsePosition ? preCollapsePosition : (minPosition + (maxPosition - minPosition) / 2) ofDividerAtIndex:i];
506  else
507  [self setPosition:minPosition ofDividerAtIndex:i];
508  }
509  else if ([self _sendDelegateSplitViewCanCollapseSubview:_subviews[i + 1]] && [self _sendDelegateSplitViewShouldCollapseSubview:_subviews[i + 1] forDoubleClickOnDividerAtIndex:i])
510  {
511  if ([self isSubviewCollapsed:_subviews[i + 1]])
512  [self setPosition:preCollapsePosition ? preCollapsePosition : (minPosition + (maxPosition - minPosition) / 2) ofDividerAtIndex:i];
513  else
514  [self setPosition:maxPosition ofDividerAtIndex:i];
515  }
516  }
517  else
518  {
519  _currentDivider = i;
520  _initialOffset = startPosition - point[_originComponent];
521 
522  // Don't autosave during a resize. We'll wait until it's done.
523  _shouldAutosave = NO;
524  [self _postNotificationWillResize];
525  }
526  }
527  }
528 
529  if (_currentDivider === CPNotFound)
530  return;
531  }
532 
533  else if (type == CPLeftMouseDragged && _currentDivider != CPNotFound)
534  {
535  var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
536 
537  [self setPosition:(point[_originComponent] + _initialOffset) ofDividerAtIndex:_currentDivider];
538  // Cursor might change if we reach a resize limit.
539  [self _updateResizeCursor:anEvent];
540  }
541 
542  [CPApp setTarget:self selector:@selector(trackDivider:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES];
543 }
544 
545 - (void)mouseDown:(CPEvent)anEvent
546 {
547  // FIXME: This should not trap events if not on a divider!
548  [self trackDivider:anEvent];
549 }
550 
551 - (void)viewDidMoveToWindow
552 {
553  // Enable split view resize cursors. Commented out pending CPTrackingArea implementation.
554  //[[self window] setAcceptsMouseMovedEvents:YES];
555 }
556 
557 - (void)mouseEntered:(CPEvent)anEvent
558 {
559  // Tracking code handles cursor by itself.
560  if (_currentDivider == CPNotFound)
561  [self _updateResizeCursor:anEvent];
562 }
563 
564 - (void)mouseMoved:(CPEvent)anEvent
565 {
566  if (_currentDivider == CPNotFound)
567  [self _updateResizeCursor:anEvent];
568 }
569 
570 - (void)mouseExited:(CPEvent)anEvent
571 {
572  if (_currentDivider == CPNotFound)
573  // FIXME: we should use CPCursor push/pop (if previous currentCursor != arrow).
575 }
576 
577 - (void)_updateResizeCursor:(CPEvent)anEvent
578 {
579  var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
580 
581  if ([anEvent type] === CPLeftMouseUp && ![[self window] acceptsMouseMovedEvents])
582  {
584  return;
585  }
586 
587  for (var i = 0, count = [_subviews count] - 1; i < count; i++)
588  {
589  // If we are currently tracking, keep the resize cursor active even outside of hit areas.
590  if (_currentDivider === i || (_currentDivider == CPNotFound && [self cursorAtPoint:point hitDividerAtIndex:i]))
591  {
592  var frameA = [_subviews[i] frame],
593  sizeA = frameA.size[_sizeComponent],
594  startPosition = frameA.origin[_originComponent] + sizeA,
595  frameB = [_subviews[i + 1] frame],
596  sizeB = frameB.size[_sizeComponent],
597  canShrink = [self _realPositionForPosition:startPosition - 1 ofDividerAtIndex:i] < startPosition,
598  canGrow = [self _realPositionForPosition:startPosition + 1 ofDividerAtIndex:i] > startPosition,
599  cursor = [CPCursor arrowCursor];
600 
601  if (sizeA === 0)
602  canGrow = YES; // Subview is collapsed.
603  else if (!canShrink && [self _sendDelegateSplitViewCanCollapseSubview:_subviews[i]])
604  canShrink = YES; // Subview is collapsible.
605 
606  if (sizeB === 0)
607  {
608  // Right/lower subview is collapsed.
609  canGrow = NO;
610  // It's safe to assume it can always be uncollapsed.
611  canShrink = YES;
612  }
613  else if (!canGrow && [self _sendDelegateSplitViewCanCollapseSubview:_subviews[i + 1]])
614  {
615  canGrow = YES; // Right/lower subview is collapsible.
616  }
617 
618  if (_isVertical && canShrink && canGrow)
619  cursor = [CPCursor resizeLeftRightCursor];
620  else if (_isVertical && canShrink)
621  cursor = [CPCursor resizeLeftCursor];
622  else if (_isVertical && canGrow)
623  cursor = [CPCursor resizeRightCursor];
624  else if (canShrink && canGrow)
625  cursor = [CPCursor resizeUpDownCursor];
626  else if (canShrink)
627  cursor = [CPCursor resizeUpCursor];
628  else if (canGrow)
629  cursor = [CPCursor resizeDownCursor];
630 
631  [cursor set];
632  return;
633  }
634  }
635 
637 }
638 
644 - (float)maxPossiblePositionOfDividerAtIndex:(int)dividerIndex
645 {
646  var frame = [_subviews[dividerIndex + 1] frame];
647 
648  if (dividerIndex + 1 < [_subviews count] - 1)
649  return frame.origin[_originComponent] + frame.size[_sizeComponent] - [self dividerThickness];
650  else
651  return [self frame].size[_sizeComponent] - [self dividerThickness];
652 }
653 
659 - (float)minPossiblePositionOfDividerAtIndex:(int)dividerIndex
660 {
661  if (dividerIndex > 0)
662  {
663  var frame = [_subviews[dividerIndex - 1] frame];
664 
665  return frame.origin[_originComponent] + frame.size[_sizeComponent] + [self dividerThickness];
666  }
667  else
668  return 0;
669 }
670 
671 - (int)_realPositionForPosition:(float)position ofDividerAtIndex:(int)dividerIndex
672 {
673  // not sure where this should override other positions?
674  var proposedPosition = [self _sendDelegateSplitViewConstrainSplitPosition:position ofSubviewAt:dividerIndex];
675 
676  // Silently ignore bad positions which could result from odd delegate responses. We don't want these
677  // bad results to go into the system and cause havoc with frame sizes as the split view tries to resize
678  // its subviews.
679  if (_IS_NUMERIC(proposedPosition))
680  position = proposedPosition;
681 
682  var proposedMax = [self maxPossiblePositionOfDividerAtIndex:dividerIndex],
683  proposedMin = [self minPossiblePositionOfDividerAtIndex:dividerIndex],
684  actualMax = proposedMax,
685  actualMin = proposedMin,
686  proposedActualMin = [self _sendDelegateSplitViewConstrainMinCoordinate:proposedMin ofSubviewAt:dividerIndex],
687  proposedActualMax = [self _sendDelegateSplitViewConstrainMaxCoordinate:proposedMax ofSubviewAt:dividerIndex];
688 
689  if (_IS_NUMERIC(proposedActualMin))
690  actualMin = proposedActualMin;
691 
692  if (_IS_NUMERIC(proposedActualMax))
693  actualMax = proposedActualMax;
694 
695  var viewA = _subviews[dividerIndex],
696  viewB = _subviews[dividerIndex + 1],
697  realPosition = MAX(MIN(position, actualMax), actualMin);
698 
699  // Is this position past the halfway point to collapse?
700  if ((position < proposedMin + (actualMin - proposedMin) / 2) && [self _sendDelegateSplitViewCanCollapseSubview:viewA])
701  realPosition = proposedMin;
702 
703  // We can also collapse to the right.
704  if ((position > proposedMax - (proposedMax - actualMax) / 2) && [self _sendDelegateSplitViewCanCollapseSubview:viewB])
705  realPosition = proposedMax;
706 
707  return realPosition;
708 }
709 
715 - (void)setPosition:(float)position ofDividerAtIndex:(int)dividerIndex
716 {
717  // Any manual changes to the divider position should override anything we are restoring from
718  // autosave.
719  _shouldRestoreFromAutosaveUnlessFrameSize = nil;
720 
722  [self _adjustSubviewsWithCalculatedSize];
723 
724  var realPosition = [self _realPositionForPosition:position ofDividerAtIndex:dividerIndex],
725  viewA = _subviews[dividerIndex],
726  frameA = [viewA frame],
727  viewB = _subviews[dividerIndex + 1],
728  frameB = [viewB frame],
729  preCollapsePosition = 0,
730  preSize = frameA.size[_sizeComponent];
731 
732  frameA.size[_sizeComponent] = realPosition - frameA.origin[_originComponent];
733 
734  if (preSize !== 0 && frameA.size[_sizeComponent] === 0)
735  preCollapsePosition = preSize;
736 
737  if (preSize !== frameA.size[_sizeComponent])
738  {
740  [_subviews[dividerIndex] setFrame:frameA];
742  }
743 
744  preSize = frameB.size[_sizeComponent];
745 
746  var preOrigin = frameB.origin[_originComponent];
747  frameB.size[_sizeComponent] = frameB.origin[_originComponent] + frameB.size[_sizeComponent] - realPosition - [self dividerThickness];
748 
749  if (preSize !== 0 && frameB.size[_sizeComponent] === 0)
750  preCollapsePosition = frameB.origin[_originComponent];
751 
752  frameB.origin[_originComponent] = realPosition + [self dividerThickness];
753 
754  if (preSize !== frameB.size[_sizeComponent] || preOrigin !== frameB.origin[_originComponent])
755  {
757  [_subviews[dividerIndex + 1] setFrame:frameB];
759  }
760 
761  if (preCollapsePosition)
762  [_preCollapsePositions setObject:preCollapsePosition forKey:"" + dividerIndex];
763 
764  [self setNeedsDisplay:YES];
765 
767  [self _postNotificationDidResize];
768 
770 }
771 
772 - (void)setFrameSize:(CGSize)aSize
773 {
774  if (_shouldRestoreFromAutosaveUnlessFrameSize)
775  _shouldAutosave = NO;
776  else
777  [self _adjustSubviewsWithCalculatedSize];
778 
779  [super setFrameSize:aSize];
780 
781  if (_shouldRestoreFromAutosaveUnlessFrameSize)
782  _shouldAutosave = YES;
783 
784  [self setNeedsDisplay:YES];
785 }
786 
787 - (void)resizeSubviewsWithOldSize:(CGSize)oldSize
788 {
789  if ([self _delegateRespondsToSplitViewResizeSubviewsWithOldSize])
790  {
791  [self _sendDelegateSplitViewResizeSubviewsWithOldSize:oldSize];
792  return;
793  }
794 
795  [self adjustSubviews];
796 }
797 
798 - (void)adjustSubviews
799 {
800  var count = [_subviews count];
801 
802  if (!count)
803  return;
804 
806  [self _postNotificationWillResize];
807 
808  var index = 0,
809  bounds = [self bounds],
810  boundsSize = bounds.size[_sizeComponent],
811  oldSize = [self _calculateSize],
812  dividerThickness = [self dividerThickness],
813  totalDividers = count - 1,
814  oldFlexibleSpace = 0,
815  totalSizablePanes = 0,
816  isSizableMap = {},
817  viewSizes = [];
818 
819  // What we want to do is to preserve non resizable sizes first, and then to preserve the ratio of size to available
820  // non fixed space for every other subview. E.g. assume fixed space was 20 pixels initially, view 1 was 20 and
821  // view 2 was 30 pixels, for a total of 70 pixels. Then the new total size becomes 140 pixels. Now we want the fixed
822  // 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
823  // view 1 to view 2 remains the same - view 1 was 66% of view 2 initially and after the resize view 1 is still
824  // 66% of view 2's size.
825  //
826  // For this calculation, we can consider the dividers themselves to also be fixed size areas - they should remain
827  // the same size before and after.
828 
829  // How much flexible size do we have in pre-resize pixels?
830  for (index = 0; index < count; ++index)
831  {
832  var view = _subviews[index],
833  isSizable = [self _sendDelegateSplitViewShouldAdjustSizeOfSubview:view],
834  size = [view frame].size[_sizeComponent];
835 
836  isSizableMap[index] = isSizable;
837  viewSizes.push(size);
838 
839  if (isSizable)
840  {
841  oldFlexibleSpace += size;
842  totalSizablePanes++;
843  }
844  }
845 
846  // nonSizableSpace is the number of fixed pixels in pre-resize terms and the desired number post-resize.
847  var nonSizableSpace = oldSize[_sizeComponent] - oldFlexibleSpace,
848  newFlexibleSpace = boundsSize - nonSizableSpace,
849  remainingFixedPixelsToRemove = 0;
850 
851  if (newFlexibleSpace < 0)
852  {
853  remainingFixedPixelsToRemove = -newFlexibleSpace;
854  newFlexibleSpace = 0;
855  }
856 
857  var remainingFixedPanes = count - totalSizablePanes;
858 
859  for (index = 0; index < count; ++index)
860  {
861  var view = _subviews[index],
862  viewFrame = CGRectMakeCopy(bounds),
863  isSizable = isSizableMap[index],
864  targetSize = 0;
865 
866  // The last area must take up exactly the remaining space, fixed or not.
867  if (index + 1 === count)
868  targetSize = boundsSize - viewFrame.origin[_originComponent];
869  // Try to keep fixed size areas the same size.
870  else if (!isSizable)
871  {
872  var removedFixedPixels = MIN(remainingFixedPixelsToRemove / remainingFixedPanes, viewSizes[index]);
873  targetSize = viewSizes[index] - removedFixedPixels;
874  remainingFixedPixelsToRemove -= removedFixedPixels;
875  remainingFixedPanes--;
876  }
877  // (new size / flexible size available) == (old size / old flexible size available)
878  else if (oldFlexibleSpace > 0)
879  targetSize = newFlexibleSpace * viewSizes[index] / oldFlexibleSpace;
880  // oldFlexibleSpace <= 0 so all flexible areas were crushed. When we get space, allocate it evenly.
881  // totalSizablePanes cannot be 0 since isSizable.
882  else
883  targetSize = newFlexibleSpace / totalSizablePanes;
884 
885  targetSize = MAX(0, ROUND(targetSize));
886  viewFrame.size[_sizeComponent] = targetSize;
887  [view setFrame:viewFrame];
888  bounds.origin[_originComponent] += targetSize + dividerThickness;
889  }
890 
892 }
893 
959 - (void)setDelegate:(id <CPSplitViewDelegate>)aDelegate
960 {
961  if (_delegate === aDelegate)
962  return;
963 
964  if ([_delegate respondsToSelector:@selector(splitViewDidResizeSubviews:)])
965  [[CPNotificationCenter defaultCenter] removeObserver:_delegate name:CPSplitViewDidResizeSubviewsNotification object:self];
966 
967  if ([_delegate respondsToSelector:@selector(splitViewWillResizeSubviews:)])
968  [[CPNotificationCenter defaultCenter] removeObserver:_delegate name:CPSplitViewWillResizeSubviewsNotification object:self];
969 
970  _delegate = aDelegate;
971  _implementedDelegateMethods = 0;
972 
973  if ([_delegate respondsToSelector:@selector(splitViewDidResizeSubviews:)])
976  name:CPSplitViewDidResizeSubviewsNotification
977  object:self];
978 
979  if ([_delegate respondsToSelector:@selector(splitViewWillResizeSubviews:)])
982  name:CPSplitViewWillResizeSubviewsNotification
983  object:self];
984 
985  if ([_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)])
986  _implementedDelegateMethods |= CPSplitViewDelegate_splitView_canCollapseSubview_;
987 
988  if ([_delegate respondsToSelector:@selector(splitView:shouldAdjustSizeOfSubview:)])
989  _implementedDelegateMethods |= CPSplitViewDelegate_splitView_shouldAdjustSizeOfSubview_;
990 
991  if ([_delegate respondsToSelector:@selector(splitView:shouldCollapseSubview:forDoubleClickOnDividerAtIndex:)])
993 
994  if ([_delegate respondsToSelector:@selector(splitView:additionalEffectiveRectOfDividerAtIndex:)])
996 
997  if ([_delegate respondsToSelector:@selector(splitView:effectiveRect:forDrawnRect:ofDividerAtIndex:)])
999 
1000  if ([_delegate respondsToSelector:@selector(splitView:constrainMaxCoordinate:ofSubviewAt:)])
1002 
1003  if ([_delegate respondsToSelector:@selector(splitView:constrainMinCoordinate:ofSubviewAt:)])
1005 
1006  if ([_delegate respondsToSelector:@selector(splitView:constrainSplitPosition:ofSubviewAt:)])
1008 
1009  if ([_delegate respondsToSelector:@selector(splitView:resizeSubviewsWithOldSize:)])
1010  _implementedDelegateMethods |= CPSplitViewDelegate_splitView_resizeSubviewsWithOldSize_;
1011 
1012 }
1013 
1029 // FIXME Should be renamed to setButtonBar:ofDividerAtIndex:.
1030 - (void)setButtonBar:(CPButtonBar)aButtonBar forDividerAtIndex:(CPUInteger)dividerIndex
1031 {
1032  if (!aButtonBar)
1033  {
1034  _buttonBars[dividerIndex] = nil;
1035  return;
1036  }
1037 
1038  var view = [aButtonBar superview],
1039  subview = aButtonBar;
1040 
1041  while (view && view !== self)
1042  {
1043  subview = view;
1044  view = [view superview];
1045  }
1046 
1047  if (view !== self)
1048  [CPException raise:CPInvalidArgumentException
1049  reason:@"CPSplitView button bar must be a subview of the split view."];
1050 
1051  var viewIndex = [[self subviews] indexOfObject:subview];
1052 
1053  [aButtonBar setHasResizeControl:YES];
1054  [aButtonBar setResizeControlIsLeftAligned:dividerIndex < viewIndex];
1055 
1056  _buttonBars[dividerIndex] = aButtonBar;
1057 }
1058 
1059 - (void)_postNotificationWillResize
1060 {
1061  var userInfo = nil;
1062 
1063  if (_currentDivider !== CPNotFound)
1064  userInfo = @{ @"CPSplitViewDividerIndex": _currentDivider };
1065 
1066  [[CPNotificationCenter defaultCenter] postNotificationName:CPSplitViewWillResizeSubviewsNotification
1067  object:self
1068  userInfo:userInfo];
1069 }
1070 
1071 - (void)_postNotificationDidResize
1072 {
1073  var userInfo = nil;
1074 
1075  if (_currentDivider !== CPNotFound)
1076  userInfo = @{ @"CPSplitViewDividerIndex": _currentDivider };
1077 
1078  [[CPNotificationCenter defaultCenter] postNotificationName:CPSplitViewDidResizeSubviewsNotification
1079  object:self
1080  userInfo:userInfo];
1081 
1082 
1083  // TODO Cocoa always autosaves on "viewDidEndLiveResize". If Cappuccino adds support for this we
1084  // should do the same.
1085  [self _autosave];
1086 }
1087 
1093 - (void)setAutosaveName:(CPString)autosaveName
1094 {
1095  if (_autosaveName == autosaveName)
1096  return;
1097 
1098  _autosaveName = autosaveName;
1099 }
1100 
1106 - (CPString)autosaveName
1107 {
1108  return _autosaveName;
1109 }
1110 
1114 - (void)_autosave
1115 {
1116  if (_shouldRestoreFromAutosaveUnlessFrameSize || !_shouldAutosave || !_autosaveName)
1117  return;
1118 
1119  var userDefaults = [CPUserDefaults standardUserDefaults],
1120  autosaveName = [self _framesKeyForAutosaveName:[self autosaveName]],
1121  autosavePrecollapseName = [self _precollapseKeyForAutosaveName:[self autosaveName]],
1122  count = [_subviews count],
1123  positions = [CPMutableArray new],
1124  preCollapseArray = [CPMutableArray new];
1125 
1126  for (var i = 0; i < count; i++)
1127  {
1128  var frame = [_subviews[i] frame];
1129  [positions addObject:CGStringFromRect(frame)];
1130  [preCollapseArray addObject:[_preCollapsePositions objectForKey:"" + i]];
1131  }
1132 
1133  [userDefaults setObject:positions forKey:autosaveName];
1134  [userDefaults setObject:preCollapseArray forKey:autosavePrecollapseName];
1135 }
1136 
1143 - (void)_restoreFromAutosaveIfNeeded
1144 {
1145  if (_shouldRestoreFromAutosaveUnlessFrameSize && !CGSizeEqualToSize([self frameSize], _shouldRestoreFromAutosaveUnlessFrameSize))
1146  {
1147  [self _restoreFromAutosave];
1148  }
1149 
1150  _shouldRestoreFromAutosaveUnlessFrameSize = nil;
1151 }
1152 
1156 - (void)_restoreFromAutosave
1157 {
1158  if (!_autosaveName)
1159  return;
1160 
1161  var autosaveName = [self _framesKeyForAutosaveName:[self autosaveName]],
1162  autosavePrecollapseName = [self _precollapseKeyForAutosaveName:[self autosaveName]],
1163  userDefaults = [CPUserDefaults standardUserDefaults],
1164  frames = [userDefaults objectForKey:autosaveName],
1165  preCollapseArray = [userDefaults objectForKey:autosavePrecollapseName];
1166 
1167  if (frames)
1168  {
1169  var dividerThickness = [self dividerThickness],
1170  position = 0;
1171 
1172  _shouldAutosave = NO;
1173 
1174  for (var i = 0, count = [frames count] - 1; i < count; i++)
1175  {
1176  var frame = CGRectFromString(frames[i]);
1177  position += frame.size[_sizeComponent];
1178 
1179  [self setPosition:position ofDividerAtIndex:i];
1180 
1181  position += dividerThickness;
1182  }
1183 
1184  _shouldAutosave = YES;
1185  }
1186 
1187  if (preCollapseArray)
1188  {
1189  _preCollapsePositions = [CPMutableDictionary new];
1190 
1191  for (var i = 0, count = [preCollapseArray count]; i < count; i++)
1192  {
1193  var item = preCollapseArray[i];
1194 
1195  if (item === nil)
1196  [_preCollapsePositions removeObjectForKey:String(i)];
1197  else
1198  [_preCollapsePositions setObject:item forKey:String(i)];
1199  }
1200  }
1201 }
1202 
1206 - (CPString)_framesKeyForAutosaveName:(CPString)theAutosaveName
1207 {
1208  if (!theAutosaveName)
1209  return nil;
1210 
1211  return @"CPSplitView Subview Frames " + theAutosaveName;
1212 }
1213 
1217 - (CPString)_precollapseKeyForAutosaveName:(CPString)theAutosaveName
1218 {
1219  if (!theAutosaveName)
1220  return nil;
1221 
1222  return @"CPSplitView Subview Precollapse Positions " + theAutosaveName;
1223 }
1224 
1225 @end
1226 
1227 
1229 
1234 - (BOOL)_delegateRespondsToSplitViewResizeSubviewsWithOldSize
1235 {
1236  return _implementedDelegateMethods & CPSplitViewDelegate_splitView_resizeSubviewsWithOldSize_;
1237 }
1238 
1243 - (BOOL)_delegateRespondsToSplitViewCanCollapseSubview
1244 {
1245  return _implementedDelegateMethods & CPSplitViewDelegate_splitView_canCollapseSubview_;
1246 }
1247 
1252 - (BOOL)_delegateRespondsToSplitViewshouldCollapseSubviewForDoubleClickOnDividerAtIndex
1253 {
1255 }
1256 
1257 
1262 - (BOOL)_sendDelegateSplitViewCanCollapseSubview:(CPView)aView
1263 {
1264  if (!(_implementedDelegateMethods & CPSplitViewDelegate_splitView_canCollapseSubview_))
1265  return NO;
1266 
1267  return [_delegate splitView:self canCollapseSubview:aView];
1268 }
1269 
1274 - (BOOL)_sendDelegateSplitViewShouldAdjustSizeOfSubview:(CPView)aView
1275 {
1276  if (!(_implementedDelegateMethods & CPSplitViewDelegate_splitView_shouldAdjustSizeOfSubview_))
1277  return YES;
1278 
1279  return [_delegate splitView:self shouldAdjustSizeOfSubview:aView];
1280 }
1281 
1286 - (BOOL)_sendDelegateSplitViewShouldCollapseSubview:(CPView)aView forDoubleClickOnDividerAtIndex:(int)anIndex
1287 {
1289  return NO;
1290 
1291  return [_delegate splitView:self shouldCollapseSubview:aView forDoubleClickOnDividerAtIndex:anIndex];
1292 }
1293 
1298 - (CGRect)_sendDelegateSplitViewAdditionalEffectiveRectOfDividerAtIndex:(int)anIndex
1299 {
1301  return nil;
1302 
1303  return [_delegate splitView:self additionalEffectiveRectOfDividerAtIndex:anIndex];
1304 }
1305 
1310 - (CGRect)_sendDelegateSplitViewEffectiveRect:(CGRect)proposedEffectiveRect forDrawnRect:(CGRect)drawnRect ofDividerAtIndex:(CPInteger)dividerIndex
1311 {
1313  return proposedEffectiveRect;
1314 
1315  return [_delegate splitView:self effectiveRect:proposedEffectiveRect forDrawnRect:drawnRect ofDividerAtIndex:dividerIndex];
1316 }
1317 
1322 - (float)_sendDelegateSplitViewConstrainMaxCoordinate:(float)proposedMax ofSubviewAt:(CPInteger)dividerIndex
1323 {
1324  if (!(_implementedDelegateMethods & CPSplitViewDelegate_splitView_constrainMaxCoordinate_ofSubviewAt_))
1325  return nil;
1326 
1327  return [_delegate splitView:self constrainMaxCoordinate:proposedMax ofSubviewAt:dividerIndex];
1328 }
1329 
1334 - (float)_sendDelegateSplitViewConstrainMinCoordinate:(float)proposedMin ofSubviewAt:(CPInteger)dividerIndex
1335 {
1336  if (!(_implementedDelegateMethods & CPSplitViewDelegate_splitView_constrainMinCoordinate_ofSubviewAt_))
1337  return nil;
1338 
1339  return [_delegate splitView:self constrainMinCoordinate:proposedMin ofSubviewAt:dividerIndex];
1340 }
1341 
1346 - (float)_sendDelegateSplitViewConstrainSplitPosition:(float)proposedMax ofSubviewAt:(CPInteger)dividerIndex
1347 {
1348  if (!(_implementedDelegateMethods & CPSplitViewDelegate_splitView_constrainSplitPosition_ofSubviewAt_))
1349  return nil;
1350 
1351  return [_delegate splitView:self constrainSplitPosition:proposedMax ofSubviewAt:dividerIndex];
1352 }
1353 
1358 - (void)_sendDelegateSplitViewResizeSubviewsWithOldSize:(CGSize)oldSize
1359 {
1360  if (!(_implementedDelegateMethods & CPSplitViewDelegate_splitView_resizeSubviewsWithOldSize_))
1361  return;
1362 
1363  [_delegate splitView:self resizeSubviewsWithOldSize:oldSize];
1364 }
1365 
1366 @end
1367 
1368 
1369 var CPSplitViewDelegateKey = "CPSplitViewDelegateKey",
1370  CPSplitViewIsVerticalKey = "CPSplitViewIsVerticalKey",
1371  CPSplitViewIsPaneSplitterKey = "CPSplitViewIsPaneSplitterKey",
1372  CPSplitViewButtonBarsKey = "CPSplitViewButtonBarsKey",
1373  CPSplitViewAutosaveNameKey = "CPSplitViewAutosaveNameKey";
1374 
1375 @implementation CPSplitView (CPCoding)
1376 
1377 /*
1378  Initializes the split view by unarchiving data from \c aCoder.
1379  @param aCoder the coder containing the archived CPSplitView.
1380 */
1381 - (id)initWithCoder:(CPCoder)aCoder
1382 {
1383  // We need to restore this property before calling super's initWithCoder:.
1384  _autosaveName = [aCoder decodeObjectForKey:CPSplitViewAutosaveNameKey];
1385 
1386  /*
1387 
1388  It is common for the main window of a Cappuccino app window to be resized to match the browser
1389  window size at the end of the UI being loaded from a cib. But at decoding time (now) whatever
1390  window size was originally saved will be in place, so if we try to restore the autosaved divider
1391  positions now they might be constrained to the wrong positions due to the difference in frame size,
1392  and in addition they might move later when the window is resized.
1393 
1394  The workaround is to restore the position once now (so it's approximately correct during loading),
1395  and then once more in the next runloop cycle when any `setFullPlatformWindow` calls are done.
1396 
1397  (However if the frame size doesn't change before the next cycle, we should not restore the position
1398  again because that would overwrite any changes the app developer might have made in user code.)
1399 
1400  The other consideration is that any parent split views need to be restored before any child
1401  subviews, otherwise the parent restore will also change the positioning of the child.
1402 
1403  */
1404  if (_autosaveName)
1405  {
1406  // Schedule /before/ [super initWithCoder:]. This way this instance's _restoreFromAutosaveIfNeeded
1407  // will happen before that of any subviews loaded by [super initWithCoder:].
1408  [[CPRunLoop currentRunLoop] performSelector:@selector(_restoreFromAutosaveIfNeeded) target:self argument:nil order:0 modes:[CPDefaultRunLoopMode]];
1409  }
1410 
1411  self = [super initWithCoder:aCoder];
1412 
1413  if (self)
1414  {
1415  _suppressResizeNotificationsMask = 0;
1416  _preCollapsePositions = [CPMutableDictionary new];
1417 
1418  _currentDivider = CPNotFound;
1419  _shouldAutosave = YES;
1420 
1421  _DOMDividerElements = [];
1422 
1423  _buttonBars = [aCoder decodeObjectForKey:CPSplitViewButtonBarsKey] || [];
1424 
1425  [self setDelegate:[aCoder decodeObjectForKey:CPSplitViewDelegateKey]];
1426 
1427  _isPaneSplitter = [aCoder decodeBoolForKey:CPSplitViewIsPaneSplitterKey];
1428  [self _setVertical:[aCoder decodeBoolForKey:CPSplitViewIsVerticalKey]];
1429 
1430  if (_autosaveName)
1431  {
1432  [self _restoreFromAutosave];
1433  // Remember the frame size we had at this point so that we can restore again if it changes
1434  // before the next runloop cycle. See above notes.
1435  _shouldRestoreFromAutosaveUnlessFrameSize = [self frameSize];
1436  }
1437  }
1438 
1439  return self;
1440 }
1441 
1442 /*
1443  Archives this split view into the provided coder.
1444  @param aCoder the coder to which the button's instance data will be written.
1445 */
1446 - (void)encodeWithCoder:(CPCoder)aCoder
1447 {
1448  [super encodeWithCoder:aCoder];
1449 
1450  //FIXME how should we handle this?
1451  //[aCoder encodeObject:_buttonBars forKey:CPSplitViewButtonBarsKey];
1452 
1453  [aCoder encodeConditionalObject:_delegate forKey:CPSplitViewDelegateKey];
1454 
1455  [aCoder encodeBool:_isVertical forKey:CPSplitViewIsVerticalKey];
1456  [aCoder encodeBool:_isPaneSplitter forKey:CPSplitViewIsPaneSplitterKey];
1457 
1458  [aCoder encodeObject:_autosaveName forKey:CPSplitViewAutosaveNameKey];
1459 }
1460 
1461 @end