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_installed_bubble_controller.h" 6 7 #include "base/i18n/rtl.h" 8 #include "base/mac/mac_util.h" 9 #include "base/sys_string_conversions.h" 10 #include "base/utf_string_conversions.h" 11 #include "chrome/browser/ui/browser.h" 12 #include "chrome/browser/ui/browser_window.h" 13 #include "chrome/browser/ui/cocoa/browser_window_cocoa.h" 14 #include "chrome/browser/ui/cocoa/browser_window_controller.h" 15 #include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h" 16 #include "chrome/browser/ui/cocoa/hover_close_button.h" 17 #include "chrome/browser/ui/cocoa/info_bubble_view.h" 18 #include "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h" 19 #include "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h" 20 #include "chrome/common/extensions/extension.h" 21 #include "chrome/common/extensions/extension_action.h" 22 #include "content/common/notification_details.h" 23 #include "content/common/notification_registrar.h" 24 #include "content/common/notification_source.h" 25 #include "grit/generated_resources.h" 26 #import "skia/ext/skia_utils_mac.h" 27 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" 28 #include "ui/base/l10n/l10n_util.h" 29 30 31 // C++ class that receives EXTENSION_LOADED notifications and proxies them back 32 // to |controller|. 33 class ExtensionLoadedNotificationObserver : public NotificationObserver { 34 public: 35 ExtensionLoadedNotificationObserver( 36 ExtensionInstalledBubbleController* controller, Profile* profile) 37 : controller_(controller) { 38 registrar_.Add(this, NotificationType::EXTENSION_LOADED, 39 Source<Profile>(profile)); 40 registrar_.Add(this, NotificationType::EXTENSION_UNLOADED, 41 Source<Profile>(profile)); 42 } 43 44 private: 45 // NotificationObserver implementation. Tells the controller to start showing 46 // its window on the main thread when the extension has finished loading. 47 void Observe(NotificationType type, 48 const NotificationSource& source, 49 const NotificationDetails& details) { 50 if (type == NotificationType::EXTENSION_LOADED) { 51 const Extension* extension = Details<const Extension>(details).ptr(); 52 if (extension == [controller_ extension]) { 53 [controller_ performSelectorOnMainThread:@selector(showWindow:) 54 withObject:controller_ 55 waitUntilDone:NO]; 56 } 57 } else if (type == NotificationType::EXTENSION_UNLOADED) { 58 const Extension* extension = Details<const Extension>(details).ptr(); 59 if (extension == [controller_ extension]) { 60 [controller_ performSelectorOnMainThread:@selector(extensionUnloaded:) 61 withObject:controller_ 62 waitUntilDone:NO]; 63 } 64 } else { 65 NOTREACHED() << "Received unexpected notification."; 66 } 67 } 68 69 NotificationRegistrar registrar_; 70 ExtensionInstalledBubbleController* controller_; // weak, owns us 71 }; 72 73 @implementation ExtensionInstalledBubbleController 74 75 @synthesize extension = extension_; 76 @synthesize pageActionRemoved = pageActionRemoved_; // Exposed for unit test. 77 78 - (id)initWithParentWindow:(NSWindow*)parentWindow 79 extension:(const Extension*)extension 80 browser:(Browser*)browser 81 icon:(SkBitmap)icon { 82 NSString* nibPath = 83 [base::mac::MainAppBundle() pathForResource:@"ExtensionInstalledBubble" 84 ofType:@"nib"]; 85 if ((self = [super initWithWindowNibPath:nibPath owner:self])) { 86 DCHECK(parentWindow); 87 parentWindow_ = parentWindow; 88 DCHECK(extension); 89 extension_ = extension; 90 DCHECK(browser); 91 browser_ = browser; 92 icon_.reset([gfx::SkBitmapToNSImage(icon) retain]); 93 pageActionRemoved_ = NO; 94 95 if (!extension->omnibox_keyword().empty()) { 96 type_ = extension_installed_bubble::kOmniboxKeyword; 97 } else if (extension->browser_action()) { 98 type_ = extension_installed_bubble::kBrowserAction; 99 } else if (extension->page_action() && 100 !extension->page_action()->default_icon_path().empty()) { 101 type_ = extension_installed_bubble::kPageAction; 102 } else { 103 NOTREACHED(); // kGeneric installs handled in the extension_install_ui. 104 } 105 106 // Start showing window only after extension has fully loaded. 107 extensionObserver_.reset(new ExtensionLoadedNotificationObserver( 108 self, browser->profile())); 109 } 110 return self; 111 } 112 113 - (void)dealloc { 114 [[NSNotificationCenter defaultCenter] removeObserver:self]; 115 [super dealloc]; 116 } 117 118 - (void)close { 119 [parentWindow_ removeChildWindow:[self window]]; 120 [super close]; 121 } 122 123 - (void)windowWillClose:(NSNotification*)notification { 124 // Turn off page action icon preview when the window closes, unless we 125 // already removed it when the window resigned key status. 126 [self removePageActionPreviewIfNecessary]; 127 extension_ = NULL; 128 browser_ = NULL; 129 parentWindow_ = nil; 130 // We caught a close so we don't need to watch for the parent closing. 131 [[NSNotificationCenter defaultCenter] removeObserver:self]; 132 [self autorelease]; 133 } 134 135 // The controller is the delegate of the window, so it receives "did resign 136 // key" notifications. When key is resigned, close the window. 137 - (void)windowDidResignKey:(NSNotification*)notification { 138 NSWindow* window = [self window]; 139 DCHECK_EQ([notification object], window); 140 DCHECK([window isVisible]); 141 142 // If the browser window is closing, we need to remove the page action 143 // immediately, otherwise the closing animation may overlap with 144 // browser destruction. 145 [self removePageActionPreviewIfNecessary]; 146 [self close]; 147 } 148 149 - (IBAction)closeWindow:(id)sender { 150 DCHECK([[self window] isVisible]); 151 [self close]; 152 } 153 154 // Extracted to a function here so that it can be overwritten for unit 155 // testing. 156 - (void)removePageActionPreviewIfNecessary { 157 if (!extension_ || !extension_->page_action() || pageActionRemoved_) 158 return; 159 pageActionRemoved_ = YES; 160 161 BrowserWindowCocoa* window = 162 static_cast<BrowserWindowCocoa*>(browser_->window()); 163 LocationBarViewMac* locationBarView = 164 [window->cocoa_controller() locationBarBridge]; 165 locationBarView->SetPreviewEnabledPageAction(extension_->page_action(), 166 false); // disables preview. 167 } 168 169 // The extension installed bubble points at the browser action icon or the 170 // page action icon (shown as a preview), depending on the extension type. 171 // We need to calculate the location of these icons and the size of the 172 // message itself (which varies with the title of the extension) in order 173 // to figure out the origin point for the extension installed bubble. 174 // TODO(mirandac): add framework to easily test extension UI components! 175 - (NSPoint)calculateArrowPoint { 176 BrowserWindowCocoa* window = 177 static_cast<BrowserWindowCocoa*>(browser_->window()); 178 NSPoint arrowPoint = NSZeroPoint; 179 180 switch(type_) { 181 case extension_installed_bubble::kOmniboxKeyword: { 182 LocationBarViewMac* locationBarView = 183 [window->cocoa_controller() locationBarBridge]; 184 arrowPoint = locationBarView->GetPageInfoBubblePoint(); 185 break; 186 } 187 case extension_installed_bubble::kBrowserAction: { 188 BrowserActionsController* controller = 189 [[window->cocoa_controller() toolbarController] 190 browserActionsController]; 191 arrowPoint = [controller popupPointForBrowserAction:extension_]; 192 break; 193 } 194 case extension_installed_bubble::kPageAction: { 195 LocationBarViewMac* locationBarView = 196 [window->cocoa_controller() locationBarBridge]; 197 198 // Tell the location bar to show a preview of the page action icon, which 199 // would ordinarily only be displayed on a page of the appropriate type. 200 // We remove this preview when the extension installed bubble closes. 201 locationBarView->SetPreviewEnabledPageAction(extension_->page_action(), 202 true); 203 204 // Find the center of the bottom of the page action icon. 205 arrowPoint = 206 locationBarView->GetPageActionBubblePoint(extension_->page_action()); 207 break; 208 } 209 default: { 210 NOTREACHED() << "Generic extension type not allowed in install bubble."; 211 } 212 } 213 return arrowPoint; 214 } 215 216 // We want this to be a child of a browser window. addChildWindow: 217 // (called from this function) will bring the window on-screen; 218 // unfortunately, [NSWindowController showWindow:] will also bring it 219 // on-screen (but will cause unexpected changes to the window's 220 // position). We cannot have an addChildWindow: and a subsequent 221 // showWindow:. Thus, we have our own version. 222 - (void)showWindow:(id)sender { 223 // Generic extensions get an infobar rather than a bubble. 224 DCHECK(type_ != extension_installed_bubble::kGeneric); 225 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 226 227 // Load nib and calculate height based on messages to be shown. 228 NSWindow* window = [self initializeWindow]; 229 int newWindowHeight = [self calculateWindowHeight]; 230 [infoBubbleView_ setFrameSize:NSMakeSize( 231 NSWidth([[window contentView] bounds]), newWindowHeight)]; 232 NSSize windowDelta = NSMakeSize( 233 0, newWindowHeight - NSHeight([[window contentView] bounds])); 234 windowDelta = [[window contentView] convertSize:windowDelta toView:nil]; 235 NSRect newFrame = [window frame]; 236 newFrame.size.height += windowDelta.height; 237 [window setFrame:newFrame display:NO]; 238 239 // Now that we have resized the window, adjust y pos of the messages. 240 [self setMessageFrames:newWindowHeight]; 241 242 // Find window origin, taking into account bubble size and arrow location. 243 NSPoint origin = 244 [parentWindow_ convertBaseToScreen:[self calculateArrowPoint]]; 245 NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset + 246 info_bubble::kBubbleArrowWidth / 2.0, 0); 247 offsets = [[window contentView] convertSize:offsets toView:nil]; 248 if ([infoBubbleView_ arrowLocation] == info_bubble::kTopRight) 249 origin.x -= NSWidth([window frame]) - offsets.width; 250 origin.y -= NSHeight([window frame]); 251 [window setFrameOrigin:origin]; 252 253 [parentWindow_ addChildWindow:window 254 ordered:NSWindowAbove]; 255 [window makeKeyAndOrderFront:self]; 256 } 257 258 // Finish nib loading, set arrow location and load icon into window. This 259 // function is exposed for unit testing. 260 - (NSWindow*)initializeWindow { 261 NSWindow* window = [self window]; // completes nib load 262 263 if (type_ == extension_installed_bubble::kOmniboxKeyword) { 264 [infoBubbleView_ setArrowLocation:info_bubble::kTopLeft]; 265 } else { 266 [infoBubbleView_ setArrowLocation:info_bubble::kTopRight]; 267 } 268 269 // Set appropriate icon, resizing if necessary. 270 if ([icon_ size].width > extension_installed_bubble::kIconSize) { 271 [icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize, 272 extension_installed_bubble::kIconSize)]; 273 } 274 [iconImage_ setImage:icon_]; 275 [iconImage_ setNeedsDisplay:YES]; 276 return window; 277 } 278 279 // Calculate the height of each install message, resizing messages in their 280 // frames to fit window width. Return the new window height, based on the 281 // total of all message heights. 282 - (int)calculateWindowHeight { 283 // Adjust the window height to reflect the sum height of all messages 284 // and vertical padding. 285 int newWindowHeight = 2 * extension_installed_bubble::kOuterVerticalMargin; 286 287 // First part of extension installed message. 288 string16 extension_name = UTF8ToUTF16(extension_->name().c_str()); 289 base::i18n::AdjustStringForLocaleDirection(&extension_name); 290 [extensionInstalledMsg_ setStringValue:l10n_util::GetNSStringF( 291 IDS_EXTENSION_INSTALLED_HEADING, extension_name)]; 292 [GTMUILocalizerAndLayoutTweaker 293 sizeToFitFixedWidthTextField:extensionInstalledMsg_]; 294 newWindowHeight += [extensionInstalledMsg_ frame].size.height + 295 extension_installed_bubble::kInnerVerticalMargin; 296 297 // If type is page action, include a special message about page actions. 298 if (type_ == extension_installed_bubble::kPageAction) { 299 [extraInfoMsg_ setHidden:NO]; 300 [[extraInfoMsg_ cell] 301 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; 302 [GTMUILocalizerAndLayoutTweaker 303 sizeToFitFixedWidthTextField:extraInfoMsg_]; 304 newWindowHeight += [extraInfoMsg_ frame].size.height + 305 extension_installed_bubble::kInnerVerticalMargin; 306 } 307 308 // If type is omnibox keyword, include a special message about the keyword. 309 if (type_ == extension_installed_bubble::kOmniboxKeyword) { 310 [extraInfoMsg_ setStringValue:l10n_util::GetNSStringF( 311 IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO, 312 UTF8ToUTF16(extension_->omnibox_keyword()))]; 313 [extraInfoMsg_ setHidden:NO]; 314 [[extraInfoMsg_ cell] 315 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; 316 [GTMUILocalizerAndLayoutTweaker 317 sizeToFitFixedWidthTextField:extraInfoMsg_]; 318 newWindowHeight += [extraInfoMsg_ frame].size.height + 319 extension_installed_bubble::kInnerVerticalMargin; 320 } 321 322 // Second part of extension installed message. 323 [[extensionInstalledInfoMsg_ cell] 324 setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; 325 [GTMUILocalizerAndLayoutTweaker 326 sizeToFitFixedWidthTextField:extensionInstalledInfoMsg_]; 327 newWindowHeight += [extensionInstalledInfoMsg_ frame].size.height; 328 329 return newWindowHeight; 330 } 331 332 // Adjust y-position of messages to sit properly in new window height. 333 - (void)setMessageFrames:(int)newWindowHeight { 334 // The extension messages will always be shown. 335 NSRect extensionMessageFrame1 = [extensionInstalledMsg_ frame]; 336 NSRect extensionMessageFrame2 = [extensionInstalledInfoMsg_ frame]; 337 338 extensionMessageFrame1.origin.y = newWindowHeight - ( 339 extensionMessageFrame1.size.height + 340 extension_installed_bubble::kOuterVerticalMargin); 341 [extensionInstalledMsg_ setFrame:extensionMessageFrame1]; 342 if (type_ == extension_installed_bubble::kPageAction || 343 type_ == extension_installed_bubble::kOmniboxKeyword) { 344 // The extra message is only shown when appropriate. 345 NSRect extraMessageFrame = [extraInfoMsg_ frame]; 346 extraMessageFrame.origin.y = extensionMessageFrame1.origin.y - ( 347 extraMessageFrame.size.height + 348 extension_installed_bubble::kInnerVerticalMargin); 349 [extraInfoMsg_ setFrame:extraMessageFrame]; 350 extensionMessageFrame2.origin.y = extraMessageFrame.origin.y - ( 351 extensionMessageFrame2.size.height + 352 extension_installed_bubble::kInnerVerticalMargin); 353 } else { 354 extensionMessageFrame2.origin.y = extensionMessageFrame1.origin.y - ( 355 extensionMessageFrame2.size.height + 356 extension_installed_bubble::kInnerVerticalMargin); 357 } 358 [extensionInstalledInfoMsg_ setFrame:extensionMessageFrame2]; 359 } 360 361 // Exposed for unit testing. 362 - (NSRect)getExtensionInstalledMsgFrame { 363 return [extensionInstalledMsg_ frame]; 364 } 365 366 - (NSRect)getExtraInfoMsgFrame { 367 return [extraInfoMsg_ frame]; 368 } 369 370 - (NSRect)getExtensionInstalledInfoMsgFrame { 371 return [extensionInstalledInfoMsg_ frame]; 372 } 373 374 - (void)extensionUnloaded:(id)sender { 375 extension_ = NULL; 376 } 377 378 @end 379