diff --git a/atom.gyp b/atom.gyp index f24f50f48a8..825a8efc0c1 100644 --- a/atom.gyp +++ b/atom.gyp @@ -98,6 +98,8 @@ 'browser/ui/accelerator_util.h', 'browser/ui/accelerator_util_mac.mm', 'browser/ui/accelerator_util_win.cc', + 'browser/ui/atom_menu_controller_mac.h', + 'browser/ui/atom_menu_controller_mac.mm', 'browser/ui/file_dialog.h', 'browser/ui/file_dialog_mac.mm', 'browser/ui/file_dialog_win.cc', diff --git a/browser/api/atom_api_menu_mac.h b/browser/api/atom_api_menu_mac.h index d34a19fbf57..342f436f5b6 100644 --- a/browser/api/atom_api_menu_mac.h +++ b/browser/api/atom_api_menu_mac.h @@ -7,7 +7,7 @@ #include "browser/api/atom_api_menu.h" -#import "chrome/browser/ui/cocoa/menu_controller.h" +#import "browser/ui/atom_menu_controller_mac.h" namespace atom { @@ -21,7 +21,7 @@ class MenuMac : public Menu { protected: virtual void Popup(NativeWindow* window) OVERRIDE; - scoped_nsobject menu_controller_; + scoped_nsobject menu_controller_; private: friend class Menu; diff --git a/browser/api/atom_api_menu_mac.mm b/browser/api/atom_api_menu_mac.mm index d7aca7613d0..2d40f6b30d2 100644 --- a/browser/api/atom_api_menu_mac.mm +++ b/browser/api/atom_api_menu_mac.mm @@ -23,9 +23,9 @@ MenuMac::~MenuMac() { } void MenuMac::Popup(NativeWindow* native_window) { - scoped_nsobject menu_controller( - [[MenuController alloc] initWithModel:model_.get() - useWithPopUpButtonCell:NO]); + scoped_nsobject menu_controller( + [[AtomMenuController alloc] initWithModel:model_.get() + useWithPopUpButtonCell:NO]); NSWindow* window = native_window->GetNativeWindow(); content::WebContents* web_contents = native_window->GetWebContents(); @@ -97,9 +97,9 @@ v8::Handle Menu::SetApplicationMenu(const v8::Arguments &args) { if (!menu) return node::ThrowError("Menu is destroyed"); - scoped_nsobject menu_controller( - [[MenuController alloc] initWithModel:menu->model_.get() - useWithPopUpButtonCell:NO]); + scoped_nsobject menu_controller( + [[AtomMenuController alloc] initWithModel:menu->model_.get() + useWithPopUpButtonCell:NO]); MenuMac::FixMenuTitles([menu_controller menu]); [NSApp setMainMenu:[menu_controller menu]]; diff --git a/browser/ui/atom_menu_controller_mac.h b/browser/ui/atom_menu_controller_mac.h new file mode 100644 index 00000000000..0455de17eb8 --- /dev/null +++ b/browser/ui/atom_menu_controller_mac.h @@ -0,0 +1,84 @@ +// Copyright (c) 2013 GitHub, Inc. All rights reserved. +// 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. + +#ifndef ATOM_BROWSER_UI_ATOM_MENU_CONTROLLER_MAC_H_ +#define ATOM_BROWSER_UI_ATOM_MENU_CONTROLLER_MAC_H_ + +#import + +#include "base/memory/scoped_nsobject.h" +#include "base/string16.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. +@interface AtomMenuController : NSObject { + @protected + ui::MenuModel* model_; // weak + scoped_nsobject 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; + +// 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 AtomMenuController (PrivateExposedForTesting) +- (BOOL)validateUserInterfaceItem:(id)item; +@end + +// Protected methods that subclassers can override. +@interface AtomMenuController (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 // ATOM_BROWSER_UI_ATOM_MENU_CONTROLLER_MAC_H_ diff --git a/browser/ui/atom_menu_controller_mac.mm b/browser/ui/atom_menu_controller_mac.mm new file mode 100644 index 00000000000..8498eb26c18 --- /dev/null +++ b/browser/ui/atom_menu_controller_mac.mm @@ -0,0 +1,268 @@ +// Copyright (c) 2013 GitHub, Inc. All rights reserved. +// 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 "browser/ui/atom_menu_controller_mac.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" +#include "ui/base/l10n/l10n_util_mac.h" +#include "ui/base/models/simple_menu_model.h" +#include "ui/gfx/image/image.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; +} + +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; +} + +// Retrieves a bitsum of ui::EventFlags from NSEvent. +int EventFlagsFromNSEvent(NSEvent* event) { + NSUInteger modifiers = [event modifierFlags]; + return EventFlagsFromNSEventWithModifiers(event, modifiers); +} + +} // namespace + +@interface AtomMenuController (Private) +- (void)addSeparatorToMenu:(NSMenu*)menu + atIndex:(int)index; +@end + +@implementation AtomMenuController + +@synthesize model = model_; +@synthesize useWithPopUpButtonCell = useWithPopUpButtonCell_; + +- (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); + 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 = 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