Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / wrench_menu / wrench_menu_controller.mm
bloba3a29b61cd9200c68a9e5e8f825cb01f14a23834
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();
136   // ZoomLevelObserver holds a subscription to ZoomEventManager, which is
137   // user-data on the BrowserContext. The BrowserContext may be destroyed soon
138   // if Chrome is quitting. In any case, |observer_| should not be needed at
139   // this point.
140   observer_.reset();
142   [browserActionsController_ browserWillBeDestroyed];
144   browser_ = nullptr;
147 - (void)addItemToMenu:(NSMenu*)menu
148               atIndex:(NSInteger)index
149             fromModel:(ui::MenuModel*)model {
150   // Non-button item types should be built as normal items, with the exception
151   // of the extensions overflow menu.
152   int command_id = model->GetCommandIdAt(index);
153   if (model->GetTypeAt(index) != ui::MenuModel::TYPE_BUTTON_ITEM &&
154       command_id != IDC_EXTENSIONS_OVERFLOW_MENU) {
155     [super addItemToMenu:menu
156                  atIndex:index
157                fromModel:model];
158     return;
159   }
161   // Handle the special-cased menu items.
162   base::scoped_nsobject<NSMenuItem> customItem(
163       [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]);
164   MenuTrackedRootView* view = nil;
165   switch (command_id) {
166     case IDC_EXTENSIONS_OVERFLOW_MENU: {
167       browserActionsMenuItem_ = customItem.get();
168       view = [buttonViewController_ toolbarActionsOverflowItem];
169       BrowserActionsContainerView* containerView =
170           [buttonViewController_ overflowActionsContainerView];
172       // The overflow browser actions container can't function properly without
173       // a main counterpart, so if the browser window hasn't initialized, abort.
174       // (This is fine because we re-populate the wrench menu each time before
175       // we show it.)
176       if (!browser_->window())
177         break;
179       BrowserActionsController* mainController =
180           [[[BrowserWindowController browserWindowControllerForWindow:browser_->
181               window()->GetNativeWindow()] toolbarController]
182                   browserActionsController];
183       browserActionsController_.reset(
184           [[BrowserActionsController alloc]
185               initWithBrowser:browser_
186                 containerView:containerView
187                mainController:mainController]);
188       break;
189     }
190     case IDC_EDIT_MENU:
191       view = [buttonViewController_ editItem];
192       break;
193     case IDC_ZOOM_MENU:
194       view = [buttonViewController_ zoomItem];
195       break;
196     default:
197       NOTREACHED();
198       break;
199   }
200   DCHECK(view);
201   [customItem setView:view];
202   [view setMenuItem:customItem];
203   [self adjustPositioning];
204   [menu insertItem:customItem.get() atIndex:index];
207 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
208   const BOOL enabled = [super validateUserInterfaceItem:item];
210   NSMenuItem* menuItem = (id)item;
211   ui::MenuModel* model =
212       static_cast<ui::MenuModel*>(
213           [[menuItem representedObject] pointerValue]);
215   // The section headers in the recent tabs submenu should be bold and black if
216   // a font list is specified for the items (bold is already applied in the
217   // |MenuController| as the font list returned by |GetLabelFontListAt| is
218   // bold).
219   if (model && model == [self recentTabsMenuModel]) {
220     if (model->GetLabelFontListAt([item tag])) {
221       DCHECK([menuItem attributedTitle]);
222       base::scoped_nsobject<NSMutableAttributedString> title(
223           [[NSMutableAttributedString alloc]
224               initWithAttributedString:[menuItem attributedTitle]]);
225       [title addAttribute:NSForegroundColorAttributeName
226                     value:[NSColor blackColor]
227                     range:NSMakeRange(0, [title length])];
228       [menuItem setAttributedTitle:title.get()];
229     } else {
230       // Not a section header. Add a tooltip with the title and the URL.
231       std::string url;
232       base::string16 title;
233       if ([self recentTabsMenuModel]->GetURLAndTitleForItemAtIndex(
234               [item tag], &url, &title)) {
235         [menuItem setToolTip:
236             cocoa_l10n_util::TooltipForURLAndTitle(
237                 base::SysUTF8ToNSString(url), base::SysUTF16ToNSString(title))];
238        }
239     }
240   }
242   return enabled;
245 - (NSMenu*)bookmarkSubMenu {
246   NSString* title = l10n_util::GetNSStringWithFixup(IDS_BOOKMARKS_MENU);
247   return [[[self menu] itemWithTitle:title] submenu];
250 - (void)updateBookmarkSubMenu {
251   NSMenu* bookmarkMenu = [self bookmarkSubMenu];
252   DCHECK(bookmarkMenu);
254   bookmarkMenuBridge_.reset(
255       new BookmarkMenuBridge([self wrenchMenuModel]->browser()->profile(),
256                              bookmarkMenu));
259 - (void)updateBrowserActionsSubmenu {
260   MenuTrackedRootView* view =
261       [buttonViewController_ toolbarActionsOverflowItem];
262   BrowserActionsContainerView* containerView =
263       [buttonViewController_ overflowActionsContainerView];
265   // Find the preferred container size for the menu width.
266   int menuWidth = [[self menu] size].width;
267   int maxContainerWidth = menuWidth - kLeftPadding - kRightPadding;
268   // Don't let the menu change sizes on us. (We lift this restriction every time
269   // the menu updates, so if something changes, this won't leave us with an
270   // awkward size.)
271   [[self menu] setMinimumWidth:menuWidth];
272   gfx::Size preferredContainerSize =
273       [browserActionsController_ sizeForOverflowWidth:maxContainerWidth];
275   // Set the origins and preferred size for the container.
276   // View hierarchy is as follows (from parent > child):
277   // |view| > |anonymous view| > containerView. We have to set the origin
278   // and size of each for it display properly.
279   // The parent views each have a size of the full width of the menu, so we can
280   // properly position the container.
281   NSSize parentSize = NSMakeSize(menuWidth, preferredContainerSize.height());
282   [view setFrameSize:parentSize];
283   [[containerView superview] setFrameSize:parentSize];
285   // The container view gets its preferred size.
286   [containerView setFrameSize:NSMakeSize(preferredContainerSize.width(),
287                                          preferredContainerSize.height())];
288   [browserActionsController_ update];
290   [view setFrameOrigin:NSZeroPoint];
291   [[containerView superview] setFrameOrigin:NSZeroPoint];
292   [containerView setFrameOrigin:NSMakePoint(kLeftPadding, 0)];
295 - (void)menuWillOpen:(NSMenu*)menu {
296   [super menuWillOpen:menu];
298   NSString* title = base::SysUTF16ToNSString(
299       [self wrenchMenuModel]->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY));
300   [[[buttonViewController_ zoomItem] viewWithTag:IDC_ZOOM_PERCENT_DISPLAY]
301       setTitle:title];
302   content::RecordAction(UserMetricsAction("ShowAppMenu"));
304   NSImage* icon = [self wrenchMenuModel]->browser()->window()->IsFullscreen() ?
305       [NSImage imageNamed:NSImageNameExitFullScreenTemplate] :
306           [NSImage imageNamed:NSImageNameEnterFullScreenTemplate];
307   [[buttonViewController_ zoomFullScreen] setImage:icon];
310 - (void)menuNeedsUpdate:(NSMenu*)menu {
311   // First empty out the menu and create a new model.
312   [self removeAllItems:menu];
313   [self createModel];
314   [menu setMinimumWidth:0];
316   // Create a new menu, which cannot be swapped because the tracking is about to
317   // start, so simply copy the items.
318   NSMenu* newMenu = [self menuFromModel:model_];
319   NSArray* itemArray = [newMenu itemArray];
320   [self removeAllItems:newMenu];
321   for (NSMenuItem* item in itemArray) {
322     [menu addItem:item];
323   }
325   [self updateRecentTabsSubmenu];
326   [self updateBookmarkSubMenu];
327   [self updateBrowserActionsSubmenu];
330 // Used to dispatch commands from the Wrench menu. The custom items within the
331 // menu cannot be hooked up directly to First Responder because the window in
332 // which the controls reside is not the BrowserWindowController, but a
333 // NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system.
334 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
335   NSInteger tag = [sender tag];
336   if (sender == [buttonViewController_ zoomPlus] ||
337       sender == [buttonViewController_ zoomMinus]) {
338     // Do a direct dispatch rather than scheduling on the outermost run loop,
339     // which would not get hit until after the menu had closed.
340     [self performCommandDispatch:[NSNumber numberWithInt:tag]];
342     // The zoom buttons should not close the menu if opened sticky.
343     if ([sender respondsToSelector:@selector(isTracking)] &&
344         [sender performSelector:@selector(isTracking)]) {
345       [menu_ cancelTracking];
346     }
347   } else {
348     // The custom views within the Wrench menu are abnormal and keep the menu
349     // open after a target-action.  Close the menu manually.
350     [menu_ cancelTracking];
352     // Executing certain commands from the nested run loop of the menu can lead
353     // to wonky behavior (e.g. http://crbug.com/49716). To avoid this, schedule
354     // the dispatch on the outermost run loop.
355     [self performSelector:@selector(performCommandDispatch:)
356                withObject:[NSNumber numberWithInt:tag]
357                afterDelay:0.0];
358   }
361 // Used to perform the actual dispatch on the outermost runloop.
362 - (void)performCommandDispatch:(NSNumber*)tag {
363   [self wrenchMenuModel]->ExecuteCommand([tag intValue], 0);
366 - (WrenchMenuModel*)wrenchMenuModel {
367   // Don't use |wrenchMenuModel_| so that a test can override the generic one.
368   return static_cast<WrenchMenuModel*>(model_);
371 - (void)updateRecentTabsSubmenu {
372   ui::MenuModel* model = [self recentTabsMenuModel];
373   if (model) {
374     recentTabsMenuModelDelegate_.reset(
375         new RecentTabsMenuModelDelegate(model, [self recentTabsSubmenu]));
376   }
379 - (BrowserActionsController*)browserActionsController {
380   return browserActionsController_.get();
383 - (void)createModel {
384   DCHECK(browser_);
385   recentTabsMenuModelDelegate_.reset();
386   wrenchMenuModel_.reset(
387       new WrenchMenuModel(acceleratorDelegate_.get(), browser_));
388   [self setModel:wrenchMenuModel_.get()];
390   buttonViewController_.reset(
391       [[WrenchMenuButtonViewController alloc] initWithController:self]);
392   [buttonViewController_ view];
394   // See comment in containerSuperviewFrameChanged:.
395   NSView* containerSuperview =
396       [[buttonViewController_ overflowActionsContainerView] superview];
397   [containerSuperview setPostsFrameChangedNotifications:YES];
400 // Fit the localized strings into the Cut/Copy/Paste control, then resize the
401 // whole menu item accordingly.
402 - (void)adjustPositioning {
403   const CGFloat kButtonPadding = 12;
404   CGFloat delta = 0;
406   // Go through the three buttons from right-to-left, adjusting the size to fit
407   // the localized strings while keeping them all aligned on their horizontal
408   // edges.
409   NSButton* views[] = {
410       [buttonViewController_ editPaste],
411       [buttonViewController_ editCopy],
412       [buttonViewController_ editCut]
413   };
414   for (size_t i = 0; i < arraysize(views); ++i) {
415     NSButton* button = views[i];
416     CGFloat originalWidth = NSWidth([button frame]);
418     // Do not let |-sizeToFit| change the height of the button.
419     NSSize size = [button frame].size;
420     [button sizeToFit];
421     size.width = [button frame].size.width + kButtonPadding;
422     [button setFrameSize:size];
424     CGFloat newWidth = size.width;
425     delta += newWidth - originalWidth;
427     NSRect frame = [button frame];
428     frame.origin.x -= delta;
429     [button setFrame:frame];
430   }
432   // Resize the menu item by the total amound the buttons changed so that the
433   // spacing between the buttons and the title remains the same.
434   NSRect itemFrame = [[buttonViewController_ editItem] frame];
435   itemFrame.size.width += delta;
436   [[buttonViewController_ editItem] setFrame:itemFrame];
438   // Also resize the superview of the buttons, which is an NSView used to slide
439   // when the item title is too big and GTM resizes it.
440   NSRect parentFrame = [[[buttonViewController_ editCut] superview] frame];
441   parentFrame.size.width += delta;
442   parentFrame.origin.x -= delta;
443   [[[buttonViewController_ editCut] superview] setFrame:parentFrame];
446 - (NSButton*)zoomDisplay {
447   return [buttonViewController_ zoomDisplay];
450 - (void)menu:(NSMenu*)menu willHighlightItem:(NSMenuItem*)item {
451   if (browserActionsController_.get()) {
452     [browserActionsController_ setFocusedInOverflow:
453         (item == browserActionsMenuItem_)];
454   }
457 // -[NSMenu removeAllItems] is only available on 10.6+.
458 - (void)removeAllItems:(NSMenu*)menu {
459   while ([menu numberOfItems]) {
460     [menu removeItemAtIndex:0];
461   }
464 - (NSMenu*)recentTabsSubmenu {
465   NSString* title = l10n_util::GetNSStringWithFixup(IDS_RECENT_TABS_MENU);
466   return [[[self menu] itemWithTitle:title] submenu];
469 // The recent tabs menu model is recognized by the existence of either the
470 // kRecentlyClosedHeaderCommandId or the kDisabledRecentlyClosedHeaderCommandId.
471 - (RecentTabsSubMenuModel*)recentTabsMenuModel {
472   int index = 0;
473   // Start searching at the wrench menu model level, |model| will be updated
474   // only if the command we're looking for is found in one of the [sub]menus.
475   ui::MenuModel* model = [self wrenchMenuModel];
476   if (ui::MenuModel::GetModelAndIndexForCommandId(
477           RecentTabsSubMenuModel::kRecentlyClosedHeaderCommandId, &model,
478           &index)) {
479     return static_cast<RecentTabsSubMenuModel*>(model);
480   }
481   if (ui::MenuModel::GetModelAndIndexForCommandId(
482           RecentTabsSubMenuModel::kDisabledRecentlyClosedHeaderCommandId,
483           &model, &index)) {
484     return static_cast<RecentTabsSubMenuModel*>(model);
485   }
486   return NULL;
489 // This overrdies the parent class to return a custom width for recent tabs
490 // menu.
491 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
492                  modelIndex:(int)modelIndex {
493   RecentTabsSubMenuModel* recentTabsMenuModel = [self recentTabsMenuModel];
494   if (recentTabsMenuModel && recentTabsMenuModel == model) {
495     return recentTabsMenuModel->GetMaxWidthForItemAtIndex(modelIndex);
496   }
497   return -1;
500 @end  // @implementation WrenchMenuController
502 ////////////////////////////////////////////////////////////////////////////////
504 @interface WrenchMenuButtonViewController ()
505 - (void)containerSuperviewFrameChanged:(NSNotification*)notification;
506 @end
508 @implementation WrenchMenuButtonViewController
510 @synthesize editItem = editItem_;
511 @synthesize editCut = editCut_;
512 @synthesize editCopy = editCopy_;
513 @synthesize editPaste = editPaste_;
514 @synthesize zoomItem = zoomItem_;
515 @synthesize zoomPlus = zoomPlus_;
516 @synthesize zoomDisplay = zoomDisplay_;
517 @synthesize zoomMinus = zoomMinus_;
518 @synthesize zoomFullScreen = zoomFullScreen_;
519 @synthesize toolbarActionsOverflowItem = toolbarActionsOverflowItem_;
520 @synthesize overflowActionsContainerView = overflowActionsContainerView_;
522 - (id)initWithController:(WrenchMenuController*)controller {
523   if ((self = [super initWithNibName:@"WrenchMenu"
524                               bundle:base::mac::FrameworkBundle()])) {
525     controller_ = controller;
526     [[NSNotificationCenter defaultCenter]
527         addObserver:self
528            selector:@selector(containerSuperviewFrameChanged:)
529                name:NSViewFrameDidChangeNotification
530              object:[overflowActionsContainerView_ superview]];
531   }
532   return self;
535 - (void)dealloc {
536   [[NSNotificationCenter defaultCenter] removeObserver:self];
537   [super dealloc];
540 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
541   [controller_ dispatchWrenchMenuCommand:sender];
544 - (void)containerSuperviewFrameChanged:(NSNotification*)notification {
545   // AppKit menus were probably never designed with a view like the browser
546   // actions container in mind, and, as a result, we come across a few oddities.
547   // One of these is that the container's superview will, on some versions of
548   // OSX, change frame position sometime after the the menu begins tracking
549   // (and thus, after all our ability to adjust it normally). Throw in the
550   // towel, and simply don't let the frame move from where it's supposed to be.
551   // TODO(devlin): Yet another Cocoa hack. It'd be good to find a workaround,
552   // but unlikely unless we replace the Cocoa menu implementation.
553   NSView* containerSuperview = [overflowActionsContainerView_ superview];
554   if (NSMinX([containerSuperview frame]) != 0)
555     [containerSuperview setFrameOrigin:NSZeroPoint];
558 @end  // @implementation WrenchMenuButtonViewController