Home | History | Annotate | Download | only in cocoa
      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