API  0.9.9
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  return [[_CPImageAndTextView alloc] initWithFrame:CGRectMakeZero()];
602 
603  return [[CPView alloc] initWithFrame:CGRectMakeZero()];
604 }
605 
607 {
608  if ([self segmentCount] <= 0)
609  return;
610 
611  var themeState = _themeStates[0],
612  isDisabled = [self hasThemeState:CPThemeStateDisabled],
613  isControlSizeSmall = [self hasThemeState:CPThemeStateControlSizeSmall],
614  isControlSizeMini = [self hasThemeState:CPThemeStateControlSizeMini];
615 
616  themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
617 
618  if (isControlSizeSmall)
619  themeState = themeState.and(CPThemeStateControlSizeSmall);
620  else if (isControlSizeMini)
621  themeState = themeState.and(CPThemeStateControlSizeMini);
622 
623  var leftCapColor = [self valueForThemeAttribute:@"left-segment-bezel-color"
624  inState:themeState],
625 
626  leftBezelView = [self layoutEphemeralSubviewNamed:@"left-segment-bezel"
627  positioned:CPWindowBelow
629 
630  [leftBezelView setBackgroundColor:leftCapColor];
631 
632  var themeState = _themeStates[_themeStates.length - 1];
633 
634  themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
635 
636  if (isControlSizeSmall)
637  themeState = themeState.and(CPThemeStateControlSizeSmall);
638  else if (isControlSizeMini)
639  themeState = themeState.and(CPThemeStateControlSizeMini);
640 
641  var rightCapColor = [self valueForThemeAttribute:@"right-segment-bezel-color"
642  inState:themeState],
643 
644  rightBezelView = [self layoutEphemeralSubviewNamed:@"right-segment-bezel"
645  positioned:CPWindowBelow
647 
648  [rightBezelView setBackgroundColor:rightCapColor];
649 
650  for (var i = 0, count = _themeStates.length; i < count; i++)
651  {
652  var themeState = _themeStates[i];
653 
654  themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
655 
656  if (isControlSizeSmall)
657  themeState = themeState.and(CPThemeStateControlSizeSmall);
658  else if (isControlSizeMini)
659  themeState = themeState.and(CPThemeStateControlSizeMini);
660 
661  var bezelColor = [self valueForThemeAttribute:@"center-segment-bezel-color"
662  inState:themeState],
663 
664  bezelView = [self layoutEphemeralSubviewNamed:"segment-bezel-" + i
665  positioned:CPWindowBelow
667 
668  [bezelView setBackgroundColor:bezelColor];
669 
670  // layout image/title views
671  var segment = _segments[i],
672  contentView = [self layoutEphemeralSubviewNamed:@"segment-content-" + i
673  positioned:CPWindowAbove
674  relativeToEphemeralSubviewNamed:@"segment-bezel-" + i];
675 
676  [contentView setText:[segment label]];
677  [contentView setImage:[segment image]];
678 
679  [contentView setFont:[self valueForThemeAttribute:@"font" inState:themeState]];
680  [contentView setTextColor:[self valueForThemeAttribute:@"text-color" inState:themeState]];
681  [contentView setAlignment:[self valueForThemeAttribute:@"alignment" inState:themeState]];
682  [contentView setVerticalAlignment:[self valueForThemeAttribute:@"vertical-alignment" inState:themeState]];
683  [contentView setLineBreakMode:[self valueForThemeAttribute:@"line-break-mode" inState:themeState]];
684  [contentView setTextShadowColor:[self valueForThemeAttribute:@"text-shadow-color" inState:themeState]];
685  [contentView setTextShadowOffset:[self valueForThemeAttribute:@"text-shadow-offset" inState:themeState]];
686  [contentView setImageScaling:[self valueForThemeAttribute:@"image-scaling" inState:themeState]];
687 
688  if ([segment image] && [segment label])
689  [contentView setImagePosition:[self valueForThemeAttribute:@"image-position" inState:themeState]];
690  else if ([segment image])
691  [contentView setImagePosition:CPImageOnly];
692 
693  if (i == count - 1)
694  continue;
695 
696 
697  var borderState = _themeStates[i].and(_themeStates[i + 1]);
698 
699  borderState = isDisabled ? borderState.and(CPThemeStateDisabled) : borderState;
700 
701  if (isControlSizeSmall)
702  borderState = borderState.and(CPThemeStateControlSizeSmall);
703  else if (isControlSizeMini)
704  borderState = borderState.and(CPThemeStateControlSizeMini);
705 
706  var borderColor = [self valueForThemeAttribute:@"divider-bezel-color"
707  inState:borderState],
708 
709  borderView = [self layoutEphemeralSubviewNamed:"divider-bezel-" + i
710  positioned:CPWindowBelow
712 
713  [borderView setBackgroundColor:borderColor];
714  }
715 }
716 
717 
723 - (void)drawSegment:(int)aSegment highlight:(BOOL)shouldHighlight
724 {
725 }
726 
728 - (void)tile
729 {
730  [self tileWithChangedSegment:0];
731 }
732 
734 - (void)tileWithChangedSegment:(CPInteger)aSegment
735 {
736  var segmentCount = [self segmentCount];
737 
738  // Corner case: when segmentCount == 0 and aSegment == 0, we do not return here because we still need to set the new frameSize bellow.
739  if (aSegment < 0 || (segmentCount > 0 && aSegment >= segmentCount))
740  return;
741 
742  var width = 0;
743 
744  if (segmentCount > 0)
745  {
746  // Invalidate frames for segments on the right. They will be lazily computed by -frameForSegment:.
747  for (var i = aSegment; i < segmentCount; i++)
748  [_segments[i] setFrame:CGRectMakeZero()];
749 
750  width = CGRectGetMaxX([self frameForSegment:(segmentCount - 1)]);
751  }
752 
753  [self setFrameSize:CGSizeMake(width, CGRectGetHeight([self frame]))];
754 
755  [self setNeedsLayout];
756  [self setNeedsDisplay:YES];
757 }
758 
763 - (CGRect)frameForSegment:(unsigned)aSegment
764 {
765  var segment = [_segments objectAtIndex:aSegment],
766  frame = [segment frame];
767 
768  if (CGRectEqualToRect(frame, CGRectMakeZero()))
769  {
770  frame = [self bezelFrameForSegment:aSegment];
771  [segment setFrame:frame];
772  }
773 
774  return frame;
775 }
776 
777 - (CGRect)bezelFrameForSegment:(unsigned)aSegment
778 {
779  var left = [self _leftOffsetForSegment:aSegment],
780  top = [self currentValueForThemeAttribute:@"bezel-inset"].top,
781  width = [self widthForSegment:aSegment],
782  height = [self currentValueForThemeAttribute:@"min-size"].height;
783 
784  if (width == 0)
785  {
786  var themeState = _themeState.hasThemeState(CPThemeStateDisabled) ? _themeStates[aSegment].and(CPThemeStateDisabled) : _themeStates[aSegment],
787  contentInset = [self valueForThemeAttribute:@"content-inset" inState:themeState],
788  contentInsetWidth = contentInset.left + contentInset.right,
789 
790  segment = _segments[aSegment],
791  label = [segment label],
792  image = [segment image];
793 
794  width = (label ? [label sizeWithFont:[self font]].width : 4.0) + (image ? [image size].width : 0) + contentInsetWidth;
795  }
796 
797  return CGRectMake(left, top, width, height);
798 }
799 
800 - (CGRect)contentFrameForSegment:(unsigned)aSegment
801 {
802  var height = [self currentValueForThemeAttribute:@"min-size"].height,
803  contentInset = [self currentValueForThemeAttribute:@"content-inset"],
804  width = CGRectGetWidth([self frameForSegment:aSegment]),
805  left = [self _leftOffsetForSegment:aSegment];
806 
807  return CGRectMake(left + contentInset.left, contentInset.top, width - contentInset.left - contentInset.right, height - contentInset.top - contentInset.bottom);
808 }
809 
810 - (CGSize)_minimumFrameSize
811 {
812  // The current width is always the minimum width.
813  return CGSizeMake(CGRectGetWidth([self frame]), [self currentValueForThemeAttribute:@"min-size"].height);
814 }
815 
821 - (unsigned)testSegment:(CGPoint)aPoint
822 {
823  var location = [self convertPoint:aPoint fromView:nil],
824  count = [self segmentCount];
825 
826  while (count--)
827  if (CGRectContainsPoint([self frameForSegment:count], aPoint))
828  return count;
829 
830  if ([self segmentCount])
831  {
832  var adjustedLastFrame = CGRectCreateCopy([self frameForSegment:(_segments.length - 1)]);
833  adjustedLastFrame.size.width = CGRectGetWidth([self bounds]) - adjustedLastFrame.origin.x;
834 
835  if (CGRectContainsPoint(adjustedLastFrame, aPoint))
836  return [self segmentCount] - 1;
837  }
838 
839  return -1;
840 }
841 
842 - (void)mouseDown:(CPEvent)anEvent
843 {
844  if (![self isEnabled])
845  return;
846 
847  [self trackSegment:anEvent];
848 }
849 
850 // FIXME: this should be fixed way up in cpbutton/cpcontrol.
851 - (void)mouseUp:(CPEvent)anEvent
852 {
853 }
854 
859 - (void)trackSegment:(CPEvent)anEvent
860 {
861  var type = [anEvent type],
862  location = [self convertPoint:[anEvent locationInWindow] fromView:nil];
863 
864  if (type == CPLeftMouseUp)
865  {
866  if (_trackingSegment == -1)
867  return;
868 
869  if (_trackingSegment === [self testSegment:location])
870  {
871  if (_trackingMode == CPSegmentSwitchTrackingSelectAny)
872  {
873  [self setSelected:![self isSelectedForSegment:_trackingSegment] forSegment:_trackingSegment];
874 
875  // With ANY, _selectedSegment means last pressed.
876  _selectedSegment = _trackingSegment;
877  }
878  else
879  [self setSelected:YES forSegment:_trackingSegment];
880 
881  [self sendAction:[self action] to:[self target]];
882 
883  if (_trackingMode == CPSegmentSwitchTrackingMomentary)
884  {
885  [self setSelected:NO forSegment:_trackingSegment];
886 
887  _selectedSegment = CPNotFound;
888  }
889  }
890 
891  [self drawSegmentBezel:_trackingSegment highlight:NO];
892 
893  _trackingSegment = -1;
894 
895  return;
896  }
897 
898  if (type == CPLeftMouseDown)
899  {
900  var trackingSegment = [self testSegment:location];
901  if (trackingSegment > -1 && [self isEnabledForSegment:trackingSegment])
902  {
903  _trackingHighlighted = YES;
904  _trackingSegment = trackingSegment;
905  [self drawSegmentBezel:_trackingSegment highlight:YES];
906  }
907  }
908 
909  else if (type == CPLeftMouseDragged)
910  {
911  if (_trackingSegment == -1)
912  return;
913 
914  var highlighted = [self testSegment:location] === _trackingSegment;
915 
916  if (highlighted != _trackingHighlighted)
917  {
918  _trackingHighlighted = highlighted;
919 
920  [self drawSegmentBezel:_trackingSegment highlight:_trackingHighlighted];
921  }
922  }
923 
924  [CPApp setTarget:self selector:@selector(trackSegment:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES];
925 }
926 
927 - (void)setFont:(CPFont)aFont
928 {
929  [super setFont:aFont];
930 
931  [self tile];
932 }
933 
934 @end
935 
936 var CPSegmentedControlSegmentsKey = "CPSegmentedControlSegmentsKey",
937  CPSegmentedControlSelectedKey = "CPSegmentedControlSelectedKey",
938  CPSegmentedControlSegmentStyleKey = "CPSegmentedControlSegmentStyleKey",
939  CPSegmentedControlTrackingModeKey = "CPSegmentedControlTrackingModeKey";
940 
942 
943 - (id)initWithCoder:(CPCoder)aCoder
944 {
945  self = [super initWithCoder:aCoder];
946 
947  if (self)
948  {
949  var frame = [self frame],
950  originalWidth = frame.size.width;
951 
952  frame.size.width = 0;
953 
954  [self setFrame:frame];
955 
956  _segments = [aCoder decodeObjectForKey:CPSegmentedControlSegmentsKey];
957  _segmentStyle = [aCoder decodeIntForKey:CPSegmentedControlSegmentStyleKey];
958  _themeStates = [];
959 
960  if ([aCoder containsValueForKey:CPSegmentedControlSelectedKey])
961  _selectedSegment = [aCoder decodeIntForKey:CPSegmentedControlSelectedKey];
962  else
963  _selectedSegment = CPNotFound;
964 
965  if ([aCoder containsValueForKey:CPSegmentedControlTrackingModeKey])
966  _trackingMode = [aCoder decodeIntForKey:CPSegmentedControlTrackingModeKey];
967  else
968  _trackingMode = CPSegmentSwitchTrackingSelectOne;
969 
970  // Here we update the themeStates array for each segments to know if there are selected or not
971  for (var i = 0; i < [self segmentCount]; i++)
972  _themeStates[i] = [_segments[i] selected] ? CPThemeStateSelected : CPThemeStateNormal;
973 
974  [self tile];
975 
976  var thickness = [self currentValueForThemeAttribute:@"divider-thickness"],
977  dividerExtraSpace = ([_segments count] - 1) * thickness,
978  difference = MAX(originalWidth - [self frame].size.width - dividerExtraSpace, 0.0),
979  remainingWidth = FLOOR(difference / [self segmentCount]),
980  widthOfAllSegments = 0;
981 
982  // We do this in a second loop because it relies on all the themeStates being set first
983  for (var i = 0; i < [self segmentCount]; i++)
984  {
985  var frame = [_segments[i] frame];
986  frame.size.width += remainingWidth;
987 
988  widthOfAllSegments += CGRectGetWidth(frame);
989  }
990 
991  // 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.
992  // This is needed to have a perfect/same alignment between our application and xCode.
993  var leftOversPixel = originalWidth - (widthOfAllSegments + dividerExtraSpace);
994 
995  // Make sure we don't make an out of range
996  if (leftOversPixel < [self segmentCount] - 1)
997  {
998  for (var i = 0; i < leftOversPixel; i++)
999  {
1000  [_segments[i] frame].size.width += 1;
1001  }
1002  }
1003 
1004  [self setFrameSize:CGSizeMake(originalWidth, CGRectGetHeight([self frame]))];
1005  [self tile];
1006  }
1007 
1008  return self;
1009 }
1010 
1011 - (void)encodeWithCoder:(CPCoder)aCoder
1012 {
1013  [super encodeWithCoder:aCoder];
1014 
1015  [aCoder encodeObject:_segments forKey:CPSegmentedControlSegmentsKey];
1016  [aCoder encodeInt:_selectedSegment forKey:CPSegmentedControlSelectedKey];
1017  [aCoder encodeInt:_segmentStyle forKey:CPSegmentedControlSegmentStyleKey];
1018  [aCoder encodeInt:_trackingMode forKey:CPSegmentedControlTrackingModeKey];
1019 }
1020 
1021 @end
1022 
1024 
1025 + (Class)_binderClassForBinding:(CPString)aBinding
1026 {
1027  if ([self _isSelectionBinding:aBinding])
1028  return [_CPSegmentedControlBinder class];
1029 
1030  return [super _binderClassForBinding:aBinding];
1031 }
1032 
1033 + (BOOL)_isSelectionBinding:(CPString)aBinding
1034 {
1035  return (aBinding === CPSelectedIndexBinding || aBinding === CPSelectedLabelBinding || aBinding === CPSelectedTagBinding);
1036 }
1037 
1038 + (BOOL)isBindingExclusive:(CPString)aBinding
1039 {
1040  return [self _isSelectionBinding:aBinding];
1041 }
1042 
1043 - (void)bind:(CPString)aBinding toObject:(id)anObject withKeyPath:(CPString)aKeyPath options:(CPDictionary)options
1044 {
1045  if ([[self class] _isSelectionBinding:aBinding] && _trackingMode !== CPSegmentSwitchTrackingSelectOne)
1046  {
1047  CPLog.warn("Binding " + aBinding + " needs CPSegmentSwitchTrackingSelectOne tracking mode");
1048  return;
1049  }
1050 
1051  [super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options];
1052 }
1053 
1054 - (void)_reverseSetBinding
1055 {
1056  [_CPSegmentedControlBinder _reverseSetValueFromExclusiveBinderForObject:self];
1057 }
1058 
1059 @end
1060 
1061 var CPSegmentedControlNoSelectionPlaceholder = "CPSegmentedControlNoSelectionPlaceholder";
1062 @implementation _CPSegmentedControlBinder : CPBinder
1063 {
1064  id __doxygen__;
1065 }
1066 
1067 - (void)_updatePlaceholdersWithOptions:(CPDictionary)options
1068 {
1069  [super _updatePlaceholdersWithOptions:options];
1070 
1071  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPMultipleValuesMarker isDefault:YES];
1072  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNoSelectionMarker isDefault:YES];
1073  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNotApplicableMarker isDefault:YES];
1074  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNullMarker isDefault:YES];
1075 }
1076 
1077 - (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
1078 {
1080  [_source setSelected:NO forSegment:[_source selectedSegment]];
1081  else
1082  [self setValue:aValue forBinding:aBinding];
1083 }
1084 
1085 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
1086 {
1087  if (aBinding == CPSelectedIndexBinding)
1088  [_source setSelectedSegment:aValue];
1089  else if (aBinding == CPSelectedTagBinding)
1090  [_source selectSegmentWithTag:aValue];
1091  else if (aBinding == CPSelectedLabelBinding)
1092  [_source _selectSegmentWithLabel:aValue];
1093 }
1094 
1095 - (id)valueForBinding:(CPString)aBinding
1096 {
1097  var selectedIndex = [_source selectedSegment];
1098 
1099  if (aBinding == CPSelectedIndexBinding)
1100  return selectedIndex;
1101  else if (aBinding == CPSelectedTagBinding)
1102  return [_source tagForSegment:selectedIndex];
1103  else if (aBinding == CPSelectedLabelBinding)
1104  return [_source labelForSegment:selectedIndex];
1105 }
1106 
1107 @end
1108 
1109 @implementation _CPSegmentItem : CPObject
1110 {
1111  CPImage image;
1114  BOOL selected;
1115  BOOL enabled;
1116  int tag;
1117  int width;
1118 
1119  CGRect frame;
1120 }
1121 
1122 - (id)init
1123 {
1124  if (self = [super init])
1125  {
1126  image = nil;
1127  label = @"";
1128  menu = nil;
1129  selected = NO;
1130  enabled = YES;
1131  tag = -1;
1132  width = 0;
1133 
1134  frame = CGRectMakeZero();
1135  }
1136  return self;
1137 }
1138 
1139 @end
1140 
1141 var CPSegmentItemImageKey = "CPSegmentItemImageKey",
1142  CPSegmentItemLabelKey = "CPSegmentItemLabelKey",
1143  CPSegmentItemMenuKey = "CPSegmentItemMenuKey",
1144  CPSegmentItemSelectedKey = "CPSegmentItemSelectedKey",
1145  CPSegmentItemEnabledKey = "CPSegmentItemEnabledKey",
1146  CPSegmentItemTagKey = "CPSegmentItemTagKey",
1147  CPSegmentItemWidthKey = "CPSegmentItemWidthKey";
1148 
1149 @implementation _CPSegmentItem (CPCoding)
1150 
1151 - (id)initWithCoder:(CPCoder)aCoder
1152 {
1153  self = [super init];
1154 
1155  if (self)
1156  {
1157  image = [aCoder decodeObjectForKey:CPSegmentItemImageKey];
1158  label = [aCoder decodeObjectForKey:CPSegmentItemLabelKey];
1159  menu = [aCoder decodeObjectForKey:CPSegmentItemMenuKey];
1160  selected = [aCoder decodeBoolForKey:CPSegmentItemSelectedKey];
1161  enabled = [aCoder decodeBoolForKey:CPSegmentItemEnabledKey];
1162  tag = [aCoder decodeIntForKey:CPSegmentItemTagKey];
1163  width = [aCoder decodeFloatForKey:CPSegmentItemWidthKey];
1164 
1165  frame = CGRectMakeZero();
1166  }
1167 
1168  return self;
1169 }
1170 
1171 - (void)encodeWithCoder:(CPCoder)aCoder
1172 {
1173  [aCoder encodeObject:image forKey:CPSegmentItemImageKey];
1174  [aCoder encodeObject:label forKey:CPSegmentItemLabelKey];
1175  [aCoder encodeObject:menu forKey:CPSegmentItemMenuKey];
1176  [aCoder encodeBool:selected forKey:CPSegmentItemSelectedKey];
1177  [aCoder encodeBool:enabled forKey:CPSegmentItemEnabledKey];
1178  [aCoder encodeInt:tag forKey:CPSegmentItemTagKey];
1179  [aCoder encodeFloat:width forKey:CPSegmentItemWidthKey];
1180 }
1181 
1182 @end
1183 
1185 
1189 - (CPArray)segments
1190 {
1191  return _segments;
1192 }
1193 
1194 @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:985
CGPoint locationInWindow()
Definition: CPEvent.j:287
CPFont font()
Definition: CPControl.j:899
CPString label
int width
id initWithFrame:(CGRect aFrame)
Definition: CPControl.j:183
CGRect bounds()
Definition: CPView.j:1287
void setControlSize:(CPControlSize aControlSize)
Definition: CPControl.j:211
CPInteger tag()
Definition: CPView.j:945
A collection of unique integers.
Definition: CPIndexSet.h:2
CPEventType type()
Definition: CPEvent.j:322
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:2142
Definition: CPImage.h:2
BOOL sendAction:to:(SEL anAction, [to] id anObject)
Definition: CPControl.j:319
var CPSegmentItemMenuKey
id init()
Definition: CPView.j:314
SEL action()
Definition: CPControl.j:290
id initWithCoder:(CPCoder aCoder)
Definition: CPControl.j:1085
CPSelectedTagBinding
void setNeedsDisplay:(BOOL aFlag)
Definition: CPView.j:2490
void enumerateIndexesUsingBlock:(Function/*(int idx,@ref BOOL stop)*/aFunction)
Definition: CPIndexSet.j:487
var CPSegmentedControlTrackingModeKey
CPLeftMouseUp
var CPSegmentedControlSelectedKey
CPSelectedIndexBinding
CPDictionary themeAttributes()
void setNeedsLayout()
Definition: CPView.j:2641
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:1086
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:51
CGRect frame()
Definition: CPView.j:1011
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:3300
CPMenu menu
CPCenterVerticalTextAlignment
Definition: CPControl.j:54
CGRect rectForEphemeralSubviewNamed:(CPString aViewName)
Definition: CPView.j:3295
Definition: CPView.j:131