• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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|.
33class 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