/* * CPMenu.j * AppKit * * Created by Francisco Tolmasky. * Copyright 2008, 280 North, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @import @import @import @import @import "CPApplication.j" @import "CPClipView.j" @import "CPMenuItem.j" @import "CPPanel.j" #include "CoreGraphics/CGGeometry.h" #include "Platform/Platform.h" CPMenuDidAddItemNotification = @"CPMenuDidAddItemNotification"; CPMenuDidChangeItemNotification = @"CPMenuDidChangeItemNotification"; CPMenuDidRemoveItemNotification = @"CPMenuDidRemoveItemNotification"; CPMenuDidEndTrackingNotification = @"CPMenuDidEndTrackingNotification"; var MENUBAR_HEIGHT = 19.0; var _CPMenuBarVisible = NO, _CPMenuBarTitle = @"", _CPMenuBarIconImage = nil, _CPMenuBarIconImageAlphaValue = 1.0, _CPMenuBarAttributes = nil, _CPMenuBarSharedWindow = nil; /*! @ingroup appkit @class CPMenu Menus provide the user with a list of actions and/or submenus. Submenus themselves are full fledged menus and so a heirarchical structure appears. */ @implementation CPMenu : CPObject { CPMenu _supermenu; CPString _title; CPString _name; CPMutableArray _items; CPMenu _attachedMenu; BOOL _autoenablesItems; BOOL _showsStateColumn; id _delegate; CPMenuItem _highlightedIndex; _CPMenuWindow _menuWindow; } // Managing the Menu Bar + (void)initialize { [[self class] setMenuBarAttributes:[CPDictionary dictionary]]; } + (BOOL)menuBarVisible { return _CPMenuBarVisible; } + (void)setMenuBarVisible:(BOOL)menuBarShouldBeVisible { if (_CPMenuBarVisible == menuBarShouldBeVisible) return; _CPMenuBarVisible = menuBarShouldBeVisible; if ([CPPlatform supportsNativeMainMenu]) return; if (menuBarShouldBeVisible) { if (!_CPMenuBarSharedWindow) _CPMenuBarSharedWindow = [[_CPMenuBarWindow alloc] init]; [_CPMenuBarSharedWindow setMenu:[CPApp mainMenu]]; [_CPMenuBarSharedWindow setTitle:_CPMenuBarTitle]; [_CPMenuBarSharedWindow setIconImage:_CPMenuBarIconImage]; [_CPMenuBarSharedWindow setIconImageAlphaValue:_CPMenuBarIconImageAlphaValue]; [_CPMenuBarSharedWindow setColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarBackgroundColor"]]; [_CPMenuBarSharedWindow setTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextColor"]]; [_CPMenuBarSharedWindow setTitleColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleColor"]]; [_CPMenuBarSharedWindow setTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextShadowColor"]]; [_CPMenuBarSharedWindow setTitleShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleShadowColor"]]; [_CPMenuBarSharedWindow setHighlightColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightColor"]]; [_CPMenuBarSharedWindow setHighlightTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextColor"]]; [_CPMenuBarSharedWindow setHighlightTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextShadowColor"]]; [_CPMenuBarSharedWindow orderFront:self]; } else [_CPMenuBarSharedWindow orderOut:self]; // FIXME: There must be a better way to do this. #if PLATFORM(DOM) [[CPPlatformWindow primaryPlatformWindow] resizeEvent:nil]; #endif } + (void)setMenuBarTitle:(CPString)aTitle { _CPMenuBarTitle = aTitle; [_CPMenuBarSharedWindow setTitle:_CPMenuBarTitle]; } + (CPString)menuBarTitle { return _CPMenuBarTitle; } + (void)setMenuBarIconImage:(CPImage)anImage { _CPMenuBarImage = anImage; [_CPMenuBarSharedWindow setIconImage:anImage]; } + (CPImage)menuBarIconImage { return _CPMenuBarImage; } + (void)setMenuBarAttributes:(CPDictionary)attributes { if (_CPMenuBarAttributes == attributes) return; _CPMenuBarAttributes = [attributes copy]; var textColor = [attributes objectForKey:@"CPMenuBarTextColor"], titleColor = [attributes objectForKey:@"CPMenuBarTitleColor"], textShadowColor = [attributes objectForKey:@"CPMenuBarTextShadowColor"], titleShadowColor = [attributes objectForKey:@"CPMenuBarTitleShadowColor"], highlightColor = [attributes objectForKey:@"CPMenuBarHighlightColor"], highlightTextColor = [attributes objectForKey:@"CPMenuBarHighlightTextColor"], highlightTextShadowColor = [attributes objectForKey:@"CPMenuBarHighlightTextShadowColor"]; if (!textColor && titleColor) [_CPMenuBarAttributes setObject:titleColor forKey:@"CPMenuBarTextColor"]; else if (textColor && !titleColor) [_CPMenuBarAttributes setObject:textColor forKey:@"CPMenuBarTitleColor"]; else if (!textColor && !titleColor) { [_CPMenuBarAttributes setObject:[CPColor colorWithRed:0.051 green:0.2 blue:0.275 alpha:1.0] forKey:@"CPMenuBarTextColor"]; [_CPMenuBarAttributes setObject:[CPColor colorWithRed:0.051 green:0.2 blue:0.275 alpha:1.0] forKey:@"CPMenuBarTitleColor"]; } if (!textShadowColor && titleShadowColor) [_CPMenuBarAttributes setObject:titleShadowColor forKey:@"CPMenuBarTextShadowColor"]; else if (textShadowColor && !titleShadowColor) [_CPMenuBarAttributes setObject:textShadowColor forKey:@"CPMenuBarTitleShadowColor"]; else if (!textShadowColor && !titleShadowColor) { [_CPMenuBarAttributes setObject:[CPColor whiteColor] forKey:@"CPMenuBarTextShadowColor"]; [_CPMenuBarAttributes setObject:[CPColor whiteColor] forKey:@"CPMenuBarTitleShadowColor"]; } if (!highlightColor) [_CPMenuBarAttributes setObject:[CPColor colorWithCalibratedRed:94.0/255.0 green:130.0/255.0 blue:186.0/255.0 alpha:1.0] forKey:@"CPMenuBarHighlightColor"]; if (!highlightTextColor) [_CPMenuBarAttributes setObject:[CPColor whiteColor] forKey:@"CPMenuBarHighlightTextColor"]; if (!highlightTextShadowColor) [_CPMenuBarAttributes setObject:[CPColor blackColor] forKey:@"CPMenuBarHighlightTextShadowColor"]; if (_CPMenuBarSharedWindow) { [_CPMenuBarSharedWindow setColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarBackgroundColor"]]; [_CPMenuBarSharedWindow setTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextColor"]]; [_CPMenuBarSharedWindow setTitleColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleColor"]]; [_CPMenuBarSharedWindow setTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTextShadowColor"]]; [_CPMenuBarSharedWindow setTitleShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarTitleShadowColor"]]; [_CPMenuBarSharedWindow setHighlightColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightColor"]]; [_CPMenuBarSharedWindow setHighlightTextColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextColor"]]; [_CPMenuBarSharedWindow setHighlightTextShadowColor:[_CPMenuBarAttributes objectForKey:@"CPMenuBarHighlightTextShadowColor"]]; } } + (CPDictionary)menuBarAttributes { return _CPMenuBarAttributes; } + (void)_setMenuBarIconImageAlphaValue:(float)anAlphaValue { _CPMenuBarIconImageAlphaValue = anAlphaValue; [_CPMenuBarSharedWindow setIconImageAlphaValue:anAlphaValue]; } - (float)menuBarHeight { if (self == [CPApp mainMenu]) return MENUBAR_HEIGHT; return 0.0; } + (float)menuBarHeight { return MENUBAR_HEIGHT; } // Creating a CPMenu Object /*! Initializes the menu with a specified title. @param aTile the menu title @return the initialized menu */ - (id)initWithTitle:(CPString)aTitle { self = [super init]; if (self) { _title = aTitle; _items = []; _autoenablesItems = YES; _showsStateColumn = YES; } return self; } - (id)init { return [self initWithTitle:@""]; } // Setting Up Menu Commands /*! Inserts a menu item at the specified index. @param aMenuItem the item to insert @param anIndex the index in the menu to insert the item. */ - (void)insertItem:(CPMenuItem)aMenuItem atIndex:(unsigned)anIndex { var menu = [aMenuItem menu]; if (menu) if (menu !== self) [CPException raise:CPInternalInconsistencyException reason:@"Attempted to insert item into menu that was already in another menu."]; else return; [aMenuItem setMenu:self]; [_items insertObject:aMenuItem atIndex:anIndex]; [[CPNotificationCenter defaultCenter] postNotificationName:CPMenuDidAddItemNotification object:self userInfo:[CPDictionary dictionaryWithObject:anIndex forKey:@"CPMenuItemIndex"]]; } /*! Creates and inserts a new menu item with the specified attributes. @param aTitle the title of the menu item @param anAction the action initiated when the user selects the item @param aKeyEquivalent the keyboard shortcut for the item @param anIndex the index location in the menu for the new item @return the new menu item */ - (CPMenuItem)insertItemWithTitle:(CPString)aTitle action:(SEL)anAction keyEquivalent:(CPString)aKeyEquivalent atIndex:(unsigned)anIndex { var item = [[CPMenuItem alloc] initWithTitle:aTitle action:anAction keyEquivalent:aKeyEquivalent]; [self insertItem:item atIndex:anIndex]; return item; } /*! Adds a menu item at the end of the menu. @param aMenuItem the menu item to add */ - (void)addItem:(CPMenuItem)aMenuItem { [self insertItem:aMenuItem atIndex:[_items count]]; } /*! Creates and adds a menu item with the specified attributes at the end of the menu. @param aTitle the title of the new menu item @param anAction the action initiated when the user selects the item @param aKeyEquivalent the keyboard shortcut for the menu item @return the new menu item */ - (CPMenuItem)addItemWithTitle:(CPString)aTitle action:(SEL)anAction keyEquivalent:(CPString)aKeyEquivalent { return [self insertItemWithTitle:aTitle action:anAction keyEquivalent:aKeyEquivalent atIndex:[_items count]]; } /*! Removes the specified item from the menu @param aMenuItem the item to remove */ - (void)removeItem:(CPMenuItem)aMenuItem { [self removeItemAtIndex:[_items indexOfObjectIdenticalTo:aMenuItem]]; } /*! Removes the item at the specified index from the menu @param anIndex the index of the item to remove */ - (void)removeItemAtIndex:(unsigned)anIndex { if (anIndex < 0 || anIndex >= _items.length) return; [_items[anIndex] setMenu:nil]; [_items removeObjectAtIndex:anIndex]; [[CPNotificationCenter defaultCenter] postNotificationName:CPMenuDidRemoveItemNotification object:self userInfo:[CPDictionary dictionaryWithObject:anIndex forKey:@"CPMenuItemIndex"]]; } /*! Called when a menu item has visually changed. @param aMenuItem the item that changed */ - (void)itemChanged:(CPMenuItem)aMenuItem { if ([aMenuItem menu] != self) return; [[CPNotificationCenter defaultCenter] postNotificationName:CPMenuDidChangeItemNotification object:self userInfo:[CPDictionary dictionaryWithObject:[_items indexOfObjectIdenticalTo:aMenuItem] forKey:@"CPMenuItemIndex"]]; } // Finding Menu Items /*! Returns the menu item with the specified tag @param the tag of the desired menu item @return the menu item or \c nil if a match was not found */ - (CPMenuItem)itemWithTag:(int)aTag { var index = [self indexOfItemWithTag:aTag]; if (index == CPNotFound) return nil; return _items[index]; } /*! Returns the menu item with the specified title. @param aTitle the title of the menu item @return the menu item or \c nil if a match was not found */ - (CPMenuItem)itemWithTitle:(CPString)aTitle { var index = [self indexOfItemWithTitle:aTitle]; if (index == CPNotFound) return nil; return _items[index]; } /*! Returns the menu item at the specified index @param anIndex the index of the requested item */ - (CPMenuItem)itemAtIndex:(int)anIndex { return [_items objectAtIndex:anIndex]; } /*! Returns the number of menu items in the menu */ - (unsigned)numberOfItems { return [_items count]; } /*! Returns the array of menu items backing this menu */ - (CPArray)itemArray { return _items; } // Finding Indices of Menu Items /*! Returns the index of the specified menu item @param aMenuItem the item to find the index for @return the item index or CPNotFound */ - (int)indexOfItem:(CPMenuItem)aMenuItem { if ([aMenuItem menu] != self) return CPNotFound; return [_items indexOfObjectIdenticalTo:aMenuItem]; } /*! Returns the index of the item with the specified title. @param aTitle the desired title to match @return the index of the item or CPNotFound */ - (int)indexOfItemWithTitle:(CPString)aTitle { var index = 0, count = _items.length; for (; index < count; ++index) if ([_items[index] title] === aTitle) return index; return CPNotFound; } /*! Returns the index of the item with the specified tag @param aTag the desired tag to match @return the index of the item or CPNotFound */ - (int)indexOfItemWithTag:(int)aTag { var index = 0, count = _items.length; for (; index < count; ++index) if ([_items[index] tag] == aTag) return index; return CPNotFound; } /*! Returns the index of the item with the specified target and action. @param aTarget the target of the desired menu item @param anAction the action of the desired menu item @return the index of the item or CPNotFound */ - (int)indexOfItemWithTarget:(id)aTarget andAction:(SEL)anAction { var index = 0, count = _items.length; for (; index < count; ++index) { var item = _items[index]; if ([item target] == aTarget && (!anAction || [item action] == anAction)) return index; } return CPNotFound; } /*! Returns the index of the menu item with the specified represented object. @param anObject the represented object of the desired item @return the index of the item or CPNotFound */ - (int)indexOfItemWithRepresentedObject:(id)anObject { var index = 0, count = _items.length; for (; index < count; ++index) if ([[_items[index] representedObject] isEqual:anObject]) return index; return CPNotFound; } /*! Returns the index of the item with the specified submenu. @param the submenu of the desired menu item @return the index of the item or CPNotFound */ - (int)indexOfItemWithSubmenu:(CPMenu)aMenu { var index = 0, count = _items.length; for (; index < count; ++index) if ([_items[index] submenu] == aMenu) return index; return CPNotFound; } // Managing Submenus /*! Sets a submenu for a menu item @param aMenu the submenu @param aMenuItem the menu item to set the submenu on */ - (void)setSubmenu:(CPMenu)aMenu forItem:(CPMenuItem)aMenuItem { [aMenuItem setTarget:aMenuItem]; [aMenuItem setAction:@selector(submenuAction:)]; [aMenuItem setSubmenu:aMenu]; } /*! The action method of menu items that open submenus. The default implementation does nothing, but it may be subclassed to provide different behavior. @param aSender the object that sent the message */ - (void)submenuAction:(id)aSender { } /*! Returns the attaced menu, or \c nil if there isn't one. */ - (CPMenu)attachedMenu { return _attachedMenu; } /*! Returns \c YES if the menu is attached to another menu. */ - (BOOL)isAttached { return _isAttached; } /*! Not yet implemented */ - (CGPoint)locationOfSubmenu:(CPMenu)aMenu { // FIXME: IMPLEMENT. } /*! Returns the super menu or \c nil if there is none. */ - (CPMenu)supermenu { return _supermenu; } /*! Sets the super menu. @param aMenu the new super menu */ - (void)setSupermenu:(CPMenu)aMenu { _supermenu = aMenu; } /*! If there are two instances of this menu visible, return \c NO. Otherwise, return \c YES if we are a detached menu and visible. */ - (BOOL)isTornOff { return !_supermenu /* || offscreen(?) */ || self == [CPApp mainMenu]; } // Enabling and Disabling Menu Items /*! Sets whether the menu automatically enables menu items. @param aFlag \c YES sets the menu to automatically enable items. */ - (void)setAutoenablesItems:(BOOL)aFlag { _autoenablesItems = aFlag; } /*! Returns \c YES if the menu auto enables items. */ - (BOOL)autoenablesItems { return _autoenablesItems; } /*! Not implemented. */ - (void)update { } // Managing the Title /*! Sets the menu title. @param the new title */ - (void)setTitle:(CPString)aTitle { _title = aTitle; } /*! Returns the menu title */ - (CPString)title { return _title; } // + (void)popUpContextMenu:(CPMenu)aMenu withEvent:(CPEvent)anEvent forView:(CPView)aView { [self popUpContextMenu:aMenu withEvent:anEvent forView:aView withFont:nil]; } + (void)popUpContextMenu:(CPMenu)aMenu withEvent:(CPEvent)anEvent forView:(CPView)aView withFont:(CPFont)aFont { [self _popUpContextMenu:aMenu withEvent:anEvent forView:aView withFont:aFont forMenuBar:NO]; } + (void)_popUpContextMenu:(CPMenu)aMenu withEvent:(CPEvent)anEvent forView:(CPView)aView withFont:(CPFont)aFont forMenuBar:(BOOL)isForMenuBar { var delegate = [aMenu delegate]; if ([delegate respondsToSelector:@selector(menuWillOpen:)]) [delegate menuWillOpen:aMenu]; if (!aFont) aFont = [CPFont systemFontOfSize:12.0]; var theWindow = [aView window], menuWindow = [_CPMenuWindow menuWindowWithMenu:aMenu font:aFont]; [menuWindow setDelegate:self]; [menuWindow setBackgroundStyle:isForMenuBar ? _CPMenuWindowMenuBarBackgroundStyle : _CPMenuWindowPopUpBackgroundStyle]; [menuWindow setFrameOrigin:[[anEvent window] convertBaseToGlobal:[anEvent locationInWindow]]]; [menuWindow orderFront:self]; [menuWindow beginTrackingWithEvent:anEvent sessionDelegate:self didEndSelector:@selector(_menuWindowDidFinishTracking:highlightedItem:)]; } + (void)_menuWindowDidFinishTracking:(_CPMenuWindow)aMenuWindow highlightedItem:(CPMenuItem)aMenuItem { var menu = [aMenuWindow menu]; [_CPMenuWindow poolMenuWindow:aMenuWindow]; if([aMenuItem isEnabled]) [CPApp sendAction:[aMenuItem action] to:[aMenuItem target] from:aMenuItem]; } // Managing Display of State Column /*! Sets whether to show the state column @param shouldShowStateColumn \c YES shows the state column */ - (void)setShowsStateColumn:(BOOL)shouldShowStateColumn { _showsStateColumn = shouldShowStateColumn; } /*! Returns \c YES if the menu shows the state column */ - (BOOL)showsStateColumn { return _showsStateColumn; } // Handling Highlighting /*! Returns the currently highlighted menu item. @return the highlighted menu item or \c nil if no item is currently highlighted */ - (CPMenuItem)highlightedItem { return _highlightedIndex >= 0 ? _items[_highlightedIndex] : nil; } // Managing the Delegate - (void)setDelegate:(id)aDelegate { _delegate = aDelegate; } - (id)delegate { return _delegate; } // Handling Tracking /*! Cancels tracking. */ - (void)cancelTracking { [[CPRunLoop currentRunLoop] performSelector:@selector(_fireCancelTrackingEvent) target:self argument:nil order:0 modes:[CPDefaultRunLoopMode]]; } - (void)_fireCancelTrackingEvent { [CPApp sendEvent:[CPEvent otherEventWithType:CPAppKitDefined location:_CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:0 subtype:0 data1:0 data2:0]]; } /* @ignore */ - (void)_setMenuWindow:(_CPMenuWindow)aMenuWindow { _menuWindow = aMenuWindow; } /*! Initiates the action of the menu item that has a keyboard shortcut equivalent to \c anEvent @param anEvent the keyboard event @return \c YES if it was handled. */ - (BOOL)performKeyEquivalent:(CPEvent)anEvent { if (_autoenablesItems) [self update]; var index = 0, count = _items.length, characters = [anEvent charactersIgnoringModifiers], modifierFlags = [anEvent modifierFlags]; for(; index < count; ++index) { var item = _items[index], modifierMask = [item keyEquivalentModifierMask]; if ((modifierFlags & (CPShiftKeyMask | CPAlternateKeyMask | CPCommandKeyMask | CPControlKeyMask)) == modifierMask && [characters caseInsensitiveCompare:[item keyEquivalent]] == CPOrderedSame) { if ([item isEnabled]) [self performActionForItemAtIndex:index]; else { //beep? } return YES; } if ([[item submenu] performKeyEquivalent:anEvent]) return YES; } return NO; } // Simulating Mouse Clicks /*! Sends the action of the menu item at the specified index. @param anIndex the index of the item */ - (void)performActionForItemAtIndex:(unsigned)anIndex { var item = _items[anIndex]; [CPApp sendAction:[item action] to:[item target] from:item]; } // /* @ignore */ - (BOOL)_itemIsHighlighted:(CPMenuItem)aMenuItem { return _items[_highlightedIndex] == aMenuItem; } /* @ignore */ - (void)_highlightItemAtIndex:(int)anIndex { var previousHighlightedIndex = _highlightedIndex; _highlightedIndex = anIndex; if (previousHighlightedIndex != CPNotFound) [[_items[previousHighlightedIndex] _menuItemView] highlight:NO]; if (_highlightedIndex != CPNotFound) [[_items[_highlightedIndex] _menuItemView] highlight:YES]; } - (void)_setMenuName:(CPString)aName { if (_name === aName) return; _name = aName; if (_name === @"CPMainMenu") [CPApp setMainMenu:self]; } - (CPString)_menuName { return _name; } - (void)awakeFromCib { if (_name === @"_CPMainMenu") { [self _setMenuName:@"CPMainMenu"]; [CPMenu setMenuBarVisible:YES]; } } - (void)_menuWithName:(CPString)aName { if (aName === _name) return self; for (var i = 0, count = [_items count]; i < count; i++) { var menu = [[_items[i] submenu] _menuWithName:aName]; if (menu) return menu; } return nil; } @end var CPMenuTitleKey = @"CPMenuTitleKey", CPMenuNameKey = @"CPMenuNameKey", CPMenuItemsKey = @"CPMenuItemsKey", CPMenuShowsStateColumnKey = @"CPMenuShowsStateColumnKey"; @implementation CPMenu (CPCoding) /*! Initializes the menu with data from the specified coder. @param aCoder the coder from which to read the data @return the initialized menu */ - (id)initWithCoder:(CPCoder)aCoder { self = [super init]; if (self) { _title = [aCoder decodeObjectForKey:CPMenuTitleKey]; _items = [aCoder decodeObjectForKey:CPMenuItemsKey]; [self _setMenuName:[aCoder decodeObjectForKey:CPMenuNameKey]]; _showsStateColumn = ![aCoder containsValueForKey:CPMenuShowsStateColumnKey] || [aCoder decodeBoolForKey:CPMenuShowsStateColumnKey]; } return self; } /*! Encodes the data of the menu into a coder @param aCoder the coder to which the data will be written */ - (void)encodeWithCoder:(CPCoder)aCoder { [aCoder encodeObject:_title forKey:CPMenuTitleKey]; if (_name) [aCoder encodeObject:_name forKey:CPMenuNameKey]; [aCoder encodeObject:_items forKey:CPMenuItemsKey]; if (!_showsStateColumn) [aCoder encodeBool:_showsStateColumn forKey:CPMenuShowsStateColumnKey]; } @end var _CPMenuWindowPool = [], _CPMenuWindowPoolCapacity = 5, _CPMenuWindowBackgroundColors = [], _CPMenuWindowScrollingStateUp = -1, _CPMenuWindowScrollingStateDown = 1, _CPMenuWindowScrollingStateNone = 0; _CPMenuWindowMenuBarBackgroundStyle = 0; _CPMenuWindowPopUpBackgroundStyle = 1; _CPMenuWindowAttachedMenuBackgroundStyle = 2; var STICKY_TIME_INTERVAL = 500, TOP_MARGIN = 5.0, LEFT_MARGIN = 1.0, RIGHT_MARGIN = 1.0, BOTTOM_MARGIN = 5.0, SCROLL_INDICATOR_HEIGHT = 16.0; /* @ignore */ @implementation _CPMenuWindow : CPWindow { _CPMenuView _menuView; CPClipView _menuClipView; CPView _lastMouseOverMenuView; CPImageView _moreAboveView; CPImageView _moreBelowView; id _sessionDelegate; SEL _didEndSelector; CPTimeInterval _startTime; int _scrollingState; CGPoint _lastGlobalLocation; BOOL _isShowingTopScrollIndicator; BOOL _isShowingBottomScrollIndicator; BOOL _trackingCanceled; CGRect _unconstrainedFrame; } + (id)menuWindowWithMenu:(CPMenu)aMenu font:(CPFont)aFont { var menuWindow = nil; if (_CPMenuWindowPool.length) menuWindow = _CPMenuWindowPool.pop(); else menuWindow = [[_CPMenuWindow alloc] init]; [menuWindow setFont:aFont]; [menuWindow setMenu:aMenu]; return menuWindow; } + (void)poolMenuWindow:(_CPMenuWindow)aMenuWindow { if (!aMenuWindow || _CPMenuWindowPool.length >= _CPMenuWindowPoolCapacity) return; _CPMenuWindowPool.push(aMenuWindow); } + (void)initialize { if (self != [_CPMenuWindow class]) return; var bundle = [CPBundle bundleForClass:self]; _CPMenuWindowMoreAboveImage = [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindowMoreAbove.png"] size:CGSizeMake(38.0, 18.0)]; _CPMenuWindowMoreBelowImage = [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindowMoreBelow.png"] size:CGSizeMake(38.0, 18.0)]; } - (id)init { self = [super initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessWindowMask]; if (self) { [self setLevel:CPPopUpMenuWindowLevel]; [self setHasShadow:YES]; [self setShadowStyle:CPMenuWindowShadowStyle]; [self setAcceptsMouseMovedEvents:YES]; _unconstrainedFrame = CGRectMakeZero(); var contentView = [self contentView]; _menuView = [[_CPMenuView alloc] initWithFrame:CGRectMakeZero()]; _menuClipView = [[CPClipView alloc] initWithFrame:CGRectMake(LEFT_MARGIN, TOP_MARGIN, 0.0, 0.0)]; [_menuClipView setDocumentView:_menuView]; [contentView addSubview:_menuClipView]; _moreAboveView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()]; [_moreAboveView setImage:_CPMenuWindowMoreAboveImage]; [_moreAboveView setFrameSize:[_CPMenuWindowMoreAboveImage size]]; [contentView addSubview:_moreAboveView]; _moreBelowView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()]; [_moreBelowView setImage:_CPMenuWindowMoreBelowImage]; [_moreBelowView setFrameSize:[_CPMenuWindowMoreBelowImage size]]; [contentView addSubview:_moreBelowView]; [self setShadowStyle:CPWindowShadowStyleMenu]; } return self; } - (CGFloat)overlapOffsetWidth { return LEFT_MARGIN; } - (void)setFont:(CPFont)aFont { [_menuView setFont:aFont]; } + (CPColor)backgroundColorForBackgroundStyle:(_CPMenuWindowBackgroundStyle)aBackgroundStyle { var color = _CPMenuWindowBackgroundColors[aBackgroundStyle]; if (!color) { var bundle = [CPBundle bundleForClass:[self class]]; if (aBackgroundStyle == _CPMenuWindowPopUpBackgroundStyle) color = [CPColor colorWithPatternImage:[[CPNinePartImage alloc] initWithImageSlices: [ [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindowRounded0.png"] size:CGSizeMake(4.0, 4.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow1.png"] size:CGSizeMake(1.0, 4.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindowRounded2.png"] size:CGSizeMake(4.0, 4.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow3.png"] size:CGSizeMake(4.0, 1.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow4.png"] size:CGSizeMake(1.0, 1.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow5.png"] size:CGSizeMake(4.0, 1.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindowRounded6.png"] size:CGSizeMake(4.0, 4.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow7.png"] size:CGSizeMake(1.0, 4.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindowRounded8.png"] size:CGSizeMake(4.0, 4.0)] ]]]; else if (aBackgroundStyle == _CPMenuWindowMenuBarBackgroundStyle) color = [CPColor colorWithPatternImage:[[CPNinePartImage alloc] initWithImageSlices: [ [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow3.png"] size:CGSizeMake(4.0, 0.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow4.png"] size:CGSizeMake(1.0, 0.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow5.png"] size:CGSizeMake(4.0, 0.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow3.png"] size:CGSizeMake(4.0, 1.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow4.png"] size:CGSizeMake(1.0, 1.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow5.png"] size:CGSizeMake(4.0, 1.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindowRounded6.png"] size:CGSizeMake(4.0, 4.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindow7.png"] size:CGSizeMake(1.0, 4.0)], [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"_CPMenuWindow/_CPMenuWindowRounded8.png"] size:CGSizeMake(4.0, 4.0)] ]]]; _CPMenuWindowBackgroundColors[aBackgroundStyle] = color; } return color; } - (void)setBackgroundStyle:(_CPMenuWindowBackgroundStyle)aBackgroundStyle { [self setBackgroundColor:[[self class] backgroundColorForBackgroundStyle:aBackgroundStyle]]; } - (void)setMenu:(CPMenu)aMenu { [aMenu _setMenuWindow:self]; [_menuView setMenu:aMenu]; var menuViewSize = [_menuView frame].size; [self setFrameSize:CGSizeMake(LEFT_MARGIN + menuViewSize.width + RIGHT_MARGIN, TOP_MARGIN + menuViewSize.height + BOTTOM_MARGIN)]; [_menuView scrollPoint:CGPointMake(0.0, 0.0)]; [_menuClipView setFrame:CGRectMake(LEFT_MARGIN, TOP_MARGIN, menuViewSize.width, menuViewSize.height)]; } - (void)setMinWidth:(float)aWidth { var size = [self frame].size; [self setFrameSize:CGSizeMake(MAX(size.width, aWidth), size.height)]; } - (CGPoint)rectForItemAtIndex:(int)anIndex { return [_menuView convertRect:[_menuView rectForItemAtIndex:anIndex] toView:nil]; } - (void)orderFront:(id)aSender { [self constrainToScreen]; [super orderFront:aSender]; } - (void)constrainToScreen { // FIXME: There are integral window issues with platform windows. // FIXME: This gets called far too often. _unconstrainedFrame = CGRectMakeCopy([self frame]); var isBrowser = [CPPlatform isBrowser], visibleFrame = CGRectInset(isBrowser ? [[self platformWindow] contentBounds] : [[self screen] visibleFrame], 5.0, 5.0), constrainedFrame = CGRectIntersection(_unconstrainedFrame, visibleFrame); // We don't want to simply intersect the visible frame and the unconstrained frame. // We should be allowing as much of the width to fit as possible (pushing back and forward). constrainedFrame.origin.x = CGRectGetMinX(_unconstrainedFrame); constrainedFrame.size.width = CGRectGetWidth(_unconstrainedFrame); if (CGRectGetWidth(constrainedFrame) > CGRectGetWidth(visibleFrame)) constrainedFrame.size.width = CGRectGetWidth(visibleFrame); if (CGRectGetMaxX(constrainedFrame) > CGRectGetMaxX(visibleFrame)) constrainedFrame.origin.x -= CGRectGetMaxX(constrainedFrame) - CGRectGetMaxX(visibleFrame); if (CGRectGetMinX(constrainedFrame) < CGRectGetMinX(visibleFrame)) constrainedFrame.origin.x = CGRectGetMinX(visibleFrame); // This needs to happen before changing the frame. var menuViewOrigin = [self convertBaseToGlobal:CGPointMake(LEFT_MARGIN, TOP_MARGIN)]; [super setFrame:constrainedFrame]; var moreAbove = menuViewOrigin.y < CGRectGetMinY(constrainedFrame) + TOP_MARGIN, moreBelow = menuViewOrigin.y + CGRectGetHeight([_menuView frame]) > CGRectGetMaxY(constrainedFrame) - BOTTOM_MARGIN, topMargin = TOP_MARGIN, bottomMargin = BOTTOM_MARGIN, contentView = [self contentView], bounds = [contentView bounds]; if (moreAbove) { topMargin += SCROLL_INDICATOR_HEIGHT; var frame = [_moreAboveView frame]; [_moreAboveView setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(frame)) / 2.0, (TOP_MARGIN + SCROLL_INDICATOR_HEIGHT - CGRectGetHeight(frame)) / 2.0)]; } [_moreAboveView setHidden:!moreAbove]; if (moreBelow) { bottomMargin += SCROLL_INDICATOR_HEIGHT; [_moreBelowView setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth([_moreBelowView frame])) / 2.0, CGRectGetHeight(bounds) - SCROLL_INDICATOR_HEIGHT - BOTTOM_MARGIN)]; } [_moreBelowView setHidden:!moreBelow]; var clipFrame = CGRectMake(LEFT_MARGIN, topMargin, CGRectGetWidth(constrainedFrame) - LEFT_MARGIN - RIGHT_MARGIN, CGRectGetHeight(constrainedFrame) - topMargin - bottomMargin) [_menuClipView setFrame:clipFrame]; [_menuView setFrameSize:CGSizeMake(CGRectGetWidth(clipFrame), CGRectGetHeight([_menuView frame]))]; [_menuView scrollPoint:CGPointMake(0.0, [self convertBaseToGlobal:clipFrame.origin].y - menuViewOrigin.y)]; } - (void)beginTrackingWithEvent:(CPEvent)anEvent sessionDelegate:(id)aSessionDelegate didEndSelector:(SEL)aDidEndSelector { CPApp._activeMenu = [_menuView menu]; _startTime = [anEvent timestamp];//new Date(); _scrollingState = _CPMenuWindowScrollingStateNone; _trackingCanceled = NO; _sessionDelegate = aSessionDelegate; _didEndSelector = aDidEndSelector; [self trackEvent:anEvent]; } - (void)trackEvent:(CPEvent)anEvent { var type = [anEvent type], menu = [_menuView menu]; // Close Menu Event. if (type === CPAppKitDefined) { // Stop all periodic events at this point. [CPEvent stopPeriodicEvents]; var highlightedItem = [[_menuView menu] highlightedItem]; [menu _highlightItemAtIndex:CPNotFound]; [self orderOut:self]; var delegate = [menu delegate]; if ([delegate respondsToSelector:@selector(menuDidClose:)]) [delegate menuDidClose:menu]; if (_sessionDelegate && _didEndSelector) objj_msgSend(_sessionDelegate, _didEndSelector, self, highlightedItem); [[CPNotificationCenter defaultCenter] postNotificationName:CPMenuDidEndTrackingNotification object:menu]; // Clear these now so its faster next time around. [_menuView setMenu:nil]; CPApp._activeMenu = nil; return; } var theWindow = [anEvent window], globalLocation = theWindow ? [theWindow convertBaseToGlobal:[anEvent locationInWindow]] : [anEvent locationInWindow]; if (type === CPPeriodic) { var constrainedBounds = CGRectInset([CPPlatform isBrowser] ? [[self platformWindow] contentBounds] : [[self screen] visibleFrame], 5.0, 5.0); if (_scrollingState == _CPMenuWindowScrollingStateUp) { if (CGRectGetMinY(_unconstrainedFrame) < CGRectGetMinY(constrainedBounds)) _unconstrainedFrame.origin.y += 10; } else if (_scrollingState == _CPMenuWindowScrollingStateDown) if (CGRectGetMaxY(_unconstrainedFrame) > CGRectGetHeight(constrainedBounds)) _unconstrainedFrame.origin.y -= 10; [self setFrame:_unconstrainedFrame]; [self constrainToScreen]; globalLocation = _lastGlobalLocation; } _lastGlobalLocation = globalLocation; var menuLocation = [self convertGlobalToBase:globalLocation], activeItemIndex = [_menuView itemIndexAtPoint:[_menuView convertPoint:menuLocation fromView:nil]], mouseOverMenuView = [[menu itemAtIndex:activeItemIndex] view]; // If we're over a custom menu view... if (mouseOverMenuView) { if (!_lastMouseOverMenuView) [menu _highlightItemAtIndex:CPNotFound]; if (_lastMouseOverMenuView != mouseOverMenuView) { [mouseOverMenuView mouseExited:anEvent]; // FIXME: Possibly multiple of these? [_lastMouseOverMenuView mouseEntered:anEvent]; _lastMouseOverMenuView = mouseOverMenuView; } [self sendEvent:[CPEvent mouseEventWithType:type location:menuLocation modifierFlags:[anEvent modifierFlags] timestamp:[anEvent timestamp] windowNumber:[self windowNumber] context:nil eventNumber:0 clickCount:[anEvent clickCount] pressure:[anEvent pressure]]]; } else { if (_lastMouseOverMenuView) { [_lastMouseOverMenuView mouseExited:anEvent]; _lastMouseOverMenuView = nil; } [menu _highlightItemAtIndex:[_menuView itemIndexAtPoint:[_menuView convertPoint:[self convertGlobalToBase:globalLocation] fromView:nil]]]; if (type == CPMouseMoved || type == CPLeftMouseDragged || type == CPLeftMouseDown) { var frame = [self frame], oldScrollingState = _scrollingState; _scrollingState = _CPMenuWindowScrollingStateNone; // If we're at or above of the top scroll indicator... if (globalLocation.y < CGRectGetMinY(frame) + TOP_MARGIN + SCROLL_INDICATOR_HEIGHT) _scrollingState = _CPMenuWindowScrollingStateUp; // If we're at or below the bottom scroll indicator... else if (globalLocation.y > CGRectGetMaxY(frame) - BOTTOM_MARGIN - SCROLL_INDICATOR_HEIGHT) _scrollingState = _CPMenuWindowScrollingStateDown; if (_scrollingState != oldScrollingState) if (_scrollingState == _CPMenuWindowScrollingStateNone) [CPEvent stopPeriodicEvents]; else if (oldScrollingState == _CPMenuWindowScrollingStateNone) [CPEvent startPeriodicEventsAfterDelay:0.0 withPeriod:0.04]; } else if (type == CPLeftMouseUp && ([anEvent timestamp] - _startTime > STICKY_TIME_INTERVAL)) [menu cancelTracking]; } [CPApp setTarget:self selector:@selector(trackEvent:) forNextEventMatchingMask:CPPeriodicMask | CPMouseMovedMask | CPLeftMouseDraggedMask | CPLeftMouseUpMask | CPAppKitDefinedMask untilDate:nil inMode:nil dequeue:YES]; } @end /* @ignore */ @implementation _CPMenuView : CPView { CPArray _menuItemViews; CPArray _visibleMenuItemInfos; CPFont _font; } /*- (id)initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; if (self) [self setAutoresizingMask:CPViewWidthSizable]; return self; }*/ - (void)setFont:(CPFont)aFont { _font = aFont; } - (CGRect)rectForItemAtIndex:(int)anIndex { return [_menuItemViews[anIndex == CPNotFound ? 0 : anIndex] frame]; } - (int)itemIndexAtPoint:(CGPoint)aPoint { var x = aPoint.x, bounds = [self bounds]; if (x < CGRectGetMinX(bounds) || x > CGRectGetMaxX(bounds)) return CPNotFound; var y = aPoint.y, low = 0, high = _visibleMenuItemInfos.length - 1; while (low <= high) { var middle = FLOOR(low + (high - low) / 2), info = _visibleMenuItemInfos[middle] frame = [info.view frame]; if (y < CGRectGetMinY(frame)) high = middle - 1; else if (y > CGRectGetMaxY(frame)) low = middle + 1; else return info.index; } return CPNotFound; } - (void)setMenu:(CPMenu)aMenu { [super setMenu:aMenu]; [_menuItemViews makeObjectsPerformSelector:@selector(removeFromSuperview)]; _menuItemViews = []; _visibleMenuItemInfos = []; var menu = [self menu]; if (!menu) return; var items = [menu itemArray], index = 0, count = [items count], maxWidth = 0, y = 0, showsStateColumn = [menu showsStateColumn]; for (; index < count; ++index) { var item = items[index], view = [item _menuItemView]; _menuItemViews.push(view); if ([item isHidden]) continue; _visibleMenuItemInfos.push({ view:view, index:index }); [view setFont:_font]; [view setShowsStateColumn:showsStateColumn]; [view synchronizeWithMenuItem]; [view setFrameOrigin:CGPointMake(0.0, y)]; [self addSubview:view]; var size = [view minSize], width = size.width; if (maxWidth < width) maxWidth = width; y += size.height; } for (index = 0; index < count; ++index) { var view = _menuItemViews[index]; [view setFrameSize:CGSizeMake(maxWidth, CGRectGetHeight([view frame]))]; } [self setAutoresizesSubviews:NO]; [self setFrameSize:CGSizeMake(maxWidth, y)]; [self setAutoresizesSubviews:YES]; } @end var MENUBAR_HEIGHT = 29.0, MENUBAR_MARGIN = 10.0, MENUBAR_LEFT_MARGIN = 10.0, MENUBAR_RIGHT_MARGIN = 10.0; var _CPMenuBarWindowBackgroundColor = nil, _CPMenuBarWindowFont = nil; @implementation _CPMenuBarWindow : CPPanel { CPMenu _menu; CPView _highlightView; CPArray _menuItemViews; CPMenuItem _trackingMenuItem; CPImageView _iconImageView; CPTextField _titleField; CPColor _textColor; CPColor _titleColor; CPColor _textShadowColor; CPColor _titleShadowColor; CPColor _highlightColor; CPColor _highlightTextColor; CPColor _highlightTextShadowColor; } + (void)initialize { if (self != [_CPMenuBarWindow class]) return; var bundle = [CPBundle bundleForClass:self]; _CPMenuBarWindowFont = [CPFont systemFontOfSize:11.0]; } - (id)init { // This only shows up in browser land, so don't bother calculating metrics in desktop. var contentRect = [[CPPlatformWindow primaryPlatformWindow] contentBounds]; contentRect.size.height = MENUBAR_HEIGHT; self = [super initWithContentRect:contentRect styleMask:CPBorderlessWindowMask]; if (self) { // FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/39-dont-allow-windows-to-go-above-menubar [self setLevel:-1];//CPTornOffMenuWindowLevel]; [self setAutoresizingMask:CPWindowWidthSizable]; var contentView = [self contentView]; [contentView setAutoresizesSubviews:NO]; [self setBecomesKeyOnlyIfNeeded:YES]; // _iconImageView = [[CPImageView alloc] initWithFrame:CGRectMake(0.0, 0.0, 16.0, 16.0)]; [contentView addSubview:_iconImageView]; _titleField = [[CPTextField alloc] initWithFrame:CGRectMakeZero()]; [_titleField setFont:[CPFont boldSystemFontOfSize:12.0]]; [_titleField setAlignment:CPCenterTextAlignment]; [_titleField setTextShadowOffset:CGSizeMake(0, 1)]; [contentView addSubview:_titleField]; } return self; } - (void)setTitle:(CPString)aTitle { #if PLATFORM(DOM) var bundleName = [[CPBundle mainBundle] objectForInfoDictionaryKey:@"CPBundleName"]; if (![bundleName length]) document.title = aTitle; else if ([aTitle length]) document.title = aTitle + @" - " + bundleName; else document.title = bundleName; #endif [_titleField setStringValue:aTitle]; [_titleField sizeToFit]; [self tile]; } - (void)setIconImage:(CPImage)anImage { [_iconImageView setImage:anImage]; [_iconImageView setHidden:anImage == nil]; [self tile]; } - (void)setIconImageAlphaValue:(float)anAlphaValue { [_iconImageView setAlphaValue:anAlphaValue]; } - (void)setColor:(CPColor)aColor { if (!aColor) { if (!_CPMenuBarWindowBackgroundColor) _CPMenuBarWindowBackgroundColor = [CPColor colorWithPatternImage:[[CPImage alloc] initWithContentsOfFile:[[CPBundle bundleForClass:[_CPMenuBarWindow class]] pathForResource:@"_CPMenuBarWindow/_CPMenuBarWindowBackground.png"] size:CGSizeMake(1.0, 18.0)]]; [[self contentView] setBackgroundColor:_CPMenuBarWindowBackgroundColor]; } else [[self contentView] setBackgroundColor:aColor]; } - (void)setTextColor:(CPColor)aColor { if (_textColor == aColor) return; _textColor = aColor; [_menuItemViews makeObjectsPerformSelector:@selector(setTextColor:) withObject:_textColor]; } - (void)setTitleColor:(CPColor)aColor { if (_titleColor == aColor) return; _titleColor = aColor; [_titleField setTextColor:aColor ? aColor : [CPColor blackColor]]; } - (void)setTextShadowColor:(CPColor)aColor { if (_textShadowColor == aColor) return; _textShadowColor = aColor; [_menuItemViews makeObjectsPerformSelector:@selector(setTextShadowColor:) withObject:_textShadowColor]; } - (void)setTitleShadowColor:(CPColor)aColor { if (_titleShadowColor == aColor) return; _titleShadowColor = aColor; [_titleField setTextShadowColor:aColor ? aColor : [CPColor whiteColor]]; } - (void)setHighlightColor:(CPColor)aColor { if (_highlightColor == aColor) return; _highlightColor = aColor; } - (void)setHighlightTextColor:(CPColor)aColor { if (_highlightTextColor == aColor) return; _highlightTextColor = aColor; [_menuItemViews makeObjectsPerformSelector:@selector(setActivateColor:) withObject:_highlightTextColor]; } - (void)setHighlightTextShadowColor:(CPColor)aColor { if (_highlightTextShadowColor == aColor) return; _highlightTextShadowColor = aColor; [_menuItemViews makeObjectsPerformSelector:@selector(setActivateShadowColor:) withObject:_highlightTextShadowColor]; } - (void)setMenu:(CPMenu)aMenu { if (_menu == aMenu) return; var defaultCenter = [CPNotificationCenter defaultCenter]; if (_menu) { [defaultCenter removeObserver:self name:CPMenuDidAddItemNotification object:_menu]; [defaultCenter removeObserver:self name:CPMenuDidChangeItemNotification object:_menu]; [defaultCenter removeObserver:self name:CPMenuDidRemoveItemNotification object:_menu]; var items = [_menu itemArray], count = items.length; while (count--) [[items[count] _menuItemView] removeFromSuperview]; } _menu = aMenu; if (_menu) { [defaultCenter addObserver:self selector:@selector(menuDidAddItem:) name:CPMenuDidAddItemNotification object:_menu]; [defaultCenter addObserver:self selector:@selector(menuDidChangeItem:) name:CPMenuDidChangeItemNotification object:_menu]; [defaultCenter addObserver:self selector:@selector(menuDidRemoveItem:) name:CPMenuDidRemoveItemNotification object:_menu]; } _menuItemViews = []; var contentView = [self contentView], items = [_menu itemArray], count = items.length; for (index = 0; index < count; ++index) { var item = items[index], menuItemView = [item _menuItemView]; _menuItemViews.push(menuItemView); [menuItemView setShowsStateColumn:NO]; [menuItemView setBelongsToMenuBar:YES]; [menuItemView setFont:_CPMenuBarWindowFont]; [menuItemView setTextColor:_textColor]; [menuItemView setHidden:[item isHidden]]; [menuItemView synchronizeWithMenuItem]; [contentView addSubview:menuItemView]; } [self tile]; } - (void)menuDidChangeItem:(CPNotification)aNotification { var menuItem = [_menu itemAtIndex:[[aNotification userInfo] objectForKey:@"CPMenuItemIndex"]], menuItemView = [menuItem _menuItemView]; [menuItemView setHidden:[menuItem isHidden]]; [menuItemView synchronizeWithMenuItem]; [self tile]; } - (void)menuDidAddItem:(CPNotification)aNotification { var index = [[aNotification userInfo] objectForKey:@"CPMenuItemIndex"], menuItem = [_menu itemAtIndex:index], menuItemView = [menuItem _menuItemView]; [_menuItemViews insertObject:menuItemView atIndex:index]; [menuItemView setShowsStateColumn:NO]; [menuItemView setBelongsToMenuBar:YES]; [menuItemView setFont:_CPMenuBarWindowFont]; [menuItemView setTextColor:_textColor]; [menuItemView setHidden:[menuItem isHidden]]; [menuItemView synchronizeWithMenuItem]; [[self contentView] addSubview:menuItemView]; [self tile]; } - (void)menuDidRemoveItem:(CPNotification)aNotification { var index = [[aNotification userInfo] objectForKey:@"CPMenuItemIndex"], menuItemView = [_menuItemViews objectAtIndex:index]; [_menuItemViews removeObjectAtIndex:index]; [menuItemView removeFromSuperview]; [self tile]; } - (CGRect)frameForMenuItem:(CPMenuItem)aMenuItem { var frame = [[aMenuItem _menuItemView] frame]; frame.origin.x -= 5.0; frame.origin.y = 0; frame.size.width += 10.0; frame.size.height = MENUBAR_HEIGHT; return frame; } - (CPMenuItem)menuItemAtPoint:(CGPoint)aPoint { var items = [_menu itemArray], count = items.length; while (count--) { var item = items[count]; if ([item isHidden] || [item isSeparatorItem]) continue; if (CGRectContainsPoint([self frameForMenuItem:item], aPoint)) return item; } return nil; } - (void)mouseDown:(CPEvent)anEvent { _trackingMenuItem = [self menuItemAtPoint:[anEvent locationInWindow]]; if (![_trackingMenuItem isEnabled]) return; if ([[_trackingMenuItem _menuItemView] eventOnSubmenu:anEvent]) return [self showMenu:anEvent]; if ([_trackingMenuItem isEnabled]) [self trackEvent:anEvent]; } - (void)trackEvent:(CPEvent)anEvent { var type = [anEvent type]; if (type === CPPeriodic) return [self showMenu:anEvent]; var frame = [self frameForMenuItem:_trackingMenuItem], menuItemView = [_trackingMenuItem _menuItemView], onMenuItemView = CGRectContainsPoint(frame, [anEvent locationInWindow]); if (type == CPLeftMouseDown) { if ([_trackingMenuItem submenu] != nil) { var action = [_trackingMenuItem action]; // If the item has a submenu, but not direct action, a.k.a. a "pure" menu, simply show the menu. // FIXME: (?) should we use submenuAction: or not? if (!action || action === @selector(submenuAction:)) return [self showMenu:anEvent]; // If this is a hybrid button/menu, show it in a bit... [CPEvent startPeriodicEventsAfterDelay:0.0 withPeriod:0.5]; } [menuItemView highlight:onMenuItemView]; } else if (type == CPLeftMouseDragged) { if (!onMenuItemView && [_trackingMenuItem submenu]) return [self showMenu:anEvent]; [menuItemView highlight:onMenuItemView]; } else /*if (type == CPLeftMouseUp)*/ { [CPEvent stopPeriodicEvents]; [menuItemView highlight:NO]; if (onMenuItemView) [CPApp sendAction:[_trackingMenuItem action] to:[_trackingMenuItem target] from:_trackingMenuItem]; return; } [CPApp setTarget:self selector:@selector(trackEvent:) forNextEventMatchingMask:CPPeriodicMask | CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; } - (void)showMenu:(CPEvent)anEvent { [CPEvent stopPeriodicEvents]; var frame = [self frameForMenuItem:_trackingMenuItem], menuItemView = [_trackingMenuItem _menuItemView]; if (!_highlightView) { _highlightView = [[CPView alloc] initWithFrame:frame]; [_highlightView setBackgroundColor:_highlightColor ? _highlightColor : [CPColor colorWithRed:95.0/255.0 green:131.0/255.0 blue:185.0/255.0 alpha:1.0]]; } else [_highlightView setFrame:frame]; [[self contentView] addSubview:_highlightView positioned:CPWindowBelow relativeTo:menuItemView]; [menuItemView activate:YES]; var submenu = [_trackingMenuItem submenu]; [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(menuDidEndTracking:) name:CPMenuDidEndTrackingNotification object:submenu]; [CPMenu _popUpContextMenu:submenu withEvent:[CPEvent mouseEventWithType:CPLeftMouseDown location:CGPointMake(CGRectGetMinX(frame), CGRectGetMaxY(frame)) modifierFlags:[anEvent modifierFlags] timestamp:[anEvent timestamp] windowNumber:[self windowNumber] context:nil eventNumber:0 clickCount:[anEvent clickCount] pressure:[anEvent pressure]] forView:[self contentView] withFont:nil forMenuBar:YES]; } - (void)menuDidEndTracking:(CPNotification)aNotification { [_highlightView removeFromSuperview]; [[_trackingMenuItem _menuItemView] activate:NO]; [[CPNotificationCenter defaultCenter] removeObserver:self name:CPMenuDidEndTrackingNotification object:[aNotification object]]; } - (void)tile { var items = [_menu itemArray], index = 0, count = items.length, x = MENUBAR_LEFT_MARGIN, y = 0.0, isLeftAligned = YES; for (; index < count; ++index) { var item = items[index]; if ([item isSeparatorItem]) { x = CGRectGetWidth([self frame]) - MENUBAR_RIGHT_MARGIN; isLeftAligned = NO; continue; } if ([item isHidden]) continue; var menuItemView = [item _menuItemView], frame = [menuItemView frame]; if (isLeftAligned) { [menuItemView setFrameOrigin:CGPointMake(x, (MENUBAR_HEIGHT - 1.0 - CGRectGetHeight(frame)) / 2.0)]; x += CGRectGetWidth([menuItemView frame]) + MENUBAR_MARGIN; } else { [menuItemView setFrameOrigin:CGPointMake(x - CGRectGetWidth(frame), (MENUBAR_HEIGHT - 1.0 - CGRectGetHeight(frame)) / 2.0)]; x = CGRectGetMinX([menuItemView frame]) - MENUBAR_MARGIN; } } var bounds = [[self contentView] bounds], titleFrame = [_titleField frame]; if ([_iconImageView isHidden]) [_titleField setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(titleFrame)) / 2.0, (CGRectGetHeight(bounds) - CGRectGetHeight(titleFrame)) / 2.0)]; else { var iconFrame = [_iconImageView frame], iconWidth = CGRectGetWidth(iconFrame), totalWidth = iconWidth + CGRectGetWidth(titleFrame); [_iconImageView setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - totalWidth) / 2.0, (CGRectGetHeight(bounds) - CGRectGetHeight(iconFrame)) / 2.0)]; [_titleField setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - totalWidth) / 2.0 + iconWidth, (CGRectGetHeight(bounds) - CGRectGetHeight(titleFrame)) / 2.0)]; } } - (void)setFrame:(CGRect)aRect display:(BOOL)shouldDisplay animate:(BOOL)shouldAnimate { var size = [self frame].size; [super setFrame:aRect display:shouldDisplay animate:shouldAnimate]; if (!_CGSizeEqualToSize(size, aRect.size)) [self tile]; } @end