blob: 8ad158fd36090ee68bb2facc20b19018098459be [file] [log] [blame]
// Copyright (c) 2011 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/wrench_menu/wrench_menu_controller.h"
#include "base/basictypes.h"
#include "base/mac/bundle_locations.h"
#include "base/mac/mac_util.h"
#include "base/string16.h"
#include "base/sys_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#import "chrome/browser/app_controller_mac.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#import "chrome/browser/ui/cocoa/accelerators_cocoa.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
#import "chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h"
#import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
#import "chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h"
#include "chrome/browser/ui/toolbar/wrench_menu_model.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/notification_observer.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/user_metrics.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
#include "ui/base/accelerators/accelerator_cocoa.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/menu_model.h"
using content::HostZoomMap;
using content::UserMetricsAction;
@interface WrenchMenuController (Private)
- (void)createModel;
- (void)adjustPositioning;
- (void)performCommandDispatch:(NSNumber*)tag;
- (NSButton*)zoomDisplay;
- (void)removeAllItems:(NSMenu*)menu;
@end
namespace WrenchMenuControllerInternal {
// A C++ delegate that handles the accelerators in the wrench menu.
class AcceleratorDelegate : public ui::AcceleratorProvider {
public:
virtual bool GetAcceleratorForCommandId(int command_id,
ui::Accelerator* accelerator_generic) {
// Downcast so that when the copy constructor is invoked below, the key
// string gets copied, too.
ui::AcceleratorCocoa* out_accelerator =
static_cast<ui::AcceleratorCocoa*>(accelerator_generic);
AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
const ui::AcceleratorCocoa* accelerator =
keymap->GetAcceleratorForCommand(command_id);
if (accelerator) {
*out_accelerator = *accelerator;
return true;
}
return false;
}
};
class ZoomLevelObserver : public content::NotificationObserver {
public:
explicit ZoomLevelObserver(WrenchMenuController* controller)
: controller_(controller) {
registrar_.Add(
this, content::NOTIFICATION_ZOOM_LEVEL_CHANGED,
content::NotificationService::AllBrowserContextsAndSources());
}
void Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK_EQ(type, content::NOTIFICATION_ZOOM_LEVEL_CHANGED);
WrenchMenuModel* wrenchMenuModel = [controller_ wrenchMenuModel];
if (HostZoomMap::GetForBrowserContext(
wrenchMenuModel->browser()->profile()) !=
content::Source<HostZoomMap>(source).ptr()) {
return;
}
wrenchMenuModel->UpdateZoomControls();
const string16 level =
wrenchMenuModel->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY);
[[controller_ zoomDisplay] setTitle:SysUTF16ToNSString(level)];
}
private:
content::NotificationRegistrar registrar_;
WrenchMenuController* controller_; // Weak; owns this.
};
} // namespace WrenchMenuControllerInternal
@implementation WrenchMenuController
- (id)initWithBrowser:(Browser*)browser {
if ((self = [super init])) {
browser_ = browser;
observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(self));
acceleratorDelegate_.reset(
new WrenchMenuControllerInternal::AcceleratorDelegate());
[self createModel];
}
return self;
}
- (void)addItemToMenu:(NSMenu*)menu
atIndex:(NSInteger)index
fromModel:(ui::MenuModel*)model
modelIndex:(int)modelIndex {
// Non-button item types should be built as normal items.
ui::MenuModel::ItemType type = model->GetTypeAt(modelIndex);
if (type != ui::MenuModel::TYPE_BUTTON_ITEM) {
[super addItemToMenu:menu
atIndex:index
fromModel:model
modelIndex:modelIndex];
return;
}
// Handle the special-cased menu items.
int command_id = model->GetCommandIdAt(modelIndex);
scoped_nsobject<NSMenuItem> customItem(
[[NSMenuItem alloc] initWithTitle:@""
action:nil
keyEquivalent:@""]);
MenuTrackedRootView* view;
switch (command_id) {
case IDC_EDIT_MENU:
view = [buttonViewController_ editItem];
DCHECK(view);
[customItem setView:view];
[view setMenuItem:customItem];
break;
case IDC_ZOOM_MENU:
view = [buttonViewController_ zoomItem];
DCHECK(view);
[customItem setView:view];
[view setMenuItem:customItem];
break;
default:
NOTREACHED();
break;
}
[self adjustPositioning];
[menu insertItem:customItem.get() atIndex:index];
}
- (NSMenu*)bookmarkSubMenu {
NSString* title = l10n_util::GetNSStringWithFixup(IDS_BOOKMARKS_MENU);
return [[[self menu] itemWithTitle:title] submenu];
}
- (void)updateBookmarkSubMenu {
NSMenu* bookmarkMenu = [self bookmarkSubMenu];
DCHECK(bookmarkMenu);
bookmarkMenuBridge_.reset(
new BookmarkMenuBridge([self wrenchMenuModel]->browser()->profile(),
bookmarkMenu));
}
- (void)menuWillOpen:(NSMenu*)menu {
[super menuWillOpen:menu];
NSString* title = base::SysUTF16ToNSString(
[self wrenchMenuModel]->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY));
[[[buttonViewController_ zoomItem] viewWithTag:IDC_ZOOM_PERCENT_DISPLAY]
setTitle:title];
content::RecordAction(UserMetricsAction("ShowAppMenu"));
NSImage* icon = [self wrenchMenuModel]->browser()->window()->IsFullscreen() ?
[NSImage imageNamed:NSImageNameExitFullScreenTemplate] :
[NSImage imageNamed:NSImageNameEnterFullScreenTemplate];
[[buttonViewController_ zoomFullScreen] setImage:icon];
}
- (void)menuNeedsUpdate:(NSMenu*)menu {
// First empty out the menu and create a new model.
[self removeAllItems:menu];
[self createModel];
// Create a new menu, which cannot be swapped because the tracking is about to
// start, so simply copy the items.
NSMenu* newMenu = [self menuFromModel:model_];
NSArray* itemArray = [newMenu itemArray];
[self removeAllItems:newMenu];
for (NSMenuItem* item in itemArray) {
[menu addItem:item];
}
[self updateBookmarkSubMenu];
}
// Used to dispatch commands from the Wrench menu. The custom items within the
// menu cannot be hooked up directly to First Responder because the window in
// which the controls reside is not the BrowserWindowController, but a
// NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system.
- (IBAction)dispatchWrenchMenuCommand:(id)sender {
NSInteger tag = [sender tag];
if (sender == [buttonViewController_ zoomPlus] ||
sender == [buttonViewController_ zoomMinus]) {
// Do a direct dispatch rather than scheduling on the outermost run loop,
// which would not get hit until after the menu had closed.
[self performCommandDispatch:[NSNumber numberWithInt:tag]];
// The zoom buttons should not close the menu if opened sticky.
if ([sender respondsToSelector:@selector(isTracking)] &&
[sender performSelector:@selector(isTracking)]) {
[menu_ cancelTracking];
}
} else {
// The custom views within the Wrench menu are abnormal and keep the menu
// open after a target-action. Close the menu manually.
[menu_ cancelTracking];
// Executing certain commands from the nested run loop of the menu can lead
// to wonky behavior (e.g. http://crbug.com/49716). To avoid this, schedule
// the dispatch on the outermost run loop.
[self performSelector:@selector(performCommandDispatch:)
withObject:[NSNumber numberWithInt:tag]
afterDelay:0.0];
}
}
// Used to perform the actual dispatch on the outermost runloop.
- (void)performCommandDispatch:(NSNumber*)tag {
[self wrenchMenuModel]->ExecuteCommand([tag intValue]);
}
- (WrenchMenuModel*)wrenchMenuModel {
// Don't use |wrenchMenuModel_| so that a test can override the generic one.
return static_cast<WrenchMenuModel*>(model_);
}
- (void)createModel {
wrenchMenuModel_.reset(
new WrenchMenuModel(acceleratorDelegate_.get(), browser_));
[self setModel:wrenchMenuModel_.get()];
buttonViewController_.reset(
[[WrenchMenuButtonViewController alloc] initWithController:self]);
[buttonViewController_ view];
}
// Fit the localized strings into the Cut/Copy/Paste control, then resize the
// whole menu item accordingly.
- (void)adjustPositioning {
const CGFloat kButtonPadding = 12;
CGFloat delta = 0;
// Go through the three buttons from right-to-left, adjusting the size to fit
// the localized strings while keeping them all aligned on their horizontal
// edges.
NSButton* views[] = {
[buttonViewController_ editPaste],
[buttonViewController_ editCopy],
[buttonViewController_ editCut]
};
for (size_t i = 0; i < arraysize(views); ++i) {
NSButton* button = views[i];
CGFloat originalWidth = NSWidth([button frame]);
// Do not let |-sizeToFit| change the height of the button.
NSSize size = [button frame].size;
[button sizeToFit];
size.width = [button frame].size.width + kButtonPadding;
[button setFrameSize:size];
CGFloat newWidth = size.width;
delta += newWidth - originalWidth;
NSRect frame = [button frame];
frame.origin.x -= delta;
[button setFrame:frame];
}
// Resize the menu item by the total amound the buttons changed so that the
// spacing between the buttons and the title remains the same.
NSRect itemFrame = [[buttonViewController_ editItem] frame];
itemFrame.size.width += delta;
[[buttonViewController_ editItem] setFrame:itemFrame];
// Also resize the superview of the buttons, which is an NSView used to slide
// when the item title is too big and GTM resizes it.
NSRect parentFrame = [[[buttonViewController_ editCut] superview] frame];
parentFrame.size.width += delta;
parentFrame.origin.x -= delta;
[[[buttonViewController_ editCut] superview] setFrame:parentFrame];
}
- (NSButton*)zoomDisplay {
return [buttonViewController_ zoomDisplay];
}
// -[NSMenu removeAllItems] is only available on 10.6+.
- (void)removeAllItems:(NSMenu*)menu {
while ([menu numberOfItems]) {
[menu removeItemAtIndex:0];
}
}
@end // @implementation WrenchMenuController
////////////////////////////////////////////////////////////////////////////////
@implementation WrenchMenuButtonViewController
@synthesize editItem = editItem_;
@synthesize editCut = editCut_;
@synthesize editCopy = editCopy_;
@synthesize editPaste = editPaste_;
@synthesize zoomItem = zoomItem_;
@synthesize zoomPlus = zoomPlus_;
@synthesize zoomDisplay = zoomDisplay_;
@synthesize zoomMinus = zoomMinus_;
@synthesize zoomFullScreen = zoomFullScreen_;
- (id)initWithController:(WrenchMenuController*)controller {
if ((self = [super initWithNibName:@"WrenchMenu"
bundle:base::mac::FrameworkBundle()])) {
controller_ = controller;
}
return self;
}
- (IBAction)dispatchWrenchMenuCommand:(id)sender {
[controller_ dispatchWrenchMenuCommand:sender];
}
@end // @implementation WrenchMenuButtonViewController