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 #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