API  0.9.7
 All Classes Files Functions Variables Macros Groups Pages
CPMenu.j
Go to the documentation of this file.
1 /*
2  * CPMenu.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 CPMenuDidAddItemNotification = @"CPMenuDidAddItemNotification";
28 CPMenuDidChangeItemNotification = @"CPMenuDidChangeItemNotification";
29 CPMenuDidRemoveItemNotification = @"CPMenuDidRemoveItemNotification";
30 
31 CPMenuDidEndTrackingNotification = @"CPMenuDidEndTrackingNotification";
32 
33 var _CPMenuBarVisible = NO,
34  _CPMenuBarTitle = @"",
35  _CPMenuBarAttributes = nil,
36  _CPMenuBarSharedWindow = nil;
37 
45 @implementation CPMenu : CPObject
46 {
47  CPMenu _supermenu;
48 
49  CPString _title;
50  CPString _name;
51 
52  CPFont _font;
53 
54  float _minimumWidth;
55 
56  CPMutableArray _items;
57 
58  BOOL _autoenablesItems;
59  BOOL _showsStateColumn;
60 
61  id _delegate;
62 
63  int _highlightedIndex;
64  _CPMenuWindow _menuWindow;
65 
66  CPEvent _lastCloseEvent;
67 }
68 
69 // Managing the Menu Bar
70 
71 + (void)initialize
72 {
73  if (self !== [CPMenu class])
74  return;
75 
76  [[self class] setMenuBarAttributes:@{}];
77 }
78 
79 + (BOOL)menuBarVisible
80 {
81  return _CPMenuBarVisible;
82 }
83 
84 + (void)setMenuBarVisible:(BOOL)menuBarShouldBeVisible
85 {
86  if (_CPMenuBarVisible === menuBarShouldBeVisible)
87  return;
88 
89  _CPMenuBarVisible = menuBarShouldBeVisible;
90 
91  if ([CPPlatform supportsNativeMainMenu])
92  return;
93 
94  if (menuBarShouldBeVisible)
95  {
96  if (!_CPMenuBarSharedWindow)
97  _CPMenuBarSharedWindow = [[_CPMenuBarWindow alloc] init];
98 
99  [_CPMenuBarSharedWindow setMenu:[CPApp mainMenu]];
100 
101  [_CPMenuBarSharedWindow setTitle:_CPMenuBarTitle];
102  [_CPMenuBarSharedWindow setIconImage:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-icon-image" forClass:_CPMenuView]];
103  [_CPMenuBarSharedWindow setIconImageAlphaValue:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-icon-image-alpha-value" forClass:_CPMenuView]];
104 
105  [_CPMenuBarSharedWindow setColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarBackgroundColor"]];
106  [_CPMenuBarSharedWindow setTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextColor"]];
107  [_CPMenuBarSharedWindow setTitleColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleColor"]];
108  [_CPMenuBarSharedWindow setTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextShadowColor"]];
109  [_CPMenuBarSharedWindow setTitleShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleShadowColor"]];
110  [_CPMenuBarSharedWindow setHighlightColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightColor"]];
111  [_CPMenuBarSharedWindow setHighlightTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextColor"]];
112  [_CPMenuBarSharedWindow setHighlightTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextShadowColor"]];
113 
114  [_CPMenuBarSharedWindow orderFront:self];
115  }
116  else
117  [_CPMenuBarSharedWindow orderOut:self];
118 
119 // FIXME: There must be a better way to do this.
120 #if PLATFORM(DOM)
121  [[CPPlatformWindow primaryPlatformWindow] resizeEvent:nil];
122 #endif
123 }
124 
125 + (void)setMenuBarTitle:(CPString)aTitle
126 {
127  _CPMenuBarTitle = aTitle;
128  [_CPMenuBarSharedWindow setTitle:_CPMenuBarTitle];
129 }
130 
131 + (CPString)menuBarTitle
132 {
133  return _CPMenuBarTitle;
134 }
135 
136 + (void)setMenuBarIconImage:(CPImage)anImage
137 {
138  _CPMenuBarImage = anImage;
139  [_CPMenuBarSharedWindow setIconImage:anImage];
140 }
141 
142 + (CPImage)menuBarIconImage
143 {
144  return _CPMenuBarImage;
145 }
146 
147 + (void)_setOrRemoveMenuBarAttribute:(id)aValue forKey:(id)aKey
148 {
149  if (aValue === nil)
150  [_CPMenuBarAttributes removeObjectForKey:aKey];
151  else
152  [_CPMenuBarAttributes setObject:aValue forKey:aKey];
153 }
154 
155 + (void)setMenuBarAttributes:(CPDictionary)attributes
156 {
157  if (_CPMenuBarAttributes == attributes)
158  return;
159 
160  _CPMenuBarAttributes = [attributes copy];
161 
162  var textColor = [attributes objectForKey:@"CPMenuBarTextColor"],
163  titleColor = [attributes objectForKey:@"CPMenuBarTitleColor"],
164  textShadowColor = [attributes objectForKey:@"CPMenuBarTextShadowColor"],
165  titleShadowColor = [attributes objectForKey:@"CPMenuBarTitleShadowColor"],
166  highlightColor = [attributes objectForKey:@"CPMenuBarHighlightColor"],
167  highlightTextColor = [attributes objectForKey:@"CPMenuBarHighlightTextColor"],
168  highlightTextShadowColor = [attributes objectForKey:@"CPMenuBarHighlightTextShadowColor"];
169 
170  if (!textColor && titleColor)
171  [_CPMenuBarAttributes setObject:titleColor forKey:@"CPMenuBarTextColor"];
172 
173  else if (textColor && !titleColor)
174  [_CPMenuBarAttributes setObject:textColor forKey:@"CPMenuBarTitleColor"];
175 
176  else if (!textColor && !titleColor)
177  {
178  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-text-color" forClass:_CPMenuView] forKey:@"CPMenuBarTextColor"];
179  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-title-color" forClass:_CPMenuView] forKey:@"CPMenuBarTitleColor"];
180  }
181 
182  if (!textShadowColor && titleShadowColor)
183  [_CPMenuBarAttributes setObject:titleShadowColor forKey:@"CPMenuBarTextShadowColor"];
184 
185  else if (textShadowColor && !titleShadowColor)
186  [_CPMenuBarAttributes setObject:textShadowColor forKey:@"CPMenuBarTitleShadowColor"];
187 
188  else if (!textShadowColor && !titleShadowColor)
189  {
190  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-text-shadow-color" forClass:_CPMenuView] forKey:@"CPMenuBarTextShadowColor"];
191  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-title-shadow-color" forClass:_CPMenuView] forKey:@"CPMenuBarTitleShadowColor"];
192  }
193 
194  if (!highlightColor)
195  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-highlight-color" forClass:_CPMenuView] forKey:@"CPMenuBarHighlightColor"];
196 
197  if (!highlightTextColor)
198  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-highlight-text-color" forClass:_CPMenuView] forKey:@"CPMenuBarHighlightTextColor"];
199 
200  if (!highlightTextShadowColor)
201  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-highlight-text-shadow-color" forClass:_CPMenuView] forKey:@"CPMenuBarHighlightTextShadowColor"];
202 
203  if (_CPMenuBarSharedWindow)
204  {
205  [_CPMenuBarSharedWindow setColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarBackgroundColor"]];
206  [_CPMenuBarSharedWindow setTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextColor"]];
207  [_CPMenuBarSharedWindow setTitleColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleColor"]];
208  [_CPMenuBarSharedWindow setTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextShadowColor"]];
209  [_CPMenuBarSharedWindow setTitleShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleShadowColor"]];
210  [_CPMenuBarSharedWindow setHighlightColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightColor"]];
211  [_CPMenuBarSharedWindow setHighlightTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextColor"]];
212  [_CPMenuBarSharedWindow setHighlightTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextShadowColor"]];
213  }
214 }
215 
216 + (CPDictionary)menuBarAttributes
217 {
218  return _CPMenuBarAttributes;
219 }
220 
221 + (void)_setMenuBarIconImageAlphaValue:(float)anAlphaValue
222 {
223  _CPMenuBarIconImageAlphaValue = anAlphaValue;
224  [_CPMenuBarSharedWindow setIconImageAlphaValue:anAlphaValue];
225 }
226 
227 - (float)menuBarHeight
228 {
229  if (self === [CPApp mainMenu])
230  return [CPMenu menuBarHeight];
231 
232  return 0.0;
233 }
234 
235 + (float)menuBarHeight
236 {
237  return [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-height" forClass:_CPMenuView];
238 }
239 
240 // Creating a CPMenu Object
246 - (id)initWithTitle:(CPString)aTitle
247 {
248  self = [super init];
249 
250  if (self)
251  {
252  _title = aTitle;
253  _items = [];
254 
255  _autoenablesItems = YES;
256  _showsStateColumn = YES;
257 
258  [self setMinimumWidth:0];
259  }
260 
261  return self;
262 }
263 
264 - (id)init
265 {
266  return [self initWithTitle:@""];
267 }
268 
269 // Setting Up Menu Commands
275 - (void)insertItem:(CPMenuItem)aMenuItem atIndex:(CPUInteger)anIndex
276 {
277  [self insertObject:aMenuItem inItemsAtIndex:anIndex];
278 }
279 
288 - (CPMenuItem)insertItemWithTitle:(CPString)aTitle action:(SEL)anAction keyEquivalent:(CPString)aKeyEquivalent atIndex:(CPUInteger)anIndex
289 {
290  var item = [[CPMenuItem alloc] initWithTitle:aTitle action:anAction keyEquivalent:aKeyEquivalent];
291 
292  [self insertItem:item atIndex:anIndex];
293 
294  return item;
295 }
296 
301 - (void)addItem:(CPMenuItem)aMenuItem
302 {
303  [self insertItem:aMenuItem atIndex:[_items count]];
304 }
305 
314 - (CPMenuItem)addItemWithTitle:(CPString)aTitle action:(SEL)anAction keyEquivalent:(CPString)aKeyEquivalent
315 {
316  return [self insertItemWithTitle:aTitle action:anAction keyEquivalent:aKeyEquivalent atIndex:[_items count]];
317 }
318 
323 - (void)removeItem:(CPMenuItem)aMenuItem
324 {
325  [self removeItemAtIndex:[_items indexOfObjectIdenticalTo:aMenuItem]];
326 }
327 
332 - (void)removeItemAtIndex:(CPUInteger)anIndex
333 {
334  [self removeObjectFromItemsAtIndex:anIndex];
335 }
336 
343 - (void)removeAllItems
344 {
345  var count = [_items count];
346 
347  // Remove the connection to this menu in case
348  // someone else has a reference to the menu item.
349  while (count--)
350  [_items[count] setMenu:nil];
351 
352  [self _highlightItemAtIndex:CPNotFound];
353 
354  // Because we are changing _items directly, be sure to notify KVO
355  [self willChangeValueForKey:@"items"];
356  _items = [CPMutableArray array];
357  [self didChangeValueForKey:@"items"];
358 }
359 
364 - (void)itemChanged:(CPMenuItem)aMenuItem
365 {
366  /*
367  During cib unarchiving, menu items will have a reference to their menu,
368  but of course the items are still being unarchived and the menu's _items
369  have not yet been instantiated. In that case we will not do anything here.
370  */
371  if ([aMenuItem menu] !== self || !_items)
372  return;
373 
374  [aMenuItem setValue:[aMenuItem valueForKey:@"changeCount"] + 1 forKey:@"changeCount"];
375 
377  postNotificationName:CPMenuDidChangeItemNotification
378  object:self
379  userInfo:@{ @"CPMenuItemIndex": [_items indexOfObjectIdenticalTo:aMenuItem] }];
380 }
381 
382 // Finding Menu Items
388 - (CPMenuItem)itemWithTag:(int)aTag
389 {
390  var index = [self indexOfItemWithTag:aTag];
391 
392  if (index == CPNotFound)
393  return nil;
394 
395  return _items[index];
396 }
397 
403 - (CPMenuItem)itemWithTitle:(CPString)aTitle
404 {
405  var index = [self indexOfItemWithTitle:aTitle];
406 
407  if (index == CPNotFound)
408  return nil;
409 
410  return _items[index];
411 }
412 
417 - (CPMenuItem)itemAtIndex:(int)anIndex
418 {
419  return [_items objectAtIndex:anIndex];
420 }
421 
425 - (unsigned)numberOfItems
426 {
427  return [_items count];
428 }
429 
433 - (CPArray)itemArray
434 {
435  return _items;
436 }
437 
438 // Finding Indices of Menu Items
444 - (int)indexOfItem:(CPMenuItem)aMenuItem
445 {
446  if ([aMenuItem menu] !== self)
447  return CPNotFound;
448 
449  return [_items indexOfObjectIdenticalTo:aMenuItem];
450 }
451 
457 - (int)indexOfItemWithTitle:(CPString)aTitle
458 {
459  var index = 0,
460  count = _items.length;
461 
462  for (; index < count; ++index)
463  if ([_items[index] title] === aTitle)
464  return index;
465 
466  return CPNotFound;
467 }
468 
474 - (int)indexOfItemWithTag:(int)aTag
475 {
476  var index = 0,
477  count = _items.length;
478 
479  for (; index < count; ++index)
480  if ([_items[index] tag] == aTag)
481  return index;
482 
483  return CPNotFound;
484 }
485 
492 - (int)indexOfItemWithTarget:(id)aTarget andAction:(SEL)anAction
493 {
494  var index = 0,
495  count = _items.length;
496 
497  for (; index < count; ++index)
498  {
499  var item = _items[index];
500 
501  if ([item target] == aTarget && (!anAction || [item action] == anAction))
502  return index;
503  }
504 
505  return CPNotFound;
506 }
507 
513 - (int)indexOfItemWithRepresentedObject:(id)anObject
514 {
515  var index = 0,
516  count = _items.length;
517 
518  for (; index < count; ++index)
519  if ([[_items[index] representedObject] isEqual:anObject])
520  return index;
521 
522  return CPNotFound;
523 }
524 
530 - (int)indexOfItemWithSubmenu:(CPMenu)aMenu
531 {
532  var index = 0,
533  count = _items.length;
534 
535  for (; index < count; ++index)
536  if ([_items[index] submenu] == aMenu)
537  return index;
538 
539  return CPNotFound;
540 }
541 
542 // Managing Submenus
548 - (void)setSubmenu:(CPMenu)aMenu forItem:(CPMenuItem)aMenuItem
549 {
550  [aMenuItem setTarget:aMenuItem];
551  [aMenuItem setAction:@selector(submenuAction:)];
552 
553  [aMenuItem setSubmenu:aMenu];
554 }
555 
562 - (void)submenuAction:(id)aSender
563 {
564 }
565 
569 - (CPMenu)supermenu
570 {
571  return _supermenu;
572 }
573 
578 - (void)setSupermenu:(CPMenu)aMenu
579 {
580  _supermenu = aMenu;
581 }
582 
587 - (BOOL)isTornOff
588 {
589  return !_supermenu /* || offscreen(?) */ || self == [CPApp mainMenu];
590 }
591 
592 // Enabling and Disabling Menu Items
597 - (void)setAutoenablesItems:(BOOL)aFlag
598 {
599  _autoenablesItems = aFlag;
600 }
601 
605 - (BOOL)autoenablesItems
606 {
607  return _autoenablesItems;
608 }
609 
616 - (void)update
617 {
618  if (!_autoenablesItems)
619  return;
620 
621  var items = [self itemArray];
622 
623  for (var i = 0; i < [items count]; i++)
624  {
625  var item = [items objectAtIndex:i];
626 
627  if ([item hasSubmenu])
628  continue;
629 
630  // If there are enabled bindings for the item, they override anything else
631  var binder = [CPBinder getBinding:CPEnabledBinding forObject:item];
632 
633  if (binder)
634  {
635  [binder setValueFor:CPEnabledBinding];
636  return;
637  }
638 
639  var validator = [CPApp targetForAction:[item action] to:[item target] from:item],
640  shouldBeEnabled = YES;
641 
642  if (!validator)
643  {
644  // If targetForAction: returns nil, it could be that there is no action.
645  // If there is an action and nil is returned, no valid target could be found.
646  if ([item action] || [item target])
647  shouldBeEnabled = NO;
648  else
649  {
650  // Check to see if there is a target binding with an invalid selector
651  var info = [CPBinder infoForBinding:CPTargetBinding forObject:item];
652 
653  if (info)
654  {
655  var object = [info objectForKey:CPObservedObjectKey],
656  keyPath = [info objectForKey:CPObservedKeyPathKey],
657  options = [info objectForKey:CPOptionsKey],
658  target = [object valueForKeyPath:keyPath],
659  selector = [options valueForKey:CPSelectorNameBindingOption];
660 
661  if (target && selector && ![target respondsToSelector:CPSelectorFromString(selector)])
662  shouldBeEnabled = NO;
663  }
664  }
665  }
666  else if (![validator respondsToSelector:[item action]])
667  shouldBeEnabled = NO;
668  else if ([validator respondsToSelector:@selector(validateMenuItem:)])
669  shouldBeEnabled = [validator validateMenuItem:item];
670  else if ([validator respondsToSelector:@selector(validateUserInterfaceItem:)])
671  shouldBeEnabled = [validator validateUserInterfaceItem:item];
672 
673  [item setEnabled:shouldBeEnabled];
674  }
675 
676  [[_menuWindow _menuView] tile];
677 }
678 
679 // Managing the Title
684 - (void)setTitle:(CPString)aTitle
685 {
686  _title = aTitle;
687 }
688 
692 - (CPString)title
693 {
694  return _title;
695 }
696 
697 - (void)setMinimumWidth:(float)aMinimumWidth
698 {
699  _minimumWidth = aMinimumWidth;
700 }
701 
702 - (float)minimumWidth
703 {
704  return _minimumWidth;
705 }
706 
707 - (void)_performActionOfHighlightedItemChain
708 {
709  var highlightedItem = [self highlightedItem];
710 
711  while ([highlightedItem submenu] && [highlightedItem action] === @selector(submenuAction:))
712  highlightedItem = [[highlightedItem submenu] highlightedItem];
713 
714  // FIXME: It is theoretically not necessarily to check isEnabled here since
715  // highlightedItem is always enabled. Do there exist edge cases: disabling on closing a menu,
716  // etc.? Requires further investigation and tests.
717  if (highlightedItem && [highlightedItem isEnabled])
718  {
719  // Perform any action binding
720  var binding = [CPBinder getBinding:CPTargetBinding forObject:highlightedItem];
721  [binding invokeAction];
722 
723  [CPApp sendAction:[highlightedItem action] to:[highlightedItem target] from:highlightedItem];
724  }
725 }
726 
727 //
728 + (CGRect)_constraintRectForView:(CPView)aView
729 {
730  if ([CPPlatform isBrowser])
731  return CGRectInset([[[aView window] platformWindow] contentBounds], 5.0, 5.0);
732 
733  return CGRectInset([[[aView window] screen] visibleFrame], 5.0, 5.0);
734 }
735 
736 - (void)popUpMenuPositioningItem:(CPMenuItem)anItem atLocation:(CGPoint)aLocation inView:(CPView)aView callback:(Function)aCallback
737 {
738  [self _popUpMenuPositioningItem:anItem
739  atLocation:aLocation
740  topY:aLocation.y
741  bottomY:aLocation.y
742  inView:aView
743  callback:aCallback];
744 }
745 
746 - (void)_popUpMenuPositioningItem:(CPMenuItem)anItem atLocation:(CGPoint)aLocation topY:(float)aTopY bottomY:(float)aBottomY inView:(CPView)aView callback:(Function)aCallback
747 {
748  var itemIndex = 0;
749 
750  if (anItem)
751  {
752  itemIndex = [self indexOfItem:anItem];
753 
754  if (itemIndex === CPNotFound)
755  throw "In call to popUpMenuPositioningItem:atLocation:inView:callback:, menu item " +
756  anItem + " is not present in menu " + self;
757  }
758 
759  var theWindow = [aView window];
760 
761  if (aView && !theWindow)
762  throw "In call to popUpMenuPositioningItem:atLocation:inView:callback:, view is not in any window.";
763 
764  [self _menuWillOpen];
765 
766  // Convert location to global coordinates if not already in them.
767  if (aView)
768  aLocation = [theWindow convertBaseToGlobal:[aView convertPoint:aLocation toView:nil]];
769 
770  // Create the window for our menu.
771  var menuWindow = [_CPMenuWindow menuWindowWithMenu:self font:[self font]];
772 
773  [menuWindow setBackgroundStyle:_CPMenuWindowPopUpBackgroundStyle];
774 
775  if (anItem)
776  // Don't convert this value to global, we care about the distance (delta) from the
777  // the edge of the window, which is equivalent to its origin.
778  aLocation.y -= [menuWindow deltaYForItemAtIndex:itemIndex];
779 
780  // Grab the constraint rect for this view.
781  var constraintRect = [CPMenu _constraintRectForView:aView];
782 
783  [menuWindow setFrameOrigin:aLocation];
784  [menuWindow setConstraintRect:constraintRect];
785 
786  // If we aren't showing enough items, reposition the view in a better place.
787  if (![menuWindow hasMinimumNumberOfVisibleItems])
788  {
789  var unconstrainedFrame = [menuWindow unconstrainedFrame],
790  unconstrainedY = CGRectGetMinY(unconstrainedFrame);
791 
792  // If we scroll to early downwards, or are offscreen (!), move it up.
793  if (unconstrainedY >= CGRectGetMaxY(constraintRect) || [menuWindow canScrollDown])
794  {
795  // Convert this to global if it isn't already.
796  if (aView)
797  aTopY = [theWindow convertBaseToGlobal:[aView convertPoint:CGPointMake(0.0, aTopY) toView:nil]].y;
798 
799  unconstrainedFrame.origin.y = MIN(CGRectGetMaxY(constraintRect), aTopY) - CGRectGetHeight(unconstrainedFrame);
800  }
801 
802  // If we scroll to early upwards, or are offscreen (!), move it down.
803  else if (unconstrainedY < CGRectGetMinY(constraintRect) || [menuWindow canScrollUp])
804  {
805  // Convert this to global if it isn't already.
806  if (aView)
807  aBottomY = [theWindow convertBaseToGlobal:[aView convertPoint:CGPointMake(0.0, aBottomY) toView:nil]].y;
808 
809  unconstrainedFrame.origin.y = MAX(CGRectGetMinY(constraintRect), aBottomY);
810  }
811 
812  [menuWindow setFrameOrigin:CGRectIntersection(unconstrainedFrame, constraintRect).origin];
813  }
814 
815  // Show it.
816  if ([CPPlatform isBrowser])
817  [menuWindow setPlatformWindow:[[aView window] platformWindow]];
818 
819  [menuWindow orderFront:self];
820 
821  // Track it.
822  [[_CPMenuManager sharedMenuManager]
823  beginTracking:[CPApp currentEvent]
824  menuContainer:menuWindow
825  constraintRect:constraintRect
826  callback:[CPMenu trackingCallbackWithCallback:aCallback]];
827 }
828 
829 + (Function)trackingCallbackWithCallback:(Function)aCallback
830 {
831  return function(aMenuWindow, aMenu)
832  {
833  [aMenuWindow setMenu:nil];
834  [aMenuWindow orderOut:self];
835 
836  [_CPMenuWindow poolMenuWindow:aMenuWindow];
837 
838  if (aCallback)
839  aCallback(aMenu);
840 
841  [aMenu _performActionOfHighlightedItemChain];
842  }
843 }
844 
845 + (void)popUpContextMenu:(CPMenu)aMenu withEvent:(CPEvent)anEvent forView:(CPView)aView
846 {
847  [self popUpContextMenu:aMenu withEvent:anEvent forView:aView withFont:nil];
848 }
849 
850 + (void)popUpContextMenu:(CPMenu)aMenu withEvent:(CPEvent)anEvent forView:(CPView)aView withFont:(CPFont)aFont
851 {
852  // This is needed when we are making several rights click
853  [[_CPMenuManager sharedMenuManager] cancelActiveMenu];
854 
855  [aMenu _menuWillOpen];
856 
857  if (!aFont)
859 
860  var theWindow = [aView window],
861  menuWindow = [_CPMenuWindow menuWindowWithMenu:aMenu font:aFont];
862 
863  [menuWindow setBackgroundStyle:_CPMenuWindowPopUpBackgroundStyle];
864 
865  var constraintRect = [CPMenu _constraintRectForView:aView],
866  aLocation = [[anEvent window] convertBaseToGlobal:[anEvent locationInWindow]];
867 
868  [menuWindow setConstraintRect:constraintRect];
869  [menuWindow setFrameOrigin:aLocation];
870 
871  // If we aren't showing enough items, reposition the view in a better place.
872  if (![menuWindow hasMinimumNumberOfVisibleItems])
873  {
874  var unconstrainedFrame = [menuWindow unconstrainedFrame],
875  unconstrainedY = CGRectGetMinY(unconstrainedFrame);
876 
877  // If we scroll to early downwards, or are offscreen (!), move it up.
878  if (unconstrainedY >= CGRectGetMaxY(constraintRect) || [menuWindow canScrollDown])
879  unconstrainedFrame.origin.y = MIN(CGRectGetMaxY(constraintRect), aLocation.y) - CGRectGetHeight(unconstrainedFrame);
880 
881  // If we scroll to early upwards, or are offscreen (!), move it down.
882  else if (unconstrainedY < CGRectGetMinY(constraintRect) || [menuWindow canScrollUp])
883  unconstrainedFrame.origin.y = MAX(CGRectGetMinY(constraintRect), aLocation.y);
884 
885  [menuWindow setFrameOrigin:CGRectIntersection(unconstrainedFrame, constraintRect).origin];
886  }
887 
888  if ([CPPlatform isBrowser])
889  [menuWindow setPlatformWindow:[[aView window] platformWindow]];
890 
891  [menuWindow orderFront:self];
892 
893  [[_CPMenuManager sharedMenuManager]
894  beginTracking:anEvent
895  menuContainer:menuWindow
896  constraintRect:[CPMenu _constraintRectForView:aView]
897  callback:[CPMenu trackingCallbackWithCallback:nil]];
898 }
899 
900 // Managing Display of State Column
905 - (void)setShowsStateColumn:(BOOL)shouldShowStateColumn
906 {
907  _showsStateColumn = shouldShowStateColumn;
908 }
909 
913 - (BOOL)showsStateColumn
914 {
915  return _showsStateColumn;
916 }
917 
918 // Handling Highlighting
923 - (CPMenuItem)highlightedItem
924 {
925  if (_highlightedIndex < 0)
926  return nil;
927 
928  var highlightedItem = _items[_highlightedIndex];
929 
930  if ([highlightedItem isSeparatorItem])
931  return nil;
932 
933  return highlightedItem;
934 }
935 
936 // Managing the Delegate
937 
938 - (void)setDelegate:(id)aDelegate
939 {
940  _delegate = aDelegate;
941 }
942 
943 - (id)delegate
944 {
945  return _delegate;
946 }
947 
948 - (void)_menuWillOpen
949 {
950  var delegate = [self delegate];
951 
952  if ([delegate respondsToSelector:@selector(menuWillOpen:)])
953  [delegate menuWillOpen:self];
954 }
955 
956 - (void)_menuDidClose
957 {
958  // Remember which event caused this menu to close, if any. CPPopUpButton uses this to detect
959  // when a click on the button itself caused the menu to close.
960  _lastCloseEvent = [CPApp currentEvent];
961 
962  var delegate = [self delegate];
963 
964  if ([delegate respondsToSelector:@selector(menuDidClose:)])
965  [delegate menuDidClose:self];
966 }
967 
968 // Handling Tracking
972 - (void)cancelTracking
973 {
974  [[CPRunLoop currentRunLoop] performSelector:@selector(_fireCancelTrackingEvent) target:self argument:nil order:0 modes:[CPDefaultRunLoopMode]];
975 }
976 
977 - (void)_fireCancelTrackingEvent
978 {
979  [CPApp sendEvent:[CPEvent
980  otherEventWithType:CPAppKitDefined
981  location:CGPointMakeZero()
982  modifierFlags:0
983  timestamp:0
984  windowNumber:0
985  context:0
986  subtype:0
987  data1:0
988  data2:0]];
989 
990  // FIXME: We need to do this because this happens in a limitDateForMode:, thus
991  // the second limitDateForMode: won't take effect and the perform selector that
992  // actually draws also won't go into effect. In Safari this works because it sends
993  // an additional mouse move after all this, but not in other browsers.
994  // This will be fixed correctly with the coming run loop changes.
995  [_CPDisplayServer run];
996 }
997 
998 /* @ignore */
999 - (void)_setMenuWindow:(_CPMenuWindow)aMenuWindow
1000 {
1001  _menuWindow = aMenuWindow;
1002 }
1003 
1004 - (void)setFont:(CPFont)aFont
1005 {
1006  _font = aFont;
1007 }
1008 
1009 - (CPFont)font
1010 {
1011  return _font;
1012 }
1013 
1020 - (BOOL)performKeyEquivalent:(CPEvent)anEvent
1021 {
1022  if (_autoenablesItems)
1023  [self update];
1024 
1025  var index = 0,
1026  count = _items.length,
1027  characters = [anEvent charactersIgnoringModifiers],
1028  modifierFlags = [anEvent modifierFlags];
1029 
1030  for (; index < count; ++index)
1031  {
1032  var item = _items[index];
1033 
1034  if ([anEvent _triggersKeyEquivalent:[item keyEquivalent] withModifierMask:[item keyEquivalentModifierMask]])
1035  {
1036  if ([item isEnabled])
1037  [self performActionForItemAtIndex:index];
1038  else
1039  {
1040  //beep?
1041  }
1042 
1043  return YES;
1044  }
1045 
1046  if ([[item submenu] performKeyEquivalent:anEvent])
1047  return YES;
1048  }
1049 
1050  return NO;
1051 }
1052 
1053 // Simulating Mouse Clicks
1058 - (void)performActionForItemAtIndex:(CPUInteger)anIndex
1059 {
1060  var item = _items[anIndex];
1061 
1062  [CPApp sendAction:[item action] to:[item target] from:item];
1063 }
1064 
1065 //
1066 /*
1067  @ignore
1068 */
1069 - (void)_highlightItemAtIndex:(int)anIndex
1070 {
1071  if (_highlightedIndex === anIndex)
1072  return;
1073 
1074  if (_highlightedIndex !== CPNotFound)
1075  [[_items[_highlightedIndex] _menuItemView] highlight:NO];
1076 
1077  _highlightedIndex = anIndex;
1078 
1079  if (_highlightedIndex !== CPNotFound)
1080  [[_items[_highlightedIndex] _menuItemView] highlight:YES];
1081 
1082  if (_highlightedIndex !== CPNotFound && _menuWindow)
1083  [_menuWindow._menuView scrollRectToVisible:[[_items[_highlightedIndex] _menuItemView] frame]];
1084 }
1085 
1086 - (void)_setMenuName:(CPString)aName
1087 {
1088  if (_name === aName)
1089  return;
1090 
1091  _name = aName;
1092 
1093  if (_name === @"CPMainMenu")
1094  [CPApp setMainMenu:self];
1095 }
1096 
1097 - (CPString)_menuName
1098 {
1099  return _name;
1100 }
1101 
1102 - (void)awakeFromCib
1103 {
1104  if (_name === @"_CPMainMenu")
1105  {
1106  [self _setMenuName:@"CPMainMenu"];
1107  [CPMenu setMenuBarVisible:YES];
1108  }
1109 }
1110 
1111 - (void)_menuWithName:(CPString)aName
1112 {
1113  if (aName === _name)
1114  return self;
1115 
1116  for (var i = 0, count = [_items count]; i < count; i++)
1117  {
1118  var menu = [[_items[i] submenu] _menuWithName:aName];
1119 
1120  if (menu)
1121  return menu;
1122  }
1123 
1124  return nil;
1125 }
1126 
1127 @end
1128 
1130 
1131 - (CPUInteger)countOfItems
1132 {
1133  return [_items count];
1134 }
1135 
1136 - (CPMenuItem)objectInItemsAtIndex:(CPUInteger)anIndex
1137 {
1138  return [_items objectAtIndex:anIndex];
1139 }
1140 
1141 - (CPArray)itemsAtIndexes:(CPIndexSet)indexes
1142 {
1143  return [_items objectsAtIndexes:indexes];
1144 }
1145 
1146 @end
1147 
1149 
1150 - (void)insertObject:(CPMenuItem)aMenuItem inItemsAtIndex:(CPUInteger)anIndex
1151 {
1152  var menu = [aMenuItem menu];
1153 
1154  if (menu)
1155  if (menu !== self)
1156  [CPException raise:CPInternalInconsistencyException reason:@"Attempted to insert item into menu that was already in another menu."];
1157  else
1158  return;
1159 
1160  [aMenuItem setMenu:self];
1161  [self _highlightItemAtIndex:CPNotFound];
1162  [_items insertObject:aMenuItem atIndex:anIndex];
1163 
1165  postNotificationName:CPMenuDidAddItemNotification
1166  object:self
1167  userInfo:@{ @"CPMenuItemIndex": anIndex }];
1168 }
1169 
1170 - (void)removeObjectFromItemsAtIndex:(CPUInteger)anIndex
1171 {
1172  if (anIndex < 0 || anIndex >= [_items count])
1173  return;
1174 
1175  [[_items objectAtIndex:anIndex] setMenu:nil];
1176  [self _highlightItemAtIndex:CPNotFound];
1177  [_items removeObjectAtIndex:anIndex];
1178 
1180  postNotificationName:CPMenuDidRemoveItemNotification
1181  object:self
1182  userInfo:@{ @"CPMenuItemIndex": anIndex }];
1183 }
1184 
1185 @end
1186 
1187 var CPMenuTitleKey = @"CPMenuTitleKey",
1188  CPMenuNameKey = @"CPMenuNameKey",
1189  CPMenuItemsKey = @"CPMenuItemsKey",
1190  CPMenuShowsStateColumnKey = @"CPMenuShowsStateColumnKey",
1191  CPMenuAutoEnablesItemsKey = @"CPMenuAutoEnablesItemsKey";
1192 
1193 @implementation CPMenu (CPCoding)
1194 
1200 - (id)initWithCoder:(CPCoder)aCoder
1201 {
1202  self = [super init];
1203 
1204  if (self)
1205  {
1206  _title = [aCoder decodeObjectForKey:CPMenuTitleKey];
1207  _items = [aCoder decodeObjectForKey:CPMenuItemsKey];
1208 
1209  [self _setMenuName:[aCoder decodeObjectForKey:CPMenuNameKey]];
1210 
1211  _showsStateColumn = ![aCoder containsValueForKey:CPMenuShowsStateColumnKey] || [aCoder decodeBoolForKey:CPMenuShowsStateColumnKey];
1212 
1213  _autoenablesItems = ![aCoder containsValueForKey:CPMenuAutoEnablesItemsKey] || [aCoder decodeBoolForKey:CPMenuAutoEnablesItemsKey];
1214 
1215  [self setMinimumWidth:0];
1216  }
1217 
1218  return self;
1219 }
1220 
1225 - (void)encodeWithCoder:(CPCoder)aCoder
1226 {
1227  [aCoder encodeObject:_title forKey:CPMenuTitleKey];
1228 
1229  if (_name)
1230  [aCoder encodeObject:_name forKey:CPMenuNameKey];
1231 
1232  [aCoder encodeObject:_items forKey:CPMenuItemsKey];
1233 
1234  if (!_showsStateColumn)
1235  [aCoder encodeBool:_showsStateColumn forKey:CPMenuShowsStateColumnKey];
1236 
1237  if (!_autoenablesItems)
1238  [aCoder encodeBool:_autoenablesItems forKey:CPMenuAutoEnablesItemsKey];
1239 }
1240 
1241 @end
1242 
1243