summaryrefslogtreecommitdiffstats
path: root/ui/base/cocoa
diff options
context:
space:
mode:
authortapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-06-06 16:26:49 +0000
committertapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-06-06 16:26:49 +0000
commit1413070cfe0f129cc159315018e5b6eedc6f50fb (patch)
tree99f115ef7db523d701e7c0ef806c78bca18a6800 /ui/base/cocoa
parenta09e0961a9ff4a9180298ad714730a5cf2f37ac7 (diff)
downloadchromium_src-1413070cfe0f129cc159315018e5b6eedc6f50fb.zip
chromium_src-1413070cfe0f129cc159315018e5b6eedc6f50fb.tar.gz
chromium_src-1413070cfe0f129cc159315018e5b6eedc6f50fb.tar.bz2
Move cocoa/menu_controller to ui/base.
This allows things outside of chrome/browser to use it. The CL also moves the Cocoa-specific event_utils.h in chrome/browser to ui/base/cocoa/cocoa_event_utils.h. BUG=138633 TEST=No functional changes Review URL: https://chromiumcodereview.appspot.com/15870006 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@204514 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/base/cocoa')
-rw-r--r--ui/base/cocoa/cocoa_event_utils.h42
-rw-r--r--ui/base/cocoa/cocoa_event_utils.mm70
-rw-r--r--ui/base/cocoa/cocoa_event_utils_unittest.mm128
-rw-r--r--ui/base/cocoa/menu_controller.h88
-rw-r--r--ui/base/cocoa/menu_controller.mm234
-rw-r--r--ui/base/cocoa/menu_controller_unittest.mm362
6 files changed, 924 insertions, 0 deletions
diff --git a/ui/base/cocoa/cocoa_event_utils.h b/ui/base/cocoa/cocoa_event_utils.h
new file mode 100644
index 0000000..1432f2d
--- /dev/null
+++ b/ui/base/cocoa/cocoa_event_utils.h
@@ -0,0 +1,42 @@
+// Copyright 2013 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.
+
+#ifndef UI_BASE_COCOA_COCOA_EVENT_UTILS_H_
+#define UI_BASE_COCOA_COCOA_EVENT_UTILS_H_
+
+#import <Cocoa/Cocoa.h>
+
+#include "ui/base/ui_export.h"
+#include "ui/base/window_open_disposition.h"
+
+namespace ui {
+
+// Retrieves a bitsum of ui::EventFlags represented by |event|,
+UI_EXPORT int EventFlagsFromNSEvent(NSEvent* event);
+
+// Retrieves a bitsum of ui::EventFlags represented by |event|,
+// but instead use the modifier flags given by |modifiers|,
+// which is the same format as |-NSEvent modifierFlags|. This allows
+// substitution of the modifiers without having to create a new event from
+// scratch.
+UI_EXPORT int EventFlagsFromNSEventWithModifiers(NSEvent* event,
+ NSUInteger modifiers);
+
+// Retrieves the WindowOpenDisposition used to open a link from a user gesture
+// represented by |event|. For example, a Cmd+Click would mean open the
+// associated link in a background tab.
+UI_EXPORT WindowOpenDisposition WindowOpenDispositionFromNSEvent(
+ NSEvent* event);
+
+// Retrieves the WindowOpenDisposition used to open a link from a user gesture
+// represented by |event|, but instead use the modifier flags given by |flags|,
+// which is the same format as |-NSEvent modifierFlags|. This allows
+// substitution of the modifiers without having to create a new event from
+// scratch.
+UI_EXPORT WindowOpenDisposition WindowOpenDispositionFromNSEventWithFlags(
+ NSEvent* event, NSUInteger flags);
+
+} // namespace ui
+
+#endif // UI_BASE_COCOA_COCOA_EVENT_UTILS_H_
diff --git a/ui/base/cocoa/cocoa_event_utils.mm b/ui/base/cocoa/cocoa_event_utils.mm
new file mode 100644
index 0000000..1920699
--- /dev/null
+++ b/ui/base/cocoa/cocoa_event_utils.mm
@@ -0,0 +1,70 @@
+// Copyright 2013 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 "ui/base/cocoa/cocoa_event_utils.h"
+
+#include "ui/base/events/event_constants.h"
+#include "ui/base/window_open_disposition.h"
+
+namespace {
+
+bool isLeftButtonEvent(NSEvent* event) {
+ NSEventType type = [event type];
+ return type == NSLeftMouseDown ||
+ type == NSLeftMouseDragged ||
+ type == NSLeftMouseUp;
+}
+
+bool isRightButtonEvent(NSEvent* event) {
+ NSEventType type = [event type];
+ return type == NSRightMouseDown ||
+ type == NSRightMouseDragged ||
+ type == NSRightMouseUp;
+}
+
+bool isMiddleButtonEvent(NSEvent* event) {
+ if ([event buttonNumber] != 2)
+ return false;
+
+ NSEventType type = [event type];
+ return type == NSOtherMouseDown ||
+ type == NSOtherMouseDragged ||
+ type == NSOtherMouseUp;
+}
+
+} // namespace
+
+namespace ui {
+
+// Retrieves a bitsum of ui::EventFlags from NSEvent.
+int EventFlagsFromNSEvent(NSEvent* event) {
+ NSUInteger modifiers = [event modifierFlags];
+ return EventFlagsFromNSEventWithModifiers(event, modifiers);
+}
+
+int EventFlagsFromNSEventWithModifiers(NSEvent* event, NSUInteger modifiers) {
+ int flags = 0;
+ flags |= (modifiers & NSAlphaShiftKeyMask) ? ui::EF_CAPS_LOCK_DOWN : 0;
+ flags |= (modifiers & NSShiftKeyMask) ? ui::EF_SHIFT_DOWN : 0;
+ flags |= (modifiers & NSControlKeyMask) ? ui::EF_CONTROL_DOWN : 0;
+ flags |= (modifiers & NSAlternateKeyMask) ? ui::EF_ALT_DOWN : 0;
+ flags |= (modifiers & NSCommandKeyMask) ? ui::EF_COMMAND_DOWN : 0;
+ flags |= isLeftButtonEvent(event) ? ui::EF_LEFT_MOUSE_BUTTON : 0;
+ flags |= isRightButtonEvent(event) ? ui::EF_RIGHT_MOUSE_BUTTON : 0;
+ flags |= isMiddleButtonEvent(event) ? ui::EF_MIDDLE_MOUSE_BUTTON : 0;
+ return flags;
+}
+
+WindowOpenDisposition WindowOpenDispositionFromNSEvent(NSEvent* event) {
+ NSUInteger modifiers = [event modifierFlags];
+ return WindowOpenDispositionFromNSEventWithFlags(event, modifiers);
+}
+
+WindowOpenDisposition WindowOpenDispositionFromNSEventWithFlags(
+ NSEvent* event, NSUInteger modifiers) {
+ int event_flags = EventFlagsFromNSEventWithModifiers(event, modifiers);
+ return ui::DispositionFromEventFlags(event_flags);
+}
+
+} // namespace ui
diff --git a/ui/base/cocoa/cocoa_event_utils_unittest.mm b/ui/base/cocoa/cocoa_event_utils_unittest.mm
new file mode 100644
index 0000000..cb5f68a
--- /dev/null
+++ b/ui/base/cocoa/cocoa_event_utils_unittest.mm
@@ -0,0 +1,128 @@
+// Copyright 2013 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 <objc/objc-class.h>
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+#include "ui/base/cocoa/cocoa_event_utils.h"
+#include "ui/base/events/event_constants.h"
+#import "ui/base/test/cocoa_test_event_utils.h"
+#import "ui/base/test/ui_cocoa_test_helper.h"
+
+// We provide a donor class with a specially modified |modifierFlags|
+// implementation that we swap with NSEvent's. This is because we can't create a
+// NSEvent that represents a middle click with modifiers.
+@interface TestEvent : NSObject
+@end
+@implementation TestEvent
+- (NSUInteger)modifierFlags { return NSShiftKeyMask; }
+@end
+
+namespace ui {
+
+namespace {
+
+class EventUtilsTest : public CocoaTest {
+};
+
+TEST_F(EventUtilsTest, TestWindowOpenDispositionFromNSEvent) {
+ // Left Click = same tab.
+ NSEvent* me = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 0);
+ EXPECT_EQ(CURRENT_TAB, WindowOpenDispositionFromNSEvent(me));
+
+ // Middle Click = new background tab.
+ me = cocoa_test_event_utils::MouseEventWithType(NSOtherMouseUp, 0);
+ EXPECT_EQ(NEW_BACKGROUND_TAB, WindowOpenDispositionFromNSEvent(me));
+
+ // Shift+Middle Click = new foreground tab.
+ {
+ ScopedClassSwizzler swizzler([NSEvent class], [TestEvent class],
+ @selector(modifierFlags));
+ me = cocoa_test_event_utils::MouseEventWithType(NSOtherMouseUp,
+ NSShiftKeyMask);
+ EXPECT_EQ(NEW_FOREGROUND_TAB, WindowOpenDispositionFromNSEvent(me));
+ }
+
+ // Cmd+Left Click = new background tab.
+ me = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
+ NSCommandKeyMask);
+ EXPECT_EQ(NEW_BACKGROUND_TAB, WindowOpenDispositionFromNSEvent(me));
+
+ // Cmd+Shift+Left Click = new foreground tab.
+ me = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
+ NSCommandKeyMask |
+ NSShiftKeyMask);
+ EXPECT_EQ(NEW_FOREGROUND_TAB, WindowOpenDispositionFromNSEvent(me));
+
+ // Shift+Left Click = new window
+ me = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
+ NSShiftKeyMask);
+ EXPECT_EQ(NEW_WINDOW, WindowOpenDispositionFromNSEvent(me));
+}
+
+
+TEST_F(EventUtilsTest, TestEventFlagsFromNSEvent) {
+ // Left click.
+ NSEvent* left = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 0);
+ EXPECT_EQ(EF_LEFT_MOUSE_BUTTON, EventFlagsFromNSEvent(left));
+
+ // Right click.
+ NSEvent* right = cocoa_test_event_utils::MouseEventWithType(NSRightMouseUp,
+ 0);
+ EXPECT_EQ(EF_RIGHT_MOUSE_BUTTON, EventFlagsFromNSEvent(right));
+
+ // Middle click.
+ NSEvent* middle = cocoa_test_event_utils::MouseEventWithType(NSOtherMouseUp,
+ 0);
+ EXPECT_EQ(EF_MIDDLE_MOUSE_BUTTON, EventFlagsFromNSEvent(middle));
+
+ // Caps + Left
+ NSEvent* caps =
+ cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
+ NSAlphaShiftKeyMask);
+ EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_CAPS_LOCK_DOWN,
+ EventFlagsFromNSEvent(caps));
+
+ // Shift + Left
+ NSEvent* shift = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
+ NSShiftKeyMask);
+ EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_SHIFT_DOWN, EventFlagsFromNSEvent(shift));
+
+ // Ctrl + Left
+ NSEvent* ctrl = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
+ NSControlKeyMask);
+ EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_CONTROL_DOWN,
+ EventFlagsFromNSEvent(ctrl));
+
+ // Alt + Left
+ NSEvent* alt = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
+ NSAlternateKeyMask);
+ EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_ALT_DOWN, EventFlagsFromNSEvent(alt));
+
+ // Cmd + Left
+ NSEvent* cmd = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
+ NSCommandKeyMask);
+ EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_COMMAND_DOWN, EventFlagsFromNSEvent(cmd));
+
+ // Shift + Ctrl + Left
+ NSEvent* shiftctrl =
+ cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
+ NSShiftKeyMask |
+ NSControlKeyMask);
+ EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_SHIFT_DOWN | EF_CONTROL_DOWN,
+ EventFlagsFromNSEvent(shiftctrl));
+
+ // Cmd + Alt + Right
+ NSEvent* cmdalt =
+ cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
+ NSCommandKeyMask |
+ NSAlternateKeyMask);
+ EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_COMMAND_DOWN | EF_ALT_DOWN,
+ EventFlagsFromNSEvent(cmdalt));
+}
+
+} // namespace
+
+} // namespace ui
diff --git a/ui/base/cocoa/menu_controller.h b/ui/base/cocoa/menu_controller.h
new file mode 100644
index 0000000..3b9718f
--- /dev/null
+++ b/ui/base/cocoa/menu_controller.h
@@ -0,0 +1,88 @@
+// Copyright 2013 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.
+
+#ifndef UI_BASE_COCOA_MENU_CONTROLLER_H_
+#define UI_BASE_COCOA_MENU_CONTROLLER_H_
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/memory/scoped_nsobject.h"
+#include "base/string16.h"
+#include "ui/base/ui_export.h"
+
+namespace ui {
+class MenuModel;
+}
+
+// A controller for the cross-platform menu model. The menu that's created
+// has the tag and represented object set for each menu item. The object is a
+// NSValue holding a pointer to the model for that level of the menu (to
+// allow for hierarchical menus). The tag is the index into that model for
+// that particular item. It is important that the model outlives this object
+// as it only maintains weak references.
+UI_EXPORT
+@interface MenuController : NSObject<NSMenuDelegate> {
+ @protected
+ ui::MenuModel* model_; // weak
+ scoped_nsobject<NSMenu> menu_;
+ BOOL useWithPopUpButtonCell_; // If YES, 0th item is blank
+ BOOL isMenuOpen_;
+}
+
+@property(nonatomic, assign) ui::MenuModel* model;
+// Note that changing this will have no effect if you use
+// |-initWithModel:useWithPopUpButtonCell:| or after the first call to |-menu|.
+@property(nonatomic) BOOL useWithPopUpButtonCell;
+
++ (string16)elideMenuTitle:(const string16&)title
+ toWidth:(int)width;
+
+// NIB-based initializer. This does not create a menu. Clients can set the
+// properties of the object and the menu will be created upon the first call to
+// |-menu|. Note that the menu will be immutable after creation.
+- (id)init;
+
+// Builds a NSMenu from the pre-built model (must not be nil). Changes made
+// to the contents of the model after calling this will not be noticed. If
+// the menu will be displayed by a NSPopUpButtonCell, it needs to be of a
+// slightly different form (0th item is empty). Note this attribute of the menu
+// cannot be changed after it has been created.
+- (id)initWithModel:(ui::MenuModel*)model
+ useWithPopUpButtonCell:(BOOL)useWithCell;
+
+// Programmatically close the constructed menu.
+- (void)cancel;
+
+// Access to the constructed menu if the complex initializer was used. If the
+// default initializer was used, then this will create the menu on first call.
+- (NSMenu*)menu;
+
+// Whether the menu is currently open.
+- (BOOL)isMenuOpen;
+
+// NSMenuDelegate methods this class implements. Subclasses should call super
+// if extending the behavior.
+- (void)menuWillOpen:(NSMenu*)menu;
+- (void)menuDidClose:(NSMenu*)menu;
+
+@end
+
+// Exposed only for unit testing, do not call directly.
+@interface MenuController (PrivateExposedForTesting)
+- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item;
+@end
+
+// Protected methods that subclassers can override.
+@interface MenuController (Protected)
+- (void)addItemToMenu:(NSMenu*)menu
+ atIndex:(NSInteger)index
+ fromModel:(ui::MenuModel*)model;
+- (NSMenu*)menuFromModel:(ui::MenuModel*)model;
+// Returns the maximum width for the menu item. Returns -1 to indicate
+// that there's no maximum width.
+- (int)maxWidthForMenuModel:(ui::MenuModel*)model
+ modelIndex:(int)modelIndex;
+@end
+
+#endif // UI_BASE_COCOA_MENU_CONTROLLER_H_
diff --git a/ui/base/cocoa/menu_controller.mm b/ui/base/cocoa/menu_controller.mm
new file mode 100644
index 0000000..cd03077
--- /dev/null
+++ b/ui/base/cocoa/menu_controller.mm
@@ -0,0 +1,234 @@
+// Copyright 2013 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 "ui/base/cocoa/menu_controller.h"
+
+#include "base/logging.h"
+#include "base/strings/sys_string_conversions.h"
+#include "ui/base/accelerators/accelerator.h"
+#include "ui/base/accelerators/platform_accelerator_cocoa.h"
+#import "ui/base/cocoa/cocoa_event_utils.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<int>([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<NSMenuItem> 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<const ui::PlatformAcceleratorCocoa*>(
+ 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<NSValidatedUserInterfaceItem>)item {
+ SEL action = [item action];
+ if (action != @selector(itemSelected:))
+ return NO;
+
+ NSInteger modelIndex = [item tag];
+ ui::MenuModel* model =
+ static_cast<ui::MenuModel*>(
+ [[(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<ui::MenuModel*>(
+ [[sender representedObject] pointerValue]);
+ DCHECK(model);
+ if (model) {
+ int event_flags = ui::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<NSMenuItem> 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
diff --git a/ui/base/cocoa/menu_controller_unittest.mm b/ui/base/cocoa/menu_controller_unittest.mm
new file mode 100644
index 0000000..d0237c6
--- /dev/null
+++ b/ui/base/cocoa/menu_controller_unittest.mm
@@ -0,0 +1,362 @@
+// Copyright 2013 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 <Cocoa/Cocoa.h>
+
+#include "base/message_loop.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "grit/ui_resources.h"
+#include "grit/ui_strings.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#import "ui/base/cocoa/menu_controller.h"
+#include "ui/base/models/simple_menu_model.h"
+#include "ui/base/resource/resource_bundle.h"
+#import "ui/base/test/ui_cocoa_test_helper.h"
+#include "ui/gfx/image/image.h"
+
+namespace ui {
+
+namespace {
+
+const int kTestLabelResourceId = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE;
+
+class MenuControllerTest : public CocoaTest {
+};
+
+// A menu delegate that counts the number of times certain things are called
+// to make sure things are hooked up properly.
+class Delegate : public SimpleMenuModel::Delegate {
+ public:
+ Delegate()
+ : execute_count_(0),
+ enable_count_(0),
+ menu_to_close_(nil),
+ did_show_(false),
+ did_close_(false) {
+ }
+
+ virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
+ return false;
+ }
+ virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE {
+ ++enable_count_;
+ return true;
+ }
+ virtual bool GetAcceleratorForCommandId(
+ int command_id,
+ Accelerator* accelerator) OVERRIDE { return false; }
+ virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE {
+ ++execute_count_;
+ }
+
+ virtual void MenuWillShow(SimpleMenuModel* /*source*/) OVERRIDE {
+ EXPECT_FALSE(did_show_);
+ EXPECT_FALSE(did_close_);
+ did_show_ = true;
+ NSArray* modes = [NSArray arrayWithObjects:NSEventTrackingRunLoopMode,
+ NSDefaultRunLoopMode,
+ nil];
+ [menu_to_close_ performSelector:@selector(cancelTracking)
+ withObject:nil
+ afterDelay:0.1
+ inModes:modes];
+ }
+
+ virtual void MenuClosed(SimpleMenuModel* /*source*/) OVERRIDE {
+ EXPECT_TRUE(did_show_);
+ EXPECT_FALSE(did_close_);
+ did_close_ = true;
+ }
+
+ int execute_count_;
+ mutable int enable_count_;
+ // The menu on which to call |-cancelTracking| after a short delay in
+ // MenuWillShow.
+ NSMenu* menu_to_close_;
+ bool did_show_;
+ bool did_close_;
+};
+
+// Just like Delegate, except the items are treated as "dynamic" so updates to
+// the label/icon in the model are reflected in the menu.
+class DynamicDelegate : public Delegate {
+ public:
+ DynamicDelegate() {}
+ virtual bool IsItemForCommandIdDynamic(int command_id) const OVERRIDE {
+ return true;
+ }
+ virtual string16 GetLabelForCommandId(int command_id) const OVERRIDE {
+ return label_;
+ }
+ virtual bool GetIconForCommandId(
+ int command_id,
+ gfx::Image* icon) const OVERRIDE {
+ if (icon_.IsEmpty()) {
+ return false;
+ } else {
+ *icon = icon_;
+ return true;
+ }
+ }
+ void SetDynamicLabel(string16 label) { label_ = label; }
+ void SetDynamicIcon(const gfx::Image& icon) { icon_ = icon; }
+
+ private:
+ string16 label_;
+ gfx::Image icon_;
+};
+
+TEST_F(MenuControllerTest, EmptyMenu) {
+ Delegate delegate;
+ SimpleMenuModel model(&delegate);
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 0);
+}
+
+TEST_F(MenuControllerTest, BasicCreation) {
+ Delegate delegate;
+ SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ model.AddItem(2, ASCIIToUTF16("two"));
+ model.AddItem(3, ASCIIToUTF16("three"));
+ model.AddSeparator(NORMAL_SEPARATOR);
+ model.AddItem(4, ASCIIToUTF16("four"));
+ model.AddItem(5, ASCIIToUTF16("five"));
+
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 6);
+
+ // Check the title, tag, and represented object are correct for a random
+ // element.
+ NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
+ NSString* title = [itemTwo title];
+ EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
+ EXPECT_EQ([itemTwo tag], 2);
+ EXPECT_EQ([[itemTwo representedObject] pointerValue], &model);
+
+ EXPECT_TRUE([[[menu menu] itemAtIndex:3] isSeparatorItem]);
+}
+
+TEST_F(MenuControllerTest, Submenus) {
+ Delegate delegate;
+ SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ SimpleMenuModel submodel(&delegate);
+ submodel.AddItem(2, ASCIIToUTF16("sub-one"));
+ submodel.AddItem(3, ASCIIToUTF16("sub-two"));
+ submodel.AddItem(4, ASCIIToUTF16("sub-three"));
+ model.AddSubMenuWithStringId(5, kTestLabelResourceId, &submodel);
+ model.AddItem(6, ASCIIToUTF16("three"));
+
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 3);
+
+ // Inspect the submenu to ensure it has correct properties.
+ NSMenu* submenu = [[[menu menu] itemAtIndex:1] submenu];
+ EXPECT_TRUE(submenu);
+ EXPECT_EQ([submenu numberOfItems], 3);
+
+ // Inspect one of the items to make sure it has the correct model as its
+ // represented object and the proper tag.
+ NSMenuItem* submenuItem = [submenu itemAtIndex:1];
+ NSString* title = [submenuItem title];
+ EXPECT_EQ(ASCIIToUTF16("sub-two"), base::SysNSStringToUTF16(title));
+ EXPECT_EQ([submenuItem tag], 1);
+ EXPECT_EQ([[submenuItem representedObject] pointerValue], &submodel);
+
+ // Make sure the item after the submenu is correct and its represented
+ // object is back to the top model.
+ NSMenuItem* item = [[menu menu] itemAtIndex:2];
+ title = [item title];
+ EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
+ EXPECT_EQ([item tag], 2);
+ EXPECT_EQ([[item representedObject] pointerValue], &model);
+}
+
+TEST_F(MenuControllerTest, EmptySubmenu) {
+ Delegate delegate;
+ SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ SimpleMenuModel submodel(&delegate);
+ model.AddSubMenuWithStringId(2, kTestLabelResourceId, &submodel);
+
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 2);
+}
+
+TEST_F(MenuControllerTest, PopUpButton) {
+ Delegate delegate;
+ SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ model.AddItem(2, ASCIIToUTF16("two"));
+ model.AddItem(3, ASCIIToUTF16("three"));
+
+ // Menu should have an extra item inserted at position 0 that has an empty
+ // title.
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:YES]);
+ EXPECT_EQ([[menu menu] numberOfItems], 4);
+ EXPECT_EQ(base::SysNSStringToUTF16([[[menu menu] itemAtIndex:0] title]),
+ string16());
+
+ // Make sure the tags are still correct (the index no longer matches the tag).
+ NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
+ EXPECT_EQ([itemTwo tag], 1);
+}
+
+TEST_F(MenuControllerTest, Execute) {
+ Delegate delegate;
+ SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 1);
+
+ // Fake selecting the menu item, we expect the delegate to be told to execute
+ // a command.
+ NSMenuItem* item = [[menu menu] itemAtIndex:0];
+ [[item target] performSelector:[item action] withObject:item];
+ EXPECT_EQ(delegate.execute_count_, 1);
+}
+
+void Validate(MenuController* controller, NSMenu* menu) {
+ for (int i = 0; i < [menu numberOfItems]; ++i) {
+ NSMenuItem* item = [menu itemAtIndex:i];
+ [controller validateUserInterfaceItem:item];
+ if ([item hasSubmenu])
+ Validate(controller, [item submenu]);
+ }
+}
+
+TEST_F(MenuControllerTest, Validate) {
+ Delegate delegate;
+ SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ model.AddItem(2, ASCIIToUTF16("two"));
+ SimpleMenuModel submodel(&delegate);
+ submodel.AddItem(2, ASCIIToUTF16("sub-one"));
+ model.AddSubMenuWithStringId(3, kTestLabelResourceId, &submodel);
+
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 3);
+
+ Validate(menu.get(), [menu menu]);
+}
+
+TEST_F(MenuControllerTest, DefaultInitializer) {
+ Delegate delegate;
+ SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("one"));
+ model.AddItem(2, ASCIIToUTF16("two"));
+ model.AddItem(3, ASCIIToUTF16("three"));
+
+ scoped_nsobject<MenuController> menu([[MenuController alloc] init]);
+ EXPECT_FALSE([menu menu]);
+
+ [menu setModel:&model];
+ [menu setUseWithPopUpButtonCell:NO];
+ EXPECT_TRUE([menu menu]);
+ EXPECT_EQ(3, [[menu menu] numberOfItems]);
+
+ // Check immutability.
+ model.AddItem(4, ASCIIToUTF16("four"));
+ EXPECT_EQ(3, [[menu menu] numberOfItems]);
+}
+
+// Test that menus with dynamic labels actually get updated.
+TEST_F(MenuControllerTest, Dynamic) {
+ DynamicDelegate delegate;
+
+ // Create a menu containing a single item whose label is "initial" and who has
+ // no icon.
+ string16 initial = ASCIIToUTF16("initial");
+ delegate.SetDynamicLabel(initial);
+ SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("foo"));
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ EXPECT_EQ([[menu menu] numberOfItems], 1);
+ // Validate() simulates opening the menu - the item label/icon should be
+ // initialized after this so we can validate the menu contents.
+ Validate(menu.get(), [menu menu]);
+ NSMenuItem* item = [[menu menu] itemAtIndex:0];
+ // Item should have the "initial" label and no icon.
+ EXPECT_EQ(initial, base::SysNSStringToUTF16([item title]));
+ EXPECT_EQ(nil, [item image]);
+
+ // Now update the item to have a label of "second" and an icon.
+ string16 second = ASCIIToUTF16("second");
+ delegate.SetDynamicLabel(second);
+ const gfx::Image& icon =
+ ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_THROBBER);
+ delegate.SetDynamicIcon(icon);
+ // Simulate opening the menu and validate that the item label + icon changes.
+ Validate(menu.get(), [menu menu]);
+ EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
+ EXPECT_TRUE([item image] != nil);
+
+ // Now get rid of the icon and make sure it goes away.
+ delegate.SetDynamicIcon(gfx::Image());
+ Validate(menu.get(), [menu menu]);
+ EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
+ EXPECT_EQ(nil, [item image]);
+}
+
+TEST_F(MenuControllerTest, OpenClose) {
+ // SimpleMenuModel posts a task that calls Delegate::MenuClosed. Create
+ // a MessageLoop for that purpose.
+ base::MessageLoop message_loop(base::MessageLoop::TYPE_UI);
+
+ // Create the model.
+ Delegate delegate;
+ SimpleMenuModel model(&delegate);
+ model.AddItem(1, ASCIIToUTF16("allays"));
+ model.AddItem(2, ASCIIToUTF16("i"));
+ model.AddItem(3, ASCIIToUTF16("bf"));
+
+ // Create the controller.
+ scoped_nsobject<MenuController> menu(
+ [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
+ delegate.menu_to_close_ = [menu menu];
+
+ EXPECT_FALSE([menu isMenuOpen]);
+
+ // In the event tracking run loop mode of the menu, verify that the controller
+ // resports the menu as open.
+ CFRunLoopPerformBlock(CFRunLoopGetCurrent(), NSEventTrackingRunLoopMode, ^{
+ EXPECT_TRUE([menu isMenuOpen]);
+ });
+
+ // Pop open the menu, which will spin an event-tracking run loop.
+ [NSMenu popUpContextMenu:[menu menu]
+ withEvent:nil
+ forView:[test_window() contentView]];
+
+ EXPECT_FALSE([menu isMenuOpen]);
+
+ // When control returns back to here, the menu will have finished running its
+ // loop and will have closed itself (see Delegate::MenuWillShow).
+ EXPECT_TRUE(delegate.did_show_);
+
+ // When the menu tells the Model it closed, the Model posts a task to notify
+ // the delegate. But since this is a test and there's no running MessageLoop,
+ // |did_close_| will remain false until we pump the task manually.
+ EXPECT_FALSE(delegate.did_close_);
+
+ // Pump the task that notifies the delegate.
+ message_loop.RunUntilIdle();
+
+ // Expect that the delegate got notified properly.
+ EXPECT_TRUE(delegate.did_close_);
+}
+
+} // namespace
+
+} // namespace ui