1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "ui/base/cocoa/menu_controller.h" 6 7#include "base/logging.h" 8#include "base/strings/sys_string_conversions.h" 9#include "ui/base/accelerators/accelerator.h" 10#include "ui/base/accelerators/platform_accelerator_cocoa.h" 11#include "ui/base/l10n/l10n_util_mac.h" 12#include "ui/base/models/simple_menu_model.h" 13#import "ui/events/event_utils.h" 14#include "ui/gfx/font_list.h" 15#include "ui/gfx/image/image.h" 16#include "ui/gfx/text_elider.h" 17 18@interface MenuController (Private) 19- (void)addSeparatorToMenu:(NSMenu*)menu 20 atIndex:(int)index; 21@end 22 23@implementation MenuController 24 25@synthesize model = model_; 26@synthesize useWithPopUpButtonCell = useWithPopUpButtonCell_; 27 28+ (base::string16)elideMenuTitle:(const base::string16&)title 29 toWidth:(int)width { 30 NSFont* nsfont = [NSFont menuBarFontOfSize:0]; // 0 means "default" 31 return gfx::ElideText(title, gfx::FontList(gfx::Font(nsfont)), width, 32 gfx::ELIDE_TAIL); 33} 34 35- (id)init { 36 self = [super init]; 37 return self; 38} 39 40- (id)initWithModel:(ui::MenuModel*)model 41 useWithPopUpButtonCell:(BOOL)useWithCell { 42 if ((self = [super init])) { 43 model_ = model; 44 useWithPopUpButtonCell_ = useWithCell; 45 [self menu]; 46 } 47 return self; 48} 49 50- (void)dealloc { 51 [menu_ setDelegate:nil]; 52 53 // Close the menu if it is still open. This could happen if a tab gets closed 54 // while its context menu is still open. 55 [self cancel]; 56 57 model_ = NULL; 58 [super dealloc]; 59} 60 61- (void)cancel { 62 if (isMenuOpen_) { 63 [menu_ cancelTracking]; 64 model_->MenuClosed(); 65 isMenuOpen_ = NO; 66 } 67} 68 69// Creates a NSMenu from the given model. If the model has submenus, this can 70// be invoked recursively. 71- (NSMenu*)menuFromModel:(ui::MenuModel*)model { 72 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@""] autorelease]; 73 74 const int count = model->GetItemCount(); 75 for (int index = 0; index < count; index++) { 76 if (model->GetTypeAt(index) == ui::MenuModel::TYPE_SEPARATOR) 77 [self addSeparatorToMenu:menu atIndex:index]; 78 else 79 [self addItemToMenu:menu atIndex:index fromModel:model]; 80 } 81 82 return menu; 83} 84 85- (int)maxWidthForMenuModel:(ui::MenuModel*)model 86 modelIndex:(int)modelIndex { 87 return -1; 88} 89 90// Adds a separator item at the given index. As the separator doesn't need 91// anything from the model, this method doesn't need the model index as the 92// other method below does. 93- (void)addSeparatorToMenu:(NSMenu*)menu 94 atIndex:(int)index { 95 NSMenuItem* separator = [NSMenuItem separatorItem]; 96 [menu insertItem:separator atIndex:index]; 97} 98 99// Adds an item or a hierarchical menu to the item at the |index|, 100// associated with the entry in the model identified by |modelIndex|. 101- (void)addItemToMenu:(NSMenu*)menu 102 atIndex:(NSInteger)index 103 fromModel:(ui::MenuModel*)model { 104 base::string16 label16 = model->GetLabelAt(index); 105 int maxWidth = [self maxWidthForMenuModel:model modelIndex:index]; 106 if (maxWidth != -1) 107 label16 = [MenuController elideMenuTitle:label16 toWidth:maxWidth]; 108 109 NSString* label = l10n_util::FixUpWindowsStyleLabel(label16); 110 base::scoped_nsobject<NSMenuItem> item( 111 [[NSMenuItem alloc] initWithTitle:label 112 action:@selector(itemSelected:) 113 keyEquivalent:@""]); 114 115 // If the menu item has an icon, set it. 116 gfx::Image icon; 117 if (model->GetIconAt(index, &icon) && !icon.IsEmpty()) 118 [item setImage:icon.ToNSImage()]; 119 120 ui::MenuModel::ItemType type = model->GetTypeAt(index); 121 if (type == ui::MenuModel::TYPE_SUBMENU) { 122 // Recursively build a submenu from the sub-model at this index. 123 [item setTarget:nil]; 124 [item setAction:nil]; 125 ui::MenuModel* submenuModel = model->GetSubmenuModelAt(index); 126 NSMenu* submenu = 127 [self menuFromModel:(ui::SimpleMenuModel*)submenuModel]; 128 [item setSubmenu:submenu]; 129 } else { 130 // The MenuModel works on indexes so we can't just set the command id as the 131 // tag like we do in other menus. Also set the represented object to be 132 // the model so hierarchical menus check the correct index in the correct 133 // model. Setting the target to |self| allows this class to participate 134 // in validation of the menu items. 135 [item setTag:index]; 136 [item setTarget:self]; 137 NSValue* modelObject = [NSValue valueWithPointer:model]; 138 [item setRepresentedObject:modelObject]; // Retains |modelObject|. 139 ui::Accelerator accelerator; 140 if (model->GetAcceleratorAt(index, &accelerator)) { 141 const ui::PlatformAcceleratorCocoa* platformAccelerator = 142 static_cast<const ui::PlatformAcceleratorCocoa*>( 143 accelerator.platform_accelerator()); 144 if (platformAccelerator) { 145 [item setKeyEquivalent:platformAccelerator->characters()]; 146 [item setKeyEquivalentModifierMask: 147 platformAccelerator->modifier_mask()]; 148 } 149 } 150 } 151 [menu insertItem:item atIndex:index]; 152} 153 154// Called before the menu is to be displayed to update the state (enabled, 155// radio, etc) of each item in the menu. Also will update the title if 156// the item is marked as "dynamic". 157- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item { 158 SEL action = [item action]; 159 if (action != @selector(itemSelected:)) 160 return NO; 161 162 NSInteger modelIndex = [item tag]; 163 ui::MenuModel* model = 164 static_cast<ui::MenuModel*>( 165 [[(id)item representedObject] pointerValue]); 166 DCHECK(model); 167 if (model) { 168 BOOL checked = model->IsItemCheckedAt(modelIndex); 169 DCHECK([(id)item isKindOfClass:[NSMenuItem class]]); 170 [(id)item setState:(checked ? NSOnState : NSOffState)]; 171 [(id)item setHidden:(!model->IsVisibleAt(modelIndex))]; 172 if (model->IsItemDynamicAt(modelIndex)) { 173 // Update the label and the icon. 174 NSString* label = 175 l10n_util::FixUpWindowsStyleLabel(model->GetLabelAt(modelIndex)); 176 [(id)item setTitle:label]; 177 178 gfx::Image icon; 179 model->GetIconAt(modelIndex, &icon); 180 [(id)item setImage:icon.IsEmpty() ? nil : icon.ToNSImage()]; 181 } 182 const gfx::FontList* font_list = model->GetLabelFontListAt(modelIndex); 183 if (font_list) { 184 NSDictionary *attributes = 185 [NSDictionary dictionaryWithObject:font_list->GetPrimaryFont(). 186 GetNativeFont() 187 forKey:NSFontAttributeName]; 188 base::scoped_nsobject<NSAttributedString> title( 189 [[NSAttributedString alloc] initWithString:[(id)item title] 190 attributes:attributes]); 191 [(id)item setAttributedTitle:title.get()]; 192 } 193 return model->IsEnabledAt(modelIndex); 194 } 195 return NO; 196} 197 198// Called when the user chooses a particular menu item. |sender| is the menu 199// item chosen. 200- (void)itemSelected:(id)sender { 201 NSInteger modelIndex = [sender tag]; 202 ui::MenuModel* model = 203 static_cast<ui::MenuModel*>( 204 [[sender representedObject] pointerValue]); 205 DCHECK(model); 206 if (model) { 207 int event_flags = ui::EventFlagsFromNative([NSApp currentEvent]); 208 model->ActivatedAt(modelIndex, event_flags); 209 } 210} 211 212- (NSMenu*)menu { 213 if (!menu_ && model_) { 214 menu_.reset([[self menuFromModel:model_] retain]); 215 [menu_ setDelegate:self]; 216 // If this is to be used with a NSPopUpButtonCell, add an item at the 0th 217 // position that's empty. Doing it after the menu has been constructed won't 218 // complicate creation logic, and since the tags are model indexes, they 219 // are unaffected by the extra item. 220 if (useWithPopUpButtonCell_) { 221 base::scoped_nsobject<NSMenuItem> blankItem( 222 [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]); 223 [menu_ insertItem:blankItem atIndex:0]; 224 } 225 } 226 return menu_.get(); 227} 228 229- (BOOL)isMenuOpen { 230 return isMenuOpen_; 231} 232 233- (void)menuWillOpen:(NSMenu*)menu { 234 isMenuOpen_ = YES; 235 model_->MenuWillShow(); 236} 237 238- (void)menuDidClose:(NSMenu*)menu { 239 if (isMenuOpen_) { 240 model_->MenuClosed(); 241 isMenuOpen_ = NO; 242 } 243} 244 245@end 246