API  0.9.7
 All Classes Files Functions Variables 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
30 
31 // CPBorderType
36 
37 // CPTitlePosition
40 CPAtTop = 2;
45 
46 
53 @implementation CPBox : CPView
54 {
55  CPBoxType _boxType;
56  CPBorderType _borderType;
57  CPView _contentView;
58 
59  CPString _title;
60  int _titlePosition;
61  CPTextField _titleView;
62 }
63 
64 + (Class)_binderClassForBinding:(CPString)aBinding
65 {
66  if ([aBinding hasPrefix:CPDisplayPatternTitleBinding])
67  return [CPTitleWithPatternBinding class];
68 
69  return [super _binderClassForBinding:aBinding];
70 }
71 
72 + (CPString)defaultThemeClass
73 {
74  return @"box";
75 }
76 
77 + (CPDictionary)themeAttributes
78 {
79  return @{
80  @"background-color": [CPNull null],
81  @"border-color": [CPNull null],
82  @"border-width": 1.0,
83  @"corner-radius": 3.0,
84  @"inner-shadow-offset": CGSizeMakeZero(),
85  @"inner-shadow-size": 6.0,
86  @"inner-shadow-color": [CPNull null],
87  @"content-margin": CGSizeMakeZero(),
88  };
89 }
90 
91 + (id)boxEnclosingView:(CPView)aView
92 {
93  var box = [[self alloc] initWithFrame:CGRectMakeZero()],
94  enclosingView = [aView superview];
95 
96  [box setAutoresizingMask:[aView autoresizingMask]];
97  [box setFrameFromContentFrame:[aView frame]];
98 
99  [enclosingView replaceSubview:aView with:box];
100 
101  [box setContentView:aView];
102 
103  return box;
104 }
105 
106 - (id)initWithFrame:(CGRect)frameRect
107 {
108  self = [super initWithFrame:frameRect];
109 
110  if (self)
111  {
112  _borderType = CPBezelBorder;
113 
114  _titlePosition = CPNoTitle;
115  _titleView = [CPTextField labelWithTitle:@""];
116 
117  _contentView = [[CPView alloc] initWithFrame:[self bounds]];
118  [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
119 
120  [self setAutoresizesSubviews:YES];
121  [self addSubview:_contentView];
122  }
123 
124  return self;
125 }
126 
127 // Configuring Boxes
128 
134 - (CGRect)borderRect
135 {
136  return [self bounds];
137 }
138 
151 - (CPBorderType)borderType
152 {
153  return _borderType;
154 }
155 
156 
169 - (void)setBorderType:(CPBorderType)aBorderType
170 {
171  if (_borderType === aBorderType)
172  return;
173 
174  _borderType = aBorderType;
175  [self setNeedsDisplay:YES];
176 }
177 
193 - (CPBoxType)boxType
194 {
195  return _boxType;
196 }
197 
213 - (void)setBoxType:(CPBoxType)aBoxType
214 {
215  if (_boxType === aBoxType)
216  return;
217 
218  _boxType = aBoxType;
219  [self setNeedsDisplay:YES];
220 }
221 
222 - (CPColor)borderColor
223 {
224  return [self valueForThemeAttribute:@"border-color"];
225 }
226 
227 - (void)setBorderColor:(CPColor)color
228 {
229  if ([color isEqual:[self borderColor]])
230  return;
231 
232  [self setValue:color forThemeAttribute:@"border-color"];
233 }
234 
235 - (float)borderWidth
236 {
237  return [self valueForThemeAttribute:@"border-width"];
238 }
239 
240 - (void)setBorderWidth:(float)width
241 {
242  if (width === [self borderWidth])
243  return;
244 
245  [self setValue:width forThemeAttribute:@"border-width"];
246 }
247 
248 - (float)cornerRadius
249 {
250  return [self valueForThemeAttribute:@"corner-radius"];
251 }
252 
253 - (void)setCornerRadius:(float)radius
254 {
255  if (radius === [self cornerRadius])
256  return;
257 
258  [self setValue:radius forThemeAttribute:@"corner-radius"];
259 }
260 
261 - (CPColor)fillColor
262 {
263  return [self valueForThemeAttribute:@"background-color"];
264 }
265 
266 - (void)setFillColor:(CPColor)color
267 {
268  if ([color isEqual:[self fillColor]])
269  return;
270 
271  [self setValue:color forThemeAttribute:@"background-color"];
272 }
273 
274 - (CPView)contentView
275 {
276  return _contentView;
277 }
278 
279 - (void)setContentView:(CPView)aView
280 {
281  if (aView === _contentView)
282  return;
283 
284  var borderWidth = [self borderWidth],
285  contentMargin = [self valueForThemeAttribute:@"content-margin"];
286 
287  [aView setFrame:CGRectInset([self bounds], contentMargin.width + borderWidth, contentMargin.height + borderWidth)];
288  [aView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
289 
290  // A nil contentView is allowed (tested in Cocoa 2013-02-22).
291  if (!aView)
292  [_contentView removeFromSuperview];
293  else if (_contentView)
294  [self replaceSubview:_contentView with:aView];
295  else
296  [self addSubview:aView];
297 
298  _contentView = aView;
299 }
300 
301 - (CGSize)contentViewMargins
302 {
303  return [self valueForThemeAttribute:@"content-margin"];
304 }
305 
306 - (void)setContentViewMargins:(CGSize)size
307 {
308  if (size.width < 0 || size.height < 0)
309  [CPException raise:CPGenericException reason:@"Margins must be positive"];
310 
311  [self setValue:CGSizeMakeCopy(size) forThemeAttribute:@"content-margin"];
312 }
313 
314 - (void)setFrameFromContentFrame:(CGRect)aRect
315 {
316  var offset = [self _titleHeightOffset],
317  borderWidth = [self borderWidth],
318  contentMargin = [self valueForThemeAttribute:@"content-margin"];
319 
320  [self setFrame:CGRectInset(aRect, -(contentMargin.width + borderWidth), -(contentMargin.height + offset[0] + borderWidth))];
321 }
322 
323 - (void)setTitle:(CPString)aTitle
324 {
325  if (aTitle == _title)
326  return;
327 
328  _title = aTitle;
329 
330  [self _manageTitlePositioning];
331 }
332 
333 - (void)setTitlePosition:(int)aTitlePotisition
334 {
335  if (aTitlePotisition == _titlePosition)
336  return;
337 
338  _titlePosition = aTitlePotisition;
339 
340  [self _manageTitlePositioning];
341 }
342 
343 - (CPFont)titleFont
344 {
345  return [_titleView font];
346 }
347 
348 - (void)setTitleFont:(CPFont)aFont
349 {
350  [_titleView setFont:aFont];
351 }
352 
358 - (CPTextField)titleView
359 {
360  return _titleView;
361 }
362 
363 - (void)_manageTitlePositioning
364 {
365  if (_titlePosition == CPNoTitle)
366  {
367  [_titleView removeFromSuperview];
368  [self setNeedsDisplay:YES];
369  return;
370  }
371 
372  [_titleView setStringValue:_title];
373  [_titleView sizeToFit];
374  [self addSubview:_titleView];
375 
376  switch (_titlePosition)
377  {
378  case CPAtTop:
379  case CPAboveTop:
380  case CPBelowTop:
381  [_titleView setFrameOrigin:CGPointMake(5.0, 0.0)];
382  [_titleView setAutoresizingMask:CPViewNotSizable];
383  break;
384 
385  case CPAboveBottom:
386  case CPAtBottom:
387  case CPBelowBottom:
388  var h = [_titleView frameSize].height;
389  [_titleView setFrameOrigin:CGPointMake(5.0, [self frameSize].height - h)];
390  [_titleView setAutoresizingMask:CPViewMinYMargin];
391  break;
392  }
393 
394  [self sizeToFit];
395  [self setNeedsDisplay:YES];
396 }
397 
398 - (void)sizeToFit
399 {
400  var contentFrame = [_contentView frame],
401  offset = [self _titleHeightOffset],
402  contentMargin = [self valueForThemeAttribute:@"content-margin"];
403 
404  if (!contentFrame)
405  return;
406 
407  [_contentView setFrameOrigin:CGPointMake(contentMargin.width, contentMargin.height + offset[1])];
408 }
409 
410 - (float)_titleHeightOffset
411 {
412  if (_titlePosition == CPNoTitle)
413  return [0.0, 0.0];
414 
415  switch (_titlePosition)
416  {
417  case CPAtTop:
418  return [[_titleView frameSize].height, [_titleView frameSize].height];
419 
420  case CPAtBottom:
421  return [[_titleView frameSize].height, 0.0];
422 
423  default:
424  return [0.0, 0.0];
425  }
426 }
427 
428 - (void)setValue:(id)aValue forKey:(CPString)aKey
429 {
430  if (aKey === CPDisplayPatternTitleBinding)
431  [self setTitle:aValue || @""];
432  else
433  [super setValue:aValue forKey:aKey];
434 }
435 
436 - (void)drawRect:(CGRect)rect
437 {
438  var bounds = [self bounds];
439 
440  switch (_boxType)
441  {
442  case CPBoxSeparator:
443  // NSBox does not include a horizontal flag for the separator type. We have to determine
444  // the type of separator to draw by the width and height of the frame.
445  if (CGRectGetWidth(bounds) === 5.0)
446  return [self _drawVerticalSeparatorInRect:bounds];
447  else if (CGRectGetHeight(bounds) === 5.0)
448  return [self _drawHorizontalSeparatorInRect:bounds];
449 
450  break;
451  }
452 
453  if (_titlePosition == CPAtTop)
454  {
455  bounds.origin.y += [_titleView frameSize].height;
456  bounds.size.height -= [_titleView frameSize].height;
457  }
458  if (_titlePosition == CPAtBottom)
459  {
460  bounds.size.height -= [_titleView frameSize].height;
461  }
462 
463  // Primary or secondary type boxes always draw the same way, unless they are CPNoBorder.
464  if ((_boxType === CPBoxPrimary || _boxType === CPBoxSecondary) && _borderType !== CPNoBorder)
465  {
466  [self _drawPrimaryBorderInRect:bounds];
467  return;
468  }
469 
470  switch (_borderType)
471  {
472  case CPBezelBorder:
473  [self _drawBezelBorderInRect:bounds];
474  break;
475 
476  case CPGrooveBorder:
477  case CPLineBorder:
478  [self _drawLineBorderInRect:bounds];
479  break;
480 
481  case CPNoBorder:
482  [self _drawNoBorderInRect:bounds];
483  break;
484  }
485 }
486 
487 - (void)_drawHorizontalSeparatorInRect:(CGRect)aRect
488 {
490 
491  CGContextSetStrokeColor(context, [self borderColor]);
492  CGContextSetLineWidth(context, 1.0);
493 
494  CGContextMoveToPoint(context, CGRectGetMinX(aRect), CGRectGetMidY(aRect));
495  CGContextAddLineToPoint(context, CGRectGetWidth(aRect), CGRectGetMidY(aRect));
496  CGContextStrokePath(context);
497 }
498 
499 - (void)_drawVerticalSeparatorInRect:(CGRect)aRect
500 {
502 
503  CGContextSetStrokeColor(context, [self borderColor]);
504  CGContextSetLineWidth(context, 1.0);
505 
506  CGContextMoveToPoint(context, CGRectGetMidX(aRect), CGRectGetMinY(aRect));
507  CGContextAddLineToPoint(context, CGRectGetMidX(aRect), CGRectGetHeight(aRect));
508  CGContextStrokePath(context);
509 }
510 
511 - (void)_drawLineBorderInRect:(CGRect)aRect
512 {
514  cornerRadius = [self cornerRadius],
515  borderWidth = [self borderWidth];
516 
517  aRect = CGRectInset(aRect, borderWidth / 2.0, borderWidth / 2.0);
518 
519  CGContextSetFillColor(context, [self fillColor]);
520  CGContextSetStrokeColor(context, [self borderColor]);
521 
522  CGContextSetLineWidth(context, borderWidth);
523  CGContextFillRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
524  CGContextStrokeRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
525 }
526 
527 - (void)_drawBezelBorderInRect:(CGRect)aRect
528 {
530  cornerRadius = [self cornerRadius],
531  borderWidth = [self borderWidth],
532  shadowOffset = [self valueForThemeAttribute:@"inner-shadow-offset"],
533  shadowSize = [self valueForThemeAttribute:@"inner-shadow-size"],
534  shadowColor = [self valueForThemeAttribute:@"inner-shadow-color"];
535 
536  var baseRect = aRect;
537  aRect = CGRectInset(aRect, borderWidth / 2.0, borderWidth / 2.0);
538 
539  CGContextSaveGState(context);
540 
541  CGContextSetStrokeColor(context, [self borderColor]);
542  CGContextSetLineWidth(context, borderWidth);
543  CGContextSetFillColor(context, [self fillColor]);
544  CGContextFillRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
545  CGContextStrokeRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
546 
547  CGContextRestoreGState(context);
548 }
549 
550 - (void)_drawPrimaryBorderInRect:(CGRect)aRect
551 {
552  // Draw the "primary" style CPBox.
553 
555  cornerRadius = [self cornerRadius],
556  borderWidth = [self borderWidth],
557  shadowOffset = [self valueForThemeAttribute:@"inner-shadow-offset"],
558  shadowSize = [self valueForThemeAttribute:@"inner-shadow-size"],
559  shadowColor = [self valueForThemeAttribute:@"inner-shadow-color"],
560  baseRect = aRect;
561 
562  aRect = CGRectInset(aRect, borderWidth / 2.0, borderWidth / 2.0);
563 
564  CGContextSaveGState(context);
565 
566  CGContextSetStrokeColor(context, [self borderColor]);
567  CGContextSetLineWidth(context, borderWidth);
568  CGContextSetFillColor(context, [self fillColor]);
569  CGContextFillRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
570 
571  CGContextBeginPath(context);
572  // Note we can't use the 0.5 inset rectangle when setting up clipping. The clipping has to be
573  // on integer coordinates for this to look right in Chrome.
574  CGContextAddPath(context, CGPathWithRoundedRectangleInRect(baseRect, cornerRadius, cornerRadius, YES, YES, YES, YES));
575  CGContextClip(context);
576  CGContextSetShadowWithColor(context, shadowOffset, shadowSize, shadowColor);
577  CGContextStrokeRoundedRectangleInRect(context, aRect, cornerRadius, YES, YES, YES, YES);
578 
579  CGContextRestoreGState(context);
580 }
581 
582 - (void)_drawNoBorderInRect:(CGRect)aRect
583 {
585 
586  CGContextSetFillColor(context, [self fillColor]);
587  CGContextFillRect(context, aRect);
588 }
589 
590 @end
591 
592 var CPBoxTypeKey = @"CPBoxTypeKey",
593  CPBoxBorderTypeKey = @"CPBoxBorderTypeKey",
594  CPBoxTitle = @"CPBoxTitle",
595  CPBoxTitlePosition = @"CPBoxTitlePosition",
596  CPBoxTitleView = @"CPBoxTitleView";
597 
598 @implementation CPBox (CPCoding)
599 
600 - (id)initWithCoder:(CPCoder)aCoder
601 {
602  self = [super initWithCoder:aCoder];
603 
604  if (self)
605  {
606  _boxType = [aCoder decodeIntForKey:CPBoxTypeKey];
607  _borderType = [aCoder decodeIntForKey:CPBoxBorderTypeKey];
608 
609  _title = [aCoder decodeObjectForKey:CPBoxTitle];
610  _titlePosition = [aCoder decodeIntForKey:CPBoxTitlePosition];
611  _titleView = [aCoder decodeObjectForKey:CPBoxTitleView] || [CPTextField labelWithTitle:_title];
612 
613  _contentView = [self subviews][0];
614 
615  [self setAutoresizesSubviews:YES];
616  [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
617 
618  [self _manageTitlePositioning];
619  }
620 
621  return self;
622 }
623 
624 - (void)encodeWithCoder:(CPCoder)aCoder
625 {
626  [super encodeWithCoder:aCoder];
627 
628  [aCoder encodeInt:_boxType forKey:CPBoxTypeKey];
629  [aCoder encodeInt:_borderType forKey:CPBoxBorderTypeKey];
630  [aCoder encodeObject:_title forKey:CPBoxTitle];
631  [aCoder encodeInt:_titlePosition forKey:CPBoxTitlePosition];
632  [aCoder encodeObject:_titleView forKey:CPBoxTitleView];
633 }
634 
635 @end
636 
638 
642 - (CPString)title
643 {
644  return _title;
645 }
646 
650 - (int)titlePosition
651 {
652  return _titlePosition;
653 }
654 
655 @end