1/** <title>NSMenu</title> 2 3 Copyright (C) 1999,2016 Free Software Foundation, Inc. 4 5 Author: Fred Kiefer <FredKiefer@gmx.de> 6 Date: Aug 2001 7 Author: David Lazaro Saz <khelekir@encomix.es> 8 Date: Oct 1999 9 Author: Michael Hanni <mhanni@sprintmail.com> 10 Date: 1999 11 Author: Felipe A. Rodriguez <far@ix.netcom.com> 12 Date: July 1998 13 and: 14 Author: Ovidiu Predescu <ovidiu@net-community.com> 15 Date: May 1997 16 17 This file is part of the GNUstep GUI Library. 18 19 This library is free software; you can redistribute it and/or 20 modify it under the terms of the GNU Lesser General Public 21 License as published by the Free Software Foundation; either 22 version 2 of the License, or (at your option) any later version. 23 24 This library is distributed in the hope that it will be useful, 25 but WITHOUT ANY WARRANTY; without even the implied warranty of 26 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 27 Lesser General Public License for more details. 28 29 You should have received a copy of the GNU Lesser General Public 30 License along with this library; see the file COPYING.LIB. 31 If not, see <http://www.gnu.org/licenses/> or write to the 32 Free Software Foundation, 51 Franklin Street, Fifth Floor, 33 Boston, MA 02110-1301, USA. 34*/ 35 36#import "config.h" 37#import <Foundation/NSCoder.h> 38#import <Foundation/NSArray.h> 39#import <Foundation/NSCharacterSet.h> 40#import <Foundation/NSDebug.h> 41#import <Foundation/NSException.h> 42#import <Foundation/NSProcessInfo.h> 43#import <Foundation/NSString.h> 44#import <Foundation/NSNotification.h> 45#import <Foundation/NSNotificationQueue.h> 46#import <Foundation/NSRunLoop.h> 47#import <Foundation/NSUserDefaults.h> 48#import <Foundation/NSValue.h> 49 50#import "AppKit/NSMatrix.h" 51#import "AppKit/NSApplication.h" 52#import "AppKit/NSCursor.h" 53#import "AppKit/NSWindow.h" 54#import "AppKit/NSEvent.h" 55#import "AppKit/NSFont.h" 56#import "AppKit/NSImage.h" 57#import "AppKit/NSMenu.h" 58#import "AppKit/NSMenuItem.h" 59#import "AppKit/NSMenuView.h" 60#import "AppKit/NSPanel.h" 61#import "AppKit/NSPopUpButton.h" 62#import "AppKit/NSPopUpButtonCell.h" 63#import "AppKit/NSScreen.h" 64#import "AppKit/NSAttributedString.h" 65 66#import "GSGuiPrivate.h" 67#import "NSDocumentFrameworkPrivate.h" 68#import "GNUstepGUI/GSTheme.h" 69 70/* 71 Drawing related: 72 73 NSMenu superMenu (if not root menu, the parent meu) 74 ^ 75 | 76 | +------------------> NSMenuView view (content, draws the menu items) 77 | | 78 NSMenu +----------+-------> NSMenuPanel A (regular window, torn off window) 79 | | `-------> NSMenuPanel B (transient window) 80 | | 81 | +------------------> NSString title (title) 82 | 83 v 84 NSMenu attachedMenu (the menu attached to this one, during navigation) 85 86 87 88 +--[NSMenuPanel]------+ 89 | +-[NSMenuView]----+ | 90 | | title if applic | | 91 | | +-------------+ | | 92 | | | NSMenuItem- | | | 93 | | | Cell | | | 94 | | +-------------+ | | 95 | | . | | 96 | | . | | 97 | +-----------------+ | 98 +---------------------+ 99 100 The two windows 101 --------------- 102 103 Basically we have for a menu two windows, window A and window B. 104 Window A is the regular window and Window B is used for transient windows. 105 106 At any one time, the views, like title view, NSMenuView are put either in 107 window A or in window B. They are moved over from one window to the oter 108 when needed. 109 110 the code is supposed to know when it is using window A or B. 111 But it will probably only work correctly when 112 113 window A correspond to transient == NO 114 window B correspond to transient == YES 115*/ 116 117 118/* Subclass of NSPanel since menus cannot become key */ 119@interface NSMenuPanel : NSPanel 120{ 121 NSMenu *_the_menu; 122} 123- (void) _setmenu: (NSMenu *)menu; 124@end 125 126@interface NSMenuView (GNUstepPrivate) 127- (NSArray *)_itemCells; 128@end 129 130 131static NSZone *menuZone = NULL; 132static NSString *NSMenuLocationsKey = @"NSMenuLocations"; 133static NSString *NSEnqueuedMenuMoveName = @"EnqueuedMoveNotificationName"; 134static NSNotificationCenter *nc; 135static BOOL menuBarVisible = YES; 136 137@interface NSMenu (GNUstepPrivate) 138 139- (NSString *) _name; 140- (void) _setName: (NSString *)name; 141- (NSMenuPanel *) _createWindow; 142- (NSString *) _locationKey; 143- (void) _rightMouseDisplay: (NSEvent*)theEvent; 144- (void) _setGeometry; 145- (void) _updateUserDefaults: (id) notification; 146- (void) _organizeMenu; 147- (BOOL) _isVisible; 148- (BOOL) _isMain; 149 150@end 151 152 153@implementation NSMenuPanel 154- (void) _setmenu: (NSMenu *)menu 155{ 156 _the_menu = menu; 157} 158 159- (BOOL) canBecomeKeyWindow 160{ 161 /* See [NSWindow-_lossOfKeyOrMainWindow] */ 162 return [_the_menu _isMain]; 163} 164 165- (void) orderFrontRegardless 166{ 167 NSInterfaceStyle style = NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil); 168 if (style == NSWindows95InterfaceStyle) 169 { 170 // if we're the top level menu in Windows mode, don't show it. 171 if([_the_menu supermenu] == nil && [_the_menu _ownedByPopUp] == NO) 172 { 173 return; 174 } 175 } 176 [super orderFrontRegardless]; 177} 178@end 179 180@implementation NSMenu (GNUstepPrivate) 181 182- (NSString *) _name; 183{ 184 return _name; 185} 186 187- (void) _setName: (NSString *)aName 188{ 189 ASSIGNCOPY(_name, aName); 190} 191 192- (NSString*) _locationKey 193{ 194 NSInterfaceStyle style = NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil); 195 if (style == NSMacintoshInterfaceStyle 196 || style == NSWindows95InterfaceStyle) 197 { 198 return nil; 199 } 200 if (_superMenu == nil) 201 { 202 if ([self _isMain]) 203 { 204 return @"\033"; /* Root menu. */ 205 } 206 else 207 { 208 return nil; /* Unused menu. */ 209 } 210 } 211 else if (_superMenu->_superMenu == nil) 212 { 213 return [NSString stringWithFormat: @"\033%@", [self title]]; 214 } 215 else 216 { 217 return [[_superMenu _locationKey] stringByAppendingFormat: @"\033%@", 218 [self title]]; 219 } 220} 221 222/* Create a non autorelease window for this menu. */ 223- (NSMenuPanel*) _createWindow 224{ 225 NSMenuPanel *win = [[NSMenuPanel alloc] 226 initWithContentRect: NSZeroRect 227 styleMask: NSBorderlessWindowMask 228 backing: NSBackingStoreBuffered 229 defer: YES]; 230 [win setBackgroundColor: [NSColor clearColor]]; 231 [win setLevel: NSSubmenuWindowLevel]; 232 [win setWorksWhenModal: NO]; 233 [win setBecomesKeyOnlyIfNeeded: YES]; 234 [win _setmenu: self]; 235 236 return win; 237} 238 239/** 240 Will track the mouse movement. It will trigger the updating of the user 241 defaults in due time. 242*/ 243- (void) _menuMoved: (id) notification 244{ 245 NSNotification *resend; 246 247 resend = [NSNotification notificationWithName: NSEnqueuedMenuMoveName 248 object: self]; 249 250 [[NSNotificationQueue defaultQueue] 251 enqueueNotification: resend 252 postingStyle: NSPostASAP 253 coalesceMask: NSNotificationCoalescingOnSender 254 forModes: [NSArray arrayWithObject: NSDefaultRunLoopMode]]; 255} 256 257- (void) _organizeMenu 258{ 259 NSString *infoString = _(@"Info"); 260 NSString *servicesString = _(@"Services"); 261 int i; 262 263 if ([self _isMain]) 264 { 265 NSString *appTitle; 266 NSMenu *appMenu; 267 id <NSMenuItem> appItem; 268 269 appTitle = [[[NSBundle mainBundle] localizedInfoDictionary] 270 objectForKey: @"ApplicationName"]; 271 if (nil == appTitle) 272 { 273 appTitle = [[NSProcessInfo processInfo] processName]; 274 } 275 appItem = [self itemWithTitle: appTitle]; 276 appMenu = [appItem submenu]; 277 278 if (_menu.horizontal == YES) 279 { 280 NSMutableArray *itemsToMove; 281 282 itemsToMove = [NSMutableArray new]; 283 284 if (appMenu == nil) 285 { 286 [self insertItemWithTitle: appTitle 287 action: NULL 288 keyEquivalent: @"" 289 atIndex: 0]; 290 appItem = [self itemAtIndex: 0]; 291 appMenu = [NSMenu new]; 292 [self setSubmenu: appMenu forItem: appItem]; 293 RELEASE(appMenu); 294 } 295 else 296 { 297 int index = [self indexOfItem: appItem]; 298 299 if (index != 0) 300 { 301 RETAIN (appItem); 302 [self removeItemAtIndex: index]; 303 [self insertItem: appItem atIndex: 0]; 304 RELEASE (appItem); 305 } 306 } 307 308 if ([[GSTheme theme] menuShouldShowIcon]) 309 { 310 NSImage *ti; 311 float bar; 312 ti = [[NSApp applicationIconImage] copy]; 313 if (ti == nil) 314 { 315 ti = [[NSImage imageNamed: @"GNUstep"] copy]; 316 } 317 [ti setScalesWhenResized: YES]; 318 bar = [NSMenuView menuBarHeight] - 4; 319 [ti setSize: NSMakeSize(bar, bar)]; 320 [appItem setImage: ti]; 321 RELEASE(ti); 322 } 323 324 // Collect all simple items plus "Info" and "Services" 325 for (i = 1; i < [_items count]; i++) 326 { 327 NSMenuItem *anItem = [_items objectAtIndex: i]; 328 NSString *title = [anItem title]; 329 NSMenu *submenu = [anItem submenu]; 330 331 if (submenu == nil) 332 { 333 [itemsToMove addObject: anItem]; 334 } 335 else 336 { 337 // The menu may not be localized, so we have to 338 // check both the English and the local version. 339 if ([title isEqual: @"Info"] || 340 [title isEqual: @"Services"] || 341 [title isEqual: infoString] || 342 [title isEqual: servicesString]) 343 { 344 [itemsToMove addObject: anItem]; 345 } 346 } 347 } 348 349 for (i = 0; i < [itemsToMove count]; i++) 350 { 351 NSMenuItem *anItem = [itemsToMove objectAtIndex: i]; 352 353 [self removeItem: anItem]; 354 [appMenu addItem: anItem]; 355 } 356 357 RELEASE(itemsToMove); 358 } 359 else 360 { 361 [appItem setImage: nil]; 362 if (appMenu != nil) 363 { 364 NSArray *array = [NSArray arrayWithArray: [appMenu itemArray]]; 365 /* 366 * Everything above the Serives menu goes into the info submenu, 367 * the rest into the main menu. 368 */ 369 int k = [appMenu indexOfItemWithTitle: servicesString]; 370 371 // The menu may not be localized, so we have to 372 // check both the English and the local version. 373 if (k == -1) 374 k = [appMenu indexOfItemWithTitle: @"Services"]; 375 376 if ((k > 0) && ([[array objectAtIndex: k - 1] isSeparatorItem])) 377 k--; 378 379 if (k == 1) 380 { 381 // Exactly one info item 382 NSMenuItem *anItem = [array objectAtIndex: 0]; 383 384 [appMenu removeItem: anItem]; 385 [self insertItem: anItem atIndex: 0]; 386 } 387 else if (k > 1) 388 { 389 id <NSMenuItem> infoItem; 390 NSMenu *infoMenu; 391 392 // Multiple info items, add a submenu for them 393 [self insertItemWithTitle: infoString 394 action: NULL 395 keyEquivalent: @"" 396 atIndex: 0]; 397 infoItem = [self itemAtIndex: 0]; 398 infoMenu = [NSMenu new]; 399 [self setSubmenu: infoMenu forItem: infoItem]; 400 RELEASE(infoMenu); 401 402 for (i = 0; i < k; i++) 403 { 404 NSMenuItem *anItem = [array objectAtIndex: i]; 405 406 [appMenu removeItem: anItem]; 407 [infoMenu addItem: anItem]; 408 } 409 } 410 else 411 { 412 // No service menu, or it is the first item. 413 // We still look for an info item. 414 NSMenuItem *anItem = [array objectAtIndex: 0]; 415 NSString *title = [anItem title]; 416 417 // The menu may not be localized, so we have to 418 // check both the English and the local version. 419 if ([title isEqual: @"Info"] || 420 [title isEqual: infoString]) 421 { 422 [appMenu removeItem: anItem]; 423 [self insertItem: anItem atIndex: 0]; 424 k = 1; 425 } 426 else 427 { 428 k = 0; 429 } 430 } 431 432 // Copy the remaining entries. 433 for (i = k; i < [array count]; i++) 434 { 435 NSMenuItem *anItem = [array objectAtIndex: i]; 436 437 [appMenu removeItem: anItem]; 438 [self addItem: anItem]; 439 } 440 441 [self removeItem: appItem]; 442 } 443 } 444 } 445 446 // recurse over all submenus 447 for (i = 0; i < [_items count]; i++) 448 { 449 NSMenuItem *anItem = [_items objectAtIndex: i]; 450 NSMenu *submenu = [anItem submenu]; 451 452 if (submenu != nil) 453 { 454 if ([submenu isTransient]) 455 { 456 [submenu closeTransient]; 457 } 458 [submenu close]; 459 [submenu _organizeMenu]; 460 } 461 } 462 463 [[self menuRepresentation] update]; 464 [self sizeToFit]; 465} 466 467- (void) _setGeometry 468{ 469 NSPoint origin; 470 471 if (_menu.horizontal == YES) 472 { 473 NSRect screenFrame = [[NSScreen mainScreen] frame]; 474 origin = NSMakePoint (0, screenFrame.size.height 475 - [_aWindow frame].size.height); 476 origin.y += screenFrame.origin.y; 477 [_aWindow setFrameOrigin: origin]; 478 [_bWindow setFrameOrigin: origin]; 479 } 480 else 481 { 482 NSString *key; 483 484 if (nil != (key = [self _locationKey])) 485 { 486 NSUserDefaults *defaults; 487 NSDictionary *menuLocations; 488 NSString *location; 489 490 defaults = [NSUserDefaults standardUserDefaults]; 491 menuLocations = [defaults objectForKey: NSMenuLocationsKey]; 492 493 if ([menuLocations isKindOfClass: [NSDictionary class]]) 494 location = [menuLocations objectForKey: key]; 495 else 496 location = nil; 497 498 if (location && [location isKindOfClass: [NSString class]]) 499 { 500 [_aWindow setFrameFromString: location]; 501 [_bWindow setFrameFromString: location]; 502 return; 503 } 504 } 505 506 if ((_aWindow != nil) && ([_aWindow screen] != nil)) 507 { 508 origin = NSMakePoint(0, [[_aWindow screen] visibleFrame].size.height 509 - [_aWindow frame].size.height); 510 511 [_aWindow setFrameOrigin: origin]; 512 [_bWindow setFrameOrigin: origin]; 513 } 514 } 515} 516 517/** 518 Save the current menu position in the standard user defaults 519*/ 520- (void) _updateUserDefaults: (id) notification 521{ 522 if (_menu.horizontal == NO) 523 { 524 NSString *key; 525 526 NSDebugLLog (@"NSMenu", @"Synchronizing user defaults"); 527 key = [self _locationKey]; 528 if (key != nil) 529 { 530 NSUserDefaults *defaults; 531 NSMutableDictionary *menuLocations; 532 NSString *locString; 533 534 defaults = [NSUserDefaults standardUserDefaults]; 535 menuLocations = [defaults objectForKey: NSMenuLocationsKey]; 536 if ([menuLocations isKindOfClass: [NSDictionary class]]) 537 menuLocations = AUTORELEASE([menuLocations mutableCopy]); 538 else 539 menuLocations = nil; 540 541 if ([_aWindow isVisible] 542 && ([self isTornOff] || ([self _isMain]))) 543 { 544 if (menuLocations == nil) 545 { 546 menuLocations = AUTORELEASE([[NSMutableDictionary alloc] 547 initWithCapacity: 2]); 548 } 549 locString = [[self window] stringWithSavedFrame]; 550 [menuLocations setObject: locString forKey: key]; 551 } 552 else 553 { 554 [menuLocations removeObjectForKey: key]; 555 } 556 557 if ([menuLocations count] > 0) 558 { 559 [defaults setObject: menuLocations 560 forKey: NSMenuLocationsKey]; 561 } 562 else 563 { 564 [defaults removeObjectForKey: NSMenuLocationsKey]; 565 } 566 [defaults synchronize]; 567 } 568 } 569} 570 571- (void) _rightMouseDisplay: (NSEvent*)theEvent 572{ 573 [[GSTheme theme] rightMouseDisplay: self 574 forEvent: theEvent]; 575} 576 577- (BOOL) _isVisible 578{ 579 return [_aWindow isVisible] || [_bWindow isVisible]; 580} 581 582- (BOOL) _isMain 583{ 584 return [NSApp mainMenu] == self; 585} 586 587@end 588 589 590@implementation NSMenu 591 592/* 593 * Class Methods 594 */ 595+ (void) initialize 596{ 597 if (self == [NSMenu class]) 598 { 599 [self setVersion: 1]; 600 nc = [NSNotificationCenter defaultCenter]; 601 } 602} 603 604+ (void) setMenuZone: (NSZone*)zone 605{ 606 menuZone = zone; 607} 608 609+ (NSZone*) menuZone 610{ 611 return menuZone; 612} 613 614+ (BOOL) menuBarVisible 615{ 616 return menuBarVisible; 617} 618 619+ (void) setMenuBarVisible: (BOOL)flag 620{ 621 menuBarVisible = flag; 622} 623 624/* 625 * 626 */ 627- (id) init 628{ 629 return [self initWithTitle: [[NSProcessInfo processInfo] processName]]; 630} 631 632- (void) dealloc 633{ 634 [nc removeObserver: self]; 635 636 // Now clean the pointer to us stored each _items element 637 [_items makeObjectsPerformSelector: @selector(setMenu:) withObject: nil]; 638 639 RELEASE(_notifications); 640 RELEASE(_title); 641 RELEASE(_items); 642 [_view setMenu: nil]; 643 RELEASE(_view); 644 RELEASE(_aWindow); 645 RELEASE(_bWindow); 646 RELEASE(_name); 647 648 [super dealloc]; 649} 650 651/* 652 653*/ 654- (id) initWithTitle: (NSString*)aTitle 655{ 656 NSMenuView *menuRep; 657 658 self = [super init]; 659 if (!self) 660 return nil; 661 662 // Keep the title. 663 ASSIGN(_title, aTitle); 664 665 // Create an array to store our menu items. 666 _items = [[NSMutableArray alloc] init]; 667 668 _menu.changedMessagesEnabled = YES; 669 _notifications = [[NSMutableArray alloc] init]; 670 _menu.needsSizing = YES; 671 // According to the spec, menus do autoenable by default. 672 _menu.autoenable = YES; 673 674 675 /* Please note that we own all this menu network of objects. So, 676 none of these objects should be retaining us. When we are deallocated, 677 we release all the objects we own, and that should cause deallocation 678 of the whole menu network. */ 679 680 // Create the windows that will display the menu. 681 _aWindow = [self _createWindow]; 682 _bWindow = [self _createWindow]; 683 [_bWindow setLevel: NSPopUpMenuWindowLevel]; 684 685 // Create a NSMenuView to draw our menu items. 686 menuRep = [[NSMenuView alloc] initWithFrame: NSZeroRect]; 687 [self setMenuRepresentation: menuRep]; 688 RELEASE(menuRep); 689 690 /* Set up the notification to start the process of redisplaying 691 the menus where the user left them the last time. 692 693 Use NSApplicationDidFinishLaunching, and not 694 NSApplicationWillFinishLaunching, so that the programmer can set 695 up menus in NSApplicationWillFinishLaunching. 696 */ 697 [nc addObserver: self 698 selector: @selector(applicationDidFinishLaunching:) 699 name: NSApplicationDidFinishLaunchingNotification 700 object: NSApp]; 701 702 [nc addObserver: self 703 selector: @selector(_showOnActivateApp:) 704 name: NSApplicationWillBecomeActiveNotification 705 object: NSApp]; 706 707 [nc addObserver: self 708 selector: @selector (_menuMoved:) 709 name: NSWindowDidMoveNotification 710 object: _aWindow]; 711 712 [nc addObserver: self 713 selector: @selector (_updateUserDefaults:) 714 name: NSEnqueuedMenuMoveName 715 object: self]; 716 717 return self; 718} 719 720/* 721 * Setting Up the Menu Commands 722 */ 723 724 - (void) menuChanged 725{ 726 // propagate notification up to the main menu 727 if ([self _isMain]) 728 _menu.mainMenuChanged = YES; 729 else 730 [[self supermenu] menuChanged]; 731} 732 733- (void) insertItem: (id <NSMenuItem>)newItem 734 atIndex: (NSInteger)index 735{ 736 NSNotification *inserted; 737 NSDictionary *d; 738 739 if (![(id)newItem conformsToProtocol: @protocol(NSMenuItem)]) 740 { 741 NSLog(@"You must use an object that conforms to NSMenuItem.\n"); 742 return; 743 } 744 745 /* 746 * If the item is already attached to another menu it 747 * isn't added. 748 */ 749 if ([newItem menu] != nil) 750 { 751 NSLog(@"The object %@ is already attached to a menu, then it isn't possible to add it.\n", newItem); 752 return; 753 } 754 755 [_items insertObject: newItem atIndex: index]; 756 _menu.needsSizing = YES; 757 [(NSMenuView*)_view setNeedsSizing: YES]; 758 759 // Create the notification for the menu representation. 760 d = [NSDictionary 761 dictionaryWithObject: [NSNumber numberWithInteger: index] 762 forKey: @"NSMenuItemIndex"]; 763 inserted = [NSNotification 764 notificationWithName: NSMenuDidAddItemNotification 765 object: self 766 userInfo: d]; 767 768 if (_menu.changedMessagesEnabled) 769 [nc postNotification: inserted]; 770 else 771 [_notifications addObject: inserted]; 772 [self menuChanged]; 773 774 // Set this after the insert notification has been sent. 775 [newItem setMenu: self]; 776} 777 778- (id <NSMenuItem>) insertItemWithTitle: (NSString*)aString 779 action: (SEL)aSelector 780 keyEquivalent: (NSString*)charCode 781 atIndex: (NSInteger)index 782{ 783 NSMenuItem *anItem = [[NSMenuItem alloc] initWithTitle: aString 784 action: aSelector 785 keyEquivalent: charCode]; 786 787 // Insert the new item into the menu. 788 [self insertItem: anItem atIndex: index]; 789 790 // For returns sake. 791 return AUTORELEASE(anItem); 792} 793 794- (void) addItem: (id <NSMenuItem>)newItem 795{ 796 [self insertItem: newItem atIndex: [_items count]]; 797} 798 799- (id <NSMenuItem>) addItemWithTitle: (NSString*)aString 800 action: (SEL)aSelector 801 keyEquivalent: (NSString*)keyEquiv 802{ 803 return [self insertItemWithTitle: aString 804 action: aSelector 805 keyEquivalent: keyEquiv 806 atIndex: [_items count]]; 807} 808 809- (void) removeItem: (id <NSMenuItem>)anItem 810{ 811 NSInteger index = [self indexOfItem: anItem]; 812 813 if (-1 == index) 814 return; 815 816 [self removeItemAtIndex: index]; 817} 818 819- (void) removeItemAtIndex: (NSInteger)index 820{ 821 NSNotification *removed; 822 NSDictionary *d; 823 id anItem = [_items objectAtIndex: index]; 824 825 if (!anItem) 826 return; 827 828 [anItem setMenu: nil]; 829 [_items removeObjectAtIndex: index]; 830 _menu.needsSizing = YES; 831 [(NSMenuView*)_view setNeedsSizing: YES]; 832 833 d = [NSDictionary dictionaryWithObject: [NSNumber numberWithInteger: index] 834 forKey: @"NSMenuItemIndex"]; 835 removed = [NSNotification 836 notificationWithName: NSMenuDidRemoveItemNotification 837 object: self 838 userInfo: d]; 839 840 if (_menu.changedMessagesEnabled) 841 [nc postNotification: removed]; 842 else 843 [_notifications addObject: removed]; 844 [self menuChanged]; 845} 846 847- (void) itemChanged: (id <NSMenuItem>)anObject 848{ 849 NSNotification *changed; 850 NSDictionary *d; 851 NSInteger index = [self indexOfItem: anObject]; 852 853 if (-1 == index) 854 return; 855 856 _menu.needsSizing = YES; 857 [(NSMenuView*)_view setNeedsSizing: YES]; 858 859 d = [NSDictionary dictionaryWithObject: [NSNumber numberWithInteger: index] 860 forKey: @"NSMenuItemIndex"]; 861 changed = [NSNotification 862 notificationWithName: NSMenuDidChangeItemNotification 863 object: self 864 userInfo: d]; 865 866 if (_menu.changedMessagesEnabled) 867 [nc postNotification: changed]; 868 else 869 [_notifications addObject: changed]; 870 [self menuChanged]; 871 872 // Update the menu. 873 [self update]; 874} 875 876/* 877 * Finding Menu Items 878 */ 879- (id <NSMenuItem>) itemWithTag: (NSInteger)aTag 880{ 881 NSUInteger i; 882 NSUInteger count = [_items count]; 883 884 for (i = 0; i < count; i++) 885 { 886 id menuItem = [_items objectAtIndex: i]; 887 888 if ([menuItem tag] == aTag) 889 return menuItem; 890 } 891 return nil; 892} 893 894- (id <NSMenuItem>) itemWithTitle: (NSString*)aString 895{ 896 NSUInteger i; 897 NSUInteger count = [_items count]; 898 899 for (i = 0; i < count; i++) 900 { 901 id menuItem = [_items objectAtIndex: i]; 902 903 if ([[menuItem title] isEqualToString: aString]) 904 return menuItem; 905 } 906 return nil; 907} 908 909- (NSMenuItem *) itemAtIndex: (NSInteger)index 910{ 911 if (index >= [_items count] || index < 0) 912 [NSException raise: NSRangeException 913 format: @"Range error in method -itemAtIndex: "]; 914 915 return [_items objectAtIndex: index]; 916} 917 918- (NSInteger) numberOfItems 919{ 920 return [_items count]; 921} 922 923- (NSArray*) itemArray 924{ 925 return (NSArray*)_items; 926} 927 928/* 929 * Finding Indices of Menu Items 930 */ 931- (NSInteger) indexOfItem: (id <NSMenuItem>)anObject 932{ 933 NSUInteger index; 934 935 index = [_items indexOfObjectIdenticalTo: anObject]; 936 937 if (index == NSNotFound) 938 return -1; 939 else 940 return index; 941} 942 943- (NSInteger) indexOfItemWithTitle: (NSString*)aTitle 944{ 945 id anItem; 946 947 if ((anItem = [self itemWithTitle: aTitle])) 948 return [_items indexOfObjectIdenticalTo: anItem]; 949 else 950 return -1; 951} 952 953- (NSInteger) indexOfItemWithTag: (NSInteger)aTag 954{ 955 id anItem; 956 957 if ((anItem = [self itemWithTag: aTag])) 958 return [_items indexOfObjectIdenticalTo: anItem]; 959 else 960 return -1; 961} 962 963- (NSInteger) indexOfItemWithTarget: (id)anObject 964 andAction: (SEL)actionSelector 965{ 966 NSUInteger i; 967 NSUInteger count = [_items count]; 968 969 for (i = 0; i < count; i++) 970 { 971 NSMenuItem *menuItem = [_items objectAtIndex: i]; 972 973 if (actionSelector == 0 || sel_isEqual([menuItem action], actionSelector)) 974 { 975 // There are different possibilities to implement the check here 976 if ([menuItem target] == anObject) 977 { 978 return i; 979 } 980 } 981 } 982 983 return -1; 984} 985 986- (NSInteger) indexOfItemWithRepresentedObject: (id)anObject 987{ 988 NSUInteger i; 989 NSUInteger count = [_items count]; 990 991 for (i = 0; i < count; i++) 992 { 993 if ([[[_items objectAtIndex: i] representedObject] 994 isEqual: anObject]) 995 { 996 return i; 997 } 998 } 999 1000 return -1; 1001} 1002 1003- (NSInteger) indexOfItemWithSubmenu: (NSMenu *)anObject 1004{ 1005 NSUInteger i; 1006 NSUInteger count = [_items count]; 1007 1008 for (i = 0; i < count; i++) 1009 { 1010 id item = [_items objectAtIndex: i]; 1011 1012 if ([item hasSubmenu] && 1013 [[item submenu] isEqual: anObject]) 1014 { 1015 return i; 1016 } 1017 } 1018 1019 return -1; 1020} 1021 1022// 1023// Managing Submenus. 1024// 1025- (void) setSubmenu: (NSMenu *)aMenu 1026 forItem: (id <NSMenuItem>)anItem 1027{ 1028 [anItem setSubmenu: aMenu]; 1029} 1030 1031- (void) submenuAction: (id)sender 1032{ 1033} 1034 1035 1036- (NSMenu *) attachedMenu 1037{ 1038 if (_attachedMenu && _menu.transient 1039 && !_attachedMenu->_menu.transient) 1040 return nil; 1041 1042 return _attachedMenu; 1043} 1044 1045 1046/** 1047 Look for the semantics in the header. Note that 1048 this implementation works because there are ... cases: 1049 <enum> 1050 <item> 1051 This menu is transient, its supermenu is also transient. 1052 In this case we just do the check between the transient windows 1053 and everything is fine 1054 </item> 1055 <item> 1056 The menu is transient, its supermenu is not transient. 1057 This can go WRONG 1058 </item> 1059 </enum> 1060*/ 1061- (BOOL) isAttached 1062{ 1063 return _superMenu && [_superMenu attachedMenu] == self; 1064} 1065 1066- (BOOL) isTornOff 1067{ 1068 return _menu.is_tornoff; 1069} 1070 1071- (NSPoint) locationForSubmenu: (NSMenu*)aSubmenu 1072{ 1073 return [_view locationForSubmenu: aSubmenu]; 1074} 1075 1076- (NSMenu *) supermenu 1077{ 1078 return _superMenu; 1079} 1080 1081- (void) setSupermenu: (NSMenu *)supermenu 1082{ 1083 /* The supermenu retains us (indirectly). Do not retain it. */ 1084 _superMenu = supermenu; 1085} 1086 1087// 1088// Enabling and Disabling Menu Items 1089// 1090- (void) setAutoenablesItems: (BOOL)flag 1091{ 1092 _menu.autoenable = flag; 1093} 1094 1095- (BOOL) autoenablesItems 1096{ 1097 return _menu.autoenable; 1098} 1099 1100- (void) _updateSubmenu 1101{ 1102 if ([self _isVisible]) 1103 { 1104 // Update the menu items when the menu is visible... 1105 [self update]; 1106 } 1107 else 1108 { 1109 // ...else only progress to submenus 1110 NSUInteger i; 1111 NSUInteger count = [_items count]; 1112 1113 for (i = 0; i < count; i++) 1114 { 1115 NSMenuItem *item = [_items objectAtIndex: i]; 1116 1117 if ([item hasSubmenu]) 1118 { 1119 [[item submenu] _updateSubmenu]; 1120 } 1121 } 1122 } 1123} 1124 1125- (void) _updateMenuWithDelegate 1126{ 1127 if ([_delegate respondsToSelector: @selector(menuNeedsUpdate:)]) 1128 { 1129 [_delegate menuNeedsUpdate: self]; 1130 } 1131 else if ([_delegate respondsToSelector: @selector(numberOfItemsInMenu:)]) 1132 { 1133 NSInteger num; 1134 1135 num = [_delegate numberOfItemsInMenu: self]; 1136 if (num > 0) 1137 { 1138 BOOL cont = YES; 1139 NSInteger i = 0; 1140 NSInteger curr = [self numberOfItems]; 1141 1142 while (num < curr) 1143 { 1144 [self removeItemAtIndex: --curr]; 1145 } 1146 while (num > curr) 1147 { 1148 [self insertItemWithTitle: @"" 1149 action: NULL 1150 keyEquivalent: @"" 1151 atIndex: curr++]; 1152 } 1153 1154 // FIXME: Should only process the items we display 1155 while (cont && i < num) 1156 { 1157 cont = [_delegate menu: self 1158 updateItem: (NSMenuItem*)[self itemAtIndex: i] 1159 atIndex: i 1160 shouldCancel: NO]; 1161 i++; 1162 } 1163 } 1164 } 1165} 1166 1167- (void) _autoenableItem: (NSMenuItem*)item 1168{ 1169 SEL action = [item action]; 1170 id validator = nil; 1171 BOOL wasEnabled = [item isEnabled]; 1172 BOOL shouldBeEnabled; 1173 1174 // If there is no action - there can be no validator for the item. 1175 if (action) 1176 { 1177 validator = [NSApp targetForAction: action 1178 to: [item target] 1179 from: item]; 1180 } 1181 else if (_popUpButtonCell != nil) 1182 { 1183 if (NULL != (action = [_popUpButtonCell action])) 1184 { 1185 validator = [NSApp targetForAction: action 1186 to: [_popUpButtonCell target] 1187 from: [_popUpButtonCell controlView]]; 1188 } 1189 } 1190 1191 if (validator == nil) 1192 { 1193 if ((action == NULL) && (_popUpButtonCell != nil)) 1194 { 1195 shouldBeEnabled = YES; 1196 } 1197 else 1198 { 1199 shouldBeEnabled = NO; 1200 } 1201 } 1202 else if ([validator respondsToSelector: @selector(validateMenuItem:)]) 1203 { 1204 shouldBeEnabled = [validator validateMenuItem: item]; 1205 } 1206 else if ([validator respondsToSelector: @selector(validateUserInterfaceItem:)]) 1207 { 1208 shouldBeEnabled = [validator validateUserInterfaceItem: item]; 1209 } 1210 else if ([item hasSubmenu] && [[item submenu] numberOfItems] == 0) 1211 { 1212 shouldBeEnabled = NO; 1213 } 1214 else 1215 { 1216 shouldBeEnabled = YES; 1217 } 1218 1219 if (shouldBeEnabled != wasEnabled) 1220 { 1221 [item setEnabled: shouldBeEnabled]; 1222 } 1223} 1224 1225- (void) update 1226{ 1227 if (_delegate) 1228 { 1229 [self _updateMenuWithDelegate]; 1230 } 1231 1232 // We use this as a recursion check. 1233 if (!_menu.changedMessagesEnabled) 1234 return; 1235 1236 if ([self autoenablesItems]) 1237 { 1238 NSUInteger i; 1239 NSUInteger count = [_items count]; 1240 1241 // Temporary disable automatic displaying of menu. 1242 [self setMenuChangedMessagesEnabled: NO]; 1243 1244 NS_DURING 1245 { 1246 for (i = 0; i < count; i++) 1247 { 1248 NSMenuItem *item = [_items objectAtIndex: i]; 1249 1250 if ([item hasSubmenu]) 1251 { 1252 [[item submenu] _updateSubmenu]; 1253 } 1254 1255 [self _autoenableItem: item]; 1256 } 1257 } 1258 NS_HANDLER 1259 { 1260 NSLog(@"Error Occurred While Updating Menu %@: %@", [self title], localException); 1261 } 1262 NS_ENDHANDLER 1263 // Reenable displaying of menus 1264 // this will send pending _notifications 1265 [self setMenuChangedMessagesEnabled: YES]; 1266 } 1267 1268 if (_menu.mainMenuChanged) 1269 { 1270 if (NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil) == NSWindows95InterfaceStyle) 1271 { 1272 [[GSTheme theme] updateAllWindowsWithMenu: self]; 1273 } 1274 _menu.mainMenuChanged = NO; 1275 } 1276 1277 if (_menu.needsSizing && [self _isVisible]) 1278 { 1279 NSDebugLLog (@"NSMenu", @" Calling Size To Fit (A)"); 1280 [self sizeToFit]; 1281 } 1282 1283 return; 1284} 1285 1286// 1287// Handling Keyboard Equivalents 1288// 1289- (BOOL) performKeyEquivalent: (NSEvent*)theEvent 1290{ 1291 NSUInteger i; 1292 NSUInteger count = [_items count]; 1293 NSEventType type = [theEvent type]; 1294 NSUInteger modifiers = [theEvent modifierFlags]; 1295 NSString *keyEquivalent = [theEvent charactersIgnoringModifiers]; 1296 NSUInteger relevantModifiersMask = NSCommandKeyMask | NSAlternateKeyMask | NSControlKeyMask; 1297 1298 if ((type != NSKeyDown && type != NSKeyUp) || [keyEquivalent length] == 0) 1299 return NO; 1300 1301 /* Take shift key into account only for control keys and arrow and function keys */ 1302 if ((modifiers & NSFunctionKeyMask) 1303 || ([keyEquivalent length] > 0 && [[NSCharacterSet controlCharacterSet] characterIsMember:[keyEquivalent characterAtIndex:0]])) 1304 relevantModifiersMask |= NSShiftKeyMask; 1305 1306 if (![self _isVisible] && _delegate) 1307 { 1308 // Need to enable items as the automatic mechanism is switched off for invisible menus 1309 [self update]; 1310 } 1311 1312 for (i = 0; i < count; i++) 1313 { 1314 NSMenuItem *item = [_items objectAtIndex: i]; 1315 1316 if ([item hasSubmenu]) 1317 { 1318 /* Ignore the Services submenu during menu traversal so that its key 1319 equivalents do not accidentally shadow standard key equivalents 1320 in the application's own menus. NSApp calls -performKeyEquivalent: 1321 explicitly for the Services menu when no matching key equivalent 1322 was found here (see NSApplication -sendEvent:). 1323 Note: Shadowing is no problem for a standard OpenStep menu, where 1324 the Services menu appears close to the end of the main menu, but 1325 is very likely for Macintosh or Windows 95 interface styles, where 1326 the Services menu appears in the first submenu of the main menu. */ 1327 // FIXME Should really remove conflicting key equivalents from the 1328 // menus so that users don't get confused. 1329 if ([item submenu] == [NSApp servicesMenu]) 1330 continue; 1331 // Recurse through submenus whether active or not. 1332 if ([[item submenu] performKeyEquivalent: theEvent]) 1333 { 1334 // The event has been handled by an item in the submenu. 1335 return YES; 1336 } 1337 } 1338 else 1339 { 1340 NSUInteger mask = [item keyEquivalentModifierMask]; 1341 1342 if ([[item keyEquivalent] isEqualToString: keyEquivalent] 1343 && (modifiers & relevantModifiersMask) == (mask & relevantModifiersMask)) 1344 { 1345 if (![self _isVisible] && !_delegate) 1346 { 1347 // Need to enable item as the automatic mechanism is switched off for invisible menus 1348 [self _autoenableItem: item]; 1349 } 1350 if ([item isEnabled]) 1351 { 1352 [_view performActionWithHighlightingForItemAtIndex: i]; 1353 } 1354 return YES; 1355 } 1356 } 1357 } 1358 return NO; 1359} 1360 1361// 1362// Simulating Mouse Clicks 1363// 1364- (void) performActionForItemAtIndex: (NSInteger)index 1365{ 1366 id<NSMenuItem> item = [_items objectAtIndex: index]; 1367 NSDictionary *d; 1368 SEL action; 1369 1370 if (![item isEnabled]) 1371 return; 1372 1373 // Send the actual action and the stipulated notifications. 1374 d = [NSDictionary dictionaryWithObject: item forKey: @"MenuItem"]; 1375 [nc postNotificationName: NSMenuWillSendActionNotification 1376 object: self 1377 userInfo: d]; 1378 1379 1380 if (_popUpButtonCell != nil) 1381 { 1382 // Tell the popup button, which item was selected 1383 [_popUpButtonCell selectItemAtIndex: index]; 1384 } 1385 1386 if ((action = [item action]) != NULL) 1387 { 1388 [NSApp sendAction: action 1389 to: [item target] 1390 from: item]; 1391 } 1392 else if (_popUpButtonCell != nil) 1393 { 1394 if ((action = [_popUpButtonCell action]) != NULL) 1395 [NSApp sendAction: action 1396 to: [_popUpButtonCell target] 1397 from: [_popUpButtonCell controlView]]; 1398 } 1399 1400 [nc postNotificationName: NSMenuDidSendActionNotification 1401 object: self 1402 userInfo: d]; 1403} 1404 1405// 1406// Setting the Title 1407// 1408- (void) setTitle: (NSString*)aTitle 1409{ 1410 ASSIGN(_title, aTitle); 1411 1412 _menu.needsSizing = YES; 1413 [(NSMenuView*)_view setNeedsSizing: YES]; 1414 if ([self _isVisible]) 1415 { 1416 [self sizeToFit]; 1417 } 1418} 1419 1420- (NSString*) title 1421{ 1422 return _title; 1423} 1424 1425- (id) delegate 1426{ 1427 return _delegate; 1428} 1429 1430- (void) setDelegate: (id)delegate 1431{ 1432 _delegate = delegate; 1433} 1434 1435- (float) menuBarHeight 1436{ 1437 // FIXME 1438 return [NSMenuView menuBarHeight]; 1439} 1440 1441// 1442// Setting the Representing Object 1443// 1444- (void) setMenuRepresentation: (id)menuRep 1445{ 1446 NSView *contentView; 1447 1448 if (![menuRep isKindOfClass: [NSMenuView class]]) 1449 { 1450 NSLog(@"You must use an NSMenuView, or a derivative thereof.\n"); 1451 return; 1452 } 1453 1454 /* If we are replacing a menu representation with a new version, 1455 * we should display it in the same view as the old representation. 1456 * If we can't find a view for that, we display in the content view 1457 * of our default window. 1458 */ 1459 if ([_view superview] == nil) 1460 { 1461 contentView = [_aWindow contentView]; 1462 } 1463 else 1464 { 1465 contentView = [_view superview]; 1466 } 1467 1468 if (_view == menuRep) 1469 { 1470 /* Hack ... if the representation was 'borrowed' for an in-window 1471 * menu, we will still have it recorded as ours, but it won't be 1472 * in our view hierarchy, so we have to re-add it. 1473 */ 1474 /* 1475 if (contentView != [menuRep superview]) 1476 { 1477 [contentView addSubview: menuRep]; 1478 } 1479 */ 1480 return; 1481 } 1482 1483 _menu.horizontal = [menuRep isHorizontal]; 1484 1485 if (_view != nil) 1486 { 1487 // remove the old representation 1488 [_view removeFromSuperview]; 1489 [_view setMenu: nil]; 1490 } 1491 1492 ASSIGN(_view, menuRep); 1493 [_view setMenu: self]; 1494 1495 // add the new representation 1496 [contentView addSubview: _view]; 1497} 1498 1499- (id) menuRepresentation 1500{ 1501 return _view; 1502} 1503 1504- (id) contextMenuRepresentation 1505{ 1506 return nil; 1507} 1508 1509- (void) setContextMenuRepresentation: (id)representation 1510{ 1511} 1512 1513- (id) tearOffMenuRepresentation 1514{ 1515 return nil; 1516} 1517 1518- (void) setTearOffMenuRepresentation: (id)representation 1519{ 1520} 1521 1522// 1523// Updating the Menu Layout 1524// 1525// Wim 20030301: Question, what happens when the notification trigger 1526// new notifications? I think it is not allowed to add items 1527// to the _notifications array while enumerating it. 1528- (void) setMenuChangedMessagesEnabled: (BOOL)flag 1529{ 1530 if (_menu.changedMessagesEnabled != flag) 1531 { 1532 if (flag) 1533 { 1534 if ([_notifications count]) 1535 { 1536 NSEnumerator *enumerator = [_notifications objectEnumerator]; 1537 id aNotification; 1538 1539 while ((aNotification = [enumerator nextObject])) 1540 [nc postNotification: aNotification]; 1541 } 1542 1543 // Clean the notification array. 1544 [_notifications removeAllObjects]; 1545 } 1546 1547 _menu.changedMessagesEnabled = flag; 1548 } 1549} 1550 1551- (BOOL) menuChangedMessagesEnabled 1552{ 1553 return _menu.changedMessagesEnabled; 1554} 1555 1556- (void) sizeToFit 1557{ 1558 NSRect oldWindowFrame; 1559 NSRect newWindowFrame; 1560 NSRect menuFrame; 1561 1562 [_view sizeToFit]; 1563 1564 menuFrame = [_view frame]; 1565 1566 // Main 1567 oldWindowFrame = [_aWindow frame]; 1568 newWindowFrame = [NSWindow frameRectForContentRect: menuFrame 1569 styleMask: [_aWindow styleMask]]; 1570 1571 if (oldWindowFrame.size.height > 1) 1572 { 1573 newWindowFrame.origin = NSMakePoint (oldWindowFrame.origin.x, 1574 oldWindowFrame.origin.y 1575 + oldWindowFrame.size.height 1576 - newWindowFrame.size.height); 1577 } 1578 [_aWindow setFrame: newWindowFrame display: NO]; 1579 1580 // Transient 1581 oldWindowFrame = [_bWindow frame]; 1582 newWindowFrame = [NSWindow frameRectForContentRect: menuFrame 1583 styleMask: [_bWindow styleMask]]; 1584 if (oldWindowFrame.size.height > 1) 1585 { 1586 newWindowFrame.origin = NSMakePoint (oldWindowFrame.origin.x, 1587 oldWindowFrame.origin.y 1588 + oldWindowFrame.size.height 1589 - newWindowFrame.size.height); 1590 } 1591 [_bWindow setFrame: newWindowFrame display: NO]; 1592 1593 if (_popUpButtonCell == nil) 1594 { 1595 [_view setFrameOrigin: NSMakePoint (0, 0)]; 1596 } 1597 1598 [_view setNeedsDisplay: YES]; 1599 1600 _menu.needsSizing = NO; 1601} 1602 1603/* 1604 * Displaying Context Sensitive Help 1605 */ 1606- (void) helpRequested: (NSEvent *)event 1607{ 1608 // TODO: Won't be implemented until we have NSHelp* 1609} 1610 1611+ (void) popUpContextMenu: (NSMenu*)menu 1612 withEvent: (NSEvent*)event 1613 forView: (NSView*)view 1614{ 1615 [self popUpContextMenu: menu 1616 withEvent: event 1617 forView: view 1618 withFont: nil]; 1619} 1620 1621+ (void) popUpContextMenu: (NSMenu *)menu 1622 withEvent: (NSEvent *)event 1623 forView: (NSView *)view 1624 withFont: (NSFont *)font 1625{ 1626 [menu _rightMouseDisplay: event]; 1627} 1628 1629/* 1630 * NSObject Protocol 1631 */ 1632- (BOOL) isEqual: (id)anObject 1633{ 1634 if (self == anObject) 1635 return YES; 1636 if ([anObject isKindOfClass: [NSMenu class]]) 1637 { 1638 if (![_title isEqualToString: [anObject title]]) 1639 return NO; 1640 return [[self itemArray] isEqual: [anObject itemArray]]; 1641 } 1642 return NO; 1643} 1644 1645/* 1646 * NSCoding Protocol 1647 */ 1648- (void) encodeWithCoder: (NSCoder*)encoder 1649{ 1650 if ([encoder allowsKeyedCoding]) 1651 { 1652 [encoder encodeObject: _title forKey: @"NSTitle"]; 1653 [encoder encodeObject: _items forKey: @"NSMenuItems"]; 1654 1655 if ([self _isMain]) 1656 { 1657 [encoder encodeObject: @"_NSMainMenu" forKey: @"NSName"]; 1658 } 1659 } 1660 else 1661 { 1662 BOOL autoenable = _menu.autoenable; 1663 1664 [encoder encodeObject: _title]; 1665 [encoder encodeObject: _items]; 1666 [encoder encodeValueOfObjCType: @encode(BOOL) at: &autoenable]; 1667 } 1668} 1669 1670- (id) initWithCoder: (NSCoder*)aDecoder 1671{ 1672 NSString *dTitle; 1673 NSString *dName; 1674 NSArray *dItems; 1675 BOOL dAuto; 1676 NSUInteger i; 1677 NSUInteger count; 1678 1679 if ([aDecoder allowsKeyedCoding]) 1680 { 1681 // 1682 // NSNoAutoenable is present when the "Autoenable" option is NOT checked. 1683 // NO = Autoenable menus, YES = Don't auto enable menus. We, therefore, 1684 // have to invert the values of this flag in order to get the value of 1685 // dAuto. 1686 // 1687 if ([aDecoder containsValueForKey: @"NSNoAutoenable"]) 1688 { 1689 dAuto = ![aDecoder decodeBoolForKey: @"NSNoAutoenable"]; 1690 } 1691 else 1692 { 1693 dAuto = YES; 1694 } 1695 dTitle = [aDecoder decodeObjectForKey: @"NSTitle"]; 1696 dItems = [aDecoder decodeObjectForKey: @"NSMenuItems"]; 1697 if ([aDecoder containsValueForKey: @"NSName"]) 1698 { 1699 dName = [aDecoder decodeObjectForKey: @"NSName"]; 1700 } 1701 else 1702 { 1703 dName = nil; 1704 } 1705 } 1706 else 1707 { 1708 dTitle = [aDecoder decodeObject]; 1709 dItems = [aDecoder decodeObject]; 1710 dName = nil; 1711 [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &dAuto]; 1712 } 1713 self = [self initWithTitle: dTitle]; 1714 [self setAutoenablesItems: dAuto]; 1715 [self _setName: dName]; 1716 1717 [self setMenuChangedMessagesEnabled: NO]; 1718 /* 1719 * Make sure that items and submenus are set correctly. 1720 */ 1721 count = [dItems count]; 1722 for (i = 0; i < count; i++) 1723 { 1724 NSMenuItem *item = [dItems objectAtIndex: i]; 1725 [self addItem: item]; 1726 } 1727 [self setMenuChangedMessagesEnabled: YES]; 1728 1729 return self; 1730} 1731 1732- (void) awakeFromNib 1733{ 1734 NSString *name = [self _name]; 1735 1736 if (name) 1737 { 1738 if ([name isEqualToString: @"_NSMainMenu"]) 1739 { 1740 // NB This is already handled by the nib loading code 1741 //[NSApp setMainMenu: self]; 1742 } 1743 else if ([name isEqualToString: @"_NSAppleMenu"]) 1744 { 1745 // GNUstep does not handle Apple's application menu specially 1746 } 1747 else if ([name isEqualToString: @"_NSWindowsMenu"]) 1748 { 1749 [NSApp setWindowsMenu: self]; 1750 } 1751 else if ([name isEqualToString: @"_NSServicesMenu"]) 1752 { 1753 [NSApp setServicesMenu: self]; 1754 } 1755 else if ([name isEqualToString: @"_NSRecentDocumentsMenu"]) 1756 { 1757 [[NSDocumentController sharedDocumentController] 1758 _setRecentDocumentsMenu: self]; 1759 } 1760 else if ([name isEqualToString: @"_NSFontMenu"]) 1761 { 1762 [[NSFontManager sharedFontManager] setFontMenu: self]; 1763 } 1764 } 1765} 1766 1767/* 1768 * NSCopying Protocol 1769 */ 1770- (id) copyWithZone: (NSZone*)zone 1771{ 1772 NSMenu *new = [[NSMenu allocWithZone: zone] initWithTitle: _title]; 1773 NSUInteger i; 1774 NSUInteger count = [_items count]; 1775 1776 [new setAutoenablesItems: _menu.autoenable]; 1777 for (i = 0; i < count; i++) 1778 { 1779 // This works because the copy on NSMenuItem sets the menu to nil!!! 1780 NSMenuItem *item = [[_items objectAtIndex: i] copyWithZone: zone]; 1781 [new insertItem: item atIndex: i]; 1782 RELEASE(item); 1783 } 1784 1785 return new; 1786} 1787 1788@end 1789 1790@implementation NSMenu (GNUstepExtra) 1791 1792- (void) setTornOff: (BOOL)flag 1793{ 1794 NSMenu *supermenu; 1795 1796 _menu.is_tornoff = flag; 1797 1798 if (flag) 1799 { 1800 supermenu = [self supermenu]; 1801 if (supermenu != nil) 1802 { 1803 [[supermenu menuRepresentation] setHighlightedItemIndex: -1]; 1804 supermenu->_attachedMenu = nil; 1805 } 1806 [nc addObserver: self 1807 selector: @selector(windowDidChangeScreen:) 1808 name: NSWindowDidBecomeKeyNotification 1809 object: nil]; 1810 [nc addObserver: self 1811 selector: @selector(windowDidChangeScreen:) 1812 name: NSWindowDidChangeScreenNotification 1813 object: nil]; 1814 } 1815 else 1816 { 1817 [nc removeObserver: self 1818 name: NSWindowDidBecomeKeyNotification 1819 object: nil]; 1820 [nc removeObserver: self 1821 name: NSWindowDidChangeScreenNotification 1822 object: nil]; 1823 } 1824 [_view update]; 1825} 1826 1827- (void) _showTornOffMenuIfAny: (NSNotification*)notification 1828{ 1829 NSInterfaceStyle style = NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil); 1830 if (style == NSMacintoshInterfaceStyle 1831 || style == NSWindows95InterfaceStyle) 1832 { 1833 return; 1834 } 1835 if (![self _isMain]) 1836 { 1837 NSString *key; 1838 1839 key = [self _locationKey]; 1840 if (key != nil) 1841 { 1842 NSString *location; 1843 NSUserDefaults *defaults; 1844 NSDictionary *menuLocations; 1845 1846 defaults = [NSUserDefaults standardUserDefaults]; 1847 menuLocations = [defaults objectForKey: NSMenuLocationsKey]; 1848 1849 if ([menuLocations isKindOfClass: [NSDictionary class]]) 1850 location = [menuLocations objectForKey: key]; 1851 else 1852 location = nil; 1853 if (location && [location isKindOfClass: [NSString class]]) 1854 { 1855 [self setTornOff: YES]; 1856 [self display]; 1857 } 1858 } 1859 } 1860} 1861 1862- (void) applicationDidFinishLaunching:(NSNotification *)notification 1863{ 1864 if (NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil) == 1865 NSWindows95InterfaceStyle) 1866 { 1867 [[GSTheme theme] updateAllWindowsWithMenu: [NSApp mainMenu]]; 1868 } 1869 [self _showTornOffMenuIfAny: notification]; 1870 1871 if ([NSApp mainMenu] == self) 1872 { 1873 [nc addObserver: self 1874 selector: @selector(windowDidChangeScreen:) 1875 name: NSWindowDidBecomeKeyNotification 1876 object: nil]; 1877 [nc addObserver: self 1878 selector: @selector(windowDidChangeScreen:) 1879 name: NSWindowDidChangeScreenNotification 1880 object: nil]; 1881 } 1882} 1883 1884- (void) _showOnActivateApp: (NSNotification*)notification 1885{ 1886 if ([self _isMain]) 1887 { 1888 [self display]; 1889 // we must make sure that any attached submenu is visible too. 1890 [[self attachedMenu] display]; 1891 } 1892} 1893 1894- (void) windowDidChangeScreen: (NSNotification*)notification 1895{ 1896 NSWindow *window = [notification object]; 1897 NSRect frame; 1898 NSRect oldScreenFrame; 1899 NSRect newScreenFrame; 1900 CGFloat yOffset; 1901 1902 if ([window isKindOfClass: [NSPanel class]] 1903 || window == _aWindow 1904 || [window isKeyWindow] == NO 1905 || [_aWindow screen] == [window screen] 1906 || [_aWindow isVisible] == NO) 1907 { 1908 return; 1909 } 1910 1911 oldScreenFrame = [[_aWindow screen] frame]; 1912 newScreenFrame = [[window screen] frame]; 1913 frame = [_aWindow frame]; 1914 1915 // Keep left offset fixed 1916 frame.origin.x += newScreenFrame.origin.x - oldScreenFrame.origin.x; 1917 1918 // Keep top offset fixed 1919 yOffset = NSMaxY(oldScreenFrame) - NSMaxY(frame); 1920 frame.origin.y = NSMaxY(newScreenFrame) - yOffset - frame.size.height; 1921 1922 // setFrame: changes _screen value. 1923 [self nestedSetFrameOrigin: frame.origin]; 1924} 1925 1926- (BOOL) isTransient 1927{ 1928 return _menu.transient; 1929} 1930 1931- (BOOL) isPartlyOffScreen 1932{ 1933 NSWindow *window; 1934 1935 window = [self window]; 1936 if ((nil != window) && (nil != [window screen])) 1937 { 1938 return !NSContainsRect([[window screen] visibleFrame], [window frame]); 1939 } 1940 else 1941 { 1942 NSLog(@"Menu has no window %@ or screen %@", window, [window screen]); 1943 return YES; 1944 } 1945} 1946 1947- (void) _performMenuClose: (id)sender 1948{ 1949 if (_attachedMenu) 1950 [_view detachSubmenu]; 1951 1952 [_view setHighlightedItemIndex: -1]; 1953 [self close]; 1954 [self setTornOff: NO]; 1955 [self _updateUserDefaults: nil]; 1956} 1957 1958- (void) display 1959{ 1960 if (_menu.transient) 1961 { 1962 NSDebugLLog (@"NSMenu", 1963 @"trying to display while already displayed transient"); 1964 } 1965 1966 [self update]; 1967 if (_menu.needsSizing) 1968 { 1969 [self sizeToFit]; 1970 } 1971 1972 if (_superMenu && ![self isTornOff]) 1973 { 1974 // query super menu for position 1975 [_aWindow setFrameOrigin: [_superMenu locationForSubmenu: self]]; 1976 _superMenu->_attachedMenu = self; 1977 } 1978 else if ([_aWindow frame].origin.y <= 0 1979 && _popUpButtonCell == nil) // get geometry only if not set 1980 { 1981 [self _setGeometry]; 1982 } 1983 1984 NSDebugLLog (@"NSMenu", 1985 @"Display, origin: %@", 1986 NSStringFromPoint ([_aWindow frame].origin)); 1987 1988 [_aWindow orderFrontRegardless]; 1989} 1990 1991- (void) displayTransient 1992{ 1993 NSPoint location; 1994 NSView *contentView; 1995 1996 if (_menu.transient) 1997 { 1998 NSDebugLLog (@"NSMenu", @"displaying transient while it is transient"); 1999 return; 2000 } 2001 2002 [self update]; 2003 if (_menu.needsSizing) 2004 { 2005 [self sizeToFit]; 2006 } 2007 2008 _oldHiglightedIndex = [[self menuRepresentation] highlightedItemIndex]; 2009 _menu.transient = YES; 2010 2011 /* 2012 * Cache the old submenu if any and query the supermenu our position. 2013 * Otherwise, raise menu under the mouse. 2014 */ 2015 if (_superMenu != nil) 2016 { 2017 _oldAttachedMenu = _superMenu->_attachedMenu; 2018 _superMenu->_attachedMenu = self; 2019 location = [_superMenu locationForSubmenu: self]; 2020 } 2021 else 2022 { 2023 NSRect frame = [_aWindow frame]; 2024 NSInterfaceStyle style; 2025 2026 location = [_aWindow mouseLocationOutsideOfEventStream]; 2027 location = [_aWindow convertBaseToScreen: location]; 2028 location.y -= frame.size.height; 2029 2030 /* When using the standard NextStep/OpenStep interface style, the 2031 center of the menu's title view is placed below the mouse cursor. 2032 However, in Macintosh and Windows95 styles, menus have no visible 2033 title. To prevent the user from accidentally selecting the first 2034 item, the top left edge is placed below the mouse cursor for them. */ 2035 style = NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil); 2036 if (style != NSWindows95InterfaceStyle && 2037 style != NSMacintoshInterfaceStyle) 2038 { 2039 location.x -= frame.size.width/2; 2040 if (location.x < 0) 2041 location.x = 0; 2042 location.y += 10; 2043 } 2044 } 2045 2046 [_bWindow setFrameOrigin: location]; 2047 2048 [_view removeFromSuperviewWithoutNeedingDisplay]; 2049 2050 contentView = [_bWindow contentView]; 2051 [contentView addSubview: _view]; 2052 2053 [_view update]; 2054 2055 [_bWindow orderFront: self]; 2056 2057 /* Right mouse buttons which display transient menus don't update 2058 * the cursor. So, the current cursor is displayed over the 2059 * contextual menu (for example an I beam). However, when menu is 2060 * closed the cursor pop, this can set a wrong cursor. We push here 2061 * an arrow cursor, the cursor we want at menus. Being sure that 2062 * this will pop when menu closes. 2063 */ 2064 [[NSCursor arrowCursor] push]; 2065} 2066 2067- (void) close 2068{ 2069 NSMenu *sub = [self attachedMenu]; 2070 2071 if (_menu.transient) 2072 { 2073 NSDebugLLog (@"NSMenu", @"We should not close ordinary menu while transient version is still open"); 2074 } 2075 2076 /* 2077 * If we have an attached submenu, we must close that too - but then make 2078 * sure we still have a record of it so that it can be re-displayed if we 2079 * are redisplayed. 2080 */ 2081 if (sub != nil) 2082 { 2083 [sub close]; 2084 _attachedMenu = sub; 2085 } 2086 [_aWindow orderOut: self]; 2087 2088 if (_superMenu && ![self isTornOff]) 2089 { 2090 _superMenu->_attachedMenu = nil; 2091 [[_superMenu menuRepresentation] setHighlightedItemIndex: -1]; 2092 } 2093} 2094 2095- (void) closeTransient 2096{ 2097 NSView *contentView; 2098 2099 if (_menu.transient == NO) 2100 { 2101 NSDebugLLog (@"NSMenu", 2102 @"Closing transient: %@ while it is NOT transient now", _title); 2103 return; 2104 } 2105 2106 [_bWindow orderOut: self]; 2107 [_view removeFromSuperviewWithoutNeedingDisplay]; 2108 2109 contentView = [_aWindow contentView]; 2110 [contentView addSubview: _view]; 2111 2112 [contentView setNeedsDisplay: YES]; 2113 2114 // Restore the old submenu (if any). 2115 if (_superMenu != nil) 2116 { 2117 _superMenu->_attachedMenu = _oldAttachedMenu; 2118 [[_superMenu menuRepresentation] setHighlightedItemIndex: 2119 [_superMenu indexOfItemWithSubmenu: _superMenu->_attachedMenu]]; 2120 } 2121 2122 [[self menuRepresentation] setHighlightedItemIndex: _oldHiglightedIndex]; 2123 2124 _menu.transient = NO; 2125 [_view update]; 2126} 2127 2128- (NSWindow*) window 2129{ 2130 if (_menu.transient) 2131 return (NSWindow *)_bWindow; 2132 else 2133 return (NSWindow *)_aWindow; 2134} 2135 2136- (void) setMain: (BOOL)isMain 2137{ 2138 if (isMain) 2139 { 2140 NSMenuView *oldRep; 2141 NSInterfaceStyle oldStyle; 2142 NSInterfaceStyle newStyle; 2143 2144 oldRep = [self menuRepresentation]; 2145 oldStyle = [oldRep interfaceStyle]; 2146 newStyle = NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil); 2147 2148 /* 2149 * If necessary, rebuild menu for (different) style 2150 */ 2151 if (oldStyle != newStyle) 2152 { 2153 NSMenuView *newRep; 2154 2155 if (oldStyle == NSWindows95InterfaceStyle) 2156 { 2157 /* Remove the menu from all main windows. 2158 */ 2159 [[GSTheme theme] updateAllWindowsWithMenu: nil]; 2160 } 2161 if (newStyle == NSWindows95InterfaceStyle) 2162 { 2163 [self close]; 2164 } 2165 newRep = [[NSMenuView alloc] initWithFrame: NSZeroRect]; 2166 if (newStyle == NSMacintoshInterfaceStyle 2167 || newStyle == NSWindows95InterfaceStyle) 2168 { 2169 [newRep setHorizontal: YES]; 2170 } 2171 else 2172 { 2173 [newRep setHorizontal: NO]; 2174 } 2175 [newRep setInterfaceStyle: newStyle]; 2176 [self setMenuRepresentation: newRep]; 2177 [self _organizeMenu]; 2178 RELEASE(newRep); 2179 if (newStyle == NSWindows95InterfaceStyle) 2180 { 2181 /* Put menu in all main windows for microsoft style. 2182 */ 2183 [[GSTheme theme] updateAllWindowsWithMenu: self]; 2184 } 2185 } 2186 2187 /* Adjust the menu window to suit the menu view unless the menu 2188 * is being displayed in the application main window. 2189 */ 2190 if (newStyle != NSWindows95InterfaceStyle) 2191 { 2192 [[self window] setTitle: [[NSProcessInfo processInfo] processName]]; 2193 [[self window] setLevel: NSMainMenuWindowLevel]; 2194 [self _setGeometry]; 2195 [self sizeToFit]; 2196 2197 if ([NSApp isActive]) 2198 { 2199 [self display]; 2200 } 2201 } 2202 } 2203 else 2204 { 2205 [[self window] setLevel: NSSubmenuWindowLevel]; 2206 } 2207} 2208 2209/** 2210 Set the frame origin of the receiver to aPoint. If a submenu of 2211 the receiver is attached. The frame origin of the submenu is set 2212 appropriately. 2213*/ 2214- (void) nestedSetFrameOrigin: (NSPoint) aPoint 2215{ 2216 NSWindow *theWindow = [self window]; 2217 2218 // Move ourself and get our width. 2219 [theWindow setFrameOrigin: aPoint]; 2220 2221 // Do the same for attached menus. 2222 if (_attachedMenu) 2223 { 2224 aPoint = [self locationForSubmenu: _attachedMenu]; 2225 [_attachedMenu nestedSetFrameOrigin: aPoint]; 2226 } 2227} 2228 2229#define SHIFT_DELTA 18.0 2230 2231- (void) shiftOnScreen 2232{ 2233 NSWindow *theWindow = [self window]; 2234 NSRect frameRect = [theWindow frame]; 2235 NSRect screenRect = [[theWindow screen] visibleFrame]; 2236 NSPoint vector = {0.0, 0.0}; 2237 BOOL moveIt = NO; 2238 NSPoint location = [theWindow mouseLocationOutsideOfEventStream]; 2239 NSPoint pointerLoc = [theWindow convertBaseToScreen: location]; 2240 NSInterfaceStyle style = NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil); 2241 2242 // Don't move the main menu bar in Macintosh interface style, this is 2243 // annoying (in particular, since the effective screen range is reduced 2244 // by the height of the menu bar!) 2245 if (style == NSMacintoshInterfaceStyle && [self _isMain]) 2246 return; 2247 2248 // 1 - determine the amount we need to shift in the y direction. 2249 if (pointerLoc.y <= 1 && NSMinY (frameRect) < 0) 2250 { 2251 vector.y = MIN (SHIFT_DELTA, -NSMinY (frameRect)); 2252 moveIt = YES; 2253 } 2254 /* Note: pointerLoc.y may be greater than NSMaxY(screenRect) if we have a 2255 horizontal menu bar at the top of the screen (Macintosh interface style) */ 2256 // FIXME Don't move the horizontal menu bar downward in this case! 2257 else if (pointerLoc.y >= NSMaxY(screenRect) && 2258 NSMaxY (frameRect) > NSMaxY (screenRect)) 2259 { 2260 vector.y = -MIN (SHIFT_DELTA, NSMaxY (frameRect) - NSMaxY (screenRect)); 2261 moveIt = YES; 2262 } 2263 2264 // 2 - determine the amount we need to shift in the x direction. 2265 if (pointerLoc.x == 0 && NSMinX (frameRect) < 0) 2266 { 2267 vector.x = MIN (SHIFT_DELTA, -NSMinX (frameRect)); 2268 moveIt = YES; 2269 } 2270 // Note the -3. This is done so the menu, after shifting completely 2271 // has some spare room on the right hand side. This is needed otherwise 2272 // the user can never access submenus of this menu. 2273 else if (pointerLoc.x == NSMaxX(screenRect) - 1 && 2274 NSMaxX (frameRect) > NSMaxX (screenRect) - 3) 2275 { 2276 vector.x 2277 = -MIN (SHIFT_DELTA, NSMaxX (frameRect) - NSMaxX (screenRect) + 3); 2278 moveIt = YES; 2279 } 2280 2281 if (moveIt) 2282 { 2283 NSPoint masterLocation; 2284 NSPoint destinationPoint; 2285 2286 if (style == NSMacintoshInterfaceStyle || _menu.horizontal) 2287 { 2288 masterLocation = frameRect.origin; 2289 destinationPoint.x = masterLocation.x + vector.x; 2290 destinationPoint.y = masterLocation.y + vector.y; 2291 [self nestedSetFrameOrigin: destinationPoint]; 2292 } 2293 else 2294 { 2295 NSMenu *candidateMenu; 2296 NSMenu *masterMenu; 2297 2298 // Look for the "master" menu, i.e. the one to move from. 2299 for (candidateMenu = masterMenu = self; 2300 (candidateMenu = masterMenu->_superMenu) 2301 && !candidateMenu->_menu.horizontal 2302 && (!masterMenu->_menu.is_tornoff 2303 || masterMenu->_menu.transient); 2304 masterMenu = candidateMenu); 2305 2306 masterLocation = [[masterMenu window] frame].origin; 2307 destinationPoint.x = masterLocation.x + vector.x; 2308 destinationPoint.y = masterLocation.y + vector.y; 2309 [masterMenu nestedSetFrameOrigin: destinationPoint]; 2310 } 2311 } 2312} 2313 2314- (BOOL)_ownedByPopUp 2315{ 2316 return _popUpButtonCell != nil; 2317} 2318 2319- (NSPopUpButtonCell *)_owningPopUp 2320{ 2321 return _popUpButtonCell; 2322} 2323 2324- (void)_setOwnedByPopUp: (NSPopUpButtonCell*)popUp 2325{ 2326 if (_popUpButtonCell != popUp) 2327 { 2328 _popUpButtonCell = popUp; 2329 if (popUp != nil) 2330 { 2331 [_aWindow setLevel: NSPopUpMenuWindowLevel]; 2332 [_bWindow setLevel: NSPopUpMenuWindowLevel]; 2333 } 2334 } 2335 [self update]; 2336} 2337 2338- (NSString*) description 2339{ 2340 return [NSString stringWithFormat: @"NSMenu: %@ (%@)", 2341 _title, _menu.transient ? @"Transient": @"Normal"]; 2342} 2343 2344@end 2345 2346