how to set up multiple expandable sections in a scrollview
I need a layout similar to IB’s Inspectors, where there are multiple expandable sections, expanded by disclosure triangles, all of which are contained within a scrollview.
If only one expandable section were needed, I’d be there already: I put the expandable section in an NSBox, give the box and everything above it a top strut but no bottom strut, and give everything below it a bottom strut but no top strut. Then I set up the disclosure triangle’s action to show/hide the box and to adjust the frame size of the scrollview’s document view.
But there开发者_JS百科 doesn’t seem to be a way to set the struts for multiple boxes. Either closing the disclosure triangles leaves gaps, or the boxes slide on top of each other.
I did take a look at NSOutlineView, but that’s a table; it can’t have subviews like comboboxes and buttons. (Or maybe it can, if I make custom cells, something I haven’t done yet — but I suspect those are not suited for full-featured layout.)
Can somebody point me in the right direction?
In case anybody else runs into this design challenge, I’ll post the IBAction I came up with.
This scheme uses regular, unflipped views. That is, the origin is at the lower left-hand corner. When the docSize is changed, space is added or removed from the top.
While for a single disclosure triangle, some controls need top struts and some need bottom struts, for this scheme, all controls must have both top and bottom struts. Otherwise they adjust themselves automatically, throwing everything off.
As noted at the end, there’s a considerable challenge involved when fully scrolled to the bottom. But that’s another chapter…
/**
Action called upon clicking a disclosure triangle.
Hides or discloses the box associated with the disclosure triangle.
*/
- (IBAction) discloseBox:(id)sender {
// Determine which box is governed by this disclosure triangle.
NSBox *boxTarget;
switch ([sender tag]) {
case kDT_Source:
boxTarget = self.boxSourceInfo;
break;
case kDT_Tweak:
boxTarget = self.boxTweak;
break;
case kDT_Series:
boxTarget = self.boxSeries;
break;
case kDT_Abbrevs:
boxTarget = self.boxAbbreviations;
break;
case kDT_Flag:
boxTarget = self.boxFlaggingAndComments;
break;
default:
break;
}
// Get size info on the content with and without the box.
NSView *docView = [self.svEditorMain documentView];
NSSize docSize = [docView frame].size;
CGFloat fHeightChange = [boxTarget frame].size.height;
// Before actually changing the content size, record what point is currently at the top of the window.
CGFloat dropFromTop_preChange = [self getCurrentDropFromTop];
// If the state is now on, make the box visible.
// If the state is now off, hide the box and make the height change negative.
switch ([sender state]) {
case NSOnState:
[boxTarget setHidden:NO];
break;
case NSOffState:
[boxTarget setHidden:YES];
fHeightChange *= -1;
break;
default:
break;
}
// Use the height change to prepare the adjusted docSize, but don't apply it yet.
NSSize adjustedDocSize = NSMakeSize(docSize.width, (docSize.height + fHeightChange));
// Make sure the adjustees array is populated.
[self populateVerticalAdjusteesArray];
// If the height change is positive, expand the content size before adjusting the origins, so that the origins will have space to move up into. (Space will be added at top.)
if (fHeightChange > 0)
[docView setFrameSize:adjustedDocSize];
// Get the current, pre-change Y origin of the target box.
CGFloat boxOriginY_preChange = [boxTarget frame].origin.y;
// Loop through the adjustees, adjusting their height.
NSControl *control;
CGFloat originX;
CGFloat originY;
for (NSUInteger ui = 0; ui < [self.carrVerticalAdjustees count]; ++ui) {
control = [self.carrVerticalAdjustees objectAtIndex:ui];
originY = [control frame].origin.y;
// Adjust all controls that are above the origin Y of the target box (but do nothing to the target box itself).
// Since coordinate system places origin at lower left corner, higher numbers are higher controls.
if (originY > boxOriginY_preChange) {
originX = [control frame].origin.x; // get originX just so you can assemble a new NSPoint
originY += fHeightChange;
[control setFrameOrigin:NSMakePoint(originX, originY)];
}
// Since the array was assembled in order from top down, once a member is encountered whose origin is below the target's, we're done.
else
break;
}
// If the height change is negative, contract the content size only now, after the origins have all been safely adjusted downwards. (Space will be removed at top.)
if (fHeightChange < 0)
[docView setFrameSize:adjustedDocSize];
// Left to its own devices, the scroller will maintain the old distance from the bottom, so whatever is under the cursor will jump up or down. To prevent this, scroll the content to maintain the old distance from the TOP, as recorded above.
// (This won't work if user is already scrolled to the bottom and then collapses a box. The only way to maintain the scroll position then would be to add blank space at the bottom, which would require moving the origin of all the content up. And then you would want to reverse those changes as soon as the blank space scrolled back out of view, which would require adding some trackers and monitoring NSViewBoundsDidChangeNotification.)
[self scrollMainTo:dropFromTop_preChange];
}
Check out InspectorKit. If you're using Xcode 4, however, keep in mind it no longer supports IBPlugins, so you'd have to use InspectorKit in code (and do without the drag-and-drop convenience of the Interface Builder plug-in).
精彩评论