diff --git a/AppKit/CPBox.j b/AppKit/CPBox.j index 6c21cc31e..c0dfc0c77 100644 --- a/AppKit/CPBox.j +++ b/AppKit/CPBox.j @@ -22,13 +22,14 @@ @import "CPTextField.j" @import "CPView.j" +@import // CPBoxType @typedef CPBoxType CPBoxPrimary = 0; -CPBoxSecondary = 1; +CPBoxSecondary = 1; // Deprecated CPBoxSeparator = 2; -CPBoxOldStyle = 3; +CPBoxOldStyle = 3; // Deprecated CPBoxCustom = 4; // CPBorderType @@ -58,11 +59,13 @@ CPBelowBottom = 6; @implementation CPBox : CPView { CPBoxType _boxType; - CPBorderType _borderType; + CPBorderType _borderType; // deprecated CPView _contentView; + CPView _boxView; // needed for CSS theming, will be transparent for non CSS themes + BOOL _transparent @accessors(getter=isTransparent); - CPString _title @accessors(getter=title); - int _titlePosition @accessors(getter=titlePosition); + CPString _title @accessors(getter=title); + int _titlePosition @accessors(getter=titlePosition); CPTextField _titleView; } @@ -90,8 +93,15 @@ CPBelowBottom = 6; @"inner-shadow-size": 6.0, @"inner-shadow-color": [CPNull null], @"content-margin": CGSizeMakeZero(), - @"nib2cib-adjustment-primary-frame": CGRectMake(4, -4, -8, -6) - }; + @"title-font": [CPNull null], + @"title-left-offset": 5.0, + @"title-top-offset": 0.0, + @"title-color": [CPNull null], + @"nib2cib-adjustment-primary-frame": CGRectMake(4, -4, -8, -6), + @"content-adjustment": CGRectMakeZero(), + @"min-y-correction-no-title": 0, + @"min-y-correction-title": 0 + }; } + (id)boxEnclosingView:(CPView)aView @@ -115,16 +125,26 @@ CPBelowBottom = 6; if (self) { - _borderType = CPBezelBorder; + _borderType = CPGrooveBorder; // Was CPBezelBorder but Cocoa default is CPGrooveBorder + _boxType = CPBoxPrimary; _titlePosition = CPNoTitle; _titleView = [CPTextField labelWithTitle:@""]; + [_titleView setFont:[self titleFont]]; + [_titleView setTextColor:[self titleColor]]; + + _boxView = [[CPView alloc] initWithFrame:[self bounds]]; + [_boxView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; _contentView = [[CPView alloc] initWithFrame:[self bounds]]; [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; [self setAutoresizesSubviews:YES]; - [self addSubview:_contentView]; + [self addSubview:_boxView]; + [_boxView setAutoresizesSubviews:YES]; + [_boxView addSubview:_contentView]; + + [self sizeToFit]; } return self; @@ -156,6 +176,8 @@ CPBelowBottom = 6; */ - (CPBorderType)borderType { + CPLog.warn("CPBox borderType is deprecated."); + return _borderType; } @@ -178,7 +200,8 @@ CPBelowBottom = 6; return; _borderType = aBorderType; - [self setNeedsDisplay:YES]; + + [self refreshDisplay]; } /*! @@ -218,13 +241,30 @@ CPBelowBottom = 6; */ - (void)setBoxType:(CPBoxType)aBoxType { + if ((aBoxType == CPBoxSecondary) || (aBoxType == CPBoxOldStyle)) + CPLog.warn("CPBox setBoxType: CPBoxSecondary and CPBoxOldStyle are deprecated."); + if (_boxType === aBoxType) return; _boxType = aBoxType; - [self setNeedsDisplay:YES]; + + [self refreshDisplay]; } +- (void)setTransparent:(BOOL)shouldBeTransparent +{ + if (_transparent == shouldBeTransparent) + return; + + _transparent = shouldBeTransparent; + + [self _manageTitlePositioning]; +} + +/*! + The receiver’s border color. It must be a custom box (that is, it has a type of CPBoxCustom) and it must have a border style of CPLineBorder. + */ - (CPColor)borderColor { return [self valueForThemeAttribute:@"border-color"]; @@ -232,12 +272,21 @@ CPBelowBottom = 6; - (void)setBorderColor:(CPColor)color { + if ((_boxType !== CPBoxCustom) || (_borderType !== CPLineBorder)) + { + CPLog.warn("CPBox setBorderColor: the box must be of type CPBoxCustom AND border of type CPLineBorder in order to use setBorderColor. Ignored."); + return; + } + if ([color isEqual:[self borderColor]]) return; [self setValue:color forThemeAttribute:@"border-color"]; } +/*! + The receiver’s border width. It must be a custom box (that is, it has a type of CPBoxCustom) and it must have a border style of CPLineBorder. + */ - (float)borderWidth { return [self valueForThemeAttribute:@"border-width"]; @@ -245,12 +294,21 @@ CPBelowBottom = 6; - (void)setBorderWidth:(float)width { + if ((_boxType !== CPBoxCustom) || (_borderType !== CPLineBorder)) + { + CPLog.warn("CPBox setBorderWidth: the box must be of type CPBoxCustom AND border of type CPLineBorder in order to use setBorderWidth. Ignored."); + return; + } + if (width === [self borderWidth]) return; [self setValue:width forThemeAttribute:@"border-width"]; } +/*! + The receiver’s corner radius. It must be a custom box (that is, it has a type of CPBoxCustom) and it must have a border style of CPLineBorder. + */ - (float)cornerRadius { return [self valueForThemeAttribute:@"corner-radius"]; @@ -258,12 +316,21 @@ CPBelowBottom = 6; - (void)setCornerRadius:(float)radius { + if ((_boxType !== CPBoxCustom) || (_borderType !== CPLineBorder)) + { + CPLog.warn("CPBox setCornerRadius: the box must be of type CPBoxCustom AND border of type CPLineBorder in order to use setCornerRadius. Ignored."); + return; + } + if (radius === [self cornerRadius]) return; [self setValue:radius forThemeAttribute:@"corner-radius"]; } +/*! + The receiver’s background color. It must be a custom box (that is, it has a type of CPBoxCustom) and it must have a border style of CPLineBorder. + */ - (CPColor)fillColor { return [self valueForThemeAttribute:@"background-color"]; @@ -271,6 +338,12 @@ CPBelowBottom = 6; - (void)setFillColor:(CPColor)color { + if ((_boxType !== CPBoxCustom) || (_borderType !== CPLineBorder)) + { + CPLog.warn("CPBox setFillColor: the box must be of type CPBoxCustom AND border of type CPLineBorder in order to use setFillColor. Ignored."); + return; + } + if ([color isEqual:[self fillColor]]) return; @@ -287,21 +360,20 @@ CPBelowBottom = 6; 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]; + [_boxView replaceSubview:_contentView with:aView]; else - [self addSubview:aView]; + [_boxView addSubview:aView]; _contentView = aView; + + [self sizeToFit]; + [self refreshDisplay]; } - (CGSize)contentViewMargins @@ -321,9 +393,14 @@ CPBelowBottom = 6; { var offset = [self _titleHeightOffset], borderWidth = [self borderWidth], - contentMargin = [self valueForThemeAttribute:@"content-margin"]; + contentMargin = [self valueForThemeAttribute:@"content-margin"], + contentAdjustment = [self valueForThemeAttribute:@"content-adjustment"], + minYCorrection = [self valueForThemeAttribute:(_titlePosition === CPNoTitle ? @"min-y-correction-no-title" : @"min-y-correction-title")]; - [self setFrame:CGRectInset(aRect, -(contentMargin.width + borderWidth), -(contentMargin.height + offset[0] + borderWidth))]; + [self setFrame:CGRectMake(aRect.origin.x - contentAdjustment.origin.x - contentMargin.width + borderWidth, + aRect.origin.y - contentAdjustment.origin.y - contentMargin.height + borderWidth - minYCorrection, + aRect.size.width + 2 * contentMargin.width - contentAdjustment.size.width, + aRect.size.height + 2 * contentMargin.height - contentAdjustment.size.height)]; } - (void)setTitle:(CPString)aTitle @@ -348,14 +425,42 @@ CPBelowBottom = 6; - (CPFont)titleFont { - return [_titleView font]; + if ([self hasThemeAttribute:@"title-font"]) + return [self valueForThemeAttribute:@"title-font"]; + else + return [_titleView font]; } - (void)setTitleFont:(CPFont)aFont { + if ([aFont isEqual:[self titleFont]]) + return; + + if ([self hasThemeAttribute:@"title-font"]) + [self setValue:aFont forThemeAttribute:@"title-font"]; + [_titleView setFont:aFont]; } +- (CPColor)titleColor +{ + if ([self hasThemeAttribute:@"title-color"]) + return [self valueForThemeAttribute:@"title-color"]; + else + return [_titleView textColor]; +} + +- (void)setTitleColor:(CPColor)aColor +{ + if ([aColor isEqual:[self titleColor]]) + return; + + if ([self hasThemeAttribute:@"title-color"]) + [self setValue:aColor forThemeAttribute:@"title-color"]; + + [_titleView setTextColor:aColor]; +} + /*! Return the text field used to display the receiver's title. @@ -366,25 +471,39 @@ CPBelowBottom = 6; return _titleView; } +/*! + Return the rectangle in which the receiver’s title is drawn. + */ +- (CGRect)titleRect +{ + return [_titleView frame]; +} + - (void)_manageTitlePositioning { - if (_titlePosition == CPNoTitle) + if ((_titlePosition == CPNoTitle) || _transparent) { [_titleView removeFromSuperview]; - [self setNeedsDisplay:YES]; + + if (_boxType !== CPBoxSeparator) + [self sizeToFit]; + + [self refreshDisplay]; return; } [_titleView setStringValue:_title]; [_titleView sizeToFit]; - [self addSubview:_titleView]; + + var titleLeftOffset = [self valueForThemeAttribute:@"title-left-offset"], + titleTopOffset = [self valueForThemeAttribute:@"title-top-offset"]; switch (_titlePosition) { case CPAtTop: case CPAboveTop: case CPBelowTop: - [_titleView setFrameOrigin:CGPointMake(5.0, 0.0)]; + [_titleView setFrameOrigin:CGPointMake(titleLeftOffset, titleTopOffset)]; // FIXME: was 0.0 [_titleView setAutoresizingMask:CPViewNotSizable]; break; @@ -392,39 +511,51 @@ CPBelowBottom = 6; case CPAtBottom: case CPBelowBottom: var h = [_titleView frameSize].height; - [_titleView setFrameOrigin:CGPointMake(5.0, [self frameSize].height - h)]; + [_titleView setFrameOrigin:CGPointMake(titleLeftOffset, [self frameSize].height - h - titleTopOffset)]; [_titleView setAutoresizingMask:CPViewMinYMargin]; break; } + if (!_transparent) + [self addSubview:_titleView]; + [self sizeToFit]; - [self setNeedsDisplay:YES]; + [self refreshDisplay]; } - (void)sizeToFit { - var contentFrame = [_contentView frame], - offset = [self _titleHeightOffset], - contentMargin = [self valueForThemeAttribute:@"content-margin"]; + var offset = [self _titleHeightOffset], + size = [self frameSize]; - if (!contentFrame) + [_boxView setFrame:CGRectMake(0, offset[1], size.width, size.height - offset[0])]; + + if (!_contentView) return; - [_contentView setFrameOrigin:CGPointMake(contentMargin.width, contentMargin.height + offset[1])]; + var boxSize = [_boxView frameSize], + contentMargin = [self valueForThemeAttribute:@"content-margin"], + contentAdjustment = [self valueForThemeAttribute:@"content-adjustment"], + borderWidth = [self valueForThemeAttribute:@"border-width"], + minYCorrection = [self valueForThemeAttribute:(_titlePosition === CPNoTitle ? @"min-y-correction-no-title" : @"min-y-correction-title")]; + + [_contentView setFrame:CGRectMake(contentAdjustment.origin.x + contentMargin.width - borderWidth, + contentAdjustment.origin.y + contentMargin.height - borderWidth + minYCorrection, + boxSize.width - 2 * contentMargin.width + contentAdjustment.size.width, + boxSize.height - 2 * contentMargin.height + contentAdjustment.size.height)]; } -- (float)_titleHeightOffset +- (CPArray)_titleHeightOffset { - if (_titlePosition == CPNoTitle) - return [0.0, 0.0]; + var titleTopOffset = [self valueForThemeAttribute:@"title-top-offset"]; switch (_titlePosition) { case CPAtTop: - return [[_titleView frameSize].height, [_titleView frameSize].height]; + return [[_titleView frameSize].height + titleTopOffset, [_titleView frameSize].height + titleTopOffset]; case CPAtBottom: - return [[_titleView frameSize].height, 0.0]; + return [[_titleView frameSize].height + titleTopOffset, 0.0]; default: return [0.0, 0.0]; @@ -441,21 +572,24 @@ CPBelowBottom = 6; - (void)drawRect:(CGRect)rect { + if ([self isCSSBased] && (_boxType !== CPBoxCustom)) + return; + var bounds = [self bounds]; - switch (_boxType) + if (_boxType === CPBoxSeparator) { - 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; + // 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]; } + if (_transparent) + return; + if (_titlePosition == CPAtTop) { bounds.origin.y += [_titleView frameSize].height; @@ -476,9 +610,6 @@ CPBelowBottom = 6; switch (_borderType) { case CPBezelBorder: - [self _drawBezelBorderInRect:bounds]; - break; - case CPGrooveBorder: case CPLineBorder: [self _drawLineBorderInRect:bounds]; @@ -595,12 +726,70 @@ CPBelowBottom = 6; @end +#pragma mark - + +@implementation CPBox (CSSTheming) + +- (void)layoutSubviews +{ + if (![self isCSSBased] || (_boxType === CPBoxCustom)) + return; + + var bounds = [self bounds]; + + if (_boxType === CPBoxSeparator) + { + if (bounds.size.width === 5.0) + { + // Vertical separator + [_boxView setFrame:CGRectMake(2,0,1,bounds.size.height)]; + } + else + { + // Horizontal separator + [_boxView setFrame:CGRectMake(0,2,bounds.size.width,1)]; + } + + [_boxView setBackgroundColor:[self valueForThemeAttribute:@"border-color"]]; + + return; + } + + // All types of boxes (beside custom which is not covered here) always draw the same way, unless they are CPNoBorder. + if ((_borderType !== CPNoBorder) && !_transparent) + { + [_boxView setBackgroundColor:[self valueForThemeAttribute:@"background-color"]]; + return; + } + + // No border or transparent + [_boxView setBackgroundColor:nil]; +} + +- (BOOL)isCSSBased +{ + return [[self theme] isCSSBased]; +} + +- (void)refreshDisplay +{ + if ([self isCSSBased] && (_boxType !== CPBoxCustom)) + [self setNeedsLayout:YES]; + else + [self setNeedsDisplay:YES]; +} + +@end + +#pragma mark - + var CPBoxTypeKey = @"CPBoxTypeKey", CPBoxBorderTypeKey = @"CPBoxBorderTypeKey", - CPBoxTitle = @"CPBoxTitle", - CPBoxTitlePosition = @"CPBoxTitlePosition", - CPBoxTitleView = @"CPBoxTitleView", - CPBoxContentView = @"CPBoxContentView"; + CPBoxTitleKey = @"CPBoxTitleKey", + CPBoxTitlePositionKey = @"CPBoxTitlePositionKey", + CPBoxTitleViewKey = @"CPBoxTitleViewKey", + CPBoxContentViewKey = @"CPBoxContentViewKey", + CPBoxBoxViewKey = @"CPBoxBoxViewKey"; @implementation CPBox (CPCoding) @@ -613,33 +802,56 @@ var CPBoxTypeKey = @"CPBoxTypeKey", _boxType = [aCoder decodeIntForKey:CPBoxTypeKey]; _borderType = [aCoder decodeIntForKey:CPBoxBorderTypeKey]; - _title = [aCoder decodeObjectForKey:CPBoxTitle]; - _titlePosition = [aCoder decodeIntForKey:CPBoxTitlePosition]; - _titleView = [aCoder decodeObjectForKey:CPBoxTitleView] || [CPTextField labelWithTitle:_title]; + _title = [aCoder decodeObjectForKey:CPBoxTitleKey]; + _titlePosition = [aCoder decodeIntForKey:CPBoxTitlePositionKey]; - if (_boxType != CPBoxSeparator) + // Important : see comment on encodeWithCoder below + + _boxView = [aCoder decodeObjectForKey:CPBoxBoxViewKey]; + + if (!_boxView) { - // 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 + // We're coming from nib2cib. - _contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]]; - var malformedContentView = [self subviews][0]; - [_contentView setSubviews:[malformedContentView subviews]]; - [self replaceSubview:malformedContentView with:_contentView]; + _boxView = [[CPView alloc] initWithFrame:[self bounds]]; + _titleView = [CPTextField labelWithTitle:_title]; } else { - _titlePosition = CPNoTitle; + // We're coming from elsewhere + + _titleView = [aCoder decodeObjectForKey:CPBoxTitleViewKey]; } - - [self setAutoresizesSubviews:YES]; + + _contentView = [aCoder decodeObjectForKey:CPBoxContentViewKey]; + + // FIXME: super-mega-hyper-trick : _contentView has a superview which is not normal ! + // FIXME: (see encodeWithCoder to understand why this is not possible) + // FIXME: we fix this by hand. This is horrible so please find a structural solution ! + + if (_contentView) + _contentView._superview = nil; + [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [_boxView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [_boxView setAutoresizesSubviews:YES]; + [self setAutoresizesSubviews:YES]; + + if (_contentView) + [_boxView setSubviews:@[_contentView]]; + + [self addSubview:_boxView]; + [self addSubview:_titleView]; + + if (_boxType === CPBoxSeparator) + _titlePosition = CPNoTitle; + + [_titleView setFont:[self titleFont]]; + [_titleView setTextColor:[self titleColor]]; [self _manageTitlePositioning]; + + [self refreshDisplay]; } return self; @@ -647,14 +859,47 @@ var CPBoxTypeKey = @"CPBoxTypeKey", - (void)encodeWithCoder:(CPCoder)aCoder { + // We have to distinguish between 2 cases : + // - we come from nib2cib + // - we come from elsewhere + // + // When coming from nib2cib, we have no _boxView, _contentView, _titleView. + // We fix _contentView to be the first (and only) subview. + // They will have to be added on decoding. + // + // When coming from elsewhere, we remove _boxView (and thus _contentView) and _titleView + // from the view hierarchy as we'll already encode them via variables. + // They will be putted back during decoding. This way, we reduce the space and speed needed for coding. + + var subviews = [self subviews]; + + if (!_boxView) + { + // We're coming from nib2cib. + + _contentView = subviews[0]; + + [_contentView removeFromSuperview]; + } + else + { + // We're coming from elsewhere. + + [_boxView removeFromSuperview]; + [_titleView removeFromSuperview]; + } + [super encodeWithCoder:aCoder]; + [self setSubviews:subviews]; + [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]; + [aCoder encodeObject:_title forKey:CPBoxTitleKey]; + [aCoder encodeInt:_titlePosition forKey:CPBoxTitlePositionKey]; + [aCoder encodeConditionalObject:_contentView forKey:CPBoxContentViewKey]; + [aCoder encodeConditionalObject:_titleView forKey:CPBoxTitleViewKey]; + [aCoder encodeConditionalObject:_boxView forKey:CPBoxBoxViewKey]; } @end diff --git a/Tools/nib2cib/NSBox.j b/Tools/nib2cib/NSBox.j index 87b558008..b702f9ca8 100644 --- a/Tools/nib2cib/NSBox.j +++ b/Tools/nib2cib/NSBox.j @@ -32,6 +32,7 @@ { _boxType = [aCoder decodeIntForKey:@"NSBoxType"]; _borderType = [aCoder decodeIntForKey:@"NSBorderType"]; + _transparent = [aCoder decodeBoolForKey:@"NSTransparent"]; var borderColor = [aCoder decodeObjectForKey:@"NSBorderColor2"], fillColor = [aCoder decodeObjectForKey:@"NSFillColor2"], @@ -70,7 +71,7 @@ - (CGRect)_nib2CibAdjustment { - if ((_boxType === CPBoxPrimary) || (_boxType === CPBoxSecondary)) + if ((_boxType !== CPBoxSeparator) && ((_boxType === CPBoxPrimary) || (_boxType === CPBoxSecondary))) { // We use a special nib2cib-adjustment-frame for primary/secondary boxes var theme = [Nib2Cib defaultTheme],