diff --git a/ChangeLog b/ChangeLog index e70c6be49..1e4f9badf 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,54 @@ +2003-03-06 Willem Rein Oudshoorn + + * Source/NSMenuView.m: Added NSMenuTitleView (moved + from NSMenu). + ([NSMenuView -detachSubmenu]): Removed setting highlighting. + ([NSMenuView -attachSubmenuForItemAtIndex:]) added logging. + ([NSMenuView -update]): Handle _titleView. + ([NSMenuView -sizeToFit]): Take titleView into account + ([NSMenuView -performActionWithHighlightingForItemAtIndex:]): + restore old highlighting. + ([NSMenuView -trackWithEvent:]): Rewritten. + ([NSMenuView -mouseDown:]): Only restore menu position + when needed. + ([NSMenuView -rightMouseDown:theEvent]): On menus treat + as ordinary mouse down to avoid rightclick menus on menus. + + * Source/NSMenu.m (NSView): Moved NSMenuWindowTitleView to + NSMenuView. Added comments. Added -_updateUserDefaults:, + -_menuMoved: methods to track moving menus. + Renamed -_setTornOff: to -setTornOff:, -isFollowTransient to + -isTransient. + ([NSMenu -initWithTitle:]): Remove some old comments, remove + reference to titleView, listen to window move notification and + enqueued move notification. + ([NSMenu -attachedMenu]): Adjusted for renamed ivar. + ([NSMenu -sizeToFit]): Remove references to titleView + ([NSMenu -setTornOff:]): Remove references to titleView, + call update to NSMenuView instead. + ([NSMenu -isPartlyOffScreen]): Do not depend in removed ivar. + ([NSMenu -_performMenuClose:]): adjust to renamed methods, will + force synchronization of userdefaults. + ([NSMenu -displayTransient]): Removed references to _titleView, + remember the highlighted index to restore with -closeTransient. + ([NSMenu -close]): remove references to _titleview, use methods instead + of relying on ivars. added call to update on menu representation. + ([NSMenu -closeTransient]): Remove references to _titleView, restore + highlighted index. Call update on menu representantation. + ([NSMenu -shiftOnScreen]): Rewrote to move in x and y direction + simultanuously. + ([NSMenuWindowTitleView -acceptsFirstMouse:]): Removed useless + code to set menuview for itemCells. + + * Headers/gnustep/gui/NSMenuView.h: added ivar _titleView. + (moved from NSMenu). + + * Headers/gnustep/gui/NSMenu.h: Added NSMenuView protocol, + added documentation to NSMenu class. + (MSMenu): Removed ivar _titleView, _isPartlyOffScreen, added + ivar _oldHighlightedIndex. Renamed method -isFollowTransient to + -isTransient. Removed method -nestedCheckOffScreen + 2003-03-05 17:07 Alexander Malmberg * Source/NSLayoutManager.m (-drawGlyphsForGlyphRange:atPoint:): diff --git a/Headers/gnustep/gui/NSMenu.h b/Headers/gnustep/gui/NSMenu.h index c39ae89e9..5737e401c 100644 --- a/Headers/gnustep/gui/NSMenu.h +++ b/Headers/gnustep/gui/NSMenu.h @@ -37,18 +37,339 @@ @class NSString; @class NSEvent; @class NSMatrix; -@class NSMenuView; @class NSPopUpButton; @class NSPopUpButtonCell; @class NSView; @class NSWindow; @class NSMutableArray; + +@protocol NSMenuView +/** + Set the menu that this view object will be drawing. + This method will NOT retain the menu. + In normal usage an instance of NSMenu will + use this method to supply the NSMenuView with reference + to itself. The NSMenu will retain the NSMenuView. + */ +- (void)setMenu:(NSMenu *)menu; +/** + Set the currently highlighted item. + + This is used by the NSMenu class to restore + the selected item when it is temporary set to + another item. This happens when both the regular + version and the transient version are on the screen. + + A value of -1 means that no item will be highlighted. +*/ +- (void)setHighlightedItemIndex:(int)index; +/** + Returns the currently highlighted item. Returns -1 + if no item is highlighted. +*/ +- (int)highlightedItemIndex; + +/** + This should ensure that if there is an attached + submenu this submenu will be detached. + + Detaching means that this particular menu representation + should be removed from the screen. + + It should implement a deep detach, that is, all + attached submenus of this menu should also be detached. +*/ +- (void) detachSubmenu; + +/** + This will relayout the NSMenuView. + + It should be called when the menu changes. Changes include + becoming detached, adding or removing submenu items etcetera. + + However, normally it does not need to be called directly because + Because the NSMenuView is supposed to listen to the NSMenu notifications + for the item added, removed and change notifications. + + It should be called explicitly when other changes occur, such as + becoming detached or changing the title. +*/ +- (void)update; + +/** + Hm, why is this method needed? Shouldn't this be done by + the update method? +*/ +- (void)sizeToFit; //!!! + +/** + Method used by NSMenuItemCell to draw itself correctly and nicely + lined up with the other menu items. +*/ +- (float)stateImageWidth; +/** + Method used by NSMenuItemCell to draw itself correctly and nicely + lined up with the other menu items +*/ +- (float)imageAndTitleOffset; +/** + Methos used by NSMenuItemCell to draw itself correctly and nicely + lined up with the other menu items. +*/ +- (float)imageAndTitleWidth; +/** + Methos used by NSMenuItemCell to draw itself correctly and nicely + lined up with the other menu items. +*/ +- (float)keyEquivalentOffset; +/** + Used by NSItemCell to ... +*/ +- (float)keyEquivalentWidth; + +/** + Used by the NSMenu to determine where to position a + submenu. +*/ +- (NSPoint)locationForSubmenu:(NSMenu *)aSubmenu; + +- (void)performActionWithHighlightingForItemAtIndex:(int)index; //???? + +/** +

+ This is method is responsible for handling all events while + the user is interacting with this menu. It should pass on this + call to another menurepresentation when the user moves the + mouse cursor over either a submenu or over the supermenu. +

+ The method returns when the interaction from the user with the + menu system is over. +

+ The method returns NO when the user releases the mouse button + above a submenu item and YES in all other cases. +

+

+ This return value can be used to determine if submenus should + be removed from the screen or that they are supposed to stay. +

+

+ The implementation should roughly follow the following logic: +

