Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / wrench_menu / wrench_menu_controller.mm
blob1c479935798058c1be108f49a7ac5de49b92e896
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/strings/string16.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "chrome/app/chrome_command_ids.h"
12 #import "chrome/browser/app_controller_mac.h"
13 #include "chrome/browser/profiles/profile.h"
14 #include "chrome/browser/ui/browser.h"
15 #include "chrome/browser/ui/browser_window.h"
16 #import "chrome/browser/ui/cocoa/accelerators_cocoa.h"
17 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
18 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
19 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
20 #import "chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.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/l10n_util.h"
24 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
25 #import "chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h"
26 #import "chrome/browser/ui/cocoa/wrench_menu/recent_tabs_menu_model_delegate.h"
27 #include "chrome/browser/ui/toolbar/recent_tabs_sub_menu_model.h"
28 #include "chrome/browser/ui/toolbar/wrench_menu_model.h"
29 #include "chrome/grit/generated_resources.h"
30 #include "components/ui/zoom/zoom_event_manager.h"
31 #include "content/public/browser/user_metrics.h"
32 #include "ui/base/l10n/l10n_util.h"
33 #include "ui/base/models/menu_model.h"
34 #include "ui/gfx/geometry/size.h"
36 namespace {
37 // Padding amounts on the left/right of a custom menu item (like the browser
38 // actions overflow container).
39 const int kLeftPadding = 16;
40 const int kRightPadding = 10;
43 namespace wrench_menu_controller {
44 const CGFloat kWrenchBubblePointOffsetY = 6;
47 using base::UserMetricsAction;
49 @interface WrenchMenuController (Private)
50 - (void)createModel;
51 - (void)adjustPositioning;
52 - (void)performCommandDispatch:(NSNumber*)tag;
53 - (NSButton*)zoomDisplay;
54 - (void)menu:(NSMenu*)menu willHighlightItem:(NSMenuItem*)item;
55 - (void)removeAllItems:(NSMenu*)menu;
56 - (NSMenu*)recentTabsSubmenu;
57 - (RecentTabsSubMenuModel*)recentTabsMenuModel;
58 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
59                  modelIndex:(int)modelIndex;
60 @end
62 namespace WrenchMenuControllerInternal {
64 // A C++ delegate that handles the accelerators in the wrench menu.
65 class AcceleratorDelegate : public ui::AcceleratorProvider {
66  public:
67   bool GetAcceleratorForCommandId(int command_id,
68                                   ui::Accelerator* out_accelerator) override {
69     AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
70     const ui::Accelerator* accelerator =
71         keymap->GetAcceleratorForCommand(command_id);
72     if (!accelerator)
73       return false;
74     *out_accelerator = *accelerator;
75     return true;
76   }
79 class ZoomLevelObserver {
80  public:
81   ZoomLevelObserver(WrenchMenuController* controller,
82                     ui_zoom::ZoomEventManager* manager)
83       : controller_(controller) {
84     subscription_ = manager->AddZoomLevelChangedCallback(
85         base::Bind(&ZoomLevelObserver::OnZoomLevelChanged,
86                    base::Unretained(this)));
87   }
89   ~ZoomLevelObserver() {}
91  private:
92   void OnZoomLevelChanged(const content::HostZoomMap::ZoomLevelChange& change) {
93     WrenchMenuModel* wrenchMenuModel = [controller_ wrenchMenuModel];
94     wrenchMenuModel->UpdateZoomControls();
95     const base::string16 level =
96         wrenchMenuModel->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY);
97     [[controller_ zoomDisplay] setTitle:SysUTF16ToNSString(level)];
98   }
100   scoped_ptr<content::HostZoomMap::Subscription> subscription_;
102   WrenchMenuController* controller_;  // Weak; owns this.
104   DISALLOW_COPY_AND_ASSIGN(ZoomLevelObserver);
107 }  // namespace WrenchMenuControllerInternal
109 @implementation WrenchMenuController
111 - (id)initWithBrowser:(Browser*)browser {
112   if ((self = [super init])) {
113     browser_ = browser;
114     observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(
115         self,
116         ui_zoom::ZoomEventManager::GetForBrowserContext(browser->profile())));
117     acceleratorDelegate_.reset(
118         new WrenchMenuControllerInternal::AcceleratorDelegate());
119     [self createModel];
120   }
121   return self;
124 - (void)dealloc {
125   [self browserWillBeDestroyed];
126   [super dealloc];
129 - (void)browserWillBeDestroyed {
130   // This method indicates imminent destruction. Destroy owned objects that hold
131   // a weak Browser*, or pass this call onto reference counted objects.
132   recentTabsMenuModelDelegate_.reset();
133   [self setModel:nullptr];
134   wrenchMenuModel_.reset();
135   buttonViewController_.reset();
137   [browserActionsController_ browserWillBeDestroyed];
139   browser_ = nullptr;
142 - (void)addItemToMenu:(NSMenu*)menu
143               atIndex:(NSInteger)index
144             fromModel:(ui::MenuModel*)model {
145   // Non-button item types should be built as normal items, with the exception
146   // of the extensions overflow menu.
147   int command_id = model->GetCommandIdAt(index);
148   if (model->GetTypeAt(index) != ui::MenuModel::TYPE_BUTTON_ITEM &&
149       command_id != IDC_EXTENSIONS_OVERFLOW_MENU) {
150     [super addItemToMenu:menu
151                  atIndex:index
152                fromModel:model];
153     return;
154   }
156   // Handle the special-cased menu items.
157   base::scoped_nsobject<NSMenuItem> customItem(
158       [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]);
159   MenuTrackedRootView* view = nil;
160   switch (command_id) {
161     case IDC_EXTENSIONS_OVERFLOW_MENU: {
162       browserActionsMenuItem_ = customItem.get();
163       view = [buttonViewController_ toolbarActionsOverflowItem];
164       BrowserActionsContainerView* containerView =
165           [buttonViewController_ overflowActionsContainerView];
167       // The overflow browser actions container can't function properly without
168       // a main counterpart, so if the browser window hasn't initialized, abort.
169       // (This is fine because we re-populate the wrench menu each time before
170       // we show it.)
171       if (!browser_->window())
172         break;
174       BrowserActionsController* mainController =
175           [[[BrowserWindowController browserWindowControllerForWindow:browser_->
176               window()->GetNativeWindow()] toolbarController]
177                   browserActionsController];
178       browserActionsController_.reset(
179           [[BrowserActionsController alloc]
180               initWithBrowser:browser_
181                 containerView:containerView
182                mainController:mainController]);
183       break;
184     }
185     case IDC_EDIT_MENU:
186       view = [buttonViewController_ editItem];
187       break;
188     case IDC_ZOOM_MENU:
189       view = [buttonViewController_ zoomItem];
190       break;
191     default:
192       NOTREACHED();
193       break;
194   }
195   DCHECK(view);
196   [customItem setView:view];
197   [view setMenuItem:customItem];
198   [self adjustPositioning];
199   [menu insertItem:customItem.get() atIndex:index];
202 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
203   const BOOL enabled = [super validateUserInterfaceItem:item];
205   NSMenuItem* menuItem = (id)item;
206   ui::MenuModel* model =
207       static_cast<ui::MenuModel*>(
208           [[menuItem representedObject] pointerValue]);
210   // The section headers in the recent tabs submenu should be bold and black if
211   // a font list is specified for the items (bold is already applied in the
212   // |MenuController| as the font list returned by |GetLabelFontListAt| is
213   // bold).
214   if (model && model == [self recentTabsMenuModel]) {
215     if (model->GetLabelFontListAt([item tag])) {
216       DCHECK([menuItem attributedTitle]);
217       base::scoped_nsobject<NSMutableAttributedString> title(
218           [[NSMutableAttributedString alloc]
219               initWithAttributedString:[menuItem attributedTitle]]);
220       [title addAttribute:NSForegroundColorAttributeName
221                     value:[NSColor blackColor]
222                     range:NSMakeRange(0, [title length])];
223       [menuItem setAttributedTitle:title.get()];
224     } else {
225       // Not a section header. Add a tooltip with the title and the URL.
226       std::string url;
227       base::string16 title;
228       if ([self recentTabsMenuModel]->GetURLAndTitleForItemAtIndex(
229               [item tag], &url, &title)) {
230         [menuItem setToolTip:
231             cocoa_l10n_util::TooltipForURLAndTitle(
232                 base::SysUTF8ToNSString(url), base::SysUTF16ToNSString(title))];
233        }
234     }
235   }
237   return enabled;
240 - (NSMenu*)bookmarkSubMenu {
241   NSString* title = l10n_util::GetNSStringWithFixup(IDS_BOOKMARKS_MENU);
242   return [[[self menu] itemWithTitle:title] submenu];
245 - (void)updateBookmarkSubMenu {
246   NSMenu* bookmarkMenu = [self bookmarkSubMenu];
247   DCHECK(bookmarkMenu);
249   bookmarkMenuBridge_.reset(
250       new BookmarkMenuBridge([self wrenchMenuModel]->browser()->profile(),
251                              bookmarkMenu));
254 - (void)updateBrowserActionsSubmenu {
255   MenuTrackedRootView* view =
256       [buttonViewController_ toolbarActionsOverflowItem];
257   BrowserActionsContainerView* containerView =
258       [buttonViewController_ overflowActionsContainerView];
260   // Find the preferred container size for the menu width.
261   int menuWidth = [[self menu] size].width;
262   int maxContainerWidth = menuWidth - kLeftPadding - kRightPadding;
263   // Don't let the menu change sizes on us. (We lift this restriction every time
264   // the menu updates, so if something changes, this won't leave us with an
265   // awkward size.)
266   [[self menu] setMinimumWidth:menuWidth];
267   gfx::Size preferredContainerSize =
268       [browserActionsController_ sizeForOverflowWidth:maxContainerWidth];
270   // Set the origins and preferred size for the container.
271   // View hierarchy is as follows (from parent > child):
272   // |view| > |anonymous view| > containerView. We have to set the origin
273   // and size of each for it display properly.
274   // The parent views each have a size of the full width of the menu, so we can
275   // properly position the container.
276   NSSize parentSize = NSMakeSize(menuWidth, preferredContainerSize.height());
277   [view setFrameSize:parentSize];
278   [[containerView superview] setFrameSize:parentSize];
280   // The container view gets its preferred size.
281   [containerView setFrameSize:NSMakeSize(preferredContainerSize.width(),
282                                          preferredContainerSize.height())];
283   [browserActionsController_ update];
285   [view setFrameOrigin:NSZeroPoint];
286   [[containerView superview] setFrameOrigin:NSZeroPoint];
287   [containerView setFrameOrigin:NSMakePoint(kLeftPadding, 0)];
290 - (void)menuWillOpen:(NSMenu*)menu {
291   [super menuWillOpen:menu];
293   NSString* title = base::SysUTF16ToNSString(
294       [self wrenchMenuModel]->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY));
295   [[[buttonViewController_ zoomItem] viewWithTag:IDC_ZOOM_PERCENT_DISPLAY]
296       setTitle:title];
297   content::RecordAction(UserMetricsAction("ShowAppMenu"));
299   NSImage* icon = [self wrenchMenuModel]->browser()->window()->IsFullscreen() ?
300       [NSImage imageNamed:NSImageNameExitFullScreenTemplate] :
301           [NSImage imageNamed:NSImageNameEnterFullScreenTemplate];
302   [[buttonViewController_ zoomFullScreen] setImage:icon];
305 - (void)menuNeedsUpdate:(NSMenu*)menu {
306   // First empty out the menu and create a new model.
307   [self removeAllItems:menu];
308   [self createModel];
309   [menu setMinimumWidth:0];
311   // Create a new menu, which cannot be swapped because the tracking is about to
312   // start, so simply copy the items.
313   NSMenu* newMenu = [self menuFromModel:model_];
314   NSArray* itemArray = [newMenu itemArray];
315   [self removeAllItems:newMenu];
316   for (NSMenuItem* item in itemArray) {
317     [menu addItem:item];
318   }
320   [self updateRecentTabsSubmenu];
321   [self updateBookmarkSubMenu];
322   [self updateBrowserActionsSubmenu];
325 // Used to dispatch commands from the Wrench menu. The custom items within the
326 // menu cannot be hooked up directly to First Responder because the window in
327 // which the controls reside is not the BrowserWindowController, but a
328 // NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system.
329 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
330   NSInteger tag = [sender tag];
331   if (sender == [buttonViewController_ zoomPlus] ||
332       sender == [buttonViewController_ zoomMinus]) {
333     // Do a direct dispatch rather than scheduling on the outermost run loop,
334     // which would not get hit until after the menu had closed.
335     [self performCommandDispatch:[NSNumber numberWithInt:tag]];
337     // The zoom buttons should not close the menu if opened sticky.
338     if ([sender respondsToSelector:@selector(isTracking)] &&
339         [sender performSelector:@selector(isTracking)]) {
340       [menu_ cancelTracking];
341     }
342   } else {
343     // The custom views within the Wrench menu are abnormal and keep the menu
344     // open after a target-action.  Close the menu manually.
345     [menu_ cancelTracking];
347     // Executing certain commands from the nested run loop of the menu can lead
348     // to wonky behavior (e.g. http://crbug.com/49716). To avoid this, schedule
349     // the dispatch on the outermost run loop.
350     [self performSelector:@selector(performCommandDispatch:)
351                withObject:[NSNumber numberWithInt:tag]
352                afterDelay:0.0];
353   }
356 // Used to perform the actual dispatch on the outermost runloop.
357 - (void)performCommandDispatch:(NSNumber*)tag {
358   [self wrenchMenuModel]->ExecuteCommand([tag intValue], 0);
361 - (WrenchMenuModel*)wrenchMenuModel {
362   // Don't use |wrenchMenuModel_| so that a test can override the generic one.
363   return static_cast<WrenchMenuModel*>(model_);
366 - (void)updateRecentTabsSubmenu {
367   ui::MenuModel* model = [self recentTabsMenuModel];
368   if (model) {
369     recentTabsMenuModelDelegate_.reset(
370         new RecentTabsMenuModelDelegate(model, [self recentTabsSubmenu]));
371   }
374 - (BrowserActionsController*)browserActionsController {
375   return browserActionsController_.get();
378 - (void)createModel {
379   DCHECK(browser_);
380   recentTabsMenuModelDelegate_.reset();
381   wrenchMenuModel_.reset(
382       new WrenchMenuModel(acceleratorDelegate_.get(), browser_));
383   [self setModel:wrenchMenuModel_.get()];
385   buttonViewController_.reset(
386       [[WrenchMenuButtonViewController alloc] initWithController:self]);
387   [buttonViewController_ view];
389   // See comment in containerSuperviewFrameChanged:.
390   NSView* containerSuperview =
391       [[buttonViewController_ overflowActionsContainerView] superview];
392   [containerSuperview setPostsFrameChangedNotifications:YES];
395 // Fit the localized strings into the Cut/Copy/Paste control, then resize the
396 // whole menu item accordingly.
397 - (void)adjustPositioning {
398   const CGFloat kButtonPadding = 12;
399   CGFloat delta = 0;
401   // Go through the three buttons from right-to-left, adjusting the size to fit
402   // the localized strings while keeping them all aligned on their horizontal
403   // edges.
404   NSButton* views[] = {
405       [buttonViewController_ editPaste],
406       [buttonViewController_ editCopy],
407       [buttonViewController_ editCut]
408   };
409   for (size_t i = 0; i < arraysize(views); ++i) {
410     NSButton* button = views[i];
411     CGFloat originalWidth = NSWidth([button frame]);
413     // Do not let |-sizeToFit| change the height of the button.
414     NSSize size = [button frame].size;
415     [button sizeToFit];
416     size.width = [button frame].size.width + kButtonPadding;
417     [button setFrameSize:size];
419     CGFloat newWidth = size.width;
420     delta += newWidth - originalWidth;
422     NSRect frame = [button frame];
423     frame.origin.x -= delta;
424     [button setFrame:frame];
425   }
427   // Resize the menu item by the total amound the buttons changed so that the
428   // spacing between the buttons and the title remains the same.
429   NSRect itemFrame = [[buttonViewController_ editItem] frame];
430   itemFrame.size.width += delta;
431   [[buttonViewController_ editItem] setFrame:itemFrame];
433   // Also resize the superview of the buttons, which is an NSView used to slide
434   // when the item title is too big and GTM resizes it.
435   NSRect parentFrame = [[[buttonViewController_ editCut] superview] frame];
436   parentFrame.size.width += delta;
437   parentFrame.origin.x -= delta;
438   [[[buttonViewController_ editCut] superview] setFrame:parentFrame];
441 - (NSButton*)zoomDisplay {
442   return [buttonViewController_ zoomDisplay];
445 - (void)menu:(NSMenu*)menu willHighlightItem:(NSMenuItem*)item {
446   if (browserActionsController_.get()) {
447     [browserActionsController_ setFocusedInOverflow:
448         (item == browserActionsMenuItem_)];
449   }
452 // -[NSMenu removeAllItems] is only available on 10.6+.
453 - (void)removeAllItems:(NSMenu*)menu {
454   while ([menu numberOfItems]) {
455     [menu removeItemAtIndex:0];
456   }
459 - (NSMenu*)recentTabsSubmenu {
460   NSString* title = l10n_util::GetNSStringWithFixup(IDS_RECENT_TABS_MENU);
461   return [[[self menu] itemWithTitle:title] submenu];
464 // The recent tabs menu model is recognized by the existence of either the
465 // kRecentlyClosedHeaderCommandId or the kDisabledRecentlyClosedHeaderCommandId.
466 - (RecentTabsSubMenuModel*)recentTabsMenuModel {
467   int index = 0;
468   // Start searching at the wrench menu model level, |model| will be updated
469   // only if the command we're looking for is found in one of the [sub]menus.
470   ui::MenuModel* model = [self wrenchMenuModel];
471   if (ui::MenuModel::GetModelAndIndexForCommandId(
472           RecentTabsSubMenuModel::kRecentlyClosedHeaderCommandId, &model,
473           &index)) {
474     return static_cast<RecentTabsSubMenuModel*>(model);
475   }
476   if (ui::MenuModel::GetModelAndIndexForCommandId(
477           RecentTabsSubMenuModel::kDisabledRecentlyClosedHeaderCommandId,
478           &model, &index)) {
479     return static_cast<RecentTabsSubMenuModel*>(model);
480   }
481   return NULL;
484 // This overrdies the parent class to return a custom width for recent tabs
485 // menu.
486 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
487                  modelIndex:(int)modelIndex {
488   RecentTabsSubMenuModel* recentTabsMenuModel = [self recentTabsMenuModel];
489   if (recentTabsMenuModel && recentTabsMenuModel == model) {
490     return recentTabsMenuModel->GetMaxWidthForItemAtIndex(modelIndex);
491   }
492   return -1;
495 @end  // @implementation WrenchMenuController
497 ////////////////////////////////////////////////////////////////////////////////
499 @interface WrenchMenuButtonViewController ()
500 - (void)containerSuperviewFrameChanged:(NSNotification*)notification;
501 @end
503 @implementation WrenchMenuButtonViewController
505 @synthesize editItem = editItem_;
506 @synthesize editCut = editCut_;
507 @synthesize editCopy = editCopy_;
508 @synthesize editPaste = editPaste_;
509 @synthesize zoomItem = zoomItem_;
510 @synthesize zoomPlus = zoomPlus_;
511 @synthesize zoomDisplay = zoomDisplay_;
512 @synthesize zoomMinus = zoomMinus_;
513 @synthesize zoomFullScreen = zoomFullScreen_;
514 @synthesize toolbarActionsOverflowItem = toolbarActionsOverflowItem_;
515 @synthesize overflowActionsContainerView = overflowActionsContainerView_;
517 - (id)initWithController:(WrenchMenuController*)controller {
518   if ((self = [super initWithNibName:@"WrenchMenu"
519                               bundle:base::mac::FrameworkBundle()])) {
520     controller_ = controller;
521     [[NSNotificationCenter defaultCenter]
522         addObserver:self
523            selector:@selector(containerSuperviewFrameChanged:)
524                name:NSViewFrameDidChangeNotification
525              object:[overflowActionsContainerView_ superview]];
526   }
527   return self;
530 - (void)dealloc {
531   [[NSNotificationCenter defaultCenter] removeObserver:self];
532   [super dealloc];
535 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
536   [controller_ dispatchWrenchMenuCommand:sender];
539 - (void)containerSuperviewFrameChanged:(NSNotification*)notification {
540   // AppKit menus were probably never designed with a view like the browser
541   // actions container in mind, and, as a result, we come across a few oddities.
542   // One of these is that the container's superview will, on some versions of
543   // OSX, change frame position sometime after the the menu begins tracking
544   // (and thus, after all our ability to adjust it normally). Throw in the
545   // towel, and simply don't let the frame move from where it's supposed to be.
546   // TODO(devlin): Yet another Cocoa hack. It'd be good to find a workaround,
547   // but unlikely unless we replace the Cocoa menu implementation.
548   NSView* containerSuperview = [overflowActionsContainerView_ superview];
549   if (NSMinX([containerSuperview frame]) != 0)
550     [containerSuperview setFrameOrigin:NSZeroPoint];
553 @end  // @implementation WrenchMenuButtonViewController