BookmarkManager: Fix 'new folder text field size changes on clicking it' issue.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / wrench_menu / wrench_menu_controller.mm
blobff4f2c808dedf81f4c5d29fbdb46f7a58a089603
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;
42 // In *very* extreme cases, it's possible that there are so many overflowed
43 // actions, we won't be able to show them all. Cap the height so that the
44 // overflow won't make the menu larger than the height of the screen.
45 // Note: With this height, we can show 104 actions. Less than 0.0002% of our
46 // users will be affected.
47 const int kMaxOverflowContainerHeight = 416;
50 namespace wrench_menu_controller {
51 const CGFloat kWrenchBubblePointOffsetY = 6;
54 using base::UserMetricsAction;
56 @interface WrenchMenuController (Private)
57 - (void)createModel;
58 - (void)adjustPositioning;
59 - (void)performCommandDispatch:(NSNumber*)tag;
60 - (NSButton*)zoomDisplay;
61 - (void)menu:(NSMenu*)menu willHighlightItem:(NSMenuItem*)item;
62 - (void)removeAllItems:(NSMenu*)menu;
63 - (NSMenu*)recentTabsSubmenu;
64 - (RecentTabsSubMenuModel*)recentTabsMenuModel;
65 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
66                  modelIndex:(int)modelIndex;
67 @end
69 namespace WrenchMenuControllerInternal {
71 // A C++ delegate that handles the accelerators in the wrench menu.
72 class AcceleratorDelegate : public ui::AcceleratorProvider {
73  public:
74   bool GetAcceleratorForCommandId(int command_id,
75                                   ui::Accelerator* out_accelerator) override {
76     AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
77     const ui::Accelerator* accelerator =
78         keymap->GetAcceleratorForCommand(command_id);
79     if (!accelerator)
80       return false;
81     *out_accelerator = *accelerator;
82     return true;
83   }
86 class ZoomLevelObserver {
87  public:
88   ZoomLevelObserver(WrenchMenuController* controller,
89                     ui_zoom::ZoomEventManager* manager)
90       : controller_(controller) {
91     subscription_ = manager->AddZoomLevelChangedCallback(
92         base::Bind(&ZoomLevelObserver::OnZoomLevelChanged,
93                    base::Unretained(this)));
94   }
96   ~ZoomLevelObserver() {}
98  private:
99   void OnZoomLevelChanged(const content::HostZoomMap::ZoomLevelChange& change) {
100     WrenchMenuModel* wrenchMenuModel = [controller_ wrenchMenuModel];
101     wrenchMenuModel->UpdateZoomControls();
102     const base::string16 level =
103         wrenchMenuModel->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY);
104     [[controller_ zoomDisplay] setTitle:SysUTF16ToNSString(level)];
105   }
107   scoped_ptr<content::HostZoomMap::Subscription> subscription_;
109   WrenchMenuController* controller_;  // Weak; owns this.
111   DISALLOW_COPY_AND_ASSIGN(ZoomLevelObserver);
114 }  // namespace WrenchMenuControllerInternal
116 @implementation WrenchMenuController
118 - (id)initWithBrowser:(Browser*)browser {
119   if ((self = [super init])) {
120     browser_ = browser;
121     observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(
122         self,
123         ui_zoom::ZoomEventManager::GetForBrowserContext(browser->profile())));
124     acceleratorDelegate_.reset(
125         new WrenchMenuControllerInternal::AcceleratorDelegate());
126     [self createModel];
127   }
128   return self;
131 - (void)dealloc {
132   [self browserWillBeDestroyed];
133   [super dealloc];
136 - (void)browserWillBeDestroyed {
137   // This method indicates imminent destruction. Destroy owned objects that hold
138   // a weak Browser*, or pass this call onto reference counted objects.
139   recentTabsMenuModelDelegate_.reset();
140   [self setModel:nullptr];
141   wrenchMenuModel_.reset();
142   buttonViewController_.reset();
143   // ZoomLevelObserver holds a subscription to ZoomEventManager, which is
144   // user-data on the BrowserContext. The BrowserContext may be destroyed soon
145   // if Chrome is quitting. In any case, |observer_| should not be needed at
146   // this point.
147   observer_.reset();
149   [browserActionsController_ browserWillBeDestroyed];
151   browser_ = nullptr;
154 - (void)addItemToMenu:(NSMenu*)menu
155               atIndex:(NSInteger)index
156             fromModel:(ui::MenuModel*)model {
157   // Non-button item types should be built as normal items, with the exception
158   // of the extensions overflow menu.
159   int command_id = model->GetCommandIdAt(index);
160   if (model->GetTypeAt(index) != ui::MenuModel::TYPE_BUTTON_ITEM &&
161       command_id != IDC_EXTENSIONS_OVERFLOW_MENU) {
162     [super addItemToMenu:menu
163                  atIndex:index
164                fromModel:model];
165     return;
166   }
168   // Handle the special-cased menu items.
169   base::scoped_nsobject<NSMenuItem> customItem(
170       [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]);
171   MenuTrackedRootView* view = nil;
172   switch (command_id) {
173     case IDC_EXTENSIONS_OVERFLOW_MENU: {
174       browserActionsMenuItem_ = customItem.get();
175       view = [buttonViewController_ toolbarActionsOverflowItem];
176       BrowserActionsContainerView* containerView =
177           [buttonViewController_ overflowActionsContainerView];
179       // The overflow browser actions container can't function properly without
180       // a main counterpart, so if the browser window hasn't initialized, abort.
181       // (This is fine because we re-populate the wrench menu each time before
182       // we show it.)
183       if (!browser_->window())
184         break;
186       BrowserActionsController* mainController =
187           [[[BrowserWindowController browserWindowControllerForWindow:browser_->
188               window()->GetNativeWindow()] toolbarController]
189                   browserActionsController];
190       browserActionsController_.reset(
191           [[BrowserActionsController alloc]
192               initWithBrowser:browser_
193                 containerView:containerView
194                mainController:mainController]);
195       break;
196     }
197     case IDC_EDIT_MENU:
198       view = [buttonViewController_ editItem];
199       break;
200     case IDC_ZOOM_MENU:
201       view = [buttonViewController_ zoomItem];
202       break;
203     default:
204       NOTREACHED();
205       break;
206   }
207   DCHECK(view);
208   [customItem setView:view];
209   [view setMenuItem:customItem];
210   [self adjustPositioning];
211   [menu insertItem:customItem.get() atIndex:index];
214 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
215   const BOOL enabled = [super validateUserInterfaceItem:item];
217   NSMenuItem* menuItem = (id)item;
218   ui::MenuModel* model =
219       static_cast<ui::MenuModel*>(
220           [[menuItem representedObject] pointerValue]);
222   // The section headers in the recent tabs submenu should be bold and black if
223   // a font list is specified for the items (bold is already applied in the
224   // |MenuController| as the font list returned by |GetLabelFontListAt| is
225   // bold).
226   if (model && model == [self recentTabsMenuModel]) {
227     if (model->GetLabelFontListAt([item tag])) {
228       DCHECK([menuItem attributedTitle]);
229       base::scoped_nsobject<NSMutableAttributedString> title(
230           [[NSMutableAttributedString alloc]
231               initWithAttributedString:[menuItem attributedTitle]]);
232       [title addAttribute:NSForegroundColorAttributeName
233                     value:[NSColor blackColor]
234                     range:NSMakeRange(0, [title length])];
235       [menuItem setAttributedTitle:title.get()];
236     } else {
237       // Not a section header. Add a tooltip with the title and the URL.
238       std::string url;
239       base::string16 title;
240       if ([self recentTabsMenuModel]->GetURLAndTitleForItemAtIndex(
241               [item tag], &url, &title)) {
242         [menuItem setToolTip:
243             cocoa_l10n_util::TooltipForURLAndTitle(
244                 base::SysUTF8ToNSString(url), base::SysUTF16ToNSString(title))];
245        }
246     }
247   }
249   return enabled;
252 - (NSMenu*)bookmarkSubMenu {
253   NSString* title = l10n_util::GetNSStringWithFixup(IDS_BOOKMARKS_MENU);
254   return [[[self menu] itemWithTitle:title] submenu];
257 - (void)updateBookmarkSubMenu {
258   NSMenu* bookmarkMenu = [self bookmarkSubMenu];
259   DCHECK(bookmarkMenu);
261   bookmarkMenuBridge_.reset(
262       new BookmarkMenuBridge([self wrenchMenuModel]->browser()->profile(),
263                              bookmarkMenu));
266 - (void)updateBrowserActionsSubmenu {
267   MenuTrackedRootView* view =
268       [buttonViewController_ toolbarActionsOverflowItem];
269   BrowserActionsContainerView* containerView =
270       [buttonViewController_ overflowActionsContainerView];
272   // Find the preferred container size for the menu width.
273   int menuWidth = [[self menu] size].width;
274   int maxContainerWidth = menuWidth - kLeftPadding - kRightPadding;
275   // Don't let the menu change sizes on us. (We lift this restriction every time
276   // the menu updates, so if something changes, this won't leave us with an
277   // awkward size.)
278   [[self menu] setMinimumWidth:menuWidth];
279   gfx::Size preferredContainerSize =
280       [browserActionsController_ sizeForOverflowWidth:maxContainerWidth];
282   // Set the origins and preferred size for the container.
283   // View hierarchy is as follows (from parent > child):
284   // |view| > |anonymous view| > containerView. We have to set the origin
285   // and size of each for it display properly.
286   // The parent views each have a size of the full width of the menu, so we can
287   // properly position the container.
288   NSSize parentSize = NSMakeSize(menuWidth,
289                                  std::min(preferredContainerSize.height(),
290                                           kMaxOverflowContainerHeight));
291   [view setFrameSize:parentSize];
292   [[containerView superview] setFrameSize:parentSize];
294   // The container view gets its preferred size.
295   [containerView setFrameSize:NSMakeSize(preferredContainerSize.width(),
296                                          preferredContainerSize.height())];
297   [browserActionsController_ update];
299   [view setFrameOrigin:NSZeroPoint];
300   [[containerView superview] setFrameOrigin:NSZeroPoint];
301   [containerView setFrameOrigin:NSMakePoint(kLeftPadding, 0)];
304 - (void)menuWillOpen:(NSMenu*)menu {
305   [super menuWillOpen:menu];
307   NSString* title = base::SysUTF16ToNSString(
308       [self wrenchMenuModel]->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY));
309   [[[buttonViewController_ zoomItem] viewWithTag:IDC_ZOOM_PERCENT_DISPLAY]
310       setTitle:title];
311   content::RecordAction(UserMetricsAction("ShowAppMenu"));
313   NSImage* icon = [self wrenchMenuModel]->browser()->window()->IsFullscreen() ?
314       [NSImage imageNamed:NSImageNameExitFullScreenTemplate] :
315           [NSImage imageNamed:NSImageNameEnterFullScreenTemplate];
316   [[buttonViewController_ zoomFullScreen] setImage:icon];
319 - (void)menuNeedsUpdate:(NSMenu*)menu {
320   // First empty out the menu and create a new model.
321   [self removeAllItems:menu];
322   [self createModel];
323   [menu setMinimumWidth:0];
325   // Create a new menu, which cannot be swapped because the tracking is about to
326   // start, so simply copy the items.
327   NSMenu* newMenu = [self menuFromModel:model_];
328   NSArray* itemArray = [newMenu itemArray];
329   [self removeAllItems:newMenu];
330   for (NSMenuItem* item in itemArray) {
331     [menu addItem:item];
332   }
334   [self updateRecentTabsSubmenu];
335   [self updateBookmarkSubMenu];
336   [self updateBrowserActionsSubmenu];
339 // Used to dispatch commands from the Wrench menu. The custom items within the
340 // menu cannot be hooked up directly to First Responder because the window in
341 // which the controls reside is not the BrowserWindowController, but a
342 // NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system.
343 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
344   NSInteger tag = [sender tag];
345   if (sender == [buttonViewController_ zoomPlus] ||
346       sender == [buttonViewController_ zoomMinus]) {
347     // Do a direct dispatch rather than scheduling on the outermost run loop,
348     // which would not get hit until after the menu had closed.
349     [self performCommandDispatch:[NSNumber numberWithInt:tag]];
351     // The zoom buttons should not close the menu if opened sticky.
352     if ([sender respondsToSelector:@selector(isTracking)] &&
353         [sender performSelector:@selector(isTracking)]) {
354       [menu_ cancelTracking];
355     }
356   } else {
357     // The custom views within the Wrench menu are abnormal and keep the menu
358     // open after a target-action.  Close the menu manually.
359     [menu_ cancelTracking];
361     // Executing certain commands from the nested run loop of the menu can lead
362     // to wonky behavior (e.g. http://crbug.com/49716). To avoid this, schedule
363     // the dispatch on the outermost run loop.
364     [self performSelector:@selector(performCommandDispatch:)
365                withObject:[NSNumber numberWithInt:tag]
366                afterDelay:0.0];
367   }
370 // Used to perform the actual dispatch on the outermost runloop.
371 - (void)performCommandDispatch:(NSNumber*)tag {
372   [self wrenchMenuModel]->ExecuteCommand([tag intValue], 0);
375 - (WrenchMenuModel*)wrenchMenuModel {
376   // Don't use |wrenchMenuModel_| so that a test can override the generic one.
377   return static_cast<WrenchMenuModel*>(model_);
380 - (void)updateRecentTabsSubmenu {
381   ui::MenuModel* model = [self recentTabsMenuModel];
382   if (model) {
383     recentTabsMenuModelDelegate_.reset(
384         new RecentTabsMenuModelDelegate(model, [self recentTabsSubmenu]));
385   }
388 - (BrowserActionsController*)browserActionsController {
389   return browserActionsController_.get();
392 - (void)createModel {
393   DCHECK(browser_);
394   recentTabsMenuModelDelegate_.reset();
395   wrenchMenuModel_.reset(
396       new WrenchMenuModel(acceleratorDelegate_.get(), browser_));
397   [self setModel:wrenchMenuModel_.get()];
399   buttonViewController_.reset(
400       [[WrenchMenuButtonViewController alloc] initWithController:self]);
401   [buttonViewController_ view];
403   // See comment in containerSuperviewFrameChanged:.
404   NSView* containerSuperview =
405       [[buttonViewController_ overflowActionsContainerView] superview];
406   [containerSuperview setPostsFrameChangedNotifications:YES];
409 // Fit the localized strings into the Cut/Copy/Paste control, then resize the
410 // whole menu item accordingly.
411 - (void)adjustPositioning {
412   const CGFloat kButtonPadding = 12;
413   CGFloat delta = 0;
415   // Go through the three buttons from right-to-left, adjusting the size to fit
416   // the localized strings while keeping them all aligned on their horizontal
417   // edges.
418   NSButton* views[] = {
419       [buttonViewController_ editPaste],
420       [buttonViewController_ editCopy],
421       [buttonViewController_ editCut]
422   };
423   for (size_t i = 0; i < arraysize(views); ++i) {
424     NSButton* button = views[i];
425     CGFloat originalWidth = NSWidth([button frame]);
427     // Do not let |-sizeToFit| change the height of the button.
428     NSSize size = [button frame].size;
429     [button sizeToFit];
430     size.width = [button frame].size.width + kButtonPadding;
431     [button setFrameSize:size];
433     CGFloat newWidth = size.width;
434     delta += newWidth - originalWidth;
436     NSRect frame = [button frame];
437     frame.origin.x -= delta;
438     [button setFrame:frame];
439   }
441   // Resize the menu item by the total amound the buttons changed so that the
442   // spacing between the buttons and the title remains the same.
443   NSRect itemFrame = [[buttonViewController_ editItem] frame];
444   itemFrame.size.width += delta;
445   [[buttonViewController_ editItem] setFrame:itemFrame];
447   // Also resize the superview of the buttons, which is an NSView used to slide
448   // when the item title is too big and GTM resizes it.
449   NSRect parentFrame = [[[buttonViewController_ editCut] superview] frame];
450   parentFrame.size.width += delta;
451   parentFrame.origin.x -= delta;
452   [[[buttonViewController_ editCut] superview] setFrame:parentFrame];
455 - (NSButton*)zoomDisplay {
456   return [buttonViewController_ zoomDisplay];
459 - (void)menu:(NSMenu*)menu willHighlightItem:(NSMenuItem*)item {
460   if (browserActionsController_.get()) {
461     [browserActionsController_ setFocusedInOverflow:
462         (item == browserActionsMenuItem_)];
463   }
466 // -[NSMenu removeAllItems] is only available on 10.6+.
467 - (void)removeAllItems:(NSMenu*)menu {
468   while ([menu numberOfItems]) {
469     [menu removeItemAtIndex:0];
470   }
473 - (NSMenu*)recentTabsSubmenu {
474   NSString* title = l10n_util::GetNSStringWithFixup(IDS_RECENT_TABS_MENU);
475   return [[[self menu] itemWithTitle:title] submenu];
478 // The recent tabs menu model is recognized by the existence of either the
479 // kRecentlyClosedHeaderCommandId or the kDisabledRecentlyClosedHeaderCommandId.
480 - (RecentTabsSubMenuModel*)recentTabsMenuModel {
481   int index = 0;
482   // Start searching at the wrench menu model level, |model| will be updated
483   // only if the command we're looking for is found in one of the [sub]menus.
484   ui::MenuModel* model = [self wrenchMenuModel];
485   if (ui::MenuModel::GetModelAndIndexForCommandId(
486           RecentTabsSubMenuModel::kRecentlyClosedHeaderCommandId, &model,
487           &index)) {
488     return static_cast<RecentTabsSubMenuModel*>(model);
489   }
490   if (ui::MenuModel::GetModelAndIndexForCommandId(
491           RecentTabsSubMenuModel::kDisabledRecentlyClosedHeaderCommandId,
492           &model, &index)) {
493     return static_cast<RecentTabsSubMenuModel*>(model);
494   }
495   return NULL;
498 // This overrdies the parent class to return a custom width for recent tabs
499 // menu.
500 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
501                  modelIndex:(int)modelIndex {
502   RecentTabsSubMenuModel* recentTabsMenuModel = [self recentTabsMenuModel];
503   if (recentTabsMenuModel && recentTabsMenuModel == model) {
504     return recentTabsMenuModel->GetMaxWidthForItemAtIndex(modelIndex);
505   }
506   return -1;
509 @end  // @implementation WrenchMenuController
511 ////////////////////////////////////////////////////////////////////////////////
513 @interface WrenchMenuButtonViewController ()
514 - (void)containerSuperviewFrameChanged:(NSNotification*)notification;
515 @end
517 @implementation WrenchMenuButtonViewController
519 @synthesize editItem = editItem_;
520 @synthesize editCut = editCut_;
521 @synthesize editCopy = editCopy_;
522 @synthesize editPaste = editPaste_;
523 @synthesize zoomItem = zoomItem_;
524 @synthesize zoomPlus = zoomPlus_;
525 @synthesize zoomDisplay = zoomDisplay_;
526 @synthesize zoomMinus = zoomMinus_;
527 @synthesize zoomFullScreen = zoomFullScreen_;
528 @synthesize toolbarActionsOverflowItem = toolbarActionsOverflowItem_;
529 @synthesize overflowActionsContainerView = overflowActionsContainerView_;
531 - (id)initWithController:(WrenchMenuController*)controller {
532   if ((self = [super initWithNibName:@"WrenchMenu"
533                               bundle:base::mac::FrameworkBundle()])) {
534     controller_ = controller;
535     [[NSNotificationCenter defaultCenter]
536         addObserver:self
537            selector:@selector(containerSuperviewFrameChanged:)
538                name:NSViewFrameDidChangeNotification
539              object:[overflowActionsContainerView_ superview]];
540   }
541   return self;
544 - (void)dealloc {
545   [[NSNotificationCenter defaultCenter] removeObserver:self];
546   [super dealloc];
549 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
550   [controller_ dispatchWrenchMenuCommand:sender];
553 - (void)containerSuperviewFrameChanged:(NSNotification*)notification {
554   // AppKit menus were probably never designed with a view like the browser
555   // actions container in mind, and, as a result, we come across a few oddities.
556   // One of these is that the container's superview will, on some versions of
557   // OSX, change frame position sometime after the the menu begins tracking
558   // (and thus, after all our ability to adjust it normally). Throw in the
559   // towel, and simply don't let the frame move from where it's supposed to be.
560   // TODO(devlin): Yet another Cocoa hack. It'd be good to find a workaround,
561   // but unlikely unless we replace the Cocoa menu implementation.
562   NSView* containerSuperview = [overflowActionsContainerView_ superview];
563   if (NSMinX([containerSuperview frame]) != 0)
564     [containerSuperview setFrameOrigin:NSZeroPoint];
567 @end  // @implementation WrenchMenuButtonViewController