Home | History | Annotate | Download | only in extensions
      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/extensions/extension_action_context_menu.h"
      6 
      7 #include "base/sys_string_conversions.h"
      8 #include "base/task.h"
      9 #include "chrome/browser/extensions/extension_service.h"
     10 #include "chrome/browser/extensions/extension_tabs_module.h"
     11 #include "chrome/browser/extensions/extension_uninstall_dialog.h"
     12 #include "chrome/browser/prefs/pref_change_registrar.h"
     13 #include "chrome/browser/prefs/pref_service.h"
     14 #include "chrome/browser/profiles/profile.h"
     15 #include "chrome/browser/ui/browser_list.h"
     16 #include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
     17 #include "chrome/browser/ui/cocoa/browser_window_controller.h"
     18 #include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
     19 #include "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
     20 #include "chrome/browser/ui/cocoa/info_bubble_view.h"
     21 #import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
     22 #include "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
     23 #include "chrome/common/extensions/extension.h"
     24 #include "chrome/common/extensions/extension_action.h"
     25 #include "chrome/common/extensions/extension_constants.h"
     26 #include "chrome/common/pref_names.h"
     27 #include "chrome/common/url_constants.h"
     28 #include "content/common/notification_details.h"
     29 #include "content/common/notification_observer.h"
     30 #include "content/common/notification_source.h"
     31 #include "content/common/notification_type.h"
     32 #include "grit/generated_resources.h"
     33 #include "ui/base/l10n/l10n_util_mac.h"
     34 
     35 // A class that loads the extension icon on the I/O thread before showing the
     36 // confirmation dialog to uninstall the given extension.
     37 // Also acts as the extension's UI delegate in order to display the dialog.
     38 class AsyncUninstaller : public ExtensionUninstallDialog::Delegate {
     39  public:
     40   AsyncUninstaller(const Extension* extension, Profile* profile)
     41       : extension_(extension),
     42         profile_(profile) {
     43     extension_uninstall_dialog_.reset(new ExtensionUninstallDialog(profile));
     44     extension_uninstall_dialog_->ConfirmUninstall(this, extension_);
     45   }
     46 
     47   ~AsyncUninstaller() {}
     48 
     49   // ExtensionUninstallDialog::Delegate:
     50   virtual void ExtensionDialogAccepted() {
     51     profile_->GetExtensionService()->
     52         UninstallExtension(extension_->id(), false, NULL);
     53   }
     54   virtual void ExtensionDialogCanceled() {}
     55 
     56  private:
     57   // The extension that we're loading the icon for. Weak.
     58   const Extension* extension_;
     59 
     60   // The current profile. Weak.
     61   Profile* profile_;
     62 
     63   scoped_ptr<ExtensionUninstallDialog> extension_uninstall_dialog_;
     64 
     65   DISALLOW_COPY_AND_ASSIGN(AsyncUninstaller);
     66 };
     67 
     68 namespace extension_action_context_menu {
     69 
     70 class DevmodeObserver : public NotificationObserver {
     71  public:
     72   DevmodeObserver(ExtensionActionContextMenu* menu,
     73                              PrefService* service)
     74       : menu_(menu), pref_service_(service) {
     75     registrar_.Init(pref_service_);
     76     registrar_.Add(prefs::kExtensionsUIDeveloperMode, this);
     77   }
     78   virtual ~DevmodeObserver() {}
     79 
     80   void Observe(NotificationType type,
     81                const NotificationSource& source,
     82                const NotificationDetails& details) {
     83     if (type == NotificationType::PREF_CHANGED)
     84       [menu_ updateInspectorItem];
     85     else
     86       NOTREACHED();
     87   }
     88 
     89  private:
     90   ExtensionActionContextMenu* menu_;
     91   PrefService* pref_service_;
     92   PrefChangeRegistrar registrar_;
     93 };
     94 
     95 class ProfileObserverBridge : public NotificationObserver {
     96  public:
     97   ProfileObserverBridge(ExtensionActionContextMenu* owner,
     98                         const Profile* profile)
     99       : owner_(owner),
    100         profile_(profile) {
    101     registrar_.Add(this, NotificationType::PROFILE_DESTROYED,
    102                    Source<Profile>(profile));
    103   }
    104 
    105   ~ProfileObserverBridge() {}
    106 
    107   // Overridden from NotificationObserver
    108   void Observe(NotificationType type,
    109                const NotificationSource& source,
    110                const NotificationDetails& details) {
    111     if (type == NotificationType::PROFILE_DESTROYED &&
    112         source == Source<Profile>(profile_)) {
    113       [owner_ invalidateProfile];
    114     }
    115   }
    116 
    117  private:
    118   ExtensionActionContextMenu* owner_;
    119   const Profile* profile_;
    120   NotificationRegistrar registrar_;
    121 };
    122 
    123 }  // namespace extension_action_context_menu
    124 
    125 @interface ExtensionActionContextMenu(Private)
    126 // Callback for the context menu items.
    127 - (void)dispatch:(id)menuItem;
    128 @end
    129 
    130 @implementation ExtensionActionContextMenu
    131 
    132 namespace {
    133 // Enum of menu item choices to their respective indices.
    134 // NOTE: You MUST keep this in sync with the |menuItems| NSArray below.
    135 enum {
    136   kExtensionContextName = 0,
    137   kExtensionContextOptions = 2,
    138   kExtensionContextDisable = 3,
    139   kExtensionContextUninstall = 4,
    140   kExtensionContextHide = 5,
    141   kExtensionContextManage = 7,
    142   kExtensionContextInspect = 8
    143 };
    144 
    145 int CurrentTabId() {
    146   Browser* browser = BrowserList::GetLastActive();
    147   if(!browser)
    148     return -1;
    149   TabContents* contents = browser->GetSelectedTabContents();
    150   if (!contents)
    151     return -1;
    152   return ExtensionTabUtil::GetTabId(contents);
    153 }
    154 
    155 }  // namespace
    156 
    157 - (id)initWithExtension:(const Extension*)extension
    158                 profile:(Profile*)profile
    159         extensionAction:(ExtensionAction*)action{
    160   if ((self = [super initWithTitle:@""])) {
    161     action_ = action;
    162     extension_ = extension;
    163     profile_ = profile;
    164 
    165     NSArray* menuItems = [NSArray arrayWithObjects:
    166         base::SysUTF8ToNSString(extension->name()),
    167         [NSMenuItem separatorItem],
    168         l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_OPTIONS),
    169         l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_DISABLE),
    170         l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_UNINSTALL),
    171         l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_HIDE_BUTTON),
    172         [NSMenuItem separatorItem],
    173         l10n_util::GetNSStringWithFixup(IDS_MANAGE_EXTENSIONS),
    174         nil];
    175 
    176     for (id item in menuItems) {
    177       if ([item isKindOfClass:[NSMenuItem class]]) {
    178         [self addItem:item];
    179       } else if ([item isKindOfClass:[NSString class]]) {
    180         NSMenuItem* itemObj = [self addItemWithTitle:item
    181                                               action:@selector(dispatch:)
    182                                        keyEquivalent:@""];
    183         // The tag should correspond to the enum above.
    184         // NOTE: The enum and the order of the menu items MUST be in sync.
    185         [itemObj setTag:[self indexOfItem:itemObj]];
    186 
    187         // Disable the 'Options' item if there are no options to set.
    188         if ([itemObj tag] == kExtensionContextOptions &&
    189             extension_->options_url().spec().length() <= 0) {
    190           // Setting the target to nil will disable the item. For some reason
    191           // setEnabled:NO does not work.
    192           [itemObj setTarget:nil];
    193         } else {
    194           [itemObj setTarget:self];
    195         }
    196 
    197         // Only browser actions can have their button hidden. Page actions
    198         // should never show the "Hide" menu item.
    199         if ([itemObj tag] == kExtensionContextHide &&
    200             !extension->browser_action()) {
    201           [itemObj setTarget:nil];  // Item is disabled.
    202           [itemObj setHidden:YES];  // Item is hidden.
    203         } else {
    204           [itemObj setTarget:self];
    205         }
    206       }
    207     }
    208 
    209     NSString* inspectorTitle =
    210         l10n_util::GetNSStringWithFixup(IDS_EXTENSION_ACTION_INSPECT_POPUP);
    211     inspectorItem_.reset([[NSMenuItem alloc] initWithTitle:inspectorTitle
    212                                                     action:@selector(dispatch:)
    213                                              keyEquivalent:@""]);
    214     [inspectorItem_.get() setTarget:self];
    215     [inspectorItem_.get() setTag:kExtensionContextInspect];
    216 
    217     PrefService* service = profile_->GetPrefs();
    218     observer_.reset(
    219         new extension_action_context_menu::DevmodeObserver(self, service));
    220     profile_observer_.reset(
    221         new extension_action_context_menu::ProfileObserverBridge(self,
    222                                                                  profile));
    223 
    224     [self updateInspectorItem];
    225     return self;
    226   }
    227   return nil;
    228 }
    229 
    230 - (void)updateInspectorItem {
    231   PrefService* service = profile_->GetPrefs();
    232   bool devmode = service->GetBoolean(prefs::kExtensionsUIDeveloperMode);
    233   if (devmode) {
    234     if ([self indexOfItem:inspectorItem_.get()] == -1)
    235       [self addItem:inspectorItem_.get()];
    236   } else {
    237     if ([self indexOfItem:inspectorItem_.get()] != -1)
    238       [self removeItem:inspectorItem_.get()];
    239   }
    240 }
    241 
    242 - (void)dispatch:(id)menuItem {
    243   Browser* browser = BrowserList::FindBrowserWithProfile(profile_);
    244   if (!browser)
    245     return;
    246 
    247   NSMenuItem* item = (NSMenuItem*)menuItem;
    248   switch ([item tag]) {
    249     case kExtensionContextName: {
    250       GURL url(std::string(extension_urls::kGalleryBrowsePrefix) +
    251                std::string("/detail/") + extension_->id());
    252       browser->OpenURL(url, GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK);
    253       break;
    254     }
    255     case kExtensionContextOptions: {
    256       DCHECK(!extension_->options_url().is_empty());
    257       profile_->GetExtensionProcessManager()->OpenOptionsPage(extension_,
    258                                                               browser);
    259       break;
    260     }
    261     case kExtensionContextDisable: {
    262       ExtensionService* extensionService = profile_->GetExtensionService();
    263       if (!extensionService)
    264         return; // Incognito mode.
    265       extensionService->DisableExtension(extension_->id());
    266       break;
    267     }
    268     case kExtensionContextUninstall: {
    269       uninstaller_.reset(new AsyncUninstaller(extension_, profile_));
    270       break;
    271     }
    272     case kExtensionContextHide: {
    273       ExtensionService* extension_service = profile_->GetExtensionService();
    274       extension_service->SetBrowserActionVisibility(extension_, false);
    275       break;
    276     }
    277     case kExtensionContextManage: {
    278       browser->OpenURL(GURL(chrome::kChromeUIExtensionsURL), GURL(),
    279                        NEW_FOREGROUND_TAB, PageTransition::LINK);
    280       break;
    281     }
    282     case kExtensionContextInspect: {
    283       BrowserWindowCocoa* window =
    284           static_cast<BrowserWindowCocoa*>(browser->window());
    285       ToolbarController* toolbarController =
    286           [window->cocoa_controller() toolbarController];
    287       LocationBarViewMac* locationBarView =
    288           [toolbarController locationBarBridge];
    289 
    290       NSPoint popupPoint = NSZeroPoint;
    291       if (extension_->page_action() == action_) {
    292         popupPoint = locationBarView->GetPageActionBubblePoint(action_);
    293 
    294       } else if (extension_->browser_action() == action_) {
    295         BrowserActionsController* controller =
    296             [toolbarController browserActionsController];
    297         popupPoint = [controller popupPointForBrowserAction:extension_];
    298 
    299       } else {
    300         NOTREACHED() << "action_ is not a page action or browser action?";
    301       }
    302 
    303       int tabId = CurrentTabId();
    304       GURL url = action_->GetPopupUrl(tabId);
    305       DCHECK(url.is_valid());
    306       [ExtensionPopupController showURL:url
    307                               inBrowser:BrowserList::GetLastActive()
    308                              anchoredAt:popupPoint
    309                           arrowLocation:info_bubble::kTopRight
    310                                 devMode:YES];
    311       break;
    312     }
    313     default:
    314       NOTREACHED();
    315       break;
    316   }
    317 }
    318 
    319 - (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
    320   if([menuItem isEqualTo:inspectorItem_.get()]) {
    321     return action_ && action_->HasPopup(CurrentTabId());
    322   }
    323   return YES;
    324 }
    325 
    326 - (void)invalidateProfile {
    327   observer_.reset();
    328   profile_ = NULL;
    329 }
    330 
    331 @end
    332