API 0.9.5
AppKit/CPSplitView.j
Go to the documentation of this file.
00001 /*
00002  * CPSplitView.j
00003  * AppKit
00004  *
00005  * Created by Thomas Robinson.
00006  * Copyright 2008, 280 North, Inc.
00007  *
00008  * This library is free software; you can redistribute it and/or
00009  * modify it under the terms of the GNU Lesser General Public
00010  * License as published by the Free Software Foundation; either
00011  * version 2.1 of the License, or (at your option) any later version.
00012  *
00013  * This library is distributed in the hope that it will be useful,
00014  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00015  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
00016  * Lesser General Public License for more details.
00017  *
00018  * You should have received a copy of the GNU Lesser General Public
00019  * License along with this library; if not, write to the Free Software
00020  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
00021  */
00022 
00023 #include "../Foundation/Foundation.h"
00024 
00025 
00026 #define SPLIT_VIEW_MAYBE_POST_WILL_RESIZE() \
00027     if ((_suppressResizeNotificationsMask & DidPostWillResizeNotification) === 0) \
00028     { \
00029         [self _postNotificationWillResize]; \
00030         _suppressResizeNotificationsMask |= DidPostWillResizeNotification; \
00031     }
00032 
00033 #define SPLIT_VIEW_MAYBE_POST_DID_RESIZE() \
00034     if ((_suppressResizeNotificationsMask & ShouldSuppressResizeNotifications) !== 0) \
00035         _suppressResizeNotificationsMask |= DidSuppressResizeNotification; \
00036     else \
00037         [self _postNotificationDidResize];
00038 
00039 #define SPLIT_VIEW_DID_SUPPRESS_RESIZE_NOTIFICATION() \
00040     ((_suppressResizeNotificationsMask & DidSuppressResizeNotification) !== 0)
00041 
00042 #define SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(shouldSuppress) \
00043     if (shouldSuppress) \
00044         _suppressResizeNotificationsMask |= ShouldSuppressResizeNotifications; \
00045     else \
00046         _suppressResizeNotificationsMask = 0;
00047 
00048 CPSplitViewDidResizeSubviewsNotification = @"CPSplitViewDidResizeSubviewsNotification";
00049 CPSplitViewWillResizeSubviewsNotification = @"CPSplitViewWillResizeSubviewsNotification";
00050 
00051 var CPSplitViewHorizontalImage = nil,
00052     CPSplitViewVerticalImage = nil,
00053 
00054     ShouldSuppressResizeNotifications   = 1,
00055     DidPostWillResizeNotification       = 1 << 1,
00056     DidSuppressResizeNotification       = 1 << 2;
00057 
00070 @implementation CPSplitView : CPView
00071 {
00072     id              _delegate;
00073     BOOL            _isVertical;
00074     BOOL            _isPaneSplitter;
00075 
00076     int             _currentDivider;
00077     float           _initialOffset;
00078     CPDictionary    _preCollapsePositions;
00079 
00080     CPString        _originComponent;
00081     CPString        _sizeComponent;
00082 
00083     CPArray         _DOMDividerElements;
00084     CPString        _dividerImagePath;
00085     int             _drawingDivider;
00086 
00087     CPString        _autosaveName;
00088     BOOL            _shouldAutosave;
00089     BOOL            _needsRestoreFromAutosave;
00090 
00091     BOOL            _needsResizeSubviews;
00092     int             _suppressResizeNotificationsMask;
00093 
00094     CPArray         _buttonBars;
00095 }
00096 
00097 + (CPString)defaultThemeClass
00098 {
00099     return @"splitview";
00100 }
00101 
00102 + (id)themeAttributes
00103 {
00104     return [CPDictionary dictionaryWithObjects:[1.0, 10.0, [CPColor grayColor]]
00105                                        forKeys:[@"divider-thickness", @"pane-divider-thickness", @"pane-divider-color"]];
00106 }
00107 
00108 /*
00109     @ignore
00110 */
00111 + (void)initialize
00112 {
00113     if (self != [CPSplitView class])
00114         return;
00115 
00116     var bundle = [CPBundle bundleForClass:self];
00117     CPSplitViewHorizontalImage = [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"CPSplitView/CPSplitViewHorizontal.png"] size:CPSizeMake(5.0, 10.0)];
00118     CPSplitViewVerticalImage = [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"CPSplitView/CPSplitViewVertical.png"] size:CPSizeMake(10.0, 5.0)];
00119 }
00120 
00121 - (id)initWithFrame:(CGRect)aFrame
00122 {
00123     if (self = [super initWithFrame:aFrame])
00124     {
00125         _suppressResizeNotificationsMask = 0;
00126         _preCollapsePositions = [CPMutableDictionary new];
00127         _currentDivider = CPNotFound;
00128 
00129         _DOMDividerElements = [];
00130         _buttonBars = [];
00131 
00132         _shouldAutosave = YES;
00133 
00134         [self _setVertical:YES];
00135     }
00136 
00137     return self;
00138 }
00139 
00144 - (float)dividerThickness
00145 {
00146     return [self currentValueForThemeAttribute:[self isPaneSplitter] ? @"pane-divider-thickness" : @"divider-thickness"];
00147 }
00148 
00153 - (BOOL)isVertical
00154 {
00155     return _isVertical;
00156 }
00157 
00162 - (void)setVertical:(BOOL)shouldBeVertical
00163 {
00164     if (![self _setVertical:shouldBeVertical])
00165         return;
00166 
00167     // Just re-adjust evenly.
00168     var frame = [self frame],
00169         dividerThickness = [self dividerThickness];
00170 
00171     [self _postNotificationWillResize];
00172 
00173     var eachSize = ROUND((frame.size[_sizeComponent] - dividerThickness * (_subviews.length - 1)) / _subviews.length),
00174         index = 0,
00175         count = _subviews.length;
00176 
00177     if ([self isVertical])
00178         for (; index < count; ++index)
00179             [_subviews[index] setFrame:CGRectMake(ROUND((eachSize + dividerThickness) * index), 0, eachSize, frame.size.height)];
00180     else
00181         for (; index < count; ++index)
00182             [_subviews[index] setFrame:CGRectMake(0, ROUND((eachSize + dividerThickness) * index), frame.size.width, eachSize)];
00183 
00184     [self setNeedsDisplay:YES];
00185 
00186     [self _postNotificationDidResize];
00187 
00188 }
00189 
00190 - (BOOL)_setVertical:(BOOL)shouldBeVertical
00191 {
00192     var changed = (_isVertical != shouldBeVertical);
00193 
00194     _isVertical = shouldBeVertical;
00195 
00196     _originComponent = [self isVertical] ? "x" : "y";
00197     _sizeComponent = [self isVertical] ? "width" : "height";
00198     _dividerImagePath = [self isVertical] ? [CPSplitViewVerticalImage filename] : [CPSplitViewHorizontalImage filename];
00199 
00200     return changed;
00201 }
00202 
00208 - (BOOL)isPaneSplitter
00209 {
00210     return _isPaneSplitter;
00211 }
00212 
00218 - (void)setIsPaneSplitter:(BOOL)shouldBePaneSplitter
00219 {
00220     if (_isPaneSplitter == shouldBePaneSplitter)
00221         return;
00222 
00223     _isPaneSplitter = shouldBePaneSplitter;
00224 
00225     if (_DOMDividerElements[_drawingDivider])
00226         [self _setupDOMDivider];
00227 
00228     // The divider changes size when pane splitter mode is toggled, so the
00229     // subviews need to change size too.
00230     _needsResizeSubviews = YES;
00231     [self setNeedsDisplay:YES];
00232 }
00233 
00234 - (void)didAddSubview:(CPView)aSubview
00235 {
00236     _needsResizeSubviews = YES;
00237 }
00238 
00244 - (BOOL)isSubviewCollapsed:(CPView)subview
00245 {
00246     return [subview frame].size[_sizeComponent] < 1 ? YES : NO;
00247 }
00248 
00255 - (CGRect)rectOfDividerAtIndex:(int)aDivider
00256 {
00257     var frame = [_subviews[aDivider] frame],
00258         rect = CGRectMakeZero();
00259 
00260     rect.size = [self frame].size;
00261 
00262     rect.size[_sizeComponent] = [self dividerThickness];
00263     rect.origin[_originComponent] = frame.origin[_originComponent] + frame.size[_sizeComponent];
00264 
00265     return rect;
00266 }
00267 
00274 - (CGRect)effectiveRectOfDividerAtIndex:(int)aDivider
00275 {
00276     var realRect = [self rectOfDividerAtIndex:aDivider],
00277         padding = 2;
00278 
00279     realRect.size[_sizeComponent] += padding * 2;
00280     realRect.origin[_originComponent] -= padding;
00281 
00282     return realRect;
00283 }
00284 
00285 - (void)drawRect:(CGRect)rect
00286 {
00287     var count = [_subviews count] - 1;
00288 
00289     while ((count--) > 0)
00290     {
00291         _drawingDivider = count;
00292         [self drawDividerInRect:[self rectOfDividerAtIndex:count]];
00293     }
00294 }
00295 
00301 - (void)willRemoveSubview:(CPView)aView
00302 {
00303 #if PLATFORM(DOM)
00304     var dividerToRemove = _DOMDividerElements.pop();
00305 
00306     // The divider may not exist if we never rendered out the DOM.
00307     if (dividerToRemove)
00308         CPDOMDisplayServerRemoveChild(_DOMElement, dividerToRemove);
00309 #endif
00310 
00311     _needsResizeSubviews = YES;
00312     [self setNeedsLayout];
00313     [self setNeedsDisplay:YES];
00314 }
00315 
00316 - (void)layoutSubviews
00317 {
00318     [self _adjustSubviewsWithCalculatedSize]
00319 }
00320 
00325 - (void)drawDividerInRect:(CGRect)aRect
00326 {
00327 #if PLATFORM(DOM)
00328     if (!_DOMDividerElements[_drawingDivider])
00329     {
00330         _DOMDividerElements[_drawingDivider] = document.createElement("div");
00331 
00332         _DOMDividerElements[_drawingDivider].style.position = "absolute";
00333         _DOMDividerElements[_drawingDivider].style.backgroundRepeat = "repeat";
00334 
00335         CPDOMDisplayServerAppendChild(_DOMElement, _DOMDividerElements[_drawingDivider]);
00336     }
00337 
00338     [self _setupDOMDivider];
00339     CPDOMDisplayServerSetStyleLeftTop(_DOMDividerElements[_drawingDivider], NULL, _CGRectGetMinX(aRect), _CGRectGetMinY(aRect));
00340     CPDOMDisplayServerSetStyleSize(_DOMDividerElements[_drawingDivider], _CGRectGetWidth(aRect), _CGRectGetHeight(aRect));
00341 #endif
00342 }
00343 
00344 - (void)_setupDOMDivider
00345 {
00346     if (_isPaneSplitter)
00347     {
00348         _DOMDividerElements[_drawingDivider].style.backgroundColor = "";
00349         _DOMDividerElements[_drawingDivider].style.backgroundImage = "url('"+_dividerImagePath+"')";
00350     }
00351     else
00352     {
00353         _DOMDividerElements[_drawingDivider].style.backgroundColor = [[self currentValueForThemeAttribute:@"pane-divider-color"] cssString];
00354         _DOMDividerElements[_drawingDivider].style.backgroundImage = "";
00355     }
00356 }
00357 
00358 - (void)viewWillDraw
00359 {
00360     [self _adjustSubviewsWithCalculatedSize];
00361 }
00362 
00363 - (void)_adjustSubviewsWithCalculatedSize
00364 {
00365     if (!_needsResizeSubviews)
00366         return;
00367 
00368     _needsResizeSubviews = NO;
00369 
00370     var subviews = [self subviews],
00371         count = subviews.length,
00372         oldSize = CGSizeMakeZero();
00373 
00374     if ([self isVertical])
00375     {
00376         oldSize.width += [self dividerThickness] * (count - 1);
00377         oldSize.height = CGRectGetHeight([self frame]);
00378     }
00379     else
00380     {
00381         oldSize.width = CGRectGetWidth([self frame]);
00382         oldSize.height += [self dividerThickness] * (count - 1);
00383     }
00384 
00385     while (count--)
00386         oldSize[_sizeComponent] += [subviews[count] frame].size[_sizeComponent];
00387 
00388     [self resizeSubviewsWithOldSize:oldSize];
00389 }
00390 
00391 - (BOOL)cursorAtPoint:(CPPoint)aPoint hitDividerAtIndex:(int)anIndex
00392 {
00393     var frame = [_subviews[anIndex] frame],
00394         startPosition = frame.origin[_originComponent] + frame.size[_sizeComponent],
00395         effectiveRect = [self effectiveRectOfDividerAtIndex:anIndex],
00396         buttonBar = _buttonBars[anIndex],
00397         buttonBarRect = null,
00398         additionalRect = null;
00399 
00400     if (buttonBar != null)
00401     {
00402         buttonBarRect = [buttonBar resizeControlFrame];
00403         buttonBarRect.origin = [self convertPoint:buttonBarRect.origin fromView:buttonBar];
00404     }
00405 
00406     if ([_delegate respondsToSelector:@selector(splitView:effectiveRect:forDrawnRect:ofDividerAtIndex:)])
00407         effectiveRect = [_delegate splitView:self effectiveRect:effectiveRect forDrawnRect:effectiveRect ofDividerAtIndex:anIndex];
00408 
00409     if ([_delegate respondsToSelector:@selector(splitView:additionalEffectiveRectOfDividerAtIndex:)])
00410         additionalRect = [_delegate splitView:self additionalEffectiveRectOfDividerAtIndex:anIndex];
00411 
00412     return CGRectContainsPoint(effectiveRect, aPoint) ||
00413            (additionalRect && CGRectContainsPoint(additionalRect, aPoint)) ||
00414            (buttonBarRect && CGRectContainsPoint(buttonBarRect, aPoint));
00415 }
00416 
00417 - (CPView)hitTest:(CGPoint)aPoint
00418 {
00419     if ([self isHidden] || ![self hitTests] || !CGRectContainsPoint([self frame], aPoint))
00420         return nil;
00421 
00422     var point = [self convertPoint:aPoint fromView:[self superview]],
00423         count = [_subviews count] - 1;
00424 
00425     for (var i = 0; i < count; i++)
00426     {
00427         if ([self cursorAtPoint:point hitDividerAtIndex:i])
00428             return self;
00429     }
00430 
00431     return [super hitTest:aPoint];
00432 }
00433 
00434 /*
00435     Tracks the divider.
00436     @param anEvent the input event
00437 */
00438 - (void)trackDivider:(CPEvent)anEvent
00439 {
00440     var type = [anEvent type];
00441 
00442     if (type == CPLeftMouseUp)
00443     {
00444         if (_currentDivider != CPNotFound)
00445         {
00446             _currentDivider = CPNotFound;
00447             [self _updateResizeCursor:anEvent];
00448         }
00449 
00450         return;
00451     }
00452 
00453     if (type == CPLeftMouseDown)
00454     {
00455         var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
00456 
00457         _currentDivider = CPNotFound;
00458         var count = [_subviews count] - 1;
00459         for (var i = 0; i < count; i++)
00460         {
00461             var frame = [_subviews[i] frame],
00462                 startPosition = frame.origin[_originComponent] + frame.size[_sizeComponent];
00463 
00464             if ([self cursorAtPoint:point hitDividerAtIndex:i])
00465             {
00466                 if ([anEvent clickCount] == 2 &&
00467                     [_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)] &&
00468                     [_delegate respondsToSelector:@selector(splitView:shouldCollapseSubview:forDoubleClickOnDividerAtIndex:)])
00469                 {
00470                     var minPosition = [self minPossiblePositionOfDividerAtIndex:i],
00471                         maxPosition = [self maxPossiblePositionOfDividerAtIndex:i],
00472                         _preCollapsePosition = [_preCollapsePositions objectForKey:"" + i] || 0;
00473 
00474                     if ([_delegate splitView:self canCollapseSubview:_subviews[i]] && [_delegate splitView:self shouldCollapseSubview:_subviews[i] forDoubleClickOnDividerAtIndex:i])
00475                     {
00476                         if ([self isSubviewCollapsed:_subviews[i]])
00477                             [self setPosition:_preCollapsePosition ? _preCollapsePosition : (minPosition + (maxPosition - minPosition) / 2) ofDividerAtIndex:i];
00478                         else
00479                             [self setPosition:minPosition ofDividerAtIndex:i];
00480                     }
00481                     else if ([_delegate splitView:self canCollapseSubview:_subviews[i + 1]] && [_delegate splitView:self shouldCollapseSubview:_subviews[i + 1] forDoubleClickOnDividerAtIndex:i])
00482                     {
00483                         if ([self isSubviewCollapsed:_subviews[i + 1]])
00484                             [self setPosition:_preCollapsePosition ? _preCollapsePosition : (minPosition + (maxPosition - minPosition) / 2) ofDividerAtIndex:i];
00485                         else
00486                             [self setPosition:maxPosition ofDividerAtIndex:i];
00487                     }
00488                 }
00489                 else
00490                 {
00491                     _currentDivider = i;
00492                     _initialOffset = startPosition - point[_originComponent];
00493 
00494                     [self _postNotificationWillResize];
00495                 }
00496             }
00497         }
00498 
00499         if (_currentDivider === CPNotFound)
00500             return;
00501     }
00502 
00503     else if (type == CPLeftMouseDragged && _currentDivider != CPNotFound)
00504     {
00505         var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
00506 
00507         [self setPosition:(point[_originComponent] + _initialOffset) ofDividerAtIndex:_currentDivider];
00508         // Cursor might change if we reach a resize limit.
00509         [self _updateResizeCursor:anEvent];
00510     }
00511 
00512     [CPApp setTarget:self selector:@selector(trackDivider:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES];
00513 }
00514 
00515 - (void)mouseDown:(CPEvent)anEvent
00516 {
00517     // FIXME: This should not trap events if not on a divider!
00518     [self trackDivider:anEvent];
00519 }
00520 
00521 - (void)viewDidMoveToWindow
00522 {
00523     // Enable split view resize cursors. Commented out pending CPTrackingArea implementation.
00524     //[[self window] setAcceptsMouseMovedEvents:YES];
00525 }
00526 
00527 - (void)mouseEntered:(CPEvent)anEvent
00528 {
00529     // Tracking code handles cursor by itself.
00530     if (_currentDivider == CPNotFound)
00531         [self _updateResizeCursor:anEvent];
00532 }
00533 
00534 - (void)mouseMoved:(CPEvent)anEvent
00535 {
00536     if (_currentDivider == CPNotFound)
00537         [self _updateResizeCursor:anEvent];
00538 }
00539 
00540 - (void)mouseExited:(CPEvent)anEvent
00541 {
00542     if (_currentDivider == CPNotFound)
00543         // FIXME: we should use CPCursor push/pop (if previous currentCursor != arrow).
00544         [[CPCursor arrowCursor] set];
00545 }
00546 
00547 - (void)_updateResizeCursor:(CPEvent)anEvent
00548 {
00549     var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
00550 
00551     if ([anEvent type] === CPLeftMouseUp && ![[self window] acceptsMouseMovedEvents])
00552     {
00553         [[CPCursor arrowCursor] set];
00554         return;
00555     }
00556 
00557     for (var i = 0, count = [_subviews count] - 1; i < count; i++)
00558     {
00559         // If we are currently tracking, keep the resize cursor active even outside of hit areas.
00560         if (_currentDivider === i || (_currentDivider == CPNotFound && [self cursorAtPoint:point hitDividerAtIndex:i]))
00561         {
00562             var frameA = [_subviews[i] frame],
00563                 sizeA = frameA.size[_sizeComponent],
00564                 startPosition = frameA.origin[_originComponent] + sizeA,
00565                 frameB = [_subviews[i + 1] frame],
00566                 sizeB = frameB.size[_sizeComponent],
00567                 canShrink = [self _realPositionForPosition:startPosition - 1 ofDividerAtIndex:i] < startPosition,
00568                 canGrow = [self _realPositionForPosition:startPosition + 1 ofDividerAtIndex:i] > startPosition,
00569                 cursor = [CPCursor arrowCursor];
00570 
00571             if (sizeA === 0)
00572                 canGrow = YES; // Subview is collapsed.
00573             else if (!canShrink &&
00574                 [_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)] &&
00575                 [_delegate splitView:self canCollapseSubview:_subviews[i]])
00576                 canShrink = YES; // Subview is collapsible.
00577 
00578             if (sizeB === 0)
00579             {
00580                 // Right/lower subview is collapsed.
00581                 canGrow = NO;
00582                 // It's safe to assume it can always be uncollapsed.
00583                 canShrink = YES;
00584             }
00585             else if (!canGrow &&
00586                 [_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)] &&
00587                 [_delegate splitView:self canCollapseSubview:_subviews[i + 1]])
00588                 canGrow = YES; // Right/lower subview is collapsible.
00589 
00590             if (_isVertical && canShrink && canGrow)
00591                 cursor = [CPCursor resizeLeftRightCursor];
00592             else if (_isVertical && canShrink)
00593                 cursor = [CPCursor resizeLeftCursor];
00594             else if (_isVertical && canGrow)
00595                 cursor = [CPCursor resizeRightCursor];
00596             else if (canShrink && canGrow)
00597                 cursor = [CPCursor resizeUpDownCursor];
00598             else if (canShrink)
00599                 cursor = [CPCursor resizeUpCursor];
00600             else if (canGrow)
00601                 cursor = [CPCursor resizeDownCursor];
00602 
00603             [cursor set];
00604             return;
00605         }
00606     }
00607 
00608     [[CPCursor arrowCursor] set];
00609 }
00610 
00616 - (float)maxPossiblePositionOfDividerAtIndex:(int)dividerIndex
00617 {
00618     var frame = [_subviews[dividerIndex + 1] frame];
00619 
00620     if (dividerIndex + 1 < [_subviews count] - 1)
00621         return frame.origin[_originComponent] + frame.size[_sizeComponent] - [self dividerThickness];
00622     else
00623         return [self frame].size[_sizeComponent] - [self dividerThickness];
00624 }
00625 
00631 - (float)minPossiblePositionOfDividerAtIndex:(int)dividerIndex
00632 {
00633     if (dividerIndex > 0)
00634     {
00635         var frame = [_subviews[dividerIndex - 1] frame];
00636 
00637         return frame.origin[_originComponent] + frame.size[_sizeComponent] + [self dividerThickness];
00638     }
00639     else
00640         return 0;
00641 }
00642 
00643 - (int)_realPositionForPosition:(float)position ofDividerAtIndex:(int)dividerIndex
00644 {
00645     // not sure where this should override other positions?
00646     if ([_delegate respondsToSelector:@selector(splitView:constrainSplitPosition:ofSubviewAt:)])
00647     {
00648         var proposedPosition = [_delegate splitView:self constrainSplitPosition:position ofSubviewAt:dividerIndex];
00649 
00650         // Silently ignore bad positions which could result from odd delegate responses. We don't want these
00651         // bad results to go into the system and cause havoc with frame sizes as the split view tries to resize
00652         // its subviews.
00653         if (_IS_NUMERIC(proposedPosition))
00654             position = proposedPosition;
00655     }
00656 
00657     var proposedMax = [self maxPossiblePositionOfDividerAtIndex:dividerIndex],
00658         proposedMin = [self minPossiblePositionOfDividerAtIndex:dividerIndex],
00659         actualMax = proposedMax,
00660         actualMin = proposedMin;
00661 
00662     if ([_delegate respondsToSelector:@selector(splitView:constrainMinCoordinate:ofSubviewAt:)])
00663     {
00664         var proposedActualMin = [_delegate splitView:self constrainMinCoordinate:proposedMin ofSubviewAt:dividerIndex];
00665         if (_IS_NUMERIC(proposedActualMin))
00666             actualMin = proposedActualMin;
00667     }
00668 
00669     if ([_delegate respondsToSelector:@selector(splitView:constrainMaxCoordinate:ofSubviewAt:)])
00670     {
00671         var proposedActualMax = [_delegate splitView:self constrainMaxCoordinate:proposedMax ofSubviewAt:dividerIndex];
00672         if (_IS_NUMERIC(proposedActualMax))
00673             actualMax = proposedActualMax;
00674     }
00675 
00676     var viewA = _subviews[dividerIndex],
00677         viewB = _subviews[dividerIndex + 1],
00678         realPosition = MAX(MIN(position, actualMax), actualMin);
00679 
00680     // Is this position past the halfway point to collapse?
00681     if (position < proposedMin + (actualMin - proposedMin) / 2)
00682         if ([_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)])
00683             if ([_delegate splitView:self canCollapseSubview:viewA])
00684                 realPosition = proposedMin;
00685     // We can also collapse to the right.
00686     if (position > proposedMax - (proposedMax - actualMax) / 2)
00687         if ([_delegate respondsToSelector:@selector(splitView:canCollapseSubview:)])
00688             if ([_delegate splitView:self canCollapseSubview:viewB])
00689                 realPosition = proposedMax;
00690 
00691     return realPosition;
00692 }
00693 
00699 - (void)setPosition:(float)position ofDividerAtIndex:(int)dividerIndex
00700 {
00701     SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(YES);
00702     [self _adjustSubviewsWithCalculatedSize];
00703 
00704     var realPosition = [self _realPositionForPosition:position ofDividerAtIndex:dividerIndex];
00705 
00706     var viewA = _subviews[dividerIndex],
00707         frameA = [viewA frame],
00708         viewB = _subviews[dividerIndex + 1],
00709         frameB = [viewB frame],
00710         _preCollapsePosition = 0;
00711 
00712     var preSize = frameA.size[_sizeComponent];
00713     frameA.size[_sizeComponent] = realPosition - frameA.origin[_originComponent];
00714     if (preSize !== 0 && frameA.size[_sizeComponent] === 0)
00715         _preCollapsePosition = preSize;
00716     if (preSize !== frameA.size[_sizeComponent])
00717     {
00718         SPLIT_VIEW_MAYBE_POST_WILL_RESIZE();
00719         [_subviews[dividerIndex] setFrame:frameA];
00720         SPLIT_VIEW_MAYBE_POST_DID_RESIZE();
00721     }
00722 
00723     preSize = frameB.size[_sizeComponent];
00724     var preOrigin = frameB.origin[_originComponent];
00725     frameB.size[_sizeComponent] = frameB.origin[_originComponent] + frameB.size[_sizeComponent] - realPosition - [self dividerThickness];
00726     if (preSize !== 0 && frameB.size[_sizeComponent] === 0)
00727         _preCollapsePosition = frameB.origin[_originComponent];
00728     frameB.origin[_originComponent] = realPosition + [self dividerThickness];
00729     if (preSize !== frameB.size[_sizeComponent] || preOrigin !== frameB.origin[_originComponent])
00730     {
00731         SPLIT_VIEW_MAYBE_POST_WILL_RESIZE();
00732         [_subviews[dividerIndex + 1] setFrame:frameB];
00733         SPLIT_VIEW_MAYBE_POST_DID_RESIZE();
00734     }
00735 
00736     if (_preCollapsePosition)
00737         [_preCollapsePositions setObject:_preCollapsePosition forKey:"" + dividerIndex];
00738 
00739     [self setNeedsDisplay:YES];
00740 
00741     if (SPLIT_VIEW_DID_SUPPRESS_RESIZE_NOTIFICATION())
00742         [self _postNotificationDidResize];
00743     SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(NO);
00744 }
00745 
00746 - (void)setFrameSize:(CGSize)aSize
00747 {
00748     if (_needsRestoreFromAutosave)
00749         _shouldAutosave = NO;
00750     else
00751         [self _adjustSubviewsWithCalculatedSize];
00752 
00753     [super setFrameSize:aSize];
00754 
00755     if (_needsRestoreFromAutosave)
00756     {
00757         _needsRestoreFromAutosave = NO;
00758         [self _restoreFromAutosave];
00759         _shouldAutosave = YES;
00760     }
00761 
00762     [self setNeedsDisplay:YES];
00763 }
00764 
00765 - (void)resizeSubviewsWithOldSize:(CPSize)oldSize
00766 {
00767     if ([_delegate respondsToSelector:@selector(splitView:resizeSubviewsWithOldSize:)])
00768     {
00769         [_delegate splitView:self resizeSubviewsWithOldSize:oldSize];
00770         return;
00771     }
00772 
00773     SPLIT_VIEW_MAYBE_POST_WILL_RESIZE();
00774     [self _postNotificationWillResize];
00775 
00776     var index = 0,
00777         count = [_subviews count],
00778         bounds = [self bounds],
00779         dividerThickness = [self dividerThickness],
00780         totalDividers = count - 1,
00781         totalSizableSpace = 0,
00782         nonSizableSpace = 0,
00783         lastSizableIndex = -1,
00784         totalSizablePanes = 0,
00785         isVertical = [self isVertical];
00786 
00787     for (index = 0; index < count; ++index)
00788     {
00789         var view = _subviews[index],
00790             isSizable = isVertical ? [view autoresizingMask] & CPViewWidthSizable : [view autoresizingMask] & CPViewHeightSizable;
00791 
00792         if (isSizable)
00793         {
00794             totalSizableSpace += [view frame].size[_sizeComponent];
00795             lastSizableIndex = index;
00796             totalSizablePanes++;
00797         }
00798     }
00799 
00800     if (totalSizablePanes === count)
00801         totalSizableSpace = 0;
00802 
00803     var nonSizableSpace = totalSizableSpace ? bounds.size[_sizeComponent] - totalSizableSpace : 0,
00804         remainingFlexibleSpace = bounds.size[_sizeComponent] - oldSize[_sizeComponent],
00805         oldDimension = (oldSize[_sizeComponent] - totalDividers * dividerThickness - nonSizableSpace),
00806         ratio = oldDimension <= 0 ? 0 : (bounds.size[_sizeComponent] - totalDividers * dividerThickness - nonSizableSpace) / oldDimension;
00807 
00808     for (index = 0; index < count; ++index)
00809     {
00810         var view = _subviews[index],
00811             viewFrame = CGRectMakeCopy(bounds),
00812             isSizable = isVertical ? [view autoresizingMask] & CPViewWidthSizable : [view autoresizingMask] & CPViewHeightSizable;
00813 
00814         if (index + 1 === count)
00815             viewFrame.size[_sizeComponent] = bounds.size[_sizeComponent] - viewFrame.origin[_originComponent];
00816 
00817         else if (totalSizableSpace && isSizable && lastSizableIndex === index)
00818             viewFrame.size[_sizeComponent] = MAX(0, ROUND([view frame].size[_sizeComponent] + remainingFlexibleSpace))
00819 
00820         else if (isSizable || !totalSizableSpace)
00821         {
00822             viewFrame.size[_sizeComponent] = MAX(0, ROUND(ratio * [view frame].size[_sizeComponent]));
00823             remainingFlexibleSpace -= (viewFrame.size[_sizeComponent] - [view frame].size[_sizeComponent]);
00824         }
00825 
00826         else if (totalSizableSpace && !isSizable)
00827             viewFrame.size[_sizeComponent] = [view frame].size[_sizeComponent];
00828 
00829         bounds.origin[_originComponent] += viewFrame.size[_sizeComponent] + dividerThickness;
00830 
00831         [view setFrame:viewFrame];
00832 
00833     }
00834 
00835     SPLIT_VIEW_MAYBE_POST_DID_RESIZE();
00836 }
00837 
00898 - (void)setDelegate:(id)delegate
00899 {
00900     if ([_delegate respondsToSelector:@selector(splitViewDidResizeSubviews:)])
00901         [[CPNotificationCenter defaultCenter] removeObserver:_delegate name:CPSplitViewDidResizeSubviewsNotification object:self];
00902     if ([_delegate respondsToSelector:@selector(splitViewWillResizeSubviews:)])
00903         [[CPNotificationCenter defaultCenter] removeObserver:_delegate name:CPSplitViewWillResizeSubviewsNotification object:self];
00904 
00905    _delegate = delegate;
00906 
00907    if ([_delegate respondsToSelector:@selector(splitViewDidResizeSubviews:)])
00908        [[CPNotificationCenter defaultCenter] addObserver:_delegate
00909                                                 selector:@selector(splitViewDidResizeSubviews:)
00910                                                     name:CPSplitViewDidResizeSubviewsNotification
00911                                                   object:self];
00912    if ([_delegate respondsToSelector:@selector(splitViewWillResizeSubviews:)])
00913        [[CPNotificationCenter defaultCenter] addObserver:_delegate
00914                                                 selector:@selector(splitViewWillResizeSubviews:)
00915                                                     name:CPSplitViewWillResizeSubviewsNotification
00916                                                   object:self];
00917 }
00918 
00934 // FIXME Should be renamed to setButtonBar:ofDividerAtIndex:.
00935 - (void)setButtonBar:(CPButtonBar)aButtonBar forDividerAtIndex:(unsigned)dividerIndex
00936 {
00937     if (!aButtonBar)
00938     {
00939         _buttonBars[dividerIndex] = nil;
00940         return;
00941     }
00942 
00943     var view = [aButtonBar superview],
00944         subview = aButtonBar;
00945 
00946     while (view && view !== self)
00947     {
00948         subview = view;
00949         view = [view superview];
00950     }
00951 
00952     if (view !== self)
00953         [CPException raise:CPInvalidArgumentException
00954                     reason:@"CPSplitView button bar must be a subview of the split view."];
00955 
00956     var viewIndex = [[self subviews] indexOfObject:subview];
00957 
00958     [aButtonBar setHasResizeControl:YES];
00959     [aButtonBar setResizeControlIsLeftAligned:dividerIndex < viewIndex];
00960 
00961     _buttonBars[dividerIndex] = aButtonBar;
00962 }
00963 
00964 - (void)_postNotificationWillResize
00965 {
00966     [[CPNotificationCenter defaultCenter] postNotificationName:CPSplitViewWillResizeSubviewsNotification object:self];
00967 }
00968 
00969 - (void)_postNotificationDidResize
00970 {
00971     [self _autosave];
00972     [[CPNotificationCenter defaultCenter] postNotificationName:CPSplitViewDidResizeSubviewsNotification object:self];
00973 }
00974 
00980 - (void)setAutosaveName:(CPString)autosaveName
00981 {
00982     if (_autosaveName == autosaveName)
00983         return;
00984     _autosaveName = autosaveName;
00985 }
00986 
00992 - (CPString)autosaveName
00993 {
00994     return _autosaveName;
00995 }
00996 
01000 - (void)_autosave
01001 {
01002     if (!_shouldAutosave)
01003         return;
01004 
01005     var userDefaults = [CPUserDefaults standardUserDefaults],
01006         autosaveName = [self _framesKeyForAutosaveName:[self autosaveName]],
01007         autosavePrecollapseName = [self _precollapseKeyForAutosaveName:[self autosaveName]],
01008         count = [_subviews count],
01009         positions = [CPMutableArray new],
01010         preCollapseArray = [CPMutableArray new];
01011 
01012     for (var i = 0; i < count; i++)
01013     {
01014         var frame = [_subviews[i] frame];
01015         [positions addObject:CPStringFromRect(frame)];
01016         [preCollapseArray addObject:[_preCollapsePositions objectForKey:"" + i]];
01017     }
01018 
01019     [userDefaults setObject:positions forKey:autosaveName];
01020     [userDefaults setObject:preCollapseArray forKey:autosavePrecollapseName];
01021 }
01022 
01026 - (void)_restoreFromAutosave
01027 {
01028     if (!_autosaveName)
01029         return;
01030 
01031     var autosaveName = [self _framesKeyForAutosaveName:[self autosaveName]],
01032         autosavePrecollapseName = [self _precollapseKeyForAutosaveName:[self autosaveName]],
01033         userDefaults = [CPUserDefaults standardUserDefaults],
01034         frames = [userDefaults objectForKey:autosaveName],
01035         preCollapseArray = [userDefaults objectForKey:autosavePrecollapseName];
01036 
01037     if (frames)
01038     {
01039         var dividerThickness = [self dividerThickness],
01040             position = 0;
01041 
01042         _shouldAutosave = NO;
01043         for (var i = 0, count = [frames count] - 1; i < count; i++)
01044         {
01045             var frame = CPRectFromString(frames[i]);
01046             position += frame.size[_sizeComponent];
01047 
01048             [self setPosition:position ofDividerAtIndex:i];
01049 
01050             position += dividerThickness;
01051         }
01052         _shouldAutosave = YES;
01053     }
01054 
01055     if (preCollapseArray)
01056     {
01057         _preCollapsePositions = [CPMutableDictionary new];
01058         for (var i = 0, count = [preCollapseArray count]; i < count; i++)
01059             [_preCollapsePositions setObject:preCollapseArray[i] forKey:i + ""];
01060     }
01061 }
01062 
01066 - (CPString)_framesKeyForAutosaveName:(CPString)theAutosaveName
01067 {
01068     return @"CPSplitView Subview Frames " + theAutosaveName;
01069 }
01070 
01074 - (CPString)_precollapseKeyForAutosaveName:(CPString)theAutosaveName
01075 {
01076     return @"CPSplitView Subview Precollapse Positions " + theAutosaveName;
01077 }
01078 
01079 @end
01080 
01081 var CPSplitViewDelegateKey          = "CPSplitViewDelegateKey",
01082     CPSplitViewIsVerticalKey        = "CPSplitViewIsVerticalKey",
01083     CPSplitViewIsPaneSplitterKey    = "CPSplitViewIsPaneSplitterKey",
01084     CPSplitViewButtonBarsKey        = "CPSplitViewButtonBarsKey",
01085     CPSplitViewAutosaveNameKey      = "CPSplitViewAutosaveNameKey";
01086 
01087 @implementation CPSplitView (CPCoding)
01088 
01089 /*
01090     Initializes the split view by unarchiving data from \c aCoder.
01091     @param aCoder the coder containing the archived CPSplitView.
01092 */
01093 - (id)initWithCoder:(CPCoder)aCoder
01094 {
01095     self = [super initWithCoder:aCoder];
01096 
01097     if (self)
01098     {
01099         _suppressResizeNotificationsMask = 0;
01100         _preCollapsePositions = [CPMutableDictionary new];
01101 
01102         _currentDivider = CPNotFound;
01103         _shouldAutosave = YES;
01104 
01105         _DOMDividerElements = [];
01106 
01107         _buttonBars = [aCoder decodeObjectForKey:CPSplitViewButtonBarsKey] || [];
01108 
01109         [self setDelegate:[aCoder decodeObjectForKey:CPSplitViewDelegateKey]];
01110 
01111         _isPaneSplitter = [aCoder decodeBoolForKey:CPSplitViewIsPaneSplitterKey];
01112         [self _setVertical:[aCoder decodeBoolForKey:CPSplitViewIsVerticalKey]];
01113 
01114         [self setAutosaveName:[aCoder decodeObjectForKey:CPSplitViewAutosaveNameKey]];
01115 
01116         // We have to wait until we know our frame size before restoring, or the frame resize later will throw
01117         // away the restored size.
01118         if (_autosaveName)
01119             _needsRestoreFromAutosave = YES;
01120     }
01121 
01122     return self;
01123 }
01124 
01125 /*
01126     Archives this split view into the provided coder.
01127     @param aCoder the coder to which the button's instance data will be written.
01128 */
01129 - (void)encodeWithCoder:(CPCoder)aCoder
01130 {
01131     [super encodeWithCoder:aCoder];
01132 
01133     //FIXME how should we handle this?
01134     //[aCoder encodeObject:_buttonBars forKey:CPSplitViewButtonBarsKey];
01135 
01136     [aCoder encodeConditionalObject:_delegate forKey:CPSplitViewDelegateKey];
01137 
01138     [aCoder encodeBool:_isVertical forKey:CPSplitViewIsVerticalKey];
01139     [aCoder encodeBool:_isPaneSplitter forKey:CPSplitViewIsPaneSplitterKey];
01140 
01141     [aCoder encodeObject:_autosaveName forKey:CPSplitViewAutosaveNameKey];
01142 }
01143 
01144 @end
 All Classes Files Functions Variables Defines