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 #include "chrome/browser/ui/views/extensions/extension_installed_bubble.h"
6
7 #include <algorithm>
8
9 #include "base/i18n/rtl.h"
10 #include "base/message_loop.h"
11 #include "base/utf_string_conversions.h"
12 #include "chrome/browser/profiles/profile.h"
13 #include "chrome/browser/ui/browser.h"
14 #include "chrome/browser/ui/browser_window.h"
15 #include "chrome/browser/ui/views/browser_actions_container.h"
16 #include "chrome/browser/ui/views/frame/browser_view.h"
17 #include "chrome/browser/ui/views/location_bar/location_bar_view.h"
18 #include "chrome/browser/ui/views/toolbar_view.h"
19 #include "chrome/common/extensions/extension.h"
20 #include "chrome/common/extensions/extension_action.h"
21 #include "content/common/notification_details.h"
22 #include "content/common/notification_source.h"
23 #include "content/common/notification_type.h"
24 #include "grit/generated_resources.h"
25 #include "grit/theme_resources.h"
26 #include "ui/base/l10n/l10n_util.h"
27 #include "ui/base/resource/resource_bundle.h"
28 #include "views/controls/button/image_button.h"
29 #include "views/controls/image_view.h"
30 #include "views/controls/label.h"
31 #include "views/layout/layout_constants.h"
32 #include "views/view.h"
33
34 namespace {
35
36 const int kIconSize = 43;
37
38 const int kRightColumnWidth = 285;
39
40 // The Bubble uses a BubbleBorder which adds about 6 pixels of whitespace
41 // around the content view. We compensate by reducing our outer borders by this
42 // amount + 4px.
43 const int kOuterMarginInset = 10;
44 const int kHorizOuterMargin = views::kPanelHorizMargin - kOuterMarginInset;
45 const int kVertOuterMargin = views::kPanelVertMargin - kOuterMarginInset;
46
47 // Interior vertical margin is 8px smaller than standard
48 const int kVertInnerMargin = views::kPanelVertMargin - 8;
49
50 // The image we use for the close button has three pixels of whitespace padding.
51 const int kCloseButtonPadding = 3;
52
53 // We want to shift the right column (which contains the header and text) up
54 // 4px to align with icon.
55 const int kRightcolumnVerticalShift = -4;
56
57 // How long to wait for browser action animations to complete before retrying.
58 const int kAnimationWaitTime = 50;
59
60 // How often we retry when waiting for browser action animation to end.
61 const int kAnimationWaitMaxRetry = 10;
62
63 } // namespace
64
65 namespace browser {
66
ShowExtensionInstalledBubble(const Extension * extension,Browser * browser,const SkBitmap & icon,Profile * profile)67 void ShowExtensionInstalledBubble(
68 const Extension* extension,
69 Browser* browser,
70 const SkBitmap& icon,
71 Profile* profile) {
72 ExtensionInstalledBubble::Show(extension, browser, icon);
73 }
74
75 } // namespace browser
76
77 // InstalledBubbleContent is the content view which is placed in the
78 // ExtensionInstalledBubble. It displays the install icon and explanatory
79 // text about the installed extension.
80 class InstalledBubbleContent : public views::View,
81 public views::ButtonListener {
82 public:
InstalledBubbleContent(const Extension * extension,ExtensionInstalledBubble::BubbleType type,SkBitmap * icon)83 InstalledBubbleContent(const Extension* extension,
84 ExtensionInstalledBubble::BubbleType type,
85 SkBitmap* icon)
86 : bubble_(NULL),
87 type_(type),
88 info_(NULL) {
89 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
90 const gfx::Font& font = rb.GetFont(ResourceBundle::BaseFont);
91
92 // Scale down to 43x43, but allow smaller icons (don't scale up).
93 gfx::Size size(icon->width(), icon->height());
94 if (size.width() > kIconSize || size.height() > kIconSize)
95 size = gfx::Size(kIconSize, kIconSize);
96 icon_ = new views::ImageView();
97 icon_->SetImageSize(size);
98 icon_->SetImage(*icon);
99 AddChildView(icon_);
100
101 string16 extension_name = UTF8ToUTF16(extension->name());
102 base::i18n::AdjustStringForLocaleDirection(&extension_name);
103 heading_ = new views::Label(UTF16ToWide(
104 l10n_util::GetStringFUTF16(IDS_EXTENSION_INSTALLED_HEADING,
105 extension_name)));
106 heading_->SetFont(rb.GetFont(ResourceBundle::MediumFont));
107 heading_->SetMultiLine(true);
108 heading_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
109 AddChildView(heading_);
110
111 if (type_ == ExtensionInstalledBubble::PAGE_ACTION) {
112 info_ = new views::Label(UTF16ToWide(l10n_util::GetStringUTF16(
113 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO)));
114 info_->SetFont(font);
115 info_->SetMultiLine(true);
116 info_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
117 AddChildView(info_);
118 }
119
120 if (type_ == ExtensionInstalledBubble::OMNIBOX_KEYWORD) {
121 info_ = new views::Label(UTF16ToWide(l10n_util::GetStringFUTF16(
122 IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO,
123 UTF8ToUTF16(extension->omnibox_keyword()))));
124 info_->SetFont(font);
125 info_->SetMultiLine(true);
126 info_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
127 AddChildView(info_);
128 }
129
130 manage_ = new views::Label(UTF16ToWide(
131 l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_MANAGE_INFO)));
132 manage_->SetFont(font);
133 manage_->SetMultiLine(true);
134 manage_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
135 AddChildView(manage_);
136
137 close_button_ = new views::ImageButton(this);
138 close_button_->SetImage(views::CustomButton::BS_NORMAL,
139 rb.GetBitmapNamed(IDR_CLOSE_BAR));
140 close_button_->SetImage(views::CustomButton::BS_HOT,
141 rb.GetBitmapNamed(IDR_CLOSE_BAR_H));
142 close_button_->SetImage(views::CustomButton::BS_PUSHED,
143 rb.GetBitmapNamed(IDR_CLOSE_BAR_P));
144 AddChildView(close_button_);
145 }
146
set_bubble(Bubble * bubble)147 void set_bubble(Bubble* bubble) { bubble_ = bubble; }
148
ButtonPressed(views::Button * sender,const views::Event & event)149 virtual void ButtonPressed(
150 views::Button* sender,
151 const views::Event& event) {
152 if (sender == close_button_) {
153 bubble_->set_fade_away_on_close(true);
154 GetWidget()->Close();
155 } else {
156 NOTREACHED() << "Unknown view";
157 }
158 }
159
160 private:
GetPreferredSize()161 virtual gfx::Size GetPreferredSize() {
162 int width = kHorizOuterMargin;
163 width += kIconSize;
164 width += views::kPanelHorizMargin;
165 width += kRightColumnWidth;
166 width += 2 * views::kPanelHorizMargin;
167 width += kHorizOuterMargin;
168
169 int height = kVertOuterMargin;
170 height += heading_->GetHeightForWidth(kRightColumnWidth);
171 height += kVertInnerMargin;
172 if (type_ == ExtensionInstalledBubble::PAGE_ACTION ||
173 type_ == ExtensionInstalledBubble::OMNIBOX_KEYWORD) {
174 height += info_->GetHeightForWidth(kRightColumnWidth);
175 height += kVertInnerMargin;
176 }
177 height += manage_->GetHeightForWidth(kRightColumnWidth);
178 height += kVertOuterMargin;
179
180 return gfx::Size(width, std::max(height, kIconSize + 2 * kVertOuterMargin));
181 }
182
Layout()183 virtual void Layout() {
184 int x = kHorizOuterMargin;
185 int y = kVertOuterMargin;
186
187 icon_->SetBounds(x, y, kIconSize, kIconSize);
188 x += kIconSize;
189 x += views::kPanelHorizMargin;
190
191 y += kRightcolumnVerticalShift;
192 heading_->SizeToFit(kRightColumnWidth);
193 heading_->SetX(x);
194 heading_->SetY(y);
195 y += heading_->height();
196 y += kVertInnerMargin;
197
198 if (type_ == ExtensionInstalledBubble::PAGE_ACTION ||
199 type_ == ExtensionInstalledBubble::OMNIBOX_KEYWORD) {
200 info_->SizeToFit(kRightColumnWidth);
201 info_->SetX(x);
202 info_->SetY(y);
203 y += info_->height();
204 y += kVertInnerMargin;
205 }
206
207 manage_->SizeToFit(kRightColumnWidth);
208 manage_->SetX(x);
209 manage_->SetY(y);
210 y += manage_->height();
211 y += kVertInnerMargin;
212
213 gfx::Size sz;
214 x += kRightColumnWidth + 2 * views::kPanelHorizMargin + kHorizOuterMargin -
215 close_button_->GetPreferredSize().width();
216 y = kVertOuterMargin;
217 sz = close_button_->GetPreferredSize();
218 // x-1 & y-1 is just slop to get the close button visually aligned with the
219 // title text and bubble arrow.
220 close_button_->SetBounds(x - 1, y - 1, sz.width(), sz.height());
221 }
222
223 // The Bubble showing us.
224 Bubble* bubble_;
225
226 ExtensionInstalledBubble::BubbleType type_;
227 views::ImageView* icon_;
228 views::Label* heading_;
229 views::Label* info_;
230 views::Label* manage_;
231 views::ImageButton* close_button_;
232
233 DISALLOW_COPY_AND_ASSIGN(InstalledBubbleContent);
234 };
235
Show(const Extension * extension,Browser * browser,const SkBitmap & icon)236 void ExtensionInstalledBubble::Show(const Extension* extension,
237 Browser *browser,
238 const SkBitmap& icon) {
239 new ExtensionInstalledBubble(extension, browser, icon);
240 }
241
ExtensionInstalledBubble(const Extension * extension,Browser * browser,const SkBitmap & icon)242 ExtensionInstalledBubble::ExtensionInstalledBubble(const Extension* extension,
243 Browser *browser,
244 const SkBitmap& icon)
245 : extension_(extension),
246 browser_(browser),
247 icon_(icon),
248 animation_wait_retries_(0) {
249 AddRef(); // Balanced in BubbleClosing.
250
251 if (!extension_->omnibox_keyword().empty()) {
252 type_ = OMNIBOX_KEYWORD;
253 } else if (extension_->browser_action()) {
254 type_ = BROWSER_ACTION;
255 } else if (extension->page_action() &&
256 !extension->page_action()->default_icon_path().empty()) {
257 type_ = PAGE_ACTION;
258 } else {
259 type_ = GENERIC;
260 }
261
262 // |extension| has been initialized but not loaded at this point. We need
263 // to wait on showing the Bubble until not only the EXTENSION_LOADED gets
264 // fired, but all of the EXTENSION_LOADED Observers have run. Only then can we
265 // be sure that a BrowserAction or PageAction has had views created which we
266 // can inspect for the purpose of previewing of pointing to them.
267 registrar_.Add(this, NotificationType::EXTENSION_LOADED,
268 Source<Profile>(browser->profile()));
269 registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
270 Source<Profile>(browser->profile()));
271 }
272
~ExtensionInstalledBubble()273 ExtensionInstalledBubble::~ExtensionInstalledBubble() {}
274
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)275 void ExtensionInstalledBubble::Observe(NotificationType type,
276 const NotificationSource& source,
277 const NotificationDetails& details) {
278 if (type == NotificationType::EXTENSION_LOADED) {
279 const Extension* extension = Details<const Extension>(details).ptr();
280 if (extension == extension_) {
281 animation_wait_retries_ = 0;
282 // PostTask to ourself to allow all EXTENSION_LOADED Observers to run.
283 MessageLoopForUI::current()->PostTask(FROM_HERE, NewRunnableMethod(this,
284 &ExtensionInstalledBubble::ShowInternal));
285 }
286 } else if (type == NotificationType::EXTENSION_UNLOADED) {
287 const Extension* extension =
288 Details<UnloadedExtensionInfo>(details)->extension;
289 if (extension == extension_)
290 extension_ = NULL;
291 } else {
292 NOTREACHED() << L"Received unexpected notification";
293 }
294 }
295
ShowInternal()296 void ExtensionInstalledBubble::ShowInternal() {
297 BrowserView* browser_view = BrowserView::GetBrowserViewForNativeWindow(
298 browser_->window()->GetNativeHandle());
299
300 const views::View* reference_view = NULL;
301 if (type_ == BROWSER_ACTION) {
302 BrowserActionsContainer* container =
303 browser_view->GetToolbarView()->browser_actions();
304 if (container->animating() &&
305 animation_wait_retries_++ < kAnimationWaitMaxRetry) {
306 // We don't know where the view will be until the container has stopped
307 // animating, so check back in a little while.
308 MessageLoopForUI::current()->PostDelayedTask(
309 FROM_HERE, NewRunnableMethod(this,
310 &ExtensionInstalledBubble::ShowInternal), kAnimationWaitTime);
311 return;
312 }
313 reference_view = container->GetBrowserActionView(
314 extension_->browser_action());
315 // If the view is not visible then it is in the chevron, so point the
316 // install bubble to the chevron instead. If this is an incognito window,
317 // both could be invisible.
318 if (!reference_view || !reference_view->IsVisible()) {
319 reference_view = container->chevron();
320 if (!reference_view || !reference_view->IsVisible())
321 reference_view = NULL; // fall back to app menu below.
322 }
323 } else if (type_ == PAGE_ACTION) {
324 LocationBarView* location_bar_view = browser_view->GetLocationBarView();
325 location_bar_view->SetPreviewEnabledPageAction(extension_->page_action(),
326 true); // preview_enabled
327 reference_view = location_bar_view->GetPageActionView(
328 extension_->page_action());
329 DCHECK(reference_view);
330 } else if (type_ == OMNIBOX_KEYWORD) {
331 LocationBarView* location_bar_view = browser_view->GetLocationBarView();
332 reference_view = location_bar_view;
333 DCHECK(reference_view);
334 }
335
336 // Default case.
337 if (reference_view == NULL)
338 reference_view = browser_view->GetToolbarView()->app_menu();
339
340 gfx::Point origin;
341 views::View::ConvertPointToScreen(reference_view, &origin);
342 gfx::Rect bounds = reference_view->bounds();
343 bounds.set_origin(origin);
344 BubbleBorder::ArrowLocation arrow_location = BubbleBorder::TOP_RIGHT;
345
346 // For omnibox keyword bubbles, move the arrow to point to the left edge
347 // of the omnibox, just to the right of the icon.
348 if (type_ == OMNIBOX_KEYWORD) {
349 bounds.set_origin(
350 browser_view->GetLocationBarView()->GetLocationEntryOrigin());
351 bounds.set_width(0);
352 arrow_location = BubbleBorder::TOP_LEFT;
353 }
354
355 bubble_content_ = new InstalledBubbleContent(extension_, type_, &icon_);
356 Bubble* bubble = Bubble::Show(browser_view->GetWidget(), bounds,
357 arrow_location, bubble_content_, this);
358 bubble_content_->set_bubble(bubble);
359 }
360
361 // BubbleDelegate
BubbleClosing(Bubble * bubble,bool closed_by_escape)362 void ExtensionInstalledBubble::BubbleClosing(Bubble* bubble,
363 bool closed_by_escape) {
364 if (extension_ && type_ == PAGE_ACTION) {
365 BrowserView* browser_view = BrowserView::GetBrowserViewForNativeWindow(
366 browser_->window()->GetNativeHandle());
367 browser_view->GetLocationBarView()->SetPreviewEnabledPageAction(
368 extension_->page_action(),
369 false); // preview_enabled
370 }
371
372 Release(); // Balanced in ctor.
373 }
374
CloseOnEscape()375 bool ExtensionInstalledBubble::CloseOnEscape() {
376 return true;
377 }
378
FadeInOnShow()379 bool ExtensionInstalledBubble::FadeInOnShow() {
380 return true;
381 }
382