• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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