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/base/text/text_elider.h" 15 #include "ui/gfx/image/image.h" 16 17 @interface MenuController (Private) 18 - (void)addSeparatorToMenu:(NSMenu*)menu 19 atIndex:(int)index; 20 @end 21 22 @implementation MenuController 23 24 @synthesize model = model_; 25 @synthesize useWithPopUpButtonCell = useWithPopUpButtonCell_; 26 27 + (string16)elideMenuTitle:(const string16&)title 28 toWidth:(int)width { 29 NSFont* nsfont = [NSFont menuBarFontOfSize:0]; // 0 means "default" 30 gfx::Font font(base::SysNSStringToUTF8([nsfont fontName]), 31 static_cast<int>([nsfont pointSize])); 32 return ui::ElideText(title, font, width, ui::ELIDE_AT_END); 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 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::Font* font = model->GetLabelFontAt(modelIndex); 183 if (font) { 184 NSDictionary *attributes = 185 [NSDictionary dictionaryWithObject:font->GetNativeFont() 186 forKey:NSFontAttributeName]; 187 base::scoped_nsobject<NSAttributedString> title( 188 [[NSAttributedString alloc] initWithString:[(id)item title] 189 attributes:attributes]); 190 [(id)item setAttributedTitle:title.get()]; 191 } 192 return model->IsEnabledAt(modelIndex); 193 } 194 return NO; 195 } 196 197 // Called when the user chooses a particular menu item. |sender| is the menu 198 // item chosen. 199 - (void)itemSelected:(id)sender { 200 NSInteger modelIndex = [sender tag]; 201 ui::MenuModel* model = 202 static_cast<ui::MenuModel*>( 203 [[sender representedObject] pointerValue]); 204 DCHECK(model); 205 if (model) { 206 int event_flags = ui::EventFlagsFromNSEvent([NSApp currentEvent]); 207 model->ActivatedAt(modelIndex, event_flags); 208 } 209 } 210 211 - (NSMenu*)menu { 212 if (!menu_ && model_) { 213 menu_.reset([[self menuFromModel:model_] retain]); 214 [menu_ setDelegate:self]; 215 // If this is to be used with a NSPopUpButtonCell, add an item at the 0th 216 // position that's empty. Doing it after the menu has been constructed won't 217 // complicate creation logic, and since the tags are model indexes, they 218 // are unaffected by the extra item. 219 if (useWithPopUpButtonCell_) { 220 base::scoped_nsobject<NSMenuItem> blankItem( 221 [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]); 222 [menu_ insertItem:blankItem atIndex:0]; 223 } 224 } 225 return menu_.get(); 226 } 227 228 - (BOOL)isMenuOpen { 229 return isMenuOpen_; 230 } 231 232 - (void)menuWillOpen:(NSMenu*)menu { 233 isMenuOpen_ = YES; 234 model_->MenuWillShow(); 235 } 236 237 - (void)menuDidClose:(NSMenu*)menu { 238 if (isMenuOpen_) { 239 model_->MenuClosed(); 240 isMenuOpen_ = NO; 241 } 242 } 243 244 @end 245