1/** <title>NSMenuView</title> 2 3 Copyright (C) 1999 Free Software Foundation, Inc. 4 5 Author: Fred Kiefer <FredKiefer@gmx.de> 6 Date: Sep 2001 7 Author: David Lazaro Saz <khelekir@encomix.es> 8 Date: Oct 1999 9 Author: Michael Hanni <mhanni@sprintmail.com> 10 Date: 1999 11 12 This file is part of the GNUstep GUI Library. 13 14 This library is free software; you can redistribute it and/or 15 modify it under the terms of the GNU Lesser General Public 16 License as published by the Free Software Foundation; either 17 version 2 of the License, or (at your option) any later version. 18 19 This library is distributed in the hope that it will be useful, 20 but WITHOUT ANY WARRANTY; without even the implied warranty of 21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 22 Lesser General Public License for more details. 23 24 You should have received a copy of the GNU Lesser General Public 25 License along with this library; see the file COPYING.LIB. 26 If not, see <http://www.gnu.org/licenses/> or write to the 27 Free Software Foundation, 51 Franklin Street, Fifth Floor, 28 Boston, MA 02110-1301, USA. 29*/ 30 31#import <Foundation/NSArray.h> 32#import <Foundation/NSCoder.h> 33#import <Foundation/NSDate.h> 34#import <Foundation/NSDebug.h> 35#import <Foundation/NSDictionary.h> 36#import <Foundation/NSException.h> 37#import <Foundation/NSNotification.h> 38#import <Foundation/NSRunLoop.h> 39#import <Foundation/NSString.h> 40#import <Foundation/NSValue.h> 41 42#import "AppKit/NSApplication.h" 43#import "AppKit/NSEvent.h" 44#import "AppKit/NSFont.h" 45#import "AppKit/NSImage.h" 46#import "AppKit/NSMenuView.h" 47#import "AppKit/NSMenu.h" 48#import "AppKit/NSButton.h" 49#import "AppKit/NSPopUpButtonCell.h" 50#import "AppKit/NSScreen.h" 51#import "AppKit/NSWindow.h" 52#import "AppKit/PSOperators.h" 53 54#import "GNUstepGUI/GSTheme.h" 55#import "GNUstepGUI/GSTitleView.h" 56 57 58typedef struct _GSCellRect { 59 NSRect rect; 60} GSCellRect; 61 62#define GSI_ARRAY_TYPES 0 63#define GSI_ARRAY_TYPE GSCellRect 64 65#define GSI_ARRAY_NO_RETAIN 66#define GSI_ARRAY_NO_RELEASE 67 68#ifdef GSIArray 69#undef GSIArray 70#endif 71#include <GNUstepBase/GSIArray.h> 72 73static NSMapTable *viewInfo = 0; 74 75#define cellRects ((GSIArray)NSMapGet(viewInfo, self)) 76 77#define HORIZONTAL_MENU_LEFT_PADDING 8 78 79/* 80 NSMenuView contains: 81 82 a) Title, if needed, this is a subview 83 b) menu items 84*/ 85 86/* A menu's title is an instance of this class */ 87@class NSButton; 88 89@interface NSMenu (Private) 90- (void) _attachMenu: (NSMenu*)aMenu; 91@end 92 93@implementation NSMenu (Private) 94- (void) _attachMenu: (NSMenu*)aMenu 95{ 96 _attachedMenu = aMenu; 97} 98@end 99 100@interface NSMenuView (Private) 101- (BOOL) _rootIsHorizontal: (BOOL*)isAppMenu; 102@end 103 104@implementation NSMenuView (Private) 105- (BOOL) _rootIsHorizontal: (BOOL*)isAppMenu 106{ 107 NSMenu *m = _attachedMenu; 108 109 /* Determine root menu of this menu hierarchy */ 110 while ([m supermenu] != nil) 111 { 112 m = [m supermenu]; 113 } 114 if (isAppMenu != 0) 115 { 116 if (m == [NSApp mainMenu]) 117 { 118 *isAppMenu = YES; 119 } 120 else 121 { 122 *isAppMenu = NO; 123 } 124 } 125 return [[m menuRepresentation] isHorizontal]; 126} 127@end 128 129@implementation NSMenuView 130 131/* 132 * Class methods. 133 */ 134 135static float menuBarHeight = 0.0; 136 137+ (void) _themeWillDeactivate: (NSNotification*)n 138{ 139 /* Clear cached information from the old theme ... will get info from 140 * the new theme as required. 141 */ 142 menuBarHeight = 0; 143} 144 145+ (void) initialize 146{ 147 if (viewInfo == 0) 148 { 149 viewInfo = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks, 150 NSNonOwnedPointerMapValueCallBacks, 20); 151 152 [[NSNotificationCenter defaultCenter] addObserver: self 153 selector: @selector(_themeWillDeactivate:) 154 name: GSThemeWillDeactivateNotification 155 object: nil]; 156 } 157} 158 159+ (float) menuBarHeight 160{ 161 if (menuBarHeight == 0.0) 162 { 163 const CGFloat themeHeight = [[GSTheme theme] menuBarHeight]; 164 165 NSFont *font = [NSFont menuBarFontOfSize: 0.0]; 166 167 menuBarHeight = [font boundingRectForFont].size.height; 168 if (menuBarHeight < themeHeight) 169 menuBarHeight = themeHeight; 170 } 171 172 return menuBarHeight; 173} 174 175/* 176 * NSView overrides 177 */ 178- (BOOL) acceptsFirstMouse: (NSEvent*)theEvent 179{ 180 return YES; 181} 182 183// We do not want to popup menus in this menu view. 184- (NSMenu *) menuForEvent: (NSEvent*) theEvent 185{ 186 NSDebugLLog (@"NSMenu", @"Query for menu in view"); 187 return nil; 188} 189 190/* 191 * Init methods. 192 */ 193- (id) initWithFrame: (NSRect)aFrame 194{ 195 self = [super initWithFrame: aFrame]; 196 if (!self) 197 return nil; 198 199 [self setFont: [NSFont menuFontOfSize: 0.0]]; 200 201 _highlightedItemIndex = -1; 202 _horizontalEdgePad = 4.; 203 204 /* Set the necessary offset for the menuView. That is, how many pixels 205 * do we need for our left side border line. 206 */ 207 _leftBorderOffset = 1; 208 209 // Create an array to store our menu item cells. 210 _itemCells = [NSMutableArray new]; 211 212 // FIXME: Should this go in NSMenu instead of here? 213 [[NSNotificationCenter defaultCenter] 214 addObserver: self 215 selector: @selector(_themeDidActivate:) 216 name: GSThemeDidActivateNotification 217 object: nil]; 218 219 return self; 220} 221 222- (id) initAsTearOff 223{ 224 self = [self initWithFrame: NSZeroRect]; 225 if (nil == self) 226 return nil; 227 228 if (_attachedMenu) 229 [_attachedMenu setTornOff: YES]; 230 231 return self; 232} 233 234- (void) dealloc 235{ 236 [[NSNotificationCenter defaultCenter] removeObserver: self]; 237 238 // We must remove the menu view from the menu list of observers. 239 if (_attachedMenu != nil) 240 { 241 [[NSNotificationCenter defaultCenter] removeObserver: self 242 name: nil 243 object: _attachedMenu]; 244 } 245 246 /* Clean the pointer to us stored into the _itemCells. */ 247 [_itemCells makeObjectsPerformSelector: @selector(setMenuView:) 248 withObject: nil]; 249 250 RELEASE(_itemCells); 251 RELEASE(_font); 252 253 /* 254 * Get rid of any cached cell rects. 255 */ 256 { 257 GSIArray a = NSMapGet(viewInfo, self); 258 259 if (a != 0) 260 { 261 GSIArrayEmpty(a); 262 NSZoneFree(NSDefaultMallocZone(), a); 263 NSMapRemove(viewInfo, self); 264 } 265 } 266 267 [super dealloc]; 268} 269 270/* 271 * Getting and Setting Menu View Attributes 272 */ 273- (void) setMenu: (NSMenu*)menu 274{ 275 NSNotificationCenter *theCenter = [NSNotificationCenter defaultCenter]; 276 unsigned count; 277 unsigned i; 278 279 if (_attachedMenu != nil) 280 { 281 // Remove this menu view from the old menu list of observers. 282 [theCenter removeObserver: self name: nil object: _attachedMenu]; 283 } 284 285 /* menu is retaining us, so we should not be retaining menu. */ 286 _attachedMenu = menu; 287 _items_link = [_attachedMenu itemArray]; 288 289 if (_attachedMenu != nil) 290 { 291 // Add this menu view to the menu's list of observers. 292 [theCenter addObserver: self 293 selector: @selector(itemChanged:) 294 name: NSMenuDidChangeItemNotification 295 object: _attachedMenu]; 296 297 [theCenter addObserver: self 298 selector: @selector(itemAdded:) 299 name: NSMenuDidAddItemNotification 300 object: _attachedMenu]; 301 302 [theCenter addObserver: self 303 selector: @selector(itemRemoved:) 304 name: NSMenuDidRemoveItemNotification 305 object: _attachedMenu]; 306 } 307 308 count = [[[self menu] itemArray] count]; 309 for (i = 0; i < count; i++) 310 { 311 NSNumber *n = [NSNumber numberWithInt: i]; 312 NSDictionary *d; 313 314 d = [NSDictionary dictionaryWithObject: n forKey: @"NSMenuItemIndex"]; 315 316 [self itemAdded: [NSNotification 317 notificationWithName: NSMenuDidAddItemNotification 318 object: self 319 userInfo: d]]; 320 } 321 322 // Force menu view's layout to be recalculated. 323 [self setNeedsSizing: YES]; 324 [self update]; 325} 326 327- (NSMenu*) menu 328{ 329 return _attachedMenu; 330} 331 332- (void) setHorizontal: (BOOL)flag 333{ 334 if (flag == YES && _horizontal == NO) 335 { 336 NSRect scRect = [[NSScreen mainScreen] frame]; 337 GSIArray a = NSZoneMalloc(NSDefaultMallocZone(), sizeof(GSIArray_t)); 338 339 GSIArrayInitWithZoneAndCapacity(a, NSDefaultMallocZone(), 8); 340 NSMapInsert(viewInfo, self, a); 341 342 scRect.size.height = [NSMenuView menuBarHeight]; 343 [self setFrameSize: scRect.size]; 344 [self setNeedsSizing: YES]; 345 } 346 else if (flag == NO && _horizontal == YES) 347 { 348 GSIArray a = NSMapGet(viewInfo, self); 349 350 if (a != 0) 351 { 352 GSIArrayEmpty(a); 353 NSZoneFree(NSDefaultMallocZone(), a); 354 NSMapRemove(viewInfo, self); 355 } 356 [self setNeedsSizing: YES]; 357 } 358 359 _horizontal = flag; 360} 361 362- (BOOL) isHorizontal 363{ 364 return _horizontal; 365} 366 367- (void) setFont: (NSFont*)font 368{ 369 ASSIGN(_font, font); 370 if (_font != nil) 371 { 372 const CGFloat themeHeight = [[GSTheme theme] menuItemHeight]; 373 374 NSRect r; 375 376 r = [_font boundingRectForFont]; 377 /* Should make up 110, 20 for default font */ 378 _cellSize = NSMakeSize (r.size.width * 10., r.size.height + 3.); 379 380 if (_cellSize.height < themeHeight) 381 _cellSize.height = themeHeight; 382 383 [self setNeedsSizing: YES]; 384 } 385} 386 387- (NSFont*) font 388{ 389 return _font; 390} 391 392- (void) setHighlightedItemIndex: (NSInteger)index 393{ 394 NSMenuItemCell *aCell; 395 396 if (index == _highlightedItemIndex) 397 return; 398 399 // Unhighlight old 400 if (_highlightedItemIndex != -1) 401 { 402 aCell = [self menuItemCellForItemAtIndex: _highlightedItemIndex]; 403 [aCell setHighlighted: NO]; 404 [self setNeedsDisplayForItemAtIndex: _highlightedItemIndex]; 405 } 406 407 // Set ivar to new index. 408 _highlightedItemIndex = index; 409 410 // Highlight new 411 if (_highlightedItemIndex != -1) 412 { 413 aCell = [self menuItemCellForItemAtIndex: _highlightedItemIndex]; 414 [aCell setHighlighted: YES]; 415 [self setNeedsDisplayForItemAtIndex: _highlightedItemIndex]; 416 } 417} 418 419- (NSInteger) highlightedItemIndex 420{ 421 return _highlightedItemIndex; 422} 423 424- (void) setMenuItemCell: (NSMenuItemCell *)cell 425 forItemAtIndex: (NSInteger)index 426{ 427 NSMenuItem *anItem = [_items_link objectAtIndex: index]; 428 429 [_itemCells replaceObjectAtIndex: index withObject: cell]; 430 431 [cell setMenuItem: anItem]; 432 [cell setMenuView: self]; 433 434 if ([self highlightedItemIndex] == index) 435 [cell setHighlighted: YES]; 436 else 437 [cell setHighlighted: NO]; 438 439 // Mark the new cell and the menu view as needing resizing. 440 [cell setNeedsSizing: YES]; 441 [self setNeedsSizing: YES]; 442 [self setNeedsDisplayForItemAtIndex: index]; 443} 444 445- (NSMenuItemCell*) menuItemCellForItemAtIndex: (NSInteger)index 446{ 447 if ((index >= 0) && (index < [_itemCells count])) 448 return [_itemCells objectAtIndex: index]; 449 else 450 return nil; 451} 452 453- (NSMenuView*) attachedMenuView 454{ 455 return [[_attachedMenu attachedMenu] menuRepresentation]; 456} 457 458- (NSMenu*) attachedMenu 459{ 460 return [_attachedMenu attachedMenu]; 461} 462 463- (BOOL) isAttached 464{ 465 return [_attachedMenu isAttached]; 466} 467 468- (BOOL) isTornOff 469{ 470 return [_attachedMenu isTornOff]; 471} 472 473- (void) setHorizontalEdgePadding: (float)pad 474{ 475 _horizontalEdgePad = pad; 476 [self setNeedsSizing: YES]; 477} 478 479- (float) horizontalEdgePadding 480{ 481 return _horizontalEdgePad; 482} 483 484/* 485 * Notification Methods 486 */ 487- (void) itemChanged: (NSNotification*)notification 488{ 489 int index = [[[notification userInfo] objectForKey: @"NSMenuItemIndex"] 490 intValue]; 491 NSMenuItemCell *aCell = [self menuItemCellForItemAtIndex: index]; 492 493 // Enabling of the item may have changed 494 [aCell setEnabled: [[aCell menuItem] isEnabled]]; 495 // Mark the cell associated with the item as needing resizing. 496 [aCell setNeedsSizing: YES]; 497 498 // Mark the menu view as needing to be resized. 499 [self setNeedsSizing: YES]; 500 [self setNeedsDisplayForItemAtIndex: index]; 501} 502 503- (void) itemAdded: (NSNotification*)notification 504{ 505 int index = [[[notification userInfo] 506 objectForKey: @"NSMenuItemIndex"] intValue]; 507 NSMenuItem *anItem = [_items_link objectAtIndex: index]; 508 id aCell = [NSMenuItemCell new]; 509 int wasHighlighted = _highlightedItemIndex; 510 511 // FIXME do we need to differentiate between popups and non popups 512 [aCell setMenuItem: anItem]; 513 [aCell setMenuView: self]; 514 [aCell setFont: _font]; 515 516 /* Unlight the previous highlighted cell if the index of the highlighted 517 * cell will be ruined up by the insertion of the new cell. */ 518 if (wasHighlighted >= index) 519 { 520 [self setHighlightedItemIndex: -1]; 521 } 522 523 [_itemCells insertObject: aCell atIndex: index]; 524 525 /* Restore the highlighted cell, with the new index for it. */ 526 if (wasHighlighted >= index) 527 { 528 /* Please note that if wasHighlighted == -1, it shouldn't be possible 529 * to be here. */ 530 [self setHighlightedItemIndex: ++wasHighlighted]; 531 } 532 533 [aCell setNeedsSizing: YES]; 534 RELEASE(aCell); 535 536 // Mark the menu view as needing to be resized. 537 [self setNeedsSizing: YES]; 538 [self setNeedsDisplay: YES]; 539} 540 541- (void) itemRemoved: (NSNotification*)notification 542{ 543 int wasHighlighted = [self highlightedItemIndex]; 544 int index = [[[notification userInfo] objectForKey: @"NSMenuItemIndex"] 545 intValue]; 546 547 if (index <= wasHighlighted) 548 { 549 [self setHighlightedItemIndex: -1]; 550 } 551 [_itemCells removeObjectAtIndex: index]; 552 553 if (wasHighlighted > index) 554 { 555 [self setHighlightedItemIndex: --wasHighlighted]; 556 } 557 // Mark the menu view as needing to be resized. 558 [self setNeedsSizing: YES]; 559 [self setNeedsDisplay: YES]; 560} 561 562/* 563 * Working with Submenus. 564 */ 565 566- (void) detachSubmenu 567{ 568 NSMenu *attachedMenu = [_attachedMenu attachedMenu]; 569 NSMenuView *attachedMenuView; 570 571 if (!attachedMenu) 572 return; 573 574 attachedMenuView = [attachedMenu menuRepresentation]; 575 576 [attachedMenuView detachSubmenu]; 577 578 NSDebugLLog (@"NSMenu", @"detach submenu: %@ from: %@", 579 attachedMenu, _attachedMenu); 580 581 if ([attachedMenu isTransient]) 582 { 583 [attachedMenu closeTransient]; 584 } 585 else 586 { 587 [attachedMenu close]; 588 // Unselect the active item 589 [self setHighlightedItemIndex: -1]; 590 } 591} 592 593- (void) attachSubmenuForItemAtIndex: (NSInteger)index 594{ 595 /* 596 * Transient menus are used for torn-off menus, which are already on the 597 * screen and for sons of transient menus. As transients disappear as 598 * soon as we release the mouse the user will be able to leave submenus 599 * open on the screen and interact with other menus at the same time. 600 */ 601 NSMenu *attachableMenu; 602 603 if (index < 0) 604 { 605 return; 606 } 607 608 attachableMenu = [[_items_link objectAtIndex: index] submenu]; 609 610 if ([attachableMenu isTornOff] || [_attachedMenu isTransient]) 611 { 612 NSDebugLLog (@"NSMenu", @"Will open transient: %@", attachableMenu); 613 [attachableMenu displayTransient]; 614 [[attachableMenu menuRepresentation] setHighlightedItemIndex: -1]; 615 } 616 else 617 { 618 NSDebugLLog (@"NSMenu", @"Will open normal: %@", attachableMenu); 619 // Check for the main menu of NSWindows95InterfaceStyle case. 620 // There we have a separate NSMenuView embedded in the window. 621 if ([_attachedMenu menuRepresentation] == self) 622 { 623 [attachableMenu display]; 624 } 625 else 626 { 627 [attachableMenu update]; 628 [attachableMenu sizeToFit]; 629 [[attachableMenu window] setFrameOrigin: [self locationForSubmenu: attachableMenu]]; 630 [_attachedMenu _attachMenu: attachableMenu]; 631 [[attachableMenu window] orderFrontRegardless]; 632 } 633 } 634} 635 636/* 637 * Calculating Menu Geometry 638 */ 639- (void) update 640{ 641 BOOL needTitleView; 642 BOOL rootIsAppMenu; 643 NSInterfaceStyle style; 644 645 NSDebugLLog (@"NSMenu", @"update called on menu view"); 646 647 /* 648 * Ensure that a title view exists only if needed. 649 */ 650 style = NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil); 651 if (style == NSWindows95InterfaceStyle || style == NSMacintoshInterfaceStyle) 652 { 653 needTitleView = NO; 654 } 655 else if (_attachedMenu == nil) 656 { 657 needTitleView = NO; 658 } 659 else if ([self _rootIsHorizontal: &rootIsAppMenu] == YES) 660 { 661 needTitleView = NO; 662 } 663 else if (rootIsAppMenu == YES) 664 { 665 needTitleView = YES; 666 } 667 else 668 { 669 // Popup menu doesn't need title bar 670 needTitleView = ([_attachedMenu _ownedByPopUp] == YES) ? NO : YES; 671 } 672 673 if (needTitleView == YES && _titleView == nil) 674 { 675 Class titleViewClass = [[GSTheme theme] titleViewClassForMenuView: self]; 676 _titleView = [[titleViewClass alloc] initWithOwner: _attachedMenu]; 677 [self addSubview: _titleView]; 678 RELEASE(_titleView); 679 } 680 if (needTitleView == NO && _titleView != nil) 681 { 682 [_titleView removeFromSuperview]; 683 _titleView = nil; 684 } 685 686 if (_titleView != nil) 687 { 688 if ([_attachedMenu isTornOff] && ![_attachedMenu isTransient]) 689 { 690 [_titleView 691 addCloseButtonWithAction: @selector(_performMenuClose:)]; 692 } 693 else 694 { 695 [_titleView removeCloseButton]; 696 } 697 } 698 699 // Ask the menu to update itself. This will call sizeToFit if needed. 700 [_attachedMenu update]; 701} 702 703- (void) setNeedsSizing: (BOOL)flag 704{ 705 _needsSizing = flag; 706} 707 708- (BOOL) needsSizing 709{ 710 return _needsSizing; 711} 712 713- (CGFloat) heightForItem: (NSInteger)idx 714{ 715 NSMenuItemCell *cell = [self menuItemCellForItemAtIndex: idx]; 716 717 if (cell != nil) 718 { 719 NSMenuItem *item = [cell menuItem]; 720 721 if ([item isSeparatorItem]) 722 { 723 return [[GSTheme theme] menuSeparatorHeight]; 724 } 725 } 726 return _cellSize.height; 727} 728 729- (CGFloat) yOriginForItem: (NSInteger)item 730{ 731 const NSInteger count = [_itemCells count]; 732 CGFloat total = 0; 733 734 if (item >= 0) 735 { 736 NSInteger i = 0; 737 for (i = (count - 1); i > item; i--) 738 { 739 total += [self heightForItem: i]; 740 } 741 } 742 return total; 743} 744 745- (CGFloat) totalHeight 746{ 747 CGFloat total = 0; 748 NSUInteger i = 0; 749 750 for (i = 0; i < [_itemCells count]; i++) 751 { 752 total += [self heightForItem: i]; 753 } 754 return total; 755} 756 757- (void) sizeToFit 758{ 759 BOOL isPullDown = 760 [_attachedMenu _ownedByPopUp] && [[_attachedMenu _owningPopUp] pullsDown]; 761 762 if (_horizontal == YES) 763 { 764 unsigned i; 765 unsigned howMany = [_itemCells count]; 766 float currentX = HORIZONTAL_MENU_LEFT_PADDING; 767// NSRect scRect = [[NSScreen mainScreen] frame]; 768 769 GSIArrayRemoveAllItems(cellRects); 770 771/* 772 scRect.size.height = [NSMenuView menuBarHeight]; 773 [self setFrameSize: scRect.size]; 774 _cellSize.height = scRect.size.height; 775*/ 776 _cellSize.height = [NSMenuView menuBarHeight]; 777 778 if (howMany && isPullDown) 779 { 780 GSCellRect elem; 781 elem.rect = NSMakeRect (currentX, 782 0, 783 (2 * _horizontalEdgePad), 784 [self heightForItem: 0]); 785 GSIArrayAddItem(cellRects, (GSIArrayItem)elem); 786 currentX += 2 * _horizontalEdgePad; 787 } 788 for (i = isPullDown ? 1 : 0; i < howMany; i++) 789 { 790 GSCellRect elem; 791 NSMenuItemCell *aCell = [self menuItemCellForItemAtIndex: i]; 792 float titleWidth = [aCell titleWidth]; 793 794 if ([aCell imageWidth]) 795 { 796 titleWidth += [aCell imageWidth] + GSCellTextImageXDist; 797 } 798 799 elem.rect = NSMakeRect (currentX, 800 0, 801 (titleWidth + (2 * _horizontalEdgePad)), 802 [self heightForItem: i]); 803 GSIArrayAddItem(cellRects, (GSIArrayItem)elem); 804 805 currentX += titleWidth + (2 * _horizontalEdgePad); 806 } 807 } 808 else 809 { 810 unsigned i; 811 unsigned howMany = [_itemCells count]; 812 unsigned wideTitleView = 1; 813 float neededImageAndTitleWidth = 0.0; 814 float neededKeyEquivalentWidth = 0.0; 815 float neededStateImageWidth = 0.0; 816 float accumulatedOffset = 0.0; 817 float popupImageWidth = 0.0; 818 float menuBarHeight = 0.0; 819 820 if (_titleView) 821 { 822 NSMenu *m = [_attachedMenu supermenu]; 823 NSMenuView *r = [m menuRepresentation]; 824 825 neededImageAndTitleWidth = [_titleView titleSize].width; 826 if (r != nil && [r isHorizontal] == YES) 827 { 828 NSMenuItemCell *msr; 829 830 msr = [r menuItemCellForItemAtIndex: 831 [m indexOfItemWithTitle: [_attachedMenu title]]]; 832 neededImageAndTitleWidth 833 = [msr titleWidth] + GSCellTextImageXDist; 834 } 835 836 if (_titleView) 837 menuBarHeight = [[self class] menuBarHeight]; 838 else 839 menuBarHeight += _leftBorderOffset; 840 } 841 else 842 { 843 menuBarHeight += _leftBorderOffset; 844 } 845 846 for (i = isPullDown ? 1 : 0; i < howMany; i++) 847 { 848 float aStateImageWidth; 849 float aTitleWidth; 850 float anImageWidth; 851 float anImageAndTitleWidth; 852 float aKeyEquivalentWidth; 853 NSMenuItemCell *aCell = [self menuItemCellForItemAtIndex: i]; 854 855 // State image area. 856 aStateImageWidth = [aCell stateImageWidth]; 857 858 // Title and Image area. 859 aTitleWidth = [aCell titleWidth]; 860 anImageWidth = [aCell imageWidth]; 861 862 // Key equivalent area. 863 aKeyEquivalentWidth = [aCell keyEquivalentWidth]; 864 865 switch ([aCell imagePosition]) 866 { 867 case NSNoImage: 868 anImageAndTitleWidth = aTitleWidth; 869 break; 870 871 case NSImageOnly: 872 anImageAndTitleWidth = anImageWidth; 873 break; 874 875 case NSImageLeft: 876 case NSImageRight: 877 anImageAndTitleWidth 878 = anImageWidth + aTitleWidth + GSCellTextImageXDist; 879 break; 880 881 case NSImageBelow: 882 case NSImageAbove: 883 case NSImageOverlaps: 884 default: 885 if (aTitleWidth > anImageWidth) 886 anImageAndTitleWidth = aTitleWidth; 887 else 888 anImageAndTitleWidth = anImageWidth; 889 break; 890 } 891 892 if (aStateImageWidth > neededStateImageWidth) 893 neededStateImageWidth = aStateImageWidth; 894 895 if (anImageAndTitleWidth > neededImageAndTitleWidth) 896 neededImageAndTitleWidth = anImageAndTitleWidth; 897 898 if (aKeyEquivalentWidth > neededKeyEquivalentWidth) 899 neededKeyEquivalentWidth = aKeyEquivalentWidth; 900 901 // Title view width less than item's left part width 902 if ((anImageAndTitleWidth + aStateImageWidth) 903 > neededImageAndTitleWidth) 904 wideTitleView = 0; 905 906 // Popup menu has only one item with nibble or arrow image 907 if (anImageWidth) 908 popupImageWidth = anImageWidth; 909 } 910 if (isPullDown && howMany) 911 howMany -= 1; 912 913 // Cache the needed widths. 914 _stateImageWidth = neededStateImageWidth; 915 _imageAndTitleWidth = neededImageAndTitleWidth; 916 _keyEqWidth = neededKeyEquivalentWidth; 917 918 accumulatedOffset = _horizontalEdgePad; 919 if (howMany) 920 { 921 // Calculate the offsets and cache them. 922 if (neededStateImageWidth) 923 { 924 _stateImageOffset = accumulatedOffset; 925 accumulatedOffset += neededStateImageWidth += _horizontalEdgePad; 926 } 927 928 if (neededImageAndTitleWidth) 929 { 930 _imageAndTitleOffset = accumulatedOffset; 931 accumulatedOffset += neededImageAndTitleWidth; 932 } 933 934 if (wideTitleView) 935 { 936 _keyEqOffset = accumulatedOffset = neededImageAndTitleWidth 937 + (3 * _horizontalEdgePad); 938 } 939 else 940 { 941 _keyEqOffset = accumulatedOffset += (2 * _horizontalEdgePad); 942 } 943 accumulatedOffset += neededKeyEquivalentWidth + _horizontalEdgePad; 944 945 if ([_attachedMenu supermenu] != nil && neededKeyEquivalentWidth < 8) 946 { 947 accumulatedOffset += 8 - neededKeyEquivalentWidth; 948 } 949 } 950 else 951 { 952 accumulatedOffset += neededImageAndTitleWidth + 3 + 2; 953 if ([_attachedMenu supermenu] != nil) 954 accumulatedOffset += 15; 955 } 956 957 // Calculate frame size. 958 if (_needsSizing) 959 { 960 // Add the border width: 1 for left, 2 for right sides 961 _cellSize.width = accumulatedOffset + 3; 962 } 963 964 if ([_attachedMenu _ownedByPopUp]) 965 { 966 _keyEqOffset = _cellSize.width - _keyEqWidth - popupImageWidth; 967 } 968 969 [self setFrameSize: NSMakeSize(_cellSize.width + _leftBorderOffset, 970 [self totalHeight] 971 + menuBarHeight)]; 972 [_titleView setFrame: NSMakeRect (0, [self totalHeight], 973 NSWidth (_bounds), menuBarHeight)]; 974 } 975 _needsSizing = NO; 976} 977 978- (float) stateImageOffset 979{ 980 if (_needsSizing) 981 [self sizeToFit]; 982 983 return _stateImageOffset; 984} 985 986- (float) stateImageWidth 987{ 988 if (_needsSizing) 989 [self sizeToFit]; 990 991 return _stateImageWidth; 992} 993 994- (float) imageAndTitleOffset 995{ 996 if (_needsSizing) 997 [self sizeToFit]; 998 999 return _imageAndTitleOffset; 1000} 1001 1002- (float) imageAndTitleWidth 1003{ 1004 if (_needsSizing) 1005 [self sizeToFit]; 1006 1007 return _imageAndTitleWidth; 1008} 1009 1010- (float) keyEquivalentOffset 1011{ 1012 if (_needsSizing) 1013 [self sizeToFit]; 1014 1015 return _keyEqOffset; 1016} 1017 1018- (float) keyEquivalentWidth 1019{ 1020 if (_needsSizing) 1021 [self sizeToFit]; 1022 1023 return _keyEqWidth; 1024} 1025 1026- (NSRect) innerRect 1027{ 1028 if (_horizontal == NO) 1029 { 1030 return NSMakeRect (_bounds.origin.x + _leftBorderOffset, 1031 _bounds.origin.y, 1032 _bounds.size.width - _leftBorderOffset, 1033 _bounds.size.height); 1034 } 1035 else 1036 { 1037 return NSMakeRect (_bounds.origin.x, 1038 _bounds.origin.y + _leftBorderOffset, 1039 _bounds.size.width, 1040 _bounds.size.height - _leftBorderOffset); 1041 } 1042} 1043 1044- (NSRect) rectOfItemAtIndex: (NSInteger)index 1045{ 1046 if (_needsSizing == YES) 1047 { 1048 [self sizeToFit]; 1049 } 1050 1051 // The first item of a pull down menu holds its title and isn't displayed 1052 if (index == 0 && [_attachedMenu _ownedByPopUp] && 1053 [[_attachedMenu _owningPopUp] pullsDown]) 1054 { 1055 return NSZeroRect; 1056 } 1057 1058 if (_horizontal == YES) 1059 { 1060 GSCellRect aRect; 1061 1062 aRect = GSIArrayItemAtIndex(cellRects, index).ext; 1063 1064 return aRect.rect; 1065 } 1066 else 1067 { 1068 NSRect theRect; 1069 1070 theRect.origin.y = [self yOriginForItem: index]; 1071 theRect.origin.x = _leftBorderOffset; 1072 theRect.size = _cellSize; 1073 theRect.size.height = [self heightForItem: index]; 1074 1075 /* NOTE: This returns the correct NSRect for drawing cells, but nothing 1076 * else (unless we are a popup). This rect will have to be modified for 1077 * event calculation, etc.. 1078 */ 1079 return theRect; 1080 } 1081} 1082 1083- (NSInteger) indexOfItemAtPoint: (NSPoint)point 1084{ 1085 unsigned howMany = [_itemCells count]; 1086 unsigned i; 1087 1088 for (i = 0; i < howMany; i++) 1089 { 1090 NSRect aRect = [self rectOfItemAtIndex: i]; 1091 1092 //NSLog(@"indexOfItemAtPoint called for %@ %@ %d %@", self, NSStringFromPoint(point), i, NSStringFromRect(aRect)); 1093 aRect.origin.x -= _leftBorderOffset; 1094 aRect.size.width += _leftBorderOffset; 1095 1096 // For horizontal menus, clicking in the left padding should be treated 1097 // as hitting the first menu item. 1098 if (_horizontal == YES && i == 0) 1099 { 1100 aRect.origin.x -= HORIZONTAL_MENU_LEFT_PADDING; 1101 aRect.size.width += HORIZONTAL_MENU_LEFT_PADDING; 1102 } 1103 1104 if (NSMouseInRect(point, aRect, NO)) 1105 return (int)i; 1106 } 1107 1108 return -1; 1109} 1110 1111- (void) setNeedsDisplayForItemAtIndex: (NSInteger)index 1112{ 1113 NSRect aRect; 1114 1115 aRect = [self rectOfItemAtIndex: index]; 1116 aRect.origin.x -= _leftBorderOffset; 1117 aRect.size.width += _leftBorderOffset; 1118 [self setNeedsDisplayInRect: aRect]; 1119} 1120 1121- (NSPoint) locationForSubmenu: (NSMenu *)aSubmenu 1122{ 1123 NSRect frame = [_window frame]; 1124 NSRect submenuFrame; 1125 const CGFloat submenuHorizOverlap = [[GSTheme theme] menuSubmenuHorizontalOverlap]; 1126 const CGFloat submenuVertOverlap = [[GSTheme theme] menuSubmenuVerticalOverlap]; 1127 1128 if (_needsSizing) 1129 [self sizeToFit]; 1130 1131 if (aSubmenu) 1132 submenuFrame = [[[aSubmenu menuRepresentation] window] frame]; 1133 else 1134 submenuFrame = NSZeroRect; 1135 1136 if (_horizontal == NO) 1137 { 1138 if (NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", 1139 [aSubmenu menuRepresentation]) 1140 == GSWindowMakerInterfaceStyle) 1141 { 1142 NSRect aRect = [self convertRect: [self rectOfItemAtIndex: 1143 [_attachedMenu indexOfItemWithSubmenu: aSubmenu]] toView: nil]; 1144 NSPoint subOrigin = [_window convertBaseToScreen: aRect.origin]; 1145 1146 return NSMakePoint (NSMaxX(frame) - submenuHorizOverlap, 1147 subOrigin.y - NSHeight(submenuFrame) - 2 + 1148 2*[NSMenuView menuBarHeight]); 1149 } 1150 else if ([self _rootIsHorizontal: 0] == YES) 1151 { 1152 NSRect aRect = [self convertRect: [self rectOfItemAtIndex: 1153 [_attachedMenu indexOfItemWithSubmenu: aSubmenu]] toView: nil]; 1154 NSPoint subOrigin = [_window convertBaseToScreen: aRect.origin]; 1155 1156 // FIXME ... why is the offset +1 needed below? 1157 return NSMakePoint (NSMaxX(frame) - submenuHorizOverlap, 1158 subOrigin.y - NSHeight(submenuFrame) + aRect.size.height + 1); 1159 } 1160 else 1161 { 1162 return NSMakePoint(NSMaxX(frame) - submenuHorizOverlap, 1163 NSMaxY(frame) - NSHeight(submenuFrame)); 1164 } 1165 } 1166 else 1167 { 1168 NSRect aRect = [self convertRect: [self rectOfItemAtIndex: 1169 [_attachedMenu indexOfItemWithSubmenu: aSubmenu]] toView: nil]; 1170 NSPoint subOrigin = [_window convertBaseToScreen: aRect.origin]; 1171 1172 /* If menu is in window, we add +1 for don't lose the track when 1173 the user move the mouse from the horizontal menu to a submenu.*/ 1174 if (NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil) == 1175 NSWindows95InterfaceStyle) 1176 { 1177 return NSMakePoint(subOrigin.x, 1178 subOrigin.y - NSHeight(submenuFrame) + 1 + submenuVertOverlap); 1179 } 1180 else 1181 { 1182 return NSMakePoint(subOrigin.x, 1183 subOrigin.y - NSHeight(submenuFrame) + submenuVertOverlap); 1184 } 1185 } 1186} 1187 1188- (void) resizeWindowWithMaxHeight: (float)maxHeight 1189{ 1190 // FIXME set the menuview's window to max height in order to keep on screen? 1191} 1192 1193- (void) setWindowFrameForAttachingToRect: (NSRect)screenRect 1194 onScreen: (NSScreen*)screen 1195 preferredEdge: (NSRectEdge)edge 1196 popUpSelectedItem: (NSInteger)selectedItemIndex 1197{ 1198 NSRect r; 1199 NSRect cellFrame; 1200 NSRect popUpFrame; 1201 int items = [_itemCells count]; 1202 BOOL growHeight = YES; 1203 BOOL resizeCell = NO; 1204 CGFloat borderOffsetInBaseCoords; 1205 1206 // Our window needs to have a nonzero size for the 1207 // -convertRect:fromView: and relatead methods to work. 1208 [_window setFrame: NSMakeRect(0,0,1,1) display: NO]; 1209 1210 // Make sure the menu entries are up to date 1211 [self update]; 1212 if (_needsSizing) 1213 [self sizeToFit]; 1214 1215 /* FIXME: Perhaps all of this belongs into NSPopupButtonCell and 1216 should be used to determine the proper screenRect to pass on into 1217 this method. 1218 */ 1219 /* certain style of pulldowns don't want sizing on the _cellSize.height */ 1220 if ([_attachedMenu _ownedByPopUp]) 1221 { 1222 NSPopUpButtonCell *bcell; 1223 1224 bcell = [_attachedMenu _owningPopUp]; 1225 if ([bcell pullsDown]) 1226 { 1227 if ([bcell isBordered] == NO) 1228 { 1229 growHeight = NO; 1230 } 1231 else 1232 { 1233 switch ([bcell bezelStyle]) 1234 { 1235 case NSRegularSquareBezelStyle: 1236 case NSShadowlessSquareBezelStyle: 1237 growHeight = NO; 1238 break; 1239 default: 1240 break; 1241 } 1242 } 1243 } 1244 } 1245 1246 cellFrame = screenRect; 1247 1248 /* 1249 we should have the calculated cell size, grow the width 1250 if needed to match the screenRect and vice versa 1251 */ 1252 if (cellFrame.size.width > [self convertSizeToBase: _cellSize].width) 1253 { 1254 _cellSize.width = [self convertSizeFromBase: cellFrame.size].width; 1255 resizeCell = YES; 1256 } 1257 else 1258 { 1259 cellFrame.size.width = [self convertSizeToBase: _cellSize].width; 1260 } 1261 1262 /* certain pop-ups don't want the height resized */ 1263 if (growHeight && cellFrame.size.height > [self convertSizeToBase: _cellSize].height) 1264 { 1265 _cellSize.height = [self convertSizeFromBase: cellFrame.size].height; 1266 resizeCell = YES; 1267 } 1268 else 1269 { 1270 cellFrame.size.height = [self convertSizeToBase: _cellSize].height; 1271 } 1272 1273 /* 1274 now sizeToFit again with needs sizing = NO so it doesn't 1275 overwrite _cellSize just recalculate the offsets. 1276 */ 1277 if (resizeCell) 1278 [self sizeToFit]; 1279 1280 /* 1281 * Compute the frame. popUpFrame is in screen coordinates 1282 */ 1283 popUpFrame = cellFrame; 1284 1285 borderOffsetInBaseCoords = [self convertSizeToBase: NSMakeSize(_leftBorderOffset, 0)].width; 1286 1287 if (items > 0) 1288 { 1289 float f; 1290 1291 if (_horizontal == NO) 1292 { 1293 f = cellFrame.size.height * (items - 1); 1294 popUpFrame.size.height += f + borderOffsetInBaseCoords; 1295 popUpFrame.origin.y -= f; 1296 popUpFrame.size.width += borderOffsetInBaseCoords; 1297 popUpFrame.origin.x -= borderOffsetInBaseCoords; 1298 1299 // If the menu is a pull down menu the first item, which would 1300 // appear at the top of the menu, holds the title and is omitted 1301 if ([_attachedMenu _ownedByPopUp]) 1302 { 1303 if ([[_attachedMenu _owningPopUp] pullsDown]) 1304 { 1305 popUpFrame.size.height -= cellFrame.size.height; 1306 popUpFrame.origin.y += cellFrame.size.height; 1307 } 1308 } 1309 1310 // Compute position for popups, if needed 1311 if (selectedItemIndex != -1) 1312 { 1313 popUpFrame.origin.y 1314 += cellFrame.size.height * selectedItemIndex; 1315 } 1316 } 1317 else 1318 { 1319 f = cellFrame.size.width * (items - 1); 1320 popUpFrame.size.width += f; 1321 1322 // If the menu is a pull down menu the first item holds the 1323 // title and is omitted 1324 if ([_attachedMenu _ownedByPopUp]) 1325 { 1326 if ([[_attachedMenu _owningPopUp] pullsDown]) 1327 { 1328 popUpFrame.size.width -= cellFrame.size.width; 1329 } 1330 } 1331 1332 // Compute position for popups, if needed 1333 if (selectedItemIndex != -1) 1334 { 1335 popUpFrame.origin.x -= cellFrame.size.width * selectedItemIndex; 1336 } 1337 } 1338 } 1339 1340 // Update position, if needed, using the preferredEdge 1341 if (selectedItemIndex == -1) 1342 { 1343 NSRect screenFrame; 1344 1345 if (screen == nil) 1346 screen = [NSScreen mainScreen]; 1347 screenFrame = [screen frame]; 1348 1349 popUpFrame.origin.y -= cellFrame.size.height; 1350 if (edge == NSMinYEdge || edge == NSMaxYEdge) 1351 { 1352 NSRect minYFrame = popUpFrame; 1353 NSRect maxYFrame = popUpFrame; 1354 1355 // show menu above or below the cell depending on the preferred edge 1356 // if the menu would be partially off screen on that edge use the 1357 // opposite edge or at least the one where more space is left 1358 maxYFrame.origin.y += 1359 maxYFrame.size.height + screenRect.size.height - _leftBorderOffset; 1360 1361 if (edge == NSMinYEdge) 1362 { 1363 if ((NSMinY(minYFrame) < NSMinY(screenFrame)) 1364 && ((NSMaxY(maxYFrame) <= NSMaxY(screenFrame)) 1365 || (NSMaxY(screenFrame) - NSMaxY(screenRect) > 1366 NSMinY(screenRect) - NSMinY(screenFrame)))) 1367 { 1368 edge = NSMaxYEdge; 1369 } 1370 } 1371 else 1372 { 1373 if ((NSMaxY(maxYFrame) > NSMaxY(screenFrame)) 1374 && ((NSMinY(minYFrame) >= NSMinY(screenFrame)) 1375 || (NSMaxY(screenFrame) - NSMaxY(screenRect) < 1376 NSMinY(screenRect) - NSMinY(screenFrame)))) 1377 { 1378 edge = NSMinYEdge; 1379 } 1380 } 1381 popUpFrame = edge == NSMinYEdge ? minYFrame : maxYFrame; 1382 } 1383 else 1384 { 1385 NSRect minXFrame = popUpFrame; 1386 NSRect maxXFrame = popUpFrame; 1387 1388 minXFrame.origin.y += screenRect.size.height; 1389 minXFrame.origin.x -= minXFrame.size.width; 1390 1391 maxXFrame.origin.y += screenRect.size.height; 1392 maxXFrame.origin.x += screenRect.size.width + _leftBorderOffset; 1393 1394 // show menu on the opposite edge if it does not fit on screen on 1395 // the preferred edge 1396 if (edge == NSMinXEdge) 1397 { 1398 if ((NSMinX(minXFrame) < NSMinX(screenFrame)) 1399 && ((NSMaxX(maxXFrame) <= NSMaxX(screenFrame)) 1400 || (NSMaxX(screenFrame) - NSMaxX(screenRect) > 1401 NSMinX(screenRect) - NSMinX(screenFrame)))) 1402 { 1403 edge = NSMaxXEdge; 1404 } 1405 } 1406 else 1407 { 1408 if ((NSMaxX(maxXFrame) > NSMaxX(screenFrame)) 1409 && ((NSMinX(minXFrame) >= NSMinX(screenFrame)) 1410 || (NSMaxX(screenFrame) - NSMaxX(screenRect) < 1411 NSMinX(screenRect) - NSMinX(screenFrame)))) 1412 { 1413 edge = NSMinXEdge; 1414 } 1415 } 1416 popUpFrame = edge == NSMinXEdge ? minXFrame : maxXFrame; 1417 } 1418 } 1419 1420 // Get the frameRect 1421 { 1422 NSSize contentSize = [self convertSizeFromBase: popUpFrame.size]; 1423 NSRect contentRect = NSMakeRect(popUpFrame.origin.x, 1424 popUpFrame.origin.y, 1425 contentSize.width, 1426 contentSize.height); 1427 r = [_window frameRectForContentRect: contentRect]; 1428 1429 1430 // Set the window frame. r should be identical to popUpFrame except with 1431 // any borders the window wanted to add. 1432 [_window setFrame: r display: NO]; 1433 } 1434} 1435 1436/* 1437 * Drawing. 1438 */ 1439- (BOOL) isOpaque 1440{ 1441 return NO; 1442} 1443 1444- (void) drawRect: (NSRect)rect 1445{ 1446 [[GSTheme theme] drawMenuRect: rect 1447 inView: self 1448 isHorizontal: _horizontal 1449 itemCells: _itemCells]; 1450} 1451 1452/* 1453 * Event Handling 1454 */ 1455- (void) performActionWithHighlightingForItemAtIndex: (NSInteger)index 1456{ 1457 NSMenu *candidateMenu = _attachedMenu; 1458 NSMenuView *targetMenuView; 1459 int indexToHighlight = index; 1460 int oldHighlightedIndex; 1461 1462 for (;;) 1463 { 1464 NSMenu *superMenu = [candidateMenu supermenu]; 1465 1466 if (superMenu == nil 1467 || [candidateMenu isAttached] 1468 || [candidateMenu isTornOff]) 1469 { 1470 targetMenuView = [candidateMenu menuRepresentation]; 1471 1472 break; 1473 } 1474 else 1475 { 1476 indexToHighlight = [superMenu indexOfItemWithSubmenu: candidateMenu]; 1477 candidateMenu = superMenu; 1478 } 1479 } 1480 1481 oldHighlightedIndex = [targetMenuView highlightedItemIndex]; 1482 [targetMenuView setHighlightedItemIndex: indexToHighlight]; 1483 1484 /* We need to let the run loop run a little so that the fact that 1485 * the item is highlighted gets displayed on screen. 1486 */ 1487 [[NSRunLoop currentRunLoop] 1488 runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.1]]; 1489 1490 [_attachedMenu performActionForItemAtIndex: index]; 1491 1492 if (![_attachedMenu _ownedByPopUp]) 1493 { 1494 [targetMenuView setHighlightedItemIndex: oldHighlightedIndex]; 1495 } 1496} 1497 1498#define MOVE_THRESHOLD_DELTA 2.0 1499#define DELAY_MULTIPLIER 10 1500 1501- (BOOL) _executeItemAtIndex: (int)indexOfActionToExecute 1502 removeSubmenu: (BOOL)subMenusNeedRemoving 1503{ 1504 NSInterfaceStyle style = 1505 NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", self); 1506 1507 if (indexOfActionToExecute >= 0 1508 && [_attachedMenu attachedMenu] != nil && [_attachedMenu attachedMenu] == 1509 [[_items_link objectAtIndex: indexOfActionToExecute] submenu]) 1510 { 1511 if (style == NSMacintoshInterfaceStyle) 1512 { 1513 // On Macintosh, clicking on or releasing the mouse over a 1514 // submenu item always closes the menu (if it is open) and 1515 // ends menu tracking. We do the same here, too. 1516 [self detachSubmenu]; 1517 return YES; 1518 } 1519 1520 if (style == NSWindows95InterfaceStyle) 1521 { 1522 return YES; 1523 } 1524 1525 if (subMenusNeedRemoving) 1526 { 1527 [self detachSubmenu]; 1528 } 1529 // Clicked on a submenu. 1530 return NO; 1531 } 1532 1533 return YES; 1534} 1535 1536- (BOOL) _trackWithEvent: (NSEvent*)event 1537 startingMenuView: (NSMenuView*)mainWindowMenuView 1538{ 1539 NSUInteger eventMask = NSPeriodicMask; 1540 NSDate *theDistantFuture = [NSDate distantFuture]; 1541 NSPoint lastLocation = {0,0}; 1542 BOOL justAttachedNewSubmenu = NO; 1543 BOOL subMenusNeedRemoving = YES; 1544 BOOL shouldFinish = YES; 1545 BOOL popUpProcessEvents = [[GSTheme theme] doesProcessEventsForPopUpMenu]; 1546 int delayCount = 0; 1547 int indexOfActionToExecute = -1; 1548 int firstIndex = -1; 1549 NSInterfaceStyle style = 1550 NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", self); 1551 NSEvent *original; 1552 NSEventType type; 1553 1554 /* 1555 * The original event is unused except to determine whether the method 1556 * was invoked in response to a right or left mouse down. 1557 * We pass the same event on when we want tracking to move into a 1558 * submenu. 1559 */ 1560 original = AUTORELEASE(RETAIN(event)); 1561 1562 type = [event type]; 1563 1564 /** 1565 * The following event tracking run loop can run for a long time, during 1566 * which new windows (for submenus) are created and put on screen. The 1567 * window manager might not place the menus where we ask them to be placed 1568 * (e.g. Metacity will move menus so they don't overlap the GNOME 1569 * top-of-screen panel). 1570 * 1571 * The mechanism which updates an NSWindow's frame when an external force 1572 * (such as the window manager) moves a window is an NSAppKitDefined event. 1573 * Since we need the frames of the menu windows to be accurate to know which 1574 * menu item the cursor is actually over, and since we are running our own 1575 * event loop and the NSAppKitDefined events aren't handled for us, we need 1576 * to request them ourselves and forward them to the relevant window. 1577 * 1578 * NOTE: While it seems messy to have to handle these events, Cocoa doesn't 1579 * handle them automatically either for code which goes in to an event 1580 * tracking loop. 1581 */ 1582 eventMask |= NSAppKitDefinedMask; 1583 1584 eventMask |= NSRightMouseUpMask | NSRightMouseDraggedMask; 1585 eventMask |= NSRightMouseDownMask; 1586 eventMask |= NSOtherMouseUpMask | NSOtherMouseDraggedMask; 1587 eventMask |= NSOtherMouseDownMask; 1588 eventMask |= NSLeftMouseUpMask | NSLeftMouseDraggedMask; 1589 eventMask |= NSLeftMouseDownMask; 1590 1591 /* We need know if the user press a modifier key to close the menu 1592 when the menu is in a window or when is owned by a popup and theme 1593 process events. */ 1594 if (style == NSWindows95InterfaceStyle || popUpProcessEvents) 1595 { 1596 eventMask |= NSFlagsChangedMask; 1597 } 1598 1599 // Ignore the first mouse up if menu is horizontal. 1600 if ([self isHorizontal] == YES || 1601 // Or if menu is transient and style is NSWindows95InterfaceStyle. 1602 ([[self menu] isTransient] && style == NSWindows95InterfaceStyle) || 1603 /* Or to mimic Mac OS X behavior for pop up menus. If the user 1604 presses the mouse button over a pop up button and then drags the mouse 1605 over the menu, the menu is closed when the user releases the mouse. On 1606 the other hand, when the user clicks on the button and then moves the 1607 mouse the menu is closed upon the next mouse click. */ 1608 ([[self menu] _ownedByPopUp] && (style == NSMacintoshInterfaceStyle || 1609 popUpProcessEvents))) 1610 { 1611 /* 1612 * Ignore the first mouse up if nothing interesting has happened. 1613 */ 1614 shouldFinish = NO; 1615 } 1616 do 1617 { 1618 if (type == NSFlagsChanged) 1619 { 1620 /* Close the menu if the user press a modifier key and menu 1621 is in a window */ 1622 if (mainWindowMenuView != nil) 1623 { 1624 [self setHighlightedItemIndex: -1]; 1625 [[[mainWindowMenuView menu] attachedMenu] close]; 1626 return NO; 1627 } 1628 1629 /* Close the menu if is owned by a popup and theme process events */ 1630 if ([[self menu] _ownedByPopUp] && popUpProcessEvents) 1631 { 1632 [[[self menu] _owningPopUp] dismissPopUp]; 1633 return NO; 1634 } 1635 } 1636 1637 if (type == NSLeftMouseUp || 1638 type == NSRightMouseUp || 1639 type == NSOtherMouseUp) 1640 { 1641 shouldFinish = YES; 1642 } 1643 1644 if (type == NSPeriodic || event == original) 1645 { 1646 NSPoint location; 1647 int index; 1648 1649 location = [_window mouseLocationOutsideOfEventStream]; 1650 index = [self indexOfItemAtPoint: 1651 [self convertPoint: location fromView: nil]]; 1652 1653 if (event == original) 1654 { 1655 firstIndex = index; 1656 } 1657 if (index != firstIndex) 1658 { 1659 shouldFinish = YES; 1660 } 1661 1662 /* 1663 * 1 - if menus is only partly visible and the mouse is at the 1664 * edge of the screen we move the menu so it will be visible. 1665 */ 1666 if ([_attachedMenu isPartlyOffScreen]) 1667 { 1668 NSPoint pointerLoc = [_window convertBaseToScreen: location]; 1669 NSRect screenFrame = [[_window screen] visibleFrame]; 1670 /* 1671 * The +/-1 in the y - direction is because the flipping 1672 * between X-coordinates and GNUstep coordinates let the 1673 * GNUstep screen coordinates start with 1. 1674 */ 1675 if (pointerLoc.x == 0 || pointerLoc.y == 1 1676 || pointerLoc.x == screenFrame.size.width - 1 1677 || pointerLoc.y == screenFrame.size.height) 1678 [_attachedMenu shiftOnScreen]; 1679 } 1680 1681 /* 1682 * 2 - Check if we have to reset the justAttachedNewSubmenu 1683 * flag to NO. 1684 */ 1685 if (justAttachedNewSubmenu && index != -1 1686 && index != _highlightedItemIndex) 1687 { 1688 if (location.x - lastLocation.x > MOVE_THRESHOLD_DELTA) 1689 { 1690 delayCount ++; 1691 if (delayCount >= DELAY_MULTIPLIER) 1692 { 1693 justAttachedNewSubmenu = NO; 1694 } 1695 } 1696 else 1697 { 1698 justAttachedNewSubmenu = NO; 1699 } 1700 } 1701 1702 1703 // 3 - If we have moved outside this menu, take appropriate action 1704 if (index == -1) 1705 { 1706 NSPoint locationInScreenCoordinates; 1707 NSWindow *windowUnderMouse; 1708 NSMenu *candidateMenu; 1709 1710 subMenusNeedRemoving = NO; 1711 1712 locationInScreenCoordinates 1713 = [_window convertBaseToScreen: location]; 1714 1715 /* 1716 * 3a - Check if moved into one of the ancestor menus. 1717 * This is tricky, there are a few possibilities: 1718 * We are a transient attached menu of a 1719 * non-transient menu 1720 * We are a non-transient attached menu 1721 * We are a root: isTornOff of AppMenu 1722 */ 1723 candidateMenu = [_attachedMenu supermenu]; 1724 while (candidateMenu 1725 && !NSMouseInRect (locationInScreenCoordinates, 1726 [[candidateMenu window] frame], NO) // not found yet 1727 && (! ([candidateMenu isTornOff] 1728 && ![candidateMenu isTransient])) // no root of display tree 1729 && [candidateMenu isAttached]) // has displayed parent 1730 { 1731 candidateMenu = [candidateMenu supermenu]; 1732 } 1733 1734 if (candidateMenu != nil 1735 && NSMouseInRect (locationInScreenCoordinates, 1736 [[candidateMenu window] frame], NO)) 1737 { 1738 BOOL candidateMenuResult; 1739 NSMenuView *subMenuView = [[candidateMenu attachedMenu] menuRepresentation]; 1740 1741 // The call to fetch attachedMenu is not needed. But putting 1742 // it here avoids flicker when we go back to an ancestor 1743 // menu and the attached menu is already correct. 1744 [subMenuView detachSubmenu]; 1745 1746 // Reset highlighted index for this menu. 1747 // This way if we return to this submenu later there 1748 // won't be a highlighted item. 1749 [subMenuView setHighlightedItemIndex: -1]; 1750 1751 candidateMenuResult = [[candidateMenu menuRepresentation] 1752 _trackWithEvent: original 1753 startingMenuView: mainWindowMenuView]; 1754 return candidateMenuResult; 1755 } 1756 1757 // 3b - Check if we enter the attached submenu 1758 windowUnderMouse = [[_attachedMenu attachedMenu] window]; 1759 if (windowUnderMouse != nil 1760 && NSMouseInRect (locationInScreenCoordinates, 1761 [windowUnderMouse frame], NO)) 1762 { 1763 BOOL wasTransient = [_attachedMenu isTransient]; 1764 BOOL subMenuResult; 1765 1766 subMenuResult 1767 = [[self attachedMenuView] _trackWithEvent: original 1768 startingMenuView: mainWindowMenuView]; 1769 if (subMenuResult 1770 && wasTransient == [_attachedMenu isTransient]) 1771 { 1772 [self detachSubmenu]; 1773 } 1774 return subMenuResult; 1775 } 1776 1777 /* We track the menu correctly when this is located 1778 in a window */ 1779 if (mainWindowMenuView != nil) 1780 { 1781 // If the user moves the mouse into the main window 1782 // horizontal menu, start tracking again. 1783 NSWindow *mainWindow = [mainWindowMenuView window]; 1784 NSPoint locationInMainWindow = [mainWindow 1785 convertScreenToBase: locationInScreenCoordinates]; 1786 if ([mainWindowMenuView 1787 hitTest: locationInMainWindow] != nil) 1788 { 1789 int index = [mainWindowMenuView indexOfItemAtPoint: 1790 [mainWindowMenuView 1791 convertPoint: locationInMainWindow 1792 fromView: nil]]; 1793 if (index != -1 && 1794 index != [mainWindowMenuView highlightedItemIndex]) 1795 { 1796 [self setHighlightedItemIndex: -1]; 1797 return [mainWindowMenuView 1798 _trackWithEvent: original 1799 startingMenuView: mainWindowMenuView]; 1800 } 1801 } 1802 } 1803 } 1804 1805 // 4 - We changed the selected item and should update. 1806 if (!justAttachedNewSubmenu && index != _highlightedItemIndex) 1807 { 1808 subMenusNeedRemoving = NO; 1809 [self detachSubmenu]; 1810 [self setHighlightedItemIndex: index]; 1811 1812 // WO: Question? Why the ivar _items_link 1813 if (index >= 0 && [[_items_link objectAtIndex: index] submenu]) 1814 { 1815 [self attachSubmenuForItemAtIndex: index]; 1816 justAttachedNewSubmenu = YES; 1817 delayCount = 0; 1818 } 1819 } 1820 1821 // Update last seen location for the justAttachedNewSubmenu logic. 1822 lastLocation = location; 1823 } 1824 1825 do 1826 { 1827 event = [NSApp nextEventMatchingMask: eventMask 1828 untilDate: theDistantFuture 1829 inMode: NSEventTrackingRunLoopMode 1830 dequeue: YES]; 1831 type = [event type]; 1832 if (type == NSAppKitDefined) 1833 { 1834 [[event window] sendEvent: event]; 1835 } 1836 } 1837 while (type == NSAppKitDefined); 1838 } 1839 while ((type != NSLeftMouseUp && 1840 type != NSRightMouseUp && 1841 type != NSOtherMouseUp) || shouldFinish == NO); 1842 1843 /* 1844 * Ok, we released the mouse 1845 * There are now a few possibilities: 1846 * A - We released the mouse outside the menu. 1847 * Then we want the situation as it was before 1848 * we entered everything. 1849 * B - We released the mouse on a submenu item 1850 * (i) - this was highlighted before we started clicking: 1851 * Remove attached menus 1852 * (ii) - this was not highlighted before pressed the mouse button; 1853 * Keep attached menus. 1854 * C - We released the mouse above an ordinary action: 1855 * Execute the action. 1856 * 1857 * In case A, B and C we want the transient menus to be removed 1858 * In case A and C we want to remove the menus that were created 1859 * during the dragging. 1860 * 1861 * So we should do the following things: 1862 * 1863 * 1 - Stop periodic events, 1864 * 2 - Determine the action. 1865 * 3 - Remove the Transient menus from the screen. 1866 * 4 - Perform the action if there is one. 1867 */ 1868 1869 // FIXME 1870 [NSEvent stopPeriodicEvents]; 1871 1872 /* 1873 * We need to store this, because _highlightedItemIndex 1874 * will not be valid after we removed this menu from the screen. 1875 */ 1876 indexOfActionToExecute = _highlightedItemIndex; 1877 1878 // remove transient menus. -------------------------------------------- 1879 { 1880 NSMenu *currentMenu = _attachedMenu; 1881 1882 while (currentMenu && ![currentMenu isTransient]) 1883 { 1884 currentMenu = [currentMenu attachedMenu]; 1885 } 1886 1887 while ([currentMenu isTransient] && [currentMenu supermenu]) 1888 { 1889 currentMenu = [currentMenu supermenu]; 1890 } 1891 1892 [[currentMenu menuRepresentation] detachSubmenu]; 1893 1894 if ([currentMenu isTransient]) 1895 { 1896 [currentMenu closeTransient]; 1897 } 1898 } 1899 1900 if (indexOfActionToExecute == -1) 1901 { 1902 return YES; 1903 } 1904 1905 // Before executing the action, uncapture the mouse 1906 [_window _releaseMouse: self]; 1907 1908 /* If we have menu in window, close the menu after select 1909 an option */ 1910 if (mainWindowMenuView != nil) 1911 { 1912 if (self != mainWindowMenuView) 1913 { 1914 [mainWindowMenuView setHighlightedItemIndex: -1]; 1915 } 1916 [[[mainWindowMenuView menu] attachedMenu] close]; 1917 } 1918 1919 if ([self _executeItemAtIndex: indexOfActionToExecute 1920 removeSubmenu: subMenusNeedRemoving] == NO) 1921 { 1922 return NO; 1923 } 1924 1925 [_attachedMenu performActionForItemAtIndex: indexOfActionToExecute]; 1926 1927 /* 1928 * Remove highlighting. 1929 * We first check if it still highlighted because it could be the 1930 * case that we choose an action in a transient window which 1931 * has already dissappeared. 1932 */ 1933 if (_highlightedItemIndex >= 0) 1934 { 1935 [self setHighlightedItemIndex: -1]; 1936 } 1937 return YES; 1938} 1939 1940- (BOOL) trackWithEvent: (NSEvent*)event 1941{ 1942 BOOL result = NO; 1943 NSMenuView *mainWindowMenuView = nil; 1944 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; 1945 1946 [nc postNotificationName: NSMenuDidBeginTrackingNotification 1947 object: [self menu]]; 1948 1949 if (NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", self) == 1950 NSWindows95InterfaceStyle && 1951 ![[self menu] isTransient] && 1952 ![[self menu] _ownedByPopUp]) 1953 { 1954 mainWindowMenuView = self; 1955 } 1956 1957 // Capture the mouse so we get clicks outside the menus and 1958 // GNUstep windows. 1959 [_window _captureMouse: self]; 1960 NS_DURING 1961 result = [self _trackWithEvent: event 1962 startingMenuView: mainWindowMenuView]; 1963 NS_HANDLER 1964 [_window _releaseMouse: self]; 1965 [localException raise]; 1966 NS_ENDHANDLER 1967 [_window _releaseMouse: self]; 1968 [nc postNotificationName: NSMenuDidEndTrackingNotification 1969 object: [self menu]]; 1970 return result; 1971} 1972 1973/** 1974 This method is called when the user clicks on a button in the menu. 1975 Or, if a right click happens and the app menu is brought up. 1976 1977 The original position is stored, so we can restore the position of menu. 1978 The position of the menu can change during the event tracking because 1979 the menu will automatillay move when parts are outside the screen and 1980 the user move the mouse pointer to the edge of the screen. 1981*/ 1982- (void) mouseDown: (NSEvent*)theEvent 1983{ 1984 NSRect currentFrame; 1985 NSRect originalFrame; 1986 NSPoint currentTopLeft; 1987 NSPoint originalTopLeft = NSZeroPoint; /* Silence compiler. */ 1988 BOOL restorePosition; 1989 /* 1990 * Only for non transient menus do we want 1991 * to remember the position. 1992 */ 1993 restorePosition = ![_attachedMenu isTransient]; 1994 1995 if (restorePosition) 1996 { // store old position; 1997 originalFrame = [_window frame]; 1998 originalTopLeft = originalFrame.origin; 1999 originalTopLeft.y += originalFrame.size.height; 2000 } 2001 2002 [NSEvent startPeriodicEventsAfterDelay: 0.1 withPeriod: 0.01]; 2003 [self trackWithEvent: theEvent]; 2004 [NSEvent stopPeriodicEvents]; 2005 2006 if (restorePosition) 2007 { 2008 currentFrame = [_window frame]; 2009 currentTopLeft = currentFrame.origin; 2010 currentTopLeft.y += currentFrame.size.height; 2011 2012 if (NSEqualPoints(currentTopLeft, originalTopLeft) == NO) 2013 { 2014 NSPoint origin = currentFrame.origin; 2015 2016 origin.x += (originalTopLeft.x - currentTopLeft.x); 2017 origin.y += (originalTopLeft.y - currentTopLeft.y); 2018 [_attachedMenu nestedSetFrameOrigin: origin]; 2019 } 2020 } 2021} 2022 2023- (void) rightMouseDown: (NSEvent*) theEvent 2024{ 2025 [self mouseDown: theEvent]; 2026} 2027 2028- (void) otherMouseDown: (NSEvent*) theEvent 2029{ 2030 [self mouseDown: theEvent]; 2031} 2032 2033- (BOOL) performKeyEquivalent: (NSEvent *)theEvent 2034{ 2035 return [_attachedMenu performKeyEquivalent: theEvent]; 2036} 2037 2038- (void) _themeDidActivate: (NSNotification*)notification 2039{ 2040 // The new theme may have different menu item sizes, 2041 // so the window size for the menu needs to be recalculated. 2042 [[self menu] sizeToFit]; 2043} 2044 2045/* 2046 * NSCoding Protocol 2047 * 2048 * Normally unused because NSMenu does not encode its NSMenuView since 2049 * NSMenuView is considered a platform specific way of rendering the menu. 2050 */ 2051- (void) encodeWithCoder: (NSCoder*)encoder 2052{ 2053 [super encodeWithCoder: encoder]; 2054 if ([encoder allowsKeyedCoding] == NO) 2055 { 2056 [encoder encodeObject: _itemCells]; 2057 [encoder encodeObject: _font]; 2058 [encoder encodeValueOfObjCType: @encode(BOOL) at: &_horizontal]; 2059 [encoder encodeValueOfObjCType: @encode(float) at: &_horizontalEdgePad]; 2060 [encoder encodeValueOfObjCType: @encode(NSSize) at: &_cellSize]; 2061 } 2062} 2063 2064- (id) initWithCoder: (NSCoder*)decoder 2065{ 2066 self = [super initWithCoder: decoder]; 2067 if (!self) 2068 return nil; 2069 2070 if ([decoder allowsKeyedCoding] == NO) 2071 { 2072 [decoder decodeValueOfObjCType: @encode(id) at: &_itemCells]; 2073 2074 [_itemCells makeObjectsPerformSelector: @selector(setMenuView:) 2075 withObject: self]; 2076 2077 [decoder decodeValueOfObjCType: @encode(id) at: &_font]; 2078 [decoder decodeValueOfObjCType: @encode(BOOL) at: &_horizontal]; 2079 [decoder decodeValueOfObjCType: @encode(float) at: &_horizontalEdgePad]; 2080 [decoder decodeValueOfObjCType: @encode(NSSize) at: &_cellSize]; 2081 2082 _highlightedItemIndex = -1; 2083 _needsSizing = YES; 2084 } 2085 return self; 2086} 2087 2088@end 2089 2090@implementation NSMenuView (GNUstepPrivate) 2091 2092- (NSArray *)_itemCells 2093{ 2094 return _itemCells; 2095} 2096 2097@end 2098 2099