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/encoding_menu_controller_delegate_mac.h"
21 #import "chrome/browser/ui/cocoa/l10n_util.h"
22 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
23 #import "chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h"
24 #import "chrome/browser/ui/cocoa/wrench_menu/recent_tabs_menu_model_delegate.h"
25 #include "chrome/browser/ui/toolbar/recent_tabs_sub_menu_model.h"
26 #include "chrome/browser/ui/toolbar/wrench_menu_model.h"
27 #include "chrome/grit/generated_resources.h"
28 #include "content/public/browser/user_metrics.h"
29 #include "ui/base/l10n/l10n_util.h"
30 #include "ui/base/models/menu_model.h"
32 namespace wrench_menu_controller {
33 const CGFloat kWrenchBubblePointOffsetY = 6;
36 using base::UserMetricsAction;
37 using content::HostZoomMap;
39 @interface WrenchMenuController (Private)
41 - (void)adjustPositioning;
42 - (void)performCommandDispatch:(NSNumber*)tag;
43 - (NSButton*)zoomDisplay;
44 - (void)removeAllItems:(NSMenu*)menu;
45 - (NSMenu*)recentTabsSubmenu;
46 - (RecentTabsSubMenuModel*)recentTabsMenuModel;
47 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
48 modelIndex:(int)modelIndex;
51 namespace WrenchMenuControllerInternal {
53 // A C++ delegate that handles the accelerators in the wrench menu.
54 class AcceleratorDelegate : public ui::AcceleratorProvider {
56 bool GetAcceleratorForCommandId(int command_id,
57 ui::Accelerator* out_accelerator) override {
58 AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
59 const ui::Accelerator* accelerator =
60 keymap->GetAcceleratorForCommand(command_id);
63 *out_accelerator = *accelerator;
68 class ZoomLevelObserver {
70 ZoomLevelObserver(WrenchMenuController* controller,
71 content::HostZoomMap* map)
72 : controller_(controller),
74 subscription_ = map_->AddZoomLevelChangedCallback(
75 base::Bind(&ZoomLevelObserver::OnZoomLevelChanged,
76 base::Unretained(this)));
79 ~ZoomLevelObserver() {}
82 void OnZoomLevelChanged(const HostZoomMap::ZoomLevelChange& change) {
83 WrenchMenuModel* wrenchMenuModel = [controller_ wrenchMenuModel];
84 wrenchMenuModel->UpdateZoomControls();
85 const base::string16 level =
86 wrenchMenuModel->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY);
87 [[controller_ zoomDisplay] setTitle:SysUTF16ToNSString(level)];
90 scoped_ptr<content::HostZoomMap::Subscription> subscription_;
92 WrenchMenuController* controller_; // Weak; owns this.
93 content::HostZoomMap* map_; // Weak.
95 DISALLOW_COPY_AND_ASSIGN(ZoomLevelObserver);
98 } // namespace WrenchMenuControllerInternal
100 @implementation WrenchMenuController
102 - (id)initWithBrowser:(Browser*)browser {
103 if ((self = [super init])) {
105 observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(
107 content::HostZoomMap::GetDefaultForBrowserContext(browser->profile())));
108 acceleratorDelegate_.reset(
109 new WrenchMenuControllerInternal::AcceleratorDelegate());
115 - (void)addItemToMenu:(NSMenu*)menu
116 atIndex:(NSInteger)index
117 fromModel:(ui::MenuModel*)model {
118 // Non-button item types should be built as normal items.
119 ui::MenuModel::ItemType type = model->GetTypeAt(index);
120 if (type != ui::MenuModel::TYPE_BUTTON_ITEM) {
121 [super addItemToMenu:menu
127 // Handle the special-cased menu items.
128 int command_id = model->GetCommandIdAt(index);
129 base::scoped_nsobject<NSMenuItem> customItem(
130 [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]);
131 MenuTrackedRootView* view;
132 switch (command_id) {
134 view = [buttonViewController_ editItem];
136 [customItem setView:view];
137 [view setMenuItem:customItem];
140 view = [buttonViewController_ zoomItem];
142 [customItem setView:view];
143 [view setMenuItem:customItem];
149 [self adjustPositioning];
150 [menu insertItem:customItem.get() atIndex:index];
153 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
154 const BOOL enabled = [super validateUserInterfaceItem:item];
156 NSMenuItem* menuItem = (id)item;
157 ui::MenuModel* model =
158 static_cast<ui::MenuModel*>(
159 [[menuItem representedObject] pointerValue]);
161 // The section headers in the recent tabs submenu should be bold and black if
162 // a font list is specified for the items (bold is already applied in the
163 // |MenuController| as the font list returned by |GetLabelFontListAt| is
165 if (model && model == [self recentTabsMenuModel]) {
166 if (model->GetLabelFontListAt([item tag])) {
167 DCHECK([menuItem attributedTitle]);
168 base::scoped_nsobject<NSMutableAttributedString> title(
169 [[NSMutableAttributedString alloc]
170 initWithAttributedString:[menuItem attributedTitle]]);
171 [title addAttribute:NSForegroundColorAttributeName
172 value:[NSColor blackColor]
173 range:NSMakeRange(0, [title length])];
174 [menuItem setAttributedTitle:title.get()];
176 // Not a section header. Add a tooltip with the title and the URL.
178 base::string16 title;
179 if ([self recentTabsMenuModel]->GetURLAndTitleForItemAtIndex(
180 [item tag], &url, &title)) {
181 [menuItem setToolTip:
182 cocoa_l10n_util::TooltipForURLAndTitle(
183 base::SysUTF8ToNSString(url), base::SysUTF16ToNSString(title))];
191 - (NSMenu*)bookmarkSubMenu {
192 NSString* title = l10n_util::GetNSStringWithFixup(IDS_BOOKMARKS_MENU);
193 return [[[self menu] itemWithTitle:title] submenu];
196 - (void)updateBookmarkSubMenu {
197 NSMenu* bookmarkMenu = [self bookmarkSubMenu];
198 DCHECK(bookmarkMenu);
200 bookmarkMenuBridge_.reset(
201 new BookmarkMenuBridge([self wrenchMenuModel]->browser()->profile(),
205 - (void)menuWillOpen:(NSMenu*)menu {
206 [super menuWillOpen:menu];
208 NSString* title = base::SysUTF16ToNSString(
209 [self wrenchMenuModel]->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY));
210 [[[buttonViewController_ zoomItem] viewWithTag:IDC_ZOOM_PERCENT_DISPLAY]
212 content::RecordAction(UserMetricsAction("ShowAppMenu"));
214 NSImage* icon = [self wrenchMenuModel]->browser()->window()->IsFullscreen() ?
215 [NSImage imageNamed:NSImageNameExitFullScreenTemplate] :
216 [NSImage imageNamed:NSImageNameEnterFullScreenTemplate];
217 [[buttonViewController_ zoomFullScreen] setImage:icon];
220 - (void)menuNeedsUpdate:(NSMenu*)menu {
221 // First empty out the menu and create a new model.
222 [self removeAllItems:menu];
225 // Create a new menu, which cannot be swapped because the tracking is about to
226 // start, so simply copy the items.
227 NSMenu* newMenu = [self menuFromModel:model_];
228 NSArray* itemArray = [newMenu itemArray];
229 [self removeAllItems:newMenu];
230 for (NSMenuItem* item in itemArray) {
234 [self updateRecentTabsSubmenu];
235 [self updateBookmarkSubMenu];
238 // Used to dispatch commands from the Wrench menu. The custom items within the
239 // menu cannot be hooked up directly to First Responder because the window in
240 // which the controls reside is not the BrowserWindowController, but a
241 // NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system.
242 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
243 NSInteger tag = [sender tag];
244 if (sender == [buttonViewController_ zoomPlus] ||
245 sender == [buttonViewController_ zoomMinus]) {
246 // Do a direct dispatch rather than scheduling on the outermost run loop,
247 // which would not get hit until after the menu had closed.
248 [self performCommandDispatch:[NSNumber numberWithInt:tag]];
250 // The zoom buttons should not close the menu if opened sticky.
251 if ([sender respondsToSelector:@selector(isTracking)] &&
252 [sender performSelector:@selector(isTracking)]) {
253 [menu_ cancelTracking];
256 // The custom views within the Wrench menu are abnormal and keep the menu
257 // open after a target-action. Close the menu manually.
258 [menu_ cancelTracking];
260 // Executing certain commands from the nested run loop of the menu can lead
261 // to wonky behavior (e.g. http://crbug.com/49716). To avoid this, schedule
262 // the dispatch on the outermost run loop.
263 [self performSelector:@selector(performCommandDispatch:)
264 withObject:[NSNumber numberWithInt:tag]
269 // Used to perform the actual dispatch on the outermost runloop.
270 - (void)performCommandDispatch:(NSNumber*)tag {
271 [self wrenchMenuModel]->ExecuteCommand([tag intValue], 0);
274 - (WrenchMenuModel*)wrenchMenuModel {
275 // Don't use |wrenchMenuModel_| so that a test can override the generic one.
276 return static_cast<WrenchMenuModel*>(model_);
279 - (void)updateRecentTabsSubmenu {
280 ui::MenuModel* model = [self recentTabsMenuModel];
282 recentTabsMenuModelDelegate_.reset(
283 new RecentTabsMenuModelDelegate(model, [self recentTabsSubmenu]));
287 - (void)createModel {
288 recentTabsMenuModelDelegate_.reset();
289 wrenchMenuModel_.reset(
290 new WrenchMenuModel(acceleratorDelegate_.get(), browser_));
291 [self setModel:wrenchMenuModel_.get()];
293 buttonViewController_.reset(
294 [[WrenchMenuButtonViewController alloc] initWithController:self]);
295 [buttonViewController_ view];
298 // Fit the localized strings into the Cut/Copy/Paste control, then resize the
299 // whole menu item accordingly.
300 - (void)adjustPositioning {
301 const CGFloat kButtonPadding = 12;
304 // Go through the three buttons from right-to-left, adjusting the size to fit
305 // the localized strings while keeping them all aligned on their horizontal
307 NSButton* views[] = {
308 [buttonViewController_ editPaste],
309 [buttonViewController_ editCopy],
310 [buttonViewController_ editCut]
312 for (size_t i = 0; i < arraysize(views); ++i) {
313 NSButton* button = views[i];
314 CGFloat originalWidth = NSWidth([button frame]);
316 // Do not let |-sizeToFit| change the height of the button.
317 NSSize size = [button frame].size;
319 size.width = [button frame].size.width + kButtonPadding;
320 [button setFrameSize:size];
322 CGFloat newWidth = size.width;
323 delta += newWidth - originalWidth;
325 NSRect frame = [button frame];
326 frame.origin.x -= delta;
327 [button setFrame:frame];
330 // Resize the menu item by the total amound the buttons changed so that the
331 // spacing between the buttons and the title remains the same.
332 NSRect itemFrame = [[buttonViewController_ editItem] frame];
333 itemFrame.size.width += delta;
334 [[buttonViewController_ editItem] setFrame:itemFrame];
336 // Also resize the superview of the buttons, which is an NSView used to slide
337 // when the item title is too big and GTM resizes it.
338 NSRect parentFrame = [[[buttonViewController_ editCut] superview] frame];
339 parentFrame.size.width += delta;
340 parentFrame.origin.x -= delta;
341 [[[buttonViewController_ editCut] superview] setFrame:parentFrame];
344 - (NSButton*)zoomDisplay {
345 return [buttonViewController_ zoomDisplay];
348 // -[NSMenu removeAllItems] is only available on 10.6+.
349 - (void)removeAllItems:(NSMenu*)menu {
350 while ([menu numberOfItems]) {
351 [menu removeItemAtIndex:0];
355 - (NSMenu*)recentTabsSubmenu {
356 NSString* title = l10n_util::GetNSStringWithFixup(IDS_RECENT_TABS_MENU);
357 return [[[self menu] itemWithTitle:title] submenu];
360 // The recent tabs menu model is recognized by the existence of either the
361 // kRecentlyClosedHeaderCommandId or the kDisabledRecentlyClosedHeaderCommandId.
362 - (RecentTabsSubMenuModel*)recentTabsMenuModel {
364 // Start searching at the wrench menu model level, |model| will be updated
365 // only if the command we're looking for is found in one of the [sub]menus.
366 ui::MenuModel* model = [self wrenchMenuModel];
367 if (ui::MenuModel::GetModelAndIndexForCommandId(
368 RecentTabsSubMenuModel::kRecentlyClosedHeaderCommandId, &model,
370 return static_cast<RecentTabsSubMenuModel*>(model);
372 if (ui::MenuModel::GetModelAndIndexForCommandId(
373 RecentTabsSubMenuModel::kDisabledRecentlyClosedHeaderCommandId,
375 return static_cast<RecentTabsSubMenuModel*>(model);
380 // This overrdies the parent class to return a custom width for recent tabs
382 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
383 modelIndex:(int)modelIndex {
384 RecentTabsSubMenuModel* recentTabsMenuModel = [self recentTabsMenuModel];
385 if (recentTabsMenuModel && recentTabsMenuModel == model) {
386 return recentTabsMenuModel->GetMaxWidthForItemAtIndex(modelIndex);
391 @end // @implementation WrenchMenuController
393 ////////////////////////////////////////////////////////////////////////////////
395 @implementation WrenchMenuButtonViewController
397 @synthesize editItem = editItem_;
398 @synthesize editCut = editCut_;
399 @synthesize editCopy = editCopy_;
400 @synthesize editPaste = editPaste_;
401 @synthesize zoomItem = zoomItem_;
402 @synthesize zoomPlus = zoomPlus_;
403 @synthesize zoomDisplay = zoomDisplay_;
404 @synthesize zoomMinus = zoomMinus_;
405 @synthesize zoomFullScreen = zoomFullScreen_;
407 - (id)initWithController:(WrenchMenuController*)controller {
408 if ((self = [super initWithNibName:@"WrenchMenu"
409 bundle:base::mac::FrameworkBundle()])) {
410 controller_ = controller;
415 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
416 [controller_ dispatchWrenchMenuCommand:sender];
419 @end // @implementation WrenchMenuButtonViewController