API  1.0.0
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 {
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 
611 - (void)layoutSubviews
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  switch (type)
870  {
871  case CPLeftMouseUp:
872 
873  if (_trackingSegment === CPNotFound)
874  return;
875 
876  if (_trackingSegment === [self testSegment:location])
877  {
878  if (_trackingMode == CPSegmentSwitchTrackingSelectAny)
879  {
880  [self setSelected:![self isSelectedForSegment:_trackingSegment] forSegment:_trackingSegment];
881 
882  // With ANY, _selectedSegment means last pressed.
883  _selectedSegment = _trackingSegment;
884  }
885  else
886  [self setSelected:YES forSegment:_trackingSegment];
887 
888  [self sendAction:[self action] to:[self target]];
889 
890  if (_trackingMode == CPSegmentSwitchTrackingMomentary)
891  {
892  [self setSelected:NO forSegment:_trackingSegment];
893 
894  _selectedSegment = CPNotFound;
895  }
896  }
897 
898  [self drawSegmentBezel:_trackingSegment highlight:NO];
899 
900  _trackingSegment = CPNotFound;
901 
902  return;
903 
904  case CPLeftMouseDown:
905 
906  var trackingSegment = [self testSegment:location];
907  if (trackingSegment > CPNotFound && [self isEnabledForSegment:trackingSegment])
908  {
909  _trackingHighlighted = YES;
910  _trackingSegment = trackingSegment;
911  [self drawSegmentBezel:_trackingSegment highlight:YES];
912  }
913 
914  break;
915 
916  case CPLeftMouseDragged:
917 
918  if (_trackingSegment === CPNotFound)
919  return;
920 
921  var highlighted = [self testSegment:location] === _trackingSegment;
922 
923  if (highlighted != _trackingHighlighted)
924  {
925  _trackingHighlighted = highlighted;
926 
927  [self drawSegmentBezel:_trackingSegment highlight:_trackingHighlighted];
928  }
929 
930  break;
931  }
932 
933  [CPApp setTarget:self selector:@selector(trackSegment:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES];
934 }
935 
936 - (void)setFont:(CPFont)aFont
937 {
938  [super setFont:aFont];
939 
940  [self tile];
941 }
942 
943 @end
944 
945 var CPSegmentedControlSegmentsKey = "CPSegmentedControlSegmentsKey",
946  CPSegmentedControlSelectedKey = "CPSegmentedControlSelectedKey",
947  CPSegmentedControlSegmentStyleKey = "CPSegmentedControlSegmentStyleKey",
948  CPSegmentedControlTrackingModeKey = "CPSegmentedControlTrackingModeKey";
949 
951 
952 - (id)initWithCoder:(CPCoder)aCoder
953 {
954  self = [super initWithCoder:aCoder];
955 
956  if (self)
957  {
958  var frame = [self frame],
959  originalWidth = frame.size.width;
960 
961  frame.size.width = 0;
962 
963  [self setFrame:frame];
964 
965  _segments = [aCoder decodeObjectForKey:CPSegmentedControlSegmentsKey];
966  _segmentStyle = [aCoder decodeIntForKey:CPSegmentedControlSegmentStyleKey];
967  _themeStates = [];
968 
969  if ([aCoder containsValueForKey:CPSegmentedControlSelectedKey])
970  _selectedSegment = [aCoder decodeIntForKey:CPSegmentedControlSelectedKey];
971  else
972  _selectedSegment = CPNotFound;
973 
974  if ([aCoder containsValueForKey:CPSegmentedControlTrackingModeKey])
975  _trackingMode = [aCoder decodeIntForKey:CPSegmentedControlTrackingModeKey];
976  else
977  _trackingMode = CPSegmentSwitchTrackingSelectOne;
978 
979  // Here we update the themeStates array for each segments to know if there are selected or not
980  for (var i = 0; i < [self segmentCount]; i++)
981  _themeStates[i] = [_segments[i] selected] ? CPThemeStateSelected : CPThemeStateNormal;
982 
983  [self tile];
984 
985  var thickness = [self currentValueForThemeAttribute:@"divider-thickness"],
986  dividerExtraSpace = ([_segments count] - 1) * thickness,
987  difference = MAX(originalWidth - [self frame].size.width - dividerExtraSpace, 0.0),
988  remainingWidth = FLOOR(difference / [self segmentCount]),
989  widthOfAllSegments = 0;
990 
991  // We do this in a second loop because it relies on all the themeStates being set first
992  for (var i = 0; i < [self segmentCount]; i++)
993  {
994  var frame = [_segments[i] frame];
995  frame.size.width += remainingWidth;
996 
997  widthOfAllSegments += CGRectGetWidth(frame);
998  }
999 
1000  // 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.
1001  // This is needed to have a perfect/same alignment between our application and xCode.
1002  var leftOversPixel = originalWidth - (widthOfAllSegments + dividerExtraSpace);
1003 
1004  // Make sure we don't make an out of range
1005  if (leftOversPixel < [self segmentCount] - 1)
1006  {
1007  for (var i = 0; i < leftOversPixel; i++)
1008  {
1009  [_segments[i] frame].size.width += 1;
1010  }
1011  }
1012 
1013  [self setFrameSize:CGSizeMake(originalWidth, CGRectGetHeight([self frame]))];
1014  [self tile];
1015  }
1016 
1017  return self;
1018 }
1019 
1020 - (void)encodeWithCoder:(CPCoder)aCoder
1021 {
1022  [super encodeWithCoder:aCoder];
1023 
1024  [aCoder encodeObject:_segments forKey:CPSegmentedControlSegmentsKey];
1025  [aCoder encodeInt:_selectedSegment forKey:CPSegmentedControlSelectedKey];
1026  [aCoder encodeInt:_segmentStyle forKey:CPSegmentedControlSegmentStyleKey];
1027  [aCoder encodeInt:_trackingMode forKey:CPSegmentedControlTrackingModeKey];
1028 }
1029 
1030 @end
1031 
1033 
1034 + (Class)_binderClassForBinding:(CPString)aBinding
1035 {
1036  if ([self _isSelectionBinding:aBinding])
1037  return [_CPSegmentedControlBinder class];
1038 
1039  return [super _binderClassForBinding:aBinding];
1040 }
1041 
1042 + (BOOL)_isSelectionBinding:(CPString)aBinding
1043 {
1044  return (aBinding === CPSelectedIndexBinding || aBinding === CPSelectedLabelBinding || aBinding === CPSelectedTagBinding);
1045 }
1046 
1047 + (BOOL)isBindingExclusive:(CPString)aBinding
1048 {
1049  return [self _isSelectionBinding:aBinding];
1050 }
1051 
1052 - (void)bind:(CPString)aBinding toObject:(id)anObject withKeyPath:(CPString)aKeyPath options:(CPDictionary)options
1053 {
1054  if ([[self class] _isSelectionBinding:aBinding] && _trackingMode !== CPSegmentSwitchTrackingSelectOne)
1055  {
1056  CPLog.warn("Binding " + aBinding + " needs CPSegmentSwitchTrackingSelectOne tracking mode");
1057  return;
1058  }
1059 
1060  [super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options];
1061 }
1062 
1063 - (void)_reverseSetBinding
1064 {
1065  [_CPSegmentedControlBinder _reverseSetValueFromExclusiveBinderForObject:self];
1066 }
1067 
1068 @end
1069 
1070 var CPSegmentedControlNoSelectionPlaceholder = "CPSegmentedControlNoSelectionPlaceholder";
1071 @implementation _CPSegmentedControlBinder : CPBinder
1072 {
1073  id __doxygen__;
1074 }
1075 
1076 - (void)_updatePlaceholdersWithOptions:(CPDictionary)options
1077 {
1078  [super _updatePlaceholdersWithOptions:options];
1079 
1080  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPMultipleValuesMarker isDefault:YES];
1081  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNoSelectionMarker isDefault:YES];
1082  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNotApplicableMarker isDefault:YES];
1083  [self _setPlaceholder:CPSegmentedControlNoSelectionPlaceholder forMarker:CPNullMarker isDefault:YES];
1084 }
1085 
1086 - (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
1087 {
1089  [_source setSelected:NO forSegment:[_source selectedSegment]];
1090  else
1091  [self setValue:aValue forBinding:aBinding];
1092 }
1093 
1094 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
1095 {
1096  if (aBinding == CPSelectedIndexBinding)
1097  [_source setSelectedSegment:aValue];
1098  else if (aBinding == CPSelectedTagBinding)
1099  [_source selectSegmentWithTag:aValue];
1100  else if (aBinding == CPSelectedLabelBinding)
1101  [_source _selectSegmentWithLabel:aValue];
1102 }
1103 
1104 - (id)valueForBinding:(CPString)aBinding
1105 {
1106  var selectedIndex = [_source selectedSegment];
1107 
1108  if (aBinding == CPSelectedIndexBinding)
1109  return selectedIndex;
1110  else if (aBinding == CPSelectedTagBinding)
1111  return [_source tagForSegment:selectedIndex];
1112  else if (aBinding == CPSelectedLabelBinding)
1113  return [_source labelForSegment:selectedIndex];
1114 }
1115 
1116 @end
1117 
1118 @implementation _CPSegmentItem : CPObject
1119 {
1120  CPImage image;
1123  BOOL selected;
1124  BOOL enabled;
1125  int tag;
1126  int width;
1127 
1128  CGRect frame;
1129 }
1130 
1131 - (id)init
1132 {
1133  if (self = [super init])
1134  {
1135  image = nil;
1136  label = @"";
1137  menu = nil;
1138  selected = NO;
1139  enabled = YES;
1140  tag = -1;
1141  width = 0;
1142 
1143  frame = CGRectMakeZero();
1144  }
1145  return self;
1146 }
1147 
1148 @end
1149 
1150 var CPSegmentItemImageKey = "CPSegmentItemImageKey",
1151  CPSegmentItemLabelKey = "CPSegmentItemLabelKey",
1152  CPSegmentItemMenuKey = "CPSegmentItemMenuKey",
1153  CPSegmentItemSelectedKey = "CPSegmentItemSelectedKey",
1154  CPSegmentItemEnabledKey = "CPSegmentItemEnabledKey",
1155  CPSegmentItemTagKey = "CPSegmentItemTagKey",
1156  CPSegmentItemWidthKey = "CPSegmentItemWidthKey";
1157 
1158 @implementation _CPSegmentItem (CPCoding)
1159 
1160 - (id)initWithCoder:(CPCoder)aCoder
1161 {
1162  self = [super init];
1163 
1164  if (self)
1165  {
1166  image = [aCoder decodeObjectForKey:CPSegmentItemImageKey];
1167  label = [aCoder decodeObjectForKey:CPSegmentItemLabelKey];
1168  menu = [aCoder decodeObjectForKey:CPSegmentItemMenuKey];
1169  selected = [aCoder decodeBoolForKey:CPSegmentItemSelectedKey];
1170  enabled = [aCoder decodeBoolForKey:CPSegmentItemEnabledKey];
1171  tag = [aCoder decodeIntForKey:CPSegmentItemTagKey];
1172  width = [aCoder decodeFloatForKey:CPSegmentItemWidthKey];
1173 
1174  frame = CGRectMakeZero();
1175  }
1176 
1177  return self;
1178 }
1179 
1180 - (void)encodeWithCoder:(CPCoder)aCoder
1181 {
1182  [aCoder encodeObject:image forKey:CPSegmentItemImageKey];
1183  [aCoder encodeObject:label forKey:CPSegmentItemLabelKey];
1184  [aCoder encodeObject:menu forKey:CPSegmentItemMenuKey];
1185  [aCoder encodeBool:selected forKey:CPSegmentItemSelectedKey];
1186  [aCoder encodeBool:enabled forKey:CPSegmentItemEnabledKey];
1187  [aCoder encodeInt:tag forKey:CPSegmentItemTagKey];
1188  [aCoder encodeFloat:width forKey:CPSegmentItemWidthKey];
1189 }
1190 
1191 @end
1192 
1194 
1198 - (CPArray)segments
1199 {
1200  return _segments;
1201 }
1202 
1203 @end
var CPSegmentedControlSegmentStyleKey
Definition: CPFont.h:2
CPSegmentSwitchTrackingSelectAny
void setFont:(CPFont aFont)
Definition: CPControl.j:891
CPThemeStateSelected
Definition: CPTheme.j:613
Definition: CPMenu.h:2
id init()
Definition: CALayer.j:126
CGRect frame
CPThemeStateControlSizeMini
Definition: CPTheme.j:630
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:1020
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:1326
void setControlSize:(CPControlSize aControlSize)
Definition: CPControl.j:211
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)
CGRect bounds()
Definition: CALayer.j:203
void tileWithChangedSegment:(CPInteger aSegment)
CGRect bezelFrameForSegment:(unsigned aSegment)
CPImageLeft
Definition: CPControl.j:71
BOOL isSelectedForSegment:(unsigned aSegment)
var CPSegmentItemSelectedKey
CPThemeStateDisabled
Definition: CPTheme.j:610
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:2249
if(CPFeatureIsCompatible(CPHTMLCanvasFeature))
Definition: CPImage.h:2
BOOL sendAction:to:(SEL anAction, [to] id anObject)
Definition: CPControl.j:319
var CPSegmentItemMenuKey
SEL action()
Definition: CPControl.j:290
id initWithCoder:(CPCoder aCoder)
Definition: CPControl.j:1092
void setNeedsDisplay:(BOOL aFlag)
Definition: CPView.j:2597
int tag
var CPSegmentedControlTrackingModeKey
CPLeftMouseUp
var CPSegmentedControlSelectedKey
void setNeedsLayout()
Definition: CPView.j:2748
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:612
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:1124
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:629
CPCenterTextAlignment
Definition: CPText.j:78
void enumerateIndexesUsingBlock:(Function/*(int idx, @ref BOOL stop) */aFunction)
Definition: CPIndexSet.j:487
CGRect frame()
Definition: CPView.j:1046
CPSegmentSwitchTrackingMomentary
void encodeWithCoder:(CPCoder aCoder)
Definition: CPControl.j:1121
var CPSegmentItemLabelKey
var CPSegmentItemEnabledKey
CPView layoutEphemeralSubviewNamed:positioned:relativeToEphemeralSubviewNamed:(CPString aViewName, [positioned] CPWindowOrderingMode anOrderingMode, [relativeToEphemeralSubviewNamed] CPString relativeToViewName)
Definition: CPView.j:3407
CPMenu menu
CPCenterVerticalTextAlignment
Definition: CPControl.j:54
CGRect rectForEphemeralSubviewNamed:(CPString aViewName)
Definition: CPView.j:3402
Definition: CPView.j:137