Created
April 12, 2010 05:31
-
-
Save nilium/363294 to your computer and use it in GitHub Desktop.
View for handling layout of subviews in Cocoa
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// NLayoutRule.h | |
// NToolkit | |
// | |
// Created by Noel Cower on 4/10/10. | |
// Copyright (c) 2010 Noel R. Cower | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in | |
// all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
// THE SOFTWARE. | |
// | |
#import <Cocoa/Cocoa.h> | |
@interface NLayoutRule : NSObject { | |
@private | |
NSString *_param; | |
NSView *__weak _base; | |
CGFloat _margin; | |
} | |
- (id)initWithLayoutParam:(NSString*)param base:(NSView*)base margin:(CGFloat)margin; | |
@property (readonly) NSString *layoutParam; | |
@property (readonly) NSView *base; | |
@property (readonly) CGFloat margin; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// NLayoutRule.m | |
// NToolkit | |
// | |
// Created by Noel Cower on 4/10/10. | |
// Copyright (c) 2010 Noel R. Cower | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in | |
// all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
// THE SOFTWARE. | |
// | |
#import "NLayoutRule.h" | |
@implementation NLayoutRule | |
@synthesize layoutParam=_param; | |
@synthesize base=_base; | |
@synthesize margin=_margin; | |
- (id)initWithLayoutParam:(NSString*)param base:(NSView*)base margin:(CGFloat)margin { | |
self = [super init]; | |
if (self) { | |
_param = [param retain]; | |
_base = base; | |
_margin = margin; | |
} | |
return self; | |
} | |
- (void)dealloc { | |
[_param release]; | |
[super dealloc]; | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// NLayoutView.h | |
// NToolkit | |
// | |
// Created by Noel Cower on 4/8/10. | |
// Copyright (c) 2010 Noel R. Cower | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in | |
// all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
// THE SOFTWARE. | |
// | |
#import <Cocoa/Cocoa.h> | |
extern NSString *NLFillHorizontal; // Make the view's X and width match the base's X and width | |
extern NSString *NLFillVertical; // Make the view's Y and height match the base's Y and height | |
extern NSString *NLAlignTop; // Align the top of the view with the top of the base | |
extern NSString *NLAlignBottom; // Align the bottom edge of the view with the bottom edge of the base | |
extern NSString *NLAlignLeft; // Align the left edge of the view with the left edge of the base | |
extern NSString *NLAlignRight; // Align the right edge of the view with the right edge of the base | |
extern NSString *NLCenterTop; // Align the top edge of the view with the vertical center of the base | |
extern NSString *NLCenterBottom; // Align the bottom edge of the view with the vertical center of the base | |
extern NSString *NLCenterLeft; // Align the left edge of the view with the horizontal center of the base | |
extern NSString *NLCenterRight; // Align the right edge of the view with the horizontal center of the base | |
extern NSString *NLBelow; // Align the top edge of the view with the bottom edge of the base | |
extern NSString *NLAbove; // Align the bottom edge of the view with the top edge of the base | |
extern NSString *NLLeftOf; // Align the right edge of the view with the left edge of the base | |
extern NSString *NLRightOf; // Align the left edge of the view with the right edge of the base | |
extern NSString *NLCenterVertical; // Vertically center the view inside the base's frame | |
extern NSString *NLCenterHorizontal; // Horizontally center the view inside the base's frame | |
@interface NLayoutView : NSView <NSCoding> { | |
@private | |
NSMapTable *relations; | |
BOOL layoutQueued; | |
} | |
// When passing the base, nil values will correspond to the NLayoutView instance | |
// Bases can be views in other windows, but this is very, very unrecommended since | |
// NSWindow does not notify for frame changes except when window movement has stalled | |
// Creates a relationship between the subview and its parent NLayoutView using the specified layout parameter and margin | |
- (void)setLayoutParam:(NSString*)param forSubview:(NSView*)view margin:(NSInteger)margin; | |
// Creates a relationship between the subview and its parent NLayoutView using the specified layout parameter and a margin of 0 | |
- (void)setLayoutParam:(NSString*)param forSubview:(NSView*)view; | |
// Creates a relationship between the subview and the base using the specified layout parameter and margin | |
- (void)setLayoutParam:(NSString*)param forSubview:(NSView*)view withBase:(NSView*)base margin:(NSInteger)margin; | |
// Creates a relationship between the subview and the base using the specified layout parameter and a margin of 0 | |
- (void)setLayoutParam:(NSString*)param forSubview:(NSView*)view withBase:(NSView*)base; | |
// Removes the specified layout parameter for the subview | |
- (void)removeLayoutParam:(NSString*)param forSubview:(NSView*)view; | |
// Removes all layout parameters for the specified subview | |
- (void)clearLayoutParamsForSubview:(NSView*)view; | |
// Removes all layout parameters for all subviews | |
- (void)clearLayoutParams; | |
// Force the view to layout its subviews | |
- (void)performLayout; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// NLayoutView.m | |
// NToolkit | |
// | |
// Created by Noel Cower on 4/8/10. | |
// Copyright (c) 2010 Noel R. Cower | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in | |
// all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
// THE SOFTWARE. | |
// | |
#import "NLayoutView.h" | |
#import "NLayoutRule.h" | |
static NSString *NLVRelationsKey = @"Relations"; | |
NSString *NLFillHorizontal = @"FillHorizontal"; | |
NSString *NLFillVertical = @"FillVertical"; | |
NSString *NLAlignTop = @"AlignTop"; | |
NSString *NLAlignBottom = @"AlignBottom"; | |
NSString *NLAlignLeft = @"AlignLeft"; | |
NSString *NLAlignRight = @"AlignRight"; | |
NSString *NLCenterTop = @"CenterTop"; | |
NSString *NLCenterBottom = @"CenterBottom"; | |
NSString *NLCenterLeft = @"CenterLeft"; | |
NSString *NLCenterRight = @"CenterRight"; | |
NSString *NLBelow = @"Below"; | |
NSString *NLAbove = @"Above"; | |
NSString *NLLeftOf = @"LeftOf"; | |
NSString *NLRightOf = @"RightOf"; | |
NSString *NLCenterVertical = @"CenterVertical"; | |
NSString *NLCenterHorizontal = @"CenterHorizontal"; | |
@implementation NLayoutView (Private) | |
- (void)_queueLayout { | |
@synchronized(self) { | |
if (layoutQueued) | |
return; | |
layoutQueued = YES; | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self performLayout]; | |
@synchronized(self) { | |
layoutQueued = NO; | |
} | |
}); | |
} | |
} | |
@end | |
@implementation NLayoutView | |
- (id)initWithCoder:(NSCoder *)aDecoder { | |
self = [super initWithCoder:aDecoder]; | |
if (self) { | |
relations = [aDecoder decodeObjectForKey:NLVRelationsKey]; | |
if (relations == nil) { | |
relations = [NSMapTable mapTableWithWeakToStrongObjects]; | |
} | |
[relations retain]; | |
} | |
return self; | |
} | |
- (id)initWithFrame:(NSRect)frame { | |
self = [super initWithFrame:frame]; | |
if (self) { | |
relations = [[NSMapTable mapTableWithWeakToStrongObjects] retain]; | |
layoutQueued = NO; | |
} | |
return self; | |
} | |
- (void)dealloc { | |
[relations release]; | |
[super dealloc]; | |
} | |
- (void)encodeWithCoder:(NSCoder *)aCoder { | |
[super encodeWithCoder:aCoder]; | |
} | |
- (BOOL)autoresizesSubviews { | |
return YES; | |
} | |
- (void)resizeSubviewsWithOldSize:(NSSize)oldSize { | |
[super resizeSubviewsWithOldSize:oldSize]; | |
[self performLayout]; | |
} | |
- (void)didAddSubview:(NSView *)subview { | |
[relations setObject:[NSMutableArray arrayWithCapacity:4] forKey:subview]; | |
[super didAddSubview:subview]; | |
} | |
- (void)setLayoutParam:(NSString*)param forSubview:(NSView*)view withBase:(NSView*)base margin:(NSInteger)margin { | |
if (base == nil) | |
base = self; | |
NSArray *subviews = [self subviews]; | |
if (![subviews containsObject:view]) { | |
@throw [NSException exceptionWithName:@"NViewNotFound" | |
reason:@"View for layout not found in subviews" | |
userInfo:nil]; | |
} | |
NLayoutRule *rule = [[NLayoutRule alloc] initWithLayoutParam:param base:base margin:margin]; | |
[[relations objectForKey:view] addObject:rule]; | |
[rule release]; | |
if ([base window] != [self window]) { | |
// this is iffy, and I honestly don't think you should ever use a window's contentview as a base | |
[[base window] addObserver:self forKeyPath:@"frame" options:0 context:nil]; | |
} else { | |
[base addObserver:self forKeyPath:@"frame" options:0 context:nil]; | |
} | |
} | |
- (void)setLayoutParam:(NSString*)param forSubview:(NSView*)view withBase:(NSView*)base { | |
[self setLayoutParam:param forSubview:view withBase:base margin:0]; | |
} | |
- (void)setLayoutParam:(NSString*)param forSubview:(NSView*)view margin:(NSInteger)margin { | |
[self setLayoutParam:param forSubview:view withBase:self margin:margin]; | |
} | |
- (void)setLayoutParam:(NSString*)param forSubview:(NSView*)view { | |
[self setLayoutParam:param forSubview:view withBase:self margin:0]; | |
} | |
- (void)removeLayoutParam:(NSString *)param forSubview:(NSView *)view { | |
NSArray *subviews = [self subviews]; | |
if (![subviews containsObject:view]) { | |
@throw [NSException exceptionWithName:@"NViewNotFound" | |
reason:@"View for layout not found in subviews" | |
userInfo:nil]; | |
} | |
NSMutableArray *rules = [relations objectForKey:view]; | |
NSMutableIndexSet *indices = [NSMutableIndexSet indexSet]; | |
[rules enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { | |
if ([[obj layoutParam] isEqual:param]) | |
[indices addIndex:idx]; | |
}]; | |
[rules removeObjectsAtIndexes:indices]; | |
} | |
- (void)clearLayoutParamsForSubview:(NSView *)view { | |
NSArray *subviews = [self subviews]; | |
if (![subviews containsObject:view]) { | |
@throw [NSException exceptionWithName:@"NViewNotFound" | |
reason:@"View for layout not found in subviews" | |
userInfo:nil]; | |
} | |
[[relations objectForKey:view] removeAllObjects]; | |
} | |
- (void)clearLayoutParams { | |
for(NSView *subview in [self subviews]) { | |
[[relations objectForKey:subview] removeAllObjects]; | |
} | |
} | |
- (void)performLayout { | |
NSArray *subviews = [self subviews]; | |
for(NSView *view in subviews) { | |
NSArray *rules = [relations objectForKey:view]; | |
NSRect __block viewframe = [view frame]; | |
BOOL __block leftSet, rightSet, topSet, bottomSet; | |
leftSet = rightSet = topSet = bottomSet = NO; | |
for (NLayoutRule *rule in rules) { | |
NSString *param = rule.layoutParam; | |
NSView *base; | |
base = rule.base; | |
NSRect baseframe; | |
if (base == self) { | |
baseframe = [self bounds]; | |
} else { | |
baseframe = [base frame]; | |
if ([base window] != [self window]) { | |
baseframe = [base convertRect:baseframe toView:nil]; | |
baseframe.origin = [[self window] convertScreenToBase:[[base window] convertBaseToScreen:baseframe.origin]]; | |
baseframe = [self convertRect:baseframe fromView:nil]; | |
} else if ([base superview] != self) { | |
baseframe = [self convertRect:baseframe fromView:[base superview]]; | |
} | |
} | |
if ([param isEqual:NLAlignTop]) { | |
if (bottomSet) { | |
viewframe.size.height = (baseframe.origin.y+baseframe.size.height) - viewframe.origin.y - rule.margin; | |
} else { | |
viewframe.origin.y = baseframe.origin.y+baseframe.size.height - viewframe.size.height - rule.margin; | |
} | |
topSet = YES; | |
} else if ([param isEqual:NLAlignBottom]) { | |
if (topSet) { | |
viewframe.size.height = (viewframe.origin.y + viewframe.size.height) - baseframe.origin.y - rule.margin; | |
} | |
viewframe.origin.y = baseframe.origin.y + rule.margin; | |
rightSet = YES; | |
} else if ([param isEqual:NLAlignLeft]) { | |
if (rightSet) { | |
viewframe.size.width = (viewframe.origin.x + viewframe.size.width) - baseframe.origin.x - rule.margin; | |
} | |
viewframe.origin.x = baseframe.origin.x + rule.margin; | |
leftSet = YES; | |
} else if ([param isEqual:NLAlignRight]) { | |
if (leftSet) { | |
viewframe.size.width = (baseframe.origin.x+baseframe.size.width) - viewframe.origin.x - rule.margin; | |
} else { | |
viewframe.origin.x = baseframe.origin.x+baseframe.size.width - viewframe.size.width - rule.margin; | |
} | |
rightSet = YES; | |
} else if ([param isEqual:NLFillHorizontal]) { | |
CGFloat margin = fmax(rule.margin, 0.0); // permit only positive margins in the case of fills | |
viewframe.size.width = baseframe.size.width - margin*2; | |
viewframe.origin.x = baseframe.origin.x + margin; | |
leftSet = YES; | |
rightSet = YES; | |
} else if ([param isEqual:NLFillVertical]) { | |
CGFloat margin = fmax(rule.margin, 0.0); | |
viewframe.origin.y = baseframe.origin.y + margin; | |
viewframe.size.height = baseframe.size.height - margin*2; | |
topSet = YES; | |
bottomSet = YES; | |
} else if ([param isEqual:NLCenterVertical] && !topSet && !bottomSet) { | |
viewframe.origin.y = baseframe.origin.y + (baseframe.size.height - viewframe.size.height)*.5 - rule.margin; | |
topSet = YES; | |
bottomSet = YES; | |
} else if ([param isEqual:NLCenterHorizontal] && !leftSet && !rightSet) { | |
viewframe.origin.x = baseframe.origin.x + (baseframe.size.width - viewframe.size.width)*.5 - rule.margin; | |
topSet = YES; | |
bottomSet = YES; | |
} else if ([param isEqual:NLAbove]) { | |
if (topSet) { | |
viewframe.size.height = (viewframe.origin.y+viewframe.size.height)-(baseframe.origin.y+baseframe.size.height)-rule.margin; | |
} | |
viewframe.origin.y = baseframe.origin.y+baseframe.size.height+rule.margin; | |
bottomSet = YES; | |
} else if ([param isEqual:NLBelow]) { | |
if (bottomSet) { | |
viewframe.size.height = baseframe.origin.y - viewframe.origin.y - rule.margin; | |
} else { | |
viewframe.origin.y = baseframe.origin.y - viewframe.size.height - rule.margin; | |
} | |
topSet = YES; | |
} else if ([param isEqual:NLLeftOf]) { | |
if (leftSet) { | |
viewframe.size.width = baseframe.origin.x - viewframe.origin.x - rule.margin; | |
} else { | |
viewframe.origin.x = baseframe.origin.x - viewframe.size.width - rule.margin; | |
} | |
rightSet = YES; | |
} else if ([param isEqual:NLRightOf]) { | |
if (rightSet) { | |
viewframe.size.width = (viewframe.origin.x+viewframe.size.width)-(baseframe.origin.x+baseframe.size.width)-rule.margin; | |
} | |
viewframe.origin.x = baseframe.origin.x+baseframe.size.width+rule.margin; | |
leftSet = YES; | |
} else if ([param isEqual:NLCenterTop]) { | |
if (bottomSet) { | |
viewframe.size.height = (baseframe.origin.y+baseframe.size.height*.5) - viewframe.origin.y - rule.margin; | |
} else { | |
viewframe.origin.y = baseframe.origin.y+baseframe.size.height*.5 - viewframe.size.height - rule.margin; | |
} | |
topSet = YES; | |
} else if ([param isEqual:NLCenterBottom]) { | |
if (topSet) { | |
viewframe.size.height = (viewframe.origin.y + viewframe.size.height) - baseframe.origin.y - baseframe.size.height * .5 - rule.margin; | |
} | |
viewframe.origin.y = baseframe.origin.y + baseframe.size.height * .5 + rule.margin; | |
rightSet = YES; | |
} else if ([param isEqual:NLCenterLeft]) { | |
if (rightSet) { | |
viewframe.size.width = (viewframe.origin.x + viewframe.size.width) - baseframe.origin.x - baseframe.size.width * .5 - rule.margin; | |
} | |
viewframe.origin.x = baseframe.origin.x + baseframe.size.width * .5 + rule.margin; | |
leftSet = YES; | |
} else if ([param isEqual:NLCenterRight]) { | |
if (leftSet) { | |
viewframe.size.width = (baseframe.origin.x+baseframe.size.width*.5) - viewframe.origin.x - rule.margin; | |
} else { | |
viewframe.origin.x = baseframe.origin.x+baseframe.size.width*.5 - viewframe.size.width - rule.margin; | |
} | |
rightSet = YES; | |
} | |
} | |
if (!NSEqualRects([view frame], viewframe)) | |
[view setFrame:viewframe]; | |
} | |
} | |
- (BOOL)isFlipped { | |
return NO; | |
} | |
- (void)observeValueForKeyPath:(NSString*)keypath ofObject:(id)obj change:(NSDictionary*)change context:(void*)ctx { | |
if (layoutQueued) | |
return; | |
for(NSView *view in relations) { | |
NSArray *rules = [relations objectForKey:view]; | |
for (NLayoutRule *rule in rules) { | |
if (rule.base == obj || [rule.base window] == obj) { | |
[self _queueLayout]; | |
return; | |
} | |
} | |
} | |
// base no longer used, no longer needs to be observed | |
[obj removeObserver:self forKeyPath:keypath]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment