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_popup_controller.h"
      6 
      7 #include <algorithm>
      8 
      9 #include "chrome/browser/debugger/devtools_manager.h"
     10 #include "chrome/browser/extensions/extension_host.h"
     11 #include "chrome/browser/extensions/extension_process_manager.h"
     12 #include "chrome/browser/profiles/profile.h"
     13 #include "chrome/browser/ui/browser.h"
     14 #import "chrome/browser/ui/cocoa/browser_window_cocoa.h"
     15 #import "chrome/browser/ui/cocoa/extensions/extension_view_mac.h"
     16 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
     17 #include "content/common/notification_details.h"
     18 #include "content/common/notification_registrar.h"
     19 #include "content/common/notification_source.h"
     20 
     21 namespace {
     22 // The duration for any animations that might be invoked by this controller.
     23 const NSTimeInterval kAnimationDuration = 0.2;
     24 
     25 // There should only be one extension popup showing at one time. Keep a
     26 // reference to it here.
     27 static ExtensionPopupController* gPopup;
     28 
     29 // Given a value and a rage, clamp the value into the range.
     30 CGFloat Clamp(CGFloat value, CGFloat min, CGFloat max) {
     31   return std::max(min, std::min(max, value));
     32 }
     33 
     34 }  // namespace
     35 
     36 class DevtoolsNotificationBridge : public NotificationObserver {
     37  public:
     38   explicit DevtoolsNotificationBridge(ExtensionPopupController* controller)
     39     : controller_(controller) {}
     40 
     41   void Observe(NotificationType type,
     42                const NotificationSource& source,
     43                const NotificationDetails& details) {
     44     switch (type.value) {
     45       case NotificationType::EXTENSION_HOST_DID_STOP_LOADING: {
     46         if (Details<ExtensionHost>([controller_ extensionHost]) == details)
     47           [controller_ showDevTools];
     48         break;
     49       }
     50       case NotificationType::DEVTOOLS_WINDOW_CLOSING: {
     51         RenderViewHost* rvh = [controller_ extensionHost]->render_view_host();
     52         if (Details<RenderViewHost>(rvh) == details)
     53           // Allow the devtools to finish detaching before we close the popup
     54           [controller_ performSelector:@selector(close)
     55                             withObject:nil
     56                             afterDelay:0.0];
     57         break;
     58       }
     59       default: {
     60         NOTREACHED() << "Received unexpected notification";
     61         break;
     62       }
     63     };
     64   }
     65 
     66  private:
     67   ExtensionPopupController* controller_;
     68 };
     69 
     70 @interface ExtensionPopupController(Private)
     71 // Callers should be using the public static method for initialization.
     72 // NOTE: This takes ownership of |host|.
     73 - (id)initWithHost:(ExtensionHost*)host
     74       parentWindow:(NSWindow*)parentWindow
     75         anchoredAt:(NSPoint)anchoredAt
     76      arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
     77            devMode:(BOOL)devMode;
     78 
     79 // Called when the extension's hosted NSView has been resized.
     80 - (void)extensionViewFrameChanged;
     81 @end
     82 
     83 @implementation ExtensionPopupController
     84 
     85 - (id)initWithHost:(ExtensionHost*)host
     86       parentWindow:(NSWindow*)parentWindow
     87         anchoredAt:(NSPoint)anchoredAt
     88      arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
     89            devMode:(BOOL)devMode {
     90 
     91   parentWindow_ = parentWindow;
     92   anchor_ = [parentWindow convertBaseToScreen:anchoredAt];
     93   host_.reset(host);
     94   beingInspected_ = devMode;
     95 
     96   scoped_nsobject<InfoBubbleView> view([[InfoBubbleView alloc] init]);
     97   if (!view.get())
     98     return nil;
     99   [view setArrowLocation:arrowLocation];
    100 
    101   host->view()->set_is_toolstrip(NO);
    102 
    103   extensionView_ = host->view()->native_view();
    104   NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
    105   [center addObserver:self
    106              selector:@selector(extensionViewFrameChanged)
    107                  name:NSViewFrameDidChangeNotification
    108                object:extensionView_];
    109 
    110   // Watch to see if the parent window closes, and if so, close this one.
    111   [center addObserver:self
    112              selector:@selector(parentWindowWillClose:)
    113                  name:NSWindowWillCloseNotification
    114                object:parentWindow_];
    115 
    116   [view addSubview:extensionView_];
    117   scoped_nsobject<InfoBubbleWindow> window(
    118       [[InfoBubbleWindow alloc]
    119           initWithContentRect:NSZeroRect
    120                     styleMask:NSBorderlessWindowMask
    121                       backing:NSBackingStoreBuffered
    122                         defer:YES]);
    123   if (!window.get())
    124     return nil;
    125 
    126   [window setDelegate:self];
    127   [window setContentView:view];
    128   self = [super initWithWindow:window];
    129   if (beingInspected_) {
    130     // Listen for the the devtools window closing.
    131     notificationBridge_.reset(new DevtoolsNotificationBridge(self));
    132     registrar_.reset(new NotificationRegistrar);
    133     registrar_->Add(notificationBridge_.get(),
    134                     NotificationType::DEVTOOLS_WINDOW_CLOSING,
    135                     Source<Profile>(host->profile()));
    136     registrar_->Add(notificationBridge_.get(),
    137                     NotificationType::EXTENSION_HOST_DID_STOP_LOADING,
    138                     Source<Profile>(host->profile()));
    139   }
    140   return self;
    141 }
    142 
    143 - (void)showDevTools {
    144   DevToolsManager::GetInstance()->OpenDevToolsWindow(host_->render_view_host());
    145 }
    146 
    147 - (void)dealloc {
    148   [[NSNotificationCenter defaultCenter] removeObserver:self];
    149   [super dealloc];
    150 }
    151 
    152 - (void)parentWindowWillClose:(NSNotification*)notification {
    153   [self close];
    154 }
    155 
    156 - (void)windowWillClose:(NSNotification *)notification {
    157   [[NSNotificationCenter defaultCenter] removeObserver:self];
    158   [gPopup autorelease];
    159   gPopup = nil;
    160 }
    161 
    162 - (void)windowDidResignKey:(NSNotification *)notification {
    163   NSWindow* window = [self window];
    164   DCHECK_EQ([notification object], window);
    165   // If the window isn't visible, it is already closed, and this notification
    166   // has been sent as part of the closing operation, so no need to close.
    167   if ([window isVisible] && !beingInspected_) {
    168     [self close];
    169   }
    170 }
    171 
    172 - (void)close {
    173   [parentWindow_ removeChildWindow:[self window]];
    174 
    175   // No longer have a parent window, so nil out the pointer and deregister for
    176   // notifications.
    177   NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
    178   [center removeObserver:self
    179                     name:NSWindowWillCloseNotification
    180                   object:parentWindow_];
    181   parentWindow_ = nil;
    182   [super close];
    183 }
    184 
    185 - (BOOL)isClosing {
    186   return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
    187 }
    188 
    189 - (ExtensionHost*)extensionHost {
    190   return host_.get();
    191 }
    192 
    193 + (ExtensionPopupController*)showURL:(GURL)url
    194                            inBrowser:(Browser*)browser
    195                           anchoredAt:(NSPoint)anchoredAt
    196                        arrowLocation:(info_bubble::BubbleArrowLocation)
    197                                          arrowLocation
    198                              devMode:(BOOL)devMode {
    199   DCHECK([NSThread isMainThread]);
    200   DCHECK(browser);
    201   if (!browser)
    202     return nil;
    203 
    204   ExtensionProcessManager* manager =
    205       browser->profile()->GetExtensionProcessManager();
    206   DCHECK(manager);
    207   if (!manager)
    208     return nil;
    209 
    210   ExtensionHost* host = manager->CreatePopup(url, browser);
    211   DCHECK(host);
    212   if (!host)
    213     return nil;
    214 
    215   // Make absolutely sure that no popups are leaked.
    216   if (gPopup) {
    217     if ([[gPopup window] isVisible])
    218       [gPopup close];
    219 
    220     [gPopup autorelease];
    221     gPopup = nil;
    222   }
    223   DCHECK(!gPopup);
    224 
    225   // Takes ownership of |host|. Also will autorelease itself when the popup is
    226   // closed, so no need to do that here.
    227   gPopup = [[ExtensionPopupController alloc]
    228       initWithHost:host
    229       parentWindow:browser->window()->GetNativeHandle()
    230         anchoredAt:anchoredAt
    231      arrowLocation:arrowLocation
    232            devMode:devMode];
    233   return gPopup;
    234 }
    235 
    236 + (ExtensionPopupController*)popup {
    237   return gPopup;
    238 }
    239 
    240 - (void)extensionViewFrameChanged {
    241   // If there are no changes in the width or height of the frame, then ignore.
    242   if (NSEqualSizes([extensionView_ frame].size, extensionFrame_.size))
    243     return;
    244 
    245   extensionFrame_ = [extensionView_ frame];
    246   // Constrain the size of the view.
    247   [extensionView_ setFrameSize:NSMakeSize(
    248       Clamp(NSWidth(extensionFrame_),
    249             ExtensionViewMac::kMinWidth,
    250             ExtensionViewMac::kMaxWidth),
    251       Clamp(NSHeight(extensionFrame_),
    252             ExtensionViewMac::kMinHeight,
    253             ExtensionViewMac::kMaxHeight))];
    254 
    255   // Pad the window by half of the rounded corner radius to prevent the
    256   // extension's view from bleeding out over the corners.
    257   CGFloat inset = info_bubble::kBubbleCornerRadius / 2.0;
    258   [extensionView_ setFrameOrigin:NSMakePoint(inset, inset)];
    259 
    260   NSRect frame = [extensionView_ frame];
    261   frame.size.height += info_bubble::kBubbleArrowHeight +
    262                        info_bubble::kBubbleCornerRadius;
    263   frame.size.width += info_bubble::kBubbleCornerRadius;
    264   frame = [extensionView_ convertRectToBase:frame];
    265   // Adjust the origin according to the height and width so that the arrow is
    266   // positioned correctly at the middle and slightly down from the button.
    267   NSPoint windowOrigin = anchor_;
    268   NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
    269                                   info_bubble::kBubbleArrowWidth / 2.0,
    270                               info_bubble::kBubbleArrowHeight / 2.0);
    271   offsets = [extensionView_ convertSize:offsets toView:nil];
    272   windowOrigin.x -= NSWidth(frame) - offsets.width;
    273   windowOrigin.y -= NSHeight(frame) - offsets.height;
    274   frame.origin = windowOrigin;
    275 
    276   // Is the window still animating in? If so, then cancel that and create a new
    277   // animation setting the opacity and new frame value. Otherwise the current
    278   // animation will continue after this frame is set, reverting the frame to
    279   // what it was when the animation started.
    280   NSWindow* window = [self window];
    281   if ([window isVisible] && [[window animator] alphaValue] < 1.0) {
    282     [NSAnimationContext beginGrouping];
    283     [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
    284     [[window animator] setAlphaValue:1.0];
    285     [[window animator] setFrame:frame display:YES];
    286     [NSAnimationContext endGrouping];
    287   } else {
    288     [window setFrame:frame display:YES];
    289   }
    290 
    291   // A NSViewFrameDidChangeNotification won't be sent until the extension view
    292   // content is loaded. The window is hidden on init, so show it the first time
    293   // the notification is fired (and consequently the view contents have loaded).
    294   if (![window isVisible]) {
    295     [self showWindow:self];
    296   }
    297 }
    298 
    299 // We want this to be a child of a browser window. addChildWindow: (called from
    300 // this function) will bring the window on-screen; unfortunately,
    301 // [NSWindowController showWindow:] will also bring it on-screen (but will cause
    302 // unexpected changes to the window's position). We cannot have an
    303 // addChildWindow: and a subsequent showWindow:. Thus, we have our own version.
    304 - (void)showWindow:(id)sender {
    305   [parentWindow_ addChildWindow:[self window] ordered:NSWindowAbove];
    306   [[self window] makeKeyAndOrderFront:self];
    307 }
    308 
    309 - (void)windowDidResize:(NSNotification*)notification {
    310   // Let the extension view know, so that it can tell plugins.
    311   if (host_->view())
    312     host_->view()->WindowFrameChanged();
    313 }
    314 
    315 - (void)windowDidMove:(NSNotification*)notification {
    316   // Let the extension view know, so that it can tell plugins.
    317   if (host_->view())
    318     host_->view()->WindowFrameChanged();
    319 }
    320 
    321 // Private (TestingAPI)
    322 - (NSView*)view {
    323   return extensionView_;
    324 }
    325 
    326 // Private (TestingAPI)
    327 + (NSSize)minPopupSize {
    328   NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
    329   return minSize;
    330 }
    331 
    332 // Private (TestingAPI)
    333 + (NSSize)maxPopupSize {
    334   NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};
    335   return maxSize;
    336 }
    337 
    338 @end
    339