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.
9 #ifdef MM_ENABLE_PLUGINS
11 #import "RBSplitView.h"
12 #import "RBSplitViewPrivateDefines.h"
14 // Please don't remove this copyright notice!
15 static const unsigned char RBSplitView_Copyright[] __attribute__ ((used)) =
16 "RBSplitView 1.1.4 Copyright(c)2004-2006 by Rainer Brockerhoff <rainer@brockerhoff.net>.";
18 // This vector keeps currently used cursors. nil means the default cursor.
19 static NSCursor* cursors[RBSVCursorTypeCount] = {nil};
21 // Our own fMIN and fMAX
22 static inline float fMIN(float a,float b) {
26 static inline float fMAX(float a,float b) {
30 @implementation RBSplitView
32 // These class methods get and set the cursor used for each type.
33 // Pass in nil to reset to the default cursor for that type.
34 + (NSCursor*)cursor:(RBSVCursorType)type {
35 if ((type>=0)&&(type<RBSVCursorTypeCount)) {
36 NSCursor* result = cursors[type];
41 case RBSVHorizontalCursor:
42 return [NSCursor resizeUpDownCursor];
43 case RBSVVerticalCursor:
44 return [NSCursor resizeLeftRightCursor];
46 return [NSCursor openHandCursor];
48 return [NSCursor closedHandCursor];
53 return [NSCursor currentCursor];
56 + (void)setCursor:(RBSVCursorType)type toCursor:(NSCursor*)cursor {
57 if ((type>=0)&&(type<RBSVCursorTypeCount)) {
58 [cursors[type] release];
59 cursors[type] = [cursor retain];
63 // This class method clears the saved state(s) for a given autosave name from the defaults.
64 + (void)removeStateUsingName:(NSString*)name {
66 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
67 [defaults removeObjectForKey:[[self class] defaultsKeyForName:name isHorizontal:NO]];
68 [defaults removeObjectForKey:[[self class] defaultsKeyForName:name isHorizontal:YES]];
72 // This class method returns the actual key used to store autosave data in the defaults.
73 + (NSString*)defaultsKeyForName:(NSString*)name isHorizontal:(BOOL)orientation {
74 return [NSString stringWithFormat:@"RBSplitView %@ %@",orientation?@"H":@"V",name];
77 // This pair of methods gets and sets the autosave name, which allows restoring the subview's
78 // state from the user defaults.
79 // We take care not to allow nil autosaveNames.
80 - (NSString*)autosaveName {
84 // Sets the autosaveName; this should be a unique key to be used to store the subviews' proportions
85 // in the user defaults. Default is @"", which doesn't save anything. Set flag to YES to set
86 // unique names for nested subviews. You are responsible for avoiding duplicates; avoid using
87 // the characters '[' and ']' in autosaveNames.
88 - (void)setAutosaveName:(NSString*)aString recursively:(BOOL)flag {
90 if ((clear = ![aString length])) {
93 [RBSplitView removeStateUsingName:autosaveName];
94 [autosaveName autorelease];
95 autosaveName = [aString retain];
97 NSArray* subviews = [self subviews];
98 int subcount = [subviews count];
100 for (i=0;i<subcount;i++) {
101 RBSplitView* sv = [[subviews objectAtIndex:i] asSplitView];
103 NSString* subst = clear?@"":[aString stringByAppendingFormat:@"[%d]",i];
104 [sv setAutosaveName:subst recursively:YES];
110 // Saves the current state of the subviews if there's a valid autosave name set. If the argument
111 // is YES, it's then also called recursively for nested RBSplitViews. Returns YES if successful.
112 // You must call restoreState explicity at least once before saveState will begin working.
113 - (BOOL)saveState:(BOOL)recurse {
114 // Saving the state is also disabled while dragging.
115 if (canSaveState&&![self isDragging]&&[autosaveName length]) {
116 [[NSUserDefaults standardUserDefaults] setObject:[self stringWithSavedState] forKey:[[self class] defaultsKeyForName:autosaveName isHorizontal:[self isHorizontal]]];
118 NSEnumerator* enumerator = [[self subviews] objectEnumerator];
120 while ((sub = [enumerator nextObject])) {
121 [[sub asSplitView] saveState:YES];
129 // Restores the saved state of the subviews if there's a valid autosave name set. If the argument
130 // is YES, it's also called recursively for nested RBSplitViews. Returns YES if successful.
131 // It's good policy to call adjustSubviews immediately after calling restoreState.
132 - (BOOL)restoreState:(BOOL)recurse {
134 if ([autosaveName length]) {
135 result = [self setStateFromString:[[NSUserDefaults standardUserDefaults] stringForKey:[[self class] defaultsKeyForName:autosaveName isHorizontal:[self isHorizontal]]]];
136 if (result&&recurse) {
137 NSEnumerator* enumerator = [[self subviews] objectEnumerator];
139 while ((sub = [enumerator nextObject])) {
140 [[sub asSplitView] restoreState:YES];
148 // Returns an array with complete state information for the receiver and all subviews, taking
149 // nesting into account. Don't store this array in a file, as its format might change in the
150 // future; this is for taking a state snapshot and later restoring it with setStatesFromArray.
151 - (NSArray*)arrayWithStates {
152 NSMutableArray* array = [NSMutableArray array];
153 [array addObject:[self stringWithSavedState]];
154 NSEnumerator* enumerator = [[self subviews] objectEnumerator];
156 while ((sub = [enumerator nextObject])) {
157 RBSplitView* suv = [sub asSplitView];
159 [array addObject:[suv arrayWithStates]];
161 [array addObject:[NSNull null]];
167 // Restores the state of the receiver and all subviews. The array must have been produced by a
168 // previous call to arrayWithStates. Returns YES if successful. This will fail if you have
169 // added or removed subviews in the meantime!
170 // You need to call adjustSubviews after calling this.
171 - (BOOL)setStatesFromArray:(NSArray*)array {
172 NSArray* subviews = [self subviews];
173 unsigned int count = [array count];
174 if (count==([subviews count]+1)) {
175 NSString* me = [array objectAtIndex:0];
176 if ([me isKindOfClass:[NSString class]]) {
177 if ([self setStateFromString:me]) {
179 for (i=1;i<count;i++) {
180 NSArray* item = [array objectAtIndex:i];
181 RBSplitView* suv = [[subviews objectAtIndex:i-1] asSplitView];
182 if ([item isKindOfClass:[NSArray class]]==(suv!=nil)) {
183 if (suv&&![suv setStatesFromArray:item]) {
197 // Returns a string encoding the current state of all direct subviews. Does not check for nesting.
198 // The string contains the number of direct subviews, then the dimension for each subview (which will
199 // be negative for collapsed subviews), all separated by blanks.
200 - (NSString*)stringWithSavedState {
201 NSArray* subviews = [self subviews];
202 NSMutableString* result = [NSMutableString stringWithFormat:@"%d",[subviews count]];
203 NSEnumerator* enumerator = [subviews objectEnumerator];
205 while ((sub = [enumerator nextObject])) {
206 double size = [sub dimension];
207 if ([sub isCollapsed]) {
210 size += +[sub RB___fraction];
212 [result appendFormat:[sub isHidden]?@" %gH":@" %g",size];
217 // Readjusts all direct subviews according to the encoded string parameter.
218 // The number of subviews must match. Returns YES if successful. Does not check for nesting.
219 - (BOOL)setStateFromString:(NSString*)aString {
220 if ([aString length]) {
221 NSArray* parts = [aString componentsSeparatedByString:@" "];
222 NSArray* subviews = [self subviews];
223 int subcount = [subviews count];
224 int k = [parts count];
225 if ((k-->1)&&([[parts objectAtIndex:0] intValue]==subcount)&&(k==subcount)) {
227 NSRect frame = [self frame];
228 BOOL ishor = [self isHorizontal];
229 for (i=0;i<subcount;i++) {
230 NSString* part = [parts objectAtIndex:i+1];
231 BOOL hidden = [part hasSuffix:@"H"];
232 double size = [part doubleValue];
233 BOOL negative = size<=0.0;
240 DIM(frame.size) = size;
241 RBSplitSubview* sub = [subviews objectAtIndex:i];
242 [sub RB___setFrame:frame withFraction:fract notify:NO];
246 [sub RB___setHidden:hidden];
248 [self setMustAdjust];
255 // This is the designated initializer for creating RBSplitViews programmatically. You can set the
256 // divider image and other parameters afterwards.
257 - (id)initWithFrame:(NSRect)frame {
258 self = [super initWithFrame:frame];
265 [self setVertical:YES];
266 [self setDivider:nil];
267 [self setAutosaveName:nil recursively:NO];
268 [self setBackground:nil];
273 // This convenience initializer adds any number of subviews and adjusts them proportionally.
274 - (id)initWithFrame:(NSRect)frame andSubviews:(unsigned)count {
275 self = [self initWithFrame:frame];
278 [self addSubview:[[[RBSplitSubview alloc] initWithFrame:frame] autorelease]];
280 [self setMustAdjust];
285 // Frees retained objects when going away.
290 [autosaveName release];
292 [background release];
296 // Sets and gets the coupling between the view and its containing RBSplitView (if any). Coupled
297 // RBSplitViews take some parameters, such as divider images, from the containing view. The default
298 // is for nested RBSplitViews is YES; however, isCoupled returns NO if we're not nested.
299 - (void)setCoupled:(BOOL)flag {
300 if (flag!=isCoupled) {
302 // If we've just been uncoupled and there's no divider image, we copy it from the containing view.
303 if (!isCoupled&&!divider) {
304 [self setDivider:[[self splitView] divider]];
306 [self setMustAdjust];
311 return isCoupled&&([super splitView]!=nil);
314 // This returns the containing splitview if they are coupled. It's guaranteed to return a RBSplitView or nil.
315 - (RBSplitView*)couplingSplitView {
316 return isCoupled?[super couplingSplitView]:nil;
319 // This returns self.
320 - (RBSplitView*)asSplitView {
324 // This return self if we're really coupled to the owning splitview.
325 - (RBSplitView*)coupledSplitView {
326 return [self isCoupled]?self:nil;
329 // We always return NO, but do special handling in RBSplitSubview's mouseDown: method.
330 - (BOOL)mouseDownCanMoveWindow {
334 // RBSplitViews must be flipped to work properly for horizontal dividers. As the subviews are never
335 // flipped, this won't make your life harder.
340 // Call this method to make sure that the subviews and divider rectangles are recalculated
341 // properly before display.
342 - (void)setMustAdjust {
344 [self setNeedsDisplay:YES];
347 // Returns YES if there's a pending adjustment.
352 // Returns YES if we're in a dragging loop.
357 // Returns YES if the view is directly contained in an NSScrollView.
358 - (BOOL)isInScrollView {
359 return isInScrollView;
362 // This pair of methods allows you to move the dividers for background windows while holding down
363 // the command key, without bringing the window to the foreground.
364 - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
365 return ([theEvent modifierFlags]&NSCommandKeyMask)==0;
368 - (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent*)theEvent {
369 return ([theEvent modifierFlags]&NSCommandKeyMask)!=0;
372 // These 3 methods handle view background colors and opacity. The default is the window background.
373 // Pass nil or a completely transparent color to setBackground to use transparency. If you set any
374 // other background color, it will completely fill the RBSplitView (including subviews and dividers).
375 // The view will be considered opaque only if its alpha is equal to 1.0.
376 // For a nested, coupled RBSplitView, background and opacity are copied from the containing RBSplitView,
377 // and setting the background has no effect.
378 - (NSColor*)background {
379 RBSplitView* sv = [self couplingSplitView];
380 return sv?[sv background]:background;
383 - (void)setBackground:(NSColor*)color {
384 if (![self couplingSplitView]) {
385 [background autorelease];
386 background = color?([color alphaComponent]>0.0?[color retain]:nil):nil;
387 [self setNeedsDisplay:YES];
392 RBSplitView* sv = [self couplingSplitView];
393 return sv?[sv isOpaque]:(background&&([background alphaComponent]>=1.0));
396 // This will make debugging a little easier by appending the state string to the
397 // default description.
398 - (NSString*)description {
399 return [NSString stringWithFormat:@"%@ {%@}",[super description],[self stringWithSavedState]];
402 // The following 3 methods handle divider orientation. The actual stored trait is horizontality,
403 // but verticality is used for setting to conform to the NSSplitView convention.
404 // For a nested RBSplitView, orientation is perpendicular to the containing RBSplitView, and
405 // setting it has no effect. This parameter is not affected by coupling.
406 // After changing the orientation you may want to restore the state with restoreState:.
407 - (BOOL)isHorizontal {
408 RBSplitView* sv = [self splitView];
409 return sv?[sv isVertical]:isHorizontal;
413 return 1-[self isHorizontal];
416 - (void)setVertical:(BOOL)flag {
417 if (![self splitView]&&(isHorizontal!=!flag)) {
418 BOOL ishor = isHorizontal = !flag;
419 NSSize size = divider?[divider size]:NSZeroSize;
420 [self setDividerThickness:DIM(size)];
421 [self setMustAdjust];
425 // Returns the subview which a given identifier.
426 - (RBSplitSubview*)subviewWithIdentifier:(NSString*)anIdentifier {
427 NSEnumerator* enumerator = [[self subviews] objectEnumerator];
428 RBSplitSubview* subview;
429 while ((subview = [enumerator nextObject])) {
430 if ([anIdentifier isEqualToString:[subview identifier]]) {
437 // Returns the subview at a given position
438 - (RBSplitSubview*)subviewAtPosition:(unsigned)position {
439 NSArray* subviews = [super subviews];
440 unsigned int subcount = [subviews count];
441 if (position<subcount) {
442 return [subviews objectAtIndex:position];
447 // This pair of methods gets and sets the delegate object. Delegates aren't retained.
452 - (void)setDelegate:(id)anObject {
456 // This pair of methods gets and sets the divider image. Setting the image automatically adjusts the
457 // divider thickness. A nil image means a 0-pixel wide divider, unless you set a thickness explicitly.
458 // For a nested RBSplitView, the divider is copied from the containing RBSplitView, and
459 // setting it has no effect. The returned image is always flipped.
460 - (NSImage*)divider {
461 RBSplitView* sv = [self couplingSplitView];
462 return sv?[sv divider]:divider;
465 - (void)setDivider:(NSImage*)image {
466 if (![self couplingSplitView]) {
467 [divider autorelease];
468 if ([image isFlipped]) {
469 // If the image is flipped, we just retain it.
470 divider = [image retain];
472 // if the image isn't flipped, we copy the image instead of retaining it, and flip it.
473 divider = [image copy];
474 [divider setFlipped:YES];
476 // We set the thickness to 0.0 so the image dimension will prevail.
477 [self setDividerThickness:0.0];
478 [self setMustAdjust];
482 // This pair of methods gets and sets the divider thickness. It should be an integer value and at least
483 // 0.0, so we make sure. Set it to 0.0 to make the image dimensions prevail.
484 - (float)dividerThickness {
485 if (dividerThickness>0.0) {
486 return dividerThickness;
488 NSImage* divdr = [self divider];
490 NSSize size = [divdr size];
491 BOOL ishor = [self isHorizontal];
497 - (void)setDividerThickness:(float)thickness {
498 float t = fMAX(0.0,floorf(thickness));
499 if ((int)dividerThickness!=(int)t) {
500 dividerThickness = t;
501 [self setMustAdjust];
505 // These three methods add subviews. If aView isn't a RBSplitSubview, one is automatically inserted above
506 // it, and aView's frame and resizing mask is set to occupy the entire RBSplitSubview.
507 - (void)addSubview:(NSView*)aView {
508 if ([aView isKindOfClass:[RBSplitSubview class]]) {
509 [super addSubview:aView];
511 [aView setFrameOrigin:NSZeroPoint];
512 RBSplitSubview* sub = [[[RBSplitSubview alloc] initWithFrame:[aView frame]] autorelease];
513 [aView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
514 [sub addSubview:aView];
515 [super addSubview:sub];
517 [self setMustAdjust];
520 - (void)addSubview:(NSView*)aView positioned:(NSWindowOrderingMode)place relativeTo:(NSView*)otherView {
521 if ([aView isKindOfClass:[RBSplitSubview class]]) {
522 [super addSubview:aView positioned:place relativeTo:otherView];
524 [aView setFrameOrigin:NSZeroPoint];
525 RBSplitSubview* sub = [[[RBSplitSubview alloc] initWithFrame:[aView frame]] autorelease];
526 [aView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
527 [sub addSubview:aView];
528 [super addSubview:sub positioned:place relativeTo:otherView];
529 [aView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
531 [self setMustAdjust];
534 - (void)addSubview:(NSView*)aView atPosition:(unsigned)position {
535 RBSplitSubview* suv = [self subviewAtPosition:position];
537 [self addSubview:aView positioned:NSWindowBelow relativeTo:suv];
539 [self addSubview:aView];
543 // This keeps the isInScrollView flag up-to-date.
544 - (void)viewDidMoveToSuperview {
545 [super viewDidMoveToSuperview];
546 NSScrollView* scrollv = [self enclosingScrollView];
547 isInScrollView = scrollv?[scrollv documentView]==self:NO;
550 // This makes sure the subviews are adjusted after a subview is removed.
551 - (void)willRemoveSubview:(NSView*)subview {
552 if ([subview respondsToSelector:@selector(RB___stopAnimation)]) {
553 [(RBSplitSubview*)subview RB___stopAnimation];
555 [super willRemoveSubview:subview];
556 [self setMustAdjust];
559 // RBSplitViews never resize their subviews automatically.
560 - (BOOL)autoresizesSubviews {
564 // This adjusts the subviews when the size is set. setFrame: calls this, so all is well. It calls
565 // the delegate if implemented.
566 - (void)setFrameSize:(NSSize)size {
567 NSSize oldsize = [self frame].size;
568 [super setFrameSize:size];
569 [self setMustAdjust];
570 if ([delegate respondsToSelector:@selector(splitView:wasResizedFrom:to:)]) {
571 BOOL ishor = [self isHorizontal];
572 float olddim = DIM(oldsize);
573 float newdim = DIM(size);
574 // The delegate is not called if the dimension hasn't changed.
575 if (((int)newdim!=(int)olddim)) {
576 [delegate splitView:self wasResizedFrom:olddim to:newdim];
579 // We adjust the subviews only if the delegate didn't.
580 if (mustAdjust&&!isAdjusting) {
581 [self adjustSubviews];
585 // This method handles dragging and double-clicking dividers with the mouse. While dragging, the
586 // "closed hand" cursor is shown. Double clicks are handled separately. Nothing will happen if
587 // no divider image is set.
588 - (void)mouseDown:(NSEvent*)theEvent {
592 NSArray* subviews = [self RB___subviews];
593 int subcount = [subviews count];
597 // If the mousedown was in an alternate dragview, or if there's no divider image, handle it in RBSplitSubview.
598 if ((actDivider<NSNotFound)||![self divider]) {
599 [super mouseDown:theEvent];
602 NSPoint where = [self convertPoint:[theEvent locationInWindow] fromView:nil];
603 BOOL ishor = [self isHorizontal];
606 // Loop over the divider rectangles.
607 for (i=0;i<subcount;i++) {
608 NSRect* divdr = ÷rs[i];
609 if ([self mouse:where inRect:*divdr]) {
610 // leading points at the subview immediately leading the divider being tracked.
611 RBSplitView* leading = [subviews objectAtIndex:i];
612 // trailing points at the subview immediately trailing the divider being tracked.
613 RBSplitView* trailing = [subviews objectAtIndex:i+1];
614 if ([delegate respondsToSelector:@selector(splitView:shouldHandleEvent:inDivider:betweenView:andView:)]) {
615 if (![delegate splitView:self shouldHandleEvent:theEvent inDivider:i betweenView:leading andView:trailing]) {
619 // If it's a double click, try to expand or collapse one of the neighboring subviews.
620 if ([theEvent clickCount]>1) {
621 // If both are collapsed, we do nothing. If one of them is collapsed, we try to expand it.
622 if ([trailing isCollapsed]) {
623 if (![leading isCollapsed]) {
624 [self RB___tryToExpandTrailing:trailing leading:leading delta:-[trailing dimension]];
627 if ([leading isCollapsed]) {
628 [self RB___tryToExpandLeading:leading divider:i trailing:trailing delta:[leading dimension]];
630 // If neither are collapsed, we check if both are collapsible.
631 BOOL lcan = [leading canCollapse];
632 BOOL tcan = [trailing canCollapse];
633 float ldim = [leading dimension];
635 // If both are collapsible, we try asking the delegate.
636 if ([delegate respondsToSelector:@selector(splitView:collapseLeading:orTrailing:)]) {
637 RBSplitSubview* sub = [delegate splitView:self collapseLeading:leading orTrailing:trailing];
638 // If the delegate returns nil, neither view will collapse.
640 tcan = sub==trailing;
642 // Otherwise we try collapsing the smaller one. If they're equal, the trailing one will be collapsed.
643 lcan = ldim<[trailing dimension];
646 // At this point, we'll try to collapse the leading subview.
648 [self RB___tryToShortenLeading:leading divider:i trailing:trailing delta:-ldim always:NO];
650 // If the leading subview didn't collapse for some reason, we try to collapse the trailing one.
651 if (!mustAdjust&&tcan) {
652 [self RB___tryToShortenTrailing:trailing divider:i leading:leading delta:[trailing dimension] always:NO];
656 // If the subviews have changed, clear the fractions, adjust and redisplay
658 [self RB___setMustClearFractions];
659 RBSplitView* sv = [self splitView];
660 [sv?sv:self adjustSubviews];
664 // Single click; record the offsets within the divider rectangle and check for nesting.
665 float divt = [self dividerThickness];
666 float offset = DIM(where)-DIM(divdr->origin);
667 // Check if the leading subview is nested and if yes, if one of its two-axis thumbs was hit.
668 int ldivdr = NSNotFound;
670 NSPoint lwhere = where;
671 NSRect lrect = NSZeroRect;
672 if ((leading = [leading coupledSplitView])) {
673 ldivdr = [leading RB___dividerHitBy:lwhere relativeToView:self thickness:divt];
674 if (ldivdr!=NSNotFound) {
675 lrect = [leading RB___dividerRect:ldivdr relativeToView:self];
676 loffset = OTHER(lwhere)-OTHER(lrect.origin);
679 // Check if the trailing subview is nested and if yes, if one of its two-axis thumbs was hit.
680 int tdivdr = NSNotFound;
682 NSPoint twhere = where;
683 NSRect trect = NSZeroRect;
684 if ((trailing = [trailing coupledSplitView])) {
685 tdivdr = [trailing RB___dividerHitBy:twhere relativeToView:self thickness:divt];
686 if (tdivdr!=NSNotFound) {
687 trect = [trailing RB___dividerRect:tdivdr relativeToView:self];
688 toffset = OTHER(twhere)-OTHER(trect.origin);
691 // Now we loop handling mouse events until we get a mouse up event, while showing the drag cursor.
692 [[RBSplitView cursor:RBSVDragCursor] push];
693 [self RB___setDragging:YES];
694 while ((theEvent = [NSApp nextEventMatchingMask:NSLeftMouseDownMask|NSLeftMouseDraggedMask|NSLeftMouseUpMask untilDate:[NSDate distantFuture] inMode:NSEventTrackingRunLoopMode dequeue:YES])&&([theEvent type]!=NSLeftMouseUp)) {
695 // Set up a local autorelease pool for the loop to prevent buildup of temporary objects.
696 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
697 NSDisableScreenUpdates();
698 // Track the mouse along the main coordinate.
699 [self RB___trackMouseEvent:theEvent from:where withBase:NSZeroPoint inDivider:i];
700 if (ldivdr!=NSNotFound) {
701 // Track any two-axis thumbs for the leading nested RBSplitView.
702 [leading RB___trackMouseEvent:theEvent from:[self convertPoint:lwhere toView:leading] withBase:NSZeroPoint inDivider:ldivdr];
704 if (tdivdr!=NSNotFound) {
705 // Track any two-axis thumbs for the trailing nested RBSplitView.
706 [trailing RB___trackMouseEvent:theEvent from:[self convertPoint:twhere toView:trailing] withBase:NSZeroPoint inDivider:tdivdr];
708 if (mustAdjust||[leading mustAdjust]||[trailing mustAdjust]) {
709 // The mouse was dragged and the subviews changed, so we must redisplay, as
710 // several divider rectangles may have changed.
711 RBSplitView* sv = [self splitView];
712 [sv?sv:self adjustSubviews];
714 divdr = ÷rs[i];
715 // Adjust to the new cursor coordinates.
716 DIM(where) = DIM(divdr->origin)+offset;
717 if ((ldivdr!=NSNotFound)&&![leading isCollapsed]) {
718 // Adjust for the leading nested RBSplitView's thumbs while it's not collapsed.
719 lrect = [leading RB___dividerRect:ldivdr relativeToView:self];
720 OTHER(lwhere) = OTHER(lrect.origin)+loffset;
722 if ((tdivdr!=NSNotFound)&&![trailing isCollapsed]) {
723 // Adjust for the trailing nested RBSplitView's thumbs while it's not collapsed.
724 trect = [trailing RB___dividerRect:tdivdr relativeToView:self];
725 OTHER(twhere) = OTHER(trect.origin)+toffset;
728 NSEnableScreenUpdates();
731 [self RB___setDragging:NO];
732 // Redisplay the previous cursor.
739 // This will be called before the view will be redisplayed, so we adjust subviews if necessary.
740 - (BOOL)needsDisplay {
741 if (mustAdjust&&!isAdjusting) {
742 [self adjustSubviews];
745 return [super needsDisplay];
748 // We implement awakeFromNib to restore the state. This works if an autosaveName is set in the nib.
749 - (void)awakeFromNib {
750 if ([RBSplitSubview instancesRespondToSelector:@selector(awakeFromNib)]) {
751 [super awakeFromNib];
753 if (![self splitView]) {
754 [self restoreState:YES];
758 // We check if subviews must be adjusted before redisplaying programmatically.
760 if (mustAdjust&&!isAdjusting) {
761 [self adjustSubviews];
766 // This method draws the divider rectangles and then the two-axis thumbs if there are any.
767 - (void)drawRect:(NSRect)rect {
768 [super drawRect:rect];
772 NSArray* subviews = [self RB___subviews];
773 int subcount = [subviews count];
774 // Return if there are no dividers to draw.
780 // Cache the divider image.
781 NSImage* divdr = [self divider];
782 float divt = [self dividerThickness];
783 // Loop over the divider rectangles.
784 for (i=0;i<subcount;i++) {
785 // Check if we need to draw this particular divider.
786 if ([self needsToDrawRect:dividers[i]]) {
787 RBSplitView* leading = [subviews objectAtIndex:i];
788 RBSplitView* trailing = [subviews objectAtIndex:i+1];
789 BOOL lexp = divdr?![leading isCollapsed]:NO;
790 BOOL texp = divdr?![trailing isCollapsed]:NO;
791 // We don't draw the divider image if either of the neighboring subviews is a non-collapsed
792 // nested split view.
793 BOOL nodiv = (lexp&&[leading coupledSplitView])||(texp&&[trailing coupledSplitView]);
794 [self drawDivider:nodiv?nil:divdr inRect:dividers[i] betweenView:leading andView:trailing];
796 // Draw the corresponding two-axis thumbs if the leading view is a nested RBSplitView.
797 if ((leading = [leading coupledSplitView])&&lexp) {
798 [leading RB___drawDividersIn:self forDividerRect:dividers[i] thickness:divt];
800 // Draw the corresponding two-axis thumbs if the trailing view is a nested RBSplitView.
801 if ((trailing = [trailing coupledSplitView])&&texp) {
802 [trailing RB___drawDividersIn:self forDividerRect:dividers[i] thickness:divt];
809 // This method draws dividers. You should never call it directly but you can override it when
810 // subclassing, if you need custom dividers. It draws the divider image centered in the divider rectangle.
811 // If we're drawing a two-axis thumb leading and trailing will be nil, and the rectangle
812 // will be the thumb rectangle.
813 // If there are nested split views this will be called once to draw the main divider rect,
814 // and again for every thumb.
815 - (void)drawDivider:(NSImage*)anImage inRect:(NSRect)rect betweenView:(RBSplitSubview*)leading andView:(RBSplitSubview*)trailing {
816 // Fill the view with the background color (if there's any). Don't draw the background again for
818 if (leading||trailing) {
819 NSColor* bg = [self background];
822 NSRectFillUsingOperation(rect,NSCompositeSourceOver);
825 // Center the image, if there is one.
826 NSRect imrect = NSZeroRect;
827 NSRect dorect = NSZeroRect;
829 imrect.size = dorect.size = [anImage size];
830 dorect.origin = NSMakePoint(floorf(rect.origin.x+(rect.size.width-dorect.size.width)/2),
831 floorf(rect.origin.y+(rect.size.height-dorect.size.height)/2));
833 // Ask the delegate for the final rect where the image should be drawn.
834 if ([delegate respondsToSelector:@selector(splitView:willDrawDividerInRect:betweenView:andView:withProposedRect:)]) {
835 dorect = [delegate splitView:self willDrawDividerInRect:rect betweenView:leading andView:trailing withProposedRect:dorect];
837 // Draw the image if the delegate returned a non-empty rect.
838 if (!NSIsEmptyRect(dorect)) {
839 [anImage drawInRect:dorect fromRect:imrect operation:NSCompositeSourceOver fraction:1.0];
843 // This method should be called only from within the splitView:wasResizedFrom:to: delegate method
844 // to keep some specific subview the same size.
845 - (void)adjustSubviewsExcepting:(RBSplitSubview*)excepting {
846 [self RB___adjustSubviewsExcepting:[excepting isCollapsed]?nil:excepting];
849 // This method adjusts subviews and divider rectangles.
850 - (void)adjustSubviews {
851 [self RB___adjustSubviewsExcepting:nil];
854 // This resets the appropriate cursors for each divider according to the orientation.
855 // No cursors are shown if there is no divider image.
856 - (void)resetCursorRects {
860 id del = [delegate respondsToSelector:@selector(splitView:cursorRect:forDivider:)]?delegate:nil;
861 NSArray* subviews = [self RB___subviews];
862 int divcount = [subviews count]-1;
863 if ((divcount<1)||![self divider]) {
864 [del splitView:self cursorRect:NSZeroRect forDivider:0];
868 NSCursor* cursor = [RBSplitView cursor:[self isVertical]?RBSVVerticalCursor:RBSVHorizontalCursor];
869 float divt = [self dividerThickness];
870 for (i=0;i<divcount;i++) {
871 RBSplitView* sub = [[subviews objectAtIndex:i] coupledSplitView];
872 // If the leading subview is a nested RBSplitView, add the thumb rectangles first.
874 [sub RB___addCursorRectsTo:self forDividerRect:dividers[i] thickness:divt];
876 sub = [[subviews objectAtIndex:i+1] coupledSplitView];
877 // If the trailing subview is a nested RBSplitView, add the thumb rectangles first.
879 [sub RB___addCursorRectsTo:self forDividerRect:dividers[i] thickness:divt];
881 // Now add thedivider rectangle.
882 NSRect divrect = dividers[i];
884 divrect = [del splitView:self cursorRect:divrect forDivider:i];
886 if (!NSIsEmptyRect(divrect)) {
887 [self addCursorRect:divrect cursor:cursor];
892 // These two methods encode and decode RBSplitViews. One peculiarity is that we encode the divider image's
893 // bitmap representation as data; this makes the nib files larger, but the user can just paste any image
894 // into the RBSplitView inspector - or use the default divider image - without having to include it into the
896 - (void)encodeWithCoder:(NSCoder *)coder {
897 [super encodeWithCoder:coder];
898 if ([coder allowsKeyedCoding]) {
899 [coder encodeConditionalObject:delegate forKey:@"delegate"];
900 [coder encodeObject:autosaveName forKey:@"autosaveName"];
901 [coder encodeObject:[divider TIFFRepresentation] forKey:@"divider"];
902 [coder encodeObject:background forKey:@"background"];
903 [coder encodeFloat:dividerThickness forKey:@"dividerThickness"];
904 [coder encodeBool:isHorizontal forKey:@"isHorizontal"];
905 [coder encodeBool:isCoupled forKey:@"isCoupled"];
907 [coder encodeConditionalObject:delegate];
908 [coder encodeObject:autosaveName];
909 [coder encodeObject:[divider TIFFRepresentation]];
910 [coder encodeObject:background];
911 [coder encodeValueOfObjCType:@encode(typeof(dividerThickness)) at:÷rThickness];
912 [coder encodeValueOfObjCType:@encode(typeof(isHorizontal)) at:&isHorizontal];
913 [coder encodeValueOfObjCType:@encode(typeof(isCoupled)) at:&isCoupled];
917 - (id)initWithCoder:(NSCoder *)coder {
918 if ((self = [super initWithCoder:coder])) {
925 if ([coder allowsKeyedCoding]) {
926 isCoupled = [coder decodeBoolForKey:@"isCoupled"];
927 [self setDelegate:[coder decodeObjectForKey:@"delegate"]];
928 [self setAutosaveName:[coder decodeObjectForKey:@"autosaveName"] recursively:NO];
929 data = [coder decodeObjectForKey:@"divider"];
930 [self setBackground:[coder decodeObjectForKey:@"background"]];
931 divt = [coder decodeFloatForKey:@"dividerThickness"];
932 isHorizontal = [coder decodeBoolForKey:@"isHorizontal"];
934 [self setDelegate:[coder decodeObject]];
935 [self setAutosaveName:[coder decodeObject] recursively:NO];
936 data = [coder decodeObject];
937 [self setBackground:[coder decodeObject]];
938 [coder decodeValueOfObjCType:@encode(typeof(divt)) at:&divt];
939 [coder decodeValueOfObjCType:@encode(typeof(isHorizontal)) at:&isHorizontal];
940 [coder decodeValueOfObjCType:@encode(typeof(isCoupled)) at:&isCoupled];
944 NSBitmapImageRep* rep = [NSBitmapImageRep imageRepWithData:data];
945 NSImage* image = [[[NSImage alloc] initWithSize:[rep size]] autorelease];
946 [image setFlipped:YES];
947 [image addRepresentation:rep];
948 [self setDivider:image];
950 [self setDivider:nil];
952 [self setDividerThickness:divt];
953 [self setMustAdjust];
954 [self performSelector:@selector(viewDidMoveToSuperview) withObject:nil afterDelay:0.0];
955 [self performSelector:@selector(RB___adjustOutermostIfNeeded) withObject:nil afterDelay:0.0];
962 @implementation RBSplitView (RB___ViewAdditions)
964 // This sets the dragging status flag. After clearing the flag, the state must be saved explicitly.
965 - (void)RB___setDragging:(BOOL)flag {
966 BOOL save = isDragging&&!flag;
973 // This returns the number of visible subviews.
974 - (unsigned int)RB___numberOfSubviews {
975 unsigned int result = 0;
976 NSEnumerator* enumerator = [[self subviews] objectEnumerator];
978 while ((sub = [enumerator nextObject])) {
984 // This returns the origin coordinate of the Nth divider.
985 - (float)RB___dividerOrigin:(int)indx {
988 BOOL ishor = [self isHorizontal];
989 result = DIM(dividers[indx].origin);
994 // This returns an array with all non-hidden subviews.
995 - (NSArray*)RB___subviews {
996 NSMutableArray* result = [NSMutableArray arrayWithArray:[self subviews]];
998 for (i=[result count]-1;i>=0;i--) {
999 RBSplitSubview* view = [result objectAtIndex:i];
1000 if ([view isHidden]) {
1001 [result removeObjectAtIndex:i];
1007 // This returns the actual value set in dividerThickness.
1008 - (float)RB___dividerThickness {
1009 return dividerThickness;
1012 // This method returns the actual dimension occupied by the subviews; that is, without dividers.
1013 - (float)RB___dimensionWithoutDividers {
1014 BOOL ishor = [self isHorizontal];
1015 NSSize size = [self frame].size;
1016 return fMAX(1.0,DIM(size)-[self dividerThickness]*([self RB___numberOfSubviews]-1));
1019 // This method returns one of the divider rectangles, or NSZeroRect if the index is invalid.
1020 // If view is non-nil, the rect will be expressed in that view's coordinates. We assume
1021 // that view is a superview of self.
1022 - (NSRect)RB___dividerRect:(unsigned)indx relativeToView:(RBSplitView*)view {
1023 if (dividers&&(indx<[self RB___numberOfSubviews]-1)) {
1024 NSRect result = dividers[indx];
1025 if (view&&(view!=self)) {
1026 result = [self convertRect:result toView:view];
1033 // Returns the index of the divider hit by the point, or NSNotFound if none.
1034 // point is in coordinates relative to view. delta is the divider thickness added
1035 // to both ends of the divider rect, to accomodate two-axis thumbs.
1036 - (unsigned)RB___dividerHitBy:(NSPoint)point relativeToView:(RBSplitView*)view thickness:(float)delta {
1040 int divcount = [self RB___numberOfSubviews]-1;
1045 BOOL ishor = [self isHorizontal];
1046 point = [self convertPoint:point fromView:view];
1047 for (i=0;i<divcount;i++) {
1048 NSRect divdr = dividers[i];
1049 OTHER(divdr.origin) -= delta;
1050 OTHER(divdr.size) += 2*delta;
1051 if ([self mouse:point inRect:divdr]) {
1058 // This method sets a flag to clear all fractions before adjusting.
1059 - (void)RB___setMustClearFractions {
1060 mustClearFractions = YES;
1063 // This local method asks the delegate if we should resize the trailing subview or the window
1064 // when a divider is dragged. Not called if we're inside an NSScrollView.
1065 - (BOOL)RB___shouldResizeWindowForDivider:(unsigned int)indx betweenView:(RBSplitSubview*)leading andView:(RBSplitSubview*)trailing willGrow:(BOOL)grow {
1066 if (!isInScrollView&&[delegate respondsToSelector:@selector(splitView:shouldResizeWindowForDivider:betweenView:andView:willGrow:)]) {
1067 return [delegate splitView:self shouldResizeWindowForDivider:indx betweenView:leading andView:trailing willGrow:grow];
1072 // This local method tries to expand the leading subview (which is assumed to be collapsed). Delta should be positive.
1073 - (void)RB___tryToExpandLeading:(RBSplitSubview*)leading divider:(unsigned int)indx trailing:(RBSplitSubview*)trailing delta:(float)delta {
1074 NSWindow* window = nil;
1075 NSView* document = nil;
1076 NSSize maxsize = NSMakeSize(WAYOUT,WAYOUT);
1077 NSRect frame = NSZeroRect;
1078 NSRect screen = NSMakeRect(0,0,WAYOUT,WAYOUT);
1080 // First we ask the delegate, if there's any, if the window should resize.
1081 BOOL dowin = ([self RB___shouldResizeWindowForDivider:indx betweenView:leading andView:trailing willGrow:YES]);
1083 // We initialize the other local variables only if we need them for the window.
1084 ishor = [self isHorizontal];
1085 document = [[self enclosingScrollView] documentView];
1087 frame = [document frame];
1089 window = [self window];
1090 frame = [window frame];
1091 maxsize = [window maxSize];
1092 screen = [[NSScreen mainScreen] visibleFrame];
1095 // The mouse has to move over half of the expanded size (plus hysteresis) and the expansion shouldn't
1096 // reduce the trailing subview to less than its minimum size (or grow the window beyond its maximum).
1097 float limit = [leading minDimension];
1098 float dimension = 0.0;
1100 float maxd = fMAX(0.0,(ishor?frame.origin.y-screen.origin.y:(screen.origin.x+screen.size.width)-(frame.origin.x+frame.size.width)));
1101 dimension = fMIN(DIM(maxsize)-DIM(frame.size),maxd);
1103 dimension = trailing?[trailing dimension]:WAYOUT;
1105 if (limit>dimension) {
1108 if (!dowin&&trailing) {
1109 limit += [trailing minDimension];
1110 if (limit>dimension) {
1111 // If the trailing subview is going below its minimum, we try to collapse it first.
1112 // However, we don't collapse if that would cause the leading subview to become larger than its maximum.
1113 if (([trailing canCollapse])&&(delta>(0.5+HYSTERESIS)*dimension)&&([leading maxDimension]<=dimension)) {
1114 delta = -[trailing RB___collapse];
1115 [leading changeDimensionBy:delta mayCollapse:NO move:NO];
1120 // The leading subview may be expanded normally.
1121 delta = -[leading changeDimensionBy:delta mayCollapse:NO move:NO];
1123 // If it does expand, we widen the window.
1124 DIM(frame.size) -= delta;
1126 DIM(frame.origin) += delta;
1129 [document setFrame:frame];
1130 [document setNeedsDisplay:YES];
1132 [window setFrame:frame display:YES];
1134 [self setMustAdjust];
1136 // If it does expand, we shorten the trailing subview.
1137 [trailing changeDimensionBy:delta mayCollapse:NO move:YES];
1141 // This local method tries to shorten the leading subview. Both subviews are assumed to be expanded.
1142 // delta should be negative. If always is NO, the subview will be shortened only if it might also be
1143 // collapsed; otherwise, it's shortened as much as possible.
1144 - (void)RB___tryToShortenLeading:(RBSplitSubview*)leading divider:(unsigned int)indx trailing:(RBSplitSubview*)trailing delta:(float)delta always:(BOOL)always {
1145 NSWindow* window = nil;
1146 NSView* document = nil;
1147 NSSize minsize = NSZeroSize;
1148 NSRect frame = NSZeroRect;
1150 // First we ask the delegate, if there's any, if the window should resize.
1151 BOOL dowin = ([self RB___shouldResizeWindowForDivider:indx betweenView:leading andView:trailing willGrow:NO]);
1153 // We initialize the other local variables only if we need them for the window.
1154 ishor = [self isHorizontal];
1155 document = [[self enclosingScrollView] documentView];
1157 frame = [document frame];
1159 window = [self window];
1160 frame = [window frame];
1161 minsize = [window minSize];
1164 // We avoid making the trailing subview larger than its maximum, or the window smaller than its minimum.
1167 limit = DIM(frame.size)-DIM(minsize);
1169 limit = trailing?([trailing maxDimension]-[trailing dimension]):WAYOUT;
1178 BOOL okl = limit>=[leading dimension];
1181 delta = -[leading changeDimensionBy:delta mayCollapse:okl move:NO];
1183 // Resize the window.
1184 DIM(frame.size) -= delta;
1186 DIM(frame.origin) += delta;
1189 [document setFrame:frame];
1190 [document setNeedsDisplay:YES];
1192 [window setFrame:frame display:YES];
1194 [self setMustAdjust];
1196 // Otherwise, resize trailing.
1197 [trailing changeDimensionBy:delta mayCollapse:NO move:YES];
1202 // This local method tries to shorten the trailing subview. Both subviews are assumed to be expanded.
1203 // delta should be positive. If always is NO, the subview will be shortened only if it might also be
1204 // collapsed; otherwise, it's shortened as much as possible.
1205 - (void)RB___tryToShortenTrailing:(RBSplitSubview*)trailing divider:(unsigned int)indx leading:(RBSplitSubview*)leading delta:(float)delta always:(BOOL)always {
1206 NSWindow* window = nil;
1207 NSView* document = nil;
1208 NSSize maxsize = NSMakeSize(WAYOUT,WAYOUT);
1209 NSRect frame = NSZeroRect;
1210 NSRect screen = NSMakeRect(0,0,WAYOUT,WAYOUT);
1212 // First we ask the delegate, if there's any, if the window should resize.
1213 BOOL dowin = ([self RB___shouldResizeWindowForDivider:indx betweenView:leading andView:trailing willGrow:YES]);
1215 // We initialize the other local variables only if we need them for the window.
1216 ishor = [self isHorizontal];
1217 document = [[self enclosingScrollView] documentView];
1219 frame = [document frame];
1221 window = [self window];
1222 frame = [window frame];
1223 maxsize = [window maxSize];
1224 screen = [[NSScreen mainScreen] visibleFrame];
1227 // We avoid making the leading subview larger than its maximum, or the window larger than its maximum.
1230 float maxd = fMAX(0.0,(ishor?frame.origin.y-screen.origin.y:(screen.origin.x+screen.size.width)-(frame.origin.x+frame.size.width)));
1231 limit = fMIN(DIM(maxsize)-DIM(frame.size),maxd);
1233 limit = [leading maxDimension]-[leading dimension];
1242 BOOL okl = dowin||(limit>=(trailing?[trailing dimension]:WAYOUT));
1245 // If we should resize the window, resize leading, then the window.
1246 delta = [leading changeDimensionBy:delta mayCollapse:NO move:NO];
1247 DIM(frame.size) += delta;
1249 DIM(frame.origin) -= delta;
1252 [document setFrame:frame];
1253 [document setNeedsDisplay:YES];
1255 [window setFrame:frame display:YES];
1257 [self setMustAdjust];
1259 // Otherwise, resize trailing, then leading.
1261 delta = -[trailing changeDimensionBy:-delta mayCollapse:okl move:YES];
1263 [leading changeDimensionBy:delta mayCollapse:NO move:NO];
1268 // This method tries to expand the trailing subview (which is assumed to be collapsed).
1269 - (void)RB___tryToExpandTrailing:(RBSplitSubview*)trailing leading:(RBSplitSubview*)leading delta:(float)delta {
1270 // The mouse has to move over half of the expanded size (plus hysteresis) and the expansion shouldn't
1271 // reduce the leading subview to less than its minimum size. If it does, we try to collapse it first.
1272 // However, we don't collapse if that would cause the trailing subview to become larger than its maximum.
1273 float limit = trailing?[trailing minDimension]:0.0;
1274 float dimension = [leading dimension];
1275 if (limit>dimension) {
1278 limit += [leading minDimension];
1279 if (limit>dimension) {
1280 if ([leading canCollapse]&&(-delta>(0.5+HYSTERESIS)*dimension)&&((trailing?[trailing maxDimension]:0.0)<=dimension)) {
1281 delta = -[leading RB___collapse];
1282 [trailing changeDimensionBy:delta mayCollapse:NO move:YES];
1286 // The trailing subview may be expanded normally. If it does expand, we shorten the leading subview.
1288 delta = -[trailing changeDimensionBy:-delta mayCollapse:NO move:YES];
1290 [leading changeDimensionBy:delta mayCollapse:NO move:NO];
1294 // This method is called by the mouseDown:method for every tracking event. It's separated out as it's
1295 // called from the Interface Builder palette in a slightly different way, and also if you have a
1296 // separate drag view designated by the delegate. You'll never need to call this directly.
1297 // theEvent is the event (which should be a NSLeftMouseDragged event).
1298 // where is the point where the original mouse-down happened, corrected for the current divider position,
1299 // and expressed in local coordinates.
1300 // base is an offset (x,y) applied to the mouse location (usually will be zero)
1301 // indx is the number of the divider that's being dragged.
1302 - (void)RB___trackMouseEvent:(NSEvent*)theEvent from:(NSPoint)where withBase:(NSPoint)base inDivider:(unsigned)indx {
1304 NSArray* subviews = [self RB___subviews];
1305 int subcount = [subviews count];
1307 // leading and trailing point at the subviews immediately leading and trailing the divider being tracked
1308 RBSplitSubview* leading = [subviews objectAtIndex:indx];
1309 RBSplitSubview* trailing = [subviews objectAtIndex:indx+1];
1310 // convert the mouse coordinates to apply to the same system the divider rects are in.
1311 NSPoint mouse = [self convertPoint:[theEvent locationInWindow] fromView:nil];
1314 result.x = mouse.x-where.x;
1315 result.y = mouse.y-where.y;
1316 // delta is the actual amount the mouse has moved in the relevant coordinate since the last event.
1317 BOOL ishor = [self isHorizontal];
1318 float delta = DIM(result);
1320 // Negative delta means the mouse is being moved left or upwards.
1321 // firstLeading will point at the first expanded subview to the left (or upwards) of the divider.
1322 // If there's none (all subviews are collapsed) it will point at the nearest subview.
1323 RBSplitSubview* firstLeading = leading;
1325 while (![firstLeading canShrink]) {
1327 firstLeading = leading;
1330 firstLeading = [subviews objectAtIndex:k];
1332 if (isInScrollView) {
1335 // If the trailing subview is collapsed, it might be expanded if some conditions are met.
1336 if ([trailing isCollapsed]) {
1337 [self RB___tryToExpandTrailing:trailing leading:firstLeading delta:delta];
1339 [self RB___tryToShortenLeading:firstLeading divider:indx trailing:trailing delta:delta always:YES];
1341 } else if (delta>0.0) {
1342 // Positive delta means the mouse is being moved right or downwards.
1343 // firstTrailing will point at the first expanded subview to the right (or downwards) of the divider.
1344 // If there's none (all subviews are collapsed) it will point at the nearest subview.
1345 RBSplitSubview* firstTrailing = nil;
1346 if (!isInScrollView) {
1347 firstTrailing = trailing;
1349 while (![firstTrailing canShrink]) {
1350 if (++k>=subcount) {
1351 firstTrailing = trailing;
1354 firstTrailing = [subviews objectAtIndex:k];
1357 // If the leading subview is collapsed, it might be expanded if some conditions are met.
1358 if ([leading isCollapsed]) {
1359 [self RB___tryToExpandLeading:leading divider:indx trailing:firstTrailing delta:delta];
1361 // The leading subview is not collapsed, so we try to shorten or even collapse it
1362 [self RB___tryToShortenTrailing:firstTrailing divider:indx leading:leading delta:delta always:YES];
1367 // This is called for nested RBSplitViews, to add the cursor rects for the two-axis thumbs.
1368 - (void)RB___addCursorRectsTo:(RBSplitView*)masterView forDividerRect:(NSRect)rect thickness:(float)delta {
1369 if (dividers&&[self divider]) {
1370 NSArray* subviews = [self RB___subviews];
1371 int divcount = [subviews count]-1;
1376 NSCursor* cursor = [RBSplitView cursor:RBSV2WayCursor];
1377 BOOL ishor = [self isHorizontal];
1378 // Loop over the divider rectangles, intersect them with the view's own, and add the thumb rectangle
1379 // to the containing split view.
1380 for (i=0;i<divcount;i++) {
1381 NSRect divdr = dividers[i];
1382 divdr.origin = [self convertPoint:divdr.origin toView:masterView];
1383 OTHER(divdr.origin) -= delta;
1384 OTHER(divdr.size) += 2*delta;
1385 divdr = NSIntersectionRect(divdr,rect);
1386 if (!NSIsEmptyRect(divdr)) {
1387 [masterView addCursorRect:divdr cursor:cursor];
1393 // This is called for nested RBSplitViews, to draw the two-axis thumbs.
1394 - (void)RB___drawDividersIn:(RBSplitView*)masterView forDividerRect:(NSRect)rect thickness:(float)delta {
1398 NSArray* subviews = [self RB___subviews];
1399 int divcount = [subviews count]-1;
1404 BOOL ishor = [self isHorizontal];
1405 // Get the outer split view's divider image.
1406 NSImage* image = [masterView divider];
1407 // Loop over the divider rectangles, intersect them with the view's own, and draw the thumb there.
1408 for (i=0;i<divcount;i++) {
1409 NSRect divdr = dividers[i];
1410 divdr.origin = [self convertPoint:divdr.origin toView:masterView];
1411 OTHER(divdr.origin) -= delta;
1412 OTHER(divdr.size) += 2*delta;
1413 divdr = NSIntersectionRect(divdr,rect);
1414 if (!NSIsEmptyRect(divdr)) {
1415 [masterView drawDivider:image inRect:divdr betweenView:nil andView:nil];
1420 // This is usually called from initWithCoder to ensure that the outermost RBSplitView is
1421 // properly adjusted when first displayed.
1422 - (void)RB___adjustOutermostIfNeeded {
1423 RBSplitView* sv = [self splitView];
1425 [sv RB___adjustOutermostIfNeeded];
1428 if (mustAdjust&&!isAdjusting) {
1429 [self adjustSubviews];
1433 // Here we try to keep all subviews adjusted in as natural a manner as possible, given the constraints.
1434 // The main idea is to always keep the RBSplitView completely covered by dividers and subviews, have at
1435 // least one expanded subview, and never make a subview smaller than its minimum dimension, or larger
1436 // than its maximum dimension.
1437 // We try to account for most unusual situations but this may fail under some circumstances. YMMV.
1438 - (void)RB___adjustSubviewsExcepting:(RBSplitSubview*)excepting {
1440 NSArray* subviews = [self RB___subviews];
1441 unsigned subcount = [subviews count];
1445 NSRect bounds = [self bounds];
1446 // Never adjust if the splitview itself is collapsed.
1447 if ((bounds.size.width<1.0)||(bounds.size.height<1.0)) {
1450 // Prevents adjustSubviews being called recursively, which unfortunately may happen otherwise.
1455 // Tell the delegate we're about to adjust subviews.
1456 if ([delegate respondsToSelector:@selector(willAdjustSubviews:)]) {
1457 [delegate willAdjustSubviews:self];
1458 bounds = [self bounds];
1460 unsigned divcount = subcount-1;
1462 // No dividers at all.
1468 // Try to allocate or resize if we already have a dividers array.
1469 unsigned long divsiz = sizeof(NSRect)*divcount;
1470 dividers = dividers?reallocf(dividers,divsiz):malloc(divsiz);
1475 // This C array of subviewCaches is used to cache the subview information.
1476 subviewCache* caches = malloc(sizeof(subviewCache)*subcount);
1477 double realsize = 0.0;
1478 double expsize = 0.0;
1479 float newsize = 0.0;
1480 float effsize = 0.0;
1484 BOOL ishor = [self isHorizontal];
1485 float divt = [self dividerThickness];
1486 // First we loop over subviews and cache their information.
1487 for (i=0;i<subcount;i++) {
1489 [[subviews objectAtIndex:i] RB___copyIntoCache:curr];
1491 // This is a counter to limit the outer loop to three iterations (six if excepting is non-nil).
1492 int sanity = excepting?-3:0;
1493 while (sanity++<3) {
1494 // We try to accomodate the exception for the first group of loops, turn it off for the second.
1498 // newsize is the available space for actual subviews (so dividers don't count). It will be an integer.
1499 // Same as calling [self RB___dimensionWithoutDividers].
1500 unsigned smallest = 0;
1501 float smalldim = -1.0;
1503 // Loop over subviews and sum the expanded dimensions into expsize, including fractions.
1504 // Also find the collapsed subview with the smallest minimum dimension.
1505 for (i=0;i<subcount;i++) {
1507 curr->constrain = NO;
1508 if (curr->size>0.0) {
1509 expsize += curr->size;
1510 if (!isInScrollView) {
1511 // ignore fractions if we're in a NSScrollView, however.
1512 expsize += curr->fraction;
1516 limit = [curr->sub minDimension];
1517 if (smalldim>limit) {
1523 // haveexp should be YES at this point. If not, all subviews were collapsed; can't have that, so we
1524 // expand the smallest subview (or the first, if all have the same minimum).
1525 curr = &caches[smallest];
1527 curr->size = [curr->sub minDimension];
1528 curr->fraction = 0.0;
1529 expsize += curr->size;
1531 if (isInScrollView) {
1532 // If we're inside an NSScrollView, we just grow the view to accommodate the subviews, instead of
1533 // the other way around.
1534 DIM(bounds.size) = expsize;
1537 // If the total dimension of all expanded subviews is less than 1.0 we set the dimension of the smallest
1538 // subview (which we're sure is expanded at this point) to the available space.
1539 newsize = DIM(bounds.size)-divcount*divt;
1541 curr->size = newsize;
1542 curr->fraction = 0.0;
1545 // Loop over the subviews and check if they're within the limits after scaling. We also recalculate the
1546 // exposed size and repeat until no more subviews hit the constraints during that loop.
1548 effsize = newsize;// we're caching newsize here, this is an integer.
1550 // scale is the scalefactor by which all views should be scaled - assuming none have constraints.
1551 // It's a double to (hopefully) keep rounding errors small enough for all practical purposes.
1552 double scale = newsize/expsize;
1556 for (i=0;i<subcount;i++) {
1557 // Loop over the cached subview info.
1559 if (curr->size>0.0) {
1560 // Check non-collapsed subviews only.
1561 if (!curr->constrain) {
1562 // Check non-constrained subviews only; calculate the proposed new size.
1563 float cursize = (curr->size+curr->fraction)*scale;
1564 // Check if we hit a limit. limit will contain either the max or min dimension, whichever was hit.
1565 if (([curr->sub RB___animationData:NO resize:NO]&&((limit = curr->size)>=0.0))||
1566 ((curr->sub==excepting)&&((limit = [curr->sub dimension])>0.0))||
1567 (cursize<(limit = [curr->sub minDimension]))||
1568 (cursize>(limit = [curr->sub maxDimension]))) {
1569 // If we hit a limit, we mark the view and set to repeat the loop; non-constrained subviews will
1570 // have to be recalculated.
1571 curr->constrain = constrained = YES;
1572 // We set the new size to the limit we hit, and subtract it from the total size to be subdivided.
1574 curr->fraction = 0.0;
1577 // If we didn't hit a limit, we round the size to the nearest integer and recalculate the fraction.
1578 double rem = fmod(cursize,1.0);
1585 curr->fraction = rem;
1587 // We store the new size in the cache.
1588 curr->size = cursize;
1590 // And add the full size with fraction to the actual sum of all expanded subviews.
1591 realsize += curr->size+curr->fraction;
1594 // At this point, newsize will be the sum of the new dimensions of non-constrained views.
1595 // expsize will be the sum of the recalculated dimensions of the same views, if any.
1596 // We repeat the loop if any view has been recently constrained, and if there are any
1597 // unconstrained views left.
1598 } while (constrained&&(expsize>0.0));
1599 // At this point, the difference between realsize and effsize should be less than 1 pixel.
1600 // realsize is the total size of expanded subviews as recalculated above, and
1601 // effsize is the value realsize should have.
1602 limit = realsize-effsize;
1604 // If realsize is larger than effsize by 1 pixel or more, we will need to collapse subviews to make room.
1605 // This in turn might expand previously collapsed subviews. So, we'll try collapsing constrained subviews
1606 // until we're back into range, and then recalculate everything from the beginning.
1607 for (i=0;i<subcount;i++) {
1609 if (curr->constrain&&(curr->sub!=excepting)&&([curr->sub RB___animationData:NO resize:NO]==nil)&&[curr->sub canCollapse]) {
1610 realsize -= curr->size;
1615 if ((realsize-effsize)<1.0) {
1620 } else if (limit<=-1.0) {
1621 // If realsize is smaller than effsize by 1 pixel or more, we will need to expand subviews.
1622 // This in turn might collapse previously expanded subviews. So, we'll try expanding collapsed subviews
1623 // until we're back into range, and then recalculate everything from the beginning.
1624 for (i=0;i<subcount;i++) {
1626 if (curr->size<=0.0) {
1627 curr->size = [curr->sub minDimension];
1628 curr->fraction = 0.0;
1629 realsize += curr->size;
1630 if ((realsize-effsize)>-1.0) {
1636 // The difference is less than 1 pixel, meaning that in all probability our calculations are
1637 // exact or off by at most one pixel after rounding, so we break the loop here.
1641 // After passing through the outer loop a few times, the frames may still be wrong, but there's nothing
1642 // else we can do about it. You probably should avoid this by some other means like setting a minimum
1643 // or maximum size for the window, for instance, or leaving at least one unlimited subview.
1645 // newframe is used to reset all subview frames. Subviews always fill the entire RBSplitView along the
1646 // current orientation.
1647 NSRect newframe = NSMakeRect(0.0,0.0,bounds.size.width,bounds.size.height);
1648 // We now loop over the subviews yet again and set the definite frames, also recalculating the
1649 // divider rectangles as we go along, and collapsing and expanding subviews whenever requested.
1650 RBSplitSubview* last = nil;
1651 // And we make a note if there's any nested RBSplitView.
1652 int nested = NSNotFound;
1653 newsize = DIM(bounds.size)-divcount*divt;
1654 for (i=0;i<subcount;i++) {
1656 // If we have a nested split view store its index.
1657 if ((nested==NSNotFound)&&([curr->sub asSplitView]!=nil)) {
1660 // Adjust the subview to the correct origin and resize it to fit into the "other" dimension.
1661 curr->rect.origin = newframe.origin;
1662 OTHER(curr->rect.size) = OTHER(newframe.size);
1663 DIM(curr->rect.size) = curr->size;
1664 // Clear fractions for expanded subviews if requested.
1665 if ((curr->size>0.0)&&mustClearFractions) {
1666 curr->fraction = 0.0;
1668 // Ask the subview to do the actual moving/resizing etc. from the cache.
1669 [curr->sub RB___updateFromCache:curr withTotalDimension:effsize];
1670 // Step to the next position and record the subview if it's not collapsed.
1671 DIM(newframe.origin) += curr->size;
1672 if (curr->size>0.0) {
1676 // We're at the last subview, so we now check if the actual and calculated dimensions
1678 float remain = DIM(bounds.size)-DIM(newframe.origin);
1679 if (last&&(fabsf(remain)>0.0)) {
1680 // We'll resize the last expanded subview to whatever it takes to squeeze within the frame.
1681 // Normally the change should be at most one pixel, but if too many subviews were constrained,
1682 // this may be a large value, and the last subview may be resized beyond its constraints;
1683 // there's nothing else to do at this point.
1684 newframe = [last frame];
1685 DIM(newframe.size) += remain;
1686 [last RB___setFrameSize:newframe.size withFraction:[last RB___fraction]-remain];
1687 // And we loop back over the rightmost dividers (if any) to adjust their offsets.
1688 while ((i>0)&&(last!=[subviews objectAtIndex:i])) {
1689 DIM(dividers[--i].origin) += remain;
1694 // For any but the last subview, we just calculate the divider frame.
1695 DIM(newframe.size) = divt;
1696 dividers[i] = newframe;
1697 DIM(newframe.origin) += divt;
1700 // We resize our frame at this point, if we're inside an NSScrollView.
1701 if (isInScrollView) {
1702 [super setFrameSize:bounds.size];
1704 // If there was at least one nested RBSplitView, we loop over the subviews and adjust those that need it.
1705 for (i=nested;i<subcount;i++) {
1707 RBSplitView* sv = [curr->sub asSplitView];
1708 if ([sv mustAdjust]) {
1709 [sv adjustSubviews];
1712 // Free the cache array.
1714 // Clear cursor rects.
1716 mustClearFractions = NO;
1717 [[self window] invalidateCursorRectsForView:self];
1718 // Save the state for all subviews.
1720 [self saveState:NO];
1722 // If we're a nested RBSplitView, also invalidate cursorRects for the superview.
1723 RBSplitView* sv = [self couplingSplitView];
1725 [[self window] invalidateCursorRectsForView:sv];
1728 // Tell the delegate we're finished.
1729 if ([delegate respondsToSelector:@selector(didAdjustSubviews:)]) {
1730 [delegate didAdjustSubviews:self];
1736 #endif // MM_ENABLE_PLUGINS