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