// // RBSplitViewPalette.m version 1.1.4 // RBSplitView // // Created by Rainer Brockerhoff on 24/09/2004. // Copyright 2004-2006 Rainer Brockerhoff. // Some Rights Reserved under the Creative Commons Attribution License, version 2.5, and/or the MIT License. // #import "RBSplitViewPalette.h" #import "RBSplitViewPrivateDefines.h" // Please don't remove this copyright notice! static const unsigned char RBSplitViewPalette_Copyright[] __attribute__ ((used)) = "RBSplitViewPalette 1.1.4 Copyright(c)2004-2006 by Rainer Brockerhoff ."; // Main palette class... // Writing a palette for a complex container view is insufficiently documented. Still, this works // mostly as expected. // These globals will always point to the 8x8 and 9x9 thumb images. static NSImage* thumb8 = nil; static NSImage* thumb9 = nil; @implementation RBSplitViewPalette // Release the palette image when going away. - (void)dealloc { [thumb8 release]; thumb8 = nil; [thumb9 release]; thumb9 = nil; [splitView release]; [super dealloc]; } // Called when the palette is loaded - (void)finishInstantiate { // Creates a RBSplitView with the same size as the sample image and two subviews. NSRect bounds = [splitImage bounds]; splitView = [[RBSplitView alloc] initWithFrame:bounds andSubviews:2]; // Sets the default divider image. This is 8x8 pixels instead of NSSplitView's 9x9, but looks as good. thumb8 = [[self imageNamed:@"Thumb8"] retain]; [thumb8 setFlipped:YES]; thumb9 = [[self imageNamed:@"Thumb9"] retain]; [thumb9 setFlipped:YES]; [splitView setDivider:thumb8]; // Associate the image with our prototype RBSplitView. [self associateObject:splitView ofType:IBViewPboardType withView:splitImage]; // Finish instantiating the palette, and register as a dragging delegate. [super finishInstantiate]; } @end // This implements the attribute inspector for RBSplitSubview... @implementation RBSplitSubviewInspector // Loads the proper inspector nib file. - (id)init { self = [super init]; [NSBundle loadNibNamed:@"RBSplitSubviewInspector" owner:self]; return self; } // Called to move values from the inspected view into the inspector window. Quite straightforward. - (void)revert:(id)sender { RBSplitSubview* suv = (RBSplitSubview*)[self object]; RBSplitView* sv = [suv splitView]; [collapseButton setState:[suv canCollapse]]; [[identifierValue cellAtIndex:0] setStringValue:[suv identifier]]; [[minimumValue cellAtIndex:0] setFloatValue:[suv minDimension]]; // No max limit is indicated by a blank value; don't want to confuse the user with 1000000.0 float dimension = [suv maxDimension]; if (dimension* owner = (NSView*)[NSApp selectionOwner]; NSUndoManager* undo = nil; if ([owner respondsToSelector:@selector(undoManager)]) { undo = [owner undoManager]; } [self setSubview:[subs objectAtIndex:0] withUndo:undo frame:[suv bounds] andAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; } } else if (sender==identifierValue) { [suv setIdentifier:[[identifierValue cellAtIndex:0] stringValue]]; } else if ((sender==minimumValue)||(sender==maximumValue)) { [suv setMinDimension:[[minimumValue cellAtIndex:0] floatValue] andMaxDimension:[[maximumValue cellAtIndex:0] floatValue]]; // No max limit is indicated by a blank value; don't want to confuse the user with 1000000.0 float dimension = [suv maxDimension]; if (dimension=WAYOUT) { [sizeLimits setStringValue:[NSString stringWithFormat:@"Minimum %g",[suv minDimension]]]; } else { [sizeLimits setStringValue:[NSString stringWithFormat:@"Minimum %g\nMaximum %g",[suv minDimension],[suv maxDimension]]]; } [collapsedButton setEnabled:[suv canCollapse]]; [collapsedButton setState:[suv isCollapsed]]; [sv adjustSubviews]; [[self inspectedDocument] drawObject:sv]; [super revert:sender]; } // The action method for the inspector control. - (void)ok:(id)sender { RBSplitSubview* suv = (RBSplitSubview*)[self object]; RBSplitView* sv = [suv ibSplitView]; [self beginUndoGrouping]; [self noteAttributesWillChangeForObject:suv]; if (sender==sizeValue) { [suv setDimension:[[sizeValue cellAtIndex:0] floatValue]]; [[sizeValue cellAtIndex:0] setFloatValue:[suv dimension]]; } else if (sender==collapsedButton) { if ([sender state]) { [suv collapse]; } else { [suv expand]; } [collapsedButton setState:[suv isCollapsed]]; } [[self inspectedDocument] drawObject:sv]; [super ok:sender]; } @end // This implements the attribute inspector for RBSplitView... @implementation RBSplitViewInspector - (id)init { self = [super init]; [NSBundle loadNibNamed:@"RBSplitViewInspector" owner:self]; return self; } // Called to move values from the inspected view into the inspector window. Quite straightforward. - (void)revert:(id)sender { RBSplitView* sv = (RBSplitView*)[self object]; [[autosaveName cellAtIndex:0] setStringValue:[sv autosaveName]]; // Show clearColor if background is nil. [hiddenButton setState:[sv isHidden]]; int count = [[sv subviews] count]; [[subviewCount cellAtIndex:0] setIntValue:count]; [subviewStepper setIntValue:count]; [[tagValue cellAtIndex:0] setIntValue:[sv tag]]; float divt = [sv RB___dividerThickness]; [thicknessValue setEnabled:(divt>0.0)||![sv divider]]; [useButton setState:divt<1.0]; [[thicknessValue cellAtIndex:0] setFloatValue:[sv dividerThickness]]; RBSplitView* suv = [sv ibSplitView]; BOOL notc = ![sv isCoupled]; [coupledButton setState:!notc]; [coupledButton setEnabled:[sv splitView]!=nil]; // We select one or another tab depending on whether we're a nested RBSplitView or not. [tabView selectTabViewItemAtIndex:suv?1:0]; // Switch the next key view according to the tab. [autosaveName setNextKeyView:suv?positionValue:subviewCount]; [collapseButton setState:[sv canCollapse]]; [[identifierValue cellAtIndex:0] setStringValue:[sv identifier]]; [[minimumValue cellAtIndex:0] setFloatValue:[sv minDimension]]; // No max limit is indicated by a blank value; don't want to confuse the user with 1000000.0 float dimension = [sv maxDimension]; if (dimension)anItem { if ([anItem tag]==5) { if (![NSImage canInitWithPasteboard:[NSPasteboard generalPasteboard]]) { return NO; } } return YES; } // This is the same action method for all inspector controls. Makes for a messy implementation // with all these if...then...else if thingies. The alternative would be to have a different // action method for every control, this is on the to-do list. // Note that some controls have their value immediately written back into the window. As those // setter methods do consistency checking, this is necessary; if the user enters an invalid // value, it gets corrected immediately. - (void)ok:(id)sender { RBSplitView* sv = (RBSplitView*)[self object]; [self beginUndoGrouping]; [self noteAttributesWillChangeForObject:sv]; if (sender==collapseButton) { [sv setCanCollapse:[collapseButton state]]; } else if (sender==useButton) { if ([useButton state]) { [sv setDividerThickness:0.0]; } else { [sv setDividerThickness:[sv dividerThickness]]; } float divt = [sv RB___dividerThickness]; [thicknessValue setEnabled:(divt>0.0)||![sv divider]]; [[thicknessValue cellAtIndex:0] setFloatValue:[sv dividerThickness]]; [useButton setState:divt<1.0]; } else if (sender==coupledButton) { [sv setCoupled:[coupledButton state]]; BOOL notc = ![sv isCoupled]; NSColor* background = [sv background]; [backgroundWell setColor:background?background:[NSColor clearColor]]; [backgroundWell setEnabled:notc]; NSImage* divider = [[[sv divider] copy] autorelease]; [divider setFlipped:NO]; [[[dividerImage menu] itemAtIndex:0] setImage:divider]; [dividerImage setEnabled:notc]; NSSize size = divider?[divider size]:NSZeroSize; [dividerSize setStringValue:notc?[NSString stringWithFormat:@"(%g x %g)",size.width,size.height]: @"(from containing view)"]; } else if (sender==identifierValue) { [sv setIdentifier:[[identifierValue cellAtIndex:0] stringValue]]; } else if ((sender==minimumValue)||(sender==maximumValue)) { [sv setMinDimension:[[minimumValue cellAtIndex:0] floatValue] andMaxDimension:[[maximumValue cellAtIndex:0] floatValue]]; // No max limit is indicated by a blank value; don't want to confuse the user with 1000000.0 float dimension = [sv maxDimension]; if (dimension0) { // Don't draw brown background if there are any subviews; draw the set background, if any. NSColor* bg = [[self splitView] background]; if (bg) { [bg set]; NSRectFillUsingOperation(rect,NSCompositeSourceOver); } return; } if (![self asSplitView]) { if ( // Comment out the next line ("YES||") for normal operation. Leaving it in shows the brown background even // under IB's "test interface" mode, which is very convenient for debugging. YES|| ![NSApp isTestingInterface]) { rect = [self bounds]; // Draws the bezel around the subview. static NSRectEdge mySides[] = {NSMinXEdge,NSMaxYEdge,NSMinXEdge,NSMinYEdge,NSMaxXEdge,NSMaxYEdge,NSMaxXEdge,NSMinYEdge}; static float myGrays[] = {0.5,0.5,1.0,1.0,1.0,1.0,0.5,0.5}; rect = NSDrawTiledRects(rect,rect,mySides,myGrays,8); static NSColor* brown = nil; if (!brown) { brown = [[[NSColor alternateSelectedControlColor] colorWithAlphaComponent:0.5] retain]; } [brown set]; NSRectFillUsingOperation(rect,NSCompositeSourceOver); // Sets up the text attributes for the dimension text. static NSDictionary* attributes = nil; if (!attributes) { attributes = [[NSDictionary alloc] initWithObjectsAndKeys:[NSColor whiteColor],NSForegroundColorAttributeName,[NSFont systemFontOfSize:12.0],NSFontAttributeName,nil]; } // Sets up the "nnnpx" string and draws it centered into the subview. NSAttributedString* label = [[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%gpx",[self dimension]] attributes:attributes] autorelease]; NSSize labelSize = [label size]; rect.origin.y += floorf((rect.size.height-labelSize.height)/2.0); rect.origin.x += floorf((rect.size.width-labelSize.width)/2.0); rect.size = labelSize; [label drawInRect:rect]; } } } // This returns the owning splitview. It's guaranteed to return a RBSplitView or nil. - (RBSplitView*)ibSplitView { id document = [NSApp documentForObject:self]; id result = document?[document parentOfObject:self]:[self superview]; if ([result isKindOfClass:[RBSplitView class]]) { return (RBSplitView*)result; } return nil; } // Returns the proper class name for the attribute inspector. - (NSString *)inspectorClassName { return [self asSplitView]?@"RBSplitViewInspector":@"RBSplitSubviewInspector"; } // Returns the class name for the size inspector (for RBSplitSubviews only). We expect never to // find RBSplitSubviews outside a RBSPlitView. - (NSString *)sizeInspectorClassName { return [self ibSplitView]?@"RBSplitSubviewSizeInspector":[super sizeInspectorClassName]; } // Shows the proper name and identifier string in the object outline view, if there's any. - (NSString *)nibLabel:(NSString*)objectName { NSString* name = [self asSplitView]?@"RBSplitView":@"RBSplitSubview"; if ([identifier length]) { return [NSString stringWithFormat:@"%@ (%@)",name,identifier]; } return name; } // RBSplitSubview presents itself as a candidate for accepting a drag. Without this, the subview wouldn't // accept drags at all. RBSplitViews don't accept drags directly. - (id)ibNearestTargetForDrag { return [self asSplitView]?nil:self; } // This combination of responses seems to be optimal... - (BOOL)ibIsContainer { return YES; } - (BOOL)ibSupportsInsideOutSelection { return YES; } - (BOOL)ibDrawFrameWhileResizing { return YES; } - (BOOL)ibSupportsLiveResize { return YES; } - (BOOL)ibShouldShowContainerGuides { return NO; } // This undocumented method fixes the subview redrawing problem! Yay! - (BOOL)editorHandlesCaches { return YES; } // Limits the minimum subview size to 16x16, which shouldn't be too much of hardship. - (NSSize)minimumFrameSizeFromKnobPosition:(IBKnobPosition)position { RBSplitView* sv = [self asSplitView]; if (sv) { unsigned count = [sv numberOfSubviews]; float size = 16.0*count+[sv dividerThickness]*(count-1); return [sv isHorizontal]?NSMakeSize(16.0,size):NSMakeSize(size,16.0); } return NSMakeSize(16.0,16.0); } // This blocks dragging of RBSplitSubviews inside RBSplitViews; non-nested RBSplitViews can be // dragged around normally. - (NSString *)trackerClassNameForEvent:(NSEvent *)event { return [self asSplitView]?[super trackerClassNameForEvent:event]:nil; } // This must return YES to allow receiving drags, or handling mouse clicks. - (BOOL)canEditSelf { return YES; } // The default NSView editor works quite well, but for RBSplitViews we need special handling for clicks. - (void)editSelf:(NSEvent*)theEvent in:(NSView*)viewEditor { if ([theEvent type]==NSLeftMouseDown) { // We handle mouse clicks separately if we're a RBSplitView, to allow for divider dragging in IB. if ([[self asSplitView] ibHandleMouseDown:theEvent in:viewEditor]) { return; } } // Otherwise have the view editor handle it. [(id)super editSelf:theEvent in:viewEditor]; } // This redisplays the receiver and all nested RBSplitSubviews. - (void)ibResetObjectInEditor:(NSView*)viewEditor { [viewEditor resetObject:self]; NSEnumerator* enumerator = [[self subviews] objectEnumerator]; RBSplitSubview* sub; while ((sub = [enumerator nextObject])) { if ([sub isKindOfClass:[RBSplitSubview class]]) { [sub ibResetObjectInEditor:viewEditor]; } } } // This handles undoing over KVC. Or something. :-P - (void)setIsCollapsed:(BOOL)status { if (status) { [self RB___collapse]; } else { [self RB___expandAndSetToMinimum:NO]; } RBSplitView* sv = [self splitView]; if (sv) { [sv adjustSubviews]; id document = [NSApp documentForObject:self]; [document drawObject:sv]; } } @end // This category adds some functionality to RBSplitView to support Interface Builder stuff. // Most of the basic stuff is actually done in RBSplitSubview. @implementation RBSplitView (RBSVIBAdditions) - (RBSplitView*)couplingSplitView { return isCoupled?[self ibSplitView]:nil; } - (RBSplitView*)splitView { return [self ibSplitView]; } // Overrides the corresponding method in RBSplitView to do nothing, since we don't // want to save state inside Interface Builder. - (BOOL)saveState:(BOOL)recurse { return NO; } // These two methods add subviews. (Both override the corresponding methods in RBSplitView). // IB may add dummy NSViews so we allow anything here, with no special handling. - (void)addSubview:(NSView*)aView { [super addSubview:aView]; [self setMustAdjust]; } - (void)addSubview:(NSView *)aView positioned:(NSWindowOrderingMode)place relativeTo:(NSView *)otherView { [super addSubview:aView positioned:place relativeTo:otherView]; [self setMustAdjust]; } // Called just after a RBSplitView has been decoded from either a palette or a nib file. - (id)awakeAfterUsingCoder:(NSCoder*)aDecoder { // At this point the RBSplitView hasn't been inserted into anything, so we ask for // ibSetupSelf to be performed in the run loop, when it will (presumably) have been. [self performSelector:@selector(ibSetupSelf) withObject:nil afterDelay:0.0]; return self; } // Called by ibSetupSelf to handle nested RBSplitViews, which actually never happens at this point, // but it's conceptually interesting to see how it would be done. - (void)ibAttachSubviewsOf:(NSView*)view inDocument:(id)document { NSEnumerator* enumerator = [[view subviews] objectEnumerator]; NSView* sub; // Loop over subviews and attach them to the parent, then recurse on nested RBSplitSubviews while ((sub = [enumerator nextObject])) { if ([sub isKindOfClass:[RBSplitSubview class]]) { [document attachObject:sub toParent:view]; [self ibAttachSubviewsOf:sub inDocument:document]; } } } // Called after a new RBSplitView is dropped into a window. - (void)ibSetupSelf { id document = [NSApp documentForObject:self]; // This may be called spuriously when we're not yet inserted into a document, or from IB's simulation mode, // so we check... if (document) { NSView* object = [document parentOfObject:self]; BOOL vert = [self isVertical]; RBSplitView* sv = nil; // Check if we've been inserted into a RBSplitSubview. if ([object isMemberOfClass:[RBSplitSubview class]]) { sv = [(RBSplitSubview*)object splitView]; if (sv) { if ([[(RBSplitSubview*)object subviews] count]>1) { // We don't nest RBSplitViews directly if there's already another subview there. vert = [sv isHorizontal]; sv = nil; } } } [self setVertical:vert]; if (sv) { // If we're nesting RBSplitViews we copy the attributes of the subview that's being replaced to the nested RBSplitView. [self RB___setFrame:[(RBSplitSubview*)object frame] withFraction:[(RBSplitSubview*)object RB___fraction] notify:NO]; [self setTag:[(RBSplitSubview*)object tag]]; [self setIdentifier:[(RBSplitSubview*)object identifier]]; [self setCanCollapse:[(RBSplitSubview*)object canCollapse]]; [self setMinDimension:[(RBSplitSubview*)object minDimension] andMaxDimension:[(RBSplitSubview*)object maxDimension]]; // The dropped view will replace the existing subview in both the view and in the IB outline hierarchies. [self retain]; [self removeFromSuperviewWithoutNeedingDisplay]; [sv replaceSubview:object with:self]; [document detachObject:self]; [document replaceObject:object withObject:self]; [self release]; } else { // As a convenience, we set the inner resizing springs if we're not nested. [self setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; } // Set up the IB outline hierarchy correctly for our subviews. [self ibAttachSubviewsOf:self inDocument:document]; // Then we recalculate subview dimensions and redraw everything. [self adjustSubviews]; [document drawObject:(sv?sv:object)]; } } // This is called when setting the number of subviews from the inspector. There's some IB stuff // interleaved to tweak the outline hierarchy. - (void)ibSetNumberOfSubviews:(unsigned)count { unsigned now = [self numberOfSubviews]; NSRect frame = NSZeroRect; id document = nil; if (now*)viewEditor { if (!dividers) { return NO; } NSPoint where = [self convertPoint:[theEvent locationInWindow] fromView:nil]; NSArray* subviews = [self subviews]; int subcount = [subviews count]; if (subcount>1) { int i; NSPoint base = NSZeroPoint; // Strangely enough, when this is called the view hierarchy isn't inserted into a window at all, but rather // into a (non-visible) container view, so we have to account for its frame offset. if (![self window]) { NSView* superv = self; // Loop over the superviews and get the outermost one's offset while (superv) { NSRect frame = [superv frame]; if (!(superv = [superv superview])) { base.x += frame.origin.x; base.y += frame.origin.y; } } } BOOL ishor = [self isHorizontal]; where.x -= base.x; where.y -= base.y; // Loop over the subviews and divider rectangles until the mouse is within one. for (i=0;iorigin); [[NSCursor closedHandCursor] push]; // Save state for undoing the divider drag. NSUndoManager* undo = [viewEditor undoManager]; if (undo) { [[undo prepareWithInvocationTarget:self] ibRestoreState:[self stringWithSavedState] in:viewEditor]; [undo setActionName:@"Divider Drag"]; } // Now we loop handling mouse events until we get a mouse up event. while ((theEvent = [NSApp nextEventMatchingMask:NSLeftMouseDownMask|NSLeftMouseDraggedMask|NSLeftMouseUpMask untilDate:[NSDate distantFuture] inMode:NSEventTrackingRunLoopMode dequeue:YES])&&([theEvent type]!=NSLeftMouseUp)) { // Set up a local autorelease pool for the loop to prevent buildup of temporary objects. NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; [self RB___trackMouseEvent:theEvent from:where withBase:base inDivider:i]; if (mustAdjust) { // The mouse was dragged and the subviews changed, so we adjust the subviews, as // several divider rectangles may have changed. [self adjustSubviews]; // Display the changed split view (or its outermost parent, if it's nested) and adjust // to the new cursor coordinate. RBSplitView* sv = [self outermostSplitView]; [sv?sv:self ibResetObjectInEditor:viewEditor]; DIM(where) = DIM(divi->origin)+offset; } [pool release]; } [NSCursor pop]; // Touch the document to show it's been changed. [[viewEditor document] touch]; return YES; } } } } return NO; } // This methods undoes divider drags. - (void)ibRestoreState:(NSString*)string in:(NSView*)viewEditor { NSUndoManager* undo = [viewEditor undoManager]; if (undo) { [[undo prepareWithInvocationTarget:self] ibRestoreState:[self stringWithSavedState] in:viewEditor]; [undo setActionName:@"Divider Drag"]; } [self setStateFromString:string]; [self adjustSubviews]; RBSplitView* sv = [self outermostSplitView]; [sv?sv:self ibResetObjectInEditor:viewEditor]; } @end