// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/ui/cocoa/menu_controller.h" #include "base/logging.h" #include "base/strings/sys_string_conversions.h" #import "chrome/browser/ui/cocoa/event_utils.h" #include "ui/base/accelerators/accelerator.h" #include "ui/base/accelerators/platform_accelerator_cocoa.h" #include "ui/base/l10n/l10n_util_mac.h" #include "ui/base/models/simple_menu_model.h" #include "ui/base/text/text_elider.h" #include "ui/gfx/image/image.h" @interface MenuController (Private) - (void)addSeparatorToMenu:(NSMenu*)menu atIndex:(int)index; @end @implementation MenuController @synthesize model = model_; @synthesize useWithPopUpButtonCell = useWithPopUpButtonCell_; + (string16)elideMenuTitle:(const string16&)title toWidth:(int)width { NSFont* nsfont = [NSFont menuBarFontOfSize:0]; // 0 means "default" gfx::Font font(base::SysNSStringToUTF8([nsfont fontName]), static_cast([nsfont pointSize])); return ui::ElideText(title, font, width, ui::ELIDE_AT_END); } - (id)init { self = [super init]; return self; } - (id)initWithModel:(ui::MenuModel*)model useWithPopUpButtonCell:(BOOL)useWithCell { if ((self = [super init])) { model_ = model; useWithPopUpButtonCell_ = useWithCell; [self menu]; } return self; } - (void)dealloc { [menu_ setDelegate:nil]; // Close the menu if it is still open. This could happen if a tab gets closed // while its context menu is still open. [self cancel]; model_ = NULL; [super dealloc]; } - (void)cancel { if (isMenuOpen_) { [menu_ cancelTracking]; model_->MenuClosed(); isMenuOpen_ = NO; } } // Creates a NSMenu from the given model. If the model has submenus, this can // be invoked recursively. - (NSMenu*)menuFromModel:(ui::MenuModel*)model { NSMenu* menu = [[[NSMenu alloc] initWithTitle:@""] autorelease]; const int count = model->GetItemCount(); for (int index = 0; index < count; index++) { if (model->GetTypeAt(index) == ui::MenuModel::TYPE_SEPARATOR) [self addSeparatorToMenu:menu atIndex:index]; else [self addItemToMenu:menu atIndex:index fromModel:model]; } return menu; } - (int)maxWidthForMenuModel:(ui::MenuModel*)model modelIndex:(int)modelIndex { return -1; } // Adds a separator item at the given index. As the separator doesn't need // anything from the model, this method doesn't need the model index as the // other method below does. - (void)addSeparatorToMenu:(NSMenu*)menu atIndex:(int)index { NSMenuItem* separator = [NSMenuItem separatorItem]; [menu insertItem:separator atIndex:index]; } // Adds an item or a hierarchical menu to the item at the |index|, // associated with the entry in the model identified by |modelIndex|. - (void)addItemToMenu:(NSMenu*)menu atIndex:(NSInteger)index fromModel:(ui::MenuModel*)model { string16 label16 = model->GetLabelAt(index); int maxWidth = [self maxWidthForMenuModel:model modelIndex:index]; if (maxWidth != -1) label16 = [MenuController elideMenuTitle:label16 toWidth:maxWidth]; NSString* label = l10n_util::FixUpWindowsStyleLabel(label16); scoped_nsobject item( [[NSMenuItem alloc] initWithTitle:label action:@selector(itemSelected:) keyEquivalent:@""]); // If the menu item has an icon, set it. gfx::Image icon; if (model->GetIconAt(index, &icon) && !icon.IsEmpty()) [item setImage:icon.ToNSImage()]; ui::MenuModel::ItemType type = model->GetTypeAt(index); if (type == ui::MenuModel::TYPE_SUBMENU) { // Recursively build a submenu from the sub-model at this index. [item setTarget:nil]; [item setAction:nil]; ui::MenuModel* submenuModel = model->GetSubmenuModelAt(index); NSMenu* submenu = [self menuFromModel:(ui::SimpleMenuModel*)submenuModel]; [item setSubmenu:submenu]; } else { // The MenuModel works on indexes so we can't just set the command id as the // tag like we do in other menus. Also set the represented object to be // the model so hierarchical menus check the correct index in the correct // model. Setting the target to |self| allows this class to participate // in validation of the menu items. [item setTag:index]; [item setTarget:self]; NSValue* modelObject = [NSValue valueWithPointer:model]; [item setRepresentedObject:modelObject]; // Retains |modelObject|. ui::Accelerator accelerator; if (model->GetAcceleratorAt(index, &accelerator)) { const ui::PlatformAcceleratorCocoa* platformAccelerator = static_cast( accelerator.platform_accelerator()); if (platformAccelerator) { [item setKeyEquivalent:platformAccelerator->characters()]; [item setKeyEquivalentModifierMask: platformAccelerator->modifier_mask()]; } } } [menu insertItem:item atIndex:index]; } // Called before the menu is to be displayed to update the state (enabled, // radio, etc) of each item in the menu. Also will update the title if // the item is marked as "dynamic". - (BOOL)validateUserInterfaceItem:(id)item { SEL action = [item action]; if (action != @selector(itemSelected:)) return NO; NSInteger modelIndex = [item tag]; ui::MenuModel* model = static_cast( [[(id)item representedObject] pointerValue]); DCHECK(model); if (model) { BOOL checked = model->IsItemCheckedAt(modelIndex); DCHECK([(id)item isKindOfClass:[NSMenuItem class]]); [(id)item setState:(checked ? NSOnState : NSOffState)]; [(id)item setHidden:(!model->IsVisibleAt(modelIndex))]; if (model->IsItemDynamicAt(modelIndex)) { // Update the label and the icon. NSString* label = l10n_util::FixUpWindowsStyleLabel(model->GetLabelAt(modelIndex)); [(id)item setTitle:label]; gfx::Image icon; model->GetIconAt(modelIndex, &icon); [(id)item setImage:icon.IsEmpty() ? nil : icon.ToNSImage()]; } return model->IsEnabledAt(modelIndex); } return NO; } // Called when the user chooses a particular menu item. |sender| is the menu // item chosen. - (void)itemSelected:(id)sender { NSInteger modelIndex = [sender tag]; ui::MenuModel* model = static_cast( [[sender representedObject] pointerValue]); DCHECK(model); if (model) { int event_flags = event_utils::EventFlagsFromNSEvent([NSApp currentEvent]); model->ActivatedAt(modelIndex, event_flags); } } - (NSMenu*)menu { if (!menu_ && model_) { menu_.reset([[self menuFromModel:model_] retain]); [menu_ setDelegate:self]; // If this is to be used with a NSPopUpButtonCell, add an item at the 0th // position that's empty. Doing it after the menu has been constructed won't // complicate creation logic, and since the tags are model indexes, they // are unaffected by the extra item. if (useWithPopUpButtonCell_) { scoped_nsobject blankItem( [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]); [menu_ insertItem:blankItem atIndex:0]; } } return menu_.get(); } - (BOOL)isMenuOpen { return isMenuOpen_; } - (void)menuWillOpen:(NSMenu*)menu { isMenuOpen_ = YES; model_->MenuWillShow(); } - (void)menuDidClose:(NSMenu*)menu { if (isMenuOpen_) { model_->MenuClosed(); isMenuOpen_ = NO; } } @end