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/gtk/extensions/extension_installed_bubble_gtk.h" 6 7 #include <string> 8 9 #include "base/i18n/rtl.h" 10 #include "base/message_loop.h" 11 #include "base/utf_string_conversions.h" 12 #include "chrome/browser/ui/browser.h" 13 #include "chrome/browser/ui/browser_dialogs.h" 14 #include "chrome/browser/ui/gtk/browser_actions_toolbar_gtk.h" 15 #include "chrome/browser/ui/gtk/browser_toolbar_gtk.h" 16 #include "chrome/browser/ui/gtk/browser_window_gtk.h" 17 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 18 #include "chrome/browser/ui/gtk/gtk_util.h" 19 #include "chrome/browser/ui/gtk/location_bar_view_gtk.h" 20 #include "chrome/common/extensions/extension.h" 21 #include "chrome/common/extensions/extension_action.h" 22 #include "content/common/notification_details.h" 23 #include "content/common/notification_source.h" 24 #include "content/common/notification_type.h" 25 #include "grit/generated_resources.h" 26 #include "grit/theme_resources.h" 27 #include "ui/base/l10n/l10n_util.h" 28 #include "ui/base/resource/resource_bundle.h" 29 #include "ui/gfx/gtk_util.h" 30 31 namespace { 32 33 const int kHorizontalColumnSpacing = 10; 34 const int kIconPadding = 3; 35 const int kIconSize = 43; 36 const int kTextColumnVerticalSpacing = 7; 37 const int kTextColumnWidth = 350; 38 39 // When showing the bubble for a new browser action, we may have to wait for 40 // the toolbar to finish animating to know where the item's final position 41 // will be. 42 const int kAnimationWaitRetries = 10; 43 const int kAnimationWaitMS = 50; 44 45 // Padding between content and edge of info bubble. 46 const int kContentBorder = 7; 47 48 } // namespace 49 50 namespace browser { 51 52 void ShowExtensionInstalledBubble( 53 const Extension* extension, 54 Browser* browser, 55 const SkBitmap& icon, 56 Profile* profile) { 57 ExtensionInstalledBubbleGtk::Show(extension, browser, icon); 58 } 59 60 } // namespace browser 61 62 void ExtensionInstalledBubbleGtk::Show(const Extension* extension, 63 Browser* browser, 64 const SkBitmap& icon) { 65 new ExtensionInstalledBubbleGtk(extension, browser, icon); 66 } 67 68 ExtensionInstalledBubbleGtk::ExtensionInstalledBubbleGtk( 69 const Extension* extension, Browser *browser, const SkBitmap& icon) 70 : extension_(extension), 71 browser_(browser), 72 icon_(icon), 73 animation_wait_retries_(kAnimationWaitRetries) { 74 AddRef(); // Balanced in Close(). 75 76 if (!extension_->omnibox_keyword().empty()) { 77 type_ = OMNIBOX_KEYWORD; 78 } else if (extension_->browser_action()) { 79 type_ = BROWSER_ACTION; 80 } else if (extension->page_action() && 81 !extension->page_action()->default_icon_path().empty()) { 82 type_ = PAGE_ACTION; 83 } else { 84 type_ = GENERIC; 85 } 86 87 // |extension| has been initialized but not loaded at this point. We need 88 // to wait on showing the Bubble until not only the EXTENSION_LOADED gets 89 // fired, but all of the EXTENSION_LOADED Observers have run. Only then can we 90 // be sure that a browser action or page action has had views created which we 91 // can inspect for the purpose of pointing to them. 92 registrar_.Add(this, NotificationType::EXTENSION_LOADED, 93 Source<Profile>(browser->profile())); 94 registrar_.Add(this, NotificationType::EXTENSION_UNLOADED, 95 Source<Profile>(browser->profile())); 96 } 97 98 ExtensionInstalledBubbleGtk::~ExtensionInstalledBubbleGtk() {} 99 100 void ExtensionInstalledBubbleGtk::Observe(NotificationType type, 101 const NotificationSource& source, 102 const NotificationDetails& details) { 103 if (type == NotificationType::EXTENSION_LOADED) { 104 const Extension* extension = Details<const Extension>(details).ptr(); 105 if (extension == extension_) { 106 // PostTask to ourself to allow all EXTENSION_LOADED Observers to run. 107 MessageLoopForUI::current()->PostTask(FROM_HERE, NewRunnableMethod(this, 108 &ExtensionInstalledBubbleGtk::ShowInternal)); 109 } 110 } else if (type == NotificationType::EXTENSION_UNLOADED) { 111 const Extension* extension = 112 Details<UnloadedExtensionInfo>(details)->extension; 113 if (extension == extension_) 114 extension_ = NULL; 115 } else { 116 NOTREACHED() << L"Received unexpected notification"; 117 } 118 } 119 120 void ExtensionInstalledBubbleGtk::ShowInternal() { 121 BrowserWindowGtk* browser_window = 122 BrowserWindowGtk::GetBrowserWindowForNativeWindow( 123 browser_->window()->GetNativeHandle()); 124 125 GtkWidget* reference_widget = NULL; 126 127 if (type_ == BROWSER_ACTION) { 128 BrowserActionsToolbarGtk* toolbar = 129 browser_window->GetToolbar()->GetBrowserActionsToolbar(); 130 131 if (toolbar->animating() && animation_wait_retries_-- > 0) { 132 MessageLoopForUI::current()->PostDelayedTask( 133 FROM_HERE, 134 NewRunnableMethod(this, &ExtensionInstalledBubbleGtk::ShowInternal), 135 kAnimationWaitMS); 136 return; 137 } 138 139 reference_widget = toolbar->GetBrowserActionWidget(extension_); 140 // glib delays recalculating layout, but we need reference_widget to know 141 // its coordinates, so we force a check_resize here. 142 gtk_container_check_resize(GTK_CONTAINER( 143 browser_window->GetToolbar()->widget())); 144 // If the widget is not visible then browser_window could be incognito 145 // with this extension disabled. Try showing it on the chevron. 146 // If that fails, fall back to default position. 147 if (reference_widget && !GTK_WIDGET_VISIBLE(reference_widget)) { 148 reference_widget = GTK_WIDGET_VISIBLE(toolbar->chevron()) ? 149 toolbar->chevron() : NULL; 150 } 151 } else if (type_ == PAGE_ACTION) { 152 LocationBarViewGtk* location_bar_view = 153 browser_window->GetToolbar()->GetLocationBarView(); 154 location_bar_view->SetPreviewEnabledPageAction(extension_->page_action(), 155 true); // preview_enabled 156 reference_widget = location_bar_view->GetPageActionWidget( 157 extension_->page_action()); 158 // glib delays recalculating layout, but we need reference_widget to know 159 // it's coordinates, so we force a check_resize here. 160 gtk_container_check_resize(GTK_CONTAINER( 161 browser_window->GetToolbar()->widget())); 162 DCHECK(reference_widget); 163 } else if (type_ == OMNIBOX_KEYWORD) { 164 LocationBarViewGtk* location_bar_view = 165 browser_window->GetToolbar()->GetLocationBarView(); 166 reference_widget = location_bar_view->location_entry_widget(); 167 DCHECK(reference_widget); 168 } 169 170 // Default case. 171 if (reference_widget == NULL) 172 reference_widget = browser_window->GetToolbar()->GetAppMenuButton(); 173 174 GtkThemeService* theme_provider = GtkThemeService::GetFrom( 175 browser_->profile()); 176 177 // Setup the InfoBubble content. 178 GtkWidget* bubble_content = gtk_hbox_new(FALSE, kHorizontalColumnSpacing); 179 gtk_container_set_border_width(GTK_CONTAINER(bubble_content), kContentBorder); 180 181 if (!icon_.isNull()) { 182 // Scale icon down to 43x43, but allow smaller icons (don't scale up). 183 GdkPixbuf* pixbuf = gfx::GdkPixbufFromSkBitmap(&icon_); 184 gfx::Size size(icon_.width(), icon_.height()); 185 if (size.width() > kIconSize || size.height() > kIconSize) { 186 if (size.width() > size.height()) { 187 size.set_height(size.height() * kIconSize / size.width()); 188 size.set_width(kIconSize); 189 } else { 190 size.set_width(size.width() * kIconSize / size.height()); 191 size.set_height(kIconSize); 192 } 193 194 GdkPixbuf* old = pixbuf; 195 pixbuf = gdk_pixbuf_scale_simple(pixbuf, size.width(), size.height(), 196 GDK_INTERP_BILINEAR); 197 g_object_unref(old); 198 } 199 200 // Put Icon in top of the left column. 201 GtkWidget* icon_column = gtk_vbox_new(FALSE, 0); 202 // Use 3 pixel padding to get visual balance with InfoBubble border on the 203 // left. 204 gtk_box_pack_start(GTK_BOX(bubble_content), icon_column, FALSE, FALSE, 205 kIconPadding); 206 GtkWidget* image = gtk_image_new_from_pixbuf(pixbuf); 207 g_object_unref(pixbuf); 208 gtk_box_pack_start(GTK_BOX(icon_column), image, FALSE, FALSE, 0); 209 } 210 211 // Center text column. 212 GtkWidget* text_column = gtk_vbox_new(FALSE, kTextColumnVerticalSpacing); 213 gtk_box_pack_start(GTK_BOX(bubble_content), text_column, FALSE, FALSE, 0); 214 215 // Heading label 216 GtkWidget* heading_label = gtk_label_new(NULL); 217 string16 extension_name = UTF8ToUTF16(extension_->name()); 218 base::i18n::AdjustStringForLocaleDirection(&extension_name); 219 std::string heading_text = l10n_util::GetStringFUTF8( 220 IDS_EXTENSION_INSTALLED_HEADING, extension_name); 221 char* markup = g_markup_printf_escaped("<span size=\"larger\">%s</span>", 222 heading_text.c_str()); 223 gtk_label_set_markup(GTK_LABEL(heading_label), markup); 224 g_free(markup); 225 226 gtk_util::SetLabelWidth(heading_label, kTextColumnWidth); 227 gtk_box_pack_start(GTK_BOX(text_column), heading_label, FALSE, FALSE, 0); 228 229 // Page action label 230 if (type_ == PAGE_ACTION) { 231 GtkWidget* info_label = gtk_label_new(l10n_util::GetStringUTF8( 232 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO).c_str()); 233 gtk_util::SetLabelWidth(info_label, kTextColumnWidth); 234 gtk_box_pack_start(GTK_BOX(text_column), info_label, FALSE, FALSE, 0); 235 } 236 237 // Omnibox keyword label 238 if (type_ == OMNIBOX_KEYWORD) { 239 GtkWidget* info_label = gtk_label_new(l10n_util::GetStringFUTF8( 240 IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO, 241 UTF8ToUTF16(extension_->omnibox_keyword())).c_str()); 242 gtk_util::SetLabelWidth(info_label, kTextColumnWidth); 243 gtk_box_pack_start(GTK_BOX(text_column), info_label, FALSE, FALSE, 0); 244 } 245 246 // Manage label 247 GtkWidget* manage_label = gtk_label_new( 248 l10n_util::GetStringUTF8(IDS_EXTENSION_INSTALLED_MANAGE_INFO).c_str()); 249 gtk_util::SetLabelWidth(manage_label, kTextColumnWidth); 250 gtk_box_pack_start(GTK_BOX(text_column), manage_label, FALSE, FALSE, 0); 251 252 // Create and pack the close button. 253 GtkWidget* close_column = gtk_vbox_new(FALSE, 0); 254 gtk_box_pack_start(GTK_BOX(bubble_content), close_column, FALSE, FALSE, 0); 255 close_button_.reset(CustomDrawButton::CloseButton(theme_provider)); 256 g_signal_connect(close_button_->widget(), "clicked", 257 G_CALLBACK(OnButtonClick), this); 258 gtk_box_pack_start(GTK_BOX(close_column), close_button_->widget(), 259 FALSE, FALSE, 0); 260 261 InfoBubbleGtk::ArrowLocationGtk arrow_location = 262 !base::i18n::IsRTL() ? 263 InfoBubbleGtk::ARROW_LOCATION_TOP_RIGHT : 264 InfoBubbleGtk::ARROW_LOCATION_TOP_LEFT; 265 266 gfx::Rect bounds = gtk_util::WidgetBounds(reference_widget); 267 if (type_ == OMNIBOX_KEYWORD) { 268 // Reverse the arrow for omnibox keywords, since the bubble will be on the 269 // other side of the window. We also clear the width to avoid centering 270 // the popup on the URL bar. 271 arrow_location = 272 !base::i18n::IsRTL() ? 273 InfoBubbleGtk::ARROW_LOCATION_TOP_LEFT : 274 InfoBubbleGtk::ARROW_LOCATION_TOP_RIGHT; 275 if (base::i18n::IsRTL()) 276 bounds.Offset(bounds.width(), 0); 277 bounds.set_width(0); 278 } 279 280 info_bubble_ = InfoBubbleGtk::Show(reference_widget, 281 &bounds, 282 bubble_content, 283 arrow_location, 284 true, // match_system_theme 285 true, // grab_input 286 theme_provider, 287 this); 288 } 289 290 // static 291 void ExtensionInstalledBubbleGtk::OnButtonClick(GtkWidget* button, 292 ExtensionInstalledBubbleGtk* bubble) { 293 if (button == bubble->close_button_->widget()) { 294 bubble->info_bubble_->Close(); 295 } else { 296 NOTREACHED(); 297 } 298 } 299 // InfoBubbleDelegate 300 void ExtensionInstalledBubbleGtk::InfoBubbleClosing(InfoBubbleGtk* info_bubble, 301 bool closed_by_escape) { 302 if (extension_ && type_ == PAGE_ACTION) { 303 // Turn the page action preview off. 304 BrowserWindowGtk* browser_window = 305 BrowserWindowGtk::GetBrowserWindowForNativeWindow( 306 browser_->window()->GetNativeHandle()); 307 LocationBarViewGtk* location_bar_view = 308 browser_window->GetToolbar()->GetLocationBarView(); 309 location_bar_view->SetPreviewEnabledPageAction(extension_->page_action(), 310 false); // preview_enabled 311 } 312 313 // We need to allow the info bubble to close and remove the widgets from 314 // the window before we call Release() because close_button_ depends 315 // on all references being cleared before it is destroyed. 316 MessageLoopForUI::current()->PostTask(FROM_HERE, NewRunnableMethod(this, 317 &ExtensionInstalledBubbleGtk::Close)); 318 } 319 320 void ExtensionInstalledBubbleGtk::Close() { 321 Release(); // Balanced in ctor. 322 info_bubble_ = NULL; 323 } 324