blob: 23eb0b8b275a8629e2dd0764efb56df79ac1f5c8 [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/strings/string16.h"
#include "base/strings/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/bookmarks/bookmark_menu_cocoa_controller.h"
#import "chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h"
#import "chrome/browser/ui/cocoa/l10n_util.h"
#import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
#import "chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h"
#import "chrome/browser/ui/cocoa/wrench_menu/recent_tabs_menu_model_delegate.h"
#include "chrome/browser/ui/toolbar/recent_tabs_sub_menu_model.h"
#include "chrome/browser/ui/toolbar/wrench_menu_model.h"
#include "content/public/browser/user_metrics.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.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;
- (NSMenu*)recentTabsSubmenu;
- (RecentTabsSubMenuModel*)recentTabsMenuModel;
- (int)maxWidthForMenuModel:(ui::MenuModel*)model
modelIndex:(int)modelIndex;
@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* out_accelerator) OVERRIDE {
AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
const ui::Accelerator* accelerator =
keymap->GetAcceleratorForCommand(command_id);
if (!accelerator)
return false;
*out_accelerator = *accelerator;
return true;
}
};
class ZoomLevelObserver {
public:
ZoomLevelObserver(WrenchMenuController* controller,
content::HostZoomMap* map)
: controller_(controller),
map_(map) {
subscription_ = map_->AddZoomLevelChangedCallback(
base::Bind(&ZoomLevelObserver::OnZoomLevelChanged,
base::Unretained(this)));
}
~ZoomLevelObserver() {}
private:
void OnZoomLevelChanged(const HostZoomMap::ZoomLevelChange& change) {
WrenchMenuModel* wrenchMenuModel = [controller_ wrenchMenuModel];
wrenchMenuModel->UpdateZoomControls();
const string16 level =
wrenchMenuModel->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY);
[[controller_ zoomDisplay] setTitle:SysUTF16ToNSString(level)];
}
scoped_ptr<content::HostZoomMap::Subscription> subscription_;
WrenchMenuController* controller_; // Weak; owns this.
content::HostZoomMap* map_; // Weak.
DISALLOW_COPY_AND_ASSIGN(ZoomLevelObserver);
};
} // namespace WrenchMenuControllerInternal
@implementation WrenchMenuController
- (id)initWithBrowser:(Browser*)browser {
if ((self = [super init])) {
browser_ = browser;
observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(
self, content::HostZoomMap::GetForBrowserContext(browser->profile())));
acceleratorDelegate_.reset(
new WrenchMenuControllerInternal::AcceleratorDelegate());
[self createModel];
}
return self;
}
- (void)addItemToMenu:(NSMenu*)menu
atIndex:(NSInteger)index
fromModel:(ui::MenuModel*)model {
// Non-button item types should be built as normal items.
ui::MenuModel::ItemType type = model->GetTypeAt(index);
if (type != ui::MenuModel::TYPE_BUTTON_ITEM) {
[super addItemToMenu:menu
atIndex:index
fromModel:model];
return;
}
// Handle the special-cased menu items.
int command_id = model->GetCommandIdAt(index);
base::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];
}
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
const BOOL enabled = [super validateUserInterfaceItem:item];
NSMenuItem* menuItem = (id)item;
ui::MenuModel* model =
static_cast<ui::MenuModel*>(
[[menuItem representedObject] pointerValue]);
// The section headers in the recent tabs submenu should be bold and black if
// a font is specified for the items (bold is already applied in the
// |MenuController| as the font returned by |GetLabelFontAt| is bold).
if (model && model == [self recentTabsMenuModel]) {
if (model->GetLabelFontAt([item tag])) {
DCHECK([menuItem attributedTitle]);
base::scoped_nsobject<NSMutableAttributedString> title(
[[NSMutableAttributedString alloc]
initWithAttributedString:[menuItem attributedTitle]]);
[title addAttribute:NSForegroundColorAttributeName
value:[NSColor blackColor]
range:NSMakeRange(0, [title length])];
[menuItem setAttributedTitle:title.get()];
} else {
// Not a section header. Add a tooltip with the title and the URL.
std::string url;
string16 title;
if ([self recentTabsMenuModel]->GetURLAndTitleForItemAtIndex(
[item tag], &url, &title)) {
[menuItem setToolTip:
cocoa_l10n_util::TooltipForURLAndTitle(
base::SysUTF8ToNSString(url), base::SysUTF16ToNSString(title))];
}
}
}
return enabled;
}
- (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 updateRecentTabsSubmenu];
[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], 0);
}
- (WrenchMenuModel*)wrenchMenuModel {
// Don't use |wrenchMenuModel_| so that a test can override the generic one.
return static_cast<WrenchMenuModel*>(model_);
}
- (void)updateRecentTabsSubmenu {
ui::MenuModel* model = [self recentTabsMenuModel];
if (model) {
recentTabsMenuModelDelegate_.reset(
new RecentTabsMenuModelDelegate(model, [self recentTabsSubmenu]));
}
}
- (void)createModel {
recentTabsMenuModelDelegate_.reset();
wrenchMenuModel_.reset(
new WrenchMenuModel(acceleratorDelegate_.get(), browser_, false));
[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];
}
}
- (NSMenu*)recentTabsSubmenu {
NSString* title = l10n_util::GetNSStringWithFixup(IDS_RECENT_TABS_MENU);
return [[[self menu] itemWithTitle:title] submenu];
}
// The recent tabs menu model is recognized by the existence of either the
// kRecentlyClosedHeaderCommandId or the kDisabledRecentlyClosedHeaderCommandId.
- (RecentTabsSubMenuModel*)recentTabsMenuModel {
int index = 0;
// Start searching at the wrench menu model level, |model| will be updated
// only if the command we're looking for is found in one of the [sub]menus.
ui::MenuModel* model = [self wrenchMenuModel];
if (ui::MenuModel::GetModelAndIndexForCommandId(
RecentTabsSubMenuModel::kRecentlyClosedHeaderCommandId, &model,
&index)) {
return static_cast<RecentTabsSubMenuModel*>(model);
}
if (ui::MenuModel::GetModelAndIndexForCommandId(
RecentTabsSubMenuModel::kDisabledRecentlyClosedHeaderCommandId,
&model, &index)) {
return static_cast<RecentTabsSubMenuModel*>(model);
}
return NULL;
}
// This overrdies the parent class to return a custom width for recent tabs
// menu.
- (int)maxWidthForMenuModel:(ui::MenuModel*)model
modelIndex:(int)modelIndex {
RecentTabsSubMenuModel* recentTabsMenuModel = [self recentTabsMenuModel];
if (recentTabsMenuModel && recentTabsMenuModel == model) {
return recentTabsMenuModel->GetMaxWidthForItemAtIndex(modelIndex);
}
return -1;
}
@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