+ +{ + while (have not released mouse button) + { + if (mouse hovers over submenu, or supermenu) + { + if ([(menurepresentation under mouse) + trackWithEvent: the event]) + { + [self detachSubmenu]; + return YES; + } + return NO; + } + //highlight item under mouse + + if (highlighting submenu item) + { + [self attachSubmenuAtIndex:..]; + } + else + { + [self detachSubmenu]; + } + get next event. + } + + execute the menu action if applicable; + + return YES | NO depending on the situation; +} + + + Note that actual implementations tend to be more complicated because + because of all kind of useability issues. Useabilities issues to + look out for are: + + Menus that are only partly on the screen. Those need to be moved while + navigation the menu. + Submenus that are hard to reach. If the natural route to the content of a submenu + travels through other menu items you do not want to remove the submenu immediately. + + Transient menus require slightly different behaviour from the normal menus. + For example, when selecting a action from a transient menu that brings up a modal + panel you would expect the transient menu to dissappear. However in the normal + menu system you want it to stay, so you still have feedback on which menu action + triggered the modal panel. + + + +*/ +- (BOOL)trackWithEvent:(NSEvent *)event; + +@end + + + +/** +

+Menus provide the user with a list of actions and/or submenus. +Submenus themselves are full fledged menus and so a heirarchical +structure of appears. +

+

+Every application has one special menu, the so called Application menu. +This menu is always visible on the screen when the application is active. +This menu normally contains items like, info, +services, print, hide and quit. +

+

+After the info item normally some submenus follow containing +the application specific actions. +

+

+On GNUstep the content of the menu is stacked vertically as oppossed to +the Windows and Mac world, where they are stacked horizontally. +Also because the menus are not part of any normal window they can be dragged +around opened and closed independend of the application windows. +

+

+This can lead to a few menus being open simultanuously. +The collection of open menus is remembered, +when the program is started again, all the torn off menus aka +detached menus, are displayed at their last known position. +

+

+The menu behaviour is richer than in most other environments and +bear some explanation. This explanation is aimed at users of Menus but more so +at the developer of custom menus. +

+ +Application menu +There alwasy at least one menu +present and visible while the application is active. This is the application menu. +This window can never be closed. + +Attached menu + +Normally when you click in a menu +on a submenu item, the submenu is shown directly next to the menu you click in. +The submenu is now called an attached menu. It is attached to the +menu that was clicked in. + +Detached menu + +A menu is detached when it is not attached +to its parent menu. A menu can become +detached when the user drags a submenu away from its parents. +A detached window contains in its title a close button. + +Transient menu + +A transient menu is a menu that dissappears as +soon as the user stops interacting with the menus. +Typically a transient menu is created when a right mouse click appears in an +application window. The right mouse click will bring up the Application menu +at the place the user clicks. While keeping the mouse button down the +user can select items by moving around. When releasing the button, all +transient menus will be removed from the screen and the action will be executed. +

+It is important to note that it is impossible to click in transient menus. +

+
+Attached transient menu + +This is a menu that is attached and transient at the same time. + +
+ +A single NSMenu instance can be displayed zero or one times when the user is +not interaction with the menus. +When the user is interaction with the menus it can occur that the same NSMenu +is displayed in two locations at the same time. This is only possible +when one of the displayed instances is a transient menu. + +To understand how the diffent kind of menus are created lets look at some user actions: + + +The user clicks on an item which is not a submenu.
+ The item is highlighted until the action corresponding with the item is completed. + More precisely, the application highlights the menu item, performs the action, and unhighlights the item. +
+The user clicks on a submenu item which is not highlighted already
+ If the submenu is not a detached menu, the submenu will become an attached + menu to the menu that is clicked in. The item that is clicked in will + become highlighted and stays highlighted. +

+ If the submenu is a detached menu, the transient version of the submenu + will be shown +

+ +
+The user clicks on a submenu item which is highlighted
+ This means that the submenu is an attached submenu for this menu. + After clicking the submenu item will no be no longer highlighted and + the submenu will be removed from the screen. +
+The user drags over a menu item
+ The item will be highlighted, if the item is a submenu item, the submenu + will be shown as an attached submenu. This can be transient, or non transient. +
+
+ + + +
+Customizing the look of Menus +
+ +There are basically three ways of customizing the look of NSMenu + +Using custom NSMenuItemCell's. This you should do when you want to influence +the look of the items displayed in the menu. +Using custom NSMenuView. This is the class to modify if you want to change +the way the menu is layout on the screen. So if you want to stack the menu +items horizontally, you should change this class. This should be rarely needed. + +Reimplement NSMenu. This you should not do. But, if you implement +everything yourself you can achieve anything. + + + +
+Information for implementing custom NSMenuView class +
+When implementing a custom NSMenuView class it is important +to keep the following information in mind. + + + The menus (or the menu items) form a tree. Navigating through this tree +is done with the methods [NSMenu-supermenu], which returns the parent menu +of the receiver, and with [NSMenu-itemAtIndex:] which returns a +NSMenuItem on which we can call [NSMenuItem-submenu] for a child menu. + + The menus as displayed on the screen do NOT form a tree. +This because detached and transient menus lead to duplicate menus on the screen. + + + +The displayed menus on the screen have the following structure: + + +The ordered graph of displayed menus (note, NOT the set of NSMenus) form a collection of line graphs. +The attached menus are precisely the non root vertices in this graph. +An attached menu of a transient menu is itself a transient menu. +The collection of transient menus form connect subgraph of the menu graph. + + +*/ @interface NSMenu : NSObject { NSString *_title; NSMutableArray *_items; - NSMenuView *_view; + NSView* _view; NSMenu *_superMenu; NSMenu *_attachedMenu; NSMutableArray *_notifications; @@ -59,21 +380,25 @@ // GNUstepExtra category NSPopUpButtonCell *_popUpButtonCell; - BOOL _follow_transient; - BOOL _isPartlyOffScreen; + BOOL _transient; @private NSWindow *_aWindow; NSWindow *_bWindow; - id _titleView; NSMenu *_oldAttachedMenu; + int _oldHiglightedIndex; } /* Controlling Allocation Zones */ + (void) setMenuZone: (NSZone*)zone; + (NSZone*) menuZone; -/* Creating an NSMenu */ + +/** + * + * + */ + - (id) initWithTitle: (NSString*)aTitle; /* Setting Up the Menu Commands */ @@ -92,6 +417,9 @@ - (void) removeItemAtIndex: (int)index; /* Finding menu items */ +/** + * Returns an array containing all menu items in this menu. + */ - (NSArray*) itemArray; - (id ) itemAtIndex: (int)index; - (id ) itemWithTag: (int)aTag; @@ -110,11 +438,52 @@ /* Managing submenus */ - (void) setSubmenu: (NSMenu*)aMenu forItem: (id )anItem; - (void) submenuAction: (id)sender; + +/** + Returns the menu that is attached to this menu. +

+ If two instances of this menu are visible, + return the attached window of the transient version + of this menu. +

+

+ If no menu is attached return nil. +

+*/ - (NSMenu*) attachedMenu; +/** + Returns if this menu is attached to its supermenu, + return nil if it does not have a parent menu. +

+ If two instances of this menu are visible, return + the outcome of the check for the transient version + of the menu. +

+*/ - (BOOL) isAttached; +/** + If there are two instances of this menu visible, return NO. + Otherwise, return YES if we are a detached menu and visible. +*/ - (BOOL) isTornOff; + +/** + Returns the position where submenu will be displayed + when it will be displayed as an attached menu of this menu. + The result is undefined when aSubmenu is not actually a submenu + of this menu. +*/ - (NSPoint) locationForSubmenu:(NSMenu*)aSubmenu; +/** + Returns the supermenu of this menu. Return nil + if this is the application menu. +*/ - (NSMenu*) supermenu; +/** + Set the supermenu of this menu. + TODO: add explanation if this will change remove this menu + from the old supermenu or if it does not. +*/ - (void) setSupermenu: (NSMenu *)supermenu; /* Enabling and disabling menu items */ @@ -128,12 +497,34 @@ /* Simulating Mouse Clicks */ - (void) performActionForItemAtIndex: (int)index; -/* Setting the Title */ +/** + Change the title of the menu. +*/ - (void) setTitle: (NSString*)aTitle; +/** + Returns the current title. +*/ - (NSString*) title; -/* Setting the representing object */ -- (void) setMenuRepresentation: (id)menuRep; +/** + Set the View that should be used to display the menu. +

+ The default is NSMenuView, but a user can supply its + own NSView object as long as it +

+ + Inherits from NSView + Implements NSMenuView protocol + +*/ +- (void) setMenuRepresentation: (id) menuRep; +/** + Return the NSView that is used for drawing + the menu. + It is the view set with [NSMenu-setMenuRepresentation:] and + therefore it should be safe to assume it is an NSView + implementing the NSMenuView protocol. +*/ - (id) menuRepresentation; /* Updating Menu Layout */ @@ -143,6 +534,7 @@ /* Displaying Context-Sensitive Help */ - (void) helpRequested: (NSEvent*)event; + + (void) popUpContextMenu: (NSMenu*)menu withEvent: (NSEvent*)event forView: (NSView*)view; @@ -175,25 +567,76 @@ - (BOOL) validateMenuItem: (NSMenuItem*)aMenuItem; @end +/** + This interface exist contains methods that are meant + for the NSMenuView. If you write your own implementation + of the NSMenuView interface you can use these methods + to popup other menus or close them. +*/ @interface NSMenu (GNUstepExtra) -- (BOOL) isFollowTransient; + +/** + Returns YES if there is a transient version + of this menu displayed on the screen. +*/ +- (BOOL) isTransient; +/** + Returns the window in which this menu is displayed. + If there is a transient version it will return the + window in which the transient version is displayed. + If the Menu is not displayed at all the result + is meaningless. +*/ - (NSWindow*) window; /* Shows the menu window on screen */ +/** + Show menu on the screen. This method can/should be used by + the menurepresentation to display a submenu on the screen. + */ - (void) display; +/** + Display the transient version of the menu. +*/ - (void) displayTransient; -- (void) setGeometry; -/* Close the associated window menu */ +/** + Positions the menu according to the standard user defaults. + If the position is not found in the defaults revert to positioning + the window in the upper left corner. +*/ +- (void) setGeometry; + +/** + When the flag is YES + this method will detach the receiver from its parent and + update the menurepresentation so it will display a close + button if appropriate. + + If the flag is NO this method will update the menurepresentation + so it will be able to remove the close button if needed. + Note that it will not reattach to its parent menu. +*/ +- (void) setTornOff: (BOOL) flag; + + +/** + Remove the window from the screen. This method can/should be + used by the menurepresentation to remove a submenu from the screen. + */ - (void) close; -- (void) closeTransient; +/** + Remove the transient version of the window from the screen. + This method is used by NSMenuView implementations that need + to open/close transient menus. +*/ +- (void) closeTransient; /* Moving menus */ - (void) nestedSetFrameOrigin: (NSPoint)aPoint; /* Shift partly off-screen menus */ -- (BOOL) isPartlyOffScreen; -- (void) nestedCheckOffScreen; +- (BOOL) isPartlyOffScreen; - (void) shiftOnScreen; /* Popup behaviour */ diff --git a/Headers/gnustep/gui/NSMenuView.h b/Headers/gnustep/gui/NSMenuView.h index d45317bf6..d5de238fb 100644 --- a/Headers/gnustep/gui/NSMenuView.h +++ b/Headers/gnustep/gui/NSMenuView.h @@ -41,11 +41,39 @@ #include #include +/** + The NSMenu class uses an object implementing the NSMenuView protocol to + do the actual drawing. + + Normally there is no good reason to write your own class implementing + this protocol. However if you want to customize your menus you should + implement this protocol to ensure that it works nicely together + with sub/super menus not using your custom menurepresentation. + +
+ How menus are drawn
+ + +*/ + @class NSColor; @class NSPopUpButton; @class NSFont; -@interface NSMenuView : NSView +/** + This class implements several menu look and feels at the same time. + The looks and feels implemented are: + + Ordinary vertically stacked menus with the NeXT submenu positioning behavour. + Vertically stacked menus with the WindowMaker submenu placement. This behaviour + is selected by choosing the GSWindowMakerInterfaceStyle. + Horizontally stacked menus. This can be only set on a individual basis by + using [NSMenuView-setHorizontal:]. + PopupButtons are actually menus. This class implements also the behaviour for the + NSPopButtons. See for the the class NSPopButton. + +*/ +@interface NSMenuView : NSView { NSMutableArray *_itemCells; BOOL _horizontal; @@ -62,9 +90,8 @@ NSSize _cellSize; @private id _items_link; - BOOL _keepAttachedMenus; - int _oldHighlightedItemIndex; int _leftBorderOffset; + id _titleView; } + (float)menuBarHeight; @@ -84,14 +111,14 @@ - (NSMenuView *)attachedMenuView; - (NSMenu *)attachedMenu; - (BOOL)isAttached; +- (void) detachSubmenu; +- (void) attachSubmenuForItemAtIndex: (int) index; - (BOOL)isTornOff; - (void)setHorizontalEdgePadding:(float)pad; - (float)horizontalEdgePadding; - (void)itemChanged:(NSNotification *)notification; - (void)itemAdded:(NSNotification *)notification; - (void)itemRemoved:(NSNotification *)notification; -- (void)detachSubmenu; -- (void)attachSubmenuForItemAtIndex:(int)index; - (void)update; - (void)setNeedsSizing:(BOOL)flag; - (BOOL)needsSizing; @@ -116,4 +143,5 @@ - (BOOL)trackWithEvent:(NSEvent *)event; @end + #endif diff --git a/Source/NSMenu.m b/Source/NSMenu.m index 0d11b9d4e..c4fa6104a 100644 --- a/Source/NSMenu.m +++ b/Source/NSMenu.m @@ -55,39 +55,78 @@ #include #include + +/* + Drawing related: + + NSMenu superMenu (if not root menu, the parent meu) + ^ + | + | +------------------> NSMenuView view (content, draws the menu items) + | | + NSMenu +----------+-------> NSMenuPanel A (regular window, torn off window) + | | `-------> NSMenuPanel B (transient window) + | | + | +------------------> NSString title (title) + | + v + NSMenu attachedMenu (the menu that is attached to this one, during navigation) + + + + +--[NSMenuPanel]------+ + | +-[NSMenuView]----+ | + | | title if applic | | + | | +-------------+ | | + | | | NSMenuItem- | | | + | | | Cell | | | + | | +-------------+ | | + | | . | | + | | . | | + | +-----------------+ | + +---------------------+ + + The two windows + --------------- + + Basically we have for a menu two windows, window A and window B. + Window A is the regular window and Window B is used for transient windows. + + At any one time, the views, like title view, NSMenuView are put either in + window A or in window B. They are moved over from one window to the oter + when needed. + + the code is supposed to know when it is using window A or B. + But it will probably only work correctly when + + window A correspond to _transient == NO + window B correspond to _transient == YES +*/ + + /* Subclass of NSPanel since menus cannot become key */ @interface NSMenuPanel : NSPanel @end -/* A menu's title is an instance of this class */ -@interface NSMenuWindowTitleView : NSView -{ - id menu; - NSButton *button; -} - -- (void) addCloseButton; -- (void) releaseCloseButton; -- (void) createButton; -- (void) setMenu: (NSMenu*)menu; -- (NSMenu*) menu; - -@end - @interface NSMenuView (GNUstepPrivate) - (NSArray *)_itemCells; @end static NSZone *menuZone = NULL; -static NSString *NSMenuLocationsKey = @"NSMenuLocations"; +static NSString *NSMenuLocationsKey = @"NSMenuLocations"; +static NSString *NSEnqueuedMenuMoveName = @"EnqueuedMoveNotificationName"; static NSNotificationCenter *nc; @interface NSMenu (GNUstepPrivate) + - (NSString *) _locationKey; - (NSMenuPanel *) _createWindow; +- (void) _updateUserDefaults: (id) notification; + @end + @implementation NSMenuPanel - (BOOL) canBecomeKeyWindow { @@ -139,6 +178,68 @@ static NSNotificationCenter *nc; return win; } +/** + Will track the mouse movement. It will trigger the updating of the user + defaults in due time. +*/ +- (void) _menuMoved: (id) notification +{ + NSNotification *resend; + + resend = [NSNotification notificationWithName: NSEnqueuedMenuMoveName + object: self]; + + [[NSNotificationQueue defaultQueue] + enqueueNotification: resend + postingStyle: NSPostASAP + coalesceMask: NSNotificationCoalescingOnSender + forModes: [NSArray arrayWithObject: NSDefaultRunLoopMode]]; +} + +/** + Save the current menu position in the standard user defaults +*/ +- (void) _updateUserDefaults: (id) notification +{ + NSString *key; + + NSDebugLLog (@"NSMenu", @"Synchronizing user defaults"); + key = [self _locationKey]; + if (key != nil) + { + NSUserDefaults *defaults; + NSMutableDictionary *menuLocations; + NSString *locString; + + defaults = [NSUserDefaults standardUserDefaults]; + menuLocations = [[defaults objectForKey: NSMenuLocationsKey] mutableCopy]; + + if ([_aWindow isVisible] && [self isTornOff]) + { + if (menuLocations == nil) + { + menuLocations = AUTORELEASE([[NSMutableDictionary alloc] initWithCapacity: 2]); + } + locString = [[self window] stringWithSavedFrame]; + [menuLocations setObject: locString forKey: key]; + } + else + { + [menuLocations removeObjectForKey: key]; + } + + if ([menuLocations count] > 0) + { + [defaults setObject: menuLocations forKey: NSMenuLocationsKey]; + } + else + { + [defaults removeObjectForKey: NSMenuLocationsKey]; + } + [defaults synchronize]; + } +} + @end @@ -168,7 +269,7 @@ static NSNotificationCenter *nc; } /* - * Initializing a New NSMenu + * */ - (id) init { @@ -185,14 +286,15 @@ static NSNotificationCenter *nc; RELEASE(_view); RELEASE(_aWindow); RELEASE(_bWindow); - RELEASE(_titleView); [super dealloc]; } +/* + +*/ - (id) initWithTitle: (NSString*)aTitle { - float height; NSView *contentView; [super init]; @@ -203,19 +305,12 @@ static NSNotificationCenter *nc; // Create an array to store out menu items. _items = [[NSMutableArray alloc] init]; - // We have no supermenu. - // _superMenu = nil; - // _is_tornoff = NO; - // _follow_transient = NO; - _changedMessagesEnabled = YES; _notifications = [[NSMutableArray alloc] init]; _changed = YES; // According to the spec, menus do autoenable by default. _autoenable = YES; - // Transient windows private stuff. - // _oldAttachedMenu = nil; /* Please note that we own all this menu network of objects. So, none of these objects should be retaining us. When we are deallocated, @@ -231,15 +326,8 @@ static NSNotificationCenter *nc; _view = [[NSMenuView alloc] initWithFrame: NSMakeRect(0,0,50,50)]; [_view setMenu: self]; - // Create the title view - height = [[_view class] menuBarHeight]; - _titleView = [[NSMenuWindowTitleView alloc] initWithFrame: - NSMakeRect(0, 0, 50, height)]; - [_titleView setMenu: self]; - contentView = [_aWindow contentView]; [contentView addSubview: _view]; - [contentView addSubview: _titleView]; /* Set up the notification to start the process of redisplaying the menus where the user left them the last time. @@ -258,6 +346,16 @@ static NSNotificationCenter *nc; name: NSApplicationWillBecomeActiveNotification object: NSApp]; + [nc addObserver: self + selector: @selector (_menuMoved:) + name: NSWindowDidMoveNotification + object: _aWindow]; + + [nc addObserver: self + selector: @selector (_updateUserDefaults:) + name: NSEnqueuedMenuMoveName + object: self]; + return self; } @@ -558,18 +656,34 @@ static NSNotificationCenter *nc; - (void) submenuAction: (id)sender { - [_view detachSubmenu]; } + - (NSMenu *) attachedMenu { - if (_attachedMenu && _follow_transient - && !_attachedMenu->_follow_transient) + if (_attachedMenu && _transient + && !_attachedMenu->_transient) return nil; return _attachedMenu; } + +/** + Look for the semantics in the header. Note that + this implementation works because there are ... cases: + + + This menu is transient, its supermenu is also transient. + In this case we just do the check between the transient windows + and everything is fine + + + The menu is transient, its supermenu is not transient. + This can go WRONG + + +*/ - (BOOL) isAttached { return _superMenu && [_superMenu attachedMenu] == self; @@ -835,6 +949,9 @@ static NSNotificationCenter *nc; // // Updating the Menu Layout // +// Wim 20030301: Question, what happens when the notification trigger +// new notifications? I think it is not allowed to add items +// to the _notifications array while enumerating it. - (void) setMenuChangedMessagesEnabled: (BOOL)flag { if (_changedMessagesEnabled != flag) @@ -869,39 +986,25 @@ static NSNotificationCenter *nc; NSRect menuFrame; NSSize size; - //if ([_view needsSizing]) - [_view sizeToFit]; + [_view sizeToFit]; menuFrame = [_view frame]; size = menuFrame.size; windowFrame = [_aWindow frame]; + [_aWindow setContentSize: size]; + [_aWindow setFrameTopLeftPoint: + NSMakePoint(NSMinX(windowFrame),NSMaxY(windowFrame))]; + windowFrame = [_bWindow frame]; + [_bWindow setContentSize: size]; + [_bWindow setFrameTopLeftPoint: + NSMakePoint(NSMinX(windowFrame),NSMaxY(windowFrame))]; + if (_popUpButtonCell == nil) { - float height = [[_view class] menuBarHeight]; - - size.height += height; - [_aWindow setContentSize: size]; - [_aWindow setFrameTopLeftPoint: - NSMakePoint(NSMinX(windowFrame),NSMaxY(windowFrame))]; - - windowFrame = [_bWindow frame]; - [_bWindow setContentSize: size]; - [_bWindow setFrameTopLeftPoint: - NSMakePoint(NSMinX(windowFrame),NSMaxY(windowFrame))]; - [_view setFrameOrigin: NSMakePoint (0, 0)]; - [_titleView setFrame: NSMakeRect (0, size.height - height, - size.width, height)]; - [_titleView setNeedsDisplay: YES]; - } - else - { - [_aWindow setContentSize: size]; - [_aWindow setFrameTopLeftPoint: - NSMakePoint(NSMinX(windowFrame),NSMaxY(windowFrame))]; - } + } [_view setNeedsDisplay: YES]; @@ -1013,23 +1116,22 @@ static NSNotificationCenter *nc; #define IS_OFFSCREEN(WINDOW) \ !(NSContainsRect([[NSScreen mainScreen] frame], [WINDOW frame])) -- (void) _setTornOff: (BOOL)flag +- (void) setTornOff: (BOOL)flag { NSMenu *supermenu; - _is_tornoff = flag; + _is_tornoff = flag; if (flag) - [_titleView addCloseButton]; - else - [_titleView releaseCloseButton]; - - supermenu = [self supermenu]; - if (supermenu != nil) { - [[supermenu menuRepresentation] setHighlightedItemIndex: -1]; - supermenu->_attachedMenu = nil; + supermenu = [self supermenu]; + if (supermenu != nil) + { + [[supermenu menuRepresentation] setHighlightedItemIndex: -1]; + supermenu->_attachedMenu = nil; + } } + [_view update]; } - (void) _showTornOffMenuIfAny: (NSNotification*)notification @@ -1051,7 +1153,7 @@ static NSNotificationCenter *nc; location = [menuLocations objectForKey: key]; if (location && [location isKindOfClass: [NSString class]]) { - [self _setTornOff: YES]; + [self setTornOff: YES]; [self display]; } } @@ -1068,52 +1170,25 @@ static NSNotificationCenter *nc; } } -- (BOOL) isFollowTransient +- (BOOL) isTransient { - return _follow_transient; + return _transient; } - (BOOL) isPartlyOffScreen { - return _isPartlyOffScreen; -} - -- (void) nestedCheckOffScreen -{ - // This method is used when the menu is moved. - if (_attachedMenu) - [_attachedMenu nestedCheckOffScreen]; - - _isPartlyOffScreen = IS_OFFSCREEN(_aWindow); + return IS_OFFSCREEN ([self window]); } - (void) _performMenuClose: (id)sender { - NSString *key; - if (_attachedMenu) [_view detachSubmenu]; - - key = [self _locationKey]; - if (key != nil) - { - NSUserDefaults *defaults; - NSMutableDictionary *menuLocations; - - defaults = [NSUserDefaults standardUserDefaults]; - menuLocations = [[defaults objectForKey: NSMenuLocationsKey] mutableCopy]; - [menuLocations removeObjectForKey: key]; - if ([menuLocations count] > 0) - [defaults setObject: menuLocations forKey: NSMenuLocationsKey]; - else - [defaults removeObjectForKey: NSMenuLocationsKey]; - RELEASE(menuLocations); - [defaults synchronize]; - } - + [_view setHighlightedItemIndex: -1]; - [self _setTornOff: NO]; [self close]; + [self setTornOff: NO]; + [self _updateUserDefaults: nil]; } - (void) _rightMouseDisplay: (NSEvent*)theEvent @@ -1125,6 +1200,11 @@ static NSNotificationCenter *nc; - (void) display { + if (_transient) + { + NSDebugLLog (@"NSMenu", @"trying to display while alreay displayed transient"); + } + if (_changed) [self sizeToFit]; @@ -1142,10 +1222,7 @@ static NSNotificationCenter *nc; [self setGeometry]; } } - [_aWindow orderFrontRegardless]; - - _isPartlyOffScreen = IS_OFFSCREEN(_aWindow); } - (void) displayTransient @@ -1153,8 +1230,15 @@ static NSNotificationCenter *nc; NSPoint location; NSView *contentView; - _follow_transient = YES; - + if (_transient) + { + NSDebugLLog (@"NSMenu", @"displaying transient while it is transient"); + return; + } + + _oldHiglightedIndex = [[self menuRepresentation] highlightedItemIndex]; + _transient = YES; + /* * Cache the old submenu if any and query the supermenu our position. * Otherwise, raise menu under the mouse. @@ -1180,20 +1264,14 @@ static NSNotificationCenter *nc; [_bWindow setFrameOrigin: location]; [_view removeFromSuperviewWithoutNeedingDisplay]; - [_titleView removeFromSuperviewWithoutNeedingDisplay]; - - if (_is_tornoff) - [_titleView releaseCloseButton]; contentView = [_bWindow contentView]; [contentView addSubview: _view]; - [contentView addSubview: _titleView]; - + [_view update]; [_bWindow orderFront: self]; - - _isPartlyOffScreen = IS_OFFSCREEN(_bWindow); } + - (void) setGeometry { NSString *key; @@ -1232,6 +1310,12 @@ static NSNotificationCenter *nc; { NSMenu *sub = [self attachedMenu]; + + if (_transient) + { + NSDebugLLog (@"NSMenu", @"We should not close ordinary menu while transient version is still open"); + } + /* * If we have an attached submenu, we must close that too - but then make * sure we still have a record of it so that it can be re-displayed if we @@ -1245,39 +1329,48 @@ static NSNotificationCenter *nc; [_aWindow orderOut: self]; [_aWindow setFrameOrigin: NSMakePoint (0, 0)]; - if (_superMenu) - _superMenu->_attachedMenu = nil; + if (_superMenu && ![self isTornOff]) + { + _superMenu->_attachedMenu = nil; + [[_superMenu menuRepresentation] setHighlightedItemIndex: -1]; + } } - (void) closeTransient { NSView *contentView; + + if (_transient == NO) + { + NSDebugLLog (@"NSMenu", @"Closing transient: %@ while it is NOT transient now", _title); + return; + } [_bWindow orderOut: self]; [_view removeFromSuperviewWithoutNeedingDisplay]; - [_titleView removeFromSuperviewWithoutNeedingDisplay]; contentView = [_aWindow contentView]; [contentView addSubview: _view]; - - if (_is_tornoff) - [_titleView addCloseButton]; - - [contentView addSubview: _titleView]; - [contentView setNeedsDisplay: YES]; + [contentView setNeedsDisplay: YES]; // Restore the old submenu (if any). if (_superMenu != nil) - _superMenu->_attachedMenu = _oldAttachedMenu; + { + _superMenu->_attachedMenu = _oldAttachedMenu; + [[_superMenu menuRepresentation] setHighlightedItemIndex: + [_superMenu indexOfItemWithSubmenu: _superMenu->_attachedMenu]]; + } - _follow_transient = NO; + [[self menuRepresentation] setHighlightedItemIndex: _oldHiglightedIndex]; + + _transient = NO; - _isPartlyOffScreen = IS_OFFSCREEN(_aWindow); + [_view update]; } - (NSWindow*) window { - if (_follow_transient) + if (_transient) return (NSWindow *)_bWindow; else return (NSWindow *)_aWindow; @@ -1290,7 +1383,7 @@ static NSNotificationCenter *nc; */ - (void) nestedSetFrameOrigin: (NSPoint) aPoint { - NSWindow *theWindow = _follow_transient ? _bWindow : _aWindow; + NSWindow *theWindow = [self window]; // Move ourself and get our width. [theWindow setFrameOrigin: aPoint]; @@ -1307,39 +1400,37 @@ static NSNotificationCenter *nc; - (void) shiftOnScreen { - NSWindow *theWindow = _follow_transient ? _bWindow : _aWindow; + NSWindow *theWindow = _transient ? _bWindow : _aWindow; NSRect frameRect = [theWindow frame]; + NSRect screenRect = [[NSScreen mainScreen] frame]; NSPoint vector = {0.0, 0.0}; - BOOL moveIt = YES; - - if (frameRect.origin.y < 0) + BOOL moveIt = NO; + + // 1 - determine the amount we need to shift in the y direction. + if (NSMinY (frameRect) < 0) { - if (frameRect.origin.y + SHIFT_DELTA <= 0) - vector.y = SHIFT_DELTA; - else - vector.y = -frameRect.origin.y; + vector.y = MIN (SHIFT_DELTA, -NSMinY (frameRect)); + moveIt = YES; } - else if (frameRect.origin.x < 0) + else if (NSMaxY (frameRect) > NSMaxY (screenRect)) { - if (frameRect.origin.x + SHIFT_DELTA <= 0) - vector.x = SHIFT_DELTA; - else - vector.x = -frameRect.origin.x; + vector.y = -MIN (SHIFT_DELTA, NSMaxY (frameRect) - NSMaxY (screenRect)); + moveIt = YES; } - else - { - vector.x = frameRect.origin.x + frameRect.size.width; - vector.x -= [[NSScreen mainScreen] frame].size.width; - if (vector.x > 0) - { - if (vector.x - SHIFT_DELTA <= 0) - vector.x = -SHIFT_DELTA; - else - vector.x = -vector.x - 2; - } - else - moveIt = NO; + // 2 - determine the amount we need to shift in the x direction. + if (NSMinX (frameRect) < 0) + { + vector.x = MIN (SHIFT_DELTA, -NSMinX (frameRect)); + moveIt = YES; + } + // Note the -3. This is done so the menu, after shifting completely + // has some spare room on the right hand side. This is needed otherwise + // the user can never access submenus of this menu. + else if (NSMaxX (frameRect) > NSMaxX (screenRect) - 3) + { + vector.x = -MIN (SHIFT_DELTA, NSMaxX (frameRect) - NSMaxX (screenRect) + 3); + moveIt = YES; } if (moveIt) @@ -1353,7 +1444,7 @@ static NSNotificationCenter *nc; for (candidateMenu = masterMenu = self; (candidateMenu = masterMenu->_superMenu) && (!masterMenu->_is_tornoff - || masterMenu->_follow_transient); + || masterMenu->_transient); masterMenu = candidateMenu); masterLocation = [[masterMenu window] frame].origin; @@ -1362,8 +1453,6 @@ static NSNotificationCenter *nc; [masterMenu nestedSetFrameOrigin: destinationPoint]; } - else - _isPartlyOffScreen = NO; } - (BOOL)_ownedByPopUp @@ -1378,191 +1467,18 @@ static NSNotificationCenter *nc; _popUpButtonCell = popUp; if (popUp != nil) { - [_titleView removeFromSuperviewWithoutNeedingDisplay]; [_aWindow setLevel: NSPopUpMenuWindowLevel]; [_bWindow setLevel: NSPopUpMenuWindowLevel]; } - - { - NSArray *itemCells = [_view _itemCells]; - int i; - int count = [itemCells count]; - - for ( i = 0; i < count; i++ ) - { - [[itemCells objectAtIndex: i] setMenuView: _view]; - } - } } + [self update]; +} + +- (NSString*) description +{ + return [NSString stringWithFormat: @"NSMenu: %@ (%@)", + _title, _transient ? @"Transient": @"Normal"]; } @end - -@implementation NSMenuWindowTitleView - -- (BOOL) acceptsFirstMouse: (NSEvent *)theEvent -{ - return YES; -} - -- (void) setMenu: (NSMenu*)aMenu -{ - menu = aMenu; -} - -- (NSMenu*) menu -{ - return menu; -} - -- (void) drawRect: (NSRect)rect -{ - NSRect workRect = [self bounds]; - NSRectEdge sides[] = {NSMinXEdge, NSMaxYEdge}; - float grays[] = {NSDarkGray, NSDarkGray}; - /* Cache the title attributes */ - static NSDictionary *attr = nil; - - // Draw the dark gray upper left lines. - workRect = NSDrawTiledRects(workRect, workRect, sides, grays, 2); - - // Draw the title box's button. - NSDrawButton(workRect, workRect); - - // Paint it Black! - workRect.origin.x += 1; - workRect.origin.y += 2; - workRect.size.height -= 3; - workRect.size.width -= 3; - [[NSColor windowFrameColor] set]; - NSRectFill(workRect); - - // Draw the title - if (attr == nil) - { - attr = [[NSDictionary alloc] - initWithObjectsAndKeys: - [NSFont boldSystemFontOfSize: 0], NSFontAttributeName, - [NSColor windowFrameTextColor], NSForegroundColorAttributeName, - nil]; - } - - // This gives the correct position - workRect.origin.x += 5; - workRect.size.width -= 5; - workRect.size.height -= 2; - [[menu title] drawInRect: workRect withAttributes: attr]; -} - -- (void) mouseDown: (NSEvent*)theEvent -{ - NSString *key; - NSPoint lastLocation; - NSPoint location; - unsigned eventMask = NSLeftMouseUpMask | NSLeftMouseDraggedMask; - BOOL done = NO; - NSDate *theDistantFuture = [NSDate distantFuture]; - - lastLocation = [theEvent locationInWindow]; - - if (![menu isTornOff] && [menu supermenu]) - { - [menu _setTornOff: YES]; - } - - while (!done) - { - theEvent = [NSApp nextEventMatchingMask: eventMask - untilDate: theDistantFuture - inMode: NSEventTrackingRunLoopMode - dequeue: YES]; - - switch ([theEvent type]) - { - case NSLeftMouseUp: - done = YES; - break; - case NSLeftMouseDragged: - location = [_window mouseLocationOutsideOfEventStream]; - if (NSEqualPoints(location, lastLocation) == NO) - { - NSPoint origin = [_window frame].origin; - - origin.x += (location.x - lastLocation.x); - origin.y += (location.y - lastLocation.y); - [menu nestedSetFrameOrigin: origin]; - [menu nestedCheckOffScreen]; - } - break; - - default: - break; - } - } - - /* - * Same current menu frame in defaults database. - */ - key = [menu _locationKey]; - if (key != nil) - { - NSUserDefaults *defaults; - NSMutableDictionary *menuLocations; - NSString *locString; - - defaults = [NSUserDefaults standardUserDefaults]; - menuLocations = [[defaults objectForKey: NSMenuLocationsKey] mutableCopy]; - if (menuLocations == nil) - { - menuLocations = AUTORELEASE([[NSMutableDictionary alloc] initWithCapacity: 2]); - } - locString = [[menu window] stringWithSavedFrame]; - [menuLocations setObject: locString forKey: key]; - [defaults setObject: menuLocations forKey: NSMenuLocationsKey]; - [defaults synchronize]; - } -} - -- (void) createButton -{ - // create the menu's close button - NSImage* closeImage = [NSImage imageNamed: @"common_Close"]; - NSImage* closeHImage = [NSImage imageNamed: @"common_CloseH"]; - NSSize imageSize = [closeImage size]; - NSRect rect = { { _frame.size.width - imageSize.width - 4, - (_frame.size.height - imageSize.height) / 2}, - { imageSize.height, imageSize.width } }; - - button = [[NSButton alloc] initWithFrame: rect]; - [button setButtonType: NSMomentaryLight]; - [button setImagePosition: NSImageOnly]; - [button setImage: closeImage]; - [button setAlternateImage: closeHImage]; - [button setBordered: NO]; - [button setTarget: menu]; - [button setAction: @selector(_performMenuClose:)]; - [button setAutoresizingMask: NSViewMinXMargin]; - - [self setAutoresizingMask: NSViewMinXMargin | NSViewMinYMargin | NSViewMaxYMargin]; -} - -- (void) releaseCloseButton -{ - [button removeFromSuperview]; -} - -- (void) addCloseButton -{ - if (button == nil) - [self createButton]; - [self addSubview: button]; - [self setNeedsDisplay: YES]; -} - -- (void) rightMouseDown: (NSEvent*)theEvent -{ - // Dont show our menu -} - -@end /* NSMenuWindowTitleView */ diff --git a/Source/NSMenuView.m b/Source/NSMenuView.m index b14ebbe59..c25bd473b 100644 --- a/Source/NSMenuView.m +++ b/Source/NSMenuView.m @@ -25,8 +25,7 @@ License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -*/ - +*/ #include @@ -34,9 +33,40 @@ #include #include #include +#include +#include #include #include +#include + +#include + +/* + NSMenuView contains: + + a) Title, if needed, this is a subview + b) menu items + + +*/ +/* A menu's title is an instance of this class */ +@class NSButton; + +@interface NSMenuWindowTitleView : NSView +{ + NSMenu *menu; + NSButton *button; +} + +- (void) addCloseButton; +- (void) removeCloseButton; +- (void) createButton; +- (void) setMenu: (NSMenu*)menu; +- (NSMenu*) menu; + +@end + @implementation NSMenuView static NSRect @@ -80,6 +110,13 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) { return YES; } +// We do not want to popup menus in this menu. +- (id) menuForEvent: (NSEvent*) theEvent +{ + NSDebugLLog (@"NSMenu", @"Query for menu in view"); + return nil; +} + /* * Init methods. @@ -108,6 +145,11 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) // Create an array to store our menu item cells. _itemCells = [NSMutableArray new]; + // Create title view and add it. CHECKME, should we do this here? + _titleView = [[NSMenuWindowTitleView alloc] init]; + + [self addSubview: _titleView]; + return self; } @@ -171,6 +213,7 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) name: NSMenuDidRemoveItemNotification object: _menu]; + [_titleView setMenu: _menu]; // WO CHECKME does this needs reorganizing? // Force menu view's layout to be recalculated. [self setNeedsSizing: YES]; } @@ -259,12 +302,7 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) - (NSMenuView*) attachedMenuView { - NSMenu *attachedMenu; - - if ((attachedMenu = [_menu attachedMenu])) - return [attachedMenu menuRepresentation]; - else - return nil; + return [[_menu attachedMenu] menuRepresentation]; } - (NSMenu*) attachedMenu @@ -366,6 +404,7 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) /* * Working with Submenus. */ + - (void) detachSubmenu { NSMenu *attachedMenu = [_menu attachedMenu]; @@ -378,17 +417,24 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) [attachedMenuView detachSubmenu]; - [attachedMenuView setHighlightedItemIndex: -1]; - - if ([attachedMenu isFollowTransient]) + NSDebugLLog (@"NSMenu", @"detach submenu: %@ from: %@", + attachedMenu, _menu); + + if ([attachedMenu isTransient]) { [attachedMenu closeTransient]; - [attachedMenuView setHighlightedItemIndex: _oldHighlightedItemIndex]; } else - [attachedMenu close]; + { + [attachedMenu close]; + } } +/** + Attach submenu if the item at index is a submenu. + It will figure out if the new submenu should be transient + or not. +*/ - (void) attachSubmenuForItemAtIndex: (int)index { /* @@ -397,17 +443,26 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) * soon as we release the mouse the user will be able to leave submenus * open on the screen and interact with other menus at the same time. */ - NSMenu *attachableMenu = [[_items_link objectAtIndex: index] submenu]; + NSMenu *attachableMenu; - if ([attachableMenu isTornOff] || [_menu isFollowTransient]) + if (index < 0) { - _oldHighlightedItemIndex = [[attachableMenu menuRepresentation] - highlightedItemIndex]; + return; + } + + attachableMenu = [[_items_link objectAtIndex: index] submenu]; + + if ([attachableMenu isTornOff] || [_menu isTransient]) + { + NSDebugLLog (@"NSMenu", @"Will open transient: %@", attachableMenu); [attachableMenu displayTransient]; - [[attachableMenu menuRepresentation] setHighlightedItemIndex: -1]; + [[attachableMenu menuRepresentation] setHighlightedItemIndex: -1]; } else - [attachableMenu display]; + { + NSDebugLLog (@"NSMenu", @"Will open normal: %@", attachableMenu); + [attachableMenu display]; + } } /* @@ -417,6 +472,15 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) { [_menu update]; + if ([_menu isTornOff] && ![_menu isTransient]) + { + [_titleView addCloseButton]; + } + else + { + [_titleView removeCloseButton]; + } + if (_needsSizing) [self sizeToFit]; } @@ -527,8 +591,12 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) if (_horizontal == NO) { + float menuBarHeight = [[self class] menuBarHeight]; + [self setFrameSize: NSMakeSize(_cellSize.width + _leftBorderOffset, - (howMany * _cellSize.height))]; + (howMany * _cellSize.height) + menuBarHeight)]; + [_titleView setFrame: NSMakeRect (0, howMany * _cellSize.height, + NSWidth (_bounds), menuBarHeight)]; } else { @@ -617,7 +685,7 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) */ if (_horizontal == NO) { - theRect.origin.y = _bounds.size.height - (_cellSize.height * (index + 1)); + theRect.origin.y = _cellSize.height * ([_itemCells count] - index - 1); theRect.origin.x = _leftBorderOffset; } else @@ -635,6 +703,11 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) return theRect; } +/** + Returns the index of the item below point. + Returns -1 if mouse is not above + a menu item. +*/ - (int) indexOfItemAtPoint: (NSPoint)point { unsigned howMany = [_itemCells count]; @@ -774,7 +847,7 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) // TODO // Compute position for popups, if needed - if (selectedItemIndex > -1) + if (selectedItemIndex != -1) { if (_horizontal == NO) { @@ -852,7 +925,8 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) NSMenu *candidateMenu = _menu; NSMenuView *targetMenuView; int indexToHighlight = index; - + int oldHighlightedIndex; + for (;;) { NSMenu *superMenu = [candidateMenu supermenu]; @@ -872,9 +946,7 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) } } - if ([targetMenuView attachedMenu]) - [targetMenuView detachSubmenu]; - + oldHighlightedIndex = [targetMenuView highlightedItemIndex]; [targetMenuView setHighlightedItemIndex: indexToHighlight]; /* We need to let the run loop run a little so that the fact that @@ -887,29 +959,77 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) if (![_menu _ownedByPopUp]) { - [targetMenuView setHighlightedItemIndex: -1]; + [targetMenuView setHighlightedItemIndex: oldHighlightedIndex]; } } #define MOVE_THRESHOLD_DELTA 2.0 #define DELAY_MULTIPLIER 10 +/** + This method is responsible for tracking the mouse while this menu + is on the screen and the user is busy navigating the menu or one + of it submenus. Responsible does not mean that this method does it + all. For submenus for example it will call, indirectly, itself for + submenu under consideration. + + It will return YES if user released mouse, not above a submenu item. + NO in all other circumstances. + + Implementation detail: + + + It use periodic events to update the highlight state + and attach / detach submenus. + + The flag justAttachedNewSubmenu is set to YES when + a new submenu is attached. The effect is that the + highlightin / attaching / detaching is surpressed + for this menu. This is done so the user is given + a change to move the mouse pointer into the newly + attached submenu. Otherwise it would immediately + be removed as the mouse pointer move over another + item. + + The logic for resetting the flag is rather adhoc. + + the flag subMenusNeedRemoving means that we + will remove all the submenus after we are done. + + This flag is used to clean up the submenus + when the user has opened a submenu by clicking + and wants to close it again by clicking on the + hihglighted item. + + When the user released the mouse this method + will cleanup all the transient menus. + + Not only its own, but also its attached menu + and all its transient super menus. + + The clean up is done BEFORE the action is executed. + This is needed otherwise `hiding' the application + leaves a dangling menu. If this is not acceptable, + there should be another mechanism of handling + the hiding. BTW besides the `hiding' the application, + model panels are also a problem when the menu + is not cleared before executing the action. + + +*/ - (BOOL) trackWithEvent: (NSEvent*)event { unsigned eventMask = NSPeriodicMask; NSDate *theDistantFuture = [NSDate distantFuture]; - int index; - NSPoint location; NSPoint lastLocation = {0,0}; - NSMenu *alreadyAttachedMenu = NO; - BOOL mouseMoved = NO; - BOOL delayedSelect = NO; + BOOL justAttachedNewSubmenu = NO; + BOOL subMenusNeedRemoving = YES; int delayCount = 0; - float xDelta = MOVE_THRESHOLD_DELTA; + int indexOfActionToExecute = -1; NSEvent *original; - NSEventType type = [event type]; + NSEventType type; NSEventType end; - + /* * The original event is unused except to determine whether the method * was invoked in response to a right or left mouse down. @@ -917,122 +1037,144 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) * submenu. */ original = AUTORELEASE(RETAIN(event)); - if (type == NSRightMouseDown) + + type = [event type]; + + if (type == NSRightMouseDown || type == NSRightMouseDragged) { end = NSRightMouseUp; eventMask |= NSRightMouseUpMask | NSRightMouseDraggedMask; } - else if (type == NSOtherMouseDown) + else if (type == NSOtherMouseDown || type == NSOtherMouseDragged) { end = NSOtherMouseUp; eventMask |= NSOtherMouseUpMask | NSOtherMouseDraggedMask; } - else + else if (type == NSLeftMouseDown || type == NSLeftMouseDragged) { end = NSLeftMouseUp; eventMask |= NSLeftMouseUpMask | NSLeftMouseDraggedMask; } - + else + { + NSLog (@"Unexpected event: %d during event tracking in NSMenuView", type); + end = NSLeftMouseUp; + eventMask |= NSLeftMouseUpMask | NSLeftMouseDraggedMask; + } + do { - location = [_window mouseLocationOutsideOfEventStream]; - index = [self indexOfItemAtPoint: location]; + if (type == NSPeriodic || event == original) + { + NSPoint location; + int index; + + location = [_window mouseLocationOutsideOfEventStream]; + index = [self indexOfItemAtPoint: location]; - if (index != _highlightedItemIndex) - { - mouseMoved = YES; /* Ok - had an initial movement. */ - } - if (type == NSPeriodic) - { + // 1 - if menus is only partly visible and the mouse is at the + // edge of the screen we move the menu so it will be visible. + if ([_menu isPartlyOffScreen]) { NSPoint pointerLoc = [_window convertBaseToScreen: location]; - - // TODO: Why 1 in the Y axis? + // The +/-1 in the y - direction is because the flipping between X-coordinates + // and GNUstep coordinates let the GNUstep screen coordinates start with 1. if (pointerLoc.x == 0 || pointerLoc.y == 1 - || pointerLoc.x == [[_window screen] frame].size.width - 1) + || pointerLoc.x == [[_window screen] frame].size.width - 1 + || pointerLoc.y == [[_window screen] frame].size.height) [_menu shiftOnScreen]; } - if (delayedSelect && mouseMoved && [event type] == NSPeriodic) - { - float xDiff = location.x - lastLocation.x; - if (xDiff > xDelta) - { - delayCount++; - if (delayCount >= DELAY_MULTIPLIER) - delayedSelect = NO; - } - else - { - delayedSelect = NO; - } + // 2 - Check if we have to reset the justAttachedNewSubmenu flag to NO. + if (justAttachedNewSubmenu && index != -1 && index != _highlightedItemIndex) + { + if (location.x - lastLocation.x > MOVE_THRESHOLD_DELTA) + { + delayCount ++; + if (delayCount >= DELAY_MULTIPLIER) + { + justAttachedNewSubmenu = NO; + } + } + else + { + justAttachedNewSubmenu = NO; + } + } - lastLocation = location; - } - } - if (index == -1) - { - NSWindow *w; + // 3 - If we have moved outside this menu, take appropriate action + if (index == -1) + { + NSPoint locationInScreenCoordinates; + NSWindow *windowUnderMouse; + NSMenu *candidateMenu; + + subMenusNeedRemoving = NO; + + locationInScreenCoordinates = [_window convertBaseToScreen: location]; - location = [_window convertBaseToScreen: location]; + // 3a - Check if moved into one of the ancester menus. + // This is tricky, there are a few possibilities: + // We are a transient attached menu of a non-transient menu + // We are a non-transient attached menu + // We are a root: isTornOff of AppMenu + candidateMenu = [_menu supermenu]; + while (candidateMenu + && !NSMouseInRect (locationInScreenCoordinates, [[candidateMenu window] frame], NO) // not found yet + && (! ([candidateMenu isTornOff] && ![candidateMenu isTransient])) // no root of display tree + && [candidateMenu isAttached]) // has displayed parent + { + candidateMenu = [candidateMenu supermenu]; + } + + if (candidateMenu != nil && NSMouseInRect (locationInScreenCoordinates, + [[candidateMenu window] frame], NO)) + { + // The call to fetch attachedMenu is not needed. But putting + // it here avoids flicker when we go back to an ancester meu + // and the attached menu is alreay correct. + [[[candidateMenu attachedMenu] menuRepresentation] detachSubmenu]; + return [[candidateMenu menuRepresentation] + trackWithEvent: original]; + } - /* - * If the mouse is back in the supermenu, we return NO so that - * our caller knows the button was not released. - */ - w = [[_menu supermenu] window]; - if (w != nil && NSMouseInRect(location, [w frame], NO) == YES) - { - return NO; - } - /* - * if the mouse is in our attached menu - get that menu to track it. - */ - w = [[_menu attachedMenu] window]; - if (w != nil && NSMouseInRect(location, [w frame], NO) == YES) - { - if ([[self attachedMenuView] trackWithEvent: original]) - return YES; - } - else - { - if (index != _highlightedItemIndex) - [self setHighlightedItemIndex: index]; - } -#if 0 - if (([_menu supermenu] && ![_menu isTornOff]) - || [_menu isFollowTransient]) - return NO; -#endif - } - else - { - if (index != _highlightedItemIndex) - { - if (![_menu attachedMenu] || !delayedSelect) - { - [self setHighlightedItemIndex: index]; + // 3b - Check if we enter the attached submenu + windowUnderMouse = [[_menu attachedMenu] window]; + if (windowUnderMouse != nil && NSMouseInRect (locationInScreenCoordinates, + [windowUnderMouse frame], NO)) + { + BOOL wasTransient = [_menu isTransient]; + BOOL subMenuResult = [[self attachedMenuView] trackWithEvent: original]; + + if (subMenuResult && wasTransient == [_menu isTransient]) + { + [self detachSubmenu]; + } + return subMenuResult; + } + } + + // 4 - We changed the selected item and should update. + if (!justAttachedNewSubmenu && index != _highlightedItemIndex) + { + subMenusNeedRemoving = NO; + [self detachSubmenu]; + [self setHighlightedItemIndex: index]; - if ([_menu attachedMenu]) - [self detachSubmenu]; - - if ((alreadyAttachedMenu = - [[_items_link objectAtIndex: index] submenu])) - { - [self attachSubmenuForItemAtIndex: index]; - mouseMoved = NO; - delayedSelect = YES; - delayCount = 0; - } - else - { - delayedSelect = NO; - } - } - } + // WO: Question? Why the ivar _items_link + if (index >= 0 && [[_items_link objectAtIndex: index] submenu]) + { + [self attachSubmenuForItemAtIndex: index]; + justAttachedNewSubmenu = YES; + delayCount = 0; + } + } + + // Update last seen location for the justAttachedNewSubmenu logic. + lastLocation = location; } event = [NSApp nextEventMatchingMask: eventMask @@ -1043,71 +1185,147 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) } while (type != end); - // Perform actions as needed. - if (index != -1 && !alreadyAttachedMenu) - { - // Stop the periodic events before performing the action - [NSEvent stopPeriodicEvents]; - [_menu performActionForItemAtIndex: index]; - if (![_menu isFollowTransient] && ![_menu _ownedByPopUp]) - [self setHighlightedItemIndex: -1]; + // Ok, we released the mouse + // There are now a few possibilities: + // A - We released the mouse outside the menu. + // Then we want the situation as it was before + // we entered everything. + // B - We released the mouse on a submenu item + // (i) - this was highlighted before we started clicking: + // Remove attached menus + // (ii) - this was not highlighted before pressed the mouse button; + // Keep attached menus. + // C - We released the mouse above an ordinary action: + // Execute the action. + // + // In case A, B and C we want the transient menus to be removed + // In case A and C we want to remove the menus that were created + // during the dragging. + + // So we should do the following things: + // + // 1 - Stop periodic events, + // 2 - Determine the action. + // 3 - Remove the Transient menus from the screen. + // 4 - Perform the action if there is one. + + [NSEvent stopPeriodicEvents]; + + // We need to store this, because _highlightedItemIndex + // will not be valid after we removed this menu from the screen. + indexOfActionToExecute = _highlightedItemIndex; + + // remove transient menus. -------------------------------------------- + { + NSMenu *currentMenu = _menu; + + while (currentMenu && ![currentMenu isTransient]) + { + currentMenu = [currentMenu attachedMenu]; + } + + while ([currentMenu isTransient] && + [currentMenu supermenu]) + { + currentMenu = [currentMenu supermenu]; + } + + [[currentMenu menuRepresentation] detachSubmenu]; + + if ([currentMenu isTransient]) + { + [currentMenu closeTransient]; + } + } + + // --------------------------------------------------------------------- + if (indexOfActionToExecute == -1) + { + return YES; + } + + if (indexOfActionToExecute >= 0 + && [_menu attachedMenu] != nil && [_menu attachedMenu] == + [[_items_link objectAtIndex: indexOfActionToExecute] submenu]) + { + if (subMenusNeedRemoving) + { + [self detachSubmenu]; + } + // Clicked on a submenu. + return NO; } - // Close menus if needed. - if (!_keepAttachedMenus || index == -1 - || (alreadyAttachedMenu && [alreadyAttachedMenu isFollowTransient])) + [_menu performActionForItemAtIndex: indexOfActionToExecute]; + + // Remove highlighting. + // We first check if it still highlighted because it could be the + // case that we choose an action in a transient window which + // has already dissappeared. + if (indexOfActionToExecute == _highlightedItemIndex) { - NSMenu *parentMenu; - NSMenu *masterMenu; - - for (parentMenu = masterMenu = _menu; - (parentMenu = [masterMenu supermenu]) - && (![masterMenu isTornOff] || [masterMenu isFollowTransient]); - masterMenu = parentMenu); - - if ([masterMenu attachedMenu]) - { - NSMenuView *masterMenuView = [masterMenu menuRepresentation]; - - [masterMenuView detachSubmenu]; - [masterMenuView setHighlightedItemIndex: -1]; - } + [self setHighlightedItemIndex: -1]; } - return YES; } +/** + This method is called when the user clicks on a button + in the menu. Or, if a right click happens and the + app menu is brought up. + + The original position is stored, so we can restore + the position of menu. The position of the menu + can change during the event tracking because + the menu will automatillay move when parts + are outside the screen and the user move the + mouse pointer to the edge of the screen. +*/ - (void) mouseDown: (NSEvent*)theEvent { NSRect currentFrame; NSRect originalFrame; NSPoint currentTopLeft; NSPoint originalTopLeft; + BOOL restorePosition; + /* + * Only for non transient menus do we want + * to remember the position. + */ + restorePosition = ![_menu isTransient]; - _keepAttachedMenus = YES; - originalFrame = [_window frame]; - originalTopLeft = originalFrame.origin; - originalTopLeft.y += originalFrame.size.height; - + if (restorePosition) + { // store old position; + originalFrame = [_window frame]; + originalTopLeft = originalFrame.origin; + originalTopLeft.y += originalFrame.size.height; + } + [NSEvent startPeriodicEventsAfterDelay: 0.1 withPeriod: 0.05]; [self trackWithEvent: theEvent]; [NSEvent stopPeriodicEvents]; - currentFrame = [_window frame]; - currentTopLeft = currentFrame.origin; - currentTopLeft.y += currentFrame.size.height; - - if (NSEqualPoints(currentTopLeft, originalTopLeft) == NO) + if (restorePosition) { - NSPoint origin = currentFrame.origin; + currentFrame = [_window frame]; + currentTopLeft = currentFrame.origin; + currentTopLeft.y += currentFrame.size.height; - origin.x += (originalTopLeft.x - currentTopLeft.x); - origin.y += (originalTopLeft.y - currentTopLeft.y); - [_menu nestedSetFrameOrigin: origin]; - [_menu nestedCheckOffScreen]; + if (NSEqualPoints(currentTopLeft, originalTopLeft) == NO) + { + NSPoint origin = currentFrame.origin; + + origin.x += (originalTopLeft.x - currentTopLeft.x); + origin.y += (originalTopLeft.y - currentTopLeft.y); + [_menu nestedSetFrameOrigin: origin]; + } } - _keepAttachedMenus = NO; +} + +- (void) rightMouseDown: (NSEvent*) theEvent +{ + [self mouseDown: theEvent]; } - (BOOL) performKeyEquivalent: (NSEvent *)theEvent @@ -1115,6 +1333,7 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) return [_menu performKeyEquivalent: theEvent]; } + /* * NSCoding Protocol * @@ -1155,8 +1374,165 @@ _addLeftBorderOffsetToRect(NSRect aRect, BOOL isHorizontal) @end @implementation NSMenuView (GNUstepPrivate) + - (NSArray *)_itemCells { return _itemCells; } + + @end + +@implementation NSMenuWindowTitleView + +- (BOOL) acceptsFirstMouse: (NSEvent *)theEvent +{ + return YES; +} + +- (void) setMenu: (NSMenu*)aMenu +{ + menu = aMenu; +} + +- (NSMenu*) menu +{ + return menu; +} + +- (void) drawRect: (NSRect)rect +{ + NSRect workRect = [self bounds]; + NSRectEdge sides[] = {NSMinXEdge, NSMaxYEdge}; float grays[] = {NSDarkGray, NSDarkGray}; + /* Cache the title attributes */ + static NSDictionary *attr = nil; + + // Draw the dark gray upper left lines. + workRect = NSDrawTiledRects(workRect, workRect, sides, grays, 2); + + // Draw the title box's button. + NSDrawButton(workRect, workRect); + + // Paint it Black! + workRect.origin.x += 1; + workRect.origin.y += 2; + workRect.size.height -= 3; + workRect.size.width -= 3; + [[NSColor windowFrameColor] set]; + NSRectFill(workRect); + + // Draw the title + if (attr == nil) + { + attr = [[NSDictionary alloc] + initWithObjectsAndKeys: + [NSFont boldSystemFontOfSize: 0], NSFontAttributeName, + [NSColor windowFrameTextColor], NSForegroundColorAttributeName, + nil]; + } + + // This gives the correct position + // WO: No it doesn't! + workRect.origin.x += 5; + workRect.size.width -= 5; + workRect.size.height -= 2; + [[menu title] drawInRect: workRect withAttributes: attr]; +} + +- (void) mouseDown: (NSEvent*)theEvent +{ + NSPoint lastLocation; + NSPoint location; + unsigned eventMask = NSLeftMouseUpMask | NSLeftMouseDraggedMask; + BOOL done = NO; + NSDate *theDistantFuture = [NSDate distantFuture]; + + NSDebugLLog (@"NSMenu", @"Mouse down in title!"); + + lastLocation = [theEvent locationInWindow]; + + if (![menu isTornOff] && [menu supermenu]) + { + [menu setTornOff: YES]; + } + + while (!done) + { + theEvent = [NSApp nextEventMatchingMask: eventMask + untilDate: theDistantFuture + inMode: NSEventTrackingRunLoopMode + dequeue: YES]; + + switch ([theEvent type]) + { + case NSRightMouseUp: + case NSLeftMouseUp: + done = YES; + break; + case NSRightMouseDragged: + case NSLeftMouseDragged: + location = [_window mouseLocationOutsideOfEventStream]; + if (NSEqualPoints(location, lastLocation) == NO) + { + NSPoint origin = [_window frame].origin; + + origin.x += (location.x - lastLocation.x); + origin.y += (location.y - lastLocation.y); + [menu nestedSetFrameOrigin: origin]; + } + break; + + default: + break; + } + } +} + +- (void) createButton +{ + // create the menu's close button + NSImage* closeImage = [NSImage imageNamed: @"common_Close"]; + NSImage* closeHImage = [NSImage imageNamed: @"common_CloseH"]; + NSSize imageSize = [closeImage size]; + NSRect rect = { { _frame.size.width - imageSize.width - 4, + (_frame.size.height - imageSize.height) / 2}, + { imageSize.height, imageSize.width } }; + + button = [[NSButton alloc] initWithFrame: rect]; + [button setButtonType: NSMomentaryLight]; + [button setImagePosition: NSImageOnly]; + [button setImage: closeImage]; + [button setAlternateImage: closeHImage]; + [button setBordered: NO]; + [button setTarget: menu]; + [button setAction: @selector(_performMenuClose:)]; + [button setAutoresizingMask: NSViewMinXMargin]; + + [self setAutoresizingMask: NSViewMinXMargin | NSViewMinYMargin | NSViewMaxYMargin]; +} + +- (void) removeCloseButton +{ + [button removeFromSuperview]; +} + +- (void) addCloseButton +{ + if (button == nil) + [self createButton]; + [self addSubview: button]; + [self setNeedsDisplay: YES]; +} + + +- (void) rightMouseDown: (NSEvent*)theEvent +{ +} + +// We do not want to popup menus in this menu. +- (id) menuForEvent: (NSEvent*) theEvent +{ + return nil; +} + +@end /* NSMenuWindowTitleView */