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_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