summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/extension_installed_bubble_controller.mm
blob: f417ff72ffd23f7d0449a6495f3e1facebbc94d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "extension_installed_bubble_controller.h"

#include "app/l10n_util.h"
#include "base/i18n/rtl.h"
#include "base/mac_util.h"
#include "base/sys_string_conversions.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
#include "chrome/browser/ui/cocoa/browser_window_controller.h"
#include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
#include "chrome/browser/ui/cocoa/hover_close_button.h"
#include "chrome/browser/ui/cocoa/info_bubble_view.h"
#include "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
#include "chrome/browser/ui/cocoa/toolbar_controller.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/extension_action.h"
#include "chrome/common/notification_details.h"
#include "chrome/common/notification_registrar.h"
#include "chrome/common/notification_source.h"
#include "grit/generated_resources.h"
#import "skia/ext/skia_utils_mac.h"
#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"


// C++ class that receives EXTENSION_LOADED notifications and proxies them back
// to |controller|.
class ExtensionLoadedNotificationObserver : public NotificationObserver {
 public:
  ExtensionLoadedNotificationObserver(
      ExtensionInstalledBubbleController* controller, Profile* profile)
          : controller_(controller) {
    registrar_.Add(this, NotificationType::EXTENSION_LOADED,
        Source<Profile>(profile));
    registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
        Source<Profile>(profile));
  }

 private:
  // NotificationObserver implementation. Tells the controller to start showing
  // its window on the main thread when the extension has finished loading.
  void Observe(NotificationType type,
               const NotificationSource& source,
               const NotificationDetails& details) {
    if (type == NotificationType::EXTENSION_LOADED) {
      const Extension* extension = Details<const Extension>(details).ptr();
      if (extension == [controller_ extension]) {
        [controller_ performSelectorOnMainThread:@selector(showWindow:)
                                      withObject:controller_
                                   waitUntilDone:NO];
      }
    } else if (type == NotificationType::EXTENSION_UNLOADED) {
      const Extension* extension = Details<const Extension>(details).ptr();
      if (extension == [controller_ extension]) {
        [controller_ performSelectorOnMainThread:@selector(extensionUnloaded:)
                                      withObject:controller_
                                   waitUntilDone:NO];
      }
    } else {
      NOTREACHED() << "Received unexpected notification.";
    }
  }

  NotificationRegistrar registrar_;
  ExtensionInstalledBubbleController* controller_;  // weak, owns us
};

@implementation ExtensionInstalledBubbleController

@synthesize extension = extension_;
@synthesize pageActionRemoved = pageActionRemoved_;  // Exposed for unit test.

- (id)initWithParentWindow:(NSWindow*)parentWindow
                 extension:(const Extension*)extension
                   browser:(Browser*)browser
                      icon:(SkBitmap)icon {
  NSString* nibPath =
      [mac_util::MainAppBundle() pathForResource:@"ExtensionInstalledBubble"
                                          ofType:@"nib"];
  if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
    DCHECK(parentWindow);
    parentWindow_ = parentWindow;
    DCHECK(extension);
    extension_ = extension;
    DCHECK(browser);
    browser_ = browser;
    icon_.reset([gfx::SkBitmapToNSImage(icon) retain]);
    pageActionRemoved_ = NO;

    if (!extension->omnibox_keyword().empty()) {
      type_ = extension_installed_bubble::kOmniboxKeyword;
    } else if (extension->browser_action()) {
      type_ = extension_installed_bubble::kBrowserAction;
    } else if (extension->page_action() &&
               !extension->page_action()->default_icon_path().empty()) {
      type_ = extension_installed_bubble::kPageAction;
    } else {
      NOTREACHED();  // kGeneric installs handled in the extension_install_ui.
    }

    // Start showing window only after extension has fully loaded.
    extensionObserver_.reset(new ExtensionLoadedNotificationObserver(
        self, browser->profile()));
  }
  return self;
}

- (void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [super dealloc];
}

- (void)close {
  [parentWindow_ removeChildWindow:[self window]];
  [super close];
}

- (void)windowWillClose:(NSNotification*)notification {
  // Turn off page action icon preview when the window closes, unless we
  // already removed it when the window resigned key status.
  [self removePageActionPreviewIfNecessary];
  extension_ = NULL;
  browser_ = NULL;
  parentWindow_ = nil;
  // We caught a close so we don't need to watch for the parent closing.
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [self autorelease];
}

// The controller is the delegate of the window, so it receives "did resign
// key" notifications.  When key is resigned, close the window.
- (void)windowDidResignKey:(NSNotification*)notification {
  NSWindow* window = [self window];
  DCHECK_EQ([notification object], window);
  DCHECK([window isVisible]);

  // If the browser window is closing, we need to remove the page action
  // immediately, otherwise the closing animation may overlap with
  // browser destruction.
  [self removePageActionPreviewIfNecessary];
  [self close];
}

