Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / wrench_menu / wrench_menu_controller.mm
blob2b56111217392356d69dfa293ecb181ed576a15c
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 "content/public/browser/user_metrics.h"
28 #include "grit/chromium_strings.h"
29 #include "grit/generated_resources.h"
30 #include "ui/base/l10n/l10n_util.h"
31 #include "ui/base/models/menu_model.h"
33 namespace wrench_menu_controller {
34 const CGFloat kWrenchBubblePointOffsetY = 6;
37 using base::UserMetricsAction;
38 using content::HostZoomMap;
40 @interface WrenchMenuController (Private)
41 - (void)createModel;
42 - (void)adjustPositioning;
43 - (void)performCommandDispatch:(NSNumber*)tag;
44 - (NSButton*)zoomDisplay;
45 - (void)removeAllItems:(NSMenu*)menu;
46 - (NSMenu*)recentTabsSubmenu;
47 - (RecentTabsSubMenuModel*)recentTabsMenuModel;
48 - (int)maxWidthForMenuModel:(ui::MenuModel*)model
49                  modelIndex:(int)modelIndex;
50 @end
52 namespace WrenchMenuControllerInternal {
54 // A C++ delegate that handles the accelerators in the wrench menu.
55 class AcceleratorDelegate : public ui::AcceleratorProvider {
56  public:
57   virtual bool GetAcceleratorForCommandId(int command_id,
58       ui::Accelerator* out_accelerator) OVERRIDE {
59     AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
60     const ui::Accelerator* accelerator =
61         keymap->GetAcceleratorForCommand(command_id);
62     if (!accelerator)
63       return false;
64     *out_accelerator = *accelerator;
65     return true;
66   }
69 class ZoomLevelObserver {
70  public:
71   ZoomLevelObserver(WrenchMenuController* controller,
72                     content::HostZoomMap* map)
73       : controller_(controller),
74         map_(map) {
75     subscription_ = map_->AddZoomLevelChangedCallback(
76         base::Bind(&ZoomLevelObserver::OnZoomLevelChanged,
77                    base::Unretained(this)));
78   }
80   ~ZoomLevelObserver() {}
82  private:
83   void OnZoomLevelChanged(const HostZoomMap::ZoomLevelChange& change) {
84     WrenchMenuModel* wrenchMenuModel = [controller_ wrenchMenuModel];
85     wrenchMenuModel->UpdateZoomControls();
86     const base::string16 level =
87         wrenchMenuModel->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY);
88     [[controller_ zoomDisplay] setTitle:SysUTF16ToNSString(level)];
89   }
91   scoped_ptr<content::HostZoomMap::Subscription> subscription_;
93   WrenchMenuController* controller_;  // Weak; owns this.
94   content::HostZoomMap* map_;  // Weak.
96   DISALLOW_COPY_AND_ASSIGN(ZoomLevelObserver);
99 }  // namespace WrenchMenuControllerInternal
101 @implementation WrenchMenuController
103 - (id)initWithBrowser:(Browser*)browser {
104   if ((self = [super init])) {
105     browser_ = browser;
106     observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(
107         self, content::HostZoomMap::GetForBrowserContext(browser->profile())));
108     acceleratorDelegate_.reset(
109         new WrenchMenuControllerInternal::AcceleratorDelegate());
110     [self createModel];
111   }
112   return self;
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
122                  atIndex:index
123                fromModel:model];
124     return;
125   }
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) {
133     case IDC_EDIT_MENU:
134       view = [buttonViewController_ editItem];
135       DCHECK(view);
136       [customItem setView:view];
137       [view setMenuItem:customItem];
138       break;
139     case IDC_ZOOM_MENU:
140       view = [buttonViewController_ zoomItem];
141       DCHECK(view);
142       [customItem setView:view];
143       [view setMenuItem:customItem];
144       break;
145     default:
146       NOTREACHED();
147       break;
148   }
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
164   // bold).
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()];
175     } else {
176       // Not a section header. Add a tooltip with the title and the URL.
177       std::string 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))];
184        }
185     }
186   }
188   return enabled;
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(),
202                              bookmarkMenu));
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]
211       setTitle:title];
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];
223   [self createModel];
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) {
231     [menu addItem:item];
232   }
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];
254     }
255   } else {
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]
265                afterDelay:0.0];
266   }
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];
281   if (model) {
282     recentTabsMenuModelDelegate_.reset(
283         new RecentTabsMenuModelDelegate(model, [self recentTabsSubmenu]));
284   }
287 - (void)createModel {
288   recentTabsMenuModelDelegate_.reset();
289   wrenchMenuModel_.reset(
290       new WrenchMenuModel(acceleratorDelegate_.get(), browser_, false));
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;
302   CGFloat delta = 0;
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
306   // edges.
307   NSButton* views[] = {
308       [buttonViewController_ editPaste],
309       [buttonViewController_ editCopy],
310       [buttonViewController_ editCut]
311   };
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;
318     [button sizeToFit];
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];
328   }
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];
352   }
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 {
363   int index = 0;
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,
369           &index)) {
370     return static_cast<RecentTabsSubMenuModel*>(model);
371   }
372   if (ui::MenuModel::GetModelAndIndexForCommandId(
373           RecentTabsSubMenuModel::kDisabledRecentlyClosedHeaderCommandId,
374           &model, &index)) {
375     return static_cast<RecentTabsSubMenuModel*>(model);
376   }
377   return NULL;
380 // This overrdies the parent class to return a custom width for recent tabs
381 // menu.
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);
387   }
388   return -1;
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;
411   }
412   return self;
415 - (IBAction)dispatchWrenchMenuCommand:(id)sender {
416   [controller_ dispatchWrenchMenuCommand:sender];
419 @end  // @implementation WrenchMenuButtonViewController