Home | History | Annotate | Download | only in wrench_menu
      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