| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2010 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 "extension_installed_bubble_controller.h" | |
| 6 | |
| 7 #include "app/l10n_util.h" | |
| 8 #include "base/i18n/rtl.h" | |
| 9 #include "base/mac/mac_util.h" | |
| 10 #include "base/sys_string_conversions.h" | |
| 11 #include "base/utf_string_conversions.h" | |
| 12 #include "chrome/browser/ui/cocoa/browser_window_cocoa.h" | |
| 13 #include "chrome/browser/ui/cocoa/browser_window_controller.h" | |
| 14 #include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h" | |
| 15 #include "chrome/browser/ui/cocoa/hover_close_button.h" | |
| 16 #include "chrome/browser/ui/cocoa/info_bubble_view.h" | |
| 17 #include "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h" | |
| 18 #include "chrome/browser/ui/cocoa/toolbar_controller.h" | |
| 19 #include "chrome/browser/ui/browser.h" | |
| 20 #include "chrome/browser/ui/browser_window.h" | |
| 21 #include "chrome/common/extensions/extension.h" | |
| 22 #include "chrome/common/extensions/extension_action.h" | |
| 23 #include "chrome/common/notification_details.h" | |
| 24 #include "chrome/common/notification_registrar.h" | |
| 25 #include "chrome/common/notification_source.h" | |
| 26 #include "grit/generated_resources.h" | |
| 27 #import "skia/ext/skia_utils_mac.h" | |
| 28 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.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 | |
| OLD | NEW |