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