- (IBAction)closeWindow:(id)sender {
  DCHECK([[self window] isVisible]);
  [self close];
}

// Extracted to a function here so that it can be overwritten for unit
// testing.
- (void)removePageActionPreviewIfNecessary {
  if (!extension_ || !extension_->page_action() || pageActionRemoved_)
    return;
  pageActionRemoved_ = YES;

  BrowserWindowCocoa* window =
      static_cast<BrowserWindowCocoa*>(browser_->window());
  LocationBarViewMac* locationBarView =
      [window->cocoa_controller() locationBarBridge];
  locationBarView->SetPreviewEnabledPageAction(extension_->page_action(),
                                               false);  // disables preview.
}

// The extension installed bubble points at the browser action icon or the
// page action icon (shown as a preview), depending on the extension type.
// We need to calculate the location of these icons and the size of the
// message itself (which varies with the title of the extension) in order
// to figure out the origin point for the extension installed bubble.
// TODO(mirandac): add framework to easily test extension UI components!
- (NSPoint)calculateArrowPoint {
  BrowserWindowCocoa* window =
      static_cast<BrowserWindowCocoa*>(browser_->window());
  NSPoint arrowPoint = NSZeroPoint;

  switch(type_) {
    case extension_installed_bubble::kOmniboxKeyword: {
      LocationBarViewMac* locationBarView =
          [window->cocoa_controller() locationBarBridge];
      arrowPoint = locationBarView->GetPageInfoBubblePoint();
      break;
    }
    case extension_installed_bubble::kBrowserAction: {
      BrowserActionsController* controller =
          [[window->cocoa_controller() toolbarController]
              browserActionsController];
      arrowPoint = [controller popupPointForBrowserAction:extension_];
      break;
    }
    case extension_installed_bubble::kPageAction: {
      LocationBarViewMac* locationBarView =
          [window->cocoa_controller() locationBarBridge];

      // Tell the location bar to show a preview of the page action icon, which
      // would ordinarily only be displayed on a page of the appropriate type.
      // We remove this preview when the extension installed bubble closes.
      locationBarView->SetPreviewEnabledPageAction(extension_->page_action(),
                                                   true);

      // Find the center of the bottom of the page action icon.
      arrowPoint =
          locationBarView->GetPageActionBubblePoint(extension_->page_action());
      break;
    }
    default: {
      NOTREACHED() << "Generic extension type not allowed in install bubble.";
    }
  }
  return arrowPoint;
}

// We want this to be a child of a browser window.  addChildWindow:
// (called from this function) will bring the window on-screen;
// unfortunately, [NSWindowController showWindow:] will also bring it
// on-screen (but will cause unexpected changes to the window's
// position).  We cannot have an addChildWindow: and a subsequent
// showWindow:. Thus, we have our own version.
- (void)showWindow:(id)sender {
  // Generic extensions get an infobar rather than a bubble.
  DCHECK(type_ != extension_installed_bubble::kGeneric);
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  // Load nib and calculate height based on messages to be shown.
  NSWindow* window = [self initializeWindow];
  int newWindowHeight = [self calculateWindowHeight];
  [infoBubbleView_ setFrameSize:NSMakeSize(
      NSWidth([[window contentView] bounds]), newWindowHeight)];
  NSSize windowDelta = NSMakeSize(
      0, newWindowHeight - NSHeight([[window contentView] bounds]));
  windowDelta = [[window contentView] convertSize:windowDelta toView:nil];
  NSRect newFrame = [window frame];
  newFrame.size.height += windowDelta.height;
  [window setFrame:newFrame display:NO];

  // Now that we have resized the window, adjust y pos of the messages.
  [self setMessageFrames:newWindowHeight];

  // Find window origin, taking into account bubble size and arrow location.
  NSPoint origin =
      [parentWindow_ convertBaseToScreen:[self calculateArrowPoint]];
  NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
                              info_bubble::kBubbleArrowWidth / 2.0, 0);
  offsets = [[window contentView] convertSize:offsets toView:nil];
  if ([infoBubbleView_ arrowLocation] == info_bubble::kTopRight)
    origin.x -= NSWidth([window frame]) - offsets.width;
  origin.y -= NSHeight([window frame]);
  [window setFrameOrigin:origin];

  [parentWindow_ addChildWindow:window
                        ordered:NSWindowAbove];
  [window makeKeyAndOrderFront:self];
}

// Finish nib loading, set arrow location and load icon into window.  This
// function is exposed for unit testing.
- (NSWindow*)initializeWindow {
  NSWindow* window = [self window];  // completes nib load

  if (type_ == extension_installed_bubble::kOmniboxKeyword) {
    [infoBubbleView_ setArrowLocation:info_bubble::kTopLeft];
  } else {
    [infoBubbleView_ setArrowLocation:info_bubble::kTopRight];
  }

  // Set appropriate icon, resizing if necessary.
  if ([icon_ size].width > extension_installed_bubble::kIconSize) {
    [icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize,
                              extension_installed_bubble::kIconSize)];
  }
  [iconImage_ setImage:icon_];
  [iconImage_ setNeedsDisplay:YES];
  return window;
 }

