API  0.9.8
 All Classes Files Functions Variables Typedefs Macros Groups Pages
CPSegmentedControl.j
Go to the documentation of this file.
1 /*
2  * CPSegmentedControl.j
3  * AppKit
4  *
5  * Created by Francisco Tolmasky.
6  * Copyright 2008, 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 
25 @global CPApp
26 
27 @typedef CPSegmentSwitchTracking
31 
37 @implementation CPSegmentedControl : CPControl
38 {
39  CPArray _segments;
40  CPArray _themeStates;
41 
42  int _selectedSegment;
43  int _segmentStyle;
44  CPSegmentSwitchTracking _trackingMode;
45 
46  unsigned _trackingSegment;
47  BOOL _trackingHighlighted;
48 }
49 
50 + (CPString)defaultThemeClass
51 {
52  return "segmented-control";
53 }
54 
55 + (CPDictionary)themeAttributes
56 {
57  return @{
58  @"alignment": CPCenterTextAlignment,
59  @"vertical-alignment": CPCenterVerticalTextAlignment,
60  @"image-position": CPImageLeft,
61  @"image-scaling": CPImageScaleNone,
62  @"bezel-inset": CGInsetMakeZero(),
63  @"content-inset": CGInsetMakeZero(),
64  @"left-segment-bezel-color": [CPNull null],
65  @"right-segment-bezel-color": [CPNull null],
66  @"center-segment-bezel-color": [CPNull null],
67  @"divider-bezel-color": [CPNull null],
68  @"divider-thickness": 1.0,
69  };
70 }
71 
72 - (id)initWithFrame:(CGRect)aRect
73 {
74  _segments = [];
75  _themeStates = [];
76 
77  self = [super initWithFrame:aRect];
78 
79  if (self)
80  {
81  _selectedSegment = -1;
82 
83  _trackingMode = CPSegmentSwitchTrackingSelectOne;
84  _trackingHighlighted = NO;
85  _trackingSegment = -1;
86  }
87 
88  return self;
89 }
90 
95 - (void)setControlSize:(CPControlSize)aControlSize
96 {
97  [super setControlSize:aControlSize];
98  [self _sizeToControlSize];
99 }
100 
104 - (int)selectedTag
105 {
106  return [[_segments objectAtIndex:_selectedSegment] tag];
107 }
108 
110 - (void)setSegments:(CPArray)segments
111 {
112  [_segments removeAllObjects];
113  [_themeStates removeAllObjects];
114 
115  [self insertSegments:segments atIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [segments count])]];
116 }
117 
119 - (void)insertSegments:(CPArray)segments atIndexes:(CPIndexSet)indices
120 {
121  if ([segments count] == 0)
122  return;
123 
124  var newStates = @[],
125  count = [indices count];
126 
127  while (count--)
128  [newStates addObject:CPThemeStateNormal];
129 
130  [_segments insertObjects:segments atIndexes:indices];
131  [_themeStates insertObjects:newStates atIndexes:indices];
132 }
133 
135 - (void)removeSegmentsAtIndexes:(CPIndexSet)indices
136 {
137  if ([indices count] == 0)
138  return;
139 
140  [_segments removeObjectsAtIndexes:indices];
141  [_themeStates removeObjectsAtIndexes:indices];
142 }
143 
144 // Specifying the number of segments
149 - (void)setSegmentCount:(unsigned)aCount
150 {
151  var prevCount = [_segments count];
152 
153  if (aCount == prevCount)
154  return;
155 
156  if (aCount > prevCount)
157  {
158  var count = aCount - prevCount,
159  segments = @[];
160 
161  while (count--)
162  [segments addObject:[[_CPSegmentItem alloc] init]];
163 
164  [self insertSegments:segments atIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(prevCount, aCount - prevCount)]];
165  }
166  else
167  [self removeSegmentsAtIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(aCount, prevCount - aCount)]];
168 
169  [self _updateSelectionIfNeeded];
170  [self tileWithChangedSegment:MAX(MIN(prevCount, aCount) - 1, 0)];
171 }
172 
173 - (void)_updateSelectionIfNeeded
174 {
175  if (_selectedSegment >= [self segmentCount])
176  _selectedSegment = -1;
177 }
178 
182 - (unsigned)segmentCount
183 {
184  return [_segments count];
185 }
186 
187 // Specifying Selected Segment
193 - (void)setSelectedSegment:(unsigned)aSegment
194 {
195  // setSelected:forSegment throws the exception for us (if necessary)
196  if (_selectedSegment == aSegment)
197  return;
198 
199  if (aSegment == -1)
200  {
201  var count = [self segmentCount];
202 
203  while (count--)
204  [self setSelected:NO forSegment:count];
205 
206  _selectedSegment = -1;
207  }
208  else
209  [self setSelected:YES forSegment:aSegment];
210 }
211 
215 - (unsigned)selectedSegment
216 {
217  return _selectedSegment;
218 }
219 
223 - (BOOL)selectSegmentWithTag:(int)aTag
224 {
225  var index = 0;
226 
227  for (; index < [_segments count]; ++index)
228  if ([[_segments objectAtIndex:index] tag] == aTag)
229  {
230  [self setSelectedSegment:index];
231 
232  return YES;
233  }
234 
235  return NO;
236 }
237 
238 - (BOOL)_selectSegmentWithLabel:(CPString)aLabel
239 {
240  var index = 0;
241 
242  for (; index < [_segments count]; ++index)
243  if ([[_segments objectAtIndex:index] label] == aLabel)
244  {
245  [self setSelectedSegment:index];
246 
247  return YES;
248  }
249 
250  return NO;
251 }
252 
253 // Specifying Tracking Mode
255 - (BOOL)isTracking
256 {
257 
258 }
259 
260 - (void)setTrackingMode:(CPSegmentSwitchTracking)aTrackingMode
261 {
262  if (_trackingMode == aTrackingMode)
263  return;
264 
265  _trackingMode = aTrackingMode;
266 
267  if (_trackingMode == CPSegmentSwitchTrackingSelectOne)
268  {
269  var index = 0,
270  selected = NO;
271 
272  for (; index < [self segmentCount]; ++index)
273  if ([_segments[index] selected])
274  if (selected)
275  [self setSelected:NO forSegment:index];
276  else
277  selected = YES;
278  }
279 
280  else if (_trackingMode == CPSegmentSwitchTrackingMomentary)
281  {
282  var index = 0;
283 
284  for (; index < [self segmentCount]; ++index)
285  if ([_segments[index] selected])
286  [self setSelected:NO forSegment:index];
287  }
288 }
289 
293 - (CPSegmentSwitchTracking)trackingMode
294 {
295  return _trackingMode;
296 }
297 
298 // Working with Individual Segments
305 - (void)setWidth:(float)aWidth forSegment:(unsigned)aSegment
306 {
307  [[_segments objectAtIndex:aSegment] setWidth:aWidth];
308  [self tileWithChangedSegment:aSegment];
309 }
310 
316 - (float)widthForSegment:(unsigned)aSegment
317 {
318  return [[_segments objectAtIndex:aSegment] width];
319 }
320 
327 - (void)setImage:(CPImage)anImage forSegment:(unsigned)aSegment
328 {
329  [[_segments objectAtIndex:aSegment] setImage:anImage];
330 
331  [self tileWithChangedSegment:aSegment];
332 }
333 
339 - (CPImage)imageForSegment:(unsigned)aSegment
340 {
341  return [[_segments objectAtIndex:aSegment] image];
342 }
343 
350 - (void)setLabel:(CPString)aLabel forSegment:(unsigned)aSegment
351 {
352  [[_segments objectAtIndex:aSegment] setLabel:aLabel];
353 
354  [self tileWithChangedSegment:aSegment];
355 }
356 
362 - (CPString)labelForSegment:(unsigned)aSegment
363 {
364  return [[_segments objectAtIndex:aSegment] label];
365 }
366 
373 - (void)setMenu:(CPMenu)aMenu forSegment:(unsigned)aSegment
374 {
375  [[_segments objectAtIndex:aSegment] setMenu:aMenu];
376 }
377 
383 - (CPMenu)menuForSegment:(unsigned)aSegment
384 {
385  return [[_segments objectAtIndex:aSegment] menu];
386 }
387 
395 - (void)setSelected:(BOOL)isSelected forSegment:(unsigned)aSegment
396 {
397  var segment = [_segments objectAtIndex:aSegment];
398 
399  // If we're already in this state, bail.
400  if ([segment selected] == isSelected)
401  return;
402 
403  [segment setSelected:isSelected];
404 
405  _themeStates[aSegment] = isSelected ? CPThemeStateSelected : CPThemeStateNormal;
406 
407  // We need to do some cleanup if we only allow one selection.
408  if (isSelected)
409  {
410  var oldSelectedSegment = _selectedSegment;
411 
412  _selectedSegment = aSegment;
413 
414  if (_trackingMode == CPSegmentSwitchTrackingSelectOne && oldSelectedSegment != aSegment && oldSelectedSegment != -1 && oldSelectedSegment < _segments.length)
415  {
416  [_segments[oldSelectedSegment] setSelected:NO];
417  _themeStates[oldSelectedSegment] = CPThemeStateNormal;
418 
419  [self drawSegmentBezel:oldSelectedSegment highlight:NO];
420  }
421  }
422 
423  if (_trackingMode != CPSegmentSwitchTrackingMomentary)
424  [self drawSegmentBezel:aSegment highlight:NO];
425 
426  [self setNeedsLayout];
427  [self setNeedsDisplay:YES];
428 }
429 
435 - (BOOL)isSelectedForSegment:(unsigned)aSegment
436 {
437  return [[_segments objectAtIndex:aSegment] selected];
438 }
439 
446 - (void)setEnabled:(BOOL)shouldBeEnabled forSegment:(unsigned)aSegment
447 {
448  var segment = [_segments objectAtIndex:aSegment];
449 
450  if ([segment enabled] === shouldBeEnabled)
451  return;
452 
453  [segment setEnabled:shouldBeEnabled];
454 
455  if (shouldBeEnabled)
456  _themeStates[aSegment] = _themeStates[aSegment].without(CPThemeStateDisabled);
457  else
458  _themeStates[aSegment] = _themeStates[aSegment].and(CPThemeStateDisabled);
459 
460  [self setNeedsLayout];
461  [self setNeedsDisplay:YES];
462 }
463 
469 - (BOOL)isEnabledForSegment:(unsigned)aSegment
470 {
471  return [[_segments objectAtIndex:aSegment] enabled];
472 }
473 
479 - (void)setTag:(int)aTag forSegment:(unsigned)aSegment
480 {
481  [[_segments objectAtIndex:aSegment] setTag:aTag];
482 }
483 
488 - (int)tagForSegment:(unsigned)aSegment
489 {
490  return [[_segments objectAtIndex:aSegment] tag];
491 }
492 
493 // Drawings
499 - (void)drawSegmentBezel:(int)aSegment highlight:(BOOL)shouldHighlight
500 {
501  if(aSegment < _themeStates.length)
502  {
503  if (shouldHighlight)
504  _themeStates[aSegment] = _themeStates[aSegment].and(CPThemeStateHighlighted);
505  else
506  _themeStates[aSegment] = _themeStates[aSegment].without(CPThemeStateHighlighted);
507  }
508 
509  [self setNeedsLayout];
510  [self setNeedsDisplay:YES];
511 }
512 
513 - (float)_leftOffsetForSegment:(unsigned)segment
514 {
515  if (segment == 0)
516  return [self currentValueForThemeAttribute:@"bezel-inset"].left;
517 
518  var thickness = [self currentValueForThemeAttribute:@"divider-thickness"];
519 
520  return [self _leftOffsetForSegment:segment - 1] + CGRectGetWidth([self frameForSegment:segment - 1]) + thickness;
521 }
522 
523 - (unsigned)_indexOfLastSegment
524 {
525  var lastSegmentIndex = [_segments count] - 1;
526 
527  if (lastSegmentIndex < 0)
528  lastSegmentIndex = 0;
529 
530  return lastSegmentIndex;
531 }
532 
533 - (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
534 {
535  var height = [self currentValueForThemeAttribute:@"min-size"].height,
536  contentInset = [self currentValueForThemeAttribute:@"content-inset"],
537  bezelInset = [self currentValueForThemeAttribute:@"bezel-inset"],
538  bounds = [self bounds];
539 
540  if (aName === "left-segment-bezel")
541  {
542  return CGRectMake(bezelInset.left, bezelInset.top, contentInset.left, height);
543  }
544  else if (aName === "right-segment-bezel")
545  {
546  return CGRectMake(CGRectGetWidth([self bounds]) - contentInset.right,
547  bezelInset.top,
548  contentInset.right,
549  height);
550  }
551  else if (aName.indexOf("segment-bezel") === 0)
552  {
553  var segment = parseInt(aName.substring("segment-bezel-".length), 10),
554  frame = CGRectCreateCopy([self frameForSegment:segment]);
555 
556  if (segment === 0)
557  {
558  frame.origin.x += contentInset.left;
559  frame.size.width -= contentInset.left;
560  }
561 
562  if (segment === [self segmentCount] - 1)
563  frame.size.width = CGRectGetWidth([self bounds]) - contentInset.right - frame.origin.x;
564 
565  return frame;
566  }
567  else if (aName.indexOf("divider-bezel") === 0)
568  {
569  var segment = parseInt(aName.substring("divider-bezel-".length), 10),
570  width = CGRectGetWidth([self frameForSegment:segment]),
571  left = [self _leftOffsetForSegment:segment],
572  thickness = [self currentValueForThemeAttribute:@"divider-thickness"];
573 
574  return CGRectMake(left + width, bezelInset.top, thickness, height);
575  }
576  else if (aName.indexOf("segment-content") === 0)
577  {
578  var segment = parseInt(aName.substring("segment-content-".length), 10);
579 
580  return [self contentFrameForSegment:segment];
581  }
582 
583  return [super rectForEphemeralSubviewNamed:aName];
584 }
585 
586 - (CPView)createEphemeralSubviewNamed:(CPString)aName
587 {
588  if ([aName hasPrefix:@"segment-content"])
589  return [[_CPImageAndTextView alloc] initWithFrame:CGRectMakeZero()];
590 
591  return [[CPView alloc] initWithFrame:CGRectMakeZero()];
592 }
593 
594 - (void)layoutSubviews
595 {
596  if ([self segmentCount] <= 0)
597  return;
598 
599  var themeState = _themeStates[0],
600  isDisabled = [self hasThemeState:CPThemeStateDisabled],
601  isControlSizeSmall = [self hasThemeState:CPThemeStateControlSizeSmall],
602  isControlSizeMini = [self hasThemeState:CPThemeStateControlSizeMini];
603 
604  themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
605 
606  if (isControlSizeSmall)
607  themeState = themeState.and(CPThemeStateControlSizeSmall);
608  else if (isControlSizeMini)
609  themeState = themeState.and(CPThemeStateControlSizeMini);
610 
611  var leftCapColor = [self valueForThemeAttribute:@"left-segment-bezel-color"
612  inState:themeState],
613 
614  leftBezelView = [self layoutEphemeralSubviewNamed:@"left-segment-bezel"
615  positioned:CPWindowBelow
617 
618  [leftBezelView setBackgroundColor:leftCapColor];
619 
620  var themeState = _themeStates[_themeStates.length - 1];
621 
622  themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
623 
624  if (isControlSizeSmall)
625  themeState = themeState.and(CPThemeStateControlSizeSmall);
626  else if (isControlSizeMini)
627  themeState = themeState.and(CPThemeStateControlSizeMini);
628 
629  var rightCapColor = [self valueForThemeAttribute:@"right-segment-bezel-color"
630  inState:themeState],
631 
632  rightBezelView = [self layoutEphemeralSubviewNamed:@"right-segment-bezel"
633  positioned:CPWindowBelow
635 
636  [rightBezelView setBackgroundColor:rightCapColor];
637 
638  for (var i = 0, count = _themeStates.length; i < count; i++)
639  {
640  var themeState = _themeStates[i];
641 
642  themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
643 
644  if (isControlSizeSmall)
645  themeState = themeState.and(CPThemeStateControlSizeSmall);
646  else if (isControlSizeMini)
647  themeState = themeState.and(CPThemeStateControlSizeMini);
648 
649  var bezelColor = [self valueForThemeAttribute:@"center-segment-bezel-color"
650  inState:themeState],
651 
652  bezelView = [self layoutEphemeralSubviewNamed:"segment-bezel-" + i
653  positioned:CPWindowBelow
655 
656  [bezelView setBackgroundColor:bezelColor];
657 
658  // layout image/title views
659  var segment = _segments[i],
660  contentView = [self layoutEphemeralSubviewNamed:@"segment-content-" + i
661  positioned:CPWindowAbove
662  relativeToEphemeralSubviewNamed:@"segment-bezel-" + i];
663 
664  [contentView setText:[segment label]];
665  [contentView setImage:[segment image]];
666 
667  [contentView setFont:[self valueForThemeAttribute:@"font" inState:themeState]];
668  [contentView setTextColor:[self valueForThemeAttribute:@"text-color" inState:themeState]];
669  [contentView setAlignment:[self valueForThemeAttribute:@"alignment" inState:themeState]];
670  [contentView setVerticalAlignment:[self valueForThemeAttribute:@"vertical-alignment" inState:themeState]];
671  [contentView setLineBreakMode:[self valueForThemeAttribute:@"line-break-mode" inState:themeState]];
672  [contentView setTextShadowColor:[self valueForThemeAttribute:@"text-shadow-color" inState:themeState]];
673  [contentView setTextShadowOffset:[self valueForThemeAttribute:@"text-shadow-offset" inState:themeState]];
674  [contentView setImageScaling:[self valueForThemeAttribute:@"image-scaling" inState:themeState]];
675 
676  if ([segment image] && [segment label])
677  [contentView setImagePosition:[self valueForThemeAttribute:@"image-position" inState:themeState]];
678  else if ([segment image])
679  [contentView setImagePosition:CPImageOnly];
680 
681  if (i == count - 1)
682  continue;
683 
684 
685  var borderState = _themeStates[i].and(_themeStates[i + 1]);
686 
687  borderState = isDisabled ? borderState.and(CPThemeStateDisabled) : borderState;
688 
689  if (isControlSizeSmall)
690  borderState = borderState.and(CPThemeStateControlSizeSmall);
691  else if (isControlSizeMini)
692  borderState = borderState.and(CPThemeStateControlSizeMini);
693 
694  var borderColor = [self valueForThemeAttribute:@"divider-bezel-color"
695  inState:borderState],
696 
697  borderView = [self layoutEphemeralSubviewNamed:"divider-bezel-" + i
698  positioned:CPWindowBelow
700 
701  [borderView setBackgroundColor:borderColor];
702  }
703 }
704 
705 
711 - (void)drawSegment:(int)aSegment highlight:(BOOL)shouldHighlight
712 {
713 }
714 
716 - (void)tile
717 {
718  [self tileWithChangedSegment:0];
719 }
720 
722 - (void)tileWithChangedSegment:(CPInteger)aSegment
723 {
724  var segmentCount = [self segmentCount];
725 
726  // Corner case: when segmentCount == 0 and aSegment == 0, we do not return here because we still need to set the new frameSize bellow.
727  if (aSegment < 0 || (segmentCount > 0 && aSegment >= segmentCount))
728  return;
729 
730  // Invalidate frames for segments on the right. They will be lazily computed by -frameForSegment:.
731  for (var i = aSegment; i < segmentCount; i++)
732  [_segments[i] setFrame:CGRectMakeZero()];
733 
734  [self setFrameSize:[self intrinsicContentSize]];
735 
736  [self setNeedsLayout];
737  [self setNeedsDisplay:YES];
738 }
739 
741 - (CGSize)intrinsicContentSize
742 {
743  // frameForSegment is recursively called backwards. All previously invalidated frames will be recomputed.
744  var segmentCount = [self segmentCount],
745  width = 0;
746 
747  if (segmentCount > 0)
748  width = CGRectGetMaxX([self frameForSegment:(segmentCount - 1)]);
749 
750  return CGSizeMake(width, [self valueForThemeAttribute:@"min-size"].height);
751 }
752 
757 - (CGRect)frameForSegment:(unsigned)aSegment
758 {
759  var segment = [_segments objectAtIndex:aSegment],
760  frame = [segment frame];
761 
762  if (CGRectEqualToRect(frame, CGRectMakeZero()))
763  {
764  frame = [self bezelFrameForSegment:aSegment];
765  [segment setFrame:frame];
766  }
767 
768  return frame;
769 }
770 
771 - (CGRect)bezelFrameForSegment:(unsigned)aSegment
772 {
773  var left = [self _leftOffsetForSegment:aSegment],
774  top = [self currentValueForThemeAttribute:@"bezel-inset"].top,
775  width = [self widthForSegment:aSegment],
776  height = [self currentValueForThemeAttribute:@"min-size"].height;
777 
778  if (width == 0)
779  {
780  var themeState = _themeState.hasThemeState(CPThemeStateDisabled) ? _themeStates[aSegment].and(CPThemeStateDisabled) : _themeStates[aSegment],
781  contentInset = [self valueForThemeAttribute:@"content-inset" inState:themeState],
782  contentInsetWidth = contentInset.left + contentInset.right,
783 
784  segment = _segments[aSegment],
785  label = [segment label],
786  image = [segment image];
787 
788  width = (label ? [label sizeWithFont:[self font]].width : 4.0) + (image ? [image size].width : 0) + contentInsetWidth;
789  }
790 
791  return CGRectMake(left, top, width, height);
792 }
793 
794 - (CGRect)contentFrameForSegment:(unsigned)aSegment
795 {
796  var height = [self currentValueForThemeAttribute:@"min-size"].height,
797  contentInset = [self currentValueForThemeAttribute:@"content-inset"],
798  width = CGRectGetWidth([self frameForSegment:aSegment]),
799  left = [self _leftOffsetForSegment:aSegment];
800 
801  return CGRectMake(left + contentInset.left, contentInset.top, width - contentInset.left - contentInset.right, height - contentInset.top - contentInset.bottom);
802 }
803 
809 - (unsigned)testSegment:(CGPoint)aPoint
810 {
811  var location = [self convertPoint:aPoint fromView:nil],
812  count = [self segmentCount];
813 
814  while (count--)
815  if (CGRectContainsPoint([self frameForSegment:count], aPoint))
816  return count;
817 
818  if ([self segmentCount])
819  {
820  var adjustedLastFrame = CGRectCreateCopy([self frameForSegment:(_segments.length - 1)]);
821  adjustedLastFrame.size.width = CGRectGetWidth([self bounds]) - adjustedLastFrame.origin.x;
822 
823  if (CGRectContainsPoint(adjustedLastFrame, aPoint))
824  return [self segmentCount] - 1;
825  }
826 
827  return -1;
828 }
829 
830 - (void)mouseDown:(CPEvent)anEvent
831 {
832  if (![self isEnabled])
833  return;
834 
835  [self trackSegment:anEvent];
836 }
837 
838 // FIXME: this should be fixed way up in cpbutton/cpcontrol.
839 - (void)mouseUp:(CPEvent)anEvent
840 {
841 }
842 
847 - (void)trackSegment:(CPEvent)anEvent
848 {
849  var type = [anEvent type],
850  location = [self convertPoint:[anEvent locationInWindow] fromView:nil];
851 
852  if (type == CPLeftMouseUp)
853  {
854  if (_trackingSegment == -1)
855  return;
856 
857  if (_trackingSegment === [self testSegment:location])
858  {
859  if (_trackingMode == CPSegmentSwitchTrackingSelectAny)
860  {
861  [self setSelected:![self isSelectedForSegment:_trackingSegment] forSegment:_trackingSegment];
862 
863  // With ANY, _selectedSegment means last pressed.
864  _selectedSegment = _trackingSegment;
865  }
866  else
867  [self setSelected:YES forSegment:_trackingSegment];
868 
869  [self sendAction:[self action] to:[self target]];
870 
871  if (_trackingMode == CPSegmentSwitchTrackingMomentary)
872  {
873  [self setSelected:NO forSegment:_trackingSegment];
874 
875  _selectedSegment = CPNotFound;
876  }
877  }
878 
879  [self drawSegmentBezel:_trackingSegment highlight:NO];
880 
881  _trackingSegment = -1;
882 
883  return;
884  }
885 
886  if (type == CPLeftMouseDown)
887  {
888  var trackingSegment = [self testSegment:location];
889  if (trackingSegment > -1 && [self isEnabledForSegment:trackingSegment])
890  {
891  _trackingHighlighted = YES;
892  _trackingSegment = trackingSegment;
893  [self drawSegmentBezel:_trackingSegment highlight:YES];
894  }
895  }
896 
897  else if (type == CPLeftMouseDragged)
898  {
899  if (_trackingSegment == -1)
900  return;
901 
902  var highlighted = [self testSegment:location] === _trackingSegment;
903 
904  if (highlighted != _trackingHighlighted)
905  {
906  _trackingHighlighted = highlighted;
907 
908  [self drawSegmentBezel:_trackingSegment highlight:_trackingHighlighted];
909  }
910  }
911 
912  [CPApp setTarget:self selector:@selector(trackSegment:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES];
913 }
914 
915 - (void)setFont:(CPFont)aFont
916 {
917  [super setFont:aFont];
918 
919  [self tile];
920 }
921 
922 @end
923 
924 var CPSegmentedControlSegmentsKey = "CPSegmentedControlSegmentsKey",
925  CPSegmentedControlSelectedKey = "CPSegmentedControlSelectedKey",
926  CPSegmentedControlSegmentStyleKey = "CPSegmentedControlSegmentStyleKey",
927  CPSegmentedControlTrackingModeKey = "CPSegmentedControlTrackingModeKey";
928 
930 
931 - (id)initWithCoder:(CPCoder)aCoder
932 {
933  self = [super initWithCoder:aCoder];
934 
935  if (self)
936  {
937  var frame = [self frame],
938  originalWidth = frame.size.width;
939 
940  frame.size.width = 0;
941 
942  [self setFrame:frame];
943 
944  _segments = [aCoder decodeObjectForKey:CPSegmentedControlSegmentsKey];
945  _segmentStyle = [aCoder decodeIntForKey:CPSegmentedControlSegmentStyleKey];
946  _themeStates = [];
947 
948  if ([aCoder containsValueForKey:CPSegmentedControlSelectedKey])
949  _selectedSegment = [aCoder decodeIntForKey:CPSegmentedControlSelectedKey];
950  else
951  _selectedSegment = CPNotFound;
952 
953  if ([aCoder containsValueForKey:CPSegmentedControlTrackingModeKey])
954  _trackingMode = [aCoder decodeIntForKey:CPSegmentedControlTrackingModeKey];
955  else
956  _trackingMode = CPSegmentSwitchTrackingSelectOne;
957 
958  // Here we update the themeStates array for each segments to know if there are selected or not
959  for (var i = 0; i < [self segmentCount]; i++)
960  _themeStates[i] = [_segments[i] selected] ? CPThemeStateSelected : CPThemeStateNormal;
961 
962  [self tile];
963 
964  var thickness = [self currentValueForThemeAttribute:@"divider-thickness"],
965  dividerExtraSpace = ([_segments count] - 1) * thickness,
966  difference = MAX(originalWidth - [self frame].size.width - dividerExtraSpace, 0.0),
967  remainingWidth = FLOOR(difference / [self segmentCount]),
968  widthOfAllSegments = 0;
969 
970  // We do this in a second loop because it relies on all the themeStates being set first
971  for (var i = 0; i < [self segmentCount]; i++)
972  {
973  var frame = [_segments[i] frame];
974  frame.size.width += remainingWidth;
975 
976  widthOfAllSegments += CGRectGetWidth(frame);
977  }
978 
979  // Here we handle the leftovers pixel, and we will add one pixel to each segment cell till we have the same size as the originalSize.
980  // This is needed to have a perfect/same alignment between our application and xCode.
981  var leftOversPixel = originalWidth - (widthOfAllSegments + dividerExtraSpace);
982 
983  // Make sure we don't make an out of range
984  if (leftOversPixel < [self segmentCount] - 1)
985  {
986  for (var i = 0; i < leftOversPixel; i++)
987  {
988  [_segments[i] frame].size.width += 1;
989  }
990  }
991 
992  [self setFrameSize:CGSizeMake(originalWidth, CGRectGetHeight([self frame]))];
993  [self tile];
994  }
995 
996  return self;
997 }
998 
999 - (void)encodeWithCoder:(CPCoder)aCoder
1000 {
1001  [super encodeWithCoder:aCoder];
1002 
1003  [aCoder encodeObject:_segments forKey:CPSegmentedControlSegmentsKey];
1004  [aCoder encodeInt:_selectedSegment forKey:CPSegmentedControlSelectedKey];
1005  [aCoder encodeInt:_segmentStyle forKey:CPSegmentedControlSegmentStyleKey];
1006  [aCoder encodeInt:_trackingMode forKey:CPSegmentedControlTrackingModeKey];
1007 }
1008 
1009 @end
1010 
1012 
1013 + (Class)_binderClassForBinding:(CPString)aBinding
1014 {
1015  if ([self _isSelectionBinding:aBinding])
1016  return [_CPSegmentedControlBinder class];
1017 
1018  return [super _binderClassForBinding:aBinding];
1019 }
1020 
1021 + (BOOL)_isSelectionBinding:(CPString)aBinding
1022 {
1023  return (aBinding === CPSelectedIndexBinding || aBinding === CPSelectedLabelBinding || aBinding === CPSelectedTagBinding);
1024 }
1025 
1026 + (BOOL)isBindingExclusive:(CPString)aBinding
1027 {
1028  return [self _isSelectionBinding:aBinding];
1029 }
1030 
1031 - (void)bind:(CPString)aBinding toObject:(id)anObject withKeyPath:(CPString)aKeyPath options:(CPDictionary)options
1032 {
1033  if ([[self class] _isSelectionBinding:aBinding] && _trackingMode !== CPSegmentSwitchTrackingSelectOne)
1034  {
1035  CPLog.warn("Binding " + aBinding + " needs CPSegmentSwitchTrackingSelectOne tracking mode");
1036  return;
1037  }
1038 
1039  [super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options];
1040 }
1041 
1042 - (void)_reverseSetBinding
1043 {
1044  [_CPSegmentedControlBinder _reverseSetValueFromExclusiveBinderForObject:self];
1045 }
1046 
1047 @end
1048 
1049 var CPSegmentedControlNoSelectionPlaceholder = "CPSegmentedControlNoSelectionPlaceholder";
1050 @implementation _CPSegmentedControlBinder : CPBinder
1051 {
1052  id __doxygen__;
1053 }
1054 
1055 - (void)_updatePlaceholdersWithOptions:(CPDictionary)options
1056 {
1057  [super _updatePlaceholdersWithOptions:options];
1058 
1059  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPMultipleValuesMarker isDefault:YES];
1060  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNoSelectionMarker isDefault:YES];
1061  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNotApplicableMarker isDefault:YES];
1062  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNullMarker isDefault:YES];
1063 }
1064 
1065 - (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
1066 {
1068  [_source setSelected:NO forSegment:[_source selectedSegment]];
1069  else
1070  [self setValue:aValue forBinding:aBinding];
1071 }
1072 
1073 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
1074 {
1075  if (aBinding == CPSelectedIndexBinding)
1076  [_source setSelectedSegment:aValue];
1077  else if (aBinding == CPSelectedTagBinding)
1078  [_source selectSegmentWithTag:aValue];
1079  else if (aBinding == CPSelectedLabelBinding)
1080  [_source _selectSegmentWithLabel:aValue];
1081 }
1082 
1083 - (id)valueForBinding:(CPString)aBinding
1084 {
1085  var selectedIndex = [_source selectedSegment];
1086 
1087  if (aBinding == CPSelectedIndexBinding)
1088  return selectedIndex;
1089  else if (aBinding == CPSelectedTagBinding)
1090  return [_source tagForSegment:selectedIndex];
1091  else if (aBinding == CPSelectedLabelBinding)
1092  return [_source labelForSegment:selectedIndex];
1093 }
1094 
1095 @end
1096 
1097 @implementation _CPSegmentItem : CPObject
1098 {
1099  CPImage image;
1102  BOOL selected;
1103  BOOL enabled;
1104  int tag;
1105  int width;
1106 
1107  CGRect frame;
1108 }
1109 
1110 - (id)init
1111 {
1112  if (self = [super init])
1113  {
1114  image = nil;
1115  label = @"";
1116  menu = nil;
1117  selected = NO;
1118  enabled = YES;
1119  tag = -1;
1120  width = 0;
1121 
1122  frame = CGRectMakeZero();
1123  }
1124  return self;
1125 }
1126 
1127 @end
1128 
1129 var CPSegmentItemImageKey = "CPSegmentItemImageKey",
1130  CPSegmentItemLabelKey = "CPSegmentItemLabelKey",
1131  CPSegmentItemMenuKey = "CPSegmentItemMenuKey",
1132  CPSegmentItemSelectedKey = "CPSegmentItemSelectedKey",
1133  CPSegmentItemEnabledKey = "CPSegmentItemEnabledKey",
1134  CPSegmentItemTagKey = "CPSegmentItemTagKey",
1135  CPSegmentItemWidthKey = "CPSegmentItemWidthKey";
1136 
1137 @implementation _CPSegmentItem (CPCoding)
1138 
1139 - (id)initWithCoder:(CPCoder)aCoder
1140 {
1141  self = [super init];
1142 
1143  if (self)
1144  {
1145  image = [aCoder decodeObjectForKey:CPSegmentItemImageKey];
1146  label = [aCoder decodeObjectForKey:CPSegmentItemLabelKey];
1147  menu = [aCoder decodeObjectForKey:CPSegmentItemMenuKey];
1148  selected = [aCoder decodeBoolForKey:CPSegmentItemSelectedKey];
1149  enabled = [aCoder decodeBoolForKey:CPSegmentItemEnabledKey];
1150  tag = [aCoder decodeIntForKey:CPSegmentItemTagKey];
1151  width = [aCoder decodeFloatForKey:CPSegmentItemWidthKey];
1152 
1153  frame = CGRectMakeZero();
1154  }
1155 
1156  return self;
1157 }
1158 
1159 - (void)encodeWithCoder:(CPCoder)aCoder
1160 {
1161  [aCoder encodeObject:image forKey:CPSegmentItemImageKey];
1162  [aCoder encodeObject:label forKey:CPSegmentItemLabelKey];
1163  [aCoder encodeObject:menu forKey:CPSegmentItemMenuKey];
1164  [aCoder encodeBool:selected forKey:CPSegmentItemSelectedKey];
1165  [aCoder encodeBool:enabled forKey:CPSegmentItemEnabledKey];
1166  [aCoder encodeInt:tag forKey:CPSegmentItemTagKey];
1167  [aCoder encodeFloat:width forKey:CPSegmentItemWidthKey];
1168 }
1169 
1170 @end
1171 
1173 
1177 - (CPArray)segments
1178 {
1179  return _segments;
1180 }
1181 
1182 @end