1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2010 Sprout Systems, 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('panes/picker');
  9 sc_require('views/menu_item');
 10 
 11 /**
 12   @class
 13 
 14   `SC.MenuPane` allows you to display a standard menu. Menus appear over other
 15   panes, and block input to other views until a selection is made or the pane
 16   is dismissed by clicking outside of its bounds.
 17 
 18   You can create a menu pane and manage it yourself, or you can use the
 19   `SC.SelectButtonView` and `SC.PopupButtonView` controls to manage the menu for
 20   you.
 21 
 22   ## Specifying Menu Items
 23 
 24   The menu pane examines the `items` property to determine what menu items
 25   should be presented to the user.
 26 
 27   In its most simple form, you can provide an array of strings. Every item
 28   will be converted into a menu item whose title is the string.
 29 
 30   If you need more control over the menu items, such as specifying a keyboard
 31   shortcut, enabled state, custom height, or submenu, you can provide an array
 32   of content objects.
 33 
 34   Out of the box, the menu pane has some default keys it uses to get
 35   information from the objects. For example, to find out the title of the menu
 36   item, the menu pane will ask your object for its `title` property. If you
 37   need to change this key, you can set the `itemTitleKey` property on the pane
 38   itself.
 39 
 40       var menuItems = [
 41         { title: 'Menu Item', keyEquivalent: 'ctrl_shift_n' },
 42         { title: 'Checked Menu Item', checkbox: YES, keyEquivalent: 'ctrl_a' },
 43         { title: 'Selected Menu Item', keyEquivalent: ['backspace', 'delete'] },
 44         { isSeparator: YES },
 45         { title: 'Menu Item with Icon', icon: 'inbox', keyEquivalent: 'ctrl_m' },
 46         { title: 'Menu Item with Icon', icon: 'folder', keyEquivalent: 'ctrl_p' }
 47       ];
 48 
 49       var menu = SC.MenuPane.create({
 50         items: menuItems
 51       });
 52 
 53   ## Observing User Selections
 54 
 55   To determine when a user clicks on a menu item, you can observe the
 56   `selectedItem` property for changes.
 57 
 58   @extends SC.PickerPane
 59   @since SproutCore 1.0
 60 */
 61 SC.MenuPane = SC.PickerPane.extend(
 62 /** @scope SC.MenuPane.prototype */ {
 63 
 64   /** @private Cache of the items array, used for clean up of observers. */
 65   _sc_menu_items: null,
 66 
 67   /**
 68     @type Array
 69     @default ['sc-menu']
 70     @see SC.View#classNames
 71   */
 72   classNames: ['sc-menu'],
 73 
 74   /**
 75     The WAI-ARIA role for menu pane.
 76 
 77     @type String
 78     @default 'menu'
 79     @constant
 80   */
 81   ariaRole: 'menu',
 82 
 83 
 84   // ..........................................................
 85   // PROPERTIES
 86   //
 87 
 88   /**
 89     The array of items to display. This can be a simple array of strings,
 90     objects or hashes. If you pass objects or hashes, you can also set the
 91     various itemKey properties to tell the menu how to extract the information
 92     it needs.
 93 
 94     @type Array
 95     @default []
 96   */
 97   items: null,
 98 
 99   /**
100     The size of the menu. This will set a CSS style on the menu that can be
101     used by the current theme to style the appearance of the control. This
102     value will also determine the default `itemHeight`, `itemSeparatorHeight`,
103     `menuHeightPadding`, and `submenuOffsetX` if you don't explicitly set these
104     properties.
105 
106     Your theme can override the default values for each control size by specifying
107     them in the `menuRenderDelegate`. For example:
108 
109         MyTheme.menuRenderDelegate = SC.BaseTheme.menuRenderDelegate.create({
110           'sc-tiny-size': {
111             itemHeight: 20,
112             itemSeparatorHeight: 9,
113             menuHeightPadding: 6,
114             submenuOffsetX: 2
115           }
116         });
117 
118     Changing the controlSize once the menu is instantiated has no effect.
119 
120     @type String
121     @default SC.REGULAR_CONTROL_SIZE
122   */
123   controlSize: SC.REGULAR_CONTROL_SIZE,
124 
125   /**
126     The height of each menu item, in pixels.
127 
128     You can override this on a per-item basis by setting the (by default)
129     `height` property on your object.
130 
131     If you don't specify a value, the item height will be inferred from
132     `controlSize`.
133 
134     @type Number
135     @default itemHeight from theme if present, or 20.
136   */
137   itemHeight: SC.propertyFromRenderDelegate('itemHeight', 20),
138 
139   /**
140     The height of separator menu items.
141 
142     You can override this on a per-item basis by setting the (by default)
143     `height` property on your object.
144 
145     If you don't specify a value, the height of the separator menu items will
146     be inferred from `controlSize`.
147 
148     @type Number
149     @default itemSeparatorHeight from theme, or 9.
150   */
151   itemSeparatorHeight: SC.propertyFromRenderDelegate('itemSeparatorHeight', 9),
152 
153   /**
154     The height of the menu pane. This is updated every time menuItemViews
155     is recalculated.
156 
157     @type Number
158     @default 0
159     @isReadOnly
160   */
161   menuHeight: 0,
162 
163   /**
164     The amount of padding to add to the height of the pane.
165 
166     The first menu item is offset by half this amount, and the other half is
167     added to the height of the menu, such that a space between the top and the
168     bottom is created.
169 
170     If you don't specify a value, the padding will be inferred from the
171     controlSize.
172 
173     @type Number
174     @default menuHeightPadding from theme, or 6
175   */
176   menuHeightPadding: SC.propertyFromRenderDelegate('menuHeightPadding', 6),
177 
178   /**
179     The amount of offset x while positioning submenu.
180 
181     If you don't specify a value, the padding will be inferred from the
182     controlSize.
183 
184     @type Number
185     @default submenuOffsetX from theme, or 2
186   */
187   submenuOffsetX: SC.propertyFromRenderDelegate('submenuOffsetX', 2),
188 
189   /**
190     The last menu item to be selected by the user.
191 
192     You can place an observer on this property to be notified when the user
193     makes a selection.
194 
195     @type SC.Object
196     @default null
197     @isReadOnly
198   */
199   selectedItem: null,
200 
201   /**
202     The view class to use when creating new menu item views.
203 
204     The menu pane will automatically create an instance of the view class you
205     set here for each item in the `items` array. You may provide your own
206     subclass for this property to display the customized content.
207 
208     @type SC.View
209     @default SC.MenuItemView
210   */
211   exampleView: SC.MenuItemView,
212 
213   /**
214     The view or element to which the menu will anchor itself.
215 
216     When the menu pane is shown, it will remain anchored to the view you
217     specify, even if the window is resized. You should specify the anchor as a
218     parameter when calling `popup()`, rather than setting it directly.
219 
220     @type SC.View
221     @isReadOnly
222   */
223   anchor: null,
224 
225   /**
226     `YES` if this menu pane was generated by a parent `SC.MenuPane`.
227 
228     @type Boolean
229     @default NO
230     @isReadOnly
231   */
232   isSubMenu: NO,
233 
234   /**
235     If true, title of menu items will be escaped to avoid scripting attacks.
236 
237     @type Boolean
238     @default YES
239   */
240   escapeHTML: YES,
241 
242   /**
243     Whether the title of menu items should be localized before display.
244 
245     @type Boolean
246     @default YES
247   */
248   localize: YES,
249 
250   /**
251     Whether or not this menu pane should accept the “current menu pane”
252     designation when visible, which is the highest-priority pane when routing
253     events.  Generally you want this set to `YES` so that your menu pane can
254     intercept keyboard events.
255 
256     @type Boolean
257     @default YES
258   */
259   acceptsMenuPane: YES,
260 
261   /**
262     Disable context menu.
263 
264     @type Boolean
265     @default NO
266   */
267   isContextMenuEnabled: NO,
268 
269 
270   // ..........................................................
271   // METHODS
272   //
273 
274   /**
275     Makes the menu visible and adds it to the HTML document.
276 
277     If you provide a view or element as the first parameter, the menu will
278     anchor itself to the view, and intelligently reposition itself if the
279     contents of the menu exceed the available space.
280 
281     @param {SC.View} anchorViewOrElement the view or element to which the menu
282     should anchor.
283     @param {Array} (preferMatrix) The prefer matrix used to position the pane.
284   */
285   popup: function (anchorViewOrElement, preferMatrix) {
286     this.beginPropertyChanges();
287     if (anchorViewOrElement) {
288       if (anchorViewOrElement.isView) {
289         this._anchorView = anchorViewOrElement;
290         this._setupScrollObservers(anchorViewOrElement);
291       } else {
292         this._anchorHTMLElement = anchorViewOrElement;
293       }
294     }
295    // this.set('anchor',anchorViewOrElement);
296     if (preferMatrix) this.set('preferMatrix', preferMatrix);
297 
298     // Resize the pane's initial height to fit the height of the menu.
299     // Note: SC.PickerPane's positioning code may adjust the height to fit within the window.
300     this.adjust('height', this.get('menuHeight'));
301     this.positionPane();
302 
303     // Because panes themselves do not receive key events, we need to set the
304     // pane's defaultResponder to itself. This way key events can be
305     // interpreted in keyUp.
306     this.set('defaultResponder', this);
307     this.endPropertyChanges();
308 
309     // Prevent body overflow (we don't want to overflow because of shadows).
310     SC.bodyOverflowArbitrator.requestHidden(this, true);
311 
312     //@if(debug)
313     // A debug-mode only flag to indicate that the popup method was called (see override of append in SC.PickerPane).
314     this._sc_didUsePopup = true;
315     //@endif
316 
317     this.append();
318   },
319 
320   // ..........................................................
321   // ITEM KEYS
322   //
323 
324   /**
325     The name of the property that contains the title for each item.
326 
327     @type String
328     @default "title"
329     @commonTask Menu Item Properties
330   */
331   itemTitleKey: 'title',
332 
333   /**
334     The name of the property that contains the value for each item.
335 
336     @type String
337     @default "value"
338     @commonTask Menu Item Properties
339   */
340   itemValueKey: 'value',
341 
342   /**
343     The name of the property that contains the tooltip for each item.
344 
345     @type String
346     @default "toolTip"
347     @commonTask Menu Item Properties
348   */
349   itemToolTipKey: 'toolTip',
350 
351   /**
352     The name of the property that determines whether the item is enabled.
353 
354     @type String
355     @default "isEnabled"
356     @commonTask Menu Item Properties
357   */
358   itemIsEnabledKey: 'isEnabled',
359 
360   /**
361     The name of the property that contains the icon for each item.
362 
363     @type String
364     @default "icon"
365     @commonTask Menu Item Properties
366   */
367   itemIconKey: 'icon',
368 
369   /**
370     The name of the property that contains the height for each item.
371 
372     @readOnly
373     @type String
374     @default "height"
375     @commonTask Menu Item Properties
376   */
377   itemHeightKey: 'height',
378 
379   /**
380     The name of the property that contains an optional submenu for each item.
381 
382     @type String
383     @default "subMenu"
384     @commonTask Menu Item Properties
385   */
386   itemSubMenuKey: 'subMenu',
387 
388   /**
389     The name of the property that determines whether the item is a menu
390     separator.
391 
392     @type String
393     @default "isSeparator"
394     @commonTask Menu Item Properties
395   */
396   itemSeparatorKey: 'isSeparator',
397 
398   /**
399     The name of the property that contains the target for the action that is
400     triggered when the user clicks the menu item.
401 
402     Note that this property is ignored if the menu item has a submenu.
403 
404     @type String
405     @default "target"
406     @commonTask Menu Item Properties
407   */
408   itemTargetKey: 'target',
409 
410   /**
411     The name of the property that contains the action that is triggered when
412     the user clicks the menu item.
413 
414     Note that this property is ignored if the menu item has a submenu.
415 
416     @type String
417     @default "action"
418     @commonTask Menu Item Properties
419   */
420   itemActionKey: 'action',
421 
422   /**
423     The name of the property that determines whether the menu item should
424     display a checkbox.
425 
426     @type String
427     @default "checkbox"
428     @commonTask Menu Item Properties
429   */
430   itemCheckboxKey: 'checkbox',
431 
432   /**
433     The name of the property that contains the shortcut to be displayed.
434 
435     The shortcut should communicate the keyboard equivalent to the user.
436 
437     @type String
438     @default "shortcut"
439     @commonTask Menu Item Properties
440   */
441   itemShortCutKey: 'shortcut',
442 
443   /**
444     The name of the property that contains the key equivalent of the menu
445     item.
446 
447     The action of the menu item will be fired, and the menu pane's
448     `selectedItem` property set to the menu item, if the user presses this
449     key combination on the keyboard.
450 
451     @type String
452     @default "keyEquivalent"
453     @commonTask Menu Item Properties
454   */
455   itemKeyEquivalentKey: 'keyEquivalent',
456 
457   /**
458     The name of the property that determines whether menu flash should be
459     disabled.
460 
461     When you click on a menu item, it will flash several times to indicate
462     selection to the user. Some browsers block windows from opening outside of
463     a mouse event, so you may wish to disable menu flashing if the action of
464     the menu item should open a new window.
465 
466     @type String
467     @default "disableMenuFlash"
468     @commonTask Menu Item Properties
469   */
470   itemDisableMenuFlashKey: 'disableMenuFlash',
471 
472   /**
473     The name of the property that determines whether layerID should be applied to the item .
474 
475     @type String
476     @default "layerId"
477     @commonTask Menu Item Properties
478   */
479   itemLayerIdKey: 'layerId',
480 
481   /**
482     The name of the property that determines whether a unique exampleView should be created for the item .
483 
484     @type String
485     @default "exampleView"
486     @commonTask Menu Item Properties
487   */
488   itemExampleViewKey: 'exampleView',
489 
490   /**
491     The array of keys used by SC.MenuItemView when inspecting your menu items
492     for display properties.
493 
494     @private
495     @isReadOnly
496     @type Array
497   */
498   menuItemKeys: ['itemTitleKey', 'itemValueKey', 'itemToolTipKey', 'itemIsEnabledKey', 'itemIconKey', 'itemSeparatorKey', 'itemActionKey', 'itemCheckboxKey', 'itemShortCutKey', 'itemHeightKey', 'itemSubMenuKey', 'itemKeyEquivalentKey', 'itemTargetKey', 'itemLayerIdKey'],
499 
500   // ..........................................................
501   // DEFAULT PROPERTIES
502   //
503 
504   /*
505     If an item doesn't specify a target, this is used. (Only used if an action is found and is not a function.)
506 
507     @type String
508     @default null
509   */
510   target: null,
511 
512   /*
513     If an item doesn't specify an action, this is used.
514 
515     @type String
516     @default null
517   */
518   action: null,
519 
520   // ..........................................................
521   // INTERNAL PROPERTIES
522   //
523 
524   /** @private */
525   preferType: SC.PICKER_MENU,
526 
527   // ..........................................................
528   // INTERNAL METHODS
529   //
530 
531   /** @private */
532   init: function () {
533     // Initialize the items array.
534     if (!this.items) { this.items = []; }
535 
536     // An associative array of the shortcut keys. The key is the shortcut in the
537     // form 'ctrl_z', and the value is the menu item of the action to trigger.
538     this._keyEquivalents = {};
539 
540     // Continue initializing now that default values exist.
541     sc_super();
542 
543     // Initialize the observer function once.
544     this._sc_menu_itemsDidChange();
545   },
546 
547   displayProperties: ['controlSize'],
548 
549   renderDelegateName: 'menuRenderDelegate',
550 
551   /**
552     Creates the child scroll view, and sets its `contentView` to a new
553     view.  This new view is saved and managed by the `SC.MenuPane`,
554     and contains the visible menu items.
555 
556     @private
557     @returns {SC.View} receiver
558   */
559   createChildViews: function () {
560     var scroll, menuView;
561 
562     // Create the menu items collection view.
563     // TODO: Should this not be an SC.ListView?
564     menuView = this._menuView = SC.View.create({
565       layout: { height: 0 }
566     });
567 
568     scroll = this._menuScrollView = this.createChildView(SC.MenuScrollView, {
569       controlSize: this.get('controlSize'),
570       contentView: menuView
571     });
572 
573     this.childViews = [scroll];
574 
575     return this;
576   },
577 
578   /**
579     Called when the pane is attached.  Takes on menu pane status.
580 
581     We don't call `sc_super()` here because `PanelPane` sets the current pane to
582     be the key pane when attached.
583   */
584   didAppendToDocument: function () {
585     this.becomeMenuPane();
586   },
587 
588   /**
589     Called when the pane is detached.  Closes all submenus and resigns menu pane
590     status.
591 
592     We don't call `sc_super()` here because `PanelPane` resigns key pane when
593     detached.
594   */
595   willRemoveFromDocument: function () {
596     var parentMenu = this.get('parentMenu');
597 
598     this.set('currentMenuItem', null);
599     this.closeOpenMenus();
600     this.resignMenuPane();
601 
602     if (parentMenu) {
603       parentMenu.becomeMenuPane();
604     }
605   },
606 
607   /**
608     Make the pane the menu pane. When you call this, all key events will
609     temporarily be routed to this pane. Make sure that you call
610     resignMenuPane; otherwise all key events will be blocked to other panes.
611 
612     @returns {SC.Pane} receiver
613   */
614   becomeMenuPane: function () {
615     if (this.rootResponder) this.rootResponder.makeMenuPane(this);
616 
617     return this;
618   },
619 
620   /**
621     Remove the menu pane status from the pane.  This will simply set the
622     `menuPane` on the `rootResponder` to `null.
623 
624     @returns {SC.Pane} receiver
625   */
626   resignMenuPane: function () {
627     if (this.rootResponder) this.rootResponder.makeMenuPane(null);
628 
629     return this;
630   },
631 
632   /**
633     The array of child menu item views that compose the menu.
634 
635     This computed property parses `displayItems` and constructs an
636     `SC.MenuItemView` (or whatever class you have set as the `exampleView`) for every item.
637 
638     This calls createMenuItemViews. If you want to override this property, override
639     that method.
640 
641     @type
642     @type Array
643     @readOnly
644   */
645   menuItemViews: function () {
646     return this.createMenuItemViews();
647   }.property('displayItems').cacheable(),
648 
649   /**
650     Processes the displayItems and creates menu item views for each item.
651 
652     Override this method to change how menuItemViews is calculated.
653 
654     @return Array
655   */
656   createMenuItemViews: function () {
657     var views = [], items = this.get('displayItems'),
658         exampleView = this.get('exampleView'), item, itemView, view,
659         exampleViewKey, itemExampleView,
660         height, heightKey, separatorKey, defaultHeight, separatorHeight,
661         menuHeight, menuHeightPadding, keyEquivalentKey, keyEquivalent,
662         keyArray, idx, layerIdKey, propertiesHash, escapeHTML,
663         len;
664 
665     if (!items) return views; // return an empty array
666 
667     heightKey = this.get('itemHeightKey');
668     separatorKey = this.get('itemSeparatorKey');
669     exampleViewKey = this.get('itemExampleViewKey');
670     defaultHeight = this.get('itemHeight');
671     keyEquivalentKey = this.get('itemKeyEquivalentKey');
672     separatorHeight = this.get('itemSeparatorHeight');
673     layerIdKey = this.get('itemLayerIdKey');
674     escapeHTML = this.get('escapeHTML');
675     menuHeightPadding = Math.floor(this.get('menuHeightPadding') / 2);
676     menuHeight = menuHeightPadding;
677 
678     keyArray = this.menuItemKeys.map(SC._menu_fetchKeys, this);
679 
680     len = items.get('length');
681     for (idx = 0; idx < len; idx++) {
682       item = items[idx];
683       height = item.get(heightKey);
684       if (!height) {
685         height = item.get(separatorKey) ? separatorHeight : defaultHeight;
686       }
687 
688       propertiesHash = {
689         layout: { height: height, top: menuHeight },
690         contentDisplayProperties: keyArray,
691         content: item,
692         parentMenu: this,
693         escapeHTML: escapeHTML
694       };
695 
696       if (item.get(layerIdKey)) {
697         propertiesHash.layerId = item.get(layerIdKey);
698       }
699 
700       // Item has its own exampleView so use it
701       itemExampleView = item.get(exampleViewKey);
702       if (itemExampleView) {
703         itemView = itemExampleView;
704       } else {
705         itemView = exampleView;
706       }
707 
708       view = this._menuView.createChildView(itemView, propertiesHash);
709       views[idx] = view;
710       menuHeight += height;
711       keyEquivalent = item.get(keyEquivalentKey);
712       if (keyEquivalent) {
713         // if array, apply each one for this view
714         if (SC.typeOf(keyEquivalent) === SC.T_ARRAY) {
715           keyEquivalent.forEach(function (keyEq) {
716             this._keyEquivalents[keyEq] = view;
717           }, this);
718         } else {
719           this._keyEquivalents[keyEquivalent] = view;
720         }
721       }
722     }
723 
724     this.set('menuHeight', menuHeight + menuHeightPadding);
725     return views;
726   },
727 
728   /**
729     Returns the menu item view for the content object at the specified index.
730 
731     @param Number idx the item index
732     @returns {SC.MenuItemView} instantiated view
733   */
734   menuItemViewForContentIndex: function (idx) {
735     var menuItemViews = this.get('menuItemViews');
736 
737     if (!menuItemViews) return undefined;
738     return menuItemViews.objectAt(idx);
739   },
740 
741   /**
742     If this is a submenu, this property corresponds to the
743     top-most parent menu. If this is the root menu, it returns
744     itself.
745 
746     @type SC.MenuPane
747     @isReadOnly
748     @type
749   */
750   rootMenu: function () {
751     if (this.get('isSubMenu')) return this.getPath('parentMenu.rootMenu');
752     return this;
753   }.property('isSubMenu').cacheable(),
754 
755   /** @private @see SC.Object */
756   destroy: function () {
757     var ret = sc_super();
758 
759     // Clean up previous enumerable observer.
760     if (this._sc_menu_items) {
761       this._sc_menu_items.removeObserver('[]', this, '_sc_menu_itemPropertiesDidChange');
762     }
763 
764     // Destroy the menu view we created.  The scroll view's container will NOT
765     // destroy this because it receives it already instantiated.
766     this._menuView.destroy();
767 
768     // Clean up caches.
769     this._sc_menu_items = null;
770     this._menuView = null;
771 
772     return ret;
773   },
774 
775   /**
776     Close the menu if the user resizes the window.
777 
778     @private
779   */
780   windowSizeDidChange: function () {
781     this.remove();
782     return sc_super();
783   },
784 
785   /**
786     Returns an array of normalized display items.
787 
788     Because the items property can be provided as either an array of strings,
789     or an object with key-value pairs, or an exotic mish-mash of both, we need
790     to normalize it for our display logic.
791 
792     If an `items` member is an object, we can assume it is formatted properly
793     and leave it as-is.
794 
795     If an `items` member is a string, we create a hash with the title value
796     set to that string, and some sensible defaults for the other properties.
797 
798     A side effect of running this computed property is that the menuHeight
799     property is updated.
800 
801     `displayItems` should never be set directly; instead, set `items` and
802     `displayItems` will update automatically.
803 
804     @type
805     @type Array
806     @isReadOnly
807   */
808   displayItems: function () {
809     var items = this.get('items'),
810       len,
811       ret = [], idx, item, itemType;
812 
813     if (!items) return null;
814 
815     len = items.get('length');
816 
817     // Loop through the items property and transmute as needed, then
818     // copy the new objects into the ret array.
819     for (idx = 0; idx < len; idx++) {
820       item = items.objectAt(idx);
821 
822       // fast track out if we can't do anything with this item
823       if (!item || (!ret.length && item[this.get('itemSeparatorKey')])) continue;
824 
825       itemType = SC.typeOf(item);
826       if (itemType === SC.T_STRING) {
827         item = SC.Object.create({ title: item,
828                                   value: item,
829                                   isEnabled: YES
830                                });
831       } else if (itemType === SC.T_HASH) {
832         item = SC.Object.create(item);
833       }
834       item.contentIndex = idx;
835 
836       ret.push(item);
837     }
838 
839     return ret;
840   }.property('items').cacheable(),
841 
842   /** @private */
843   _sc_menu_itemsDidChange: function () {
844     var items = this.get('items');
845 
846     // Clean up previous enumerable observer.
847     if (this._sc_menu_items) {
848       this._sc_menu_items.removeObserver('[]', this, '_sc_menu_itemPropertiesDidChange');
849     }
850 
851     // Add new enumerable observer
852     if (items) {
853       items.addObserver('[]', this, '_sc_menu_itemPropertiesDidChange');
854     }
855 
856     // Cache the last items.
857     this._sc_menu_items = items;
858 
859     var itemViews;
860     itemViews = this.get('menuItemViews');
861     this._menuView.replaceAllChildren(itemViews);
862     this._menuView.adjust('height', this.get('menuHeight'));
863   }.observes('items'),
864 
865   /** @private */
866   _sc_menu_itemPropertiesDidChange: function () {
867     // Indicate that the displayItems changed.
868     this.notifyPropertyChange('displayItems');
869 
870     var itemViews;
871     itemViews = this.get('menuItemViews');
872     this._menuView.replaceAllChildren(itemViews);
873     this._menuView.adjust('height', this.get('menuHeight'));
874   },
875 
876   currentMenuItem: function (key, value) {
877     if (value !== undefined) {
878       if (this._currentMenuItem !== null) {
879         this.set('previousMenuItem', this._currentMenuItem);
880       }
881       this._currentMenuItem = value;
882       this.setPath('rootMenu.targetMenuItem', value);
883 
884       return value;
885     }
886 
887     return this._currentMenuItem;
888   }.property().cacheable(),
889 
890   /** @private */
891   _sc_menu_currentMenuItemDidChange: function () {
892     var currentMenuItem = this.get('currentMenuItem'),
893         previousMenuItem = this.get('previousMenuItem');
894 
895     if (previousMenuItem) {
896       if (previousMenuItem.get('hasSubMenu') && currentMenuItem === null) {
897 
898       } else {
899         previousMenuItem.resignFirstResponder();
900         this.closeOpenMenusFor(previousMenuItem);
901       }
902     }
903 
904     // Scroll to the selected menu item if it's not visible on screen.
905     // This is useful for keyboard navigation and programmatically selecting
906     // the selected menu item, as in `SelectButtonView`.
907     if (currentMenuItem && currentMenuItem.get('isEnabled')) {
908       currentMenuItem.scrollToVisible();
909     }
910   }.observes('currentMenuItem'),
911 
912   closeOpenMenusFor: function (menuItem) {
913     if (!menuItem) return;
914 
915     var menu = menuItem.get('parentMenu');
916 
917     // Close any open menus if a root menu changes
918     while (menu && menuItem) {
919       menu = menuItem.get('subMenu');
920       if (menu) {
921         menu.remove();
922         menuItem.resignFirstResponder();
923         menuItem = menu.get('previousMenuItem');
924       }
925     }
926   },
927 
928   closeOpenMenus: function () {
929     this.closeOpenMenusFor(this.get('previousMenuItem'));
930   },
931 
932   //Mouse and Key Events
933 
934   /** @private */
935   mouseDown: function (evt) {
936     this.modalPaneDidClick(evt);
937     return YES;
938   },
939 
940   /** @private
941     Note when the mouse has entered, so that if this is a submenu,
942     the menu item to which it belongs knows whether to maintain its highlight
943     or not.
944 
945     @param {Event} evt
946   */
947   mouseEntered: function () {
948     this.set('mouseHasEntered', YES);
949   },
950 
951   keyUp: function (evt) {
952     var ret = this.interpretKeyEvents(evt);
953     return !ret ? NO : ret;
954   },
955 
956   /**
957     Selects the next enabled menu item above the currently
958     selected menu item when the up-arrow key is pressed.
959 
960     @private
961   */
962   moveUp: function () {
963     var currentMenuItem = this.get('currentMenuItem'),
964         items = this.get('menuItemViews'),
965         currentIndex, idx;
966 
967     if (!currentMenuItem) {
968       idx = items.get('length') - 1;
969     } else {
970       currentIndex = currentMenuItem.getPath('content.contentIndex');
971       if (currentIndex === 0) return YES;
972       idx = currentIndex - 1;
973     }
974 
975     while (idx >= 0) {
976       if (items[idx].get('isEnabled')) {
977         this.set('currentMenuItem', items[idx]);
978         items[idx].becomeFirstResponder();
979         break;
980       }
981       idx--;
982     }
983 
984     return YES;
985   },
986 
987   /**
988     Selects the next enabled menu item below the currently
989     selected menu item when the down-arrow key is pressed.
990 
991     @private
992   */
993   moveDown: function () {
994     var currentMenuItem = this.get('currentMenuItem'),
995         items = this.get('menuItemViews'),
996         len = items.get('length'),
997         currentIndex, idx;
998 
999     if (!currentMenuItem) {
1000       idx = 0;
1001     } else {
1002       currentIndex = currentMenuItem.getPath('content.contentIndex');
1003       if (currentIndex === len) return YES;
1004       idx = currentIndex + 1;
1005     }
1006 
1007     while (idx < len) {
1008       if (items[idx].get('isEnabled')) {
1009         this.set('currentMenuItem', items[idx]);
1010         items[idx].becomeFirstResponder();
1011         break;
1012       }
1013       idx++;
1014     }
1015 
1016     return YES;
1017   },
1018 
1019   /**
1020     Selects the first enabled menu item when the home key is pressed.
1021 
1022     @private
1023    */
1024   moveToBeginningOfDocument: function () {
1025     var items = this.get('menuItemViews'),
1026         len = items.get('length'),
1027         idx = 0;
1028 
1029     while (idx < len) {
1030       if (items[idx].get('isEnabled')) {
1031         this.set('currentMenuItem', items[idx]);
1032         items[idx].becomeFirstResponder();
1033         break;
1034       }
1035       ++idx;
1036     }
1037 
1038     return YES;
1039   },
1040 
1041   /**
1042     Selects the last enabled menu item when the end key is pressed.
1043 
1044     @private
1045   */
1046   moveToEndOfDocument: function () {
1047     var items = this.get('menuItemViews'),
1048         len = items.get('length'),
1049         idx = len - 1;
1050 
1051     while (idx >= 0) {
1052       if (items[idx].get('isEnabled')) {
1053         this.set('currentMenuItem', items[idx]);
1054         items[idx].becomeFirstResponder();
1055         break;
1056       }
1057       --idx;
1058     }
1059 
1060     return YES;
1061   },
1062 
1063   /**
1064     Selects the next item one page down. If that is not enabled,
1065     continues to move down until it finds an enabled item.
1066 
1067     @private
1068   */
1069   pageDown: function () {
1070     var currentMenuItem = this.get('currentMenuItem'),
1071         item, items = this.get('menuItemViews'),
1072         len = items.get('length'),
1073         idx = 0,
1074         foundItemIdx,
1075         verticalOffset = 0,
1076         verticalPageScroll;
1077 
1078     if (this._menuScrollView && this._menuScrollView.bottomScrollerView) {
1079 
1080       if (currentMenuItem) {
1081         idx = currentMenuItem.getPath('content.contentIndex');
1082       }
1083 
1084       foundItemIdx = idx;
1085       verticalPageScroll = this._menuScrollView.get('verticalPageScroll');
1086       for (idx; idx < len; idx++) {
1087         item = items[idx];
1088         verticalOffset += item.get('layout').height;
1089 
1090         if (verticalOffset > verticalPageScroll) {
1091           // We've gone further than an entire page scroll, so stop.
1092           break;
1093         } else {
1094           // Only accept enabled items (which also excludes separators).
1095           if (item.get('isEnabled')) { foundItemIdx = idx; }
1096         }
1097       }
1098 
1099       item = items[foundItemIdx];
1100       this.set('currentMenuItem', item);
1101       item.becomeFirstResponder();
1102     } else {
1103       this.moveToEndOfDocument();
1104     }
1105 
1106     return YES;
1107   },
1108 
1109   /**
1110     Selects the previous item one page up. If that is not enabled,
1111     continues to move up until it finds an enabled item.
1112 
1113     @private
1114   */
1115   pageUp: function () {
1116     var currentMenuItem = this.get('currentMenuItem'),
1117         item, items = this.get('menuItemViews'),
1118         len = items.get('length'),
1119         idx = len - 1,
1120         foundItemIdx,
1121         verticalOffset = 0,
1122         verticalPageScroll;
1123 
1124     if (this._menuScrollView && this._menuScrollView.topScrollerView) {
1125 
1126       if (currentMenuItem) {
1127         idx = currentMenuItem.getPath('content.contentIndex');
1128       }
1129 
1130       foundItemIdx = idx;
1131       verticalPageScroll = this._menuScrollView.get('verticalPageScroll');
1132       for (idx; idx >= 0; idx--) {
1133         item = items[idx];
1134         verticalOffset += item.get('layout').height;
1135 
1136         if (verticalOffset > verticalPageScroll) {
1137           // We've gone further than an entire page scroll, so stop.
1138           break;
1139         } else {
1140           // Only accept enabled items (which also excludes separators).
1141           if (item.get('isEnabled')) { foundItemIdx = idx; }
1142         }
1143       }
1144 
1145       item = items[foundItemIdx];
1146       this.set('currentMenuItem', item);
1147       item.becomeFirstResponder();
1148     } else {
1149       this.moveToBeginningOfDocument();
1150     }
1151 
1152     return YES;
1153   },
1154 
1155   insertText: function (chr) {
1156     var timer = this._timer, keyBuffer = this._keyBuffer;
1157 
1158     if (timer) {
1159       timer.invalidate();
1160     }
1161     timer = this._timer = SC.Timer.schedule({
1162       target: this,
1163       action: 'clearKeyBuffer',
1164       interval: 500,
1165       isPooled: NO
1166     });
1167 
1168     keyBuffer = keyBuffer || '';
1169     keyBuffer += chr.toUpperCase();
1170 
1171     this.selectMenuItemForString(keyBuffer);
1172     this._keyBuffer = keyBuffer;
1173 
1174     return YES;
1175   },
1176 
1177   /** @private
1178     Called by the view hierarchy when the menu should respond to a shortcut
1179     key being pressed.
1180 
1181     Normally, the menu will only respond to key presses when it is visible.
1182     However, when the menu is part of another control, such as an
1183     SC.PopupButtonView, the menu should still respond if it is hidden but its
1184     parent control is visible. In those cases, the parameter
1185     fromVisibleControl will be set to `YES`.
1186 
1187     @param keyEquivalent {String} the shortcut key sequence that was pressed
1188     @param fromVisibleControl Boolean if the shortcut key press was proxied
1189     to this menu by a visible parent control
1190     @returns Boolean
1191   */
1192   performKeyEquivalent: function (keyEquivalent, evt, fromVisibleControl) {
1193     //If menu is not visible
1194     if (!fromVisibleControl && !this.get('isVisibleInWindow')) return NO;
1195 
1196     // Look for menu item that has this key equivalent
1197     var menuItem = this._keyEquivalents[keyEquivalent];
1198 
1199     // If found, have it perform its action
1200     if (menuItem) {
1201       menuItem.performAction(YES);
1202       return YES;
1203     }
1204 
1205     // If escape key or the enter key was pressed and no menu item handled it,
1206     // close the menu pane and return YES that the event was handled
1207     if (keyEquivalent === 'escape' || keyEquivalent === 'return') {
1208       this.remove();
1209       return YES;
1210     }
1211 
1212     return NO;
1213 
1214   },
1215 
1216   selectMenuItemForString: function (buffer) {
1217     var items = this.get('menuItemViews'), item, title, idx, len, bufferLength;
1218     if (!items) return;
1219 
1220     bufferLength = buffer.length;
1221     len = items.get('length');
1222     for (idx = 0; idx < len; idx++) {
1223       item = items.objectAt(idx);
1224       title = item.get('title');
1225 
1226       if (!title) continue;
1227 
1228       title = title.replace(/ /g, '').substr(0, bufferLength).toUpperCase();
1229       if (title === buffer) {
1230         this.set('currentMenuItem', item);
1231         item.becomeFirstResponder();
1232         break;
1233       }
1234     }
1235   },
1236 
1237   /**
1238     Clear the key buffer if the user does not enter any text after a certain
1239     amount of time.
1240 
1241     This is called by the timer created in the `insertText` method.
1242 
1243     @private
1244   */
1245   clearKeyBuffer: function () {
1246     this._keyBuffer = '';
1247   },
1248 
1249   /**
1250     Close the menu and any open submenus if the user clicks outside the menu.
1251 
1252     Because only the root-most menu has a modal pane, this will only ever get
1253     called once.
1254 
1255     @returns Boolean
1256     @private
1257   */
1258   modalPaneDidClick: function () {
1259     this.remove();
1260 
1261     return YES;
1262   }
1263 });
1264 
1265 
1266 /** @private */
1267 SC._menu_fetchKeys = function (k) {
1268   return this.get(k);
1269 };
1270 
1271 /** @private */
1272 SC._menu_fetchItem = function (k) {
1273   if (!k) return null;
1274   return this.get ? this.get(k) : this[k];
1275 };
1276 
1277 
1278 /**
1279   Default metrics for the different control sizes.
1280 */
1281 SC.MenuPane.TINY_MENU_ITEM_HEIGHT = 10;
1282 // SC.MenuPane.TINY_MENU_ITEM_SEPARATOR_HEIGHT = 2;
1283 // SC.MenuPane.TINY_MENU_HEIGHT_PADDING = 2;
1284 // SC.MenuPane.TINY_SUBMENU_OFFSET_X = 0;
1285 
1286 SC.MenuPane.SMALL_MENU_ITEM_HEIGHT = 16;
1287 // SC.MenuPane.SMALL_MENU_ITEM_SEPARATOR_HEIGHT = 7;
1288 // SC.MenuPane.SMALL_MENU_HEIGHT_PADDING = 4;
1289 // SC.MenuPane.SMALL_SUBMENU_OFFSET_X = 2;
1290 
1291 SC.MenuPane.REGULAR_MENU_ITEM_HEIGHT = 22;
1292 // SC.MenuPane.REGULAR_MENU_ITEM_SEPARATOR_HEIGHT = 9;
1293 // SC.MenuPane.REGULAR_MENU_HEIGHT_PADDING = 6;
1294 // SC.MenuPane.REGULAR_SUBMENU_OFFSET_X = 2;
1295 
1296 SC.MenuPane.LARGE_MENU_ITEM_HEIGHT = 31;
1297 // SC.MenuPane.LARGE_MENU_ITEM_SEPARATOR_HEIGHT = 20;
1298 // SC.MenuPane.LARGE_MENU_HEIGHT_PADDING = 0;
1299 // SC.MenuPane.LARGE_SUBMENU_OFFSET_X = 4;
1300 
1301 SC.MenuPane.HUGE_MENU_ITEM_HEIGHT = 20;
1302 // SC.MenuPane.HUGE_MENU_ITEM_SEPARATOR_HEIGHT = 9;
1303 // SC.MenuPane.HUGE_MENU_HEIGHT_PADDING = 0;
1304 // SC.MenuPane.HUGE_SUBMENU_OFFSET_X = 0;
1305