/** NSMenuItemCell Copyright (C) 1999 Free Software Foundation, Inc. Author: Michael Hanni Date: 1999 This file is part of the GNUstep GUI Library. 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 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; see the file COPYING.LIB. If not, see or write to the Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "config.h" #import #import #import //#import #import //#import //#import #import #import #import "AppKit/NSAttributedString.h" #import "AppKit/NSBezierPath.h" #import "AppKit/NSColor.h" #import "AppKit/NSEvent.h" #import "AppKit/NSFont.h" #import "AppKit/NSGraphics.h" #import "AppKit/NSImage.h" #import "AppKit/NSMenu.h" #import "AppKit/NSMenuItemCell.h" #import "AppKit/NSMenuView.h" #import "AppKit/NSParagraphStyle.h" #import "GNUstepGUI/GSTheme.h" #import "GSGuiPrivate.h" static NSString *controlKeyString = @"^"; static NSString *alternateKeyString = @"+"; static NSString *shiftKeyString = @"/"; static NSString *commandKeyString = @"#"; @implementation NSMenuItemCell + (void) initialize { if (self == [NSMenuItemCell class]) { NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; NSString *keyString; [self setVersion: 2]; keyString = [userDefaults objectForKey: @"GSControlKeyString"]; if (nil != keyString) { controlKeyString = [keyString retain]; } keyString = [userDefaults objectForKey: @"GSAlternateKeyString"]; if (nil != keyString) { alternateKeyString = [keyString retain]; } keyString = [userDefaults objectForKey: @"GSShiftKeyString"]; if (nil != keyString) { shiftKeyString = [keyString retain]; } keyString = [userDefaults objectForKey: @"GSCommandKeyString"]; if (nil != keyString) { commandKeyString = [keyString retain]; } } } + (NSFocusRingType) defaultFocusRingType { return NSFocusRingTypeNone; } - (id) init { self = [super init]; if (nil == self) return nil; [self setButtonType: NSMomentaryLightButton]; [self setAlignment: NSLeftTextAlignment]; [self setFont: [NSFont menuFontOfSize: 0]]; [self setNeedsSizing: YES]; return self; } - (void) dealloc { RELEASE(_menuItem); [super dealloc]; } - (void) setHighlighted:(BOOL)flag { _cell.is_highlighted = flag; [self setNeedsSizing: YES]; } - (BOOL) isHighlighted { // Same as in super class return _cell.is_highlighted; } - (NSColor *)textColor { if (_cell.is_highlighted && [self isEnabled]) { return [NSColor selectedMenuItemTextColor]; } return [super textColor]; } - (NSColor *) backgroundColor { unsigned mask; NSColor *color; GSThemeControlState state = GSThemeNormalState; if (_cell.is_highlighted) { mask = _highlightsByMask; if (_cell.state) mask &= ~_showAltStateMask; } else if (_cell.state) mask = _showAltStateMask; else mask = NSNoCellMask; // Determine the background color if (mask & (NSChangeGrayCellMask | NSChangeBackgroundCellMask)) { state = GSThemeHighlightedState; } if (mask & NSPushInCellMask) { state = GSThemeSelectedState; } color = [[GSTheme theme] colorNamed: @"NSMenuItem" state: state]; if (color == nil) { if ((state == GSThemeHighlightedState) || (state == GSThemeSelectedState)) { color = [NSColor selectedMenuItemColor]; } else { color = [NSColor controlBackgroundColor]; } } return color; } - (void) setMenuItem: (NSMenuItem *)item { ASSIGN(_menuItem, item); [self setEnabled: [_menuItem isEnabled]]; [self setNeedsSizing: YES]; } - (NSMenuItem *) menuItem { return _menuItem; } - (NSInteger) tag { return [[self menuItem] tag]; } - (void) setMenuView: (NSMenuView *)menuView { /* The menu view is retaining us, we should not retain it. */ _menuView = menuView; /* * Determine whether we have horizontal or vertical layout and adjust. */ if ([_menuView isHorizontal] == YES) { [self setAlignment: NSCenterTextAlignment]; [self setBordered: NO]; [self setImagePosition: NSImageLeft]; } else { [self setAlignment: NSLeftTextAlignment]; [self setBordered: YES]; } [self setNeedsSizing: YES]; } - (NSMenuView *) menuView { return _menuView; } - (NSString*) _keyEquivalentString { NSString *key = [_menuItem keyEquivalent]; unsigned int m = [_menuItem keyEquivalentModifierMask]; NSString *ucKey = [key uppercaseString]; unichar uchar; if ((key == nil) || [key isEqualToString: @""]) return key; uchar = [key characterAtIndex: 0]; if (uchar >= 0xF700) { // FIXME: At the moment we are not able to handle function keys // as key equivalent return nil; } if ([key isEqualToString: @"\\r"]) key = @"RET"; else if ([key isEqualToString: @"\\e"]) key = @"ESC"; else if ([key isEqualToString: @"\\d"]) key = @"DEL"; if (m != 0) { BOOL shift; // shift mask and not an upper case string? shift = (m & NSShiftKeyMask) & ![key isEqualToString: ucKey]; key = [NSString stringWithFormat:@"%@%@%@%@%@", (m & NSControlKeyMask) ? controlKeyString : @"", (m & NSAlternateKeyMask) ? alternateKeyString : @"", shift ? shiftKeyString : @"", (m & NSCommandKeyMask) ? commandKeyString : @"", key]; } return key; } - (void) calcSize { NSSize componentSize; NSImage *anImage = nil; float neededMenuItemHeight = 20; // Check if _mcell_belongs_to_popupbutton = NO while cell owned by // popup button. FIXME if (!_mcell_belongs_to_popupbutton && [[_menuView menu] _ownedByPopUp]) { _mcell_belongs_to_popupbutton = YES; [self setImagePosition: NSImageRight]; } // State Image if ([_menuItem changesState]) { // NSOnState if ([_menuItem onStateImage]) componentSize = [[_menuItem onStateImage] size]; else componentSize = NSMakeSize(0,0); _stateImageWidth = componentSize.width; if (componentSize.height > neededMenuItemHeight) neededMenuItemHeight = componentSize.height; // NSOffState if ([_menuItem offStateImage]) componentSize = [[_menuItem offStateImage] size]; else componentSize = NSMakeSize(0,0); if (componentSize.width > _stateImageWidth) _stateImageWidth = componentSize.width; if (componentSize.height > neededMenuItemHeight) neededMenuItemHeight = componentSize.height; // NSMixedState if ([_menuItem mixedStateImage]) componentSize = [[_menuItem mixedStateImage] size]; else componentSize = NSMakeSize(0,0); if (componentSize.width > _stateImageWidth) _stateImageWidth = componentSize.width; if (componentSize.height > neededMenuItemHeight) neededMenuItemHeight = componentSize.height; } else { _stateImageWidth = 0.0; } // Image if ((anImage = [_menuItem image]) && _cell.image_position == NSNoImage) [self setImagePosition: NSImageLeft]; if (anImage) { componentSize = [anImage size]; _imageWidth = componentSize.width; if (componentSize.height > neededMenuItemHeight) neededMenuItemHeight = componentSize.height; } else { _imageWidth = 0.0; } // Title and Key Equivalent componentSize = [self _sizeText: [_menuItem title]]; _titleWidth = componentSize.width; if (componentSize.height > neededMenuItemHeight) neededMenuItemHeight = componentSize.height; componentSize = [self _sizeText: [self _keyEquivalentString]]; _keyEquivalentWidth = componentSize.width; if (componentSize.height > neededMenuItemHeight) neededMenuItemHeight = componentSize.height; // Submenu Arrow if ([_menuItem hasSubmenu]) { NSImage *arrow = [NSImage imageNamed: @"NSMenuArrow"]; if (arrow != nil) { componentSize = [arrow size]; } else { componentSize = NSMakeSize(0, 0); } _keyEquivalentWidth = componentSize.width; if (componentSize.height > neededMenuItemHeight) neededMenuItemHeight = componentSize.height; } // Cache definitive height _menuItemHeight = neededMenuItemHeight; // At the end we set sizing to NO. _needs_sizing = NO; } - (void) setNeedsSizing:(BOOL)flag { _needs_sizing = flag; } - (BOOL) needsSizing { return _needs_sizing; } - (void) setNeedsDisplay:(BOOL)flag { _needs_display = flag; } - (BOOL) needsDisplay { return _needs_display; } - (float) imageWidth { if (_needs_sizing) [self calcSize]; return _imageWidth; } - (float) titleWidth { if (_needs_sizing) [self calcSize]; return _titleWidth; } - (float) keyEquivalentWidth { if (_needs_sizing) [self calcSize]; return _keyEquivalentWidth; } - (float) stateImageWidth { if (_needs_sizing) [self calcSize]; return _stateImageWidth; } // // Sizes for drawing taking into account NSMenuView adjustments. // - (NSRect) imageRectForBounds: (NSRect)cellFrame { if (_needs_sizing) [self calcSize]; if (_mcell_belongs_to_popupbutton && _cell.image_position) { // Special case: draw image on the extreme right cellFrame.origin.x += cellFrame.size.width - _imageWidth - 4; cellFrame.size.width = _imageWidth; return cellFrame; } if ([_menuView isHorizontal] == YES) { switch (_cell.image_position) { case NSNoImage: cellFrame = NSZeroRect; break; case NSImageOnly: case NSImageOverlaps: break; case NSImageLeft: cellFrame.origin.x += 4.; // _horizontalEdgePad cellFrame.size.width = _imageWidth; break; case NSImageRight: cellFrame.origin.x += _titleWidth; cellFrame.size.width = _imageWidth; break; case NSImageBelow: cellFrame.size.height /= 2; break; case NSImageAbove: cellFrame.size.height /= 2; cellFrame.origin.y += cellFrame.size.height; break; } } else { // Calculate the image part of cell frame from NSMenuView cellFrame.origin.x += [_menuView imageAndTitleOffset]; cellFrame.size.width = [_menuView imageAndTitleWidth]; switch (_cell.image_position) { case NSNoImage: cellFrame = NSZeroRect; break; case NSImageOnly: case NSImageOverlaps: break; case NSImageLeft: cellFrame.size.width = _imageWidth; break; case NSImageRight: cellFrame.origin.x += _titleWidth + GSCellTextImageXDist; cellFrame.size.width = _imageWidth; break; case NSImageBelow: cellFrame.size.height /= 2; break; case NSImageAbove: cellFrame.size.height /= 2; cellFrame.origin.y += cellFrame.size.height; break; } } return cellFrame; } - (NSRect) keyEquivalentRectForBounds:(NSRect)cellFrame { // Calculate the image part of cell frame from NSMenuView cellFrame.origin.x += [_menuView keyEquivalentOffset]; cellFrame.size.width = [_menuView keyEquivalentWidth]; return cellFrame; } - (NSRect) stateImageRectForBounds:(NSRect)cellFrame { // Calculate the image part of cell frame from NSMenuView cellFrame.origin.x += [_menuView stateImageOffset]; cellFrame.size.width = [_menuView stateImageWidth]; return cellFrame; } - (NSRect) titleRectForBounds:(NSRect)cellFrame { if (_needs_sizing) [self calcSize]; if ([_menuView isHorizontal] == YES) { /* This adjust will center us within the menubar. */ cellFrame.size.height -= 2; switch (_cell.image_position) { case NSNoImage: case NSImageOverlaps: break; case NSImageOnly: cellFrame = NSZeroRect; break; case NSImageLeft: cellFrame.origin.x += _imageWidth + GSCellTextImageXDist + 4; cellFrame.size.width = _titleWidth; break; case NSImageRight: cellFrame.size.width = _titleWidth; break; case NSImageBelow: cellFrame.size.height /= 2; cellFrame.origin.y += cellFrame.size.height; break; case NSImageAbove: cellFrame.size.height /= 2; break; } } else { // Calculate the image part of cell frame from NSMenuView cellFrame.origin.x += [_menuView imageAndTitleOffset]; cellFrame.size.width = [_menuView imageAndTitleWidth]; switch (_cell.image_position) { case NSNoImage: case NSImageOverlaps: break; case NSImageOnly: cellFrame = NSZeroRect; break; case NSImageLeft: cellFrame.origin.x += _imageWidth + GSCellTextImageXDist; cellFrame.size.width = _titleWidth; break; case NSImageRight: cellFrame.size.width = _titleWidth; break; case NSImageBelow: cellFrame.size.height /= 2; cellFrame.origin.y += cellFrame.size.height; break; case NSImageAbove: cellFrame.size.height /= 2; break; } } return cellFrame; } - (NSRect) drawingRectForBounds: (NSRect)theRect { if (_needs_sizing) [self calcSize]; if ([_menuView isHorizontal] == YES) { /* A horizontal menu does not have borders drawn by the cell, * but it does have a border round the menu as a whole, so we * must inset from that. */ return NSMakeRect (theRect.origin.x, theRect.origin.y + 2, theRect.size.width, theRect.size.height - 2); } else { if (_cell.is_bordered) { float yDelta = [_control_view isFlipped] ? 1. : 2.; unsigned mask; NSRect interiorFrame; if (_cell.is_highlighted) { mask = _highlightsByMask; if (_cell.state) mask &= ~_showAltStateMask; } else if (_cell.state) mask = _showAltStateMask; else mask = NSNoCellMask; /* * Special case: Buttons have only three different paths for border. * One white path at the top left corner, one black path at the * bottom right and another in dark gray at the inner bottom right. */ interiorFrame = NSMakeRect(theRect.origin.x + 1., theRect.origin.y + yDelta, theRect.size.width - 3., theRect.size.height - 3.); // pushed in buttons contents are displaced to the bottom right 1px if ((mask & NSPushInCellMask)) { interiorFrame = NSOffsetRect(interiorFrame, 1., [_control_view isFlipped] ? 1. : -1.); } return interiorFrame; } else { return theRect; } } } // // Drawing. // - (void) drawBorderAndBackgroundWithFrame: (NSRect)cellFrame inView: (NSView *)controlView { unsigned mask; GSThemeControlState state = GSThemeNormalState; // set the mask if (_cell.is_highlighted) { mask = _highlightsByMask; if (_cell.state) { mask &= ~_showAltStateMask; } } else if (_cell.state) mask = _showAltStateMask; else mask = NSNoCellMask; /* Determine the background color. We draw when there is a border or when highlightsByMask is NSChangeBackgroundCellMask or NSChangeGrayCellMask, as required by our nextstep-like look and feel. */ if (mask & (NSChangeGrayCellMask | NSChangeBackgroundCellMask)) { state = GSThemeHighlightedState; } /* Pushed in buttons contents are displaced to the bottom right 1px. */ if (mask & NSPushInCellMask) { state = GSThemeSelectedState; } [[GSTheme theme] drawBorderAndBackgroundForMenuItemCell: self withFrame: cellFrame inView: controlView state: state isHorizontal: [_menuView isHorizontal]]; } - (void) drawImageWithFrame: (NSRect)cellFrame inView: (NSView *)controlView { cellFrame = [self imageRectForBounds: cellFrame]; [self drawImage: _imageToDisplay withFrame: cellFrame inView: controlView]; } - (void) drawKeyEquivalentWithFrame: (NSRect)cellFrame inView: (NSView *)controlView { NSImage *arrow = [NSImage imageNamed: @"NSMenuArrow"]; cellFrame = [self keyEquivalentRectForBounds: cellFrame]; if ([_menuItem hasSubmenu] && arrow != nil) { NSSize size; NSPoint position; size = [arrow size]; position.x = cellFrame.origin.x + cellFrame.size.width - size.width; position.y = MAX(NSMidY(cellFrame) - (size.height/2.), 0.); /* * Images are always drawn with their bottom-left corner at the origin * so we must adjust the position to take account of a flipped view. */ if ([controlView isFlipped]) position.y += size.height; [arrow compositeToPoint: position operation: NSCompositeSourceOver]; } /* FIXME/TODO here - decide a consistent policy for images. * * The reason of the following code is that we draw the key * equivalent, but not if we are a popup button and are displaying * an image (the image is displayed in the title or selected entry * in the popup, it's the small square on the right). In that case, * the image will be drawn in the same position where the key * equivalent would be, so we do not display the key equivalent, * else they would be displayed one over the other one. */ else if (![[_menuView menu] _ownedByPopUp] || (_imageToDisplay == nil)) { [self _drawText: [self _keyEquivalentString] inFrame: cellFrame]; } } - (void) drawSeparatorItemWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { NSInterfaceStyle style = NSInterfaceStyleForKey(@"NSMenuInterfaceStyle", nil); if (style == NSMacintoshInterfaceStyle || style == NSWindows95InterfaceStyle) { NSBezierPath *path = [NSBezierPath bezierPath]; NSPoint start = NSMakePoint(3, cellFrame.size.height/2 + cellFrame.origin.y); NSPoint end = NSMakePoint(cellFrame.size.width - 3, cellFrame.size.height/2 + cellFrame.origin.y); [[NSColor blackColor] set]; [path moveToPoint: start]; [path lineToPoint: end]; [path stroke]; /* NSRect lineFrame = NSMakeRect(cellFrame.origin.x, cellFrame.origin.y + cellFrame.size.height/2, cellFrame.size.width, 1); NSBox *line = [[NSBox alloc] initWithFrame: lineFrame]; [controlView addSubview:line]; RELEASE(line); */ } } - (void) drawStateImageWithFrame: (NSRect)cellFrame inView: (NSView*)controlView { NSImage *imageToDisplay; switch ([_menuItem state]) { case NSOnState: imageToDisplay = [_menuItem onStateImage]; break; case NSMixedState: imageToDisplay = [_menuItem mixedStateImage]; break; case NSOffState: default: imageToDisplay = [_menuItem offStateImage]; break; } if (imageToDisplay == nil) { return; } cellFrame = [self stateImageRectForBounds: cellFrame]; [self drawImage: imageToDisplay withFrame: cellFrame inView: controlView]; } - (void) drawTitleWithFrame: (NSRect)cellFrame inView: (NSView *)controlView { [self _drawText: [_menuItem title] inFrame: [self titleRectForBounds: cellFrame]]; } - (void) _drawBorderAndBackgroundWithFrame: (NSRect)cellFrame inView: (NSView*)controlView { [self drawBorderAndBackgroundWithFrame: cellFrame inView: controlView]; } - (void) drawInteriorWithFrame: (NSRect)cellFrame inView: (NSView*)controlView { unsigned mask; // Transparent buttons never draw if (_buttoncell_is_transparent) return; if ([_menuItem isSeparatorItem]) { [self drawSeparatorItemWithFrame: cellFrame inView: controlView]; return; } cellFrame = [self drawingRectForBounds: cellFrame]; // FIXME: I think all this code belongs into calcSize. if (_cell.is_highlighted) { mask = _highlightsByMask; if (_cell.state) mask &= ~_showAltStateMask; } else if (_cell.state) mask = _showAltStateMask; else mask = NSNoCellMask; /* * Determine the image and the title that will be * displayed. If the NSContentsCellMask is set the * image and title are swapped only if state is 1 or * if highlighting is set (when a button is pushed it's * content is changed to the face of reversed state). * The results are saved in two ivars for use in other * drawing methods. */ if (mask & NSContentsCellMask) { _imageToDisplay = _altImage; if (!_imageToDisplay) _imageToDisplay = [_menuItem image]; _titleToDisplay = _altContents; if (_titleToDisplay == nil || [_titleToDisplay isEqual: @""]) _titleToDisplay = [_menuItem title]; } else { _imageToDisplay = [_menuItem image]; _titleToDisplay = [_menuItem title]; } if (_imageToDisplay) { _imageWidth = [_imageToDisplay size].width; } if ([_menuView isHorizontal] == YES) { // Draw the image if (_imageWidth > 0) [self drawImageWithFrame: cellFrame inView: controlView]; // Draw the title if (_titleWidth > 0) [self drawTitleWithFrame: cellFrame inView: controlView]; } else { // Draw the state image if (_stateImageWidth > 0) [self drawStateImageWithFrame: cellFrame inView: controlView]; // Draw the image if (_imageWidth > 0) [self drawImageWithFrame: cellFrame inView: controlView]; // Draw the title if (_titleWidth > 0) [self drawTitleWithFrame: cellFrame inView: controlView]; // Draw the key equivalent if (_keyEquivalentWidth > 0) [self drawKeyEquivalentWithFrame: cellFrame inView: controlView]; } } // // NSCopying protocol // - (id) copyWithZone: (NSZone*)zone { NSMenuItemCell *c = [super copyWithZone: zone]; if (_menuItem) c->_menuItem = [_menuItem copyWithZone: zone]; /* We do not copy _menuView, because _menuView owns the old cell, but not the new one! _menuView knows nothing about c. If we copy the pointer to _menuView into c, then that pointer might become invalid at any point in time (it never becomes invalid for the original cell because _menuView will call [originalCell setMenuView: nil] when it's being deallocated. But it will not do the same for c, because it doesn't even know that c exists!) */ c->_menuView = nil; return c; } /* * NSCoding protocol * * Normally unused since the NSMenu encodes/decodes the NSMenuItems, but * not the NSMenuItemCells. */ - (void) encodeWithCoder: (NSCoder*)aCoder { [super encodeWithCoder: aCoder]; if ([aCoder allowsKeyedCoding]) { [aCoder encodeObject: _menuItem forKey: @"NSMenuItem"]; } else { [aCoder encodeConditionalObject: _menuItem]; } } - (id) initWithCoder: (NSCoder*)aDecoder { self = [super initWithCoder: aDecoder]; if (nil == self) return nil; if ([aDecoder allowsKeyedCoding]) { [self setMenuItem: [aDecoder decodeObjectForKey: @"NSMenuItem"]]; } else { ASSIGN (_menuItem, [aDecoder decodeObject]); if ([aDecoder versionForClassName: @"NSMenuItemCell"] < 2) { /* In version 1, we used to encode the _menuView here. */ [aDecoder decodeObject]; } } _needs_sizing = YES; return self; } @end