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 #import "ui/base/cocoa/cocoa_event_utils.h" 12 #include "ui/base/l10n/l10n_util_mac.h" 13 #include "ui/base/models/simple_menu_model.h" 14 #include "ui/gfx/font.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 + (string16)elideMenuTitle:(const string16&)title 29 toWidth:(int)width { 30 NSFont* nsfont = [NSFont menuBarFontOfSize:0]; // 0 means "default" 31 gfx::Font font(base::SysNSStringToUTF8([nsfont fontName]), 32 static_cast<int>([nsfont pointSize])); 33 return gfx::ElideText(title, font, width, gfx::ELIDE_AT_END); 34 } 35 36 - (id)init { 37 self = [super init]; 38 return self; 39 } 40 41 - (id)initWithModel:(ui::MenuModel*)model 42 useWithPopUpButtonCell:(BOOL)useWithCell { 43 if ((self = [super init])) { 44 model_ = model; 45 useWithPopUpButtonCell_ = useWithCell; 46 [self menu]; 47 } 48 return self; 49 } 50 51 - (void)dealloc { 52 [menu_ setDelegate:nil]; 53 54 // Close the menu if it is still open. This could happen if a tab gets closed 55 // while its context menu is still open. 56 [self cancel]; 57 58 model_ = NULL; 59 [super dealloc]; 60 } 61 62 - (void)cancel { 63 if (isMenuOpen_) { 64 [menu_ cancelTracking]; 65 model_->MenuClosed(); 66 isMenuOpen_ = NO; 67 } 68 } 69 70 // Creates a NSMenu from the given model. If the model has submenus, this can 71 // be invoked recursively. 72 - (NSMenu*)menuFromModel:(ui::MenuModel*)model { 73 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@""] autorelease]; 74 75 const int count = model->GetItemCount(); 76 for (int index = 0; index < count; index++) { 77 if (model->GetTypeAt(index) == ui::MenuModel::TYPE_SEPARATOR) 78 [self addSeparatorToMenu:menu atIndex:index]; 79 else 80 [self addItemToMenu:menu atIndex:index fromModel:model]; 81 } 82 83 return menu; 84 } 85 86 - (int)maxWidthForMenuModel:(ui::MenuModel*)model 87 modelIndex:(int)modelIndex { 88 return -1; 89 } 90 91 // Adds a separator item at the given index. As the separator doesn't need 92 // anything from the model, this method doesn't need the model index as the 93 // other method below does. 94 - (void)addSeparatorToMenu:(NSMenu*)menu 95 atIndex:(int)index { 96 NSMenuItem* separator = [NSMenuItem separatorItem]; 97 [menu insertItem:separator atIndex:index]; 98 } 99 100 // Adds an item or a hierarchical menu to the item at the |index|, 101 // associated with the entry in the model identified by |modelIndex|. 102 - (void)addItemToMenu:(NSMenu*)menu 103 atIndex:(NSInteger)index 104 fromModel:(ui::MenuModel*)model { 105 string16 label16 = model->GetLabelAt(index); 106 int maxWidth = [self maxWidthForMenuModel:model modelIndex:index]; 107 if (maxWidth != -1) 108 label16 = [MenuController elideMenuTitle:label16 toWidth:maxWidth]; 109 110 NSString* label = l10n_util::FixUpWindowsStyleLabel(label16); 111 base::scoped_nsobject<NSMenuItem> item( 112 [[NSMenuItem alloc] initWithTitle:label 113 action:@selector(itemSelected:) 114 keyEquivalent:@""]); 115 116 // If the menu item has an icon, set it. 117 gfx::Image icon; 118 if (model->GetIconAt(index, &icon) && !icon.IsEmpty()) 119 [item setImage:icon.ToNSImage()]; 120 121 ui::MenuModel::ItemType type = model->GetTypeAt(index); 122 if (type == ui::MenuModel::TYPE_SUBMENU) { 123 // Recursively build a submenu from the sub-model at this index. 124 [item setTarget:nil]; 125 [item setAction:nil]; 126 ui::MenuModel* submenuModel = model->GetSubmenuModelAt(index); 127 NSMenu* submenu = 128 [self menuFromModel:(ui::SimpleMenuModel*)submenuModel]; 129 [item setSubmenu:submenu]; 130 } else { 131 // The MenuModel works on indexes so we can't just set the command id as the 132 // tag like we do in other menus. Also set the represented object to be 133 // the model so hierarchical menus check the correct index in the correct 134 // model. Setting the target to |self| allows this class to participate 135 // in validation of the menu items. 136 [item setTag:index]; 137 [item setTarget:self]; 138 NSValue* modelObject = [NSValue valueWithPointer:model]; 139 [item setRepresentedObject:modelObject]; // Retains |modelObject|. 140 ui::Accelerator accelerator; 141 if (model->GetAcceleratorAt(index, &accelerator)) { 142 const ui::PlatformAcceleratorCocoa* platformAccelerator = 143 static_cast<const ui::PlatformAcceleratorCocoa*>( 144 accelerator.platform_accelerator()); 145 if (platformAccelerator) { 146 [item setKeyEquivalent:platformAccelerator->characters()]; 147 [item setKeyEquivalentModifierMask: 148 platformAccelerator->modifier_mask()]; 149 } 150 } 151 } 152 [menu insertItem:item atIndex:index]; 153 } 154 155 // Called before the menu is to be displayed to update the state (enabled, 156 // radio, etc) of each item in the menu. Also will update the title if 157 // the item is marked as "dynamic". 158 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item { 159 SEL action = [item action]; 160 if (action != @selector(itemSelected:)) 161 return NO; 162 163 NSInteger modelIndex = [item tag]; 164 ui::MenuModel* model = 165 static_cast<ui::MenuModel*>( 166 [[(id)item representedObject] pointerValue]); 167 DCHECK(model); 168 if (model) { 169 BOOL checked = model->IsItemCheckedAt(modelIndex); 170 DCHECK([(id)item isKindOfClass:[NSMenuItem class]]); 171 [(id)item setState:(checked ? NSOnState : NSOffState)]; 172 [(id)item setHidden:(!model->IsVisibleAt(modelIndex))]; 173 if (model->IsItemDynamicAt(modelIndex)) { 174 // Update the label and the icon. 175 NSString* label = 176 l10n_util::FixUpWindowsStyleLabel(model->GetLabelAt(modelIndex)); 177 [(id)item setTitle:label]; 178 179 gfx::Image icon; 180 model->GetIconAt(modelIndex, &icon); 181 [(id)item setImage:icon.IsEmpty() ? nil : icon.ToNSImage()]; 182 } 183 const gfx::Font* font = model->GetLabelFontAt(modelIndex); 184 if (font) { 185 NSDictionary *attributes = 186 [NSDictionary dictionaryWithObject:font->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::EventFlagsFromNSEvent([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