// // BWSplitView.m // BWToolkit // // Created by Brandon Walkin (www.brandonwalkin.com) and Fraser Kuyvenhoven. // All code is provided under the New BSD license. // #import "BWSplitView.h" #import "NSColor+BWAdditions.h" #import "NSEvent+BWAdditions.h" static NSGradient *gradient; static NSImage *dimpleImageBitmap, *dimpleImageVector; static NSColor *borderColor, *gradientStartColor, *gradientEndColor; static float scaleFactor = 1.0f; #define dimpleDimension 4.0f #define RESIZE_DEBUG_LOGS 0 @interface BWSplitView (BWSVPrivate) - (void)drawDimpleInRect:(NSRect)aRect; - (void)drawGradientDividerInRect:(NSRect)aRect; - (int)resizableSubviews; - (BOOL)subviewIsResizable:(NSView *)subview; - (BOOL)subviewIsCollapsible:(NSView *)subview; - (BOOL)subviewIsCollapsed:(NSView *)subview; - (int)collapsibleSubviewIndex; - (NSView *)collapsibleSubview; - (BOOL)hasCollapsibleSubview; - (BOOL)collapsibleSubviewIsCollapsed; - (CGFloat)subviewMinimumSize:(int)subviewIndex; - (CGFloat)subviewMaximumSize:(int)subviewIndex; - (void)recalculatePreferredProportionsAndSizes; - (BOOL)validatePreferredProportionsAndSizes; - (void)validateAndCalculatePreferredProportionsAndSizes; - (void)clearPreferredProportionsAndSizes; - (void)resizeAndAdjustSubviews; @end @interface BWSplitView () @property BOOL checkboxIsEnabled; @end @implementation BWSplitView @synthesize color, colorIsEnabled, checkboxIsEnabled, minValues, maxValues, minUnits, maxUnits, collapsiblePopupSelection, dividerCanCollapse, collapsibleSubviewCollapsed; @synthesize resizableSubviewPreferredProportion, nonresizableSubviewPreferredSize, stateForLastPreferredCalculations; @synthesize toggleCollapseButton; + (void)initialize; { borderColor = [[NSColor colorWithCalibratedWhite:(165.0f / 255.0f) alpha:1] retain]; gradientStartColor = [[NSColor colorWithCalibratedWhite:(253.0f / 255.0f) alpha:1] retain]; gradientEndColor = [[NSColor colorWithCalibratedWhite:(222.0f / 255.0f) alpha:1] retain]; gradient = [[NSGradient alloc] initWithStartingColor:gradientStartColor endingColor:gradientEndColor]; NSBundle *bundle = [NSBundle bundleForClass:[BWSplitView class]]; dimpleImageBitmap = [[NSImage alloc] initWithContentsOfFile:[bundle pathForImageResource:@"GradientSplitViewDimpleBitmap.tif"]]; dimpleImageVector = [[NSImage alloc] initWithContentsOfFile:[bundle pathForImageResource:@"GradientSplitViewDimpleVector.pdf"]]; [dimpleImageBitmap setFlipped:YES]; [dimpleImageVector setFlipped:YES]; } - (id)initWithCoder:(NSCoder *)decoder; { if ((self = [super initWithCoder:decoder]) != nil) { [self setColor:[decoder decodeObjectForKey:@"BWSVColor"]]; [self setColorIsEnabled:[decoder decodeBoolForKey:@"BWSVColorIsEnabled"]]; [self setMinValues:[decoder decodeObjectForKey:@"BWSVMinValues"]]; [self setMaxValues:[decoder decodeObjectForKey:@"BWSVMaxValues"]]; [self setMinUnits:[decoder decodeObjectForKey:@"BWSVMinUnits"]]; [self setMaxUnits:[decoder decodeObjectForKey:@"BWSVMaxUnits"]]; [self setCollapsiblePopupSelection:[decoder decodeIntForKey:@"BWSVCollapsiblePopupSelection"]]; [self setDividerCanCollapse:[decoder decodeBoolForKey:@"BWSVDividerCanCollapse"]]; // Delegate set in nib has been decoded, but we want that to be the secondary delegate [self setDelegate:[super delegate]]; [super setDelegate:self]; } return self; } - (void)encodeWithCoder:(NSCoder*)coder { // Temporarily change delegate [super setDelegate:secondaryDelegate]; [super encodeWithCoder:coder]; [coder encodeObject:[self color] forKey:@"BWSVColor"]; [coder encodeBool:[self colorIsEnabled] forKey:@"BWSVColorIsEnabled"]; [coder encodeObject:[self minValues] forKey:@"BWSVMinValues"]; [coder encodeObject:[self maxValues] forKey:@"BWSVMaxValues"]; [coder encodeObject:[self minUnits] forKey:@"BWSVMinUnits"]; [coder encodeObject:[self maxUnits] forKey:@"BWSVMaxUnits"]; [coder encodeInt:[self collapsiblePopupSelection] forKey:@"BWSVCollapsiblePopupSelection"]; [coder encodeBool:[self dividerCanCollapse] forKey:@"BWSVDividerCanCollapse"]; // Set delegate back [self setDelegate:[super delegate]]; [super setDelegate:self]; } - (void)awakeFromNib { scaleFactor = [[NSScreen mainScreen] userSpaceScaleFactor]; } - (void)drawDividerInRect:(NSRect)aRect { if ([self isVertical]) { aRect.size.width = [self dividerThickness]; if (colorIsEnabled && color != nil) [color drawSwatchInRect:aRect]; else [super drawDividerInRect:aRect]; } else { aRect.size.height = [self dividerThickness]; if ([self dividerThickness] <= 1.01) { if (colorIsEnabled && color != nil) [color drawSwatchInRect:aRect]; else [super drawDividerInRect:aRect]; } else { [self drawGradientDividerInRect:aRect]; } } } - (void)drawGradientDividerInRect:(NSRect)aRect { aRect = [self centerScanRect:aRect]; // Draw gradient NSRect gradRect = NSMakeRect(aRect.origin.x,aRect.origin.y + 1 / scaleFactor,aRect.size.width,aRect.size.height - 1 / scaleFactor); [gradient drawInRect:gradRect angle:90]; // Draw top and bottom borders [borderColor drawPixelThickLineAtPosition:0 withInset:0 inRect:aRect inView:self horizontal:YES flip:NO]; [borderColor drawPixelThickLineAtPosition:0 withInset:0 inRect:aRect inView:self horizontal:YES flip:YES]; [self drawDimpleInRect:aRect]; } - (void)drawDimpleInRect:(NSRect)aRect { float startY = aRect.origin.y + roundf((aRect.size.height / 2) - (dimpleDimension / 2)); float startX = aRect.origin.x + roundf((aRect.size.width / 2) - (dimpleDimension / 2)); NSRect destRect = NSMakeRect(startX,startY,dimpleDimension,dimpleDimension); // Draw at pixel bounds destRect = [self convertRectToBase:destRect]; destRect.origin.x = floor(destRect.origin.x); double param, fractPart, intPart; param = destRect.origin.y; fractPart = modf(param, &intPart); if (fractPart < 0.99) destRect.origin.y = floor(destRect.origin.y); destRect = [self convertRectFromBase:destRect]; if (scaleFactor == 1) { NSRect dimpleRect = NSMakeRect(0,0,dimpleDimension,dimpleDimension); [dimpleImageBitmap drawInRect:destRect fromRect:dimpleRect operation:NSCompositeSourceOver fraction:1]; } else { NSRect dimpleRect = NSMakeRect(0,0,[dimpleImageVector size].width,[dimpleImageVector size].height); [dimpleImageVector drawInRect:destRect fromRect:dimpleRect operation:NSCompositeSourceOver fraction:1]; } } - (CGFloat)dividerThickness { float thickness; if ([self isVertical]) { thickness = 1; } else { if ([super dividerThickness] < 1.01) thickness = 1; else thickness = 10; } return thickness; } - (void)setDelegate:(id)anObj { if (secondaryDelegate != self) secondaryDelegate = anObj; else secondaryDelegate = nil; } - (BOOL)subviewIsCollapsible:(NSView *)subview; { // check if this is the collapsible subview int subviewIndex = [[self subviews] indexOfObject:subview]; BOOL isCollapsibleSubview = (([self collapsiblePopupSelection] == 1 && subviewIndex == 0) || ([self collapsiblePopupSelection] == 2 && subviewIndex == [[self subviews] count] - 1)); return isCollapsibleSubview; } - (BOOL)subviewIsCollapsed:(NSView *)subview; { BOOL isCollapsibleSubview = [self subviewIsCollapsible:subview]; return [super isSubviewCollapsed:subview] || (isCollapsibleSubview && collapsibleSubviewCollapsed); } - (BOOL)collapsibleSubviewIsCollapsed; { return [self subviewIsCollapsed:[self collapsibleSubview]]; } - (int)collapsibleSubviewIndex; { switch ([self collapsiblePopupSelection]) { case 1: return 0; break; case 2: return [[self subviews] count] - 1; break; default: return -1; break; } } - (NSView *)collapsibleSubview; { int index = [self collapsibleSubviewIndex]; if (index >= 0) return [[self subviews] objectAtIndex:index]; else return nil; } - (BOOL)hasCollapsibleSubview; { return [self collapsiblePopupSelection] != 0; } // This is done to support the use of Core Animation to collapse subviews - (void)adjustSubviews { [super adjustSubviews]; [[self window] invalidateCursorRectsForView:self]; } - (void)setCollapsibleSubviewCollapsedHelper:(NSNumber *)flag { [self setCollapsibleSubviewCollapsed:[flag boolValue]]; } - (void)animationEnded { isAnimating = NO; } - (float)animationDuration { if ([NSEvent shiftKeyIsDown]) return 2.0; return 0.25; } - (BOOL)hasCollapsibleDivider { if ([self hasCollapsibleSubview] && (dividerCanCollapse || [self dividerThickness] < 1.01)) return YES; return NO; } - (int)collapsibleDividerIndex { if ([self hasCollapsibleDivider]) { if ([self collapsiblePopupSelection] == 1) return 0; else if ([self collapsiblePopupSelection] == 2) return [self subviews].count - 2; } return -1; } - (void)setCollapsibleSubviewCollapsed:(BOOL)flag { collapsibleSubviewCollapsed = flag; if (flag) [[self toggleCollapseButton] setState:0]; else [[self toggleCollapseButton] setState:1]; } - (void)setMinSizeForCollapsibleSubview:(NSNumber *)minSize { if ([self hasCollapsibleSubview]) { NSMutableDictionary *tempMinValues = [[self minValues] mutableCopy]; [tempMinValues setObject:minSize forKey:[NSNumber numberWithInt:[[self subviews] indexOfObject:[self collapsibleSubview]]]]; [self setMinValues:tempMinValues]; [tempMinValues release]; } } - (void)removeMinSizeForCollapsibleSubview { if ([self hasCollapsibleSubview]) { NSMutableDictionary *tempMinValues = [[self minValues] mutableCopy]; [tempMinValues removeObjectForKey:[NSNumber numberWithInt:[[self subviews] indexOfObject:[self collapsibleSubview]]]]; [self setMinValues:tempMinValues]; [tempMinValues release]; } } - (IBAction)toggleCollapse:(id)sender { if ([self respondsToSelector:@selector(ibDidAddToDesignableDocument:)]) return; if ([self hasCollapsibleSubview] == NO || [self collapsibleSubview] == nil) return; if (isAnimating) return; // Check to see if the collapsible subview has a minimum width/height and record it. // We'll later remove the min size temporarily while animating and then restore it. BOOL hasMinSize = NO; NSNumber *minSize = [minValues objectForKey:[NSNumber numberWithInt:[[self subviews] indexOfObject:[self collapsibleSubview]]]]; minSize = [[minSize copy] autorelease]; if (minSize != nil || [minSize intValue] != 0) hasMinSize = YES; // Get a reference to the button and modify its behavior if ([self toggleCollapseButton] == nil) { [self setToggleCollapseButton:sender]; [[toggleCollapseButton cell] setHighlightsBy:NSPushInCellMask]; [[toggleCollapseButton cell] setShowsStateBy:NSContentsCellMask]; } // Temporary: For simplicty, there should only be 1 subview other than the collapsible subview that's resizable for the collapse to happen NSView *resizableSubview = nil; for (NSView *subview in [self subviews]) { if ([self subviewIsResizable:subview] && subview != [self collapsibleSubview]) { resizableSubview = subview; } } if (resizableSubview == nil) return; // Get the thickness of the collapsible divider. If the divider cannot collapse, we set it to 0 so it doesn't affect our calculations. float collapsibleDividerThickness = [self dividerThickness]; if ([self hasCollapsibleDivider] == NO) collapsibleDividerThickness = 0; if ([self isVertical]) { float constantHeight = [self collapsibleSubview].frame.size.height; if ([self collapsibleSubviewCollapsed] == NO) { uncollapsedSize = [self collapsibleSubview].frame.size.width; if (hasMinSize) [self removeMinSizeForCollapsibleSubview]; [NSAnimationContext beginGrouping]; [[NSAnimationContext currentContext] setDuration:([self animationDuration])]; [[[self collapsibleSubview] animator] setFrameSize:NSMakeSize(0.0, constantHeight)]; [[resizableSubview animator] setFrameSize:NSMakeSize(resizableSubview.frame.size.width + uncollapsedSize + collapsibleDividerThickness, constantHeight)]; [NSAnimationContext endGrouping]; if (hasMinSize) [self performSelector:@selector(setMinSizeForCollapsibleSubview:) withObject:minSize afterDelay:[self animationDuration]]; [self performSelector:@selector(setCollapsibleSubviewCollapsedHelper:) withObject:[NSNumber numberWithBool:YES] afterDelay:[self animationDuration]]; } else { if (hasMinSize) [self removeMinSizeForCollapsibleSubview]; [NSAnimationContext beginGrouping]; [[NSAnimationContext currentContext] setDuration:([self animationDuration])]; [[[self collapsibleSubview] animator] setFrameSize:NSMakeSize(uncollapsedSize, constantHeight)]; [[resizableSubview animator] setFrameSize:NSMakeSize(resizableSubview.frame.size.width - uncollapsedSize - collapsibleDividerThickness, constantHeight)]; [NSAnimationContext endGrouping]; if (hasMinSize) [self performSelector:@selector(setMinSizeForCollapsibleSubview:) withObject:minSize afterDelay:[self animationDuration]]; [self setCollapsibleSubviewCollapsed:NO]; } } else { float constantWidth = [self collapsibleSubview].frame.size.width; if ([self collapsibleSubviewCollapsed] == NO) { uncollapsedSize = [self collapsibleSubview].frame.size.height; if (hasMinSize) [self removeMinSizeForCollapsibleSubview]; [NSAnimationContext beginGrouping]; [[NSAnimationContext currentContext] setDuration:([self animationDuration])]; [[[self collapsibleSubview] animator] setFrameSize:NSMakeSize(constantWidth, 0.0)]; [[resizableSubview animator] setFrameSize:NSMakeSize(constantWidth, resizableSubview.frame.size.height + uncollapsedSize + collapsibleDividerThickness)]; [NSAnimationContext endGrouping]; if (hasMinSize) [self performSelector:@selector(setMinSizeForCollapsibleSubview:) withObject:minSize afterDelay:[self animationDuration]]; [self performSelector:@selector(setCollapsibleSubviewCollapsedHelper:) withObject:[NSNumber numberWithBool:YES] afterDelay:[self animationDuration]]; } else { if (hasMinSize) [self removeMinSizeForCollapsibleSubview]; [NSAnimationContext beginGrouping]; [[NSAnimationContext currentContext] setDuration:([self animationDuration])]; [[[self collapsibleSubview] animator] setFrameSize:NSMakeSize(constantWidth, uncollapsedSize)]; [[resizableSubview animator] setFrameSize:NSMakeSize(constantWidth, resizableSubview.frame.size.height - uncollapsedSize - collapsibleDividerThickness)]; [NSAnimationContext endGrouping]; if (hasMinSize) [self performSelector:@selector(setMinSizeForCollapsibleSubview:) withObject:minSize afterDelay:[self animationDuration]]; [self setCollapsibleSubviewCollapsed:NO]; } } isAnimating = YES; [self performSelector:@selector(animationEnded) withObject:nil afterDelay:[self animationDuration]]; [self performSelector:@selector(resizeAndAdjustSubviews) withObject:nil afterDelay:[self animationDuration]]; } #pragma mark NSSplitView Delegate Methods - (BOOL)splitView:(NSSplitView *)splitView shouldHideDividerAtIndex:(NSInteger)dividerIndex { if ([secondaryDelegate respondsToSelector:@selector(splitView:shouldHideDividerAtIndex:)]) return [secondaryDelegate splitView:splitView shouldHideDividerAtIndex:dividerIndex]; if ([self respondsToSelector:@selector(ibDidAddToDesignableDocument:)] == NO) { if ([self hasCollapsibleDivider] && [self collapsibleDividerIndex] == dividerIndex) { [self setDividerCanCollapse:YES]; return YES; } } return NO; } - (NSRect)splitView:(NSSplitView *)splitView additionalEffectiveRectOfDividerAtIndex:(NSInteger)dividerIndex { if ([secondaryDelegate respondsToSelector:@selector(splitView:additionalEffectiveRectOfDividerAtIndex:)]) return [secondaryDelegate splitView:splitView additionalEffectiveRectOfDividerAtIndex:dividerIndex]; return NSZeroRect; } - (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview { if ([secondaryDelegate respondsToSelector:@selector(splitView:canCollapseSubview:)]) return [secondaryDelegate splitView:sender canCollapseSubview:subview]; int subviewIndex = [[self subviews] indexOfObject:subview]; if ([self respondsToSelector:@selector(ibDidAddToDesignableDocument:)] == NO) { if ([self collapsiblePopupSelection] == 1 && subviewIndex == 0) return YES; else if ([self collapsiblePopupSelection] == 2 && subviewIndex == [[self subviews] count] - 1) return YES; } return NO; } - (BOOL)splitView:(NSSplitView *)splitView shouldCollapseSubview:(NSView *)subview forDoubleClickOnDividerAtIndex:(NSInteger)dividerIndex { if ([secondaryDelegate respondsToSelector:@selector(splitView:shouldCollapseSubview:forDoubleClickOnDividerAtIndex:)]) return [secondaryDelegate splitView:splitView shouldCollapseSubview:subview forDoubleClickOnDividerAtIndex:dividerIndex]; int subviewIndex = [[self subviews] indexOfObject:subview]; if ([self respondsToSelector:@selector(ibDidAddToDesignableDocument:)] == NO) { if (([self collapsiblePopupSelection] == 1 && subviewIndex == 0 && dividerIndex == 0) || ([self collapsiblePopupSelection] == 2 && subviewIndex == [[self subviews] count] - 1 && dividerIndex == [[splitView subviews] count] - 2)) { [self setCollapsibleSubviewCollapsed:YES]; // Cause the collapse ourselves by calling the resize method [self resizeAndAdjustSubviews]; [self setNeedsDisplay:YES]; // Since we manually did the resize above, we pretend that we don't want to collapse return NO; } } return NO; } - (CGFloat)splitView:(NSSplitView *)sender constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)offset { if ([secondaryDelegate respondsToSelector:@selector(splitView:constrainMaxCoordinate:ofSubviewAt:)]) return [secondaryDelegate splitView:sender constrainMaxCoordinate:proposedMax ofSubviewAt:offset]; // Max coordinate depends on max of subview offset, and the min of subview offset + 1 CGFloat newMaxFromThisSubview = proposedMax; CGFloat newMaxFromNextSubview = proposedMax; // Max from this subview CGFloat maxValue = [self subviewMaximumSize:offset]; if (maxValue != FLT_MAX) { NSView *subview = [[self subviews] objectAtIndex:offset]; CGFloat originCoord = [self isVertical] ? [subview frame].origin.x : [subview frame].origin.y; newMaxFromThisSubview = originCoord + maxValue; } // Max from the next subview int nextOffset = offset + 1; if ([[self subviews] count] > nextOffset) { CGFloat minValue = [self subviewMinimumSize:nextOffset]; if (minValue != 0) { NSView *subview = [[self subviews] objectAtIndex:nextOffset]; CGFloat endCoord = [self isVertical] ? [subview frame].origin.x + [subview frame].size.width : [subview frame].origin.y + [subview frame].size.height; newMaxFromNextSubview = endCoord - minValue - [self dividerThickness]; // This could cause trouble when over constrained (TODO) } } CGFloat newMax = fminf(newMaxFromThisSubview, newMaxFromNextSubview); if (newMax < proposedMax) return newMax; return proposedMax; } - (CGFloat)splitView:(NSSplitView *)sender constrainMinCoordinate:(CGFloat)proposedMin ofSubviewAt:(NSInteger)offset { if ([secondaryDelegate respondsToSelector:@selector(splitView:constrainMinCoordinate:ofSubviewAt:)]) return [secondaryDelegate splitView:sender constrainMinCoordinate:proposedMin ofSubviewAt:offset]; // Min coordinate depends on min of subview offset and the max of subview offset + 1 CGFloat newMinFromThisSubview = proposedMin; CGFloat newMaxFromNextSubview = proposedMin; // Min from this subview CGFloat minValue = [self subviewMinimumSize:offset]; if (minValue != 0) { NSView *subview = [[self subviews] objectAtIndex:offset]; CGFloat originCoord = [self isVertical] ? [subview frame].origin.x : [subview frame].origin.y; newMinFromThisSubview = originCoord + minValue; } // Min from the next subview int nextOffset = offset + 1; if ([[self subviews] count] > nextOffset) { CGFloat maxValue = [self subviewMaximumSize:nextOffset]; if (maxValue != FLT_MAX) { NSView *subview = [[self subviews] objectAtIndex:nextOffset]; CGFloat endCoord = [self isVertical] ? [subview frame].origin.x + [subview frame].size.width : [subview frame].origin.y + [subview frame].size.height; newMaxFromNextSubview = endCoord - maxValue - [self dividerThickness]; // This could cause trouble when over constrained (TODO) } } CGFloat newMin = fmaxf(newMinFromThisSubview, newMaxFromNextSubview); if (newMin > proposedMin) return newMin; return proposedMin; } - (CGFloat)splitView:(NSSplitView *)sender constrainSplitPosition:(CGFloat)proposedPosition ofSubviewAt:(NSInteger)offset { [self clearPreferredProportionsAndSizes]; if ([self respondsToSelector:@selector(ibDidAddToDesignableDocument:)]) return proposedPosition; if ([secondaryDelegate respondsToSelector:@selector(splitView:constrainSplitPosition:ofSubviewAt:)]) return [secondaryDelegate splitView:sender constrainSplitPosition:proposedPosition ofSubviewAt:offset]; return proposedPosition; } - (NSRect)splitView:(NSSplitView *)splitView effectiveRect:(NSRect)proposedEffectiveRect forDrawnRect:(NSRect)drawnRect ofDividerAtIndex:(NSInteger)dividerIndex { if ([secondaryDelegate respondsToSelector:@selector(splitView:effectiveRect:forDrawnRect:ofDividerAtIndex:)]) return [secondaryDelegate splitView:splitView effectiveRect:proposedEffectiveRect forDrawnRect:drawnRect ofDividerAtIndex:dividerIndex]; return proposedEffectiveRect; } - (void)splitViewDidResizeSubviews:(NSNotification *)aNotification { if (collapsibleSubviewCollapsed && ([self isVertical] ? [[self collapsibleSubview] frame].size.width > 0 : [[self collapsibleSubview] frame].size.height > 0)) { [self setCollapsibleSubviewCollapsed:NO]; [self resizeAndAdjustSubviews]; } else if (!collapsibleSubviewCollapsed && ([self isVertical] ? [[self collapsibleSubview] frame].size.width < 0.1 : [[self collapsibleSubview] frame].size.height < 0.1)) { [self setCollapsibleSubviewCollapsed:YES]; [self resizeAndAdjustSubviews]; } else if ([self collapsibleSubviewIsCollapsed]) { [self resizeAndAdjustSubviews]; } [self setNeedsDisplay:YES]; } #pragma mark - Resize Subviews Delegate Method and Helper Methods - (int)resizableSubviews { int resizableSubviews = 0; for (NSView *subview in [self subviews]) { if ([self subviewIsResizable:subview]) resizableSubviews++; } return resizableSubviews; } - (BOOL)subviewIsResizable:(NSView *)subview { if ([self isVertical] && [subview autoresizingMask] & NSViewWidthSizable) return YES; if (![self isVertical] && [subview autoresizingMask] & NSViewHeightSizable) return YES; return NO; } - (CGFloat)subviewMinimumSize:(int)subviewIndex; { NSNumber *minNum = [minValues objectForKey:[NSNumber numberWithInt:subviewIndex]]; if (!minNum) return 0; int units = 0; NSNumber *unitsNum = [minUnits objectForKey:[NSNumber numberWithInt:subviewIndex]]; if (unitsNum) units = [unitsNum intValue]; CGFloat min = [minNum floatValue]; switch (units) { case 1: { // Percent CGFloat dividerThicknessTotal = [self dividerThickness] * ([[self subviews] count] - 1); CGFloat totalSize = [self isVertical] ? [self frame].size.width : [self frame].size.height; totalSize -= dividerThicknessTotal; return roundf((min / 100.0) * totalSize); break; } case 0: default: { // Points return min; break; } } } - (CGFloat)subviewMaximumSize:(int)subviewIndex; { NSNumber *maxNum = [maxValues objectForKey:[NSNumber numberWithInt:subviewIndex]]; if (!maxNum) return FLT_MAX; int units = 0; NSNumber *unitsNum = [maxUnits objectForKey:[NSNumber numberWithInt:subviewIndex]]; if (unitsNum) units = [unitsNum intValue]; CGFloat max = [maxNum floatValue]; switch (units) { case 1: { // Percent CGFloat dividerThicknessTotal = [self dividerThickness] * ([[self subviews] count] - 1); CGFloat totalSize = [self isVertical] ? [self frame].size.width : [self frame].size.height; totalSize -= dividerThicknessTotal; return roundf((max / 100.0) * totalSize); break; } case 0: default: { // Points return max; break; } } } // PREFERRED PROPORTIONS AND SIZES // // Preferred proportions (for resizable) // Need to store resizable subviews preferred proportions for calculating new sizes // // Preferred sizes (for non-resizable) // If a non-resizable subview is ever forced larger or smaller than it prefers, we need to know it's preferred size // // Need to recalculate both of the above whenever a divider is moved, or a subview is added/removed or changed between resizable/non-resizable - (void)recalculatePreferredProportionsAndSizes; { NSMutableArray *stateArray = [NSMutableArray arrayWithCapacity:[[self subviews] count]]; NSMutableDictionary *preferredProportions = [NSMutableDictionary dictionary]; NSMutableDictionary *preferredSizes = [NSMutableDictionary dictionary]; // Total is only the sum of resizable subviews CGFloat resizableTotal = 0; // Calculate resizable total for (NSView *subview in [self subviews]) { if ([self subviewIsResizable:subview]) resizableTotal += [self isVertical] ? [subview frame].size.width : [subview frame].size.height; } // Calculate resizable preferred propotions and set non-resizable preferred sizes for (NSView *subview in [self subviews]) { int index = [[self subviews] indexOfObject:subview]; if ([self subviewIsResizable:subview]) { CGFloat size = [self isVertical] ? [subview frame].size.width : [subview frame].size.height; CGFloat proportion = (resizableTotal > 0) ? (size / resizableTotal) : 0; [preferredProportions setObject:[NSNumber numberWithFloat:proportion] forKey:[NSNumber numberWithInt:index]]; [stateArray addObject:[NSNumber numberWithBool:YES]]; } else { CGFloat size = [self isVertical] ? [subview frame].size.width : [subview frame].size.height; [preferredSizes setObject:[NSNumber numberWithFloat:size] forKey:[NSNumber numberWithInt:index]]; [stateArray addObject:[NSNumber numberWithBool:NO]]; } } [self setResizableSubviewPreferredProportion:preferredProportions]; [self setNonresizableSubviewPreferredSize:preferredSizes]; if (RESIZE_DEBUG_LOGS) NSLog(@"resizableSubviewPreferredProportion: %@", resizableSubviewPreferredProportion); if (RESIZE_DEBUG_LOGS) NSLog(@"nonresizableSubviewPreferredSize: %@", nonresizableSubviewPreferredSize); // Remember state to know when to recalculate [self setStateForLastPreferredCalculations:stateArray]; if (RESIZE_DEBUG_LOGS) NSLog(@"stateForLastPreferredCalculations: %@", stateForLastPreferredCalculations); } // Checks if the number or type of subviews has changed since we last recalculated - (BOOL)validatePreferredProportionsAndSizes; { if (RESIZE_DEBUG_LOGS) NSLog(@"validating preferred proportions and sizes"); // Check if we even have saved proportions and sizes if (![self resizableSubviewPreferredProportion] || ![self nonresizableSubviewPreferredSize]) return NO; // Check if number of items has changed if ([[self subviews] count] != [[self stateForLastPreferredCalculations] count]) return NO; // Check if any of the subviews have changed between resizable and non-resizable for (NSView *subview in [self subviews]) { int index = [[self subviews] indexOfObject:subview]; if ([self subviewIsResizable:subview] != [[[self stateForLastPreferredCalculations] objectAtIndex:index] boolValue]) return NO; } return YES; } - (void)correctCollapsiblePreferredProportionOrSize; { // TODO: Assuming that the collapsible subview does not change between resizable and non-resizable while collapsed if (![self hasCollapsibleSubview]) return; NSMutableDictionary *preferredProportions = [[self resizableSubviewPreferredProportion] mutableCopy]; NSMutableDictionary *preferredSizes = [[self nonresizableSubviewPreferredSize] mutableCopy]; NSNumber *key = [NSNumber numberWithInt:[self collapsibleSubviewIndex]]; NSView *subview = [self collapsibleSubview]; // If the collapsible subview is collapsed, we put aside its preferred propotion/size if ([self subviewIsCollapsed:subview]) { BOOL resizable = [self subviewIsResizable:subview]; if (!resizable) { NSNumber *sizeNum = [preferredSizes objectForKey:key]; if (sizeNum) { if (RESIZE_DEBUG_LOGS) NSLog(@"removing collapsible view from preferred sizes"); // TODO: Save the size for later // Remove from preferred sizes [preferredSizes removeObjectForKey:key]; } } else { NSNumber *proportionNum = [preferredProportions objectForKey:key]; if (proportionNum) { if (RESIZE_DEBUG_LOGS) NSLog(@"removing collapsible view from preferred proportions"); CGFloat proportion = [proportionNum floatValue]; // TODO: Save the proportion for later // Remove from preferred proportions [preferredProportions removeObjectForKey:key]; // Recalculate other proportions CGFloat proportionTotal = 1.0 - proportion; if (proportionTotal > 0) { for (NSNumber *pkey in [preferredProportions allKeys]) { CGFloat oldProportion = [[preferredProportions objectForKey:pkey] floatValue]; CGFloat newPropotion = oldProportion / proportionTotal; [preferredProportions setObject:[NSNumber numberWithFloat:newPropotion] forKey:pkey]; } } } } [self setResizableSubviewPreferredProportion:preferredProportions]; [self setNonresizableSubviewPreferredSize:preferredSizes]; } else // Otherwise, we reintegrate its preferred proportion/size { [self clearPreferredProportionsAndSizes]; [self recalculatePreferredProportionsAndSizes]; } [preferredProportions release]; [preferredSizes release]; } - (void)validateAndCalculatePreferredProportionsAndSizes; { if (![self validatePreferredProportionsAndSizes]) [self recalculatePreferredProportionsAndSizes]; // Need to make sure the collapsed subviews preferred size/proportion is in the right place [self correctCollapsiblePreferredProportionOrSize]; } - (void)clearPreferredProportionsAndSizes; { if (RESIZE_DEBUG_LOGS) NSLog(@"clearing preferred proportions and sizes"); [self setResizableSubviewPreferredProportion:nil]; [self setNonresizableSubviewPreferredSize:nil]; } // RESIZING ALGORITHM // non-resizable subviews are given preferred size // overall remaining size is calculated // resizable subviews are calculated based on remaining size and preferred proportions // resizable subviews are checked for min/max constraint violations // if violating constraint, set to valid size and remove from resizable subviews // recalculate other resizable subviews and repeat // if all resizable subviews reached constraints without meeting target size, need to resize non-resizable views // non-resizable subviews are adjusted proportionally to meet target size // non-resizable subviews are checked for min/max constraint violations // if violating constraint, set to valid size and remove from non-resizable subviews // recalculate other non-resizable subviews and repeat // if all subviews reached constraints without meeting target size, need to adjust all views to fit // proportionally resize all subviews to fit in target size, ignoring min/max constraints - (void)resizeAndAdjustSubviews; { // Temporary: for now, we will just remember the proportions the first time subviews are resized // we should be remember them in the user defaults so they save across quits (TODO) [self validateAndCalculatePreferredProportionsAndSizes]; if (RESIZE_DEBUG_LOGS) NSLog(@"resizeSubviews begins -----------------------------------------------------"); NSMutableDictionary *newSubviewSizes = [NSMutableDictionary dictionaryWithCapacity:[[self subviews] count]]; // Get new total size CGFloat totalAvailableSize = [self isVertical] ? [self frame].size.width : [self frame].size.height; if (RESIZE_DEBUG_LOGS) NSLog(@"totalAvailableSize: %f", totalAvailableSize); // Calculate non-resizable subviews total CGFloat nonresizableSubviewsTotalPreferredSize = 0; for (NSNumber *size in [nonresizableSubviewPreferredSize allValues]) nonresizableSubviewsTotalPreferredSize += [size floatValue]; if (RESIZE_DEBUG_LOGS) NSLog(@"nonresizableSubviewsTotalPreferredSize: %f", nonresizableSubviewsTotalPreferredSize); // Calculate divider thickness total int dividerCount = [[self subviews] count] - 1; if ([self collapsibleSubviewIsCollapsed] && dividerCanCollapse) dividerCount--; CGFloat dividerThicknessTotal = [self dividerThickness] * dividerCount; if (RESIZE_DEBUG_LOGS) NSLog(@"dividerThicknessTotal: %f", dividerThicknessTotal); // Calculate overall remaining size (could be negative) CGFloat resizableSubviewsTotalAvailableSize = totalAvailableSize - nonresizableSubviewsTotalPreferredSize - dividerThicknessTotal; if (RESIZE_DEBUG_LOGS) NSLog(@"resizableSubviewsTotalAvailableSize: %f", resizableSubviewsTotalAvailableSize); // Special case for the collapsible subview if ([self collapsibleSubviewIsCollapsed]) { [newSubviewSizes setObject:[NSNumber numberWithFloat:0.0] forKey:[NSNumber numberWithInt:[self collapsibleSubviewIndex]]]; } // Set non-resizable subviews to preferred size [newSubviewSizes addEntriesFromDictionary:nonresizableSubviewPreferredSize]; // Set sizes of resizable views based on proportions (could be negative) CGFloat resizableSubviewAvailableSizeUsed = 0; int resizableSubviewCounter = 0; int resizableSubviewCount = [resizableSubviewPreferredProportion count]; for (NSNumber *key in [resizableSubviewPreferredProportion allKeys]) { resizableSubviewCounter++; CGFloat proportion = [[resizableSubviewPreferredProportion objectForKey:key] floatValue]; CGFloat size = roundf(proportion * resizableSubviewsTotalAvailableSize); resizableSubviewAvailableSizeUsed += size; if (resizableSubviewCounter == resizableSubviewCount) { // Make adjustment if necessary size += (resizableSubviewsTotalAvailableSize - resizableSubviewAvailableSizeUsed); } [newSubviewSizes setObject:[NSNumber numberWithFloat:size] forKey:key]; } if (RESIZE_DEBUG_LOGS) NSLog(@"newSubviewSizes after resizable proportional resizing: %@", newSubviewSizes); // TODO: Could add a special case for resizableSubviewsTotalAvailableSize <= 0 : just set all resizable subviews to minimum size // Make array of all the resizable subviews indexes NSMutableArray *resizableSubviewIndexes = [[resizableSubviewPreferredProportion allKeys] mutableCopy]; [resizableSubviewIndexes sortUsingDescriptors:[NSArray arrayWithObject:[[[NSSortDescriptor alloc] initWithKey:@"self" ascending:YES] autorelease]]]; // Loop until none of the resizable subviews' constraints are violated CGFloat proportionTotal = 1; CGFloat resizableSubviewsRemainingAvailableSize = resizableSubviewsTotalAvailableSize; int i; for (i = 0; i < [resizableSubviewIndexes count]; i++) { NSNumber *key = [resizableSubviewIndexes objectAtIndex:i]; CGFloat size = [[newSubviewSizes objectForKey:key] floatValue]; CGFloat minSize = [self subviewMinimumSize:[key intValue]]; CGFloat maxSize = [self subviewMaximumSize:[key intValue]]; BOOL overMax = size > maxSize; BOOL underMin = size < minSize; // Check if current item in array violates constraints if (underMin || overMax) { CGFloat constrainedSize = underMin ? minSize : maxSize; if (RESIZE_DEBUG_LOGS) NSLog(@"resizable subview %@ was %@, set to %f", key, (underMin ? @"under min" : @"over max"), constrainedSize); // Give subview constrained size and remove from array [newSubviewSizes setObject:[NSNumber numberWithFloat:constrainedSize] forKey:key]; [resizableSubviewIndexes removeObject:key]; // Adjust total proportion and remaining available size proportionTotal -= [[resizableSubviewPreferredProportion objectForKey:key] floatValue]; resizableSubviewsRemainingAvailableSize -= underMin ? minSize : maxSize; // Recalculate remaining subview sizes CGFloat resizableSubviewRemainingSizeUsed = 0; int j; for (j = 0; j < [resizableSubviewIndexes count]; j++) { NSNumber *jKey = [resizableSubviewIndexes objectAtIndex:j]; CGFloat proportion = 0; if (proportionTotal > 0) proportion = [[resizableSubviewPreferredProportion objectForKey:jKey] floatValue] / proportionTotal; else proportion = 1.0 / [resizableSubviewIndexes count]; CGFloat size = roundf(proportion * resizableSubviewsRemainingAvailableSize); resizableSubviewRemainingSizeUsed += size; if (j == [resizableSubviewIndexes count] - 1) { // Make adjustment if necessary size += (resizableSubviewsRemainingAvailableSize - resizableSubviewRemainingSizeUsed); } [newSubviewSizes setObject:[NSNumber numberWithFloat:size] forKey:jKey]; // Reset outer loop to start from beginning i = -1; } } } if (RESIZE_DEBUG_LOGS) NSLog(@"newSubviewSizes after resizable constraint fulfilling: %@", newSubviewSizes); if ([resizableSubviewIndexes count] == 0 && resizableSubviewsRemainingAvailableSize != 0) { if (RESIZE_DEBUG_LOGS) NSLog(@"entering nonresizable adjustment stage"); // All resizable subviews have reached constraints without reaching the target size // First try to adjust non-resizable subviews, with resizableSubviewsRemainingAvailableSize being the amount of adjustment needed // Make array of non-resizable preferred proportions (normally go by preferred sizes) NSMutableDictionary *nonresizableSubviewPreferredProportion = [NSMutableDictionary dictionary]; for (NSNumber *key in [nonresizableSubviewPreferredSize allKeys]) { CGFloat proportion = [[nonresizableSubviewPreferredSize objectForKey:key] floatValue] / nonresizableSubviewsTotalPreferredSize; [nonresizableSubviewPreferredProportion setObject:[NSNumber numberWithFloat:proportion] forKey:key]; } // ResizableSubviewsRemainingAvailableSize is the amount of adjustment needed CGFloat nonresizableSubviewsRemainingAvailableSize = nonresizableSubviewsTotalPreferredSize + resizableSubviewsRemainingAvailableSize; // Set sizes of nonresizable views based on proportions (could be negative) CGFloat nonresizableSubviewAvailableSizeUsed = 0; int nonresizableSubviewCounter = 0; int nonresizableSubviewCount = [nonresizableSubviewPreferredProportion count]; for (NSNumber *key in [nonresizableSubviewPreferredProportion allKeys]) { nonresizableSubviewCounter++; CGFloat proportion = [[nonresizableSubviewPreferredProportion objectForKey:key] floatValue]; CGFloat size = roundf(proportion * nonresizableSubviewsRemainingAvailableSize); nonresizableSubviewAvailableSizeUsed += size; if (nonresizableSubviewCounter == nonresizableSubviewCount) { // Make adjustment if necessary size += (nonresizableSubviewsRemainingAvailableSize - nonresizableSubviewAvailableSizeUsed); } [newSubviewSizes setObject:[NSNumber numberWithFloat:size] forKey:key]; } if (RESIZE_DEBUG_LOGS) NSLog(@"newSubviewSizes after nonresizable proportional resizing: %@", newSubviewSizes); // Make array of all the non-resizable subviews indexes NSMutableArray *nonresizableSubviewIndexes = [[nonresizableSubviewPreferredSize allKeys] mutableCopy]; [nonresizableSubviewIndexes sortUsingDescriptors:[NSArray arrayWithObject:[[[NSSortDescriptor alloc] initWithKey:@"self" ascending:YES] autorelease]]]; // Loop until none of the non-resizable subviews' constraints are violated CGFloat proportionTotal = 1; int i; for (i = 0; i < [nonresizableSubviewIndexes count]; i++) { NSNumber *key = [nonresizableSubviewIndexes objectAtIndex:i]; CGFloat size = [[newSubviewSizes objectForKey:key] floatValue]; CGFloat minSize = [self subviewMinimumSize:[key intValue]]; CGFloat maxSize = [self subviewMaximumSize:[key intValue]]; BOOL overMax = size > maxSize; BOOL underMin = size < minSize; // Check if current item in array violates constraints if (underMin || overMax) { CGFloat constrainedSize = underMin ? minSize : maxSize; if (RESIZE_DEBUG_LOGS) NSLog(@"nonresizable subview %@ was %@, set to %f", key, (underMin ? @"under min" : @"over max"), constrainedSize); // Give subview constrained size and remove from array [newSubviewSizes setObject:[NSNumber numberWithFloat:constrainedSize] forKey:key]; [nonresizableSubviewIndexes removeObject:key]; // Adjust total proportion and remaining available size proportionTotal -= [[nonresizableSubviewPreferredProportion objectForKey:key] floatValue]; nonresizableSubviewsRemainingAvailableSize -= underMin ? minSize : maxSize; // Recalculate remaining subview sizes CGFloat nonresizableSubviewRemainingSizeUsed = 0; int j; for (j = 0; j < [nonresizableSubviewIndexes count]; j++) { NSNumber *jKey = [nonresizableSubviewIndexes objectAtIndex:j]; CGFloat proportion = 0; if (proportionTotal > 0) proportion = [[nonresizableSubviewPreferredProportion objectForKey:jKey] floatValue] / proportionTotal; else proportion = 1.0 / [nonresizableSubviewIndexes count]; CGFloat size = roundf(proportion * nonresizableSubviewsRemainingAvailableSize); nonresizableSubviewRemainingSizeUsed += size; if (j == [nonresizableSubviewIndexes count] - 1) { // Make adjustment if necessary size += (nonresizableSubviewsRemainingAvailableSize - nonresizableSubviewRemainingSizeUsed); } [newSubviewSizes setObject:[NSNumber numberWithFloat:size] forKey:jKey]; // Reset outer loop to start from beginning i = -1; } } } if (RESIZE_DEBUG_LOGS) NSLog(@"newSubviewSizes after nonresizable constraint fulfilling: %@", newSubviewSizes); // If there is still overall violation, resize everything proportionally to make up the difference if ([resizableSubviewIndexes count] == 0 && nonresizableSubviewsRemainingAvailableSize != 0) { if (RESIZE_DEBUG_LOGS) NSLog(@"entering all subviews forced adjustment stage"); // Calculate current proportions and use to calculate new size CGFloat allSubviewTotalCurrentSize = 0; for (NSNumber *size in [newSubviewSizes allValues]) allSubviewTotalCurrentSize += [size floatValue]; CGFloat allSubviewRemainingSizeUsed = 0; CGFloat allSubviewTotalSize = totalAvailableSize - dividerThicknessTotal; // TODO: What to do if even the dividers don't fit? int k; for (k = 0; k < [newSubviewSizes count]; k++) { NSNumber *key = [NSNumber numberWithInt:k]; CGFloat currentSize = [[newSubviewSizes objectForKey:key] floatValue]; CGFloat proportion = currentSize / allSubviewTotalCurrentSize; CGFloat size = roundf(proportion * allSubviewTotalSize); allSubviewRemainingSizeUsed += size; if (k == [newSubviewSizes count] - 1) { // Make adjustment if necessary size += allSubviewTotalSize - allSubviewRemainingSizeUsed; } [newSubviewSizes setObject:[NSNumber numberWithFloat:size] forKey:key]; } if (RESIZE_DEBUG_LOGS) NSLog(@"newSubviewSizes after all subviews forced adjustment: %@", newSubviewSizes); } // Otherwise there is still flexibiliy in the non-resizable views, so we are done } // Otherwise there is still flexibility in the resizable views, so we are done // Set subview frames CGFloat position = 0; for (i = 0; i < [[self subviews] count]; i++) { NSView *subview = [[self subviews] objectAtIndex:i]; CGFloat size = [[newSubviewSizes objectForKey:[NSNumber numberWithInt:i]] floatValue]; NSRect subviewFrame = NSZeroRect; if ([self isVertical]) { subviewFrame.size.height = [self frame].size.height; subviewFrame.size.width = size; subviewFrame.origin.y = [subview frame].origin.y; subviewFrame.origin.x = position; } else { subviewFrame.size.height = size; subviewFrame.size.width = [self frame].size.width; subviewFrame.origin.y = position; subviewFrame.origin.x = [subview frame].origin.x; } [subview setFrame:subviewFrame]; position += size; if (dividerCanCollapse && [self subviewIsCollapsed:subview]) { // Do nothing } else { position += [self dividerThickness]; } } } - (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize { if ([secondaryDelegate isKindOfClass:NSClassFromString(@"BWAnchoredButtonBar")]) { [self resizeAndAdjustSubviews]; } else if ([secondaryDelegate respondsToSelector:@selector(splitView:resizeSubviewsWithOldSize:)]) { [secondaryDelegate splitView:sender resizeSubviewsWithOldSize:oldSize]; } else if (sender == self) { [self resizeAndAdjustSubviews]; } else { [sender adjustSubviews]; } } #pragma mark Force Vertical Splitters to Thin Appearance // This class doesn't have an appearance for wide vertical splitters, so we force all vertical splitters to thin. // We also post notifications that are used by the inspector to show & hide controls. - (void)setDividerStyle:(NSSplitViewDividerStyle)aStyle { BOOL styleChanged = NO; if (aStyle != [self dividerStyle]) styleChanged = YES; if ([self isVertical]) [super setDividerStyle:NSSplitViewDividerStyleThin]; else [super setDividerStyle:aStyle]; // There can be sizing issues during design-time if we don't call this [self adjustSubviews]; if (styleChanged) [[NSNotificationCenter defaultCenter] postNotificationName:@"BWSplitViewDividerThicknessChanged" object:self]; } - (void)setVertical:(BOOL)flag { BOOL orientationChanged = NO; if (flag != [self isVertical]) orientationChanged = YES; if (flag) [super setDividerStyle:NSSplitViewDividerStyleThin]; [super setVertical:flag]; if (orientationChanged) [[NSNotificationCenter defaultCenter] postNotificationName:@"BWSplitViewOrientationChanged" object:self]; } #pragma mark IB Inspector Support Methods - (BOOL)checkboxIsEnabled { if (![self isVertical] && [super dividerThickness] > 1.01) return NO; return YES; } - (void)setColorIsEnabled:(BOOL)flag { colorIsEnabled = flag; [self setNeedsDisplay:YES]; } - (void)setColor:(NSColor *)aColor { if (color != aColor) { [color release]; color = [aColor copy]; } [self setNeedsDisplay:YES]; } - (NSColor *)color { if (color == nil) color = [[NSColor blackColor] retain]; return [[color retain] autorelease]; } - (NSMutableDictionary *)minValues { if (minValues == nil) minValues = [NSMutableDictionary new]; return [[minValues retain] autorelease]; } - (NSMutableDictionary *)maxValues { if (maxValues == nil) maxValues = [NSMutableDictionary new]; return [[maxValues retain] autorelease]; } - (NSMutableDictionary *)minUnits { if (minUnits == nil) minUnits = [NSMutableDictionary new]; return [[minUnits retain] autorelease]; } - (NSMutableDictionary *)maxUnits { if (maxUnits == nil) maxUnits = [NSMutableDictionary new]; return [[maxUnits retain] autorelease]; } - (void)dealloc { [color release]; [minValues release]; [maxValues release]; [minUnits release]; [maxUnits release]; [resizableSubviewPreferredProportion release]; [nonresizableSubviewPreferredSize release]; [toggleCollapseButton release]; [stateForLastPreferredCalculations release]; [super dealloc]; } @end