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