Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 02883f8b88
Fetching contributors…

Cannot retrieve contributors at this time

675 lines (518 sloc) 23.753 kb
//
// PSBaseSubtreeView.m
// PSTreeGraphView
//
// Created by Ed Preston on 7/25/10.
// Copyright 2010 Preston Software. All rights reserved.
//
//
// This is a port of the sample code from Max OS X to iOS (iPad).
//
// WWDC 2010 Session 141, “Crafting Custom Cocoa Views”
//
#import "PSBaseSubtreeView.h"
#import "PSBaseBranchView.h"
#import "PSBaseTreeGraphView.h"
// for CALayer definition
#import <QuartzCore/QuartzCore.h>
#define CENTER_COLLAPSED_SUBTREE_ROOT 1
static UIColor *subtreeBorderColor(void)
{
return [[UIColor colorWithRed:0.0f green:0.5f blue:0.0f alpha:1.0f] retain];
}
static CGFloat subtreeBorderWidth(void)
{
return 2.0f;
}
@interface PSBaseSubtreeView ()
{
@private
// Model
id <PSTreeGraphModelNode> modelNode_; // the model node that nodeView represents
// Views
UIView *nodeView_; // the subview of this SubtreeView that shows a representation
// of the modelNode
PSBaseBranchView *connectorsView_; // the view that shows connections from nodeView to its child nodes
// State
BOOL expanded_; // YES if this subtree is expanded to show its descendants;
// NO if it's been collapsed to show just its root node
BOOL needsGraphLayout_; // YES if this SubtreeView needs to position its child views
// and assess its size; NO if we're sure its layout is up to date
}
- (CGSize) layoutExpandedGraph;
- (CGSize) layoutCollapsedGraph;
@end
@implementation PSBaseSubtreeView
#pragma mark - Attributes
@synthesize modelNode = modelNode_;
@synthesize nodeView = nodeView_;
@synthesize expanded = expanded_;
- (void) setExpanded:(BOOL)flag
{
if (expanded_ != flag) {
// Remember this SubtreeView's new state.
expanded_ = flag;
// Notify the TreeGraph we need layout.
[[self enclosingTreeGraph] setNeedsGraphLayout];
// Expand or collapse subtrees recursively.
for (UIView *subview in [self subviews]) {
if ([subview isKindOfClass:[PSBaseSubtreeView class]]) {
[(PSBaseSubtreeView *)subview setExpanded:expanded_];
}
}
}
}
- (IBAction) toggleExpansion:(id)sender
{
[UIView beginAnimations:@"TreeNodeExpansion" context:nil];
// [UIView setAnimationDuration:8.0];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
[self setExpanded:![self isExpanded]];
[[self enclosingTreeGraph] layoutGraphIfNeeded];
if ( [self modelNode] != nil ) {
NSSet *visibleSet = [NSSet setWithObject:[self modelNode]];
[[self enclosingTreeGraph] scrollModelNodesToVisible:visibleSet animated:NO];
}
[UIView commitAnimations];
}
- (BOOL) isLeaf
{
return [[[self modelNode] childModelNodes] count] == 0;
}
#pragma mark - Instance Initialization
- initWithModelNode:(id <PSTreeGraphModelNode> )newModelNode
{
NSParameterAssert(newModelNode);
self = [super initWithFrame:CGRectMake(10.0, 10.0, 100.0, 25.0)];
if (self) {
// Initialize ivars directly. As a rule, it's best to avoid invoking accessors from an -init...
// method, since they may wrongly expect the instance to be fully formed.
expanded_ = YES;
needsGraphLayout_ = YES;
// autoresizesSubviews defaults to YES. We don't want autoresizing, which would interfere
// with the explicit layout we do, so we switch it off for SubtreeView instances.
[self setAutoresizesSubviews:NO];
self.modelNode = newModelNode;
connectorsView_ = [[PSBaseBranchView alloc] initWithFrame:CGRectZero];
if (connectorsView_) {
[connectorsView_ setAutoresizesSubviews:YES];
// if we dont redraw lines, they get out of place
[connectorsView_ setContentMode:UIViewContentModeRedraw];
[connectorsView_ setOpaque:YES];
[self addSubview:connectorsView_];
}
}
return self;
}
- (PSBaseTreeGraphView *) enclosingTreeGraph
{
UIView *ancestor = [self superview];
while (ancestor) {
if ([ancestor isKindOfClass:[PSBaseTreeGraphView class]]) {
return (PSBaseTreeGraphView *)ancestor;
}
ancestor = [ancestor superview];
}
return nil;
}
#pragma mark - Resource Management
- (void) dealloc
{
// [nodeView release]; // not retained, since an IBOutlet
[connectorsView_ release];
[modelNode_ release];
[super dealloc];
}
#pragma mark - Layout
@synthesize needsGraphLayout = needsGraphLayout_;
- (void) recursiveSetNeedsGraphLayout
{
[self setNeedsGraphLayout:YES];
for (UIView *subview in [self subviews]) {
if ([subview isKindOfClass:[PSBaseSubtreeView class]]) {
[(PSBaseSubtreeView *)subview recursiveSetNeedsGraphLayout];
}
}
}
- (CGSize) sizeNodeViewToFitContent
{
// TODO: Node size is hardwired for now, but the layout algorithm could accommodate
// variable-sized nodes if we implement size-to-fit for nodes.
return [self.nodeView frame].size;
}
- (CGSize) layoutGraphIfNeeded
{
// Return early if layout not needed
if ( !self.needsGraphLayout )
return [self frame].size;
// Do the layout
CGSize selfTargetSize;
if ( [self isExpanded] ) {
selfTargetSize = [self layoutExpandedGraph];
} else {
selfTargetSize = [self layoutCollapsedGraph];
}
// Mark as having completed layout.
self.needsGraphLayout = NO;
// Return our new size.
return selfTargetSize;
}
- (CGSize) layoutExpandedGraph
{
CGSize selfTargetSize;
PSBaseTreeGraphView *treeGraph = [self enclosingTreeGraph];
CGFloat parentChildSpacing = [treeGraph parentChildSpacing];
CGFloat siblingSpacing = [treeGraph siblingSpacing];
PSTreeGraphOrientationStyle treeOrientation = [treeGraph treeGraphOrientation];
// Size this SubtreeView's nodeView to fit its content. Our tree layout model assumes the assessment
// of a node's natural size is a function of intrinsic properties of the node, and isn't influenced
// by any other nodes or layout in the tree.
CGSize rootNodeViewSize = [self sizeNodeViewToFitContent];
// Recurse to lay out each of our child SubtreeViews (and their non-collapsed descendants in turn).
// Knowing the sizes of our child SubtreeViews will tell us what size this SubtreeView needs to be
// to contain them (and our nodeView and connectorsView).
NSArray *subviews = [self subviews];
NSInteger count = [subviews count];
NSInteger index;
NSUInteger subtreeViewCount = 0;
CGFloat maxWidth = 0.0f;
CGFloat maxHeight = 0.0f;
CGPoint nextSubtreeViewOrigin = CGPointZero;
if ( treeOrientation == PSTreeGraphOrientationStyleHorizontal ) {
nextSubtreeViewOrigin = CGPointMake(rootNodeViewSize.width + parentChildSpacing, 0.0f);
} else {
nextSubtreeViewOrigin = CGPointMake(0.0f, rootNodeViewSize.height + parentChildSpacing);
}
for (index = count - 1; index >= 0; index--) {
UIView *subview = [subviews objectAtIndex:index];
if ([subview isKindOfClass:[PSBaseSubtreeView class]]) {
++subtreeViewCount;
// Unhide the view if needed.
[subview setHidden:NO];
// Recursively layout the subtree, and obtain the SubtreeView's resultant size.
CGSize subtreeViewSize = [(PSBaseSubtreeView *)subview layoutGraphIfNeeded];
// Position the SubtreeView.
// [(animateLayout ? [subview animator] : subview) setFrameOrigin:nextSubtreeViewOrigin];
if ( treeOrientation == PSTreeGraphOrientationStyleHorizontal ) {
// Since SubtreeView is unflipped, lay out our child SubtreeViews going upward from our
// bottom edge, from last to first.
subview.frame = CGRectMake( nextSubtreeViewOrigin.x,
nextSubtreeViewOrigin.y,
subtreeViewSize.width,
subtreeViewSize.height );
// Advance nextSubtreeViewOrigin for the next SubtreeView.
nextSubtreeViewOrigin.y += subtreeViewSize.height + siblingSpacing;
// Keep track of the widest SubtreeView width we encounter.
if (maxWidth < subtreeViewSize.width) {
maxWidth = subtreeViewSize.width;
}
} else {
// TODO: Lay out our child SubtreeViews going from our left edge, last to first. SWITCH ME
subview.frame = CGRectMake( nextSubtreeViewOrigin.x,
nextSubtreeViewOrigin.y,
subtreeViewSize.width,
subtreeViewSize.height );
// Advance nextSubtreeViewOrigin for the next SubtreeView.
nextSubtreeViewOrigin.x += subtreeViewSize.width + siblingSpacing;
// Keep track of the widest SubtreeView width we encounter.
if (maxHeight < subtreeViewSize.height) {
maxHeight = subtreeViewSize.height;
}
}
}
}
// Calculate the total height of all our SubtreeViews, including the vertical spacing between them.
// We have N child SubtreeViews, but only (N-1) gaps between them, so subtract 1 increment of
// siblingSpacing that was added by the loop above.
CGFloat totalHeight = 0.0f;
CGFloat totalWidth = 0.0f;
if ( treeOrientation == PSTreeGraphOrientationStyleHorizontal ) {
totalHeight = nextSubtreeViewOrigin.y;
if (subtreeViewCount > 0) {
totalHeight -= siblingSpacing;
}
} else {
totalWidth = nextSubtreeViewOrigin.x;
if (subtreeViewCount > 0) {
totalWidth -= siblingSpacing;
}
}
// Size self to contain our nodeView all our child SubtreeViews, and position our
// nodeView and connectorsView.
if (subtreeViewCount > 0) {
// Determine our width and height.
if ( treeOrientation == PSTreeGraphOrientationStyleHorizontal ) {
selfTargetSize = CGSizeMake(rootNodeViewSize.width + parentChildSpacing + maxWidth,
MAX(totalHeight, rootNodeViewSize.height) );
} else {
selfTargetSize = CGSizeMake(MAX(totalWidth, rootNodeViewSize.width),
rootNodeViewSize.height + parentChildSpacing + maxHeight);
}
// Resize to our new width and height.
// [(animateLayout ? [self animator] : self) setFrameSize:selfTargetSize];
self.frame = CGRectMake(self.frame.origin.x,
self.frame.origin.y,
selfTargetSize.width,
selfTargetSize.height );
CGPoint nodeViewOrigin = CGPointZero;
if ( treeOrientation == PSTreeGraphOrientationStyleHorizontal ) {
// Position our nodeView vertically centered along the left edge of our new bounds.
nodeViewOrigin = CGPointMake(0.0f, 0.5f * (selfTargetSize.height - rootNodeViewSize.height));
} else {
// Position our nodeView horizontally centered along the top edge of our new bounds.
nodeViewOrigin = CGPointMake(0.5f * (selfTargetSize.width - rootNodeViewSize.width), 0.0f);
}
// Pixel-align its position to keep its rendering crisp.
CGPoint windowPoint = [self convertPoint:nodeViewOrigin toView:nil];
windowPoint.x = round(windowPoint.x);
windowPoint.y = round(windowPoint.y);
nodeViewOrigin = [self convertPoint:windowPoint fromView:nil];
self.nodeView.frame = CGRectMake(nodeViewOrigin.x,
nodeViewOrigin.y,
self.nodeView.frame.size.width,
self.nodeView.frame.size.height );
// Position, show our connectorsView and button.
// TODO: Can shrink height a bit on top and bottom ends, since the connecting lines
// meet at the nodes' vertical centers
// NOTE: It may be better to stretch the content if a collapse animation is added?
// Be sure to test. Given the size, and how they just contain the lines, it seems
// best to just redraw these things.
// [_connectorsView setContentMode:UIViewContentModeScaleToFill ];
if ( treeOrientation == PSTreeGraphOrientationStyleHorizontal ) {
connectorsView_.frame = CGRectMake(rootNodeViewSize.width,
0.0f,
parentChildSpacing,
selfTargetSize.height );
} else {
connectorsView_.frame = CGRectMake(0.0f,
rootNodeViewSize.height,
selfTargetSize.width,
parentChildSpacing );
}
// NOTE: Enable this line if a collapse animation is added (line below not used)
// [_connectorsView setContentMode:UIViewContentModeRedraw];
[connectorsView_ setHidden:NO];
} else {
// No SubtreeViews; this is a leaf node.
// Size self to exactly wrap nodeView, hide connectorsView, and hide the button.
selfTargetSize = rootNodeViewSize;
self.frame = CGRectMake(self.frame.origin.x,
self.frame.origin.y,
selfTargetSize.width,
selfTargetSize.height );
self.nodeView.frame = CGRectMake(0.0f,
0.0f,
self.nodeView.frame.size.width,
self.nodeView.frame.size.height );
[connectorsView_ setHidden:YES];
}
// Return our new size.
return selfTargetSize;
}
- (CGSize) layoutCollapsedGraph
{
// This node is collapsed. Everything will be collapsed behind the leafNode
CGSize selfTargetSize = [self sizeNodeViewToFitContent];
self.frame = CGRectMake(self.frame.origin.x,
self.frame.origin.y,
selfTargetSize.width,
selfTargetSize.height );
for (UIView *subview in [self subviews]) {
if ([subview isKindOfClass:[PSBaseSubtreeView class]]) {
[(PSBaseSubtreeView *)subview layoutGraphIfNeeded];
subview.frame = CGRectMake(0.0f,
0.0f,
subview.frame.size.width,
subview.frame.size.height );
[subview setHidden:YES];
} else if (subview == connectorsView_) {
// NOTE: It may be better to stretch the content if a collapse animation is added?
// Be sure to test. Given the size, and how they just contain the lines, it seems
// best to just redraw these things.
// [_connectorsView setContentMode:UIViewContentModeScaleToFill ];
PSTreeGraphOrientationStyle treeOrientation = [[self enclosingTreeGraph] treeGraphOrientation];
if ( treeOrientation == PSTreeGraphOrientationStyleHorizontal ) {
connectorsView_.frame = CGRectMake(0.0f,
0.5f * selfTargetSize.height,
0.0f,
0.0f );
} else {
connectorsView_.frame = CGRectMake(0.5f * selfTargetSize.width,
0.0f,
0.0f,
0.0f );
}
// NOTE: Enable this line if a collapse animation is added (line below not used)
// [_connectorsView setContentMode:UIViewContentModeRedraw];
[subview setHidden:YES];
} else if (subview == nodeView_) {
subview.frame = CGRectMake(0.0f,
0.0f,
selfTargetSize.width,
selfTargetSize.height );
}
}
// Return our new size.
return selfTargetSize;
}
#pragma mark - Drawing
- (void) updateSubtreeBorder
{
CALayer *layer = [self layer];
if (layer) {
// Disable implicit animations during these layer property changes, to make them take effect immediately.
// BOOL actionsWereDisabled = [CATransaction disableActions];
// [CATransaction setDisableActions:YES];
// If the enclosing TreeGraph has its "showsSubtreeFrames" debug feature enabled,
// configure the backing layer to draw its border programmatically. This is much more efficient
// than allocating a backing store for each SubtreeView's backing layer, only to stroke a simple
// rectangle into that backing store.
PSBaseTreeGraphView *treeGraph = [self enclosingTreeGraph];
if ([treeGraph showsSubtreeFrames]) {
[layer setBorderWidth:subtreeBorderWidth()];
[layer setBorderColor:[subtreeBorderColor() CGColor]];
} else {
[layer setBorderWidth:0.0];
}
// [CATransaction setDisableActions:actionsWereDisabled];
}
}
#pragma mark - Invalidation
- (void) recursiveSetConnectorsViewsNeedDisplay
{
// Mark this SubtreeView's connectorsView as needing display.
[connectorsView_ setNeedsDisplay];
// Recurse for descendant SubtreeViews.
NSArray *subviews = [self subviews];
for (UIView *subview in subviews) {
if ([subview isKindOfClass:[PSBaseSubtreeView class]]) {
[(PSBaseSubtreeView *)subview recursiveSetConnectorsViewsNeedDisplay];
}
}
}
- (void) resursiveSetSubtreeBordersNeedDisplay
{
if ( [self layer] ) {
// We only need this if layer-backed. When we have a backing layer, we use the
// layer's "border" properties to draw the subtree debug border.
[self updateSubtreeBorder];
// Recurse for descendant SubtreeViews.
NSArray *subviews = [self subviews];
for (UIView *subview in subviews) {
if ([subview isKindOfClass:[PSBaseSubtreeView class]]) {
[(PSBaseSubtreeView *)subview updateSubtreeBorder];
}
}
} else {
[self setNeedsDisplay];
}
}
#pragma mark - Selection State
- (BOOL) nodeIsSelected
{
return [[[self enclosingTreeGraph] selectedModelNodes] containsObject:[self modelNode]];
}
#pragma mark - Node Hit-Testing
- (id <PSTreeGraphModelNode> ) modelNodeAtPoint:(CGPoint)p
{
// Check for intersection with our subviews, enumerating them in reverse order to get
// front-to-back ordering. We could use UIView's -hitTest: method here, but we don't
// want to bother hit-testing deeper than the nodeView level.
NSArray *subviews = [self subviews];
NSInteger count = [subviews count];
NSInteger index;
for (index = count - 1; index >= 0; index--) {
UIView *subview = [subviews objectAtIndex:index];
// CGRect subviewBounds = [subview bounds];
CGPoint subviewPoint = [subview convertPoint:p fromView:self];
//
// if (CGPointInRect(subviewPoint, subviewBounds)) {
if ( [subview pointInside:subviewPoint withEvent:nil] ) {
if (subview == [self nodeView]) {
return [self modelNode];
} else if ( [subview isKindOfClass:[PSBaseSubtreeView class]] ) {
return [(PSBaseSubtreeView *)subview modelNodeAtPoint:subviewPoint];
} else {
// Ignore subview. It's probably a BranchView.
}
}
}
// We didn't find a hit.
return nil;
}
- (id <PSTreeGraphModelNode> ) modelNodeClosestToY:(CGFloat)y
{
// Do a simple linear search of our subviews, ignoring non-SubtreeViews. If performance was ever
// an issue for this code, we could take advantage of knowing the layout order of the nodes to do
// a sort of binary search.
NSArray *subviews = [self subviews];
PSBaseSubtreeView *subtreeViewWithClosestNodeView = nil;
CGFloat closestNodeViewDistance = MAXFLOAT;
for (UIView *subview in subviews) {
if ([subview isKindOfClass:[PSBaseSubtreeView class]]) {
UIView *childNodeView = [(PSBaseSubtreeView *)subview nodeView];
if (childNodeView) {
CGRect rect = [self convertRect:[childNodeView bounds] fromView:childNodeView];
CGFloat nodeViewDistance = fabs(y - CGRectGetMidY(rect));
if (nodeViewDistance < closestNodeViewDistance) {
closestNodeViewDistance = nodeViewDistance;
subtreeViewWithClosestNodeView = (PSBaseSubtreeView *)subview;
}
}
}
}
return [subtreeViewWithClosestNodeView modelNode];
}
- (id <PSTreeGraphModelNode> ) modelNodeClosestToX:(CGFloat)x
{
// Do a simple linear search of our subviews, ignoring non-SubtreeViews. If performance was ever
// an issue for this code, we could take advantage of knowing the layout order of the nodes to do
// a sort of binary search.
NSArray *subviews = [self subviews];
PSBaseSubtreeView *subtreeViewWithClosestNodeView = nil;
CGFloat closestNodeViewDistance = MAXFLOAT;
for (UIView *subview in subviews) {
if ([subview isKindOfClass:[PSBaseSubtreeView class]]) {
UIView *childNodeView = [(PSBaseSubtreeView *)subview nodeView];
if (childNodeView) {
CGRect rect = [self convertRect:[childNodeView bounds] fromView:childNodeView];
CGFloat nodeViewDistance = fabs(x - CGRectGetMidX(rect));
if (nodeViewDistance < closestNodeViewDistance) {
closestNodeViewDistance = nodeViewDistance;
subtreeViewWithClosestNodeView = (PSBaseSubtreeView *)subview;
}
}
}
}
return [subtreeViewWithClosestNodeView modelNode];
}
#pragma mark - Debugging
// These methods access private member variables so they do not invoke app logic. We only
// want to dump the current state. Change this if these properties are synthesized.
- (NSString *) description
{
return [NSString stringWithFormat:@"SubtreeView<%@>", [modelNode_ description]];
}
- (NSString *) nodeSummary
{
return [NSString stringWithFormat:@"f=%@ %@", NSStringFromCGRect([nodeView_ frame]), [modelNode_ description]];
}
- (NSString *) treeSummaryWithDepth:(NSInteger)depth
{
NSEnumerator *subviewsEnumerator = [[self subviews] objectEnumerator];
UIView *subview;
NSMutableString *description = [NSMutableString string];
NSInteger i;
for (i = 0; i < depth; i++) {
[description appendString:@" "];
}
[description appendFormat:@"%@\n", [self nodeSummary]];
while (subview = [subviewsEnumerator nextObject]) {
if ([subview isKindOfClass:[PSBaseSubtreeView class]]) {
[description appendString:[(PSBaseSubtreeView *)subview treeSummaryWithDepth:(depth + 1)]];
}
}
return description;
}
@end
Jump to Line
Something went wrong with that request. Please try again.