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
34namespace {
35
36const int kIconSize = 43;
37
38const 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.
43const int kOuterMarginInset = 10;
44const int kHorizOuterMargin = views::kPanelHorizMargin - kOuterMarginInset;
45const int kVertOuterMargin = views::kPanelVertMargin - kOuterMarginInset;
46
47// Interior vertical margin is 8px smaller than standard
48const int kVertInnerMargin = views::kPanelVertMargin - 8;
49
50// The image we use for the close button has three pixels of whitespace padding.
51const 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.
55const int kRightcolumnVerticalShift = -4;
56
57// How long to wait for browser action animations to complete before retrying.
58const int kAnimationWaitTime = 50;
59
60// How often we retry when waiting for browser action animation to end.
61const int kAnimationWaitMaxRetry = 10;
62
63}  // namespace
64
65namespace browser {
66
67void 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.
80class InstalledBubbleContent : public views::View,
81                               public views::ButtonListener {
82 public:
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
147  void set_bubble(Bubble* bubble) { bubble_ = bubble; }
148
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:
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
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
236void ExtensionInstalledBubble::Show(const Extension* extension,
237                                    Browser *browser,
238                                    const SkBitmap& icon) {
239  new ExtensionInstalledBubble(extension, browser, icon);
240}
241
242ExtensionInstalledBubble::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
273ExtensionInstalledBubble::~ExtensionInstalledBubble() {}
274
275void 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
296void 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
362void 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
375bool ExtensionInstalledBubble::CloseOnEscape() {
376  return true;
377}
378
379bool ExtensionInstalledBubble::FadeInOnShow() {
380  return true;
381}
382