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. 4 5 #import "chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.h" 6 7 #include "base/sys_string_conversions.h" 8 #include "chrome/app/chrome_command_ids.h" 9 #include "chrome/browser/metrics/user_metrics.h" 10 #include "chrome/browser/ui/browser.h" 11 #include "chrome/browser/ui/browser_window.h" 12 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h" 13 #import "chrome/browser/ui/cocoa/wrench_menu/menu_tracked_root_view.h" 14 #include "chrome/browser/ui/toolbar/wrench_menu_model.h" 15 #include "content/common/notification_observer.h" 16 #include "content/common/notification_service.h" 17 #include "content/common/notification_source.h" 18 #include "content/common/notification_type.h" 19 #include "grit/chromium_strings.h" 20 #include "grit/generated_resources.h" 21 #include "ui/base/l10n/l10n_util.h" 22 #include "ui/base/models/menu_model.h" 23 24 @interface WrenchMenuController (Private) 25 - (void)adjustPositioning; 26 - (void)performCommandDispatch:(NSNumber*)tag; 27 - (NSButton*)zoomDisplay; 28 @end 29 30 namespace WrenchMenuControllerInternal { 31 32 class ZoomLevelObserver : public NotificationObserver { 33 public: 34 explicit ZoomLevelObserver(WrenchMenuController* controller) 35 : controller_(controller) { 36 registrar_.Add(this, NotificationType::ZOOM_LEVEL_CHANGED, 37 NotificationService::AllSources()); 38 } 39 40 void Observe(NotificationType type, 41 const NotificationSource& source, 42 const NotificationDetails& details) { 43 DCHECK_EQ(type.value, NotificationType::ZOOM_LEVEL_CHANGED); 44 WrenchMenuModel* wrenchMenuModel = [controller_ wrenchMenuModel]; 45 wrenchMenuModel->UpdateZoomControls(); 46 const string16 level = 47 wrenchMenuModel->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY); 48 [[controller_ zoomDisplay] setTitle:SysUTF16ToNSString(level)]; 49 } 50 51 private: 52 NotificationRegistrar registrar_; 53 WrenchMenuController* controller_; // Weak; owns this. 54 }; 55 56 } // namespace WrenchMenuControllerInternal 57 58 @implementation WrenchMenuController 59 60 - (id)init { 61 if ((self = [super init])) { 62 observer_.reset(new WrenchMenuControllerInternal::ZoomLevelObserver(self)); 63 } 64 return self; 65 } 66 67 - (void)addItemToMenu:(NSMenu*)menu 68 atIndex:(NSInteger)index 69 fromModel:(ui::MenuModel*)model 70 modelIndex:(int)modelIndex { 71 // Non-button item types should be built as normal items. 72 ui::MenuModel::ItemType type = model->GetTypeAt(modelIndex); 73 if (type != ui::MenuModel::TYPE_BUTTON_ITEM) { 74 [super addItemToMenu:menu 75 atIndex:index 76 fromModel:model 77 modelIndex:modelIndex]; 78 return; 79 } 80 81 // Handle the special-cased menu items. 82 int command_id = model->GetCommandIdAt(modelIndex); 83 scoped_nsobject<NSMenuItem> customItem( 84 [[NSMenuItem alloc] initWithTitle:@"" 85 action:nil 86 keyEquivalent:@""]); 87 switch (command_id) { 88 case IDC_EDIT_MENU: 89 DCHECK(editItem_); 90 [customItem setView:editItem_]; 91 [editItem_ setMenuItem:customItem]; 92 break; 93 case IDC_ZOOM_MENU: 94 DCHECK(zoomItem_); 95 [customItem setView:zoomItem_]; 96 [zoomItem_ setMenuItem:customItem]; 97 break; 98 default: 99 NOTREACHED(); 100 break; 101 } 102 [self adjustPositioning]; 103 [menu insertItem:customItem.get() atIndex:index]; 104 } 105 106 - (NSMenu*)menu { 107 NSMenu* menu = [super menu]; 108 if (![menu delegate]) { 109 [menu setDelegate:self]; 110 } 111 return menu; 112 } 113 114 - (void)menuWillOpen:(NSMenu*)menu { 115 NSString* title = base::SysUTF16ToNSString( 116 [self wrenchMenuModel]->GetLabelForCommandId(IDC_ZOOM_PERCENT_DISPLAY)); 117 [[zoomItem_ viewWithTag:IDC_ZOOM_PERCENT_DISPLAY] setTitle:title]; 118 UserMetrics::RecordAction(UserMetricsAction("ShowAppMenu")); 119 120 NSImage* icon = [self wrenchMenuModel]->browser()->window()->IsFullscreen() ? 121 [NSImage imageNamed:NSImageNameExitFullScreenTemplate] : 122 [NSImage imageNamed:NSImageNameEnterFullScreenTemplate]; 123 [zoomFullScreen_ setImage:icon]; 124 } 125 126 // Used to dispatch commands from the Wrench menu. The custom items within the 127 // menu cannot be hooked up directly to First Responder because the window in 128 // which the controls reside is not the BrowserWindowController, but a 129 // NSCarbonMenuWindow; this screws up the typical |-commandDispatch:| system. 130 - (IBAction)dispatchWrenchMenuCommand:(id)sender { 131 NSInteger tag = [sender tag]; 132 if (sender == zoomPlus_ || sender == zoomMinus_) { 133 // Do a direct dispatch rather than scheduling on the outermost run loop, 134 // which would not get hit until after the menu had closed. 135 [self performCommandDispatch:[NSNumber numberWithInt:tag]]; 136 137 // The zoom buttons should not close the menu if opened sticky. 138 if ([sender respondsToSelector:@selector(isTracking)] && 139 [sender performSelector:@selector(isTracking)]) { 140 [menu_ cancelTracking]; 141 } 142 } else { 143 // The custom views within the Wrench menu are abnormal and keep the menu 144 // open after a target-action. Close the menu manually. 145 [menu_ cancelTracking]; 146 [self dispatchCommandInternal:tag]; 147 } 148 } 149 150 - (void)dispatchCommandInternal:(NSInteger)tag { 151 // Executing certain commands from the nested run loop of the menu can lead 152 // to wonky behavior (e.g. http://crbug.com/49716). To avoid this, schedule 153 // the dispatch on the outermost run loop. 154 [self performSelector:@selector(performCommandDispatch:) 155 withObject:[NSNumber numberWithInt:tag] 156 afterDelay:0.0]; 157 } 158 159 // Used to perform the actual dispatch on the outermost runloop. 160 - (void)performCommandDispatch:(NSNumber*)tag { 161 [self wrenchMenuModel]->ExecuteCommand([tag intValue]); 162 } 163 164 - (WrenchMenuModel*)wrenchMenuModel { 165 return static_cast<WrenchMenuModel*>(model_); 166 } 167 168 // Fit the localized strings into the Cut/Copy/Paste control, then resize the 169 // whole menu item accordingly. 170 - (void)adjustPositioning { 171 const CGFloat kButtonPadding = 12; 172 CGFloat delta = 0; 173 174 // Go through the three buttons from right-to-left, adjusting the size to fit 175 // the localized strings while keeping them all aligned on their horizontal 176 // edges. 177 const size_t kAdjustViewCount = 3; 178 NSButton* views[kAdjustViewCount] = { editPaste_, editCopy_, editCut_ }; 179 for (size_t i = 0; i < kAdjustViewCount; ++i) { 180 NSButton* button = views[i]; 181 CGFloat originalWidth = NSWidth([button frame]); 182 183 // Do not let |-sizeToFit| change the height of the button. 184 NSSize size = [button frame].size; 185 [button sizeToFit]; 186 size.width = [button frame].size.width + kButtonPadding; 187 [button setFrameSize:size]; 188 189 CGFloat newWidth = size.width; 190 delta += newWidth - originalWidth; 191 192 NSRect frame = [button frame]; 193 frame.origin.x -= delta; 194 [button setFrame:frame]; 195 } 196 197 // Resize the menu item by the total amound the buttons changed so that the 198 // spacing between the buttons and the title remains the same. 199 NSRect itemFrame = [editItem_ frame]; 200 itemFrame.size.width += delta; 201 [editItem_ setFrame:itemFrame]; 202 203 // Also resize the superview of the buttons, which is an NSView used to slide 204 // when the item title is too big and GTM resizes it. 205 NSRect parentFrame = [[editCut_ superview] frame]; 206 parentFrame.size.width += delta; 207 parentFrame.origin.x -= delta; 208 [[editCut_ superview] setFrame:parentFrame]; 209 } 210 211 - (NSButton*)zoomDisplay { 212 return zoomDisplay_; 213 } 214 215 @end // @implementation WrenchMenuController 216