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.
5 #import "ui/base/cocoa/menu_controller.h"
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"
18 @interface MenuController (Private)
19 - (void)addSeparatorToMenu:(NSMenu*)menu
23 @implementation MenuController
25 @synthesize model = model_;
26 @synthesize useWithPopUpButtonCell = useWithPopUpButtonCell_;
28 + (base::string16)elideMenuTitle:(const base::string16&)title
30 NSFont* nsfont = [NSFont menuBarFontOfSize:0]; // 0 means "default"
31 return gfx::ElideText(title, gfx::FontList(gfx::Font(nsfont)), width,
40 - (id)initWithModel:(ui::MenuModel*)model
41 useWithPopUpButtonCell:(BOOL)useWithCell {
42 if ((self = [super init])) {
44 useWithPopUpButtonCell_ = useWithCell;
51 [menu_ setDelegate:nil];
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.
63 [menu_ cancelTracking];
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];
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];
79 [self addItemToMenu:menu atIndex:index fromModel:model];
85 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
86 modelIndex:(int)modelIndex {
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
95 NSMenuItem* separator = [NSMenuItem separatorItem];
96 [menu insertItem:separator atIndex:index];
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];
107 label16 = [MenuController elideMenuTitle:label16 toWidth:maxWidth];
109 NSString* label = l10n_util::FixUpWindowsStyleLabel(label16);
110 base::scoped_nsobject<NSMenuItem> item(
111 [[NSMenuItem alloc] initWithTitle:label
112 action:@selector(itemSelected:)
115 // If the menu item has an icon, set it.
117 if (model->GetIconAt(index, &icon) && !icon.IsEmpty())
118 [item setImage:icon.ToNSImage()];
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);
127 [self menuFromModel:(ui::SimpleMenuModel*)submenuModel];
128 [item setSubmenu:submenu];
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.
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()];
151 [menu insertItem:item atIndex:index];
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:))
162 NSInteger modelIndex = [item tag];
163 ui::MenuModel* model =
164 static_cast<ui::MenuModel*>(
165 [[(id)item representedObject] pointerValue]);
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.
175 l10n_util::FixUpWindowsStyleLabel(model->GetLabelAt(modelIndex));
176 [(id)item setTitle:label];
179 model->GetIconAt(modelIndex, &icon);
180 [(id)item setImage:icon.IsEmpty() ? nil : icon.ToNSImage()];
182 const gfx::FontList* font_list = model->GetLabelFontListAt(modelIndex);
184 NSDictionary *attributes =
185 [NSDictionary dictionaryWithObject:font_list->GetPrimaryFont().
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()];
193 return model->IsEnabledAt(modelIndex);
198 // Called when the user chooses a particular menu item. |sender| is the menu
200 - (void)itemSelected:(id)sender {
201 NSInteger modelIndex = [sender tag];
202 ui::MenuModel* model =
203 static_cast<ui::MenuModel*>(
204 [[sender representedObject] pointerValue]);
207 int event_flags = ui::EventFlagsFromNative([NSApp currentEvent]);
208 model->ActivatedAt(modelIndex, event_flags);
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];
233 - (void)menuWillOpen:(NSMenu*)menu {
235 model_->MenuWillShow();
238 - (void)menuDidClose:(NSMenu*)menu {
240 model_->MenuClosed();