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