API  1.0.0
CPBox.j
Go to the documentation of this file.
1 /*
2  * CPBox.j
3  * AppKit
4  *
5  * Created by Ross Boucher.
6  * Copyright 2009, 280 North, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21  */
22 
23 
24 // CPBoxType
25 @typedef CPBoxType
31 
32 // CPBorderType
33 @typedef CPBorderType
38 
39 // CPTitlePosition
40 @typedef CPTitlePosition
43 CPAtTop = 2;
48 
49 
55 @implementation CPBox : CPView
56 {
57  CPBoxType _boxType;
58  CPBorderType _borderType;
59  CPView _contentView;
60 
61  CPString _title;
62  int _titlePosition;
63  CPTextField _titleView;
64 }
65 
66 + (Class)_binderClassForBinding:(CPString)aBinding
67 {
68  if ([aBinding hasPrefix:CPDisplayPatternTitleBinding])
69  return [CPTitleWithPatternBinding class];
70 
71  return [super _binderClassForBinding:aBinding];
72 }
73 
74 + (CPString)defaultThemeClass
75 {
76  return @"box";
77 }
78 
79 + (CPDictionary)themeAttributes
80 {
81  return @{
82  @"background-color": [CPNull null],
83  @"border-color": [CPNull null],
84  @"border-width": 1.0,
85  @"corner-radius": 3.0,
86  @"inner-shadow-offset": CGSizeMakeZero(),
87  @"inner-shadow-size": 6.0,
88  @"inner-shadow-color": [CPNull null],
89  @"content-margin": CGSizeMakeZero(),
90  };
91 }
92 
93 + (id)boxEnclosingView:(CPView)aView
94 {
95  var box = [[self alloc] initWithFrame:CGRectMakeZero()],
96  enclosingView = [aView superview];
97 
98  [box setAutoresizingMask:[aView autoresizingMask]];
99  [box setFrameFromContentFrame:[aView frame]];
100 
101  [enclosingView replaceSubview:aView with:box];
102 
103  [box setContentView:aView];
104 
105  return box;
106 }
107 
108 - (id)initWithFrame:(CGRect)frameRect
109 {
110  self = [super initWithFrame:frameRect];
111 
112  if (self)
113  {
114  _borderType = CPBezelBorder;
115 
116  _titlePosition = CPNoTitle;
117  _titleView = [CPTextField labelWithTitle:@""];
118 
119  _contentView = [[CPView alloc] initWithFrame:[self bounds]];
120  [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
121 
122  [self setAutoresizesSubviews:YES];
123  [self addSubview:_contentView];
124  }
125 
126  return self;
127 }
128 
129 // Configuring Boxes
130 
136 - (CGRect)borderRect
137 {
138  return [self bounds];
139 }
140 
153 - (CPBorderType)borderType
154 {
155  return _borderType;
156 }
157 
158 
171 - (void)setBorderType:(CPBorderType)aBorderType
172 {
173  if (_borderType === aBorderType)
174  return;
175 
176  _borderType = aBorderType;
177  [self setNeedsDisplay:YES];
178 }
179 
195 - (CPBoxType)boxType
196 {
197  return _boxType;
198 }
199 
215 - (void)setBoxType:(CPBoxType)aBoxType
216 {
217  if (_boxType === aBoxType)
218  return;
219 
220  _boxType = aBoxType;
221  [self setNeedsDisplay:YES];
222 }
223 
224 - (CPColor)borderColor
225 {
226  return [self valueForThemeAttribute:@"border-color"];
227 }
228 
229 - (void)setBorderColor:(CPColor)color
230 {
231  if ([color isEqual:[self borderColor]])
232  return;
233 
234  [self setValue:color forThemeAttribute:@"border-color"];
235 }
236 
237 - (float)borderWidth
238 {
239  return [self valueForThemeAttribute:@"border-width"];
240 }
241 
242 - (void)setBorderWidth:(float)width
243 {
244  if (width === [self borderWidth])
245  return;
246 
247  [self setValue:width forThemeAttribute:@"border-width"];
248 }
249 
250 - (float)cornerRadius
251 {
252  return [self valueForThemeAttribute:@"corner-radius"];
253 }
254 
255 - (void)setCornerRadius:(float)radius
256 {
257  if (radius === [self cornerRadius])
258  return;
259 
260  [self setValue:radius forThemeAttribute:@"corner-radius"];
261 }
262 
263 - (CPColor)fillColor
264 {
265  return [self valueForThemeAttribute:@"background-color"];
266 }
267 
268 - (void)setFillColor:(CPColor)color
269 {
270  if ([color isEqual:[self fillColor]])
271  return;
272 
273  [self setValue:color forThemeAttribute:@"background-color"];
274 }
275 
276 - (CPView)contentView
277 {
278  return _contentView;
279 }
280 
281 - (void)setContentView:(CPView)aView
282 {
283  if (aView === _contentView)
284  return;
285 
286  var borderWidth = [self borderWidth],
287  contentMargin = [self valueForThemeAttribute:@"content-margin"];
288 
289  [aView setFrame:CGRectInset([self bounds], contentMargin.width + borderWidth, contentMargin.height + borderWidth)];
290  [aView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
291 
292  // A nil contentView is allowed (tested in Cocoa 2013-02-22).
293  if (!aView)
294  [_contentView removeFromSuperview];
295  else if (_contentView)
296  [self replaceSubview:_contentView with:aView];
297  else
298  [self addSubview:aView];
299 
300  _contentView = aView;
301 }
302 
303 - (CGSize)contentViewMargins
304 {
305  return [self valueForThemeAttribute:@"content-margin"];
306 }
307 
308 - (void)setContentViewMargins:(CGSize)size
309 {
310  if (size.width < 0 || size.height < 0)
311  [CPException raise:CPGenericException reason:@"Margins must be positive"];
312 
313  [self setValue:CGSizeMakeCopy(size) forThemeAttribute:@"content-margin"];
314 }
315 
316 - (void)setFrameFromContentFrame:(CGRect)aRect
317 {
318  var offset = [self _titleHeightOffset],
319  borderWidth = [self borderWidth],
320  contentMargin = [self valueForThemeAttribute:@"content-margin"];
321 
322  [self setFrame:CGRectInset(aRect, -(contentMargin.width + borderWidth), -(contentMargin.height + offset[0] + borderWidth))];
323 }
324 
325 - (void)setTitle:(CPString)aTitle
326 {
327  if (aTitle == _title)
328  return;
329 
330  _title = aTitle;
331 
332  [self _manageTitlePositioning];
333 }
334 
335 - (void)setTitlePosition:(int)aTitlePotisition
336 {
337  if (aTitlePotisition == _titlePosition)
338  return;
339 
340  _titlePosition = aTitlePotisition;
341 
342  [self _manageTitlePositioning];
343 }
344 
345 - (CPFont)titleFont
346 {
347  return [_titleView font];
348 }
349 
350 - (void)setTitleFont:(CPFont)aFont
351 {
352  [_titleView setFont:aFont];
353 }
354 
360 - (CPTextField)titleView
361 {
362  return _titleView;
363 }
364 
365 - (void)_manageTitlePositioning
366 {
367  if (_titlePosition == CPNoTitle)
368  {
369  [_titleView removeFromSuperview];
370  [self setNeedsDisplay:YES];
371  return;
372  }
373 
374  [_titleView setStringValue:_title];
375  [_titleView sizeToFit];
376  [self addSubview:_titleView];
377 
378  switch (_titlePosition)
379  {
380  case CPAtTop:
381  case CPAboveTop:
382  case CPBelowTop:
383  [_titleView setFrameOrigin:CGPointMake(5.0, 0.0)];
384  [_titleView setAutoresizingMask:CPViewNotSizable];
385  break;
386 
387  case CPAboveBottom:
388  case CPAtBottom:
389  case CPBelowBottom:
390  var h = [_titleView frameSize].height;
391  [_titleView setFrameOrigin:CGPointMake(5.0, [self frameSize].height - h)];
392  [_titleView setAutoresizingMask:CPViewMinYMargin];
393  break;
394  }
395 
396  [self sizeToFit];
397  [self setNeedsDisplay:YES];
398 }
399 
400 - (void)sizeToFit
401 {
402  var contentFrame = [_contentView frame],
403  offset = [self _titleHeightOffset],
404  contentMargin = [self valueForThemeAttribute:@"content-margin"];
405 
406  if (!contentFrame)
407  return;
408 
409  [_contentView setFrameOrigin:CGPointMake(contentMargin.width, contentMargin.height + offset[1])];
410 }
411 
412 - (float)_titleHeightOffset
413 {
414  if (_titlePosition == CPNoTitle)
415  return [0.0, 0.0];
416 
417  switch (_titlePosition)
418  {
419  case CPAtTop:
420  return [[_titleView frameSize].height, [_titleView frameSize].height];
421 
422  case CPAtBottom:
423  return [[_titleView frameSize].height, 0.0];
424 
425  default:
426  return [0.0, 0.0];
427  }
428 }
429 
430 - (void)setValue:(id)aValue forKey:(CPString)aKey
431 {
432  if (aKey === CPDisplayPatternTitleBinding)
433  [self setTitle:aValue || @""];
434  else
435  [super setValue:aValue forKey:aKey];
436 }
437 
438 - (void)drawRect:(CGRect)rect
439 {
440  var bounds = [self bounds];
441 
442  switch (_boxType)
443  {
444  case CPBoxSeparator:
445  // NSBox does not include a horizontal flag for the separator type. We have to determine
446  // the type of separator to draw by the width and height of the frame.
447  if (CGRectGetWidth(bounds) === 5.0)
448  return [self _drawVerticalSeparatorInRect:bounds];
449  else if (CGRectGetHeight(bounds) === 5.0)
450  return [self _drawHorizontalSeparatorInRect:bounds];
451 
452  break;
453  }
454 
455  if (_titlePosition == CPAtTop)
456  {
457  bounds.origin.y += [_titleView frameSize].height;
458  bounds.size.height -= [_titleView frameSize].height;
459  }
460  if (_titlePosition == CPAtBottom)
461  {
462  bounds.size.height -= [_titleView frameSize].height;
463  }
464 
465  // Primary or secondary type boxes always draw the same way, unless they are CPNoBorder.
466  if ((_boxType === CPBoxPrimary || _boxType === CPBoxSecondary) && _borderType !== CPNoBorder)
467  {
468  [self _drawPrimaryBorderInRect:bounds];
469  return;
470  }
471 
472  switch (_borderType)
473  {
474  case CPBezelBorder:
475  [self _drawBezelBorderInRect:bounds];
476  break;
477 
478  case CPGrooveBorder:
479  case CPLineBorder:
480  [self _drawLineBorderInRect:bounds];
481  break;
482 
483  case CPNoBorder:
484  [self _drawNoBorderInRect:bounds];
485  break;
486  }
487 }
488 
489 - (void)_drawHorizontalSeparatorInRect:(CGRect)aRect
490 {
492 
493  CGContextSetStrokeColor(context, [self borderColor]);
494  CGContextSetLineWidth(context, 1.0);
495 
496  CGContextMoveToPoint(context, CGRectGetMinX(aRect), CGRectGetMidY(aRect));
497  CGContextAddLineToPoint(context, CGRectGetWidth(aRect), CGRectGetMidY(aRect));
498  CGContextStrokePath(context);
499 }
500 
501 - (void)_drawVerticalSeparatorInRect:(CGRect)aRect
502 {
504 
505  CGContextSetStrokeColor(context, [self borderColor]);
506  CGContextSetLineWidth(context, 1.0);
507 
508  CGContextMoveToPoint(context, CGRectGetMidX(aRect), CGRectGetMinY(aRect));
509  CGContextAddLineToPoint(context, CGRectGetMidX(aRect), CGRectGetHeight(aRect));
510  CGContextStrokePath(context);
511 }
512 
513 - (void)_drawLineBorderInRect:(CGRect)aRect
514 {
516  cornerRadius = [self cornerRadius],
517  borderWidth = [self borderWidth];
518 
519  aRect = CGRectInset(aRect, borderWidth / 2.0, borderWidth / 2.0);
520 
521  CGContextSetFillColor(context, [self fillColor]);
522  CGContextSetStrokeColor(context, [self borderColor]);
523 
524  CGContextSetLineWidth(context, borderWidth);
525  CGContextFillRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
526  CGContextStrokeRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
527 }
528 
529 - (void)_drawBezelBorderInRect:(CGRect)aRect
530 {
532  cornerRadius = [self cornerRadius],
533  borderWidth = [self borderWidth],
534  shadowOffset = [self valueForThemeAttribute:@"inner-shadow-offset"],
535  shadowSize = [self valueForThemeAttribute:@"inner-shadow-size"],
536  shadowColor = [self valueForThemeAttribute:@"inner-shadow-color"];
537 
538  var baseRect = aRect;
539  aRect = CGRectInset(aRect, borderWidth / 2.0, borderWidth / 2.0);
540 
541  CGContextSaveGState(context);
542 
543  CGContextSetStrokeColor(context, [self borderColor]);
544  CGContextSetLineWidth(context, borderWidth);
545  CGContextSetFillColor(context, [self fillColor]);
546  CGContextFillRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
547  CGContextStrokeRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
548 
549  CGContextRestoreGState(context);
550 }
551 
552 - (void)_drawPrimaryBorderInRect:(CGRect)aRect
553 {
554  // Draw the "primary" style CPBox.
555 
557  cornerRadius = [self cornerRadius],
558  borderWidth = [self borderWidth],
559  shadowOffset = [self valueForThemeAttribute:@"inner-shadow-offset"],
560  shadowSize = [self valueForThemeAttribute:@"inner-shadow-size"],
561  shadowColor = [self valueForThemeAttribute:@"inner-shadow-color"],
562  baseRect = aRect;
563 
564  aRect = CGRectInset(aRect, borderWidth / 2.0, borderWidth / 2.0);
565 
566  CGContextSaveGState(context);
567 
568  CGContextSetStrokeColor(context, [self borderColor]);
569  CGContextSetLineWidth(context, borderWidth);
570  CGContextSetFillColor(context, [self fillColor]);
571  CGContextFillRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
572 
573  CGContextBeginPath(context);
574  // Note we can't use the 0.5 inset rectangle when setting up clipping. The clipping has to be
575  // on integer coordinates for this to look right in Chrome.
576  CGContextAddPath(context, CGPathWithRoundedRectangleInRect(baseRect, cornerRadius, cornerRadius, YES, YES, YES, YES));
577  CGContextClip(context);
578  CGContextSetShadowWithColor(context, shadowOffset, shadowSize, shadowColor);
579  CGContextStrokeRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
580 
581  CGContextRestoreGState(context);
582 }
583 
584 - (void)_drawNoBorderInRect:(CGRect)aRect
585 {
587 
588  CGContextSetFillColor(context, [self fillColor]);
589  CGContextFillRect(context, aRect);
590 }
591 
592 @end
593 
594 var CPBoxTypeKey = @"CPBoxTypeKey",
595  CPBoxBorderTypeKey = @"CPBoxBorderTypeKey",
596  CPBoxTitle = @"CPBoxTitle",
597  CPBoxTitlePosition = @"CPBoxTitlePosition",
598  CPBoxTitleView = @"CPBoxTitleView",
599  CPBoxContentView = @"CPBoxContentView";
600 
601 @implementation CPBox (CPCoding)
602 
603 - (id)initWithCoder:(CPCoder)aCoder
604 {
605  self = [super initWithCoder:aCoder];
606 
607  if (self)
608  {
609  _boxType = [aCoder decodeIntForKey:CPBoxTypeKey];
610  _borderType = [aCoder decodeIntForKey:CPBoxBorderTypeKey];
611 
612  _title = [aCoder decodeObjectForKey:CPBoxTitle];
613  _titlePosition = [aCoder decodeIntForKey:CPBoxTitlePosition];
614  _titleView = [aCoder decodeObjectForKey:CPBoxTitleView] || [CPTextField labelWithTitle:_title];
615 
616  if (_boxType != CPBoxSeparator)
617  {
618  // FIXME: we have a problem with CIB decoding here.
619  // We should be able to simply add : _contentView = [self subviews][0]
620  // but first box subview seems to be malformed (badly decoded).
621  // For example, when deployed, this view doesn't have its _trackingAreas array initialized.
622  // As a (temporary) workaround, we encode/decode the _contentView property. We then transfer the subview hierarchy
623  // and replace the first (and only) box subview with this _contentView
624 
625  _contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]];
626  var malformedContentView = [self subviews][0];
627  [_contentView setSubviews:[malformedContentView subviews]];
628  [self replaceSubview:malformedContentView with:_contentView];
629  }
630  else
631  {
632  _titlePosition = CPNoTitle;
633  }
634 
635  [self setAutoresizesSubviews:YES];
636  [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
637 
638  [self _manageTitlePositioning];
639  }
640 
641  return self;
642 }
643 
644 - (void)encodeWithCoder:(CPCoder)aCoder
645 {
646  [super encodeWithCoder:aCoder];
647 
648  [aCoder encodeInt:_boxType forKey:CPBoxTypeKey];
649  [aCoder encodeInt:_borderType forKey:CPBoxBorderTypeKey];
650  [aCoder encodeObject:_title forKey:CPBoxTitle];
651  [aCoder encodeInt:_titlePosition forKey:CPBoxTitlePosition];
652  [aCoder encodeObject:_titleView forKey:CPBoxTitleView];
653  [aCoder encodeObject:_contentView forKey:CPBoxContentView];
654 }
655 
656 @end
657 
659 
663 - (CPString)title
664 {
665  return _title;
666 }
667 
671 - (int)titlePosition
672 {
673  return _titlePosition;
674 }
675 
676 @end
id initWithFrame:(CGRect aFrame)
Definition: CPView.j:351
Used to implement exception handling (creating & raising).
Definition: CPException.h:2
Definition: CPFont.h:2
function CGContextSetShadowWithColor(aContext, aSize, aBlur, aColor)
Definition: CGContext.j:504
CPBoxCustom
Definition: CPBox.j:30
void addSubview:(CPView aSubview)
Definition: CPView.j:536
function CGPathWithRoundedRectangleInRect(aRect, xRadius, yRadius, ne, se, sw, nw)
Definition: CGPath.j:352
var isEqual
CPGraphicsContext currentContext()
An object representation of nil.
Definition: CPNull.h:2
id initWithCoder:(CPCoder aCoder)
Definition: CPView.j:3696
float borderWidth()
Definition: CPBox.j:237
CPAtTop
Definition: CPBox.j:43
function CGContextSetStrokeColor(aContext, aColor)
Definition: CGContext.j:675
CPBorderType CPNoBorder
Definition: CPBox.j:34
void setFrame:(CGRect aFrame)
Definition: CPView.j:1020
function CGContextRestoreGState(aContext)
Definition: CGContext.j:156
CPAboveTop
Definition: CPBox.j:42
int width
CGRect bounds()
Definition: CPView.j:1326
void raise:reason:(CPString aName, [reason] CPString aReason)
Definition: CPException.j:66
void replaceSubview:with:(CPView aSubview, [with] CPView aView)
Definition: CPView.j:720
function CGContextAddLineToPoint(aContext, x, y)
Definition: CGContext.j:247
void setValue:forThemeAttribute:(id aValue, [forThemeAttribute] CPString aName)
Definition: CPView.j:3384
function CGContextStrokePath(aContext)
Definition: CGContext.j:619
A mutable key-value pair collection.
Definition: CPDictionary.h:2
function CGContextSetLineWidth(aContext, aLineWidth)
Definition: CGContext.j:177
CGRect bounds()
Definition: CALayer.j:203
CPAboveBottom
Definition: CPBox.j:45
An immutable string (collection of characters).
Definition: CPString.h:2
CPNull null()
Definition: CPNull.j:51
CPBelowTop
Definition: CPBox.j:44
var CPBoxBorderTypeKey
Definition: CPBox.j:595
function CGContextAddPath(aContext, aPath)
Definition: CGContext.j:258
function CGContextSetFillColor(aContext, aColor)
Definition: CGContext.j:663
void setAutoresizingMask:(unsigned aMask)
Definition: CPView.j:1511
void setNeedsDisplay:(BOOL aFlag)
Definition: CPView.j:2597
void encodeWithCoder:(CPCoder aCoder)
Definition: CPView.j:3806
function CGContextStrokeRoundedRectangleInRect(aContext, aRect, aRadius, ne, se, sw, nw)
Definition: CGContext.j:711
function CGContextBeginPath(aContext)
Definition: CGContext.j:311
var CPBoxTitle
Definition: CPBox.j:596
CPLineBorder
Definition: CPBox.j:35
var CPBoxTitlePosition
Definition: CPBox.j:597
void setAutoresizesSubviews:(BOOL aFlag)
Definition: CPView.j:1493
CPArray subviews()
Definition: CPView.j:519
CPTitlePosition CPNoTitle
Definition: CPBox.j:41
var CPBoxTitleView
Definition: CPBox.j:598
CPBoxType CPBoxPrimary
Definition: CPBox.j:26
CPGrooveBorder
Definition: CPBox.j:37
Defines methods for use when archiving & restoring (enc/decoding).
Definition: CPCoder.h:2
CPTextField labelWithTitle:(CPString aTitle)
Definition: CPTextField.j:200
function CGContextSaveGState(aContext)
Definition: CGContext.j:146
void setTitle:(CPString aTitle)
Definition: CPBox.j:325
var CPBoxTypeKey
Definition: CPBox.j:594
CPBelowBottom
Definition: CPBox.j:47
function CGContextFillRoundedRectangleInRect(aContext, aRect, aRadius, ne, se, sw, nw)
Definition: CGContext.j:692
CPBoxSecondary
Definition: CPBox.j:27
function CGContextFillRect(aContext, aRect)
Definition: CGContext.j:358
CPAtBottom
Definition: CPBox.j:46
Definition: CPBox.h:2
CPBoxOldStyle
Definition: CPBox.j:29
CPView superview()
Definition: CPView.j:510
unsigned autoresizingMask()
Definition: CPView.j:1519
CGRect frame()
Definition: CPView.j:1046
CPBezelBorder
Definition: CPBox.j:36
function CGContextMoveToPoint(aContext, x, y)
Definition: CGContext.j:344
var CPBoxContentView
Definition: CPBox.j:599
CPBoxSeparator
Definition: CPBox.j:28
Definition: CPView.j:137