Cappuccino  1.0.0
 All Classes Files Functions Variables Typedefs Macros Groups Pages
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