// Calculate the height of each install message, resizing messages in their
// frames to fit window width.  Return the new window height, based on the
// total of all message heights.
- (int)calculateWindowHeight {
  // Adjust the window height to reflect the sum height of all messages
  // and vertical padding.
  int newWindowHeight = 2 * extension_installed_bubble::kOuterVerticalMargin;

  // First part of extension installed message.
  string16 extension_name = UTF8ToUTF16(extension_->name().c_str());
  base::i18n::AdjustStringForLocaleDirection(&extension_name);
  [extensionInstalledMsg_ setStringValue:l10n_util::GetNSStringF(
      IDS_EXTENSION_INSTALLED_HEADING, extension_name)];
  [GTMUILocalizerAndLayoutTweaker
      sizeToFitFixedWidthTextField:extensionInstalledMsg_];
  newWindowHeight += [extensionInstalledMsg_ frame].size.height +
      extension_installed_bubble::kInnerVerticalMargin;

  // If type is page action, include a special message about page actions.
  if (type_ == extension_installed_bubble::kPageAction) {
    [extraInfoMsg_ setHidden:NO];
    [[extraInfoMsg_ cell]
        setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
    [GTMUILocalizerAndLayoutTweaker
        sizeToFitFixedWidthTextField:extraInfoMsg_];
    newWindowHeight += [extraInfoMsg_ frame].size.height +
        extension_installed_bubble::kInnerVerticalMargin;
  }

  // If type is omnibox keyword, include a special message about the keyword.
  if (type_ == extension_installed_bubble::kOmniboxKeyword) {
    [extraInfoMsg_ setStringValue:l10n_util::GetNSStringF(
        IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO,
        UTF8ToUTF16(extension_->omnibox_keyword()))];
    [extraInfoMsg_ setHidden:NO];
    [[extraInfoMsg_ cell]
        setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
    [GTMUILocalizerAndLayoutTweaker
        sizeToFitFixedWidthTextField:extraInfoMsg_];
    newWindowHeight += [extraInfoMsg_ frame].size.height +
        extension_installed_bubble::kInnerVerticalMargin;
  }

  // Second part of extension installed message.
  [[extensionInstalledInfoMsg_ cell]
      setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
  [GTMUILocalizerAndLayoutTweaker
      sizeToFitFixedWidthTextField:extensionInstalledInfoMsg_];
  newWindowHeight += [extensionInstalledInfoMsg_ frame].size.height;

  return newWindowHeight;
}

// Adjust y-position of messages to sit properly in new window height.
- (void)setMessageFrames:(int)newWindowHeight {
  // The extension messages will always be shown.
  NSRect extensionMessageFrame1 = [extensionInstalledMsg_ frame];
  NSRect extensionMessageFrame2 = [extensionInstalledInfoMsg_ frame];

  extensionMessageFrame1.origin.y = newWindowHeight - (
      extensionMessageFrame1.size.height +
      extension_installed_bubble::kOuterVerticalMargin);
  [extensionInstalledMsg_ setFrame:extensionMessageFrame1];
  if (type_ == extension_installed_bubble::kPageAction ||
      type_ == extension_installed_bubble::kOmniboxKeyword) {
    // The extra message is only shown when appropriate.
    NSRect extraMessageFrame = [extraInfoMsg_ frame];
    extraMessageFrame.origin.y = extensionMessageFrame1.origin.y - (
        extraMessageFrame.size.height +
        extension_installed_bubble::kInnerVerticalMargin);
    [extraInfoMsg_ setFrame:extraMessageFrame];
    extensionMessageFrame2.origin.y = extraMessageFrame.origin.y - (
        extensionMessageFrame2.size.height +
        extension_installed_bubble::kInnerVerticalMargin);
  } else {
    extensionMessageFrame2.origin.y = extensionMessageFrame1.origin.y - (
        extensionMessageFrame2.size.height +
        extension_installed_bubble::kInnerVerticalMargin);
  }
  [extensionInstalledInfoMsg_ setFrame:extensionMessageFrame2];
}

// Exposed for unit testing.
- (NSRect)getExtensionInstalledMsgFrame {
  return [extensionInstalledMsg_ frame];
}

- (NSRect)getExtraInfoMsgFrame {
  return [extraInfoMsg_ frame];
}

- (NSRect)getExtensionInstalledInfoMsgFrame {
  return [extensionInstalledInfoMsg_ frame];
}

- (void)extensionUnloaded:(id)sender {
  extension_ = NULL;
}

@end