API  1.0.0
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 @protocol CPMenuDelegate <CPObject>
28 
29 @optional
30 - (void)menuWillOpen:(CPMenu)aMenu;
31 - (void)menuDidClose:(CPMenu)aMenu;
32 
33 @end
34 
37 
38 CPMenuDidAddItemNotification = @"CPMenuDidAddItemNotification";
39 CPMenuDidChangeItemNotification = @"CPMenuDidChangeItemNotification";
40 CPMenuDidRemoveItemNotification = @"CPMenuDidRemoveItemNotification";
41 
42 CPMenuDidEndTrackingNotification = @"CPMenuDidEndTrackingNotification";
43 
44 var _CPMenuBarVisible = NO,
45  _CPMenuBarTitle = @"",
46  _CPMenuBarAttributes = nil,
47  _CPMenuBarSharedWindow = nil;
48 
55 @implementation CPMenu : CPObject
56 {
57  CPMenu _supermenu;
58 
59  CPString _title;
60  CPString _name;
61 
62  CPFont _font;
63 
64  float _minimumWidth;
65 
66  CPMutableArray _items;
67 
68  BOOL _autoenablesItems;
69  BOOL _showsStateColumn;
70 
71  id <CPMenuDelegate> _delegate;
72  unsigned _implementedDelegateMethods;
73 
74  int _highlightedIndex;
75  _CPMenuWindow _menuWindow;
76 
77  CPEvent _lastCloseEvent;
78 }
79 
80 // Managing the Menu Bar
81 
82 + (void)initialize
83 {
84  if (self !== [CPMenu class])
85  return;
86 
87  [[self class] setMenuBarAttributes:@{}];
88 }
89 
90 + (BOOL)menuBarVisible
91 {
92  return _CPMenuBarVisible;
93 }
94 
95 + (void)setMenuBarVisible:(BOOL)menuBarShouldBeVisible
96 {
97  if (_CPMenuBarVisible === menuBarShouldBeVisible)
98  return;
99 
100  _CPMenuBarVisible = menuBarShouldBeVisible;
101 
102  if ([CPPlatform supportsNativeMainMenu])
103  return;
104 
105  if (menuBarShouldBeVisible)
106  {
107  if (!_CPMenuBarSharedWindow)
108  _CPMenuBarSharedWindow = [[_CPMenuBarWindow alloc] init];
109 
110  [_CPMenuBarSharedWindow setMenu:[CPApp mainMenu]];
111 
112  [_CPMenuBarSharedWindow setTitle:_CPMenuBarTitle];
113  [_CPMenuBarSharedWindow setIconImage:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-icon-image" forClass:_CPMenuView]];
114  [_CPMenuBarSharedWindow setIconImageAlphaValue:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-icon-image-alpha-value" forClass:_CPMenuView]];
115 
116  [_CPMenuBarSharedWindow setColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarBackgroundColor"]];
117  [_CPMenuBarSharedWindow setTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextColor"]];
118  [_CPMenuBarSharedWindow setTitleColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleColor"]];
119  [_CPMenuBarSharedWindow setTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextShadowColor"]];
120  [_CPMenuBarSharedWindow setTitleShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleShadowColor"]];
121  [_CPMenuBarSharedWindow setHighlightColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightColor"]];
122  [_CPMenuBarSharedWindow setHighlightTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextColor"]];
123  [_CPMenuBarSharedWindow setHighlightTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextShadowColor"]];
124 
125  [_CPMenuBarSharedWindow orderFront:self];
126  }
127  else
128  [_CPMenuBarSharedWindow orderOut:self];
129 
130 // FIXME: There must be a better way to do this.
131 #if PLATFORM(DOM)
132  [[CPPlatformWindow primaryPlatformWindow] resizeEvent:nil];
133 #endif
134 }
135 
136 + (void)setMenuBarTitle:(CPString)aTitle
137 {
138  _CPMenuBarTitle = aTitle;
139  [_CPMenuBarSharedWindow setTitle:_CPMenuBarTitle];
140 }
141 
142 + (CPString)menuBarTitle
143 {
144  return _CPMenuBarTitle;
145 }
146 
147 + (void)setMenuBarIconImage:(CPImage)anImage
148 {
149  _CPMenuBarImage = anImage;
150  [_CPMenuBarSharedWindow setIconImage:anImage];
151 }
152 
153 + (CPImage)menuBarIconImage
154 {
155  return _CPMenuBarImage;
156 }
157 
158 + (void)_setOrRemoveMenuBarAttribute:(id)aValue forKey:(id)aKey
159 {
160  if (aValue === nil)
161  [_CPMenuBarAttributes removeObjectForKey:aKey];
162  else
163  [_CPMenuBarAttributes setObject:aValue forKey:aKey];
164 }
165 
166 + (void)setMenuBarAttributes:(CPDictionary)attributes
167 {
168  if (_CPMenuBarAttributes == attributes)
169  return;
170 
171  _CPMenuBarAttributes = [attributes copy];
172 
173  var textColor = [attributes objectForKey:@"CPMenuBarTextColor"],
174  titleColor = [attributes objectForKey:@"CPMenuBarTitleColor"],
175  textShadowColor = [attributes objectForKey:@"CPMenuBarTextShadowColor"],
176  titleShadowColor = [attributes objectForKey:@"CPMenuBarTitleShadowColor"],
177  highlightColor = [attributes objectForKey:@"CPMenuBarHighlightColor"],
178  highlightTextColor = [attributes objectForKey:@"CPMenuBarHighlightTextColor"],
179  highlightTextShadowColor = [attributes objectForKey:@"CPMenuBarHighlightTextShadowColor"];
180 
181  if (!textColor && titleColor)
182  [_CPMenuBarAttributes setObject:titleColor forKey:@"CPMenuBarTextColor"];
183 
184  else if (textColor && !titleColor)
185  [_CPMenuBarAttributes setObject:textColor forKey:@"CPMenuBarTitleColor"];
186 
187  else if (!textColor && !titleColor)
188  {
189  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-text-color" forClass:_CPMenuView] forKey:@"CPMenuBarTextColor"];
190  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-title-color" forClass:_CPMenuView] forKey:@"CPMenuBarTitleColor"];
191  }
192 
193  if (!textShadowColor && titleShadowColor)
194  [_CPMenuBarAttributes setObject:titleShadowColor forKey:@"CPMenuBarTextShadowColor"];
195 
196  else if (textShadowColor && !titleShadowColor)
197  [_CPMenuBarAttributes setObject:textShadowColor forKey:@"CPMenuBarTitleShadowColor"];
198 
199  else if (!textShadowColor && !titleShadowColor)
200  {
201  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-text-shadow-color" forClass:_CPMenuView] forKey:@"CPMenuBarTextShadowColor"];
202  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-title-shadow-color" forClass:_CPMenuView] forKey:@"CPMenuBarTitleShadowColor"];
203  }
204 
205  if (!highlightColor)
206  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-highlight-color" forClass:_CPMenuView] forKey:@"CPMenuBarHighlightColor"];
207 
208  if (!highlightTextColor)
209  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-highlight-text-color" forClass:_CPMenuView] forKey:@"CPMenuBarHighlightTextColor"];
210 
211  if (!highlightTextShadowColor)
212  [self _setOrRemoveMenuBarAttribute:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-highlight-text-shadow-color" forClass:_CPMenuView] forKey:@"CPMenuBarHighlightTextShadowColor"];
213 
214  if (_CPMenuBarSharedWindow)
215  {
216  [_CPMenuBarSharedWindow setColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarBackgroundColor"]];
217  [_CPMenuBarSharedWindow setTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextColor"]];
218  [_CPMenuBarSharedWindow setTitleColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleColor"]];
219  [_CPMenuBarSharedWindow setTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextShadowColor"]];
220  [_CPMenuBarSharedWindow setTitleShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleShadowColor"]];
221  [_CPMenuBarSharedWindow setHighlightColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightColor"]];
222  [_CPMenuBarSharedWindow setHighlightTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextColor"]];
223  [_CPMenuBarSharedWindow setHighlightTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextShadowColor"]];
224  }
225 }
226 
227 + (CPDictionary)menuBarAttributes
228 {
229  return _CPMenuBarAttributes;
230 }
231 
232 + (void)_setMenuBarIconImageAlphaValue:(float)anAlphaValue
233 {
234  _CPMenuBarIconImageAlphaValue = anAlphaValue;
235  [_CPMenuBarSharedWindow setIconImageAlphaValue:anAlphaValue];
236 }
237 
238 - (float)menuBarHeight
239 {
240  if (self === [CPApp mainMenu])
241  return [CPMenu menuBarHeight];
242 
243  return 0.0;
244 }
245 
246 + (float)menuBarHeight
247 {
248  return [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-height" forClass:_CPMenuView];
249 }
250 
251 // Creating a CPMenu Object
257 - (id)initWithTitle:(CPString)aTitle
258 {
259  self = [super init];
260 
261  if (self)
262  {
263  _title = aTitle;
264  _items = [];
265 
266  _autoenablesItems = YES;
267  _showsStateColumn = YES;
268 
269  [self setMinimumWidth:0];
270  }
271 
272  return self;
273 }
274 
275 - (id)init
276 {
277  return [self initWithTitle:@""];
278 }
279 
280 // Setting Up Menu Commands
286 - (void)insertItem:(CPMenuItem)aMenuItem atIndex:(CPUInteger)anIndex
287 {
288  [self insertObject:aMenuItem inItemsAtIndex:anIndex];
289 }
290 
299 - (CPMenuItem)insertItemWithTitle:(CPString)aTitle action:(SEL)anAction keyEquivalent:(CPString)aKeyEquivalent atIndex:(CPUInteger)anIndex
300 {
301  var item = [[CPMenuItem alloc] initWithTitle:aTitle action:anAction keyEquivalent:aKeyEquivalent];
302 
303  [self insertItem:item atIndex:anIndex];
304 
305  return item;
306 }
307 
312 - (void)addItem:(CPMenuItem)aMenuItem
313 {
314  [self insertItem:aMenuItem atIndex:[_items count]];
315 }
316 
325 - (CPMenuItem)addItemWithTitle:(CPString)aTitle action:(SEL)anAction keyEquivalent:(CPString)aKeyEquivalent
326 {
327  return [self insertItemWithTitle:aTitle action:anAction keyEquivalent:aKeyEquivalent atIndex:[_items count]];
328 }
329 
334 - (void)removeItem:(CPMenuItem)aMenuItem
335 {
336  [self removeItemAtIndex:[_items indexOfObjectIdenticalTo:aMenuItem]];
337 }
338 
343 - (void)removeItemAtIndex:(CPUInteger)anIndex
344 {
345  [self removeObjectFromItemsAtIndex:anIndex];
346 }
347 
354 - (void)removeAllItems
355 {
356  var count = [_items count];
357 
358  // Remove the connection to this menu in case
359  // someone else has a reference to the menu item.
360  while (count--)
361  [_items[count] setMenu:nil];
362 
363  [self _highlightItemAtIndex:CPNotFound];
364 
365  // Because we are changing _items directly, be sure to notify KVO
366  [self willChangeValueForKey:@"items"];
367  _items = [CPMutableArray array];
368  [self didChangeValueForKey:@"items"];
369 }
370 
375 - (void)itemChanged:(CPMenuItem)aMenuItem
376 {
377  /*
378  During cib unarchiving, menu items will have a reference to their menu,
379  but of course the items are still being unarchived and the menu's _items
380  have not yet been instantiated. In that case we will not do anything here.
381  */
382  if ([aMenuItem menu] !== self || !_items)
383  return;
384 
385  [aMenuItem setValue:[aMenuItem valueForKey:@"changeCount"] + 1 forKey:@"changeCount"];
386 
388  postNotificationName:CPMenuDidChangeItemNotification
389  object:self
390  userInfo:@{ @"CPMenuItemIndex": [_items indexOfObjectIdenticalTo:aMenuItem] }];
391 }
392 
393 // Finding Menu Items
399 - (CPMenuItem)itemWithTag:(int)aTag
400 {
401  var index = [self indexOfItemWithTag:aTag];
402 
403  if (index == CPNotFound)
404  return nil;
405 
406  return _items[index];
407 }
408 
414 - (CPMenuItem)itemWithTitle:(CPString)aTitle
415 {
416  var index = [self indexOfItemWithTitle:aTitle];
417 
418  if (index == CPNotFound)
419  return nil;
420 
421  return _items[index];
422 }
423 
428 - (CPMenuItem)itemAtIndex:(int)anIndex
429 {
430  return [_items objectAtIndex:anIndex];
431 }
432 
436 - (unsigned)numberOfItems
437 {
438  return [_items count];
439 }
440 
444 - (CPArray)itemArray
445 {
446  return _items;
447 }
448 
449 // Finding Indices of Menu Items
455 - (int)indexOfItem:(CPMenuItem)aMenuItem
456 {
457  if ([aMenuItem menu] !== self)
458  return CPNotFound;
459 
460  return [_items indexOfObjectIdenticalTo:aMenuItem];
461 }
462 
468 - (int)indexOfItemWithTitle:(CPString)aTitle
469 {
470  var index = 0,
471  count = _items.length;
472 
473  for (; index < count; ++index)
474  if ([_items[index] title] === aTitle)
475  return index;
476 
477  return CPNotFound;
478 }
479 
485 - (int)indexOfItemWithTag:(int)aTag
486 {
487  var index = 0,
488  count = _items.length;
489 
490  for (; index < count; ++index)
491  if ([_items[index] tag] == aTag)
492  return index;
493 
494  return CPNotFound;
495 }
496 
503 - (int)indexOfItemWithTarget:(id)aTarget andAction:(SEL)anAction
504 {
505  var index = 0,
506  count = _items.length;
507 
508  for (; index < count; ++index)
509  {
510  var item = _items[index];
511 
512  if ([item target] == aTarget && (!anAction || [item action] == anAction))
513  return index;
514  }
515 
516  return CPNotFound;
517 }
518 
524 - (int)indexOfItemWithRepresentedObject:(id)anObject
525 {
526  var index = 0,
527  count = _items.length;
528 
529  for (; index < count; ++index)
530  if ([[_items[index] representedObject] isEqual:anObject])
531  return index;
532 
533  return CPNotFound;
534 }
535 
541 - (int)indexOfItemWithSubmenu:(CPMenu)aMenu
542 {
543  var index = 0,
544  count = _items.length;
545 
546  for (; index < count; ++index)
547  if ([_items[index] submenu] == aMenu)
548  return index;
549 
550  return CPNotFound;
551 }
552 
553 // Managing Submenus
559 - (void)setSubmenu:(CPMenu)aMenu forItem:(CPMenuItem)aMenuItem
560 {
561  [aMenuItem setTarget:aMenuItem];
562  [aMenuItem setAction:@selector(submenuAction:)];
563 
564  [aMenuItem setSubmenu:aMenu];
565 }
566 
573 - (void)submenuAction:(id)aSender
574 {
575 }
576 
580 - (CPMenu)supermenu
581 {
582  return _supermenu;
583 }
584 
589 - (void)setSupermenu:(CPMenu)aMenu
590 {
591  _supermenu = aMenu;
592 }
593 
598 - (BOOL)isTornOff
599 {
600  return !_supermenu /* || offscreen(?) */ || self == [CPApp mainMenu];
601 }
602 
603 // Enabling and Disabling Menu Items
608 - (void)setAutoenablesItems:(BOOL)aFlag
609 {
610  _autoenablesItems = aFlag;
611 }
612 
616 - (BOOL)autoenablesItems
617 {
618  return _autoenablesItems;
619 }
620 
627 - (void)update
628 {
629  if (!_autoenablesItems)
630  return;
631 
632  var items = [self itemArray];
633 
634  for (var i = 0; i < [items count]; i++)
635  {
636  var item = [items objectAtIndex:i];
637 
638  if ([item hasSubmenu])
639  continue;
640 
641  // If there are enabled bindings for the item, they override anything else
642  var binder = [CPBinder getBinding:CPEnabledBinding forObject:item];
643 
644  if (binder)
645  {
646  [binder setValueFor:CPEnabledBinding];
647  [[_menuWindow _menuView] tile];
648  return;
649  }
650 
651  var validator = [CPApp targetForAction:[item action] to:[item target] from:item],
652  shouldBeEnabled = YES;
653 
654  if (!validator)
655  {
656  // If targetForAction: returns nil, it could be that there is no action.
657  // If there is an action and nil is returned, no valid target could be found.
658  if ([item action] || [item target])
659  shouldBeEnabled = NO;
660  else
661  {
662  // Check to see if there is a target binding with an invalid selector
663  var info = [CPBinder infoForBinding:CPTargetBinding forObject:item];
664 
665  if (info)
666  {
667  var object = [info objectForKey:CPObservedObjectKey],
668  keyPath = [info objectForKey:CPObservedKeyPathKey],
669  options = [info objectForKey:CPOptionsKey],
670  target = [object valueForKeyPath:keyPath],
671  selector = [options valueForKey:CPSelectorNameBindingOption];
672 
673  if (target && selector && ![target respondsToSelector:CPSelectorFromString(selector)])
674  shouldBeEnabled = NO;
675  }
676  }
677  }
678  else if (![validator respondsToSelector:[item action]])
679  shouldBeEnabled = NO;
680  else if ([validator respondsToSelector:@selector(validateMenuItem:)])
681  shouldBeEnabled = [validator validateMenuItem:item];
682  else if ([validator respondsToSelector:@selector(validateUserInterfaceItem:)])
683  shouldBeEnabled = [validator validateUserInterfaceItem:item];
684 
685  [item setEnabled:shouldBeEnabled];
686  }
687 
688  [[_menuWindow _menuView] tile];
689 }
690 
691 // Managing the Title
696 - (void)setTitle:(CPString)aTitle
697 {
698  _title = aTitle;
699 }
700 
704 - (CPString)title
705 {
706  return _title;
707 }
708 
709 - (void)setMinimumWidth:(float)aMinimumWidth
710 {
711  _minimumWidth = aMinimumWidth;
712 }
713 
714 - (float)minimumWidth
715 {
716  return _minimumWidth;
717 }
718 
719 - (void)_performActionOfHighlightedItemChain
720 {
721  var highlightedItem = [self highlightedItem];
722 
723  while ([highlightedItem submenu] && [highlightedItem action] === @selector(submenuAction:))
724  highlightedItem = [[highlightedItem submenu] highlightedItem];
725 
726  // FIXME: It is theoretically not necessarily to check isEnabled here since
727  // highlightedItem is always enabled. Do there exist edge cases: disabling on closing a menu,
728  // etc.? Requires further investigation and tests.
729  if (highlightedItem && [highlightedItem isEnabled])
730  {
731  // Perform any action binding
732  var binding = [CPBinder getBinding:CPTargetBinding forObject:highlightedItem];
733  [binding invokeAction];
734 
735  [CPApp sendAction:[highlightedItem action] to:[highlightedItem target] from:highlightedItem];
736  }
737 }
738 
739 //
740 + (CGRect)_constraintRectForView:(CPView)aView
741 {
742  if ([CPPlatform isBrowser])
743  return CGRectInset([[[aView window] platformWindow] contentBounds], 5.0, 5.0);
744 
745  return CGRectInset([[[aView window] screen] visibleFrame], 5.0, 5.0);
746 }
747 
748 - (void)popUpMenuPositioningItem:(CPMenuItem)anItem atLocation:(CGPoint)aLocation inView:(CPView)aView callback:(Function)aCallback
749 {
750  [self _popUpMenuPositioningItem:anItem
751  atLocation:aLocation
752  topY:aLocation.y
753  bottomY:aLocation.y
754  inView:aView
755  callback:aCallback];
756 }
757 
758 - (void)_popUpMenuPositioningItem:(CPMenuItem)anItem atLocation:(CGPoint)aLocation topY:(float)aTopY bottomY:(float)aBottomY inView:(CPView)aView callback:(Function)aCallback
759 {
760  var itemIndex = 0;
761 
762  if (anItem)
763  {
764  itemIndex = [self indexOfItem:anItem];
765 
766  if (itemIndex === CPNotFound)
767  throw "In call to popUpMenuPositioningItem:atLocation:inView:callback:, menu item " +
768  anItem + " is not present in menu " + self;
769  }
770 
771  var theWindow = [aView window];
772 
773  if (aView && !theWindow)
774  throw "In call to popUpMenuPositioningItem:atLocation:inView:callback:, view is not in any window.";
775 
776  [self _menuWillOpen];
777 
778  // Convert location to global coordinates if not already in them.
779  if (aView)
780  aLocation = [theWindow convertBaseToGlobal:[aView convertPoint:aLocation toView:nil]];
781 
782  // Create the window for our menu.
783  var menuWindow = [_CPMenuWindow menuWindowWithMenu:self font:[self font]];
784 
785  [menuWindow setBackgroundStyle:_CPMenuWindowPopUpBackgroundStyle];
786 
787  if (anItem)
788  // Don't convert this value to global, we care about the distance (delta) from the
789  // the edge of the window, which is equivalent to its origin.
790  aLocation.y -= [menuWindow deltaYForItemAtIndex:itemIndex];
791 
792  // Grab the constraint rect for this view.
793  var constraintRect = [CPMenu _constraintRectForView:aView];
794 
795  [menuWindow setFrameOrigin:aLocation];
796  [menuWindow setConstraintRect:constraintRect];
797 
798  // If we aren't showing enough items, reposition the view in a better place.
799  if (![menuWindow hasMinimumNumberOfVisibleItems])
800  {
801  var unconstrainedFrame = [menuWindow unconstrainedFrame],
802  unconstrainedY = CGRectGetMinY(unconstrainedFrame);
803 
804  // If we scroll to early downwards, or are offscreen (!), move it up.
805  if (unconstrainedY >= CGRectGetMaxY(constraintRect) || [menuWindow canScrollDown])
806  {
807  // Convert this to global if it isn't already.
808  if (aView)
809  aTopY = [theWindow convertBaseToGlobal:[aView convertPoint:CGPointMake(0.0, aTopY) toView:nil]].y;
810 
811  unconstrainedFrame.origin.y = MIN(CGRectGetMaxY(constraintRect), aTopY) - CGRectGetHeight(unconstrainedFrame);
812  }
813 
814  // If we scroll to early upwards, or are offscreen (!), move it down.
815  else if (unconstrainedY < CGRectGetMinY(constraintRect) || [menuWindow canScrollUp])
816  {
817  // Convert this to global if it isn't already.
818  if (aView)
819  aBottomY = [theWindow convertBaseToGlobal:[aView convertPoint:CGPointMake(0.0, aBottomY) toView:nil]].y;
820 
821  unconstrainedFrame.origin.y = MAX(CGRectGetMinY(constraintRect), aBottomY);
822  }
823 
824  [menuWindow setFrameOrigin:CGRectIntersection(unconstrainedFrame, constraintRect).origin];
825  }
826 
827  // Show it.
828  if ([CPPlatform isBrowser])
829  [menuWindow setPlatformWindow:[[aView window] platformWindow]];
830 
831  [menuWindow orderFront:self];
832 
833  // Track it.
834  [[_CPMenuManager sharedMenuManager]
835  beginTracking:[CPApp currentEvent]
836  menuContainer:menuWindow
837  constraintRect:constraintRect
838  callback:[CPMenu trackingCallbackWithCallback:aCallback]];
839 }
840 
841 + (Function)trackingCallbackWithCallback:(Function)aCallback
842 {
843  return function(aMenuWindow, aMenu)
844  {
845  [aMenuWindow setMenu:nil];
846  [aMenuWindow orderOut:self];
847 
848  [_CPMenuWindow poolMenuWindow:aMenuWindow];
849 
850  if (aCallback)
851  aCallback(aMenu);
852 
853  [aMenu _performActionOfHighlightedItemChain];
854  }
855 }
856 
857 + (void)popUpContextMenu:(CPMenu)aMenu withEvent:(CPEvent)anEvent forView:(CPView)aView
858 {
859  [self popUpContextMenu:aMenu withEvent:anEvent forView:aView withFont:nil];
860 }
861 
862 + (void)popUpContextMenu:(CPMenu)aMenu withEvent:(CPEvent)anEvent forView:(CPView)aView withFont:(CPFont)aFont
863 {
864  // This is needed when we are making several rights click
865  [[_CPMenuManager sharedMenuManager] cancelActiveMenu];
866 
867  [aMenu _menuWillOpen];
868 
869  if (!aFont)
871 
872  var theWindow = [aView window],
873  menuWindow = [_CPMenuWindow menuWindowWithMenu:aMenu font:aFont];
874 
875  [menuWindow setBackgroundStyle:_CPMenuWindowPopUpBackgroundStyle];
876 
877  var constraintRect = [CPMenu _constraintRectForView:aView],
878  aLocation = [[anEvent window] convertBaseToGlobal:[anEvent locationInWindow]];
879 
880  [menuWindow setConstraintRect:constraintRect];
881  [menuWindow setFrameOrigin:aLocation];
882 
883  // If we aren't showing enough items, reposition the view in a better place.
884  if (![menuWindow hasMinimumNumberOfVisibleItems])
885  {
886  var unconstrainedFrame = [menuWindow unconstrainedFrame],
887  unconstrainedY = CGRectGetMinY(unconstrainedFrame);
888 
889  // If we scroll to early downwards, or are offscreen (!), move it up.
890  if (unconstrainedY >= CGRectGetMaxY(constraintRect) || [menuWindow canScrollDown])
891  unconstrainedFrame.origin.y = MIN(CGRectGetMaxY(constraintRect), aLocation.y) - CGRectGetHeight(unconstrainedFrame);
892 
893  // If we scroll to early upwards, or are offscreen (!), move it down.
894  else if (unconstrainedY < CGRectGetMinY(constraintRect) || [menuWindow canScrollUp])
895  unconstrainedFrame.origin.y = MAX(CGRectGetMinY(constraintRect), aLocation.y);
896 
897  [menuWindow setFrameOrigin:CGRectIntersection(unconstrainedFrame, constraintRect).origin];
898  }
899 
900  if ([CPPlatform isBrowser])
901  [menuWindow setPlatformWindow:[[aView window] platformWindow]];
902 
903  [menuWindow orderFront:self];
904 
905  [[_CPMenuManager sharedMenuManager]
906  beginTracking:anEvent
907  menuContainer:menuWindow
908  constraintRect:[CPMenu _constraintRectForView:aView]
909  callback:[CPMenu trackingCallbackWithCallback:nil]];
910 }
911 
912 // Managing Display of State Column
917 - (void)setShowsStateColumn:(BOOL)shouldShowStateColumn
918 {
919  _showsStateColumn = shouldShowStateColumn;
920 }
921 
925 - (BOOL)showsStateColumn
926 {
927  return _showsStateColumn;
928 }
929 
930 // Handling Highlighting
935 - (CPMenuItem)highlightedItem
936 {
937  if (_highlightedIndex < 0)
938  return nil;
939 
940  var highlightedItem = _items[_highlightedIndex];
941 
942  if ([highlightedItem isSeparatorItem])
943  return nil;
944 
945  return highlightedItem;
946 }
947 
948 // Managing the Delegate
949 
950 - (void)setDelegate:(id <CPMenuDelegate>)aDelegate
951 {
952  if (_delegate === aDelegate)
953  return;
954 
955  _delegate = aDelegate;
956  _implementedDelegateMethods = 0;
957 
958  if ([_delegate respondsToSelector:@selector(menuWillOpen:)])
959  _implementedDelegateMethods |= CPMenuDelegate_menuWillOpen_;
960 
961  if ([_delegate respondsToSelector:@selector(menuDidClose:)])
962  _implementedDelegateMethods |= CPMenuDelegate_menuDidClose_;
963 }
964 
965 - (id)delegate
966 {
967  return _delegate;
968 }
969 
970 - (void)_menuWillOpen
971 {
972  [self _sendDelegateMenuWillOpen];
973 }
974 
975 - (void)_menuDidClose
976 {
977  // Remember which event caused this menu to close, if any. CPPopUpButton uses this to detect
978  // when a click on the button itself caused the menu to close.
979  _lastCloseEvent = [CPApp currentEvent];
980 
981  [self _sendDelegateMenuDidClose];
982 }
983 
984 // Handling Tracking
988 - (void)cancelTracking
989 {
990  [[CPRunLoop currentRunLoop] performSelector:@selector(_fireCancelTrackingEvent) target:self argument:nil order:0 modes:[CPDefaultRunLoopMode]];
991 }
992 
993 - (void)_fireCancelTrackingEvent
994 {
995  [CPApp sendEvent:[CPEvent
996  otherEventWithType:CPAppKitDefined
997  location:CGPointMakeZero()
998  modifierFlags:0
999  timestamp:0
1000  windowNumber:0
1001  context:0
1002  subtype:0
1003  data1:0
1004  data2:0]];
1005 
1006  // FIXME: We need to do this because this happens in a limitDateForMode:, thus
1007  // the second limitDateForMode: won't take effect and the perform selector that
1008  // actually draws also won't go into effect. In Safari this works because it sends
1009  // an additional mouse move after all this, but not in other browsers.
1010  // This will be fixed correctly with the coming run loop changes.
1011  [_CPDisplayServer run];
1012 }
1013 
1014 /* @ignore */
1015 - (void)_setMenuWindow:(_CPMenuWindow)aMenuWindow
1016 {
1017  _menuWindow = aMenuWindow;
1018 }
1019 
1020 - (void)setFont:(CPFont)aFont
1021 {
1022  _font = aFont;
1023 }
1024 
1025 - (CPFont)font
1026 {
1027  return _font;
1028 }
1029 
1036 - (BOOL)performKeyEquivalent:(CPEvent)anEvent
1037 {
1038  if (_autoenablesItems)
1039  [self update];
1040 
1041  var index = 0,
1042  count = _items.length,
1043  characters = [anEvent charactersIgnoringModifiers],
1044  modifierFlags = [anEvent modifierFlags];
1045 
1046  for (; index < count; ++index)
1047  {
1048  var item = _items[index];
1049 
1050  if ([anEvent _triggersKeyEquivalent:[item keyEquivalent] withModifierMask:[item keyEquivalentModifierMask]])
1051  {
1052  if ([item isEnabled])
1053  [self performActionForItemAtIndex:index];
1054  else
1055  {
1056  //beep?
1057  }
1058 
1059  return YES;
1060  }
1061 
1062  if ([[item submenu] performKeyEquivalent:anEvent])
1063  return YES;
1064  }
1065 
1066  return NO;
1067 }
1068 
1069 // Simulating Mouse Clicks
1074 - (void)performActionForItemAtIndex:(CPUInteger)anIndex
1075 {
1076  var item = _items[anIndex];
1077 
1078  [CPApp sendAction:[item action] to:[item target] from:item];
1079 }
1080 
1081 //
1082 /*
1083  @ignore
1084 */
1085 - (void)_highlightItemAtIndex:(int)anIndex
1086 {
1087  if (_highlightedIndex === anIndex)
1088  return;
1089 
1090  if (_highlightedIndex !== CPNotFound)
1091  [[_items[_highlightedIndex] _menuItemView] highlight:NO];
1092 
1093  _highlightedIndex = anIndex;
1094 
1095  if (_highlightedIndex !== CPNotFound)
1096  [[_items[_highlightedIndex] _menuItemView] highlight:YES];
1097 
1098  if (_highlightedIndex !== CPNotFound && _menuWindow)
1099  [_menuWindow._menuView scrollRectToVisible:[[_items[_highlightedIndex] _menuItemView] frame]];
1100 }
1101 
1102 - (void)_setMenuName:(CPString)aName
1103 {
1104  if (_name === aName)
1105  return;
1106 
1107  _name = aName;
1108 
1109  if (_name === @"CPMainMenu")
1110  [CPApp setMainMenu:self];
1111 }
1112 
1113 - (CPString)_menuName
1114 {
1115  return _name;
1116 }
1117 
1118 - (void)awakeFromCib
1119 {
1120  if (_name === @"_CPMainMenu")
1121  {
1122  [self _setMenuName:@"CPMainMenu"];
1123  [CPMenu setMenuBarVisible:YES];
1124  }
1125 }
1126 
1127 - (void)_menuWithName:(CPString)aName
1128 {
1129  if (aName === _name)
1130  return self;
1131 
1132  for (var i = 0, count = [_items count]; i < count; i++)
1133  {
1134  var menu = [[_items[i] submenu] _menuWithName:aName];
1135 
1136  if (menu)
1137  return menu;
1138  }
1139 
1140  return nil;
1141 }
1142 
1143 @end
1144 
1145 
1147 
1152 - (void)_sendDelegateMenuWillOpen
1153 {
1154  if (!(_implementedDelegateMethods & CPMenuDelegate_menuWillOpen_))
1155  return;
1156 
1157  [_delegate menuWillOpen:self];
1158 }
1159 
1164 - (void)_sendDelegateMenuDidClose
1165 {
1166  if (!(_implementedDelegateMethods & CPMenuDelegate_menuDidClose_))
1167  return;
1168 
1169  [_delegate menuDidClose:self];
1170 }
1171 
1172 @end
1173 
1174 
1176 
1177 - (CPUInteger)countOfItems
1178 {
1179  return [_items count];
1180 }
1181 
1182 - (CPMenuItem)objectInItemsAtIndex:(CPUInteger)anIndex
1183 {
1184  return [_items objectAtIndex:anIndex];
1185 }
1186 
1187 - (CPArray)itemsAtIndexes:(CPIndexSet)indexes
1188 {
1189  return [_items objectsAtIndexes:indexes];
1190 }
1191 
1192 @end
1193 
1195 
1196 - (void)insertObject:(CPMenuItem)aMenuItem inItemsAtIndex:(CPUInteger)anIndex
1197 {
1198  var menu = [aMenuItem menu];
1199 
1200  if (menu)
1201  if (menu !== self)
1202  [CPException raise:CPInternalInconsistencyException reason:@"Attempted to insert item into menu that was already in another menu."];
1203  else
1204  return;
1205 
1206  [aMenuItem setMenu:self];
1207  [self _highlightItemAtIndex:CPNotFound];
1208  [_items insertObject:aMenuItem atIndex:anIndex];
1209 
1211  postNotificationName:CPMenuDidAddItemNotification
1212  object:self
1213  userInfo:@{ @"CPMenuItemIndex": anIndex }];
1214 }
1215 
1216 - (void)removeObjectFromItemsAtIndex:(CPUInteger)anIndex
1217 {
1218  if (anIndex < 0 || anIndex >= [_items count])
1219  return;
1220 
1221  [[_items objectAtIndex:anIndex] setMenu:nil];
1222  [self _highlightItemAtIndex:CPNotFound];
1223  [_items removeObjectAtIndex:anIndex];
1224 
1226  postNotificationName:CPMenuDidRemoveItemNotification
1227  object:self
1228  userInfo:@{ @"CPMenuItemIndex": anIndex }];
1229 }
1230 
1231 @end
1232 
1233 var CPMenuTitleKey = @"CPMenuTitleKey",
1234  CPMenuNameKey = @"CPMenuNameKey",
1235  CPMenuItemsKey = @"CPMenuItemsKey",
1236  CPMenuShowsStateColumnKey = @"CPMenuShowsStateColumnKey",
1237  CPMenuAutoEnablesItemsKey = @"CPMenuAutoEnablesItemsKey";
1238 
1239 @implementation CPMenu (CPCoding)
1240 
1246 - (id)initWithCoder:(CPCoder)aCoder
1247 {
1248  self = [super init];
1249 
1250  if (self)
1251  {
1252  _title = [aCoder decodeObjectForKey:CPMenuTitleKey];
1253  _items = [aCoder decodeObjectForKey:CPMenuItemsKey];
1254 
1255  [self _setMenuName:[aCoder decodeObjectForKey:CPMenuNameKey]];
1256 
1257  _showsStateColumn = ![aCoder containsValueForKey:CPMenuShowsStateColumnKey] || [aCoder decodeBoolForKey:CPMenuShowsStateColumnKey];
1258 
1259  _autoenablesItems = ![aCoder containsValueForKey:CPMenuAutoEnablesItemsKey] || [aCoder decodeBoolForKey:CPMenuAutoEnablesItemsKey];
1260 
1261  [self setMinimumWidth:0];
1262  }
1263 
1264  return self;
1265 }
1266 
1271 - (void)encodeWithCoder:(CPCoder)aCoder
1272 {
1273  [aCoder encodeObject:_title forKey:CPMenuTitleKey];
1274 
1275  if (_name)
1276  [aCoder encodeObject:_name forKey:CPMenuNameKey];
1277 
1278  [aCoder encodeObject:_items forKey:CPMenuItemsKey];
1279 
1280  if (!_showsStateColumn)
1281  [aCoder encodeBool:_showsStateColumn forKey:CPMenuShowsStateColumnKey];
1282 
1283  if (!_autoenablesItems)
1284  [aCoder encodeBool:_autoenablesItems forKey:CPMenuAutoEnablesItemsKey];
1285 }
1286 
1287 @end
1288 
1289 
Used to implement exception handling (creating & raising).
Definition: CPException.h:2
Definition: CPFont.h:2
var CPMenuAutoEnablesItemsKey
Definition: CPMenu.j:1237
Definition: CPMenu.h:2
void popUpContextMenu:withEvent:forView:withFont:(CPMenu aMenu, [withEvent] CPEvent anEvent, [forView] CPView aView, [withFont] CPFont aFont)
Definition: CPMenu.j:862
void willChangeValueForKey:(CPString aKey)
id init()
Definition: CALayer.j:126
CPDictionary infoForBinding:forObject:(CPString aBinding, [forObject] id anObject)
float menuBarHeight()
Definition: CPMenu.j:238
var isEqual
void postNotificationName:object:userInfo:(CPString aNotificationName, [object] id anObject, [userInfo] CPDictionary aUserInfo)
CPArray itemArray()
Definition: CPMenu.j:444
CPFont systemFontOfSize:(CGSize aSize)
Definition: CPFont.j:282
void setMenu:(CPMenu aMenu)
Definition: CPMenuItem.j:558
CPMenuItem insertItemWithTitle:action:keyEquivalent:atIndex:(CPString aTitle, [action] SEL anAction, [keyEquivalent] CPString aKeyEquivalent, [atIndex] CPUInteger anIndex)
Definition: CPMenu.j:299
The main run loop for the application.
Definition: CPRunLoop.h:2
void performSelector:target:argument:order:modes:(SEL aSelector, [target] id aTarget, [argument] id anArgument, [order] int anOrder, [modes] CPArray modes)
Definition: CPRunLoop.j:253
CGPoint locationInWindow()
Definition: CPEvent.j:290
id delegate()
Definition: CALayer.j:965
id initWithTitle:(CPString aTitle)
Definition: CPMenu.j:257
void setMinimumWidth:(float aMinimumWidth)
Definition: CPMenu.j:709
Function trackingCallbackWithCallback:(Function aCallback)
Definition: CPMenu.j:841
int indexOfItemWithTitle:(CPString aTitle)
Definition: CPMenu.j:468
var CPMenuDelegate_menuDidClose_
Definition: CPMenu.j:36
A collection of unique integers.
Definition: CPIndexSet.h:2
CPString charactersIgnoringModifiers()
Definition: CPEvent.j:392
function CPSelectorFromString(aSelectorName)
Definition: CPObjJRuntime.j:28
unsigned modifierFlags()
Definition: CPEvent.j:309
CPMenu menu()
Definition: CPMenuItem.j:566
CPNotificationCenter defaultCenter()
A mutable key-value pair collection.
Definition: CPDictionary.h:2
CPRunLoop currentRunLoop()
Definition: CPRunLoop.j:232
var CPMenuNameKey
Definition: CPMenu.j:1234
CPWindow window()
Definition: CPView.j:527
void update()
Definition: CPMenu.j:627
void removeItemAtIndex:(CPUInteger anIndex)
Definition: CPMenu.j:343
void setMenuBarVisible:(BOOL menuBarShouldBeVisible)
Definition: CPMenu.j:95
An immutable string (collection of characters).
Definition: CPString.h:2
id objectForKey:(id aKey)
Definition: CPDictionary.j:515
Definition: CPImage.h:2
var CPMenuShowsStateColumnKey
Definition: CPMenu.j:1236
var CPMenuTitleKey
Definition: CPMenu.j:1233
CPMenuDidRemoveItemNotification
Definition: CPMenu.j:40
CGPoint convertBaseToGlobal:(CGPoint aPoint)
Definition: CPWindow.j:3603
id valueForKey:(CPString aKey)
CPBinder getBinding:forObject:(CPString aBinding, [forObject] id anObject)
CGPoint convertPoint:toView:(CGPoint aPoint, [toView] CPView aView)
Definition: CPView.j:2273
int tag
void performActionForItemAtIndex:(CPUInteger anIndex)
Definition: CPMenu.j:1074
CPWindow window()
Definition: CPEvent.j:341
CPMenuDidEndTrackingNotification
Definition: CPMenu.j:42
void didChangeValueForKey:(CPString aKey)
CPEvent otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:(CPEventType anEventType, [location] CGPoint aLocation, [modifierFlags] unsigned modifierFlags, [timestamp] CPTimeInterval aTimestamp, [windowNumber] int aWindowNumber, [context] CPGraphicsContext aGraphicsContext, [subtype] short aSubtype, [data1] int aData1, [data2] int aData2)
Definition: CPEvent.j:174
CPTheme defaultTheme()
Definition: CPTheme.j:44
CPInternalInconsistencyException
Definition: CPException.j:28
float systemFontSize()
Definition: CPFont.j:168
void insertObject:inItemsAtIndex:(CPMenuItem aMenuItem, [inItemsAtIndex] CPUInteger anIndex)
Definition: CPMenu.j:1196
Defines methods for use when archiving & restoring (enc/decoding).
Definition: CPCoder.h:2
CPNotFound
Definition: CPObjJRuntime.j:62
void setValue:forKey:(id aValue, [forKey] CPString aKey)
id init()
Definition: CPObject.j:145
Sends messages (CPNotification) between objects.
CPMenuDidChangeItemNotification
Definition: CPMenu.j:39
void removeObjectFromItemsAtIndex:(CPUInteger anIndex)
Definition: CPMenu.j:1216
Definition: CPTheme.h:2
void setAction:(SEL anAction)
Definition: CPMenuItem.j:238
id initWithTitle:action:keyEquivalent:(CPString aTitle, [action] SEL anAction, [keyEquivalent] CPString aKeyEquivalent)
Definition: CPMenuItem.j:121
void setTarget:(id aTarget)
Definition: CPMenuItem.j:221
void insertItem:atIndex:(CPMenuItem aMenuItem, [atIndex] CPUInteger anIndex)
Definition: CPMenu.j:286
var CPMenuItemsKey
Definition: CPMenu.j:1235
Definition: CPEvent.h:2
Class class()
Definition: CPObject.j:179
var CPMenuDelegate_menuWillOpen_
Definition: CPMenu.j:35
CPPlatformWindow platformWindow()
Definition: CPWindow.j:389
CPMenuDidAddItemNotification
Definition: CPMenu.j:38
int indexOfItemWithTag:(int aTag)
Definition: CPMenu.j:485
id valueForAttributeWithName:forClass:(CPString aName, [forClass] id aClass)
Definition: CPTheme.j:218
void setSubmenu:(CPMenu aMenu)
Definition: CPMenuItem.j:483
CPDictionary copy()
Definition: CPDictionary.j:292
CPPlatformWindow primaryPlatformWindow()
CPMenu menu
id alloc()
Definition: CPObject.j:130
Definition: CPView.j:137