diff options
author | tapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-06-06 16:26:49 +0000 |
---|---|---|
committer | tapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-06-06 16:26:49 +0000 |
commit | 1413070cfe0f129cc159315018e5b6eedc6f50fb (patch) | |
tree | 99f115ef7db523d701e7c0ef806c78bca18a6800 /ui/base/cocoa | |
parent | a09e0961a9ff4a9180298ad714730a5cf2f37ac7 (diff) | |
download | chromium_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.h | 42 | ||||
-rw-r--r-- | ui/base/cocoa/cocoa_event_utils.mm | 70 | ||||
-rw-r--r-- | ui/base/cocoa/cocoa_event_utils_unittest.mm | 128 | ||||
-rw-r--r-- | ui/base/cocoa/menu_controller.h | 88 | ||||
-rw-r--r-- | ui/base/cocoa/menu_controller.mm | 234 | ||||
-rw-r--r-- | ui/base/cocoa/menu_controller_unittest.mm | 362 |
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 |