1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.h"
7 #include "base/basictypes.h"
8 #include "base/mac/bundle_locations.h"
9 #include "base/mac/mac_util.h"
10 #include "base/strings/string16.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "chrome/app/chrome_command_ids.h"
13 #import "chrome/browser/app_controller_mac.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/browser/ui/browser.h"
16 #include "chrome/browser/ui/browser_window.h"
17 #import "chrome/browser/ui/cocoa/accelerators_cocoa.h"
18 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
19 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
20 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
21 #import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
22 #import "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
23 #import "chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h"
24 #import "chrome/browser/ui/cocoa/l10n_util.h"
25 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
26 #import "chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h"
27 #import "chrome/browser/ui/cocoa/wrench_menu/recent_tabs_menu_model_delegate.h"
28 #include "chrome/browser/ui/toolbar/recent_tabs_sub_menu_model.h"
29 #include "chrome/browser/ui/toolbar/wrench_menu_model.h"
30 #include "chrome/grit/generated_resources.h"
31 #include "components/ui/zoom/zoom_event_manager.h"
32 #include "content/public/browser/user_metrics.h"
33 #include "ui/base/l10n/l10n_util.h"
34 #include "ui/base/models/menu_model.h"
35 #include "ui/gfx/geometry/size.h"
37 namespace wrench_menu_controller {
38 const CGFloat kWrenchBubblePointOffsetY = 6;
41 using base::UserMetricsAction;
43 @interface WrenchMenuController (Private)
45 - (void)adjustPositioning;
46 - (void)performCommandDispatch:(NSNumber*)tag;
47 - (NSButton*)zoomDisplay;
48 - (void)removeAllItems:(NSMenu*)menu;
49 - (NSMenu*)recentTabsSubmenu;
50 - (RecentTabsSubMenuModel*)recentTabsMenuModel;
51 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
52 modelIndex:(int)modelIndex;
55 namespace WrenchMenuControllerInternal {
57 // A C++ delegate that handles the accelerators in the wrench menu.
58 class AcceleratorDelegate : public ui::AcceleratorProvider {
60 bool GetAcceleratorForCommandId(int command_id,
61 ui::Accelerator* out_accelerator) override {
62 AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
63 const ui::Accelerator* accelerator =
64 keymap->GetAcceleratorForCommand(command_id);
67 *out_accelerator = *accelerator;
72 class ZoomLevelObserver {
74 ZoomLevelObserver(WrenchMenuController* controller,
75 ui_zoom::ZoomEventManager* manager)
76 : controller_(controller) {
77 subscription_ = manager->AddZoomLevelChangedCallback(
78 base::Bind(&ZoomLevelObserver::OnZoomLevelChanged,
79 base::Unretained(this)));
82 ~ZoomLevelObserver() {}
85 void OnZoomLevelChanged(const content::HostZoomMap::ZoomLevelChange& change) {
86 WrenchMenuModel* wrenchMenuModel = [controller_ wrenchMenuModel];
87 wrenchMenuModel->UpdateZoomControls();
88 const base::string16 level =
89 wrenchMenuModel->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY);
90 [[controller_ zoomDisplay] setTitle:SysUTF16ToNSString(level)];
93 scoped_ptr<content::HostZoomMap::Subscription> subscription_;
95 WrenchMenuController* controller_; // Weak; owns this.
97 DISALLOW_COPY_AND_ASSIGN(ZoomLevelObserver);
100 } // namespace WrenchMenuControllerInternal
102 @implementation WrenchMenuController
104 - (id)initWithBrowser:(Browser*)browser {
105 if ((self = [super init])) {
107 observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(
109 ui_zoom::ZoomEventManager::GetForBrowserContext(browser->profile())));
110 acceleratorDelegate_.reset(
111 new WrenchMenuControllerInternal::AcceleratorDelegate());
117 - (void)addItemToMenu:(NSMenu*)menu
118 atIndex:(NSInteger)index
119 fromModel:(ui::MenuModel*)model {
120 // Non-button item types should be built as normal items, with the exception
121 // of the extensions overflow menu.
122 int command_id = model->GetCommandIdAt(index);
123 if (model->GetTypeAt(index) != ui::MenuModel::TYPE_BUTTON_ITEM &&
124 command_id != IDC_EXTENSIONS_OVERFLOW_MENU) {
125 [super addItemToMenu:menu
131 // Handle the special-cased menu items.
132 base::scoped_nsobject<NSMenuItem> customItem(
133 [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]);
134 MenuTrackedRootView* view = nil;
135 switch (command_id) {
136 case IDC_EXTENSIONS_OVERFLOW_MENU: {
137 view = [buttonViewController_ toolbarActionsOverflowItem];
138 BrowserActionsContainerView* containerView =
139 [buttonViewController_ overflowActionsContainerView];
141 // The overflow browser actions container can't function properly without
142 // a main counterpart, so if the browser window hasn't initialized, abort.
143 // (This is fine because we re-populate the wrench menu each time before
145 if (!browser_->window())
148 BrowserActionsController* mainController =
149 [[[BrowserWindowController browserWindowControllerForWindow:browser_->
150 window()->GetNativeWindow()] toolbarController]
151 browserActionsController];
152 browserActionsController_.reset(
153 [[BrowserActionsController alloc]
154 initWithBrowser:browser_
155 containerView:containerView
156 mainController:mainController]);
158 // Set the origins and preferred size for the container.
159 gfx::Size preferredSize = [browserActionsController_ preferredSize];
160 NSSize preferredNSSize = NSMakeSize(preferredSize.width(),
161 preferredSize.height());
162 // View hierarchy is as follows (from parent > child):
163 // |view| > |anonymous view| > containerView. We have to set the origin
164 // and size of each for it display properly.
165 [view setFrameSize:preferredNSSize];
166 [view setFrameOrigin:NSMakePoint(0, 0)];
167 [[containerView superview] setFrameSize:preferredNSSize];
168 [[containerView superview] setFrameOrigin:NSMakePoint(0, 0)];
169 [containerView setFrameSize:preferredNSSize];
170 [containerView setFrameOrigin:NSMakePoint(0, 0)];
171 [browserActionsController_ update];
175 view = [buttonViewController_ editItem];
178 view = [buttonViewController_ zoomItem];
185 [customItem setView:view];
186 [view setMenuItem:customItem];
187 [self adjustPositioning];
188 [menu insertItem:customItem.get() atIndex:index];
191 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
192 const BOOL enabled = [super validateUserInterfaceItem:item];
194 NSMenuItem* menuItem = (id)item;
195 ui::MenuModel* model =
196 static_cast<ui::MenuModel*>(
197 [[menuItem representedObject] pointerValue]);
199 // The section headers in the recent tabs submenu should be bold and black if
200 // a font list is specified for the items (bold is already applied in the
201 // |MenuController| as the font list returned by |GetLabelFontListAt| is
203 if (model && model == [self recentTabsMenuModel]) {
204 if (model->GetLabelFontListAt([item tag])) {
205 DCHECK([menuItem attributedTitle]);
206 base::scoped_nsobject<NSMutableAttributedString> title(
207 [[NSMutableAttributedString alloc]
208 initWithAttributedString:[menuItem attributedTitle]]);
209 [title addAttribute:NSForegroundColorAttributeName
210 value:[NSColor blackColor]
211 range:NSMakeRange(0, [title length])];
212 [menuItem setAttributedTitle:title.get()];
214 // Not a section header. Add a tooltip with the title and the URL.
216 base::string16 title;
217 if ([self recentTabsMenuModel]->GetURLAndTitleForItemAtIndex(
218 [item tag], &url, &title)) {
219 [menuItem setToolTip:
220 cocoa_l10n_util::TooltipForURLAndTitle(
221 base::SysUTF8ToNSString(url), base::SysUTF16ToNSString(title))];
229 - (NSMenu*)bookmarkSubMenu {
230 NSString* title = l10n_util::GetNSStringWithFixup(IDS_BOOKMARKS_MENU);
231 return [[[self menu] itemWithTitle:title] submenu];
234 - (void)updateBookmarkSubMenu {
235 NSMenu* bookmarkMenu = [self bookmarkSubMenu];
236 DCHECK(bookmarkMenu);
238 bookmarkMenuBridge_.reset(
239 new BookmarkMenuBridge([self wrenchMenuModel]->browser()->profile(),
243 - (void)menuWillOpen:(NSMenu*)menu {
244 [super menuWillOpen:menu];
246 NSString* title = base::SysUTF16ToNSString(
247 [self wrenchMenuModel]->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY));
248 [[[buttonViewController_ zoomItem] viewWithTag:IDC_ZOOM_PERCENT_DISPLAY]
250 content::RecordAction(UserMetricsAction("ShowAppMenu"));
252 NSImage* icon = [self wrenchMenuModel]->browser()->window()->IsFullscreen() ?
253 [NSImage imageNamed:NSImageNameExitFullScreenTemplate] :
254 [NSImage imageNamed:NSImageNameEnterFullScreenTemplate];
255 [[buttonViewController_ zoomFullScreen] setImage:icon];
258 - (void)menuNeedsUpdate:(NSMenu*)menu {
259 // First empty out the menu and create a new model.
260 [self removeAllItems:menu];
263 // Create a new menu, which cannot be swapped because the tracking is about to
264 // start, so simply copy the items.
265 NSMenu* newMenu = [self menuFromModel:model_];
266 NSArray* itemArray = [newMenu itemArray];
267 [self removeAllItems:newMenu];
268 for (NSMenuItem* item in itemArray) {
272 [self updateRecentTabsSubmenu];
273 [self updateBookmarkSubMenu];
276 // Used to dispatch commands from the Wrench menu. The custom items within the
277 // menu cannot be hooked up directly to First Responder because the window in
278 // which the controls reside is not the BrowserWindowController, but a
279 // NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system.
280 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
281 NSInteger tag = [sender tag];
282 if (sender == [buttonViewController_ zoomPlus] ||
283 sender == [buttonViewController_ zoomMinus]) {
284 // Do a direct dispatch rather than scheduling on the outermost run loop,
285 // which would not get hit until after the menu had closed.
286 [self performCommandDispatch:[NSNumber numberWithInt:tag]];
288 // The zoom buttons should not close the menu if opened sticky.
289 if ([sender respondsToSelector:@selector(isTracking)] &&
290 [sender performSelector:@selector(isTracking)]) {
291 [menu_ cancelTracking];
294 // The custom views within the Wrench menu are abnormal and keep the menu
295 // open after a target-action. Close the menu manually.
296 [menu_ cancelTracking];
298 // Executing certain commands from the nested run loop of the menu can lead
299 // to wonky behavior (e.g. http://crbug.com/49716). To avoid this, schedule
300 // the dispatch on the outermost run loop.
301 [self performSelector:@selector(performCommandDispatch:)
302 withObject:[NSNumber numberWithInt:tag]
307 // Used to perform the actual dispatch on the outermost runloop.
308 - (void)performCommandDispatch:(NSNumber*)tag {
309 [self wrenchMenuModel]->ExecuteCommand([tag intValue], 0);
312 - (WrenchMenuModel*)wrenchMenuModel {
313 // Don't use |wrenchMenuModel_| so that a test can override the generic one.
314 return static_cast<WrenchMenuModel*>(model_);
317 - (void)updateRecentTabsSubmenu {
318 ui::MenuModel* model = [self recentTabsMenuModel];
320 recentTabsMenuModelDelegate_.reset(
321 new RecentTabsMenuModelDelegate(model, [self recentTabsSubmenu]));
325 - (void)createModel {
326 recentTabsMenuModelDelegate_.reset();
327 wrenchMenuModel_.reset(
328 new WrenchMenuModel(acceleratorDelegate_.get(), browser_));
329 [self setModel:wrenchMenuModel_.get()];
331 buttonViewController_.reset(
332 [[WrenchMenuButtonViewController alloc] initWithController:self]);
333 [buttonViewController_ view];
336 // Fit the localized strings into the Cut/Copy/Paste control, then resize the
337 // whole menu item accordingly.
338 - (void)adjustPositioning {
339 const CGFloat kButtonPadding = 12;
342 // Go through the three buttons from right-to-left, adjusting the size to fit
343 // the localized strings while keeping them all aligned on their horizontal
345 NSButton* views[] = {
346 [buttonViewController_ editPaste],
347 [buttonViewController_ editCopy],
348 [buttonViewController_ editCut]
350 for (size_t i = 0; i < arraysize(views); ++i) {
351 NSButton* button = views[i];
352 CGFloat originalWidth = NSWidth([button frame]);
354 // Do not let |-sizeToFit| change the height of the button.
355 NSSize size = [button frame].size;
357 size.width = [button frame].size.width + kButtonPadding;
358 [button setFrameSize:size];
360 CGFloat newWidth = size.width;
361 delta += newWidth - originalWidth;
363 NSRect frame = [button frame];
364 frame.origin.x -= delta;
365 [button setFrame:frame];
368 // Resize the menu item by the total amound the buttons changed so that the
369 // spacing between the buttons and the title remains the same.
370 NSRect itemFrame = [[buttonViewController_ editItem] frame];
371 itemFrame.size.width += delta;
372 [[buttonViewController_ editItem] setFrame:itemFrame];
374 // Also resize the superview of the buttons, which is an NSView used to slide
375 // when the item title is too big and GTM resizes it.
376 NSRect parentFrame = [[[buttonViewController_ editCut] superview] frame];
377 parentFrame.size.width += delta;
378 parentFrame.origin.x -= delta;
379 [[[buttonViewController_ editCut] superview] setFrame:parentFrame];
382 - (NSButton*)zoomDisplay {
383 return [buttonViewController_ zoomDisplay];
386 // -[NSMenu removeAllItems] is only available on 10.6+.
387 - (void)removeAllItems:(NSMenu*)menu {
388 while ([menu numberOfItems]) {
389 [menu removeItemAtIndex:0];
393 - (NSMenu*)recentTabsSubmenu {
394 NSString* title = l10n_util::GetNSStringWithFixup(IDS_RECENT_TABS_MENU);
395 return [[[self menu] itemWithTitle:title] submenu];
398 // The recent tabs menu model is recognized by the existence of either the
399 // kRecentlyClosedHeaderCommandId or the kDisabledRecentlyClosedHeaderCommandId.
400 - (RecentTabsSubMenuModel*)recentTabsMenuModel {
402 // Start searching at the wrench menu model level, |model| will be updated
403 // only if the command we're looking for is found in one of the [sub]menus.
404 ui::MenuModel* model = [self wrenchMenuModel];
405 if (ui::MenuModel::GetModelAndIndexForCommandId(
406 RecentTabsSubMenuModel::kRecentlyClosedHeaderCommandId, &model,
408 return static_cast<RecentTabsSubMenuModel*>(model);
410 if (ui::MenuModel::GetModelAndIndexForCommandId(
411 RecentTabsSubMenuModel::kDisabledRecentlyClosedHeaderCommandId,
413 return static_cast<RecentTabsSubMenuModel*>(model);
418 // This overrdies the parent class to return a custom width for recent tabs
420 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
421 modelIndex:(int)modelIndex {
422 RecentTabsSubMenuModel* recentTabsMenuModel = [self recentTabsMenuModel];
423 if (recentTabsMenuModel && recentTabsMenuModel == model) {
424 return recentTabsMenuModel->GetMaxWidthForItemAtIndex(modelIndex);
429 @end // @implementation WrenchMenuController
431 ////////////////////////////////////////////////////////////////////////////////
433 @implementation WrenchMenuButtonViewController
435 @synthesize editItem = editItem_;
436 @synthesize editCut = editCut_;
437 @synthesize editCopy = editCopy_;
438 @synthesize editPaste = editPaste_;
439 @synthesize zoomItem = zoomItem_;
440 @synthesize zoomPlus = zoomPlus_;
441 @synthesize zoomDisplay = zoomDisplay_;
442 @synthesize zoomMinus = zoomMinus_;
443 @synthesize zoomFullScreen = zoomFullScreen_;
444 @synthesize toolbarActionsOverflowItem = toolbarActionsOverflowItem_;
445 @synthesize overflowActionsContainerView = overflowActionsContainerView_;
447 - (id)initWithController:(WrenchMenuController*)controller {
448 if ((self = [super initWithNibName:@"WrenchMenu"
449 bundle:base::mac::FrameworkBundle()])) {
450 controller_ = controller;
455 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
456 [controller_ dispatchWrenchMenuCommand:sender];
459 @end // @implementation WrenchMenuButtonViewController