2 // RBSplitView.m version 1.1.4
5 // Created by Rainer Brockerhoff on 24/09/2004.
6 // Copyright 2004-2006 Rainer Brockerhoff.
7 // Some Rights Reserved under the Creative Commons Attribution License, version 2.5, and/or the MIT License.
10 #import "RBSplitView.h"
11 #import "RBSplitViewPrivateDefines.h"
13 // Please don't remove this copyright notice!
14 static const unsigned char RBSplitView_Copyright[] __attribute__ ((used)) =
15 "RBSplitView 1.1.4 Copyright(c)2004-2006 by Rainer Brockerhoff <rainer@brockerhoff.net>.";
17 // This vector keeps currently used cursors. nil means the default cursor.
18 static NSCursor* cursors[RBSVCursorTypeCount] = {nil};
20 // Our own fMIN and fMAX
21 static inline float fMIN(float a,float b) {
25 static inline float fMAX(float a,float b) {
29 @implementation RBSplitView
31 // These class methods get and set the cursor used for each type.
32 // Pass in nil to reset to the default cursor for that type.
33 + (NSCursor*)cursor:(RBSVCursorType)type {
34 if ((type>=0)&&(type<RBSVCursorTypeCount)) {
35 NSCursor* result = cursors[type];
40 case RBSVHorizontalCursor:
41 return [NSCursor resizeUpDownCursor];
42 case RBSVVerticalCursor:
43 return [NSCursor resizeLeftRightCursor];
45 return [NSCursor openHandCursor];
47 return [NSCursor closedHandCursor];
52 return [NSCursor currentCursor];
55 + (void)setCursor:(RBSVCursorType)type toCursor:(NSCursor*)cursor {
56 if ((type>=0)&&(type<RBSVCursorTypeCount)) {
57 [cursors[type] release];
58 cursors[type] = [cursor retain];
62 // This class method clears the saved state(s) for a given autosave name from the defaults.
63 + (void)removeStateUsingName:(NSString*)name {
65 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
66 [defaults removeObjectForKey:[[self class] defaultsKeyForName:name isHorizontal:NO]];
67 [defaults removeObjectForKey:[[self class] defaultsKeyForName:name isHorizontal:YES]];
71 // This class method returns the actual key used to store autosave data in the defaults.
72 + (NSString*)defaultsKeyForName:(NSString*)name isHorizontal:(BOOL)orientation {
73 return [NSString stringWithFormat:@"RBSplitView %@ %@",orientation?@"H":@"V",name];
76 // This pair of methods gets and sets the autosave name, which allows restoring the subview's
77 // state from the user defaults.
78 // We take care not to allow nil autosaveNames.
79 - (NSString*)autosaveName {
83 // Sets the autosaveName; this should be a unique key to be used to store the subviews' proportions
84 // in the user defaults. Default is @"", which doesn't save anything. Set flag to YES to set
85 // unique names for nested subviews. You are responsible for avoiding duplicates; avoid using
86 // the characters '[' and ']' in autosaveNames.
87 - (void)setAutosaveName:(NSString*)aString recursively:(BOOL)flag {
89 if ((clear = ![aString length])) {
92 [RBSplitView removeStateUsingName:autosaveName];
93 [autosaveName autorelease];
94 autosaveName = [aString retain];
96 NSArray* subviews = [self subviews];
97 int subcount = [subviews count];
99 for (i=0;i<subcount;i++) {
100 RBSplitView* sv = [[subviews objectAtIndex:i] asSplitView];
102 NSString* subst = clear?@"":[aString stringByAppendingFormat:@"[%d]",i];
103 [sv setAutosaveName:subst recursively:YES];
109 // Saves the current state of the subviews if there's a valid autosave name set. If the argument
110 // is YES, it's then also called recursively for nested RBSplitViews. Returns YES if successful.
111 // You must call restoreState explicity at least once before saveState will begin working.
112 - (BOOL)saveState:(BOOL)recurse {
113 // Saving the state is also disabled while dragging.
114 if (canSaveState&&![self isDragging]&&[autosaveName length]) {
115 [[NSUserDefaults standardUserDefaults] setObject:[self stringWithSavedState] forKey:[[self class] defaultsKeyForName:autosaveName isHorizontal:[self isHorizontal]]];
117 NSEnumerator* enumerator = [[self subviews] objectEnumerator];
119 while ((sub = [enumerator nextObject])) {
120 [[sub asSplitView] saveState:YES];
128 // Restores the saved state of the subviews if there's a valid autosave name set. If the argument
129 // is YES, it's also called recursively for nested RBSplitViews. Returns YES if successful.
130 // It's good policy to call adjustSubviews immediately after calling restoreState.
131 - (BOOL)restoreState:(BOOL)recurse {
133 if ([autosaveName length]) {
134 result = [self setStateFromString:[[NSUserDefaults standardUserDefaults] stringForKey:[[self class] defaultsKeyForName:autosaveName isHorizontal:[self isHorizontal]]]];
135 if (result&&recurse) {
136 NSEnumerator* enumerator = [[self subviews] objectEnumerator];
138 while ((sub = [enumerator nextObject])) {
139 [[sub asSplitView] restoreState:YES];
147 // Returns an array with complete state information for the receiver and all subviews, taking
148 // nesting into account. Don't store this array in a file, as its format might change in the
149 // future; this is for taking a state snapshot and later restoring it with setStatesFromArray.
150 - (NSArray*)arrayWithStates {
151 NSMutableArray* array = [NSMutableArray array];
152 [array addObject:[self stringWithSavedState]];
153 NSEnumerator* enumerator = [[self subviews] objectEnumerator];
155 while ((sub = [enumerator nextObject])) {
156 RBSplitView* suv = [sub asSplitView];
158 [array addObject:[suv arrayWithStates]];
160 [array addObject:[NSNull null]];
166 // Restores the state of the receiver and all subviews. The array must have been produced by a
167 // previous call to arrayWithStates. Returns YES if successful. This will fail if you have
168 // added or removed subviews in the meantime!
169 // You need to call adjustSubviews after calling this.
170 - (BOOL)setStatesFromArray:(NSArray*)array {
171 NSArray* subviews = [self subviews];
172 unsigned int count = [array count];
173 if (count==([subviews count]+1)) {
174 NSString* me = [array objectAtIndex:0];
175 if ([me isKindOfClass:[NSString class]]) {
176 if ([self setStateFromString:me]) {
178 for (i=1;i<count;i++) {
179 NSArray* item = [array objectAtIndex:i];
180 RBSplitView* suv = [[subviews objectAtIndex:i-1] asSplitView];
181 if ([item isKindOfClass:[NSArray class]]==(suv!=nil)) {
182 if (suv&&![suv setStatesFromArray:item]) {
196 // Returns a string encoding the current state of all direct subviews. Does not check for nesting.
197 // The string contains the number of direct subviews, then the dimension for each subview (which will
198 // be negative for collapsed subviews), all separated by blanks.
199 - (NSString*)stringWithSavedState {
200 NSArray* subviews = [self subviews];
201 NSMutableString* result = [NSMutableString stringWithFormat:@"%d",[subviews count]];
202 NSEnumerator* enumerator = [subviews objectEnumerator];
204 while ((sub = [enumerator nextObject])) {
205 double size = [sub dimension];
206 if ([sub isCollapsed]) {
209 size += +[sub RB___fraction];
211 [result appendFormat:[sub isHidden]?@" %gH":@" %g",size];
216 // Readjusts all direct subviews according to the encoded string parameter.
217 // The number of subviews must match. Returns YES if successful. Does not check for nesting.
218 - (BOOL)setStateFromString:(NSString*)aString {
219 if ([aString length]) {
220 NSArray* parts = [aString componentsSeparatedByString:@" "];
221 NSArray* subviews = [self subviews];
222 int subcount = [subviews count];
223 int k = [parts count];
224 if ((k-->1)&&([[parts objectAtIndex:0] intValue]==subcount)&&(k==subcount)) {
226 NSRect frame = [self frame];
227 BOOL ishor = [self isHorizontal];
228 for (i=0;i<subcount;i++) {
229 NSString* part = [parts objectAtIndex:i+1];
230 BOOL hidden = [part hasSuffix:@"H"];
231 double size = [part doubleValue];
232 BOOL negative = size<=0.0;
239 DIM(frame.size) = size;
240 RBSplitSubview* sub = [subviews objectAtIndex:i];
241 [sub RB___setFrame:frame withFraction:fract notify:NO];
245 [sub RB___setHidden:hidden];
247 [self setMustAdjust];
254 // This is the designated initializer for creating RBSplitViews programmatically. You can set the
255 // divider image and other parameters afterwards.
256 - (id)initWithFrame:(NSRect)frame {
257 self = [super initWithFrame:frame];
264 [self setVertical:YES];
265 [self setDivider:nil];
266 [self setAutosaveName:nil recursively:NO];
267 [self setBackground:nil];
272 // This convenience initializer adds any number of subviews and adjusts them proportionally.
273 - (id)initWithFrame:(NSRect)frame andSubviews:(unsigned)count {
274 self = [self initWithFrame:frame];
277 [self addSubview:[[[RBSplitSubview alloc] initWithFrame:frame] autorelease]];
279 [self setMustAdjust];
284 // Frees retained objects when going away.
289 [autosaveName release];
291 [background release];
295 // Sets and gets the coupling between the view and its containing RBSplitView (if any). Coupled
296 // RBSplitViews take some parameters, such as divider images, from the containing view. The default
297 // is for nested RBSplitViews is YES; however, isCoupled returns NO if we're not nested.
298 - (void)setCoupled:(BOOL)flag {
299 if (flag!=isCoupled) {
301 // If we've just been uncoupled and there's no divider image, we copy it from the containing view.
302 if (!isCoupled&&!divider) {
303 [self setDivider:[[self splitView] divider]];
305 [self setMustAdjust];
310 return isCoupled&&([super splitView]!=nil);
313 // This returns the containing splitview if they are coupled. It's guaranteed to return a RBSplitView or nil.
314 - (RBSplitView*)couplingSplitView {
315 return isCoupled?[super couplingSplitView]:nil;
318 // This returns self.
319 - (RBSplitView*)asSplitView {
323 // This return self if we're really coupled to the owning splitview.
324 - (RBSplitView*)coupledSplitView {
325 return [self isCoupled]?self:nil;
328 // We always return NO, but do special handling in RBSplitSubview's mouseDown: method.
329 - (BOOL)mouseDownCanMoveWindow {
333 // RBSplitViews must be flipped to work properly for horizontal dividers. As the subviews are never
334 // flipped, this won't make your life harder.
339 // Call this method to make sure that the subviews and divider rectangles are recalculated
340 // properly before display.
341 - (void)setMustAdjust {
343 [self setNeedsDisplay:YES];
346 // Returns YES if there's a pending adjustment.
351 // Returns YES if we're in a dragging loop.
356 // Returns YES if the view is directly contained in an NSScrollView.
357 - (BOOL)isInScrollView {
358 return isInScrollView;
361 // This pair of methods allows you to move the dividers for background windows while holding down
362 // the command key, without bringing the window to the foreground.
363 - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
364 return ([theEvent modifierFlags]&NSCommandKeyMask)==0;
367 - (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent*)theEvent {
368 return ([theEvent modifierFlags]&NSCommandKeyMask)!=0;
371 // These 3 methods handle view background colors and opacity. The default is the window background.
372 // Pass nil or a completely transparent color to setBackground to use transparency. If you set any
373 // other background color, it will completely fill the RBSplitView (including subviews and dividers).
374 // The view will be considered opaque only if its alpha is equal to 1.0.
375 // For a nested, coupled RBSplitView, background and opacity are copied from the containing RBSplitView,
376 // and setting the background has no effect.
377 - (NSColor*)background {
378 RBSplitView* sv = [self couplingSplitView];
379 return sv?[sv background]:background;
382 - (void)setBackground:(NSColor*)color {
383 if (![self couplingSplitView]) {
384 [background autorelease];
385 background = color?([color alphaComponent]>0.0?[color retain]:nil):nil;
386 [self setNeedsDisplay:YES];
391 RBSplitView* sv = [self couplingSplitView];
392 return sv?[sv isOpaque]:(background&&([background alphaComponent]>=1.0));
395 // This will make debugging a little easier by appending the state string to the
396 // default description.
397 - (NSString*)description {
398 return [NSString stringWithFormat:@"%@ {%@}",[super description],[self stringWithSavedState]];
401 // The following 3 methods handle divider orientation. The actual stored trait is horizontality,
402 // but verticality is used for setting to conform to the NSSplitView convention.
403 // For a nested RBSplitView, orientation is perpendicular to the containing RBSplitView, and
404 // setting it has no effect. This parameter is not affected by coupling.
405 // After changing the orientation you may want to restore the state with restoreState:.
406 - (BOOL)isHorizontal {
407 RBSplitView* sv = [self splitView];
408 return sv?[sv isVertical]:isHorizontal;
412 return 1-[self isHorizontal];
415 - (void)setVertical:(BOOL)flag {
416 if (![self splitView]&&(isHorizontal!=!flag)) {
417 BOOL ishor = isHorizontal = !flag;
418 NSSize size = divider?[divider size]:NSZeroSize;
419 [self setDividerThickness:DIM(size)];
420 [self setMustAdjust];
424 // Returns the subview which a given identifier.
425 - (RBSplitSubview*)subviewWithIdentifier:(NSString*)anIdentifier {
426 NSEnumerator* enumerator = [[self subviews] objectEnumerator];
427 RBSplitSubview* subview;
428 while ((subview = [enumerator nextObject])) {
429 if ([anIdentifier isEqualToString:[subview identifier]]) {
436 // Returns the subview at a given position
437 - (RBSplitSubview*)subviewAtPosition:(unsigned)position {
438 NSArray* subviews = [super subviews];
439 unsigned int subcount = [subviews count];
440 if (position<subcount) {
441 return [subviews objectAtIndex:position];
446 // This pair of methods gets and sets the delegate object. Delegates aren't retained.
451 - (void)setDelegate:(id)anObject {
455 // This pair of methods gets and sets the divider image. Setting the image automatically adjusts the
456 // divider thickness. A nil image means a 0-pixel wide divider, unless you set a thickness explicitly.
457 // For a nested RBSplitView, the divider is copied from the containing RBSplitView, and
458 // setting it has no effect. The returned image is always flipped.
459 - (NSImage*)divider {
460 RBSplitView* sv = [self couplingSplitView];
461 return sv?[sv divider]:divider;
464 - (void)setDivider:(NSImage*)image {
465 if (![self couplingSplitView]) {
466 [divider autorelease];
467 if ([image isFlipped]) {
468 // If the image is flipped, we just retain it.
469 divider = [image retain];
471 // if the image isn't flipped, we copy the image instead of retaining it, and flip it.
472 divider = [image copy];
473 [divider setFlipped:YES];
475 // We set the thickness to 0.0 so the image dimension will prevail.
476 [self setDividerThickness:0.0];
477 [self setMustAdjust];
481 // This pair of methods gets and sets the divider thickness. It should be an integer value and at least
482 // 0.0, so we make sure. Set it to 0.0 to make the image dimensions prevail.
483 - (float)dividerThickness {
484 if (dividerThickness>0.0) {
485 return dividerThickness;
487 NSImage* divdr = [self divider];
489 NSSize size = [divdr size];
490 BOOL ishor = [self isHorizontal];
496 - (void)setDividerThickness:(float)thickness {
497 float t = fMAX(0.0,floorf(thickness));
498 if ((int)dividerThickness!=(int)t) {
499 dividerThickness = t;
500 [self setMustAdjust];
504 // These three methods add subviews. If aView isn't a RBSplitSubview, one is automatically inserted above
505 // it, and aView's frame and resizing mask is set to occupy the entire RBSplitSubview.
506 - (void)addSubview:(NSView*)aView {
507 if ([aView isKindOfClass:[RBSplitSubview class]]) {
508 [super addSubview:aView];
510 [aView setFrameOrigin:NSZeroPoint];
511 RBSplitSubview* sub = [[[RBSplitSubview alloc] initWithFrame:[aView frame]] autorelease];
512 [aView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
513 [sub addSubview:aView];
514 [super addSubview:sub];
516 [self setMustAdjust];
519 - (void)addSubview:(NSView*)aView positioned:(NSWindowOrderingMode)place relativeTo:(NSView*)otherView {
520 if ([aView isKindOfClass:[RBSplitSubview class]]) {
521 [super addSubview:aView positioned:place relativeTo:otherView];
523 [aView setFrameOrigin:NSZeroPoint];
524 RBSplitSubview* sub = [[[RBSplitSubview alloc] initWithFrame:[aView frame]] autorelease];
525 [aView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
526 [sub addSubview:aView];
527 [super addSubview:sub positioned:place relativeTo:otherView];
528 [aView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
530 [self setMustAdjust];
533 - (void)addSubview:(NSView*)aView atPosition:(unsigned)position {
534 RBSplitSubview* suv = [self subviewAtPosition:position];
536 [self addSubview:aView positioned:NSWindowBelow relativeTo:suv];
538 [self addSubview:aView];
542 // This keeps the isInScrollView flag up-to-date.
543 - (void)viewDidMoveToSuperview {
544 [super viewDidMoveToSuperview];
545 NSScrollView* scrollv = [self enclosingScrollView];
546 isInScrollView = scrollv?[scrollv documentView]==self:NO;
549 // This makes sure the subviews are adjusted after a subview is removed.
550 - (void)willRemoveSubview:(NSView*)subview {
551 if ([subview respondsToSelector:@selector(RB___stopAnimation)]) {
552 [(RBSplitSubview*)subview RB___stopAnimation];
554 [super willRemoveSubview:subview];
555 [self setMustAdjust];
558 // RBSplitViews never resize their subviews automatically.
559 - (BOOL)autoresizesSubviews {
563 // This adjusts the subviews when the size is set. setFrame: calls this, so all is well. It calls
564 // the delegate if implemented.
565 - (void)setFrameSize:(NSSize)size {
566 NSSize oldsize = [self frame].size;
567 [super setFrameSize:size];
568 [self setMustAdjust];
569 if ([delegate respondsToSelector:@selector(splitView:wasResizedFrom:to:)]) {
570 BOOL ishor = [self isHorizontal];
571 float olddim = DIM(oldsize);
572 float newdim = DIM(size);
573 // The delegate is not called if the dimension hasn't changed.
574 if (((int)newdim!=(int)olddim)) {
575 [delegate splitView:self wasResizedFrom:olddim to:newdim];
578 // We adjust the subviews only if the delegate didn't.
579 if (mustAdjust&&!isAdjusting) {
580 [self adjustSubviews];
584 // This method handles dragging and double-clicking dividers with the mouse. While dragging, the
585 // "closed hand" cursor is shown. Double clicks are handled separately. Nothing will happen if
586 // no divider image is set.
587 - (void)mouseDown:(NSEvent*)theEvent {
591 NSArray* subviews = [self RB___subviews];
592 int subcount = [subviews count];
596 // If the mousedown was in an alternate dragview, or if there's no divider image, handle it in RBSplitSubview.
597 if ((actDivider<NSNotFound)||![self divider]) {
598 [super mouseDown:theEvent];
601 NSPoint where = [self convertPoint:[theEvent locationInWindow] fromView:nil];
602 BOOL ishor = [self isHorizontal];
605 // Loop over the divider rectangles.
606 for (i=0;i<subcount;i++) {
607 NSRect* divdr = ÷rs[i];
608 if ([self mouse:where inRect:*divdr]) {
609 // leading points at the subview immediately leading the divider being tracked.
610 RBSplitView* leading = [subviews objectAtIndex:i];
611 // trailing points at the subview immediately trailing the divider being tracked.
612 RBSplitView* trailing = [subviews objectAtIndex:i+1];
613 if ([delegate respondsToSelector:@selector(splitView:shouldHandleEvent:inDivider:betweenView:andView:)]) {
614 if (![delegate splitView:self shouldHandleEvent:theEvent inDivider:i betweenView:leading andView:trailing]) {
618 // If it's a double click, try to expand or collapse one of the neighboring subviews.
619 if ([theEvent clickCount]>1) {
620 // If both are collapsed, we do nothing. If one of them is collapsed, we try to expand it.
621 if ([trailing isCollapsed]) {
622 if (![leading isCollapsed]) {
623 [self RB___tryToExpandTrailing:trailing leading:leading delta:-[trailing dimension]];
626 if ([leading isCollapsed]) {
627 [self RB___tryToExpandLeading:leading divider:i trailing:trailing delta:[leading dimension]];
629 // If neither are collapsed, we check if both are collapsible.
630 BOOL lcan = [leading canCollapse];
631 BOOL tcan = [trailing canCollapse];
632 float ldim = [leading dimension];
634 // If both are collapsible, we try asking the delegate.
635 if ([delegate respondsToSelector:@selector(splitView:collapseLeading:orTrailing:)]) {
636 RBSplitSubview* sub = [delegate splitView:self collapseLeading:leading orTrailing:trailing];
637 // If the delegate returns nil, neither view will collapse.
639 tcan = sub==trailing;
641 // Otherwise we try collapsing the smaller one. If they're equal, the trailing one will be collapsed.
642 lcan = ldim<[trailing dimension];
645 // At this point, we'll try to collapse the leading subview.
647 [self RB___tryToShortenLeading:leading divider:i trailing:trailing delta:-ldim always:NO];
649 // If the leading subview didn't collapse for some reason, we try to collapse the trailing one.
650 if (!mustAdjust&&tcan) {
651 [self RB___tryToShortenTrailing:trailing divider:i leading:leading delta:[trailing dimension] always:NO];
655 // If the subviews have changed, clear the fractions, adjust and redisplay
657 [self RB___setMustClearFractions];
658 RBSplitView* sv = [self splitView];
659 [sv?sv:self adjustSubviews];
663 // Single click; record the offsets within the divider rectangle and check for nesting.
664 float divt = [self dividerThickness];
665 float offset = DIM(where)-DIM(divdr->origin);
666 // Check if the leading subview is nested and if yes, if one of its two-axis thumbs was hit.
667 int ldivdr = NSNotFound;
669 NSPoint lwhere = where;
670 NSRect lrect = NSZeroRect;
671 if ((leading = [leading coupledSplitView])) {
672 ldivdr = [leading RB___dividerHitBy:lwhere relativeToView:self thickness:divt];
673 if (ldivdr!=NSNotFound) {
674 lrect = [leading RB___dividerRect:ldivdr relativeToView:self];
675 loffset = OTHER(lwhere)-OTHER(lrect.origin);
678 // Check if the trailing subview is nested and if yes, if one of its two-axis thumbs was hit.
679 int tdivdr = NSNotFound;
681 NSPoint twhere = where;
682 NSRect trect = NSZeroRect;
683 if ((trailing = [trailing coupledSplitView])) {
684 tdivdr = [trailing RB___dividerHitBy:twhere relativeToView:self thickness:divt];
685 if (tdivdr!=NSNotFound) {
686 trect = [trailing RB___dividerRect:tdivdr relativeToView:self];
687 toffset = OTHER(twhere)-OTHER(trect.origin);
690 // Now we loop handling mouse events until we get a mouse up event, while showing the drag cursor.
691 [[RBSplitView cursor:RBSVDragCursor] push];
692 [self RB___setDragging:YES];
693 while ((theEvent = [NSApp nextEventMatchingMask:NSLeftMouseDownMask|NSLeftMouseDraggedMask|NSLeftMouseUpMask untilDate:[NSDate distantFuture] inMode:NSEventTrackingRunLoopMode dequeue:YES])&&([theEvent type]!=NSLeftMouseUp)) {
694 // Set up a local autorelease pool for the loop to prevent buildup of temporary objects.
695 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
696 NSDisableScreenUpdates();
697 // Track the mouse along the main coordinate.
698 [self RB___trackMouseEvent:theEvent from:where withBase:NSZeroPoint inDivider:i];
699 if (ldivdr!=NSNotFound) {
700 // Track any two-axis thumbs for the leading nested RBSplitView.
701 [leading RB___trackMouseEvent:theEvent from:[self convertPoint:lwhere toView:leading] withBase:NSZeroPoint inDivider:ldivdr];
703 if (tdivdr!=NSNotFound) {
704 // Track any two-axis thumbs for the trailing nested RBSplitView.
705 [trailing RB___trackMouseEvent:theEvent from:[self convertPoint:twhere toView:trailing] withBase:NSZeroPoint inDivider:tdivdr];
707 if (mustAdjust||[leading mustAdjust]||[trailing mustAdjust]) {
708 // The mouse was dragged and the subviews changed, so we must redisplay, as
709 // several divider rectangles may have changed.
710 RBSplitView* sv = [self splitView];
711 [sv?sv:self adjustSubviews];
713 divdr = ÷rs[i];
714 // Adjust to the new cursor coordinates.
715 DIM(where) = DIM(divdr->origin)+offset;
716 if ((ldivdr!=NSNotFound)&&![leading isCollapsed]) {
717 // Adjust for the leading nested RBSplitView's thumbs while it's not collapsed.
718 lrect = [leading RB___dividerRect:ldivdr relativeToView:self];
719 OTHER(lwhere) = OTHER(lrect.origin)+loffset;
721 if ((tdivdr!=NSNotFound)&&![trailing isCollapsed]) {
722 // Adjust for the trailing nested RBSplitView's thumbs while it's not collapsed.
723 trect = [trailing RB___dividerRect:tdivdr relativeToView:self];
724 OTHER(twhere) = OTHER(trect.origin)+toffset;
727 NSEnableScreenUpdates();
730 [self RB___setDragging:NO];
731 // Redisplay the previous cursor.
738 // This will be called before the view will be redisplayed, so we adjust subviews if necessary.
739 - (BOOL)needsDisplay {
740 if (mustAdjust&&!isAdjusting) {
741 [self adjustSubviews];
744 return [super needsDisplay];
747 // We implement awakeFromNib to restore the state. This works if an autosaveName is set in the nib.
748 - (void)awakeFromNib {
749 if ([RBSplitSubview instancesRespondToSelector:@selector(awakeFromNib)]) {
750 [super awakeFromNib];
752 if (![self splitView]) {
753 [self restoreState:YES];
757 // We check if subviews must be adjusted before redisplaying programmatically.
759 if (mustAdjust&&!isAdjusting) {
760 [self adjustSubviews];
765 // This method draws the divider rectangles and then the two-axis thumbs if there are any.
766 - (void)drawRect:(NSRect)rect {
767 [super drawRect:rect];
771 NSArray* subviews = [self RB___subviews];
772 int subcount = [subviews count];
773 // Return if there are no dividers to draw.
779 // Cache the divider image.
780 NSImage* divdr = [self divider];
781 float divt = [self dividerThickness];
782 // Loop over the divider rectangles.
783 for (i=0;i<subcount;i++) {
784 // Check if we need to draw this particular divider.
785 if ([self needsToDrawRect:dividers[i]]) {
786 RBSplitView* leading = [subviews objectAtIndex:i];
787 RBSplitView* trailing = [subviews objectAtIndex:i+1];
788 BOOL lexp = divdr?![leading isCollapsed]:NO;
789 BOOL texp = divdr?![trailing isCollapsed]:NO;
790 // We don't draw the divider image if either of the neighboring subviews is a non-collapsed
791 // nested split view.
792 BOOL nodiv = (lexp&&[leading coupledSplitView])||(texp&&[trailing coupledSplitView]);
793 [self drawDivider:nodiv?nil:divdr inRect:dividers[i] betweenView:leading andView:trailing];
795 // Draw the corresponding two-axis thumbs if the leading view is a nested RBSplitView.
796 if ((leading = [leading coupledSplitView])&&lexp) {
797 [leading RB___drawDividersIn:self forDividerRect:dividers[i] thickness:divt];
799 // Draw the corresponding two-axis thumbs if the trailing view is a nested RBSplitView.
800 if ((trailing = [trailing coupledSplitView])&&texp) {
801 [trailing RB___drawDividersIn:self forDividerRect:dividers[i] thickness:divt];
808 // This method draws dividers. You should never call it directly but you can override it when
809 // subclassing, if you need custom dividers. It draws the divider image centered in the divider rectangle.
810 // If we're drawing a two-axis thumb leading and trailing will be nil, and the rectangle
811 // will be the thumb rectangle.
812 // If there are nested split views this will be called once to draw the main divider rect,
813 // and again for every thumb.
814 - (void)drawDivider:(NSImage*)anImage inRect:(NSRect)rect betweenView:(RBSplitSubview*)leading andView:(RBSplitSubview*)trailing {
815 // Fill the view with the background color (if there's any). Don't draw the background again for
817 if (leading||trailing) {
818 NSColor* bg = [self background];
821 NSRectFillUsingOperation(rect,NSCompositeSourceOver);
824 // Center the image, if there is one.
825 NSRect imrect = NSZeroRect;
826 NSRect dorect = NSZeroRect;
828 imrect.size = dorect.size = [anImage size];
829 dorect.origin = NSMakePoint(floorf(rect.origin.x+(rect.size.width-dorect.size.width)/2),
830 floorf(rect.origin.y+(rect.size.height-dorect.size.height)/2));
832 // Ask the delegate for the final rect where the image should be drawn.
833 if ([delegate respondsToSelector:@selector(splitView:willDrawDividerInRect:betweenView:andView:withProposedRect:)]) {
834 dorect = [delegate splitView:self willDrawDividerInRect:rect betweenView:leading andView:trailing withProposedRect:dorect];
836 // Draw the image if the delegate returned a non-empty rect.
837 if (!NSIsEmptyRect(dorect)) {
838 [anImage drawInRect:dorect fromRect:imrect operation:NSCompositeSourceOver fraction:1.0];
842 // This method should be called only from within the splitView:wasResizedFrom:to: delegate method
843 // to keep some specific subview the same size.
844 - (void)adjustSubviewsExcepting:(RBSplitSubview*)excepting {
845 [self RB___adjustSubviewsExcepting:[excepting isCollapsed]?nil:excepting];
848 // This method adjusts subviews and divider rectangles.
849 - (void)adjustSubviews {
850 [self RB___adjustSubviewsExcepting:nil];
853 // This resets the appropriate cursors for each divider according to the orientation.
854 // No cursors are shown if there is no divider image.
855 - (void)resetCursorRects {
859 id del = [delegate respondsToSelector:@selector(splitView:cursorRect:forDivider:)]?delegate:nil;
860 NSArray* subviews = [self RB___subviews];
861 int divcount = [subviews count]-1;
862 if ((divcount<1)||![self divider]) {
863 [del splitView:self cursorRect:NSZeroRect forDivider:0];
867 NSCursor* cursor = [RBSplitView cursor:[self isVertical]?RBSVVerticalCursor:RBSVHorizontalCursor];
868 float divt = [self dividerThickness];
869 for (i=0;i<divcount;i++) {
870 RBSplitView* sub = [[subviews objectAtIndex:i] coupledSplitView];
871 // If the leading subview is a nested RBSplitView, add the thumb rectangles first.
873 [sub RB___addCursorRectsTo:self forDividerRect:dividers[i] thickness:divt];
875 sub = [[subviews objectAtIndex:i+1] coupledSplitView];
876 // If the trailing subview is a nested RBSplitView, add the thumb rectangles first.
878 [sub RB___addCursorRectsTo:self forDividerRect:dividers[i] thickness:divt];
880 // Now add thedivider rectangle.
881 NSRect divrect = dividers[i];
883 divrect = [del splitView:self cursorRect:divrect forDivider:i];
885 if (!NSIsEmptyRect(divrect)) {
886 [self addCursorRect:divrect cursor:cursor];
891 // These two methods encode and decode RBSplitViews. One peculiarity is that we encode the divider image's
892 // bitmap representation as data; this makes the nib files larger, but the user can just paste any image
893 // into the RBSplitView inspector - or use the default divider image - without having to include it into the
895 - (void)encodeWithCoder:(NSCoder *)coder {
896 [super encodeWithCoder:coder];
897 if ([coder allowsKeyedCoding]) {
898 [coder encodeConditionalObject:delegate forKey:@"delegate"];
899 [coder encodeObject:autosaveName forKey:@"autosaveName"];
900 [coder encodeObject:[divider TIFFRepresentation] forKey:@"divider"];
901 [coder encodeObject:background forKey:@"background"];
902 [coder encodeFloat:dividerThickness forKey:@"dividerThickness"];
903 [coder encodeBool:isHorizontal forKey:@"isHorizontal"];
904 [coder encodeBool:isCoupled forKey:@"isCoupled"];
906 [coder encodeConditionalObject:delegate];
907 [coder encodeObject:autosaveName];
908 [coder encodeObject:[divider TIFFRepresentation]];
909 [coder encodeObject:background];
910 [coder encodeValueOfObjCType:@encode(typeof(dividerThickness)) at:÷rThickness];
911 [coder encodeValueOfObjCType:@encode(typeof(isHorizontal)) at:&isHorizontal];
912 [coder encodeValueOfObjCType:@encode(typeof(isCoupled)) at:&isCoupled];
916 - (id)initWithCoder:(NSCoder *)coder {
917 if ((self = [super initWithCoder:coder])) {
924 if ([coder allowsKeyedCoding]) {
925 isCoupled = [coder decodeBoolForKey:@"isCoupled"];
926 [self setDelegate:[coder decodeObjectForKey:@"delegate"]];
927 [self setAutosaveName:[coder decodeObjectForKey:@"autosaveName"] recursively:NO];
928 data = [coder decodeObjectForKey:@"divider"];
929 [self setBackground:[coder decodeObjectForKey:@"background"]];
930 divt = [coder decodeFloatForKey:@"dividerThickness"];
931 isHorizontal = [coder decodeBoolForKey:@"isHorizontal"];
933 [self setDelegate:[coder decodeObject]];
934 [self setAutosaveName:[coder decodeObject] recursively:NO];
935 data = [coder decodeObject];
936 [self setBackground:[coder decodeObject]];
937 [coder decodeValueOfObjCType:@encode(typeof(divt)) at:&divt];
938 [coder decodeValueOfObjCType:@encode(typeof(isHorizontal)) at:&isHorizontal];
939 [coder decodeValueOfObjCType:@encode(typeof(isCoupled)) at:&isCoupled];
943 NSBitmapImageRep* rep = [NSBitmapImageRep imageRepWithData:data];
944 NSImage* image = [[[NSImage alloc] initWithSize:[rep size]] autorelease];
945 [image setFlipped:YES];
946 [image addRepresentation:rep];
947 [self setDivider:image];
949 [self setDivider:nil];
951 [self setDividerThickness:divt];
952 [self setMustAdjust];
953 [self performSelector:@selector(viewDidMoveToSuperview) withObject:nil afterDelay:0.0];
954 [self performSelector:@selector(RB___adjustOutermostIfNeeded) withObject:nil afterDelay:0.0];
961 @implementation RBSplitView (RB___ViewAdditions)
963 // This sets the dragging status flag. After clearing the flag, the state must be saved explicitly.
964 - (void)RB___setDragging:(BOOL)flag {
965 BOOL save = isDragging&&!flag;
972 // This returns the number of visible subviews.
973 - (unsigned int)RB___numberOfSubviews {
974 unsigned int result = 0;
975 NSEnumerator* enumerator = [[self subviews] objectEnumerator];
977 while ((sub = [enumerator nextObject])) {
983 // This returns the origin coordinate of the Nth divider.
984 - (float)RB___dividerOrigin:(int)indx {
987 BOOL ishor = [self isHorizontal];
988 result = DIM(dividers[indx].origin);
993 // This returns an array with all non-hidden subviews.
994 - (NSArray*)RB___subviews {
995 NSMutableArray* result = [NSMutableArray arrayWithArray:[self subviews]];
997 for (i=[result count]-1;i>=0;i--) {
998 RBSplitSubview* view = [result objectAtIndex:i];
999 if ([view isHidden]) {
1000 [result removeObjectAtIndex:i];
1006 // This returns the actual value set in dividerThickness.
1007 - (float)RB___dividerThickness {
1008 return dividerThickness;
1011 // This method returns the actual dimension occupied by the subviews; that is, without dividers.
1012 - (float)RB___dimensionWithoutDividers {
1013 BOOL ishor = [self isHorizontal];
1014 NSSize size = [self frame].size;
1015 return fMAX(1.0,DIM(size)-[self dividerThickness]*([self RB___numberOfSubviews]-1));
1018 // This method returns one of the divider rectangles, or NSZeroRect if the index is invalid.
1019 // If view is non-nil, the rect will be expressed in that view's coordinates. We assume
1020 // that view is a superview of self.
1021 - (NSRect)RB___dividerRect:(unsigned)indx relativeToView:(RBSplitView*)view {
1022 if (dividers&&(indx<[self RB___numberOfSubviews]-1)) {
1023 NSRect result = dividers[indx];
1024 if (view&&(view!=self)) {
1025 result = [self convertRect:result toView:view];
1032 // Returns the index of the divider hit by the point, or NSNotFound if none.
1033 // point is in coordinates relative to view. delta is the divider thickness added
1034 // to both ends of the divider rect, to accomodate two-axis thumbs.
1035 - (unsigned)RB___dividerHitBy:(NSPoint)point relativeToView:(RBSplitView*)view thickness:(float)delta {
1039 int divcount = [self RB___numberOfSubviews]-1;
1044 BOOL ishor = [self isHorizontal];
1045 point = [self convertPoint:point fromView:view];
1046 for (i=0;i<divcount;i++) {
1047 NSRect divdr = dividers[i];
1048 OTHER(divdr.origin) -= delta;
1049 OTHER(divdr.size) += 2*delta;
1050 if ([self mouse:point inRect:divdr]) {
1057 // This method sets a flag to clear all fractions before adjusting.
1058 - (void)RB___setMustClearFractions {
1059 mustClearFractions = YES;
1062 // This local method asks the delegate if we should resize the trailing subview or the window
1063 // when a divider is dragged. Not called if we're inside an NSScrollView.
1064 - (BOOL)RB___shouldResizeWindowForDivider:(unsigned int)indx betweenView:(RBSplitSubview*)leading andView:(RBSplitSubview*)trailing willGrow:(BOOL)grow {
1065 if (!isInScrollView&&[delegate respondsToSelector:@selector(splitView:shouldResizeWindowForDivider:betweenView:andView:willGrow:)]) {
1066 return [delegate splitView:self shouldResizeWindowForDivider:indx betweenView:leading andView:trailing willGrow:grow];
1071 // This local method tries to expand the leading subview (which is assumed to be collapsed). Delta should be positive.
1072 - (void)RB___tryToExpandLeading:(RBSplitSubview*)leading divider:(unsigned int)indx trailing:(RBSplitSubview*)trailing delta:(float)delta {
1073 NSWindow* window = nil;
1074 NSView* document = nil;
1075 NSSize maxsize = NSMakeSize(WAYOUT,WAYOUT);
1076 NSRect frame = NSZeroRect;
1077 NSRect screen = NSMakeRect(0,0,WAYOUT,WAYOUT);
1079 // First we ask the delegate, if there's any, if the window should resize.
1080 BOOL dowin = ([self RB___shouldResizeWindowForDivider:indx betweenView:leading andView:trailing willGrow:YES]);
1082 // We initialize the other local variables only if we need them for the window.
1083 ishor = [self isHorizontal];
1084 document = [[self enclosingScrollView] documentView];
1086 frame = [document frame];
1088 window = [self window];
1089 frame = [window frame];
1090 maxsize = [window maxSize];
1091 screen = [[NSScreen mainScreen] visibleFrame];
1094 // The mouse has to move over half of the expanded size (plus hysteresis) and the expansion shouldn't
1095 // reduce the trailing subview to less than its minimum size (or grow the window beyond its maximum).
1096 float limit = [leading minDimension];
1097 float dimension = 0.0;
1099 float maxd = fMAX(0.0,(ishor?frame.origin.y-screen.origin.y:(screen.origin.x+screen.size.width)-(frame.origin.x+frame.size.width)));
1100 dimension = fMIN(DIM(maxsize)-DIM(frame.size),maxd);
1102 dimension = trailing?[trailing dimension]:WAYOUT;
1104 if (limit>dimension) {
1107 if (!dowin&&trailing) {
1108 limit += [trailing minDimension];
1109 if (limit>dimension) {
1110 // If the trailing subview is going below its minimum, we try to collapse it first.
1111 // However, we don't collapse if that would cause the leading subview to become larger than its maximum.
1112 if (([trailing canCollapse])&&(delta>(0.5+HYSTERESIS)*dimension)&&([leading maxDimension]<=dimension)) {
1113 delta = -[trailing RB___collapse];
1114 [leading changeDimensionBy:delta mayCollapse:NO move:NO];
1119 // The leading subview may be expanded normally.
1120 delta = -[leading changeDimensionBy:delta mayCollapse:NO move:NO];
1122 // If it does expand, we widen the window.
1123 DIM(frame.size) -= delta;
1125 DIM(frame.origin) += delta;
1128 [document setFrame:frame];
1129 [document setNeedsDisplay:YES];
1131 [window setFrame:frame display:YES];
1133 [self setMustAdjust];
1135 // If it does expand, we shorten the trailing subview.
1136 [trailing changeDimensionBy:delta mayCollapse:NO move:YES];
1140 // This local method tries to shorten the leading subview. Both subviews are assumed to be expanded.
1141 // delta should be negative. If always is NO, the subview will be shortened only if it might also be
1142 // collapsed; otherwise, it's shortened as much as possible.
1143 - (void)RB___tryToShortenLeading:(RBSplitSubview*)leading divider:(unsigned int)indx trailing:(RBSplitSubview*)trailing delta:(float)delta always:(BOOL)always {
1144 NSWindow* window = nil;
1145 NSView* document = nil;
1146 NSSize minsize = NSZeroSize;
1147 NSRect frame = NSZeroRect;
1149 // First we ask the delegate, if there's any, if the window should resize.
1150 BOOL dowin = ([self RB___shouldResizeWindowForDivider:indx betweenView:leading andView:trailing willGrow:NO]);
1152 // We initialize the other local variables only if we need them for the window.
1153 ishor = [self isHorizontal];
1154 document = [[self enclosingScrollView] documentView];
1156 frame = [document frame];
1158 window = [self window];
1159 frame = [window frame];
1160 minsize = [window minSize];
1163 // We avoid making the trailing subview larger than its maximum, or the window smaller than its minimum.
1166 limit = DIM(frame.size)-DIM(minsize);
1168 limit = trailing?([trailing maxDimension]-[trailing dimension]):WAYOUT;
1177 BOOL okl = limit>=[leading dimension];
1180 delta = -[leading changeDimensionBy:delta mayCollapse:okl move:NO];
1182 // Resize the window.
1183 DIM(frame.size) -= delta;
1185 DIM(frame.origin) += delta;
1188 [document setFrame:frame];
1189 [document setNeedsDisplay:YES];
1191 [window setFrame:frame display:YES];
1193 [self setMustAdjust];
1195 // Otherwise, resize trailing.
1196 [trailing changeDimensionBy:delta mayCollapse:NO move:YES];
1201 // This local method tries to shorten the trailing subview. Both subviews are assumed to be expanded.
1202 // delta should be positive. If always is NO, the subview will be shortened only if it might also be
1203 // collapsed; otherwise, it's shortened as much as possible.
1204 - (void)RB___tryToShortenTrailing:(RBSplitSubview*)trailing divider:(unsigned int)indx leading:(RBSplitSubview*)leading delta:(float)delta always:(BOOL)always {
1205 NSWindow* window = nil;
1206 NSView* document = nil;
1207 NSSize maxsize = NSMakeSize(WAYOUT,WAYOUT);
1208 NSRect frame = NSZeroRect;
1209 NSRect screen = NSMakeRect(0,0,WAYOUT,WAYOUT);
1211 // First we ask the delegate, if there's any, if the window should resize.
1212 BOOL dowin = ([self RB___shouldResizeWindowForDivider:indx betweenView:leading andView:trailing willGrow:YES]);
1214 // We initialize the other local variables only if we need them for the window.
1215 ishor = [self isHorizontal];
1216 document = [[self enclosingScrollView] documentView];
1218 frame = [document frame];
1220 window = [self window];
1221 frame = [window frame];
1222 maxsize = [window maxSize];
1223 screen = [[NSScreen mainScreen] visibleFrame];
1226 // We avoid making the leading subview larger than its maximum, or the window larger than its maximum.
1229 float maxd = fMAX(0.0,(ishor?frame.origin.y-screen.origin.y:(screen.origin.x+screen.size.width)-(frame.origin.x+frame.size.width)));
1230 limit = fMIN(DIM(maxsize)-DIM(frame.size),maxd);
1232 limit = [leading maxDimension]-[leading dimension];
1241 BOOL okl = dowin||(limit>=(trailing?[trailing dimension]:WAYOUT));
1244 // If we should resize the window, resize leading, then the window.
1245 delta = [leading changeDimensionBy:delta mayCollapse:NO move:NO];
1246 DIM(frame.size) += delta;
1248 DIM(frame.origin) -= delta;
1251 [document setFrame:frame];
1252 [document setNeedsDisplay:YES];
1254 [window setFrame:frame display:YES];
1256 [self setMustAdjust];
1258 // Otherwise, resize trailing, then leading.
1260 delta = -[trailing changeDimensionBy:-delta mayCollapse:okl move:YES];
1262 [leading changeDimensionBy:delta mayCollapse:NO move:NO];
1267 // This method tries to expand the trailing subview (which is assumed to be collapsed).
1268 - (void)RB___tryToExpandTrailing:(RBSplitSubview*)trailing leading:(RBSplitSubview*)leading delta:(float)delta {
1269 // The mouse has to move over half of the expanded size (plus hysteresis) and the expansion shouldn't
1270 // reduce the leading subview to less than its minimum size. If it does, we try to collapse it first.
1271 // However, we don't collapse if that would cause the trailing subview to become larger than its maximum.
1272 float limit = trailing?[trailing minDimension]:0.0;
1273 float dimension = [leading dimension];
1274 if (limit>dimension) {
1277 limit += [leading minDimension];
1278 if (limit>dimension) {
1279 if ([leading canCollapse]&&(-delta>(0.5+HYSTERESIS)*dimension)&&((trailing?[trailing maxDimension]:0.0)<=dimension)) {
1280 delta = -[leading RB___collapse];
1281 [trailing changeDimensionBy:delta mayCollapse:NO move:YES];
1285 // The trailing subview may be expanded normally. If it does expand, we shorten the leading subview.
1287 delta = -[trailing changeDimensionBy:-delta mayCollapse:NO move:YES];
1289 [leading changeDimensionBy:delta mayCollapse:NO move:NO];
1293 // This method is called by the mouseDown:method for every tracking event. It's separated out as it's
1294 // called from the Interface Builder palette in a slightly different way, and also if you have a
1295 // separate drag view designated by the delegate. You'll never need to call this directly.
1296 // theEvent is the event (which should be a NSLeftMouseDragged event).
1297 // where is the point where the original mouse-down happened, corrected for the current divider position,
1298 // and expressed in local coordinates.
1299 // base is an offset (x,y) applied to the mouse location (usually will be zero)
1300 // indx is the number of the divider that's being dragged.
1301 - (void)RB___trackMouseEvent:(NSEvent*)theEvent from:(NSPoint)where withBase:(NSPoint)base inDivider:(unsigned)indx {
1303 NSArray* subviews = [self RB___subviews];
1304 int subcount = [subviews count];
1306 // leading and trailing point at the subviews immediately leading and trailing the divider being tracked
1307 RBSplitSubview* leading = [subviews objectAtIndex:indx];
1308 RBSplitSubview* trailing = [subviews objectAtIndex:indx+1];
1309 // convert the mouse coordinates to apply to the same system the divider rects are in.
1310 NSPoint mouse = [self convertPoint:[theEvent locationInWindow] fromView:nil];
1313 result.x = mouse.x-where.x;
1314 result.y = mouse.y-where.y;
1315 // delta is the actual amount the mouse has moved in the relevant coordinate since the last event.
1316 BOOL ishor = [self isHorizontal];
1317 float delta = DIM(result);
1319 // Negative delta means the mouse is being moved left or upwards.
1320 // firstLeading will point at the first expanded subview to the left (or upwards) of the divider.
1321 // If there's none (all subviews are collapsed) it will point at the nearest subview.
1322 RBSplitSubview* firstLeading = leading;
1324 while (![firstLeading canShrink]) {
1326 firstLeading = leading;
1329 firstLeading = [subviews objectAtIndex:k];
1331 if (isInScrollView) {
1334 // If the trailing subview is collapsed, it might be expanded if some conditions are met.
1335 if ([trailing isCollapsed]) {
1336 [self RB___tryToExpandTrailing:trailing leading:firstLeading delta:delta];
1338 [self RB___tryToShortenLeading:firstLeading divider:indx trailing:trailing delta:delta always:YES];
1340 } else if (delta>0.0) {
1341 // Positive delta means the mouse is being moved right or downwards.
1342 // firstTrailing will point at the first expanded subview to the right (or downwards) of the divider.
1343 // If there's none (all subviews are collapsed) it will point at the nearest subview.
1344 RBSplitSubview* firstTrailing = nil;
1345 if (!isInScrollView) {
1346 firstTrailing = trailing;
1348 while (![firstTrailing canShrink]) {
1349 if (++k>=subcount) {
1350 firstTrailing = trailing;
1353 firstTrailing = [subviews objectAtIndex:k];
1356 // If the leading subview is collapsed, it might be expanded if some conditions are met.
1357 if ([leading isCollapsed]) {
1358 [self RB___tryToExpandLeading:leading divider:indx trailing:firstTrailing delta:delta];
1360 // The leading subview is not collapsed, so we try to shorten or even collapse it
1361 [self RB___tryToShortenTrailing:firstTrailing divider:indx leading:leading delta:delta always:YES];
1366 // This is called for nested RBSplitViews, to add the cursor rects for the two-axis thumbs.
1367 - (void)RB___addCursorRectsTo:(RBSplitView*)masterView forDividerRect:(NSRect)rect thickness:(float)delta {
1368 if (dividers&&[self divider]) {
1369 NSArray* subviews = [self RB___subviews];
1370 int divcount = [subviews count]-1;
1375 NSCursor* cursor = [RBSplitView cursor:RBSV2WayCursor];
1376 BOOL ishor = [self isHorizontal];
1377 // Loop over the divider rectangles, intersect them with the view's own, and add the thumb rectangle
1378 // to the containing split view.
1379 for (i=0;i<divcount;i++) {
1380 NSRect divdr = dividers[i];
1381 divdr.origin = [self convertPoint:divdr.origin toView:masterView];
1382 OTHER(divdr.origin) -= delta;
1383 OTHER(divdr.size) += 2*delta;
1384 divdr = NSIntersectionRect(divdr,rect);
1385 if (!NSIsEmptyRect(divdr)) {
1386 [masterView addCursorRect:divdr cursor:cursor];
1392 // This is called for nested RBSplitViews, to draw the two-axis thumbs.
1393 - (void)RB___drawDividersIn:(RBSplitView*)masterView forDividerRect:(NSRect)rect thickness:(float)delta {
1397 NSArray* subviews = [self RB___subviews];
1398 int divcount = [subviews count]-1;
1403 BOOL ishor = [self isHorizontal];
1404 // Get the outer split view's divider image.
1405 NSImage* image = [masterView divider];
1406 // Loop over the divider rectangles, intersect them with the view's own, and draw the thumb there.
1407 for (i=0;i<divcount;i++) {
1408 NSRect divdr = dividers[i];
1409 divdr.origin = [self convertPoint:divdr.origin toView:masterView];
1410 OTHER(divdr.origin) -= delta;
1411 OTHER(divdr.size) += 2*delta;
1412 divdr = NSIntersectionRect(divdr,rect);
1413 if (!NSIsEmptyRect(divdr)) {
1414 [masterView drawDivider:image inRect:divdr betweenView:nil andView:nil];
1419 // This is usually called from initWithCoder to ensure that the outermost RBSplitView is
1420 // properly adjusted when first displayed.
1421 - (void)RB___adjustOutermostIfNeeded {
1422 RBSplitView* sv = [self splitView];
1424 [sv RB___adjustOutermostIfNeeded];
1427 if (mustAdjust&&!isAdjusting) {
1428 [self adjustSubviews];
1432 // Here we try to keep all subviews adjusted in as natural a manner as possible, given the constraints.
1433 // The main idea is to always keep the RBSplitView completely covered by dividers and subviews, have at
1434 // least one expanded subview, and never make a subview smaller than its minimum dimension, or larger
1435 // than its maximum dimension.
1436 // We try to account for most unusual situations but this may fail under some circumstances. YMMV.
1437 - (void)RB___adjustSubviewsExcepting:(RBSplitSubview*)excepting {
1439 NSArray* subviews = [self RB___subviews];
1440 unsigned subcount = [subviews count];
1444 NSRect bounds = [self bounds];
1445 // Never adjust if the splitview itself is collapsed.
1446 if ((bounds.size.width<1.0)||(bounds.size.height<1.0)) {
1449 // Prevents adjustSubviews being called recursively, which unfortunately may happen otherwise.
1454 // Tell the delegate we're about to adjust subviews.
1455 if ([delegate respondsToSelector:@selector(willAdjustSubviews:)]) {
1456 [delegate willAdjustSubviews:self];
1457 bounds = [self bounds];
1459 unsigned divcount = subcount-1;
1461 // No dividers at all.
1467 // Try to allocate or resize if we already have a dividers array.
1468 unsigned long divsiz = sizeof(NSRect)*divcount;
1469 dividers = dividers?reallocf(dividers,divsiz):malloc(divsiz);
1474 // This C array of subviewCaches is used to cache the subview information.
1475 subviewCache* caches = malloc(sizeof(subviewCache)*subcount);
1476 double realsize = 0.0;
1477 double expsize = 0.0;
1478 float newsize = 0.0;
1479 float effsize = 0.0;
1483 BOOL ishor = [self isHorizontal];
1484 float divt = [self dividerThickness];
1485 // First we loop over subviews and cache their information.
1486 for (i=0;i<subcount;i++) {
1488 [[subviews objectAtIndex:i] RB___copyIntoCache:curr];
1490 // This is a counter to limit the outer loop to three iterations (six if excepting is non-nil).
1491 int sanity = excepting?-3:0;
1492 while (sanity++<3) {
1493 // We try to accomodate the exception for the first group of loops, turn it off for the second.
1497 // newsize is the available space for actual subviews (so dividers don't count). It will be an integer.
1498 // Same as calling [self RB___dimensionWithoutDividers].
1499 unsigned smallest = 0;
1500 float smalldim = -1.0;
1502 // Loop over subviews and sum the expanded dimensions into expsize, including fractions.
1503 // Also find the collapsed subview with the smallest minimum dimension.
1504 for (i=0;i<subcount;i++) {
1506 curr->constrain = NO;
1507 if (curr->size>0.0) {
1508 expsize += curr->size;
1509 if (!isInScrollView) {
1510 // ignore fractions if we're in a NSScrollView, however.
1511 expsize += curr->fraction;
1515 limit = [curr->sub minDimension];
1516 if (smalldim>limit) {
1522 // haveexp should be YES at this point. If not, all subviews were collapsed; can't have that, so we
1523 // expand the smallest subview (or the first, if all have the same minimum).
1524 curr = &caches[smallest];
1526 curr->size = [curr->sub minDimension];
1527 curr->fraction = 0.0;
1528 expsize += curr->size;
1530 if (isInScrollView) {
1531 // If we're inside an NSScrollView, we just grow the view to accommodate the subviews, instead of
1532 // the other way around.
1533 DIM(bounds.size) = expsize;
1536 // If the total dimension of all expanded subviews is less than 1.0 we set the dimension of the smallest
1537 // subview (which we're sure is expanded at this point) to the available space.
1538 newsize = DIM(bounds.size)-divcount*divt;
1540 curr->size = newsize;
1541 curr->fraction = 0.0;
1544 // Loop over the subviews and check if they're within the limits after scaling. We also recalculate the
1545 // exposed size and repeat until no more subviews hit the constraints during that loop.
1547 effsize = newsize;// we're caching newsize here, this is an integer.
1549 // scale is the scalefactor by which all views should be scaled - assuming none have constraints.
1550 // It's a double to (hopefully) keep rounding errors small enough for all practical purposes.
1551 double scale = newsize/expsize;
1555 for (i=0;i<subcount;i++) {
1556 // Loop over the cached subview info.
1558 if (curr->size>0.0) {
1559 // Check non-collapsed subviews only.
1560 if (!curr->constrain) {
1561 // Check non-constrained subviews only; calculate the proposed new size.
1562 float cursize = (curr->size+curr->fraction)*scale;
1563 // Check if we hit a limit. limit will contain either the max or min dimension, whichever was hit.
1564 if (([curr->sub RB___animationData:NO resize:NO]&&((limit = curr->size)>=0.0))||
1565 ((curr->sub==excepting)&&((limit = [curr->sub dimension])>0.0))||
1566 (cursize<(limit = [curr->sub minDimension]))||
1567 (cursize>(limit = [curr->sub maxDimension]))) {
1568 // If we hit a limit, we mark the view and set to repeat the loop; non-constrained subviews will
1569 // have to be recalculated.
1570 curr->constrain = constrained = YES;
1571 // We set the new size to the limit we hit, and subtract it from the total size to be subdivided.
1573 curr->fraction = 0.0;
1576 // If we didn't hit a limit, we round the size to the nearest integer and recalculate the fraction.
1577 double rem = fmod(cursize,1.0);
1584 curr->fraction = rem;
1586 // We store the new size in the cache.
1587 curr->size = cursize;
1589 // And add the full size with fraction to the actual sum of all expanded subviews.
1590 realsize += curr->size+curr->fraction;
1593 // At this point, newsize will be the sum of the new dimensions of non-constrained views.
1594 // expsize will be the sum of the recalculated dimensions of the same views, if any.
1595 // We repeat the loop if any view has been recently constrained, and if there are any
1596 // unconstrained views left.
1597 } while (constrained&&(expsize>0.0));
1598 // At this point, the difference between realsize and effsize should be less than 1 pixel.
1599 // realsize is the total size of expanded subviews as recalculated above, and
1600 // effsize is the value realsize should have.
1601 limit = realsize-effsize;
1603 // If realsize is larger than effsize by 1 pixel or more, we will need to collapse subviews to make room.
1604 // This in turn might expand previously collapsed subviews. So, we'll try collapsing constrained subviews
1605 // until we're back into range, and then recalculate everything from the beginning.
1606 for (i=0;i<subcount;i++) {
1608 if (curr->constrain&&(curr->sub!=excepting)&&([curr->sub RB___animationData:NO resize:NO]==nil)&&[curr->sub canCollapse]) {
1609 realsize -= curr->size;
1614 if ((realsize-effsize)<1.0) {
1619 } else if (limit<=-1.0) {
1620 // If realsize is smaller than effsize by 1 pixel or more, we will need to expand subviews.
1621 // This in turn might collapse previously expanded subviews. So, we'll try expanding collapsed subviews
1622 // until we're back into range, and then recalculate everything from the beginning.
1623 for (i=0;i<subcount;i++) {
1625 if (curr->size<=0.0) {
1626 curr->size = [curr->sub minDimension];
1627 curr->fraction = 0.0;
1628 realsize += curr->size;
1629 if ((realsize-effsize)>-1.0) {
1635 // The difference is less than 1 pixel, meaning that in all probability our calculations are
1636 // exact or off by at most one pixel after rounding, so we break the loop here.
1640 // After passing through the outer loop a few times, the frames may still be wrong, but there's nothing
1641 // else we can do about it. You probably should avoid this by some other means like setting a minimum
1642 // or maximum size for the window, for instance, or leaving at least one unlimited subview.
1644 // newframe is used to reset all subview frames. Subviews always fill the entire RBSplitView along the
1645 // current orientation.
1646 NSRect newframe = NSMakeRect(0.0,0.0,bounds.size.width,bounds.size.height);
1647 // We now loop over the subviews yet again and set the definite frames, also recalculating the
1648 // divider rectangles as we go along, and collapsing and expanding subviews whenever requested.
1649 RBSplitSubview* last = nil;
1650 // And we make a note if there's any nested RBSplitView.
1651 int nested = NSNotFound;
1652 newsize = DIM(bounds.size)-divcount*divt;
1653 for (i=0;i<subcount;i++) {
1655 // If we have a nested split view store its index.
1656 if ((nested==NSNotFound)&&([curr->sub asSplitView]!=nil)) {
1659 // Adjust the subview to the correct origin and resize it to fit into the "other" dimension.
1660 curr->rect.origin = newframe.origin;
1661 OTHER(curr->rect.size) = OTHER(newframe.size);
1662 DIM(curr->rect.size) = curr->size;
1663 // Clear fractions for expanded subviews if requested.
1664 if ((curr->size>0.0)&&mustClearFractions) {
1665 curr->fraction = 0.0;
1667 // Ask the subview to do the actual moving/resizing etc. from the cache.
1668 [curr->sub RB___updateFromCache:curr withTotalDimension:effsize];
1669 // Step to the next position and record the subview if it's not collapsed.
1670 DIM(newframe.origin) += curr->size;
1671 if (curr->size>0.0) {
1675 // We're at the last subview, so we now check if the actual and calculated dimensions
1677 float remain = DIM(bounds.size)-DIM(newframe.origin);
1678 if (last&&(fabsf(remain)>0.0)) {
1679 // We'll resize the last expanded subview to whatever it takes to squeeze within the frame.
1680 // Normally the change should be at most one pixel, but if too many subviews were constrained,
1681 // this may be a large value, and the last subview may be resized beyond its constraints;
1682 // there's nothing else to do at this point.
1683 newframe = [last frame];
1684 DIM(newframe.size) += remain;
1685 [last RB___setFrameSize:newframe.size withFraction:[last RB___fraction]-remain];
1686 // And we loop back over the rightmost dividers (if any) to adjust their offsets.
1687 while ((i>0)&&(last!=[subviews objectAtIndex:i])) {
1688 DIM(dividers[--i].origin) += remain;
1693 // For any but the last subview, we just calculate the divider frame.
1694 DIM(newframe.size) = divt;
1695 dividers[i] = newframe;
1696 DIM(newframe.origin) += divt;
1699 // We resize our frame at this point, if we're inside an NSScrollView.
1700 if (isInScrollView) {
1701 [super setFrameSize:bounds.size];
1703 // If there was at least one nested RBSplitView, we loop over the subviews and adjust those that need it.
1704 for (i=nested;i<subcount;i++) {
1706 RBSplitView* sv = [curr->sub asSplitView];
1707 if ([sv mustAdjust]) {
1708 [sv adjustSubviews];
1711 // Free the cache array.
1713 // Clear cursor rects.
1715 mustClearFractions = NO;
1716 [[self window] invalidateCursorRectsForView:self];
1717 // Save the state for all subviews.
1719 [self saveState:NO];
1721 // If we're a nested RBSplitView, also invalidate cursorRects for the superview.
1722 RBSplitView* sv = [self couplingSplitView];
1724 [[self window] invalidateCursorRectsForView:sv];
1727 // Tell the delegate we're finished.
1728 if ([delegate respondsToSelector:@selector(didAdjustSubviews:)]) {
1729 [delegate didAdjustSubviews:self];