Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 1133 lines (958 sloc) 39.3 KB
//
// KFSplitView.m
// KFSplitView v. 1.3, 11/27/2004
//
// Copyright (c) 2003-2004 Ken Ferry. Some rights reserved.
// http://homepage.mac.com/kenferry/software.html
//
// Other contributors: Kirk Baker, John Pannell
//
// This work is licensed under a Creative Commons license:
// http://creativecommons.org/licenses/by-nc/1.0/
//
// Send me an email if you have any problems (after you've read what there is to read).
//
// You can reach me at kenferry at the domain mac.com.
//
// On this whole major axis, minor axis thing:
//
// The 'major' axis refers to the direction in which dividers can move.
// It's the y-axis when [self isVertical] returns NO, and the x-axis otherwise.
// Pretty much everything that uses coordinates or dimensions in this file works
// more comfortably in that coordinate system.
//
// Other
//
// This class is a basically a complete reimplementation of NSSplitView. The
// underlying NSSplitView is mostly used for drawing dividers.
#import "KFSplitView.h"
#pragma mark File-level and global vars:
NSString *KFSplitViewDidCollapseSubviewNotification = @"KFSplitViewDidCollapseSubviewNotification";
NSString *KFSplitViewDidExpandSubviewNotification = @"KFSplitViewDidExpandSubviewNotification";
const NSPoint KFOffScreenPoint = {1000000.0,1000000.0};
static NSMutableSet *kfInUsePositionNames;
#pragma mark Utility:
// these are macros (instead of inlines) so that we can use the instance variable kfIsVertical
// they're undef'd at the bottom of the file
#define KFMAJORCOORDOFPOINT(point) (kfIsVertical ? (point).x : (point).y)
#define KFMINORCOORDOFPOINT(point) (kfIsVertical ? (point).y : (point).x)
#define KFMAJORDIMOFSIZE(size) (kfIsVertical ? (size).width : (size).height)
#define KFMINORDIMOFSIZE(size) (kfIsVertical ? (size).height : (size).width)
#define KFPOINTWITHMAJMIN(major, minor) (kfIsVertical ? NSMakePoint((major), (minor)) : NSMakePoint((minor), (major)))
#define KFSIZEWITHMAJMIN(major, minor) (kfIsVertical ? NSMakeSize((major), (minor)) : NSMakeSize((minor), (major)))
#define KFMAX(a,b) ((a)>(b)?(a):(b))
// proportionally scale a list of integers so that the sum of the resulting list is targetTotal
// Will fail (return NO) if all integers are zero
// Favors not completely zeroing out a nonzero int
static BOOL kfScaleUInts(unsigned *integers, int numInts, unsigned targetTotal)
{
unsigned total;
float scalingFactor;
int i, numNonZeroInts;
// compute total
total = 0;
numNonZeroInts = 0;
for (i = 0; i < numInts; i++)
{
if (integers[i] != 0)
{
total += integers[i];
numNonZeroInts++;
}
}
if (numNonZeroInts == 0) // fail
{
return NO;
}
// compute scalingFactor
scalingFactor = (float)targetTotal / total;
// scale all ints and recompute total (which may not equal targetTotal due to roundoff error)
total = 0;
for (i = 0; i < numInts; i++)
{
if (integers[i] != 0)
{
// this is preferable to rounding when used for subviews - helps
// prevent a subview getting stuck at thickness 1 during a drag resize
integers[i] = MAX(floor(scalingFactor*integers[i]), 1);
total += integers[i];
}
}
// Each non-zero integer may be as much as 1 off of its "proper" floating point value due to roundoff,
// so abs(targetTotal - total) might be as much as numNonZero. We randomly choose integers to increment (or decrement)
// to make up the gap, and we choose only from the non-zero values.
int gap = abs(targetTotal - total);
int closeGapIncrement = (targetTotal > total) ? 1 : -1;
int numRemainingNonZeroInts = numNonZeroInts;
for (i = 0; i < numInts && gap > 0; i++)
{
if (integers[i] > 0)
{
BOOL shouldIncrementInt = (gap == numRemainingNonZeroInts) || (rand() < (float) gap / numRemainingNonZeroInts * RAND_MAX);
if (shouldIncrementInt)
{
integers[i] += closeGapIncrement;
gap--;
}
numRemainingNonZeroInts--;
}
}
return YES;
}
@interface KFSplitView (kfPrivate)
+ (NSString *)kfDefaultsKeyForName:(NSString *)name;
- (void)kfSetup;
- (void)kfSetupResizeCursors;
- (int)kfGetDividerAtMajCoord:(float)coord;
- (void)kfPutDivider:(int)offset atMajCoord:(float)coord;
- (void)kfRecalculateDividerRects;
- (void)kfMoveCollapsedSubviewsOffScreen;
- (void)kfSavePositionUsingAutosaveName:(id)sender;
- (void)kfLayoutSubviewsUsingThicknesses:(unsigned *)subviewThicknesses;
@end
@implementation KFSplitView
/*****************
* Initialization
*****************/
#pragma mark Setup/teardown:
+ (void)initialize
{
kfInUsePositionNames = [[NSMutableSet alloc] init];
}
- initWithFrame:(NSRect)frameRect
{
if ((self = [super initWithFrame:frameRect]))
{
[self kfSetup];
}
return self;
}
- initWithCoder:(NSCoder *)coder
{
if ((self = [super initWithCoder:coder]))
{
[self kfSetup];
}
return self;
}
- (void)kfSetup
{
// be sure to setup cursors before calling setVertical:
[self kfSetupResizeCursors];
kfCollapsedSubviews = [[NSMutableSet alloc] init];
kfDividerRects = [[NSMutableArray alloc] init];
kfDefaults = [NSUserDefaults standardUserDefaults];
kfNotificationCenter = [NSNotificationCenter defaultCenter];
[self setVertical:[self isVertical]];
}
// Attempts to find cursors to use as kfIsVerticalResizeCursor and kfNotIsVerticalResizeCursor.
// These cursors are eventually released, so make sure each receives a retain message now.
// If no good cursors can be found, an error is printed and the arrow cursor is used.
- (void)kfSetupResizeCursors
{
NSImage *isVerticalImage, *isNotVerticalImage;
if ((isVerticalImage = [NSImage imageNamed:@"NSTruthHorizontalResizeCursor"])); // standard Jaguar NSSplitView resize cursor
else if ((isVerticalImage = [NSImage imageNamed:@"NSTruthHResizeCursor"]));
if (isVerticalImage)
{
kfIsVerticalResizeCursor = [[NSCursor alloc] initWithImage:isVerticalImage
hotSpot:NSMakePoint(8,8)];
}
if ((isNotVerticalImage = [NSImage imageNamed:@"NSTruthVerticalResizeCursor"])); // standard Jaguar NSSplitView resize cursor
else if ((isNotVerticalImage = [NSImage imageNamed:@"NSTruthVResizeCursor"]));
if (isNotVerticalImage)
{
kfNotIsVerticalResizeCursor = [[NSCursor alloc] initWithImage:isNotVerticalImage
hotSpot:NSMakePoint(8,8)];
}
if (kfIsVerticalResizeCursor == nil)
{
kfIsVerticalResizeCursor = [[NSCursor arrowCursor] retain];
NSLog(@"Warning - no horizontal resizing cursor located. Please report this as a bug.");
}
if (kfNotIsVerticalResizeCursor == nil)
{
kfNotIsVerticalResizeCursor = [[NSCursor arrowCursor] retain];
NSLog(@"Warning - no vertical resizing cursor located. Please report this as a bug.");
}
}
- (void)awakeFromNib
{
[self kfRecalculateDividerRects];
}
- (void)dealloc
{
[self setDelegate:nil];
[self setPositionAutosaveName:@""];
[kfCollapsedSubviews release];
[kfDividerRects release];
[kfIsVerticalResizeCursor release];
[kfNotIsVerticalResizeCursor release];
[super dealloc];
}
/******************
* Main processing
******************/
#pragma mark Main processing:
- (void)mouseDown:(NSEvent *)theEvent
{
// All coordinates are major axis coordinates unless otherwise specified. See the top of the file
// for an explanation of major and minor axes.
float minorDim; // common dimension of all subviews
int divider; // index of a divider being dragged
float mouseCoord, mouseToDividerOffset; // the mouse holds on to whatever part of the divider it grabs onto
float dividerThickness;
float dividerCoord, prevDividerCoord;
float hardMinCoord, hardMaxCoord; // absolute boundaries for dividerCoord
float delMinCoord, delMaxCoord; // boundaries for dividerCoord according to the delegate (not absolute)
NSView *firstSubview, *secondSubview; // subviews above and below the divider (if !isVertical)
float firstSubviewMinCoord, secondSubviewMaxCoord; // top of the first, bottom of the second (if !isVertical)
BOOL firstSubviewCanCollapse, secondSubviewCanCollapse;
NSDate *distantFuture;
float (*splitPosConstraintFunc)(id, SEL, ...); // delegate supplied function to constrain dividerCoord
// setup
minorDim = KFMINORDIMOFSIZE([self frame].size);
dividerThickness = [self dividerThickness];
distantFuture = [NSDate distantFuture];
// PRECOMPUTATION - we do as much as we can before starting the event loop.
// figure out which divider is being dragged
mouseCoord = KFMAJORCOORDOFPOINT([self convertPoint:[theEvent locationInWindow] fromView:nil]);
divider = [self kfGetDividerAtMajCoord:mouseCoord];
if (divider == NSNotFound)
{
return;
}
// if the event is a double click we let the delegate deal with it
if ([theEvent clickCount] > 1)
{
if ([kfDelegate respondsToSelector:@selector(splitView:didDoubleClickInDivider:)])
{
[kfDelegate splitView:self didDoubleClickInDivider:divider];
return;
}
}
// firstSubview is the subview above (left) of the divider
// secondSubview is the subview below (right) of the divider
firstSubview = [[self subviews] objectAtIndex:divider];
secondSubview = [[self subviews] objectAtIndex:divider+1];
// set firstSubviewMinCoord and secondSubviewMaxCoord. Here's a little diagram:
// ------------ <- firstSubviewMinCoord
//
//
//
//
// ------------ <- dividerCoord (not set yet)
// ------------
//
//
// ------------ <- secondSubviewMaxCoord
if (![self isSubviewCollapsed:firstSubview])
{
firstSubviewMinCoord = KFMAJORCOORDOFPOINT([firstSubview frame].origin);
}
else
{
firstSubviewMinCoord = KFMAJORCOORDOFPOINT([[kfDividerRects objectAtIndex:divider] rectValue].origin);
}
if (![self isSubviewCollapsed:secondSubview])
{
secondSubviewMaxCoord = KFMAJORCOORDOFPOINT([secondSubview frame].origin) + KFMAJORDIMOFSIZE([secondSubview frame].size);
}
else
{
secondSubviewMaxCoord = KFMAJORCOORDOFPOINT([[kfDividerRects objectAtIndex:divider] rectValue].origin) + dividerThickness;
}
// hardMinCoord and hardMaxCoord are the absolute minimum and maximum values that may be
// assigned to dividerCoord. delMinCoord and delMaxCoord are minimum and maximum values
// for dividerCoord that are supplied by the delegate. These last are _not_ absolute: if the
// delegate allows collapsing of subviews then dividerCoord can snap from delMinCoord to
// hardMinCoord if the user drags the divider more than halfway across the region between them.
// See Apple's NSSplitView documenation under - splitView:canCollapseSubview:.
hardMinCoord = firstSubviewMinCoord;
hardMaxCoord = secondSubviewMaxCoord - dividerThickness;
delMinCoord = hardMinCoord;
delMaxCoord = hardMaxCoord;
if ([kfDelegate respondsToSelector:@selector(splitView:constrainMinCoordinate:ofSubviewAt:)])
{
delMinCoord = [kfDelegate splitView:self
constrainMinCoordinate:delMinCoord
ofSubviewAt:divider];
}
if ([kfDelegate respondsToSelector:@selector(splitView:constrainMaxCoordinate:ofSubviewAt:)])
{
delMaxCoord = [kfDelegate splitView:self
constrainMaxCoordinate:delMaxCoord
ofSubviewAt:divider];
}
delMinCoord = (delMinCoord < hardMinCoord) ? hardMinCoord : delMinCoord;
delMaxCoord = (delMaxCoord > hardMaxCoord) ? hardMaxCoord : delMaxCoord;
if (delMinCoord > delMaxCoord)
{
// this follows apple's implementation. It says that if the delegate does
// not supply any zone where the divider can sit without collapsing a subview then
// ignore the delegate. The other option would be to always collapse to one subview
// or the other, if one or both of the subviews are collasible. That could be a bit of a UI
// problem, because the user could try to drag a subview and have nothing happen.
delMinCoord = hardMinCoord;
delMaxCoord = hardMaxCoord;
}
firstSubviewCanCollapse = NO;
secondSubviewCanCollapse = NO;
if ([kfDelegate respondsToSelector:@selector(splitView:canCollapseSubview:)])
{
firstSubviewCanCollapse = [kfDelegate splitView:self canCollapseSubview:firstSubview];
secondSubviewCanCollapse = [kfDelegate splitView:self canCollapseSubview:secondSubview];
}
// The delegate may constrain the possible values for dividerCoord.
// Since this method will be called repeatedly we cache a pointer to it.
splitPosConstraintFunc = NULL;
if ([kfDelegate respondsToSelector:@selector(splitView:constrainSplitPosition:ofSubviewAt:)])
{
splitPosConstraintFunc = (float (*)(id, SEL, ...))[kfDelegate methodForSelector:@selector(splitView:constrainSplitPosition:ofSubviewAt:)];
}
// When the user grabs and drags the divider he holds onto that
// particular spot while dragging.
// mouseToDividerOffset is the difference between dividerCoord (the top of
// the divider) and mouseCoord.
mouseToDividerOffset = KFMAJORCOORDOFPOINT([[kfDividerRects objectAtIndex:divider] rectValue].origin) - mouseCoord;
// EVENT-LOOP
prevDividerCoord = 1000000; // something non-sensical
do
{
mouseCoord = KFMAJORCOORDOFPOINT([self convertPoint:[theEvent locationInWindow] fromView:nil]);
dividerCoord = mouseCoord + mouseToDividerOffset;
if (splitPosConstraintFunc != NULL)
{
dividerCoord = (*splitPosConstraintFunc)(kfDelegate,
@selector(splitView:constrainSplitPosition:ofSubviewAt:),
self,
dividerCoord,
divider);
}
// There are five regions where user may have dragged the divider:
// collapse first subview
// stick the divider to delMinCoord
// move freely
// stick the divider to delMaxCoord
// collapse the second subview
if ( hardMinCoord == hardMaxCoord )
{
// special case: divider is pinned. It is possible to collapse both subviews.
[self setSubview:firstSubview isCollapsed:firstSubviewCanCollapse];
[self setSubview:secondSubview isCollapsed:secondSubviewCanCollapse];
dividerCoord = hardMinCoord;
}
else if ( firstSubviewCanCollapse &&
dividerCoord < hardMinCoord + (delMinCoord - hardMinCoord)/2)
{
// collapse first subview
[self setSubview:secondSubview isCollapsed:NO];
[self setSubview:firstSubview isCollapsed:YES];
dividerCoord = hardMinCoord;
}
else if ( dividerCoord < delMinCoord )
{
// stick to delMinCoord
[self setSubview:firstSubview isCollapsed:NO];
[self setSubview:secondSubview isCollapsed:NO];
dividerCoord = delMinCoord;
}
else if ( dividerCoord < delMaxCoord )
{
// move freely
[self setSubview:firstSubview isCollapsed:NO];
[self setSubview:secondSubview isCollapsed:NO];
}
else if ( !secondSubviewCanCollapse ||
dividerCoord < hardMaxCoord - (hardMaxCoord - delMaxCoord)/2 )
{
// stick to delMaxCoord
[self setSubview:firstSubview isCollapsed:NO];
[self setSubview:secondSubview isCollapsed:NO];
dividerCoord = delMaxCoord;
}
else
{
// collapse second subview
[self setSubview:firstSubview isCollapsed:NO];
[self setSubview:secondSubview isCollapsed:YES];
dividerCoord = hardMaxCoord;
}
if (prevDividerCoord != dividerCoord)
{
// Position and resize elements. A collapsing subview's frame size doesn't change,
// the subview just gets moved way offscreen (as in NSSplitView).
// The diagram may help:
//
// ------------ <- firstSubviewMinCoord
//
//
//
// ------------ <- dividerCoord
// ------------ <- dividerCoord + dividerThickness
//
//
// ------------ <- secondSubviewMaxCoord
[kfNotificationCenter postNotificationName:NSSplitViewWillResizeSubviewsNotification object:self];
// divider
[self kfPutDivider:divider atMajCoord:dividerCoord];
// firstSubview
if (![self isSubviewCollapsed:firstSubview])
{
NSRect newFrame;
newFrame.origin = KFPOINTWITHMAJMIN(firstSubviewMinCoord,0);
newFrame.size = KFSIZEWITHMAJMIN(dividerCoord - firstSubviewMinCoord, minorDim);
if (!NSEqualRects([firstSubview frame],newFrame))
{
[firstSubview setFrame:newFrame];
[firstSubview setNeedsDisplay:YES];
}
}
else
{
[firstSubview setFrameOrigin:KFOffScreenPoint];
}
// secondSubview
if (![self isSubviewCollapsed:secondSubview])
{
NSRect newFrame;
newFrame.origin = KFPOINTWITHMAJMIN(dividerCoord + dividerThickness, 0);
newFrame.size = KFSIZEWITHMAJMIN(secondSubviewMaxCoord - (dividerCoord + dividerThickness), minorDim);
if (!NSEqualRects([secondSubview frame],newFrame))
{
[secondSubview setFrame:newFrame];
[secondSubview setNeedsDisplay:YES];
}
}
else
{
[secondSubview setFrameOrigin:KFOffScreenPoint];
}
[kfNotificationCenter postNotificationName:NSSplitViewDidResizeSubviewsNotification object:self];
prevDividerCoord = dividerCoord;
}
// get the next relevant event
theEvent = [NSApp nextEventMatchingMask:NSLeftMouseDraggedMask|NSLeftMouseUpMask
untilDate:distantFuture
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
} while ([theEvent type] == NSLeftMouseDragged);
// inform delegate that user has finished dragging divider
if ([kfDelegate respondsToSelector:@selector(splitView:didFinishDragInDivider:)])
{
[kfDelegate splitView:self didFinishDragInDivider:divider];
}
}
// Call this method to retile the subviews, not adjustSubviews.
// It 1) dispatches will and did resize subviews notifications
// 2) calls the appropriate method to do the retiling. That's a method of
// the delegate if it has one and the default adjustSubviews otherwise.
// 3) cleans up some other layout, like divider positions
- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize
{
[kfNotificationCenter postNotificationName:NSSplitViewWillResizeSubviewsNotification object:self];
if ([kfDelegate respondsToSelector:@selector(splitView:resizeSubviewsWithOldSize:)])
{
[kfDelegate splitView:self resizeSubviewsWithOldSize:oldBoundsSize];
}
else
{
[self adjustSubviews];
}
[self kfRecalculateDividerRects];
[self kfMoveCollapsedSubviewsOffScreen];
[kfNotificationCenter postNotificationName:NSSplitViewDidResizeSubviewsNotification object:self];
}
// See Apple's NSSplitView docs. However, note that in general you want to call
// resizeSubviewsWithOldSize:, not this method. The exception is that you might
// want to call adjustSubviews from splitView:resizeSubviewsWithOldSize: in the
// the delegate
- (void)adjustSubviews
{
int i, numSubviews;
NSArray *subviews;
// The 'thickness' of a subview will mean the amount of space along
// the major axis that the subview occupies in the splitview.
// We work in integral values, though actual thicknesses are floats.
// In the current OS, the floats actually have integral values.
//
// Ex 1: The thickness of a collapsed subview is 0.
// Ex 2: For an uncollapsed subview in a horizontal (standard direction) splitview,
// thickness means height.
unsigned *subviewThicknesses;
// setup
subviews = [self subviews];
numSubviews = [subviews count];
if (numSubviews == 0)
{
return;
}
subviewThicknesses = malloc(sizeof(unsigned)*numSubviews);
// Fill out subviewThicknesses array.
// Also keep track of the total thickness of all subviews, and
// of the first expanded subview
unsigned totalSubviewThicknesses = 0;
int firstExpandedSubviewIndex = NSNotFound;
for (i = 0; i < numSubviews; i++)
{
NSView *subview = [subviews objectAtIndex:i];
if (![self isSubviewCollapsed:subview])
{
subviewThicknesses[i] = floor(KFMAJORDIMOFSIZE([subview frame].size));
totalSubviewThicknesses += subviewThicknesses[i];
if (firstExpandedSubviewIndex == NSNotFound) { firstExpandedSubviewIndex = i; }
}
else
{
subviewThicknesses[i] = 0;
}
}
// Compute new thicknesses for subviews.
// In the end, the subview thicknesses should sum to the thickness of the splitview minus the space occupied by dividers.
unsigned targetTotalSubviewsThickness = KFMAX(floor(KFMAJORDIMOFSIZE([self frame].size) - [self dividerThickness]*(numSubviews - 1)), 0);
// If at least one of the subviews has positive thickness
if (totalSubviewThicknesses != 0)
{
// then we can scale all the thicknesses
kfScaleUInts(subviewThicknesses, numSubviews, targetTotalSubviewsThickness);
}
else // otherwise we'll have to expand one of the subviews to fill the entire space
{
if (firstExpandedSubviewIndex != NSNotFound)
{
subviewThicknesses[firstExpandedSubviewIndex] = targetTotalSubviewsThickness;
}
else
{
subviewThicknesses[0] = targetTotalSubviewsThickness;
}
}
// layout subviews
[self kfLayoutSubviewsUsingThicknesses:subviewThicknesses];
// cleanup
free(subviewThicknesses);
}
// Required: Sum of all subviewThicknesses <= splitViewThickness - dividersThickness.
// If the splitview has positive available space for subviews, then one of the supplied subview thicknesses
// must also be positive. Extra space will be dumped into the last subview with positive thickness.
// See adjustSubviews for the definition of 'thickness'.
//
// Does not currently put collapsed subviews off screen or do divider placement.
// Could be done efficiently here, but would duplicate functionality of other methods.
- (void)kfLayoutSubviewsUsingThicknesses:(unsigned *)subviewThicknesses
{
int i, lastPositiveThicknessSubviewIndex, numSubviews;
float minorDimOfSplitViewSize;
float curMajAxisPos, dividerThickness;
NSArray *subviews;
// setup
subviews = [self subviews];
numSubviews = [subviews count];
minorDimOfSplitViewSize = KFMINORDIMOFSIZE([self frame].size);
dividerThickness = [self dividerThickness];
// Compute lastPositiveThicknessSubviewIndex.
lastPositiveThicknessSubviewIndex = NSNotFound;
for (i = numSubviews - 1; i >= 0; i--)
{
if (subviewThicknesses[i] != 0)
{
lastPositiveThicknessSubviewIndex = i;
break;
}
}
// We walk down the major axis, setting subview frames as we go.
curMajAxisPos = 0;
for (i = 0; i < numSubviews; i++)
{
NSView *subview = [subviews objectAtIndex:i];
float newSubviewThickness = -1; // sentinel value, meaning "do not change"
if (subviewThicknesses[i] == 0) // If subview should have no thickness
{
// then shrink its frame if it is uncollapsed.
if (![self isSubviewCollapsed:subview])
{
newSubviewThickness = 0;
}
}
else // If supplied thickness is positive
{
// make sure the subview isn't collapsed.
if ([self isSubviewCollapsed:subview])
{
[self setSubview:subview isCollapsed:NO];
}
// If this is the last subview that we're going to give a positive thickness
if (i == lastPositiveThicknessSubviewIndex)
{
// we overrule the given the given value and just fill all available area.
float remainingDividersThickness = (numSubviews - 1 - i)*dividerThickness;
float splitViewThickness = KFMAJORDIMOFSIZE([self frame].size);
newSubviewThickness = KFMAX(splitViewThickness - curMajAxisPos - remainingDividersThickness, 0);
}
else // If this isn't the last subview that we're going to set to a positive thickness
{
// use the supplied thickness.
newSubviewThickness = subviewThicknesses[i];
}
}
// If we found a new subview thickness
if (newSubviewThickness != -1)
{
// set the subview's frame accordingly
NSPoint newSubviewOrigin = KFPOINTWITHMAJMIN(curMajAxisPos, 0);
NSSize newSubviewSize = KFSIZEWITHMAJMIN(newSubviewThickness, minorDimOfSplitViewSize);
NSRect newFrame = NSMakeRect(newSubviewOrigin.x, newSubviewOrigin.y,
newSubviewSize.width, newSubviewSize.height);
if (!NSEqualRects([subview frame],newFrame))
{
[subview setFrame:newFrame];
[subview setNeedsDisplay:YES];
}
// and advance down the major axis.
curMajAxisPos += newSubviewThickness;
}
// Account for divider thickness.
if (i < numSubviews - 1)
{
curMajAxisPos += dividerThickness;
}
}
}
- (void)kfMoveCollapsedSubviewsOffScreen
{
NSEnumerator *collapsedSubviewEnumerator = [kfCollapsedSubviews objectEnumerator];
NSView *subview;
while ((subview = [collapsedSubviewEnumerator nextObject]))
{
[subview setFrameOrigin:KFOffScreenPoint];
}
}
- (void)drawRect:(NSRect)rect
{
int i, numDividers;
numDividers = [kfDividerRects count];
for (i = 0; i < numDividers; i++)
{
[self drawDividerInRect:[[kfDividerRects objectAtIndex:i] rectValue]];
}
}
// returns the index ('offset' in Apple's docs) of the divider under the
// given coordinate, or NSNotFound if there isn't a divider there.
- (int)kfGetDividerAtMajCoord:(float)coord
{
int i, numDividers, result;
float curDividerMinimumMajorCoord, dividerThickness;
numDividers = [kfDividerRects count];
result = NSNotFound;
dividerThickness = [self dividerThickness];
for (i = 0; i < numDividers; i++)
{
curDividerMinimumMajorCoord = KFMAJORCOORDOFPOINT([[kfDividerRects objectAtIndex:i] rectValue].origin);
if (curDividerMinimumMajorCoord <= coord && coord < curDividerMinimumMajorCoord + dividerThickness)
{
result = i;
break;
}
}
return result;
}
- (void)kfPutDivider:(int)offset atMajCoord:(float)coord
{
NSPoint newOrigin;
NSSize newSize;
NSRect newFrame;
while ([kfDividerRects count] <= offset)
{
[kfDividerRects addObject:[NSValue valueWithRect:NSZeroRect]];
}
newOrigin = KFPOINTWITHMAJMIN(coord,0);
newSize = KFSIZEWITHMAJMIN([self dividerThickness], KFMINORDIMOFSIZE([self frame].size));
newFrame = NSMakeRect(newOrigin.x, newOrigin.y, newSize.width, newSize.height);
if (!NSEqualRects([[kfDividerRects objectAtIndex:offset] rectValue], newFrame))
{
[kfDividerRects replaceObjectAtIndex:offset withObject:[NSValue valueWithRect:newFrame]];
[self setNeedsDisplayInRect:newFrame];
[[[self subviews] objectAtIndex:offset] setNeedsDisplay:YES];
[[[self subviews] objectAtIndex:offset+1] setNeedsDisplay:YES];
}
}
// positions all dividers based on the current location of the subviews
- (void)kfRecalculateDividerRects
{
float curMajAxisPos, dividerThickness;
id subview, subviews;
int numSubviews, i;
dividerThickness = [self dividerThickness];
subviews = [self subviews];
numSubviews = [subviews count];
curMajAxisPos = 0;
for (i = 0; i < numSubviews - 1; i++)
{
subview = [subviews objectAtIndex:i];
if (![self isSubviewCollapsed:subview])
{
curMajAxisPos += KFMAJORDIMOFSIZE([subview frame].size);
}
[self kfPutDivider:i atMajCoord:curMajAxisPos];
curMajAxisPos += dividerThickness;
}
int numDividerRects = [kfDividerRects count];
if (numDividerRects > numSubviews - 1)
{
[kfDividerRects removeObjectsInRange:NSMakeRange(numSubviews-1,numDividerRects - numSubviews + 1)];
}
[[self window] invalidateCursorRectsForView:self];
}
- (void)resetCursorRects
{
int i, numDividers;
numDividers = [kfDividerRects count];
for (i = 0; i < numDividers; i++)
{
[self addCursorRect:[[kfDividerRects objectAtIndex:i] rectValue]
cursor:kfCurrentResizeCursor];
}
}
/******************
* Accessors
******************/
- (void)setVertical:(BOOL)flag
{
[super setVertical:flag];
kfIsVertical = flag;
if (kfIsVertical)
{
kfCurrentResizeCursor = kfIsVerticalResizeCursor;
}
else
{
kfCurrentResizeCursor = kfNotIsVerticalResizeCursor;
}
}
- (id)delegate
{
return kfDelegate;
}
// automatically registers the delegate for relevant notifications, and unregisters
// the old delegate for those same notifications.
- (void)setDelegate:(id)delegate
{
id delegateAutoRegNotifications, delegateMethodNames;
int i, numAutoRegNotifications;
SEL methodSelector;
delegateAutoRegNotifications = [NSArray arrayWithObjects:
NSSplitViewWillResizeSubviewsNotification,
NSSplitViewDidResizeSubviewsNotification,
KFSplitViewDidCollapseSubviewNotification,
KFSplitViewDidExpandSubviewNotification, nil];
delegateMethodNames = [NSArray arrayWithObjects:
@"splitViewWillResizeSubviews:",
@"splitViewDidResizeSubviews:",
@"splitViewDidCollapseSubview:",
@"splitViewDidExpandSubview:", nil];
numAutoRegNotifications = [delegateAutoRegNotifications count];
if (kfDelegate)
{
for (i = 0; i < numAutoRegNotifications; i++)
{
[kfNotificationCenter removeObserver:kfDelegate
name:[delegateAutoRegNotifications objectAtIndex:i]
object:self];
}
}
kfDelegate = delegate;
if (kfDelegate)
{
for (i = 0; i < numAutoRegNotifications; i++)
{
methodSelector = sel_registerName([[delegateMethodNames objectAtIndex:i] cString]);
if ([kfDelegate respondsToSelector:methodSelector])
{
[kfNotificationCenter addObserver:kfDelegate
selector:methodSelector
name:[delegateAutoRegNotifications objectAtIndex:i]
object:self];
}
}
}
}
- (BOOL)isSubviewCollapsed:(NSView *)subview
{
return [kfCollapsedSubviews containsObject:subview];
}
- (void)setSubview:(NSView *)subview isCollapsed:(BOOL)flag
{
NSDictionary *subviewDictionary;
if (flag != [self isSubviewCollapsed:subview])
{
subviewDictionary = [NSDictionary dictionaryWithObject:subview forKey:@"subview"];
if (flag)
{
[kfCollapsedSubviews addObject:subview];
[kfNotificationCenter postNotificationName:KFSplitViewDidCollapseSubviewNotification
object:self
userInfo:subviewDictionary];
}
else
{
[kfCollapsedSubviews removeObject:subview];
[kfNotificationCenter postNotificationName:KFSplitViewDidExpandSubviewNotification
object:self
userInfo:subviewDictionary];
}
}
}
/**********************
* Position saving
**********************/
#pragma mark Position saving:
// FOR DOCUMENTATION OF POSITION SAVING METHODS SEE APPLE'S NSWINDOW DOCS
+ (void)removePositionUsingName:(NSString *)name
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:[[self class] kfDefaultsKeyForName:name]];
}
- (void)savePositionUsingName:(NSString *)name
{
NSString *key = [[self class] kfDefaultsKeyForName:name];
NSString *prop = [self plistObjectWithSavedPosition];
[kfDefaults setObject:prop forKey:key];
}
- (BOOL)setPositionUsingName:(NSString *)name
{
BOOL result;
id object;
object = [kfDefaults objectForKey:[[self class] kfDefaultsKeyForName:name]];
if (object)
{
[self setPositionFromPlistObject:object];
result = YES;
}
else
{
result = NO;
}
return result;
}
- (BOOL)setPositionAutosaveName:(NSString *)name
{
if ([name isEqualToString:@""])
{
name = nil;
}
if ([kfInUsePositionNames containsObject:name])
{
return NO;
}
if (kfPositionAutosaveName)
{
[kfInUsePositionNames removeObject:kfPositionAutosaveName];
[kfPositionAutosaveName autorelease];
}
kfPositionAutosaveName = [name copy];
if (kfPositionAutosaveName)
{
[self setPositionUsingName:kfPositionAutosaveName];
[kfInUsePositionNames addObject:kfPositionAutosaveName];
[kfNotificationCenter addObserver:self
selector:@selector(kfSavePositionUsingAutosaveName:)
name:NSSplitViewDidResizeSubviewsNotification
object:self];
}
else
{
[kfNotificationCenter removeObserver:self
name:NSSplitViewDidResizeSubviewsNotification
object:self];
}
return YES;
}
- (NSString *)positionAutosaveName
{
return kfPositionAutosaveName;
}
static NSString *savedPositionVersionKey = @"version";
static NSString *savedPositionSubviewsKey = @"subviews";
static NSString *savedPositionSubviewFrameKey = @"frame";
static NSString *savedPositionSubviewIsCollapsedKey = @"collapsed";
static NSString *savedPositionIsVerticalKey = @"isVertical";
- (void)setPositionFromPlistObject:(id)plistObject
{
if ([plistObject isKindOfClass:[NSDictionary class]])
{
NSDictionary *positionDict = (NSDictionary *)plistObject;
// check position data format version
if ([[positionDict objectForKey:savedPositionVersionKey] intValue] == 2)
{
NSArray *subviews, *subviewPositionsArray;
int numSubviews, numSavedSubviews, numSettableSubviews, i;
// set subview positions
subviews = [self subviews];
numSubviews = [subviews count];
subviewPositionsArray = [positionDict objectForKey:savedPositionSubviewsKey];
// what if the number of saved subview records and the actual number of subviews don't match?
// we'll set positions until we run out of either subviews or data records
numSavedSubviews = [subviewPositionsArray count];
numSettableSubviews = (numSubviews < numSavedSubviews) ? numSubviews : numSavedSubviews;
for (i = 0; i < numSettableSubviews; i++)
{
NSView *subview;
NSDictionary *subviewPositionData;
subview = [subviews objectAtIndex:i];
subviewPositionData = [subviewPositionsArray objectAtIndex:i];
// subview data consists of frame and collapse state
[subview setFrame:NSRectFromString([subviewPositionData objectForKey:savedPositionSubviewFrameKey])];
[self setSubview:subview isCollapsed:[[subviewPositionData objectForKey:savedPositionSubviewIsCollapsedKey] boolValue]];
}
// set isVertical
[self setVertical:[[positionDict objectForKey:savedPositionIsVerticalKey] boolValue]];
}
}
[self resizeSubviewsWithOldSize:[self bounds].size];
}
- (id)plistObjectWithSavedPosition
{
NSMutableDictionary *positionDict = [NSMutableDictionary dictionary];
// save position data format version
[positionDict setObject:[NSNumber numberWithInt:2] forKey:savedPositionVersionKey];
// save subview positions
NSArray *subviews;
NSMutableArray *subviewPositionsArray;
int numSubviews, i;
subviews = [self subviews];
numSubviews = [subviews count];
subviewPositionsArray = [NSMutableArray array];
for (i = 0; i < numSubviews; i++)
{
NSView *subview;
NSDictionary *subviewPositionData;
subview = [subviews objectAtIndex:i];
// subview data consists of frame and collapse state
subviewPositionData = [NSDictionary dictionaryWithObjectsAndKeys:
NSStringFromRect([subview frame]), savedPositionSubviewFrameKey,
[NSNumber numberWithBool:[self isSubviewCollapsed:subview]], savedPositionSubviewIsCollapsedKey,
nil];
[subviewPositionsArray addObject:subviewPositionData];
}
[positionDict setObject:subviewPositionsArray forKey:savedPositionSubviewsKey];
// save isVertical
BOOL isVertical = [self isVertical];
[positionDict setObject:[NSNumber numberWithBool:isVertical]
forKey:savedPositionIsVerticalKey];
return positionDict;
}
+ (NSString *)kfDefaultsKeyForName:(NSString *)name
{
return [@"KFSplitView Position " stringByAppendingString:name];
}
- (void)kfSavePositionUsingAutosaveName:(id)sender
{
[self savePositionUsingName:kfPositionAutosaveName];
}
@end
#undef KFMAJORCOORDOFPOINT
#undef KFMINORCOORDOFPOINT
#undef KFMAJORDIMOFSIZE
#undef KFMINORDIMOFSIZE
#undef KFPOINTWITHMAJORMINOR
#undef KFSIZEWITHMAJORMINOR
#undef KFMAX