1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require('views/segment');
  9 
 10 /**
 11   @class
 12 
 13   SegmentedView is a special type of button that can display multiple
 14   segments.  Each segment has a value assigned to it.  When the user clicks
 15   on the segment, the value of that segment will become the new value of
 16   the control.
 17 
 18   You can also optionally configure a target/action that will fire whenever
 19   the user clicks on an item.  This will give your code an opportunity to take
 20   some action depending on the new value.  (of course, you can always bind to
 21   the value as well, which is generally the preferred approach.)
 22 
 23   # Defining Your Segments
 24 
 25   You define your segments by providing a items array, much like you provide
 26   to a RadioView.  Your items array can be as simple as an array of strings
 27   or as complex as full model objects.  Based on how you configure your
 28   itemKey properties, the segmented view will read the properties it needs
 29   from the array and construct the button.
 30 
 31   You can define the following properties on objects you pass in:
 32 
 33     - *itemTitleKey* - the title of the button
 34     - *itemValueKey* - the value of the button
 35     - *itemWidthKey* - the preferred width. if omitted, it autodetects
 36     - *itemIconKey*  - an icon
 37     - *itemActionKey* - an optional action to fire when pressed
 38     - *itemTargetKey* - an optional target for the action
 39     - *itemLayerIdKey* - an optional target for the action
 40     - *segmentViewClass* - class to be used for creating segments
 41 
 42   @extends SC.View
 43   @extends SC.Control
 44   @since SproutCore 1.0
 45 */
 46 SC.SegmentedView = SC.View.extend(SC.Control,
 47 /** @scope SC.SegmentedView.prototype */ {
 48 
 49   /** @private
 50     @ field
 51     @type Boolean
 52     @default YES
 53   */
 54   acceptsFirstResponder: function () {
 55     if (SC.FOCUS_ALL_CONTROLS) { return this.get('isEnabledInPane'); }
 56     return NO;
 57   }.property('isEnabledInPane').cacheable(),
 58 
 59   /** @private
 60     @type String
 61     @default 'tablist'
 62     @readOnly
 63   */
 64   //ariaRole: 'tablist',
 65   ariaRole: 'group', // workaround for <rdar://problem/10444670>; switch back to 'tablist' later with <rdar://problem/10463928> (also see segment.js)
 66 
 67   /** @private
 68     @type Array
 69     @default ['sc-segmented-view']
 70     @see SC.View#classNames
 71   */
 72   classNames: ['sc-segmented-view'],
 73 
 74   /**
 75     @type String
 76     @default 'square'
 77     @see SC.ButtonView#theme
 78   */
 79   theme: 'square',
 80 
 81   /**
 82     The value of the segmented view.
 83 
 84     The SegmentedView's value will always be the value of the currently
 85     selected button or buttons.  Setting this value will change the selected
 86     button or buttons.
 87 
 88     If you set this value to something that has no matching button, then
 89     no buttons will be selected.
 90 
 91     Note: if allowsMultipleSelection is set to true, then the value must be
 92     an Array.
 93 
 94     @type Object | Array
 95     @default null
 96   */
 97   value: null,
 98 
 99   /**
100     If YES, clicking a selected button again will deselect it, setting the
101     segmented views value to null.
102 
103     @type Boolean
104     @default NO
105   */
106   allowsEmptySelection: NO,
107 
108   /**
109     If YES, then clicking on a tab will not deselect the other segments, it
110     will simply add or remove it from the selection.
111 
112     @type Boolean
113     @default NO
114   */
115   allowsMultipleSelection: NO,
116 
117   /**
118     If YES, it will set the segment value even if an action is defined.
119 
120     @type Boolean
121     @default NO
122   */
123   selectSegmentWhenTriggeringAction: NO,
124 
125   /**
126     @type Boolean
127     @default YES
128   */
129   localize: YES,
130 
131   /**
132     Aligns the segments of the segmented view within its frame horizontally.
133     Possible values:
134 
135       - SC.ALIGN_LEFT
136       - SC.ALIGN_RIGHT
137       - SC.ALIGN_CENTER
138 
139     @type String
140     @default SC.ALIGN_CENTER
141   */
142   align: SC.ALIGN_CENTER,
143 
144   /**
145     Change the layout direction to make this a vertical set of tabs instead
146     of horizontal ones. Possible values:
147 
148       - SC.LAYOUT_HORIZONTAL
149       - SC.LAYOUT_VERTICAL
150 
151     @type String
152     @default SC.LAYOUT_HORIZONTAL
153   */
154   layoutDirection: SC.LAYOUT_HORIZONTAL,
155 
156 
157   // ..........................................................
158   // SEGMENT DEFINITION
159   //
160 
161   /**
162     The array of items to display.  This may be a simple array of strings, objects
163     or SC.Objects.  If you pass objects or SC.Objects, you must also set the
164     various itemKey properties to tell the SegmentedView how to extract the
165     information it needs.
166 
167     Note: only SC.Object items support key-value coding and therefore may be
168     observed by the view for changes to titles, values, icons, widths,
169     isEnabled values & tooltips.
170 
171     @type Array
172     @default null
173   */
174   items: null,
175 
176   /**
177     The key that contains the title for each item.
178 
179     @type String
180     @default null
181   */
182   itemTitleKey: null,
183 
184   /**
185     The key that contains the value for each item.
186 
187     @type String
188     @default null
189   */
190   itemValueKey: null,
191 
192   /**
193     A key that determines if this item in particular is enabled.  Note if the
194     control in general is not enabled, no items will be enabled, even if the
195     item's enabled property returns YES.
196 
197     @type String
198     @default null
199   */
200   itemIsEnabledKey: null,
201 
202   /**
203     The key that contains the icon for each item.  If omitted, no icons will
204     be displayed.
205 
206     @type String
207     @default null
208   */
209   itemIconKey: null,
210 
211   /**
212     The key that contains the desired width for each item.  If omitted, the
213     width will autosize.
214 
215     @type String
216     @default null
217   */
218   itemWidthKey: null,
219 
220   /**
221     The key that contains the action for this item.  If defined, then
222     selecting this item will fire the action in addition to changing the
223     value.  See also itemTargetKey.
224 
225     @type String
226     @default null
227   */
228   itemActionKey: null,
229 
230   /**
231     The key that contains the target for this item.  If this and itemActionKey
232     are defined, then this will be the target of the action fired.
233 
234     @type String
235     @default null
236   */
237   itemTargetKey: null,
238 
239   /**
240     The key that contains the layerId for each item.
241     @type String
242   */
243   itemLayerIdKey: null,
244 
245   /**
246     The key that contains the key equivalent for each item.  If defined then
247     pressing that key equivalent will be like selecting the tab.  Also,
248     pressing the Alt or Option key for 3 seconds will display the key
249     equivalent in the tab.
250 
251     @type String
252     @default null
253   */
254   itemKeyEquivalentKey: null,
255 
256   /**
257     If YES, overflowing items are placed into a menu and an overflow segment is
258     added to popup that menu.
259 
260     @type Boolean
261     @default YES
262   */
263   shouldHandleOverflow: YES,
264 
265   /**
266     The title to use for the overflow segment if it appears.
267 
268     NOTE: This will not be HTML escaped and must never be assigned to user inserted text!
269 
270     @type String
271     @default '»'
272   */
273   overflowTitle: '»',
274 
275   /**
276     The toolTip to use for the overflow segment if it appears.
277 
278     @type String
279     @default 'More…'
280   */
281   overflowToolTip: 'More…',
282 
283   /**
284     The icon to use for the overflow segment if it appears.
285 
286     @type String
287     @default null
288   */
289   overflowIcon: null,
290 
291   /**
292     The view class used when creating segments.
293 
294     @type SC.View
295     @default SC.SegmentView
296   */
297   segmentViewClass: SC.SegmentView,
298 
299   /**
300     Set to YES if you would like your SegmentedView to size itself based on its
301     visible segments. Useful if you're using SegmentedView in a flowed context
302     (for example if its parent view has `childViewLayout: SC.View.HORIZONTAL_STACK`).
303 
304     The view will not auto-resize unless you define an initial value for the layout
305     property which will be auto-resized (i.e. `width` when in the default horizontal
306     orientation). This is to prevent the view from inappropriately adding width to a
307     flexible (`{ left: 0, right: 0 }`) layout.
308 
309     Has no effect if `shouldHandleOverflow` is NO.
310 
311     @type Boolean
312     @default NO
313    */
314   shouldAutoResize: NO,
315 
316   /** @private
317     The following properties are used to map items to child views. Item keys
318     are looked up on the item based on this view's value for each 'itemKey'.
319     If a value in the item is found, then that value is mapped to a child
320     view using the matching viewKey.
321 
322     @type Array
323   */
324   itemKeys: ['itemTitleKey', 'itemValueKey', 'itemIsEnabledKey', 'itemIconKey', 'itemWidthKey', 'itemToolTipKey', 'itemKeyEquivalentKey', 'itemLayerIdKey'],
325 
326   /** @private */
327   viewKeys: ['title', 'value', 'isEnabled', 'icon', 'width', 'toolTip', 'keyEquivalent', 'layerId'],
328 
329   /** @private
330     Call itemsDidChange once to initialize segment child views for the items that exist at
331     creation time.
332   */
333   init: function () {
334     sc_super();
335 
336     // Initialize.
337     this.shouldHandleOverflowDidChange();
338     this.itemsDidChange();
339   },
340 
341   shouldHandleOverflowDidChange: function () {
342     var overflowView = this.get('overflowView');
343 
344     if (this.get('shouldHandleOverflow')) {
345       var title = this.get('overflowTitle'),
346           toolTip = this.get('overflowToolTip'),
347           icon = this.get('overflowIcon');
348 
349       overflowView = this.get('segmentViewClass').create({
350         controlSize: this.get('controlSize'),
351         escapeHTML: false,
352         localize: this.get('localize'),
353         title: title,
354         toolTip: toolTip,
355         icon: icon,
356         isLastSegment: YES,
357         isOverflowSegment: YES,
358         layoutDirection: this.get('layoutDirection')
359       });
360       this.appendChild(overflowView);
361       this.set('overflowView', overflowView);
362 
363       // remeasure should show/hide it as needed
364       this.invokeLast(this.remeasure);
365     } else {
366       if (overflowView) { // There will not be an overflow view on initialization.
367         this.removeChildAndDestroy(overflowView);
368         this.set('overflowView', null);
369       }
370     }
371   }.observes('shouldHandleOverflow'),
372 
373   /** @private
374     Called whenever the number of items changes.  This method populates SegmentedView's childViews, taking
375     care to re-use existing childViews if possible.
376   */
377   itemsDidChange: function () {
378     var items = this.get('items') || [],
379         localItem,                        // Used to avoid altering the original items
380         previousItem,
381         childViews = this.get('childViews'),
382         childView,
383         overflowView = this.get('overflowView'),
384         value = this.get('value'),        // The value can change if items that were once selected are removed
385         isSelected,
386         itemKeys = this.get('itemKeys'),
387         itemKey,
388         segmentViewClass = this.get('segmentViewClass'),
389         i, j;
390 
391     // Update childViews
392     var childViewsLength = this.get('shouldHandleOverflow') ? childViews.get('length') - 1 : childViews.get('length');
393     if (childViewsLength > items.get('length')) {   // We've lost segments (ie. childViews)
394 
395       // Remove unneeded segments from the end back
396       for (i = childViewsLength - 1; i >= items.get('length'); i--) {
397         childView = childViews.objectAt(i);
398         localItem = childView.get('localItem');
399 
400         // Remove observers from items we are losing off the end
401         if (localItem instanceof SC.Object) {
402 
403           for (j = itemKeys.get('length') - 1; j >= 0; j--) {
404             itemKey = this.get(itemKeys.objectAt(j));
405 
406             if (itemKey) {
407               localItem.removeObserver(itemKey, this, this.itemContentDidChange);
408             }
409           }
410         }
411 
412         // If a selected childView has been removed then update our value
413         if (SC.isArray(value)) {
414           value.removeObject(localItem);
415         } else if (value === localItem) {
416           value = null;
417         }
418 
419         this.removeChildAndDestroy(childView);
420       }
421 
422       // Update our value which may have changed
423       this.set('value', value);
424 
425     } else if (childViewsLength < items.get('length')) {  // We've gained segments
426 
427       // Create the new segments
428       for (i = childViewsLength; i < items.get('length'); i++) {
429 
430         // We create a default SC.ButtonView-like object for each segment
431         childView = segmentViewClass.create({
432           controlSize: this.get('controlSize'),
433           localize: this.get('localize'),
434           layoutDirection: this.get('layoutDirection')
435         });
436 
437         // Attach the child
438         this.insertBefore(childView, overflowView);
439       }
440     }
441 
442     // Because the items array can be altered with insertAt or removeAt, we can't be sure that the items
443     // continue to match 1-to-1 the existing views, so once we have the correct number of childViews,
444     // simply update them all
445     childViews = this.get('childViews');
446 
447     for (i = 0; i < items.get('length'); i++) {
448       localItem = items.objectAt(i);
449       childView = childViews.objectAt(i);
450       previousItem = childView.get('localItem');
451 
452       if (previousItem instanceof SC.Object && !items.contains(previousItem)) {
453         // If the old item is no longer in the view, remove its observers
454         for (j = itemKeys.get('length') - 1; j >= 0; j--) {
455           itemKey = this.get(itemKeys.objectAt(j));
456 
457           if (itemKey) {
458             previousItem.removeObserver(itemKey, this, this.itemContentDidChange);
459           }
460         }
461       }
462 
463       // Skip null/undefined items (but don't skip empty strings)
464       if (SC.none(localItem)) continue;
465 
466       // Normalize the item (may be a String, Object or SC.Object)
467       if (SC.typeOf(localItem) === SC.T_STRING) {
468 
469         localItem = SC.Object.create({
470           'title': localItem.humanize().titleize(),
471           'value': localItem
472         });
473 
474         // Update our keys accordingly
475         this.set('itemTitleKey', 'title');
476         this.set('itemValueKey', 'value');
477       } else if (SC.typeOf(localItem) === SC.T_HASH) {
478 
479         localItem = SC.Object.create(localItem);
480       } else if (localItem instanceof SC.Object)  {
481 
482         // We don't need to make any changes to SC.Object items, but we can observe them
483         for (j = itemKeys.get('length') - 1; j >= 0; j--) {
484           itemKey = this.get(itemKeys.objectAt(j));
485 
486           if (itemKey) {
487             localItem.removeObserver(itemKey, this, this.itemContentDidChange);
488             localItem.addObserver(itemKey, this, this.itemContentDidChange, i);
489           }
490         }
491       } else {
492         SC.Logger.error('SC.SegmentedView items may be Strings, Objects (ie. Hashes) or SC.Objects only');
493       }
494 
495       // Determine whether this segment is selected based on the view's existing value(s)
496       isSelected = NO;
497       if (SC.isArray(value) ? value.indexOf(localItem.get(this.get('itemValueKey'))) >= 0 : value === localItem.get(this.get('itemValueKey'))) {
498         isSelected = YES;
499       }
500       childView.set('isSelected', isSelected);
501 
502       // Assign segment specific properties based on position
503       childView.set('index', i);
504       childView.set('isFirstSegment', i === 0);
505       childView.set('isMiddleSegment',  i < items.get('length') - 1 && i > 0);
506       childView.set('isLastSegment', i === items.get('length') - 1);
507 
508       // Be sure to update the view's properties for the (possibly new) matched item
509       childView.updateItem(this, localItem);
510     }
511 
512     // Force a segment remeasure to check overflow
513     if (this.get('shouldHandleOverflow')) {
514       this.invokeLast(this.remeasure);
515     }
516   }.observes('*items.[]'),
517 
518   /** @private
519     This observer method is called whenever any of the relevant properties of an item change.  This only applies
520     to SC.Object based items that may be observed.
521   */
522   itemContentDidChange: function (item, key, alwaysNull, index) {
523     var childViews = this.get('childViews'),
524         childView;
525 
526     childView = childViews.objectAt(index);
527     if (childView) {
528 
529       // Update the childView
530       childView.updateItem(this, item);
531 
532       // Reset our measurements (which depend on width/height or title) and adjust visible views
533       if (this.get('shouldHandleOverflow')) {
534         this.invokeLast(this.remeasure);
535       }
536     }
537   },
538 
539   /** @private
540     Whenever the view resizes, we need to check to see if we're overflowing.
541   */
542   viewDidResize: function () {
543     this._sc_viewFrameDidChange();
544 
545     var isHorizontal = this.get('layoutDirection') === SC.LAYOUT_HORIZONTAL,
546       visibleDim = isHorizontal ? this.$().width() : this.$().height();
547 
548     // Only overflow if we've gone below the minimum dimension required to fit all the segments
549     if (this.get('shouldHandleOverflow') && (this.get('isOverflowing') || visibleDim <= this.cachedMinimumDim)) {
550       this.invokeLast(this.remeasure);
551     }
552   },
553 
554   /** @private
555     Whenever visibility changes, we need to check to see if we're overflowing.
556   */
557   isVisibleInWindowDidChange: function () {
558     if (this.get('shouldHandleOverflow')) {
559       this.invokeLast(this.remeasure);
560     }
561   }.observes('isVisibleInWindow'),
562 
563   /** @private
564     Calling this method forces the segments to be remeasured and will also adjust the
565     segments for overflow if necessary.
566   */
567   remeasure: function () {
568     if (!this.get('shouldHandleOverflow')) { return; }
569 
570     var childViews = this.get('childViews'),
571         overflowView;
572 
573     if (this.get('isVisibleInWindow')) {
574       // Make all the views visible so that they can be measured
575       overflowView = this.get('overflowView');
576       overflowView.set('isVisible', YES);
577 
578       for (var i = childViews.get('length') - 1; i >= 0; i--) {
579         childViews.objectAt(i).set('isVisible', YES);
580       }
581 
582       this.cachedDims = this.segmentDimensions();
583       this.cachedOverflowDim = this.overflowSegmentDim();
584 
585       this.adjustOverflow();
586     }
587   },
588 
589   /** @private
590     This method is called to adjust the segment views to see if we need to handle for overflow.
591    */
592   adjustOverflow: function () {
593     if (!this.get('shouldHandleOverflow')) { return; }
594 
595     var childViews = this.get('childViews'),
596         childView,
597         value = this.get('value'),
598         overflowView = this.get('overflowView'),
599         isHorizontal = this.get('layoutDirection') === SC.LAYOUT_HORIZONTAL,
600         visibleDim = isHorizontal ? this.$().width() : this.$().height(),  // The inner width/height of the div
601         curElementsDim = 0,
602         dimToFit, length, i,
603         isOverflowing = NO,
604         wantsAutoResize = this.get('shouldAutoResize'),
605         layoutProperty = isHorizontal ? 'width' : 'height',
606         canAutoResize = !SC.none(this.getPath('layout.%@'.fmt(layoutProperty))),
607         willAutoResize = wantsAutoResize && canAutoResize;
608 
609     // If child views and cachedDims lengths are out of sync here, it means adjustOverflow
610     // got called in between itemsDidChange and remeasure. Since we know that the remeasure is
611     // scheduled, just return and let the remeasure + adjustOverflow happen later.
612     if (childViews.get('length') !== this.cachedDims.length + 1) { return; }
613 
614     // This variable is useful to optimize when we are overflowing
615     isOverflowing = NO;
616     overflowView.set('isSelected', NO);
617 
618     // Clear out the overflow items (these are the items not currently visible)
619     this.overflowItems = [];
620 
621     length = this.cachedDims.length;
622     for (i = 0; i < length; i++) {
623       childView = childViews.objectAt(i);
624       curElementsDim += this.cachedDims[i];
625 
626       // Check and see if this item kicks us over into overflow.
627       if (!isOverflowing && !willAutoResize) {
628         // (don't leave room for the overflow segment on the last item)
629         dimToFit = (i === length - 1) ? curElementsDim : curElementsDim + this.cachedOverflowDim;
630         if (dimToFit > visibleDim) isOverflowing = YES;
631       }
632 
633       // Update the view depending on overflow state.
634       if (isOverflowing) {
635         // Add the localItem to the overflowItems
636         this.overflowItems.pushObject(childView.get('localItem'));
637 
638         childView.set('isVisible', NO);
639 
640         // If the first item is already overflowed, make the overflowView first segment
641         if (i === 0) overflowView.set('isFirstSegment', YES);
642 
643         // If the overflowed segment was selected, show the overflowView as selected instead
644         if (SC.isArray(value) ? value.indexOf(childView.get('value')) >= 0 : value === childView.get('value')) {
645           overflowView.set('isSelected', YES);
646         }
647       } else {
648         childView.set('isVisible', YES);
649 
650         // If the first item is not overflowed, don't make the overflowView first segment
651         if (i === 0) overflowView.set('isFirstSegment', NO);
652       }
653     }
654 
655     // Show/hide the overflow view as needed.
656     overflowView.set('isVisible', isOverflowing);
657 
658     // Set the overflowing property.
659     this.setIfChanged('isOverflowing', isOverflowing);
660 
661     // Autosize if needed.
662     if (willAutoResize) {
663       this.adjust(layoutProperty, this.isOverflowing ? this.cachedMinimumDim : curElementsDim);
664     }
665 
666     // Store the minimum dimension (height/width) before overflow
667     this.cachedMinimumDim = curElementsDim + this.cachedOverflowDim;
668   },
669 
670   /**
671     Return the dimensions (either heights or widths depending on the layout direction) of the DOM
672     elements of the segments.  This will be measured by the view to determine which segments should
673     be overflowed.
674 
675     It ignores the last segment (the overflow segment).
676   */
677   segmentDimensions: function () {
678     var cv = this.get('childViews'),
679         v, f,
680         dims = [],
681         isHorizontal = this.get('layoutDirection') === SC.LAYOUT_HORIZONTAL;
682 
683     for (var i = 0, length = cv.length; i < length - 1; i++) {
684       v = cv[i];
685       f = v.get('frame');
686       dims[i] = isHorizontal ? f.width : f.height;
687     }
688 
689     return dims;
690   },
691 
692   /**
693     Return the dimension (height or width depending on the layout direction) over the overflow segment.
694   */
695   overflowSegmentDim: function () {
696     var cv = this.get('childViews'),
697         v, f,
698         isHorizontal = this.get('layoutDirection') === SC.LAYOUT_HORIZONTAL;
699 
700     v = cv.length && cv[cv.length - 1];
701     if (v) {
702       f = v.get('frame');
703       return isHorizontal ? f.width : f.height;
704     }
705 
706     return 0;
707   },
708 
709   /**
710     Return the index of the segment view that is the target of the mouse click.
711   */
712   indexForClientPosition: function (x, y) {
713     var cv = this.get('childViews'),
714         length, i,
715         v, rect,
716         point;
717 
718     point = { x: x, y: y };
719     for (i = 0, length = cv.length; i < length; i++) {
720       v = cv[i];
721 
722       rect = v.get('layer').getBoundingClientRect();
723       rect = {
724         x: rect.left,
725         y: rect.top,
726         width: (rect.right - rect.left),
727         height: (rect.bottom - rect.top)
728       };
729 
730       // Return the index early if found
731       if (SC.pointInRect(point, rect)) return i;
732     }
733 
734     // Default not found
735     return -1;
736   },
737 
738   // ..........................................................
739   // RENDERING/DISPLAY SUPPORT
740   //
741 
742   /**
743     @type Array
744     @default ['align']
745     @see SC.View#displayProperties
746   */
747   displayProperties: ['align'],
748 
749   /**
750     @type String
751     @default 'segmentedRenderDelegate'
752   */
753   renderDelegateName: 'segmentedRenderDelegate',
754 
755   // ..........................................................
756   // EVENT HANDLING
757   //
758 
759   /** @private
760     Determines the index into the displayItems array where the passed mouse
761     event occurred.
762   */
763   displayItemIndexForEvent: function (evt) {
764     var el = evt.target,
765         x = evt.clientX,
766         y = evt.clientY,
767         ret = -1;
768 
769     if (el && el !== this.get('layer')) {
770       // Accessibility workaround: WebKit sends all event coords as 0,0 for all AXPress-triggered events.
771       // For example, triggering an element with VoiceOver in OS X.
772       // Note: by ensuring that the event target wasn't our own layer, we avoid the situation where an
773       // actual mouse clicked at 0,0 and hit only our layer.
774       if (x === 0 && y === 0) {
775         var offset = SC.offset(el);
776 
777         // Generate point coordinates in the middle of the target element.
778         x = offset.x + Math.round(el.offsetWidth / 2);
779         y = offset.y + Math.round(el.offsetHeight / 2);
780       }
781 
782       var renderDelegate = this.get('renderDelegate');
783       if (renderDelegate && renderDelegate.indexForClientPosition) {
784         ret = renderDelegate.indexForClientPosition(this, x, y);
785       } else {
786         ret = this.indexForClientPosition(x, y);
787       }
788     }
789 
790     return ret;
791   },
792 
793   /** @private */
794   keyDown: function (evt) {
795     var childViews,
796         childView,
797         i, length,
798         value, isArray;
799 
800     // handle tab key
801     if (evt.which === 9 || evt.keyCode === 9) {
802       var view = evt.shiftKey ? this.get('previousValidKeyView') : this.get('nextValidKeyView');
803       if (view) view.becomeFirstResponder();
804       else evt.allowDefault();
805       return YES; // handled
806     }
807 
808     // handle arrow keys
809     if (!this.get('allowsMultipleSelection')) {
810       childViews = this.get('childViews');
811 
812       length = childViews.get('length');
813       value = this.get('value');
814       isArray = SC.isArray(value);
815 
816       // Select from the left to the right
817       if (evt.which === 39 || evt.which === 40) {
818 
819         if (value) {
820           for (i = 0; i < length - 2; i++) {
821             childView = childViews.objectAt(i);
822             if (isArray ? (value.indexOf(childView.get('value')) >= 0) : (childView.get('value') === value)) {
823               this.triggerItemAtIndex(i + 1);
824             }
825           }
826         } else {
827           this.triggerItemAtIndex(0);
828         }
829         return YES; // handled
830 
831       // Select from the right to the left
832       } else if (evt.which === 37 || evt.which === 38) {
833 
834         if (value) {
835           for (i = 1; i < length - 1; i++) {
836             childView = childViews.objectAt(i);
837             if (isArray ? (value.indexOf(childView.get('value')) >= 0) : (childView.get('value') === value)) {
838               this.triggerItemAtIndex(i - 1);
839             }
840           }
841         } else {
842           this.triggerItemAtIndex(length - 2);
843         }
844 
845         return YES; // handled
846       }
847     }
848 
849     return NO;
850   },
851 
852   /** @private */
853   mouseDown: function (evt) {
854     // Fast path, reject secondary clicks.
855     if (evt.which !== 1) return false;
856 
857     var childViews = this.get('childViews'),
858         childView,
859         index;
860 
861     if (!this.get('isEnabledInPane')) return YES; // nothing to do // TODO: return NO?
862 
863     index = this.displayItemIndexForEvent(evt);
864     if (index >= 0) {
865       childView = childViews.objectAt(index);
866       if (childView.get('isEnabled')) childView.set('isActive', YES);
867       this.activeChildView = childView;
868 
869       // if mouse was pressed on the overflow segment, popup the menu
870       var overflowIndex = this.get('shouldHandleOverflow') ? childViews.get('length') - 1 : null;
871       if (index === overflowIndex) this.showOverflowMenu();
872       else this._isMouseDown = YES;
873 
874       return YES;
875     }
876     // If this event originated outside of a segment, pass the event along up.
877     else {
878       return NO;
879     }
880   },
881 
882   /** @private */
883   mouseUp: function (evt) {
884     var activeChildView,
885         index;
886 
887     index = this.displayItemIndexForEvent(evt);
888     if (this._isMouseDown && (index >= 0)) {
889 
890       this.triggerItemAtIndex(index);
891 
892       // Clean up
893       activeChildView = this.activeChildView;
894       if (activeChildView) {
895         activeChildView.set('isActive', NO);
896         this.activeChildView = null;
897       }
898     }
899 
900     this._isMouseDown = NO;
901     return YES;
902   },
903 
904   /** @private */
905   mouseMoved: function (evt) {
906     var childViews = this.get('childViews'),
907         activeChildView,
908         childView,
909         index;
910 
911     if (this._isMouseDown) {
912       // Update the last segment
913       index = this.displayItemIndexForEvent(evt);
914 
915       activeChildView = this.activeChildView;
916       childView = childViews.objectAt(index);
917 
918       if (childView && childView !== activeChildView) {
919         // Changed
920         if (activeChildView) activeChildView.set('isActive', NO);
921         if (childView.get('isEnabled')) childView.set('isActive', YES);
922         this.activeChildView = childView;
923 
924         var overflowIndex = this.get('shouldHandleOverflow') ? childViews.get('length') - 1 : null;
925         if (index === overflowIndex) {
926           this.showOverflowMenu();
927           this._isMouseDown = NO;
928         }
929       }
930     }
931     return YES;
932   },
933 
934   /** @private */
935   mouseEntered: function (evt) {
936     var childViews = this.get('childViews'),
937         childView,
938         index;
939 
940     // if mouse was pressed down initially, start detection again
941     if (this._isMouseDown) {
942       index = this.displayItemIndexForEvent(evt);
943 
944       // if mouse was pressed on the overflow segment, popup the menu
945       var overflowIndex = this.get('shouldHandleOverflow') ? childViews.get('length') - 1 : null;
946       if (index === overflowIndex) {
947         this.showOverflowMenu();
948         this._isMouseDown = NO;
949       } else if (index >= 0) {
950         childView = childViews.objectAt(index);
951         if (childView.get('isEnabled')) childView.set('isActive', YES);
952 
953         this.activeChildView = childView;
954       }
955     }
956     return YES;
957   },
958 
959   /** @private */
960   mouseExited: function (evt) {
961     var activeChildView;
962 
963     // if mouse was down, hide active index
964     if (this._isMouseDown) {
965       activeChildView = this.activeChildView;
966       if (activeChildView) activeChildView.set('isActive', NO);
967 
968       this.activeChildView = null;
969     }
970 
971     return YES;
972   },
973 
974   /** @private */
975   touchStart: function (touch) {
976     var childViews = this.get('childViews'),
977         childView,
978         index;
979 
980     if (!this.get('isEnabledInPane')) return YES; // nothing to do
981 
982     index = this.displayItemIndexForEvent(touch);
983 
984     if (index >= 0) {
985       childView = childViews.objectAt(index);
986       childView.set('isActive', YES);
987       this.activeChildView = childView;
988 
989       // if touch was on the overflow segment, popup the menu
990       var overflowIndex = this.get('shouldHandleOverflow') ? childViews.get('length') - 1 : null;
991       if (index === overflowIndex) this.showOverflowMenu();
992       else this._isTouching = YES;
993     }
994 
995     return YES;
996   },
997 
998   /** @private */
999   touchEnd: function (touch) {
1000     var activeChildView,
1001         index;
1002 
1003     index = this.displayItemIndexForEvent(touch);
1004 
1005     if (this._isTouching && (index >= 0)) {
1006       this.triggerItemAtIndex(index);
1007 
1008       // Clean up
1009       activeChildView = this.activeChildView;
1010       activeChildView.set('isActive', NO);
1011       this.activeChildView = null;
1012 
1013       this._isTouching = NO;
1014     }
1015 
1016     return YES;
1017   },
1018 
1019   /** @private */
1020   touchesDragged: function (evt, touches) {
1021     var isTouching = this.touchIsInBoundary(evt),
1022         childViews = this.get('childViews'),
1023         activeChildView,
1024         childView,
1025         index;
1026 
1027     if (isTouching) {
1028       if (!this._isTouching) {
1029         this._touchDidEnter(evt);
1030       }
1031       index = this.displayItemIndexForEvent(evt);
1032 
1033       activeChildView = this.activeChildView;
1034       childView = childViews[index];
1035 
1036       if (childView && childView !== activeChildView) {
1037         // Changed
1038         if (activeChildView) activeChildView.set('isActive', NO);
1039         childView.set('isActive', YES);
1040 
1041         this.activeChildView = childView;
1042 
1043         var overflowIndex = this.get('shouldHandleOverflow') ? childViews.get('length') - 1 : null;
1044         if (index === overflowIndex) {
1045           this.showOverflowMenu();
1046           this._isMouseDown = NO;
1047         }
1048       }
1049     } else {
1050       if (this._isTouching) this._touchDidExit(evt);
1051     }
1052 
1053     this._isTouching = isTouching;
1054 
1055     return YES;
1056   },
1057 
1058   /** @private */
1059   _touchDidExit: function (evt) {
1060     var activeChildView;
1061 
1062     if (this.isTouching) {
1063       activeChildView = this.activeChildView;
1064       activeChildView.set('isActive', NO);
1065       this.activeChildView = null;
1066     }
1067 
1068     return YES;
1069   },
1070 
1071   /** @private */
1072   _touchDidEnter: function (evt) {
1073     var childViews = this.get('childViews'),
1074         childView,
1075         index;
1076 
1077     index = this.displayItemIndexForEvent(evt);
1078 
1079     var overflowIndex = this.get('shouldHandleOverflow') ? childViews.get('length') - 1 : null;
1080     if (index === overflowIndex) {
1081       this.showOverflowMenu();
1082       this._isTouching = NO;
1083     } else if (index >= 0) {
1084       childView = childViews.objectAt(index);
1085       childView.set('isActive', YES);
1086       this.activeChildView = childView;
1087     }
1088 
1089     return YES;
1090   },
1091 
1092   /** @private
1093     Simulates the user clicking on the segment at the specified index. This
1094     will update the value if possible and fire the action.
1095   */
1096   triggerItemAtIndex: function (index) {
1097     var childViews = this.get('childViews'),
1098         childView,
1099         childValue, value, allowEmpty, allowMult;
1100 
1101     childView = childViews.objectAt(index);
1102 
1103     if (!childView.get('isEnabled')) return this; // nothing to do!
1104 
1105     allowEmpty = this.get('allowsEmptySelection');
1106     allowMult = this.get('allowsMultipleSelection');
1107 
1108     // get new value... bail if not enabled. Also save original for later.
1109     childValue = childView.get('value');
1110     value = this.get('value');
1111 
1112     // if we do not allow multiple selection, either replace the current
1113     // selection or deselect it
1114     if (!allowMult) {
1115       // if we allow empty selection and the current value is the same as
1116       // the selected value, then deselect it.
1117       if (allowEmpty && value === childValue) {
1118         value = null;
1119       } else {
1120         // otherwise, simply replace the value.
1121         value = childValue;
1122       }
1123     } else {
1124       // Lazily create the value array.
1125       if (!value) {
1126         value = [];
1127       } else if (!SC.isArray(value)) {
1128         value = [value];
1129       }
1130 
1131       // if we do allow multiple selection, then add or remove item to the array.
1132       if (value.indexOf(childValue) >= 0) {
1133         if (value.get('length') > 1 || (value.objectAt(0) !== childValue) || allowEmpty) {
1134           value = value.without(childValue);
1135         }
1136       } else {
1137         value = value.concat(childValue);
1138       }
1139     }
1140 
1141     // also, trigger target if needed.
1142     var actionKey = this.get('itemActionKey'),
1143         targetKey = this.get('itemTargetKey'),
1144         action, target = null,
1145         resp = this.getPath('pane.rootResponder'),
1146         item;
1147 
1148     if (actionKey && (item = this.get('items').objectAt(index))) {
1149       // get the source item from the item array.  use the index stored...
1150       action = item.get ? item.get(actionKey) : item[actionKey];
1151       if (targetKey) {
1152         target = item.get ? item.get(targetKey) : item[targetKey];
1153       }
1154       if (resp) resp.sendAction(action, target, this, this.get('pane'), value);
1155     }
1156 
1157     if (value !== undefined && (!action || this.get('selectSegmentWhenTriggeringAction'))) {
1158       this.set('value', value);
1159     }
1160 
1161     // if an action/target is defined on self use that also
1162     action = this.get('action');
1163     if (action && resp) {
1164       resp.sendAction(action, this.get('target'), this, this.get('pane'), value);
1165     }
1166   },
1167 
1168   /** @private
1169     Invoked whenever an item is selected in the overflow menu.
1170   */
1171   selectOverflowItem: function (menu) {
1172     var item = menu.get('selectedItem');
1173 
1174     this.triggerItemAtIndex(item.get('index'));
1175 
1176     // Cleanup
1177     menu.removeObserver('selectedItem', this, 'selectOverflowItem');
1178 
1179     this.activeChildView.set('isActive', NO);
1180     this.activeChildView = null;
1181   },
1182 
1183   /** @private
1184     Presents the popup menu containing overflowed segments.
1185   */
1186   showOverflowMenu: function () {
1187     var self = this,
1188         childViews = this.get('childViews'),
1189         itemValueKey = this.get('itemValueKey'),
1190         itemLayerIdKey = this.get('itemLayerIdKey'),
1191         overflowItems = this.overflowItems,
1192         overflowItemsLength,
1193         startIndex,
1194         isArray,
1195         value,
1196         item,
1197         layerId,
1198         layer,
1199         overflowElement;
1200 
1201     // Check the currently selected item if it is in overflowItems
1202     overflowItemsLength = overflowItems.get('length');
1203     startIndex = childViews.get('length') - 1 - overflowItemsLength;
1204 
1205     value = this.get('value');
1206     isArray = SC.isArray(value);
1207     for (var i = 0; i < overflowItemsLength; i++) {
1208       item = overflowItems.objectAt(i);
1209 
1210       if (isArray ? value.indexOf(item.get(itemValueKey)) >= 0 : value === item.get(itemValueKey)) {
1211         item.set('isChecked', YES);
1212       } else {
1213         item.set('isChecked', NO);
1214       }
1215 
1216       // Track the matching segment index
1217       item.set('index', startIndex + i);
1218 
1219       // Add '-overflow-menu-item' to the existing layer id (if set),
1220       // and use that as the layer id on the menu. This prevents the original
1221       // segment from being removed when the menu closes.
1222       layerId = item.get(itemLayerIdKey);
1223       if (layerId) {
1224         item.set('overflowLayerId', layerId + '-overflow-menu-item');
1225       }
1226     }
1227 
1228     // TODO: we can't pass a shortcut key to the menu, because it isn't a property of SegmentedView (yet?)
1229     var menu = SC.MenuPane.create({
1230       layout: { width: 200 },
1231       items: overflowItems,
1232       itemTitleKey: this.get('itemTitleKey'),
1233       itemIconKey: this.get('itemIconKey'),
1234       itemIsEnabledKey: this.get('itemIsEnabledKey'),
1235       itemKeyEquivalentKey: this.get('itemKeyEquivalentKey'),
1236       itemCheckboxKey: 'isChecked',
1237       itemLayerIdKey: 'overflowLayerId',
1238 
1239       // We need to be able to update our overflow segment even if the user clicks outside of the menu.  Since
1240       // there is no callback method or observable property when the menu closes, override modalPaneDidClick().
1241       modalPaneDidClick: function () {
1242         sc_super();
1243 
1244         // Cleanup
1245         this.removeObserver('selectedItem', self, 'selectOverflowItem');
1246 
1247         self.activeChildView.set('isActive', NO);
1248         self.activeChildView = null;
1249       }
1250     });
1251 
1252     layer = this.get('layer');
1253     overflowElement = layer.childNodes[layer.childNodes.length - 1];
1254     menu.popup(overflowElement);
1255 
1256     menu.addObserver("selectedItem", this, 'selectOverflowItem');
1257   },
1258 
1259   /** @private
1260     Whenever the value changes, update the segments accordingly.
1261   */
1262   valueDidChange: function () {
1263     var value = this.get('value'),
1264         overflowItemsLength,
1265         childViews = this.get('childViews'),
1266         childViewsLength = this.get('shouldHandleOverflow') ? childViews.get('length') - 1 : childViews.get('length'),
1267         overflowIndex = Infinity,
1268         overflowView = this.get('overflowView'),
1269         childView;
1270 
1271     // The index where childViews are all overflowed
1272     if (this.overflowItems) {
1273       overflowItemsLength = this.overflowItems.get('length');
1274       overflowIndex = childViewsLength - overflowItemsLength;
1275 
1276       // Clear out the selected value of the overflowView (if it's set)
1277       overflowView.set('isSelected', NO);
1278     }
1279 
1280     for (var i = childViewsLength - 1; i >= 0; i--) {
1281       childView = childViews.objectAt(i);
1282       if (SC.isArray(value) ? value.indexOf(childView.get('value')) >= 0 : value === childView.get('value')) {
1283         childView.set('isSelected', YES);
1284 
1285         // If we've gone over the overflow index, the child view is represented in overflow items
1286         if (i >= overflowIndex) overflowView.set('isSelected', YES);
1287       } else {
1288         childView.set('isSelected', NO);
1289       }
1290     }
1291   }.observes('value')
1292 
1293 });
1294