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