API  0.9.10
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 
51 {
52  return "segmented-control";
53 }
54 
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 
105 {
106  return [[_segments objectAtIndex:_selectedSegment] tag];
107 }
108 
110 - (void)setSegments:(CPArray)segments
111 {
113 
114  [self insertSegments:segments atIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [segments count])]];
115 }
116 
118 - (void)insertSegments:(CPArray)segments atIndexes:(CPIndexSet)indices
119 {
120  if ([segments count] == 0)
121  return;
122 
123  var newStates = @[],
124  count = [indices count];
125 
126  while (count--)
127  [newStates addObject:CPThemeStateNormal];
128 
129  [_segments insertObjects:segments atIndexes:indices];
130  [_themeStates insertObjects:newStates atIndexes:indices];
131 
132  if (_selectedSegment >= [indices firstIndex])
133  _selectedSegment += [indices count];
134 }
135 
137 - (void)removeSegmentsAtIndexes:(CPIndexSet)indices
138 {
139  if ([indices count] == 0)
140  return;
141 
142  [indices enumerateIndexesUsingBlock:function(idx, stop)
143  {
144  [[_segments objectAtIndex:idx] setSelected:NO];
145  }];
146 
147  if ([indices containsIndex:_selectedSegment])
148  _selectedSegment = -1;
149  else if ([indices lastIndex] < _selectedSegment)
150  _selectedSegment -= [indices count];
151 
152  [_segments removeObjectsAtIndexes:indices];
153  [_themeStates removeObjectsAtIndexes:indices];
154 }
155 
156 // Specifying the number of segments
161 - (void)setSegmentCount:(unsigned)aCount
162 {
163  var prevCount = [_segments count];
164 
165  if (aCount == prevCount)
166  return;
167 
168  if (aCount > prevCount)
169  {
170  var count = aCount - prevCount,
171  segments = @[];
172 
173  while (count--)
174  [segments addObject:[[_CPSegmentItem alloc] init]];
175 
176  [self insertSegments:segments atIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(prevCount, aCount - prevCount)]];
177  }
178  else
179  [self removeSegmentsAtIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(aCount, prevCount - aCount)]];
180 
181  [self _updateSelectionIfNeeded];
182  [self tileWithChangedSegment:MAX(MIN(prevCount, aCount) - 1, 0)];
183 }
184 
185 - (void)_updateSelectionIfNeeded
186 {
187  if (_selectedSegment >= [self segmentCount])
188  _selectedSegment = -1;
189 }
190 
194 - (unsigned)segmentCount
195 {
196  return [_segments count];
197 }
198 
199 // Specifying Selected Segment
205 - (void)setSelectedSegment:(unsigned)aSegment
206 {
207  // setSelected:forSegment throws the exception for us (if necessary)
208  if (_selectedSegment == aSegment)
209  return;
210 
211  if (aSegment == -1)
212  {
213  var count = [self segmentCount];
214 
215  while (count--)
216  [self setSelected:NO forSegment:count];
217 
218  _selectedSegment = -1;
219  }
220  else
221  [self setSelected:YES forSegment:aSegment];
222 }
223 
227 - (unsigned)selectedSegment
228 {
229  return _selectedSegment;
230 }
231 
235 - (BOOL)selectSegmentWithTag:(int)aTag
236 {
237  var index = 0;
238 
239  for (; index < [_segments count]; ++index)
240  if ([[_segments objectAtIndex:index] tag] == aTag)
241  {
242  [self setSelectedSegment:index];
243 
244  return YES;
245  }
246 
247  return NO;
248 }
249 
250 - (BOOL)_selectSegmentWithLabel:(CPString)aLabel
251 {
252  var index = 0;
253 
254  for (; index < [_segments count]; ++index)
255  if ([[_segments objectAtIndex:index] label] == aLabel)
256  {
257  [self setSelectedSegment:index];
258 
259  return YES;
260  }
261 
262  return NO;
263 }
264 
265 // Specifying Tracking Mode
267 - (BOOL)isTracking
268 {
269 
270 }
271 
272 - (void)setTrackingMode:(CPSegmentSwitchTracking)aTrackingMode
273 {
274  if (_trackingMode == aTrackingMode)
275  return;
276 
277  _trackingMode = aTrackingMode;
278 
279  if (_trackingMode == CPSegmentSwitchTrackingSelectOne)
280  {
281  var index = 0,
282  selected = NO;
283 
284  for (; index < [self segmentCount]; ++index)
285  if ([_segments[index] selected])
286  if (selected)
287  [self setSelected:NO forSegment:index];
288  else
289  selected = YES;
290  }
291 
292  else if (_trackingMode == CPSegmentSwitchTrackingMomentary)
293  {
294  var index = 0;
295 
296  for (; index < [self segmentCount]; ++index)
297  if ([_segments[index] selected])
298  [self setSelected:NO forSegment:index];
299  }
300 }
301 
305 - (CPSegmentSwitchTracking)trackingMode
306 {
307  return _trackingMode;
308 }
309 
310 // Working with Individual Segments
317 - (void)setWidth:(float)aWidth forSegment:(unsigned)aSegment
318 {
319  [[_segments objectAtIndex:aSegment] setWidth:aWidth];
320  [self tileWithChangedSegment:aSegment];
321 }
322 
328 - (float)widthForSegment:(unsigned)aSegment
329 {
330  return [[_segments objectAtIndex:aSegment] width];
331 }
332 
339 - (void)setImage:(CPImage)anImage forSegment:(unsigned)aSegment
340 {
341  [[_segments objectAtIndex:aSegment] setImage:anImage];
342 
343  [self tileWithChangedSegment:aSegment];
344 }
345 
351 - (CPImage)imageForSegment:(unsigned)aSegment
352 {
353  return [[_segments objectAtIndex:aSegment] image];
354 }
355 
362 - (void)setLabel:(CPString)aLabel forSegment:(unsigned)aSegment
363 {
364  [[_segments objectAtIndex:aSegment] setLabel:aLabel];
365 
366  [self tileWithChangedSegment:aSegment];
367 }
368 
374 - (CPString)labelForSegment:(unsigned)aSegment
375 {
376  return [[_segments objectAtIndex:aSegment] label];
377 }
378 
385 - (void)setMenu:(CPMenu)aMenu forSegment:(unsigned)aSegment
386 {
387  [[_segments objectAtIndex:aSegment] setMenu:aMenu];
388 }
389 
395 - (CPMenu)menuForSegment:(unsigned)aSegment
396 {
397  return [[_segments objectAtIndex:aSegment] menu];
398 }
399 
407 - (void)setSelected:(BOOL)isSelected forSegment:(unsigned)aSegment
408 {
409  var segment = [_segments objectAtIndex:aSegment];
410 
411  // If we're already in this state, bail.
412  if ([segment selected] == isSelected)
413  return;
414 
415  [segment setSelected:isSelected];
416 
417  _themeStates[aSegment] = isSelected ? CPThemeStateSelected : CPThemeStateNormal;
418 
419  // We need to do some cleanup if we only allow one selection.
420  if (isSelected)
421  {
422  var oldSelectedSegment = _selectedSegment;
423 
424  _selectedSegment = aSegment;
425 
426  if (_trackingMode == CPSegmentSwitchTrackingSelectOne && oldSelectedSegment != aSegment && oldSelectedSegment != -1 && oldSelectedSegment < _segments.length)
427  {
428  [_segments[oldSelectedSegment] setSelected:NO];
429  _themeStates[oldSelectedSegment] = CPThemeStateNormal;
430 
431  [self drawSegmentBezel:oldSelectedSegment highlight:NO];
432  }
433  }
434 
435  if (_trackingMode != CPSegmentSwitchTrackingMomentary)
436  [self drawSegmentBezel:aSegment highlight:NO];
437 
438  [self setNeedsLayout];
439  [self setNeedsDisplay:YES];
440 }
441 
447 - (BOOL)isSelectedForSegment:(unsigned)aSegment
448 {
449  return [[_segments objectAtIndex:aSegment] selected];
450 }
451 
458 - (void)setEnabled:(BOOL)shouldBeEnabled forSegment:(unsigned)aSegment
459 {
460  var segment = [_segments objectAtIndex:aSegment];
461 
462  if ([segment enabled] === shouldBeEnabled)
463  return;
464 
465  [segment setEnabled:shouldBeEnabled];
466 
467  if (shouldBeEnabled)
468  _themeStates[aSegment] = _themeStates[aSegment].without(CPThemeStateDisabled);
469  else
470  _themeStates[aSegment] = _themeStates[aSegment].and(CPThemeStateDisabled);
471 
472  [self setNeedsLayout];
473  [self setNeedsDisplay:YES];
474 }
475 
481 - (BOOL)isEnabledForSegment:(unsigned)aSegment
482 {
483  return [[_segments objectAtIndex:aSegment] enabled];
484 }
485 
491 - (void)setTag:(int)aTag forSegment:(unsigned)aSegment
492 {
493  [[_segments objectAtIndex:aSegment] setTag:aTag];
494 }
495 
500 - (int)tagForSegment:(unsigned)aSegment
501 {
502  return [[_segments objectAtIndex:aSegment] tag];
503 }
504 
505 // Drawings
511 - (void)drawSegmentBezel:(int)aSegment highlight:(BOOL)shouldHighlight
512 {
513  if (aSegment < _themeStates.length)
514  {
515  if (shouldHighlight)
516  _themeStates[aSegment] = _themeStates[aSegment].and(CPThemeStateHighlighted);
517  else
518  _themeStates[aSegment] = _themeStates[aSegment].without(CPThemeStateHighlighted);
519  }
520 
521  [self setNeedsLayout];
522  [self setNeedsDisplay:YES];
523 }
524 
525 - (float)_leftOffsetForSegment:(unsigned)segment
526 {
527  if (segment == 0)
528  return [self currentValueForThemeAttribute:@"bezel-inset"].left;
529 
530  var thickness = [self currentValueForThemeAttribute:@"divider-thickness"];
531 
532  return [self _leftOffsetForSegment:segment - 1] + CGRectGetWidth([self frameForSegment:segment - 1]) + thickness;
533 }
534 
535 - (unsigned)_indexOfLastSegment
536 {
537  var lastSegmentIndex = [_segments count] - 1;
538 
539  if (lastSegmentIndex < 0)
540  lastSegmentIndex = 0;
541 
542  return lastSegmentIndex;
543 }
544 
545 - (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
546 {
547  var height = [self currentValueForThemeAttribute:@"min-size"].height,
548  contentInset = [self currentValueForThemeAttribute:@"content-inset"],
549  bezelInset = [self currentValueForThemeAttribute:@"bezel-inset"],
550  bounds = [self bounds];
551 
552  if (aName === "left-segment-bezel")
553  {
554  return CGRectMake(bezelInset.left, bezelInset.top, contentInset.left, height);
555  }
556  else if (aName === "right-segment-bezel")
557  {
558  return CGRectMake(CGRectGetWidth([self bounds]) - contentInset.right,
559  bezelInset.top,
560  contentInset.right,
561  height);
562  }
563  else if (aName.indexOf("segment-bezel") === 0)
564  {
565  var segment = parseInt(aName.substring("segment-bezel-".length), 10),
566  frame = CGRectCreateCopy([self frameForSegment:segment]);
567 
568  if (segment === 0)
569  {
570  frame.origin.x += contentInset.left;
571  frame.size.width -= contentInset.left;
572  }
573 
574  if (segment === [self segmentCount] - 1)
575  frame.size.width = CGRectGetWidth([self bounds]) - contentInset.right - frame.origin.x;
576 
577  return frame;
578  }
579  else if (aName.indexOf("divider-bezel") === 0)
580  {
581  var segment = parseInt(aName.substring("divider-bezel-".length), 10),
582  width = CGRectGetWidth([self frameForSegment:segment]),
583  left = [self _leftOffsetForSegment:segment],
584  thickness = [self currentValueForThemeAttribute:@"divider-thickness"];
585 
586  return CGRectMake(left + width, bezelInset.top, thickness, height);
587  }
588  else if (aName.indexOf("segment-content") === 0)
589  {
590  var segment = parseInt(aName.substring("segment-content-".length), 10);
591 
592  return [self contentFrameForSegment:segment];
593  }
594 
595  return [super rectForEphemeralSubviewNamed:aName];
596 }
597 
598 - (CPView)createEphemeralSubviewNamed:(CPString)aName
599 {
600  if ([aName hasPrefix:@"segment-content"])
601  {
602  var view = [[_CPImageAndTextView alloc] initWithFrame:CGRectMakeZero()];
603  [view _setUsesSingleLineMode:YES];
604 
605  return view;
606  }
607 
608  return [[CPView alloc] initWithFrame:CGRectMakeZero()];
609 }
610 
612 {
613  if ([self segmentCount] <= 0)
614  return;
615 
616  var themeState = _themeStates[0],
617  isDisabled = [self hasThemeState:CPThemeStateDisabled],
618  isControlSizeSmall = [self hasThemeState:CPThemeStateControlSizeSmall],
619  isControlSizeMini = [self hasThemeState:CPThemeStateControlSizeMini];
620 
621  themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
622 
623  if (isControlSizeSmall)
624  themeState = themeState.and(CPThemeStateControlSizeSmall);
625  else if (isControlSizeMini)
626  themeState = themeState.and(CPThemeStateControlSizeMini);
627 
628  var leftCapColor = [self valueForThemeAttribute:@"left-segment-bezel-color"
629  inState:themeState],
630 
631  leftBezelView = [self layoutEphemeralSubviewNamed:@"left-segment-bezel"
632  positioned:CPWindowBelow
634 
635  [leftBezelView setBackgroundColor:leftCapColor];
636 
637  var themeState = _themeStates[_themeStates.length - 1];
638 
639  themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
640 
641  if (isControlSizeSmall)
642  themeState = themeState.and(CPThemeStateControlSizeSmall);
643  else if (isControlSizeMini)
644  themeState = themeState.and(CPThemeStateControlSizeMini);
645 
646  var rightCapColor = [self valueForThemeAttribute:@"right-segment-bezel-color"
647  inState:themeState],
648 
649  rightBezelView = [self layoutEphemeralSubviewNamed:@"right-segment-bezel"
650  positioned:CPWindowBelow
652 
653  [rightBezelView setBackgroundColor:rightCapColor];
654 
655  for (var i = 0, count = _themeStates.length; i < count; i++)
656  {
657  var themeState = _themeStates[i];
658 
659  themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
660 
661  if (isControlSizeSmall)
662  themeState = themeState.and(CPThemeStateControlSizeSmall);
663  else if (isControlSizeMini)
664  themeState = themeState.and(CPThemeStateControlSizeMini);
665 
666  var bezelColor = [self valueForThemeAttribute:@"center-segment-bezel-color"
667  inState:themeState],
668 
669  bezelView = [self layoutEphemeralSubviewNamed:"segment-bezel-" + i
670  positioned:CPWindowBelow
672 
673  [bezelView setBackgroundColor:bezelColor];
674 
675  // layout image/title views
676  var segment = _segments[i],
677  contentView = [self layoutEphemeralSubviewNamed:@"segment-content-" + i
678  positioned:CPWindowAbove
679  relativeToEphemeralSubviewNamed:@"segment-bezel-" + i];
680 
681  [contentView setText:[segment label]];
682  [contentView setImage:[segment image]];
683 
684  [contentView setFont:[self valueForThemeAttribute:@"font" inState:themeState]];
685  [contentView setTextColor:[self valueForThemeAttribute:@"text-color" inState:themeState]];
686  [contentView setAlignment:[self valueForThemeAttribute:@"alignment" inState:themeState]];
687  [contentView setVerticalAlignment:[self valueForThemeAttribute:@"vertical-alignment" inState:themeState]];
688  [contentView setLineBreakMode:[self valueForThemeAttribute:@"line-break-mode" inState:themeState]];
689  [contentView setTextShadowColor:[self valueForThemeAttribute:@"text-shadow-color" inState:themeState]];
690  [contentView setTextShadowOffset:[self valueForThemeAttribute:@"text-shadow-offset" inState:themeState]];
691  [contentView setImageScaling:[self valueForThemeAttribute:@"image-scaling" inState:themeState]];
692 
693  if ([segment image] && [segment label])
694  [contentView setImagePosition:[self valueForThemeAttribute:@"image-position" inState:themeState]];
695  else if ([segment image])
696  [contentView setImagePosition:CPImageOnly];
697 
698  if (i == count - 1)
699  continue;
700 
701 
702  var borderState = _themeStates[i].and(_themeStates[i + 1]);
703 
704  borderState = isDisabled ? borderState.and(CPThemeStateDisabled) : borderState;
705 
706  if (isControlSizeSmall)
707  borderState = borderState.and(CPThemeStateControlSizeSmall);
708  else if (isControlSizeMini)
709  borderState = borderState.and(CPThemeStateControlSizeMini);
710 
711  var borderColor = [self valueForThemeAttribute:@"divider-bezel-color"
712  inState:borderState],
713 
714  borderView = [self layoutEphemeralSubviewNamed:"divider-bezel-" + i
715  positioned:CPWindowBelow
717 
718  [borderView setBackgroundColor:borderColor];
719  }
720 }
721 
722 
728 - (void)drawSegment:(int)aSegment highlight:(BOOL)shouldHighlight
729 {
730 }
731 
733 - (void)tile
734 {
735  [self tileWithChangedSegment:0];
736 }
737 
739 - (void)tileWithChangedSegment:(CPInteger)aSegment
740 {
741  var segmentCount = [self segmentCount];
742 
743  // Corner case: when segmentCount == 0 and aSegment == 0, we do not return here because we still need to set the new frameSize bellow.
744  if (aSegment < 0 || (segmentCount > 0 && aSegment >= segmentCount))
745  return;
746 
747  var width = 0;
748 
749  if (segmentCount > 0)
750  {
751  // Invalidate frames for segments on the right. They will be lazily computed by -frameForSegment:.
752  for (var i = aSegment; i < segmentCount; i++)
753  [_segments[i] setFrame:CGRectMakeZero()];
754 
755  width = CGRectGetMaxX([self frameForSegment:(segmentCount - 1)]);
756  }
757 
758  [self setFrameSize:CGSizeMake(width, CGRectGetHeight([self frame]))];
759 
760  [self setNeedsLayout];
761  [self setNeedsDisplay:YES];
762 }
763 
768 - (CGRect)frameForSegment:(unsigned)aSegment
769 {
770  var segment = [_segments objectAtIndex:aSegment],
771  frame = [segment frame];
772 
773  if (CGRectEqualToRect(frame, CGRectMakeZero()))
774  {
775  frame = [self bezelFrameForSegment:aSegment];
776  [segment setFrame:frame];
777  }
778 
779  return frame;
780 }
781 
782 - (CGRect)bezelFrameForSegment:(unsigned)aSegment
783 {
784  var left = [self _leftOffsetForSegment:aSegment],
785  top = [self currentValueForThemeAttribute:@"bezel-inset"].top,
786  width = [self widthForSegment:aSegment],
787  height = [self currentValueForThemeAttribute:@"min-size"].height;
788 
789  if (width == 0)
790  {
791  var themeState = _themeState.hasThemeState(CPThemeStateDisabled) ? _themeStates[aSegment].and(CPThemeStateDisabled) : _themeStates[aSegment],
792  contentInset = [self valueForThemeAttribute:@"content-inset" inState:themeState],
793  contentInsetWidth = contentInset.left + contentInset.right,
794 
795  segment = _segments[aSegment],
796  label = [segment label],
797  image = [segment image];
798 
799  width = (label ? [label sizeWithFont:[self font]].width : 4.0) + (image ? [image size].width : 0) + contentInsetWidth;
800  }
801 
802  return CGRectMake(left, top, width, height);
803 }
804 
805 - (CGRect)contentFrameForSegment:(unsigned)aSegment
806 {
807  var height = [self currentValueForThemeAttribute:@"min-size"].height,
808  contentInset = [self currentValueForThemeAttribute:@"content-inset"],
809  width = CGRectGetWidth([self frameForSegment:aSegment]),
810  left = [self _leftOffsetForSegment:aSegment];
811 
812  return CGRectMake(left + contentInset.left, contentInset.top, width - contentInset.left - contentInset.right, height - contentInset.top - contentInset.bottom);
813 }
814 
815 - (CGSize)_minimumFrameSize
816 {
817  // The current width is always the minimum width.
818  return CGSizeMake(CGRectGetWidth([self frame]), [self currentValueForThemeAttribute:@"min-size"].height);
819 }
820 
826 - (unsigned)testSegment:(CGPoint)aPoint
827 {
828  var location = [self convertPoint:aPoint fromView:nil],
829  count = [self segmentCount];
830 
831  while (count--)
832  if (CGRectContainsPoint([self frameForSegment:count], aPoint))
833  return count;
834 
835  if ([self segmentCount])
836  {
837  var adjustedLastFrame = CGRectCreateCopy([self frameForSegment:(_segments.length - 1)]);
838  adjustedLastFrame.size.width = CGRectGetWidth([self bounds]) - adjustedLastFrame.origin.x;
839 
840  if (CGRectContainsPoint(adjustedLastFrame, aPoint))
841  return [self segmentCount] - 1;
842  }
843 
844  return -1;
845 }
846 
847 - (void)mouseDown:(CPEvent)anEvent
848 {
849  if (![self isEnabled])
850  return;
851 
852  [self trackSegment:anEvent];
853 }
854 
855 // FIXME: this should be fixed way up in cpbutton/cpcontrol.
856 - (void)mouseUp:(CPEvent)anEvent
857 {
858 }
859 
864 - (void)trackSegment:(CPEvent)anEvent
865 {
866  var type = [anEvent type],
867  location = [self convertPoint:[anEvent locationInWindow] fromView:nil];
868 
869  if (type == CPLeftMouseUp)
870  {
871  if (_trackingSegment == -1)
872  return;
873 
874  if (_trackingSegment === [self testSegment:location])
875  {
876  if (_trackingMode == CPSegmentSwitchTrackingSelectAny)
877  {
878  [self setSelected:![self isSelectedForSegment:_trackingSegment] forSegment:_trackingSegment];
879 
880  // With ANY, _selectedSegment means last pressed.
881  _selectedSegment = _trackingSegment;
882  }
883  else
884  [self setSelected:YES forSegment:_trackingSegment];
885 
886  [self sendAction:[self action] to:[self target]];
887 
888  if (_trackingMode == CPSegmentSwitchTrackingMomentary)
889  {
890  [self setSelected:NO forSegment:_trackingSegment];
891 
892  _selectedSegment = CPNotFound;
893  }
894  }
895 
896  [self drawSegmentBezel:_trackingSegment highlight:NO];
897 
898  _trackingSegment = -1;
899 
900  return;
901  }
902 
903  if (type == CPLeftMouseDown)
904  {
905  var trackingSegment = [self testSegment:location];
906  if (trackingSegment > -1 && [self isEnabledForSegment:trackingSegment])
907  {
908  _trackingHighlighted = YES;
909  _trackingSegment = trackingSegment;
910  [self drawSegmentBezel:_trackingSegment highlight:YES];
911  }
912  }
913 
914  else if (type == CPLeftMouseDragged)
915  {
916  if (_trackingSegment == -1)
917  return;
918 
919  var highlighted = [self testSegment:location] === _trackingSegment;
920 
921  if (highlighted != _trackingHighlighted)
922  {
923  _trackingHighlighted = highlighted;
924 
925  [self drawSegmentBezel:_trackingSegment highlight:_trackingHighlighted];
926  }
927  }
928 
929  [CPApp setTarget:self selector:@selector(trackSegment:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES];
930 }
931 
932 - (void)setFont:(CPFont)aFont
933 {
934  [super setFont:aFont];
935 
936  [self tile];
937 }
938 
939 @end
940 
941 var CPSegmentedControlSegmentsKey = "CPSegmentedControlSegmentsKey",
942  CPSegmentedControlSelectedKey = "CPSegmentedControlSelectedKey",
943  CPSegmentedControlSegmentStyleKey = "CPSegmentedControlSegmentStyleKey",
944  CPSegmentedControlTrackingModeKey = "CPSegmentedControlTrackingModeKey";
945 
947 
948 - (id)initWithCoder:(CPCoder)aCoder
949 {
950  self = [super initWithCoder:aCoder];
951 
952  if (self)
953  {
954  var frame = [self frame],
955  originalWidth = frame.size.width;
956 
957  frame.size.width = 0;
958 
959  [self setFrame:frame];
960 
961  _segments = [aCoder decodeObjectForKey:CPSegmentedControlSegmentsKey];
962  _segmentStyle = [aCoder decodeIntForKey:CPSegmentedControlSegmentStyleKey];
963  _themeStates = [];
964 
965  if ([aCoder containsValueForKey:CPSegmentedControlSelectedKey])
966  _selectedSegment = [aCoder decodeIntForKey:CPSegmentedControlSelectedKey];
967  else
968  _selectedSegment = CPNotFound;
969 
970  if ([aCoder containsValueForKey:CPSegmentedControlTrackingModeKey])
971  _trackingMode = [aCoder decodeIntForKey:CPSegmentedControlTrackingModeKey];
972  else
973  _trackingMode = CPSegmentSwitchTrackingSelectOne;
974 
975  // Here we update the themeStates array for each segments to know if there are selected or not
976  for (var i = 0; i < [self segmentCount]; i++)
977  _themeStates[i] = [_segments[i] selected] ? CPThemeStateSelected : CPThemeStateNormal;
978 
979  [self tile];
980 
981  var thickness = [self currentValueForThemeAttribute:@"divider-thickness"],
982  dividerExtraSpace = ([_segments count] - 1) * thickness,
983  difference = MAX(originalWidth - [self frame].size.width - dividerExtraSpace, 0.0),
984  remainingWidth = FLOOR(difference / [self segmentCount]),
985  widthOfAllSegments = 0;
986 
987  // We do this in a second loop because it relies on all the themeStates being set first
988  for (var i = 0; i < [self segmentCount]; i++)
989  {
990  var frame = [_segments[i] frame];
991  frame.size.width += remainingWidth;
992 
993  widthOfAllSegments += CGRectGetWidth(frame);
994  }
995 
996  // 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.
997  // This is needed to have a perfect/same alignment between our application and xCode.
998  var leftOversPixel = originalWidth - (widthOfAllSegments + dividerExtraSpace);
999 
1000  // Make sure we don't make an out of range
1001  if (leftOversPixel < [self segmentCount] - 1)
1002  {
1003  for (var i = 0; i < leftOversPixel; i++)
1004  {
1005  [_segments[i] frame].size.width += 1;
1006  }
1007  }
1008 
1009  [self setFrameSize:CGSizeMake(originalWidth, CGRectGetHeight([self frame]))];
1010  [self tile];
1011  }
1012 
1013  return self;
1014 }
1015 
1016 - (void)encodeWithCoder:(CPCoder)aCoder
1017 {
1018  [super encodeWithCoder:aCoder];
1019 
1020  [aCoder encodeObject:_segments forKey:CPSegmentedControlSegmentsKey];
1021  [aCoder encodeInt:_selectedSegment forKey:CPSegmentedControlSelectedKey];
1022  [aCoder encodeInt:_segmentStyle forKey:CPSegmentedControlSegmentStyleKey];
1023  [aCoder encodeInt:_trackingMode forKey:CPSegmentedControlTrackingModeKey];
1024 }
1025 
1026 @end
1027 
1029 
1030 + (Class)_binderClassForBinding:(CPString)aBinding
1031 {
1032  if ([self _isSelectionBinding:aBinding])
1033  return [_CPSegmentedControlBinder class];
1034 
1035  return [super _binderClassForBinding:aBinding];
1036 }
1037 
1038 + (BOOL)_isSelectionBinding:(CPString)aBinding
1039 {
1040  return (aBinding === CPSelectedIndexBinding || aBinding === CPSelectedLabelBinding || aBinding === CPSelectedTagBinding);
1041 }
1042 
1043 + (BOOL)isBindingExclusive:(CPString)aBinding
1044 {
1045  return [self _isSelectionBinding:aBinding];
1046 }
1047 
1048 - (void)bind:(CPString)aBinding toObject:(id)anObject withKeyPath:(CPString)aKeyPath options:(CPDictionary)options
1049 {
1050  if ([[self class] _isSelectionBinding:aBinding] && _trackingMode !== CPSegmentSwitchTrackingSelectOne)
1051  {
1052  CPLog.warn("Binding " + aBinding + " needs CPSegmentSwitchTrackingSelectOne tracking mode");
1053  return;
1054  }
1055 
1056  [super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options];
1057 }
1058 
1059 - (void)_reverseSetBinding
1060 {
1061  [_CPSegmentedControlBinder _reverseSetValueFromExclusiveBinderForObject:self];
1062 }
1063 
1064 @end
1065 
1066 var CPSegmentedControlNoSelectionPlaceholder = "CPSegmentedControlNoSelectionPlaceholder";
1067 @implementation _CPSegmentedControlBinder : CPBinder
1068 {
1069  id __doxygen__;
1070 }
1071 
1072 - (void)_updatePlaceholdersWithOptions:(CPDictionary)options
1073 {
1074  [super _updatePlaceholdersWithOptions:options];
1075 
1076  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPMultipleValuesMarker isDefault:YES];
1077  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNoSelectionMarker isDefault:YES];
1078  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNotApplicableMarker isDefault:YES];
1079  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNullMarker isDefault:YES];
1080 }
1081 
1082 - (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
1083 {
1085  [_source setSelected:NO forSegment:[_source selectedSegment]];
1086  else
1087  [self setValue:aValue forBinding:aBinding];
1088 }
1089 
1090 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
1091 {
1092  if (aBinding == CPSelectedIndexBinding)
1093  [_source setSelectedSegment:aValue];
1094  else if (aBinding == CPSelectedTagBinding)
1095  [_source selectSegmentWithTag:aValue];
1096  else if (aBinding == CPSelectedLabelBinding)
1097  [_source _selectSegmentWithLabel:aValue];
1098 }
1099 
1100 - (id)valueForBinding:(CPString)aBinding
1101 {
1102  var selectedIndex = [_source selectedSegment];
1103 
1104  if (aBinding == CPSelectedIndexBinding)
1105  return selectedIndex;
1106  else if (aBinding == CPSelectedTagBinding)
1107  return [_source tagForSegment:selectedIndex];
1108  else if (aBinding == CPSelectedLabelBinding)
1109  return [_source labelForSegment:selectedIndex];
1110 }
1111 
1112 @end
1113 
1114 @implementation _CPSegmentItem : CPObject
1115 {
1116  CPImage image;
1119  BOOL selected;
1120  BOOL enabled;
1121  int tag;
1122  int width;
1123 
1124  CGRect frame;
1125 }
1126 
1127 - (id)init
1128 {
1129  if (self = [super init])
1130  {
1131  image = nil;
1132  label = @"";
1133  menu = nil;
1134  selected = NO;
1135  enabled = YES;
1136  tag = -1;
1137  width = 0;
1138 
1139  frame = CGRectMakeZero();
1140  }
1141  return self;
1142 }
1143 
1144 @end
1145 
1146 var CPSegmentItemImageKey = "CPSegmentItemImageKey",
1147  CPSegmentItemLabelKey = "CPSegmentItemLabelKey",
1148  CPSegmentItemMenuKey = "CPSegmentItemMenuKey",
1149  CPSegmentItemSelectedKey = "CPSegmentItemSelectedKey",
1150  CPSegmentItemEnabledKey = "CPSegmentItemEnabledKey",
1151  CPSegmentItemTagKey = "CPSegmentItemTagKey",
1152  CPSegmentItemWidthKey = "CPSegmentItemWidthKey";
1153 
1154 @implementation _CPSegmentItem (CPCoding)
1155 
1156 - (id)initWithCoder:(CPCoder)aCoder
1157 {
1158  self = [super init];
1159 
1160  if (self)
1161  {
1162  image = [aCoder decodeObjectForKey:CPSegmentItemImageKey];
1163  label = [aCoder decodeObjectForKey:CPSegmentItemLabelKey];
1164  menu = [aCoder decodeObjectForKey:CPSegmentItemMenuKey];
1165  selected = [aCoder decodeBoolForKey:CPSegmentItemSelectedKey];
1166  enabled = [aCoder decodeBoolForKey:CPSegmentItemEnabledKey];
1167  tag = [aCoder decodeIntForKey:CPSegmentItemTagKey];
1168  width = [aCoder decodeFloatForKey:CPSegmentItemWidthKey];
1169 
1170  frame = CGRectMakeZero();
1171  }
1172 
1173  return self;
1174 }
1175 
1176 - (void)encodeWithCoder:(CPCoder)aCoder
1177 {
1178  [aCoder encodeObject:image forKey:CPSegmentItemImageKey];
1179  [aCoder encodeObject:label forKey:CPSegmentItemLabelKey];
1180  [aCoder encodeObject:menu forKey:CPSegmentItemMenuKey];
1181  [aCoder encodeBool:selected forKey:CPSegmentItemSelectedKey];
1182  [aCoder encodeBool:enabled forKey:CPSegmentItemEnabledKey];
1183  [aCoder encodeInt:tag forKey:CPSegmentItemTagKey];
1184  [aCoder encodeFloat:width forKey:CPSegmentItemWidthKey];
1185 }
1186 
1187 @end
1188 
1190 
1194 - (CPArray)segments
1195 {
1196  return _segments;
1197 }
1198 
1199 @end
var CPSegmentedControlSegmentStyleKey
Definition: CPFont.h:2
CPSegmentSwitchTrackingSelectAny
void setFont:(CPFont aFont)
Definition: CPControl.j:891
CPThemeStateSelected
Definition: CPTheme.j:548
Definition: CPMenu.h:2
BOOL isEnabled()
Definition: CPControl.j:970
CPThemeStateControlSizeMini
Definition: CPTheme.j:565
void setSelected:forSegment:(BOOL isSelected, [forSegment] unsigned aSegment)
An object representation of nil.
Definition: CPNull.h:2
var CPSegmentedControlSegmentsKey
BOOL selected
void setFrame:(CGRect aFrame)
Definition: CPView.j:996
CGPoint locationInWindow()
Definition: CPEvent.j:290
CPFont font()
Definition: CPControl.j:899
CPString label
int width
id initWithFrame:(CGRect aFrame)
Definition: CPControl.j:183
CGRect bounds()
Definition: CPView.j:1302
void setControlSize:(CPControlSize aControlSize)
Definition: CPControl.j:211
CPInteger tag()
Definition: CPView.j:956
A collection of unique integers.
Definition: CPIndexSet.h:2
CPEventType type()
Definition: CPEvent.j:325
global CPApp typedef CPSegmentSwitchTracking CPSegmentSwitchTrackingSelectOne
float widthForSegment:(unsigned aSegment)
A mutable key-value pair collection.
Definition: CPDictionary.h:2
void drawSegmentBezel:highlight:(int aSegment, [highlight] BOOL shouldHighlight)
BOOL enabled
CPImageScaleNone
Definition: CPControl.j:65
void trackSegment:(CPEvent anEvent)
void tileWithChangedSegment:(CPInteger aSegment)
CGRect bezelFrameForSegment:(unsigned aSegment)
CPImageLeft
Definition: CPControl.j:71
BOOL isSelectedForSegment:(unsigned aSegment)
var CPSegmentItemSelectedKey
CPThemeStateDisabled
Definition: CPTheme.j:545
An immutable string (collection of characters).
Definition: CPString.h:2
CPNull null()
Definition: CPNull.j:51
CGPoint convertPoint:fromView:(CGPoint aPoint, [fromView] CPView aView)
Definition: CPView.j:2208
Definition: CPImage.h:2
BOOL sendAction:to:(SEL anAction, [to] id anObject)
Definition: CPControl.j:319
var CPSegmentItemMenuKey
id init()
Definition: CPView.j:325
SEL action()
Definition: CPControl.j:290
id initWithCoder:(CPCoder aCoder)
Definition: CPControl.j:1085
CPSelectedTagBinding
void setNeedsDisplay:(BOOL aFlag)
Definition: CPView.j:2556
var CPSegmentedControlTrackingModeKey
CPLeftMouseUp
var CPSegmentedControlSelectedKey
CPSelectedIndexBinding
CPDictionary themeAttributes()
void setNeedsLayout()
Definition: CPView.j:2707
void removeSegmentsAtIndexes:(CPIndexSet indices)
id target()
Definition: CPControl.j:308
CPLeftMouseDragged
Defines methods for use when archiving & restoring (enc/decoding).
Definition: CPCoder.h:2
CPThemeStateHighlighted
Definition: CPTheme.j:547
CPNotFound
Definition: CPObjJRuntime.j:62
CGRect contentFrameForSegment:(unsigned aSegment)
var CPSegmentedControlNoSelectionPlaceholder
CPLeftMouseDown
void setSelectedSegment:(unsigned aSegment)
void insertSegments:atIndexes:(CPArray segments, [atIndexes] CPIndexSet indices)
void setFrameSize:(CGSize aSize)
Definition: CPView.j:1100
var CPSegmentItemWidthKey
var CPSegmentItemImageKey
id indexSetWithIndexesInRange:(CPRange aRange)
Definition: CPIndexSet.j:60
Definition: CPEvent.h:2
unsigned testSegment:(CGPoint aPoint)
var CPSegmentItemTagKey
CPThemeStateControlSizeSmall
Definition: CPTheme.j:564
CPSelectedLabelBinding
CPCenterTextAlignment
Definition: CPText.j:75
void enumerateIndexesUsingBlock:(Function/*(int idx, @ref BOOL stop) */aFunction)
Definition: CPIndexSet.j:487
CGRect frame()
Definition: CPView.j:1022
CPSegmentSwitchTrackingMomentary
CPSegmentSwitchTracking trackingMode()
void encodeWithCoder:(CPCoder aCoder)
Definition: CPControl.j:1114
var CPSegmentItemLabelKey
var CPSegmentItemEnabledKey
CPView layoutEphemeralSubviewNamed:positioned:relativeToEphemeralSubviewNamed:(CPString aViewName, [positioned] CPWindowOrderingMode anOrderingMode, [relativeToEphemeralSubviewNamed] CPString relativeToViewName)
Definition: CPView.j:3366
CPMenu menu
CPCenterVerticalTextAlignment
Definition: CPControl.j:54
CGRect rectForEphemeralSubviewNamed:(CPString aViewName)
Definition: CPView.j:3361
Definition: CPView.j:136