Skip to content

Instantly share code, notes, and snippets.

@nilium
Created April 12, 2010 05:31
Show Gist options
  • Save nilium/363294 to your computer and use it in GitHub Desktop.
Save nilium/363294 to your computer and use it in GitHub Desktop.
View for handling layout of subviews in Cocoa
//
// 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
//
// 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
//
// 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
//
// 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