[Extension Toolbar] Refactor and finish pop out logic for actions
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / wrench_menu / wrench_menu_controller.mm
blob8877a169d53b99cc3702630481e02fbfd7541e32
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)
44 - (void)createModel;
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;
53 @end
55 namespace WrenchMenuControllerInternal {
57 // A C++ delegate that handles the accelerators in the wrench menu.
58 class AcceleratorDelegate : public ui::AcceleratorProvider {
59  public:
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);
65     if (!accelerator)
66       return false;
67     *out_accelerator = *accelerator;
68     return true;
69   }
72 class ZoomLevelObserver {
73  public:
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)));
80   }
82   ~ZoomLevelObserver() {}
84  private:
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)];
91   }
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])) {
106     browser_ = browser;
107     observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(
108         self,
109         ui_zoom::ZoomEventManager::GetForBrowserContext(browser->profile())));
110     acceleratorDelegate_.reset(
111         new WrenchMenuControllerInternal::AcceleratorDelegate());
112     [self createModel];
113   }
114   return self;
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
126                  atIndex:index
127                fromModel:model];
128     return;
129   }
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
144       // we show it.)
145       if (!browser_->window())
146         break;
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];
172       break;
173     }
174     case IDC_EDIT_MENU:
175       view = [buttonViewController_ editItem];
176       break;
177     case IDC_ZOOM_MENU:
178       view = [buttonViewController_ zoomItem];
179       break;
180     default:
181       NOTREACHED();
182       break;
183   }
184   DCHECK(view);
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
202   // bold).
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()];
213     } else {
214       // Not a section header. Add a tooltip with the title and the URL.
215       std::string 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))];
222        }
223     }
224   }
226   return enabled;
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(),
240                              bookmarkMenu));
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]
249       setTitle:title];
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];
261   [self createModel];
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) {
269     [menu addItem:item];
270   }
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];
292     }
293   } else {
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]
303                afterDelay:0.0];
304   }
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];
319   if (model) {
320     recentTabsMenuModelDelegate_.reset(
321         new RecentTabsMenuModelDelegate(model, [self recentTabsSubmenu]));
322   }
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;
340   CGFloat delta = 0;
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
344   // edges.
345   NSButton* views[] = {
346       [buttonViewController_ editPaste],
347       [buttonViewController_ editCopy],
348       [buttonViewController_ editCut]
349   };
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;
356     [button sizeToFit];
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];
366   }
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];
390   }
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 {
401   int index = 0;
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,
407           &index)) {
408     return static_cast<RecentTabsSubMenuModel*>(model);
409   }
410   if (ui::MenuModel::GetModelAndIndexForCommandId(
411           RecentTabsSubMenuModel::kDisabledRecentlyClosedHeaderCommandId,
412           &model, &index)) {
413     return static_cast<RecentTabsSubMenuModel*>(model);
414   }
415   return NULL;
418 // This overrdies the parent class to return a custom width for recent tabs
419 // menu.
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);
425   }
426   return -1;
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;
451   }
452   return self;
455 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
456   [controller_ dispatchWrenchMenuCommand:sender];
459 @end  // @implementation WrenchMenuButtonViewController