/* * CPBox.j * AppKit * * Created by Ross Boucher. * Copyright 2009, 280 North, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @import "CPTextField.j" @import "CPView.j" // CPBoxType @typedef CPBoxType CPBoxPrimary = 0; CPBoxSecondary = 1; CPBoxSeparator = 2; CPBoxOldStyle = 3; CPBoxCustom = 4; // CPBorderType @typedef CPBorderType CPNoBorder = 0; CPLineBorder = 1; CPBezelBorder = 2; CPGrooveBorder = 3; // CPTitlePosition @typedef CPTitlePosition CPNoTitle = 0; CPAboveTop = 1; CPAtTop = 2; CPBelowTop = 3; CPAboveBottom = 4; CPAtBottom = 5; CPBelowBottom = 6; /*! @ingroup appkit @class CPBox A CPBox is a simple view which can display a border. */ @implementation CPBox : CPView { CPBoxType _boxType; CPBorderType _borderType; CPView _contentView; CPString _title @accessors(getter=title); int _titlePosition @accessors(getter=titlePosition); CPTextField _titleView; } + (Class)_binderClassForBinding:(CPString)aBinding { if ([aBinding hasPrefix:CPDisplayPatternTitleBinding]) return [CPTitleWithPatternBinding class]; return [super _binderClassForBinding:aBinding]; } + (CPString)defaultThemeClass { return @"box"; } + (CPDictionary)themeAttributes { return @{ @"background-color": [CPNull null], @"border-color": [CPNull null], @"border-width": 1.0, @"corner-radius": 3.0, @"inner-shadow-offset": CGSizeMakeZero(), @"inner-shadow-size": 6.0, @"inner-shadow-color": [CPNull null], @"content-margin": CGSizeMakeZero(), }; } + (id)boxEnclosingView:(CPView)aView { var box = [[self alloc] initWithFrame:CGRectMakeZero()], enclosingView = [aView superview]; [box setAutoresizingMask:[aView autoresizingMask]]; [box setFrameFromContentFrame:[aView frame]]; [enclosingView replaceSubview:aView with:box]; [box setContentView:aView]; return box; } - (id)initWithFrame:(CGRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { _borderType = CPBezelBorder; _titlePosition = CPNoTitle; _titleView = [CPTextField labelWithTitle:@""]; _contentView = [[CPView alloc] initWithFrame:[self bounds]]; [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; [self setAutoresizesSubviews:YES]; [self addSubview:_contentView]; } return self; } // Configuring Boxes /*! Returns the receiver's border rectangle. @return the border rectangle of the box */ - (CGRect)borderRect { return [self bounds]; } /*! Returns the receiver's border type. Possible values are:
CPNoBorder
CPLineBorder
CPBezelBorder
CPGrooveBorder
@return the border type of the box
*/
- (CPBorderType)borderType
{
return _borderType;
}
/*!
Sets the receiver's border type. Valid values are:
CPNoBorder
CPLineBorder
CPBezelBorder
CPGrooveBorder
@param borderType the border type to use
*/
- (void)setBorderType:(CPBorderType)aBorderType
{
if (_borderType === aBorderType)
return;
_borderType = aBorderType;
[self setNeedsDisplay:YES];
}
/*!
Returns the receiver's box type. Possible values are:
CPBoxPrimary
CPBoxSecondary
CPBoxSeparator
CPBoxOldStyle
CPBoxCustom
(In the current implementation, all values act the same except CPBoxSeparator.)
@return the box type of the box.
*/
- (CPBoxType)boxType
{
return _boxType;
}
/*!
Sets the receiver's box type. Valid values are:
CPBoxPrimary
CPBoxSecondary
CPBoxSeparator
CPBoxOldStyle
CPBoxCustom
(In the current implementation, all values act the same except CPBoxSeparator.)
@param aBoxType the box type of the box.
*/
- (void)setBoxType:(CPBoxType)aBoxType
{
if (_boxType === aBoxType)
return;
_boxType = aBoxType;
[self setNeedsDisplay:YES];
}
- (CPColor)borderColor
{
return [self valueForThemeAttribute:@"border-color"];
}
- (void)setBorderColor:(CPColor)color
{
if ([color isEqual:[self borderColor]])
return;
[self setValue:color forThemeAttribute:@"border-color"];
}
- (float)borderWidth
{
return [self valueForThemeAttribute:@"border-width"];
}
- (void)setBorderWidth:(float)width
{
if (width === [self borderWidth])
return;
[self setValue:width forThemeAttribute:@"border-width"];
}
- (float)cornerRadius
{
return [self valueForThemeAttribute:@"corner-radius"];
}
- (void)setCornerRadius:(float)radius
{
if (radius === [self cornerRadius])
return;
[self setValue:radius forThemeAttribute:@"corner-radius"];
}
- (CPColor)fillColor
{
return [self valueForThemeAttribute:@"background-color"];
}
- (void)setFillColor:(CPColor)color
{
if ([color isEqual:[self fillColor]])
return;
[self setValue:color forThemeAttribute:@"background-color"];
}
- (CPView)contentView
{
return _contentView;
}
- (void)setContentView:(CPView)aView
{
if (aView === _contentView)
return;
var borderWidth = [self borderWidth],
contentMargin = [self valueForThemeAttribute:@"content-margin"];
[aView setFrame:CGRectInset([self bounds], contentMargin.width + borderWidth, contentMargin.height + borderWidth)];
[aView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
// A nil contentView is allowed (tested in Cocoa 2013-02-22).
if (!aView)
[_contentView removeFromSuperview];
else if (_contentView)
[self replaceSubview:_contentView with:aView];
else
[self addSubview:aView];
_contentView = aView;
}
- (CGSize)contentViewMargins
{
return [self valueForThemeAttribute:@"content-margin"];
}
- (void)setContentViewMargins:(CGSize)size
{
if (size.width < 0 || size.height < 0)
[CPException raise:CPGenericException reason:@"Margins must be positive"];
[self setValue:CGSizeMakeCopy(size) forThemeAttribute:@"content-margin"];
}
- (void)setFrameFromContentFrame:(CGRect)aRect
{
var offset = [self _titleHeightOffset],
borderWidth = [self borderWidth],
contentMargin = [self valueForThemeAttribute:@"content-margin"];
[self setFrame:CGRectInset(aRect, -(contentMargin.width + borderWidth), -(contentMargin.height + offset[0] + borderWidth))];
}
- (void)setTitle:(CPString)aTitle
{
if (aTitle == _title)
return;
_title = aTitle;
[self _manageTitlePositioning];
}
- (void)setTitlePosition:(int)aTitlePotisition
{
if (aTitlePotisition == _titlePosition)
return;
_titlePosition = aTitlePotisition;
[self _manageTitlePositioning];
}
- (CPFont)titleFont
{
return [_titleView font];
}
- (void)setTitleFont:(CPFont)aFont
{
[_titleView setFont:aFont];
}
/*!
Return the text field used to display the receiver's title.
This is the Cappuccino equivalent to the `titleCell` method.
*/
- (CPTextField)titleView
{
return _titleView;
}
- (void)_manageTitlePositioning
{
if (_titlePosition == CPNoTitle)
{
[_titleView removeFromSuperview];
[self setNeedsDisplay:YES];
return;
}
[_titleView setStringValue:_title];
[_titleView sizeToFit];
[self addSubview:_titleView];
switch (_titlePosition)
{
case CPAtTop:
case CPAboveTop:
case CPBelowTop:
[_titleView setFrameOrigin:CGPointMake(5.0, 0.0)];
[_titleView setAutoresizingMask:CPViewNotSizable];
break;
case CPAboveBottom:
case CPAtBottom:
case CPBelowBottom:
var h = [_titleView frameSize].height;
[_titleView setFrameOrigin:CGPointMake(5.0, [self frameSize].height - h)];
[_titleView setAutoresizingMask:CPViewMinYMargin];
break;
}
[self sizeToFit];
[self setNeedsDisplay:YES];
}
- (void)sizeToFit
{
var contentFrame = [_contentView frame],
offset = [self _titleHeightOffset],
contentMargin = [self valueForThemeAttribute:@"content-margin"];
if (!contentFrame)
return;
[_contentView setFrameOrigin:CGPointMake(contentMargin.width, contentMargin.height + offset[1])];
}
- (float)_titleHeightOffset
{
if (_titlePosition == CPNoTitle)
return [0.0, 0.0];
switch (_titlePosition)
{
case CPAtTop:
return [[_titleView frameSize].height, [_titleView frameSize].height];
case CPAtBottom:
return [[_titleView frameSize].height, 0.0];
default:
return [0.0, 0.0];
}
}
- (void)setValue:(id)aValue forKey:(CPString)aKey
{
if (aKey === CPDisplayPatternTitleBinding)
[self setTitle:aValue || @""];
else
[super setValue:aValue forKey:aKey];
}
- (void)drawRect:(CGRect)rect
{
var bounds = [self bounds];
switch (_boxType)
{
case CPBoxSeparator:
// NSBox does not include a horizontal flag for the separator type. We have to determine
// the type of separator to draw by the width and height of the frame.
if (CGRectGetWidth(bounds) === 5.0)
return [self _drawVerticalSeparatorInRect:bounds];
else if (CGRectGetHeight(bounds) === 5.0)
return [self _drawHorizontalSeparatorInRect:bounds];
break;
}
if (_titlePosition == CPAtTop)
{
bounds.origin.y += [_titleView frameSize].height;
bounds.size.height -= [_titleView frameSize].height;
}
if (_titlePosition == CPAtBottom)
{
bounds.size.height -= [_titleView frameSize].height;
}
// Primary or secondary type boxes always draw the same way, unless they are CPNoBorder.
if ((_boxType === CPBoxPrimary || _boxType === CPBoxSecondary) && _borderType !== CPNoBorder)
{
[self _drawPrimaryBorderInRect:bounds];
return;
}
switch (_borderType)
{
case CPBezelBorder:
[self _drawBezelBorderInRect:bounds];
break;
case CPGrooveBorder:
case CPLineBorder:
[self _drawLineBorderInRect:bounds];
break;
case CPNoBorder:
[self _drawNoBorderInRect:bounds];
break;
}
}
- (void)_drawHorizontalSeparatorInRect:(CGRect)aRect
{
var context = [[CPGraphicsContext currentContext] graphicsPort];
CGContextSetStrokeColor(context, [self borderColor]);
CGContextSetLineWidth(context, 1.0);
CGContextMoveToPoint(context, CGRectGetMinX(aRect), CGRectGetMidY(aRect));
CGContextAddLineToPoint(context, CGRectGetWidth(aRect), CGRectGetMidY(aRect));
CGContextStrokePath(context);
}
- (void)_drawVerticalSeparatorInRect:(CGRect)aRect
{
var context = [[CPGraphicsContext currentContext] graphicsPort];
CGContextSetStrokeColor(context, [self borderColor]);
CGContextSetLineWidth(context, 1.0);
CGContextMoveToPoint(context, CGRectGetMidX(aRect), CGRectGetMinY(aRect));
CGContextAddLineToPoint(context, CGRectGetMidX(aRect), CGRectGetHeight(aRect));
CGContextStrokePath(context);
}
- (void)_drawLineBorderInRect:(CGRect)aRect
{
var context = [[CPGraphicsContext currentContext] graphicsPort],
cornerRadius = [self cornerRadius],
borderWidth = [self borderWidth];
aRect = CGRectInset(aRect, borderWidth / 2.0, borderWidth / 2.0);
CGContextSetFillColor(context, [self fillColor]);
CGContextSetStrokeColor(context, [self borderColor]);
CGContextSetLineWidth(context, borderWidth);
CGContextFillRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
CGContextStrokeRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
}
- (void)_drawBezelBorderInRect:(CGRect)aRect
{
var context = [[CPGraphicsContext currentContext] graphicsPort],
cornerRadius = [self cornerRadius],
borderWidth = [self borderWidth],
shadowOffset = [self valueForThemeAttribute:@"inner-shadow-offset"],
shadowSize = [self valueForThemeAttribute:@"inner-shadow-size"],
shadowColor = [self valueForThemeAttribute:@"inner-shadow-color"];
var baseRect = aRect;
aRect = CGRectInset(aRect, borderWidth / 2.0, borderWidth / 2.0);
CGContextSaveGState(context);
CGContextSetStrokeColor(context, [self borderColor]);
CGContextSetLineWidth(context, borderWidth);
CGContextSetFillColor(context, [self fillColor]);
CGContextFillRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
CGContextStrokeRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
CGContextRestoreGState(context);
}
- (void)_drawPrimaryBorderInRect:(CGRect)aRect
{
// Draw the "primary" style CPBox.
var context = [[CPGraphicsContext currentContext] graphicsPort],
cornerRadius = [self cornerRadius],
borderWidth = [self borderWidth],
shadowOffset = [self valueForThemeAttribute:@"inner-shadow-offset"],
shadowSize = [self valueForThemeAttribute:@"inner-shadow-size"],
shadowColor = [self valueForThemeAttribute:@"inner-shadow-color"],
baseRect = aRect;
aRect = CGRectInset(aRect, borderWidth / 2.0, borderWidth / 2.0);
CGContextSaveGState(context);
CGContextSetStrokeColor(context, [self borderColor]);
CGContextSetLineWidth(context, borderWidth);
CGContextSetFillColor(context, [self fillColor]);
CGContextFillRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
CGContextBeginPath(context);
// Note we can't use the 0.5 inset rectangle when setting up clipping. The clipping has to be
// on integer coordinates for this to look right in Chrome.
CGContextAddPath(context, CGPathWithRoundedRectangleInRect(baseRect, cornerRadius, cornerRadius, YES, YES, YES, YES));
CGContextClip(context);
CGContextSetShadowWithColor(context, shadowOffset, shadowSize, shadowColor);
CGContextStrokeRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
CGContextRestoreGState(context);
}
- (void)_drawNoBorderInRect:(CGRect)aRect
{
var context = [[CPGraphicsContext currentContext] graphicsPort];
CGContextSetFillColor(context, [self fillColor]);
CGContextFillRect(context, aRect);
}
@end
var CPBoxTypeKey = @"CPBoxTypeKey",
CPBoxBorderTypeKey = @"CPBoxBorderTypeKey",
CPBoxTitle = @"CPBoxTitle",
CPBoxTitlePosition = @"CPBoxTitlePosition",
CPBoxTitleView = @"CPBoxTitleView",
CPBoxContentView = @"CPBoxContentView";
@implementation CPBox (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super initWithCoder:aCoder];
if (self)
{
_boxType = [aCoder decodeIntForKey:CPBoxTypeKey];
_borderType = [aCoder decodeIntForKey:CPBoxBorderTypeKey];
_title = [aCoder decodeObjectForKey:CPBoxTitle];
_titlePosition = [aCoder decodeIntForKey:CPBoxTitlePosition];
_titleView = [aCoder decodeObjectForKey:CPBoxTitleView] || [CPTextField labelWithTitle:_title];
if (_boxType != CPBoxSeparator)
{
// FIXME: we have a problem with CIB decoding here.
// We should be able to simply add : _contentView = [self subviews][0]
// but first box subview seems to be malformed (badly decoded).
// For example, when deployed, this view doesn't have its _trackingAreas array initialized.
// As a (temporary) workaround, we encode/decode the _contentView property. We then transfer the subview hierarchy
// and replace the first (and only) box subview with this _contentView
_contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]];
var malformedContentView = [self subviews][0];
[_contentView setSubviews:[malformedContentView subviews]];
[self replaceSubview:malformedContentView with:_contentView];
}
else
{
_titlePosition = CPNoTitle;
}
[self setAutoresizesSubviews:YES];
[_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[self _manageTitlePositioning];
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[super encodeWithCoder:aCoder];
[aCoder encodeInt:_boxType forKey:CPBoxTypeKey];
[aCoder encodeInt:_borderType forKey:CPBoxBorderTypeKey];
[aCoder encodeObject:_title forKey:CPBoxTitle];
[aCoder encodeInt:_titlePosition forKey:CPBoxTitlePosition];
[aCoder encodeObject:_titleView forKey:CPBoxTitleView];
[aCoder encodeObject:_contentView forKey:CPBoxContentView];
}
@end