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 |