1 // Copyright (c) 2012 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/bind.h" 10 #include "base/bind_helpers.h" 11 #include "base/i18n/rtl.h" 12 #include "base/message_loop/message_loop.h" 13 #include "base/strings/utf_string_conversions.h" 14 #include "chrome/browser/chrome_notification_types.h" 15 #include "chrome/browser/extensions/api/commands/command_service.h" 16 #include "chrome/browser/extensions/extension_action.h" 17 #include "chrome/browser/extensions/extension_action_manager.h" 18 #include "chrome/browser/ui/browser.h" 19 #include "chrome/browser/ui/browser_dialogs.h" 20 #include "chrome/browser/ui/gtk/browser_actions_toolbar_gtk.h" 21 #include "chrome/browser/ui/gtk/browser_toolbar_gtk.h" 22 #include "chrome/browser/ui/gtk/browser_window_gtk.h" 23 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 24 #include "chrome/browser/ui/gtk/gtk_util.h" 25 #include "chrome/browser/ui/gtk/location_bar_view_gtk.h" 26 #include "chrome/browser/ui/singleton_tabs.h" 27 #include "chrome/common/extensions/api/extension_action/action_info.h" 28 #include "chrome/common/extensions/api/omnibox/omnibox_handler.h" 29 #include "chrome/common/extensions/extension.h" 30 #include "chrome/common/url_constants.h" 31 #include "content/public/browser/notification_details.h" 32 #include "content/public/browser/notification_source.h" 33 #include "grit/chromium_strings.h" 34 #include "grit/generated_resources.h" 35 #include "grit/theme_resources.h" 36 #include "ui/base/gtk/gtk_hig_constants.h" 37 #include "ui/base/l10n/l10n_util.h" 38 #include "ui/base/resource/resource_bundle.h" 39 #include "ui/gfx/gtk_util.h" 40 41 using extensions::Extension; 42 using extensions::ExtensionActionManager; 43 44 namespace { 45 46 const int kHorizontalColumnSpacing = 10; 47 const int kIconPadding = 3; 48 const int kIconSize = 43; 49 const int kTextColumnVerticalSpacing = 7; 50 const int kTextColumnWidth = 350; 51 52 // When showing the bubble for a new browser action, we may have to wait for 53 // the toolbar to finish animating to know where the item's final position 54 // will be. 55 const int kAnimationWaitRetries = 10; 56 const int kAnimationWaitMS = 50; 57 58 } // namespace 59 60 namespace chrome { 61 62 void ShowExtensionInstalledBubble(const Extension* extension, 63 Browser* browser, 64 const SkBitmap& icon) { 65 ExtensionInstalledBubbleGtk::Show(extension, browser, icon); 66 } 67 68 } // namespace chrome 69 70 void ExtensionInstalledBubbleGtk::Show(const Extension* extension, 71 Browser* browser, 72 const SkBitmap& icon) { 73 new ExtensionInstalledBubbleGtk(extension, browser, icon); 74 } 75 76 ExtensionInstalledBubbleGtk::ExtensionInstalledBubbleGtk( 77 const Extension* extension, Browser *browser, const SkBitmap& icon) 78 : extension_(extension), 79 browser_(browser), 80 icon_(icon), 81 animation_wait_retries_(kAnimationWaitRetries), 82 bubble_(NULL), 83 weak_factory_(this) { 84 if (!extensions::OmniboxInfo::GetKeyword(extension_).empty()) 85 type_ = OMNIBOX_KEYWORD; 86 else if (extensions::ActionInfo::GetBrowserActionInfo(extension_)) 87 type_ = BROWSER_ACTION; 88 else if (extensions::ActionInfo::GetPageActionInfo(extension) && 89 extensions::ActionInfo::IsVerboseInstallMessage(extension)) 90 type_ = PAGE_ACTION; 91 else 92 type_ = GENERIC; 93 94 // |extension| has been initialized but not loaded at this point. We need 95 // to wait on showing the Bubble until not only the EXTENSION_LOADED gets 96 // fired, but all of the EXTENSION_LOADED Observers have run. Only then can we 97 // be sure that a browser action or page action has had views created which we 98 // can inspect for the purpose of pointing to them. 99 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED, 100 content::Source<Profile>(browser->profile())); 101 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED, 102 content::Source<Profile>(browser->profile())); 103 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING, 104 content::Source<Browser>(browser)); 105 } 106 107 ExtensionInstalledBubbleGtk::~ExtensionInstalledBubbleGtk() {} 108 109 void ExtensionInstalledBubbleGtk::Observe( 110 int type, 111 const content::NotificationSource& source, 112 const content::NotificationDetails& details) { 113 if (type == chrome::NOTIFICATION_EXTENSION_LOADED) { 114 const Extension* extension = 115 content::Details<const Extension>(details).ptr(); 116 if (extension == extension_) { 117 // PostTask to ourself to allow all EXTENSION_LOADED Observers to run. 118 base::MessageLoopForUI::current()->PostTask( 119 FROM_HERE, 120 base::Bind(&ExtensionInstalledBubbleGtk::ShowInternal, 121 weak_factory_.GetWeakPtr())); 122 } 123 } else if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED) { 124 const Extension* extension = 125 content::Details<extensions::UnloadedExtensionInfo>(details)->extension; 126 if (extension == extension_) { 127 // Extension is going away, make sure ShowInternal won't be called. 128 weak_factory_.InvalidateWeakPtrs(); 129 extension_ = NULL; 130 } 131 } else if (type == chrome::NOTIFICATION_BROWSER_CLOSING) { 132 // The browser closed before the bubble could be created. 133 if (!bubble_) 134 delete this; 135 } else { 136 NOTREACHED() << L"Received unexpected notification"; 137 } 138 } 139 140 void ExtensionInstalledBubbleGtk::OnDestroy(GtkWidget* widget) { 141 bubble_ = NULL; 142 delete this; 143 } 144 145 void ExtensionInstalledBubbleGtk::ShowInternal() { 146 BrowserWindowGtk* browser_window = 147 BrowserWindowGtk::GetBrowserWindowForNativeWindow( 148 browser_->window()->GetNativeWindow()); 149 150 GtkWidget* reference_widget = NULL; 151 152 if (type_ == BROWSER_ACTION) { 153 BrowserActionsToolbarGtk* toolbar = 154 browser_window->GetToolbar()->GetBrowserActionsToolbar(); 155 156 if (toolbar->animating() && animation_wait_retries_-- > 0) { 157 base::MessageLoopForUI::current()->PostDelayedTask( 158 FROM_HERE, 159 base::Bind(&ExtensionInstalledBubbleGtk::ShowInternal, 160 weak_factory_.GetWeakPtr()), 161 base::TimeDelta::FromMilliseconds(kAnimationWaitMS)); 162 return; 163 } 164 165 reference_widget = toolbar->GetBrowserActionWidget(extension_); 166 // glib delays recalculating layout, but we need reference_widget to know 167 // its coordinates, so we force a check_resize here. 168 gtk_container_check_resize(GTK_CONTAINER( 169 browser_window->GetToolbar()->widget())); 170 // If the widget is not visible then browser_window could be incognito 171 // with this extension disabled. Try showing it on the chevron. 172 // If that fails, fall back to default position. 173 if (reference_widget && !gtk_widget_get_visible(reference_widget)) { 174 reference_widget = gtk_widget_get_visible(toolbar->chevron()) ? 175 toolbar->chevron() : NULL; 176 } 177 } else if (type_ == PAGE_ACTION) { 178 LocationBarViewGtk* location_bar_view = 179 browser_window->GetToolbar()->GetLocationBarView(); 180 ExtensionAction* page_action = 181 ExtensionActionManager::Get(browser_->profile())-> 182 GetPageAction(*extension_); 183 location_bar_view->SetPreviewEnabledPageAction(page_action, 184 true); // preview_enabled 185 reference_widget = location_bar_view->GetPageActionWidget(page_action); 186 // glib delays recalculating layout, but we need reference_widget to know 187 // its coordinates, so we force a check_resize here. 188 gtk_container_check_resize(GTK_CONTAINER( 189 browser_window->GetToolbar()->widget())); 190 DCHECK(reference_widget); 191 } else if (type_ == OMNIBOX_KEYWORD) { 192 LocationBarViewGtk* location_bar_view = 193 browser_window->GetToolbar()->GetLocationBarView(); 194 reference_widget = location_bar_view->location_entry_widget(); 195 DCHECK(reference_widget); 196 } 197 198 // Default case. 199 if (reference_widget == NULL) 200 reference_widget = browser_window->GetToolbar()->GetAppMenuButton(); 201 202 GtkThemeService* theme_provider = GtkThemeService::GetFrom( 203 browser_->profile()); 204 205 // Setup the BubbleGtk content. 206 GtkWidget* bubble_content = gtk_hbox_new(FALSE, kHorizontalColumnSpacing); 207 gtk_container_set_border_width(GTK_CONTAINER(bubble_content), 208 ui::kContentAreaBorder); 209 210 if (!icon_.isNull()) { 211 // Scale icon down to 43x43, but allow smaller icons (don't scale up). 212 GdkPixbuf* pixbuf = gfx::GdkPixbufFromSkBitmap(icon_); 213 gfx::Size size(icon_.width(), icon_.height()); 214 if (size.width() > kIconSize || size.height() > kIconSize) { 215 if (size.width() > size.height()) { 216 size.set_height(size.height() * kIconSize / size.width()); 217 size.set_width(kIconSize); 218 } else { 219 size.set_width(size.width() * kIconSize / size.height()); 220 size.set_height(kIconSize); 221 } 222 223 GdkPixbuf* old = pixbuf; 224 pixbuf = gdk_pixbuf_scale_simple(pixbuf, size.width(), size.height(), 225 GDK_INTERP_BILINEAR); 226 g_object_unref(old); 227 } 228 229 // Put Icon in top of the left column. 230 GtkWidget* icon_column = gtk_vbox_new(FALSE, 0); 231 // Use 3 pixel padding to get visual balance with BubbleGtk border on the 232 // left. 233 gtk_box_pack_start(GTK_BOX(bubble_content), icon_column, FALSE, FALSE, 234 kIconPadding); 235 GtkWidget* image = gtk_image_new_from_pixbuf(pixbuf); 236 g_object_unref(pixbuf); 237 gtk_box_pack_start(GTK_BOX(icon_column), image, FALSE, FALSE, 0); 238 } 239 240 // Center text column. 241 GtkWidget* text_column = gtk_vbox_new(FALSE, kTextColumnVerticalSpacing); 242 gtk_box_pack_start(GTK_BOX(bubble_content), text_column, FALSE, FALSE, 0); 243 244 // Heading label. 245 GtkWidget* heading_label = gtk_label_new(NULL); 246 string16 extension_name = UTF8ToUTF16(extension_->name()); 247 base::i18n::AdjustStringForLocaleDirection(&extension_name); 248 std::string heading_text = l10n_util::GetStringFUTF8( 249 IDS_EXTENSION_INSTALLED_HEADING, extension_name); 250 char* markup = g_markup_printf_escaped("<span size=\"larger\">%s</span>", 251 heading_text.c_str()); 252 gtk_label_set_markup(GTK_LABEL(heading_label), markup); 253 g_free(markup); 254 255 gtk_util::SetLabelWidth(heading_label, kTextColumnWidth); 256 gtk_box_pack_start(GTK_BOX(text_column), heading_label, FALSE, FALSE, 0); 257 258 bool has_keybinding = false; 259 260 // Browser action label. 261 if (type_ == BROWSER_ACTION) { 262 extensions::CommandService* command_service = 263 extensions::CommandService::Get(browser_->profile()); 264 extensions::Command browser_action_command; 265 GtkWidget* info_label; 266 if (!command_service->GetBrowserActionCommand( 267 extension_->id(), 268 extensions::CommandService::ACTIVE_ONLY, 269 &browser_action_command, 270 NULL)) { 271 info_label = gtk_label_new(l10n_util::GetStringUTF8( 272 IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO).c_str()); 273 } else { 274 info_label = gtk_label_new(l10n_util::GetStringFUTF8( 275 IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO_WITH_SHORTCUT, 276 browser_action_command.accelerator().GetShortcutText()).c_str()); 277 has_keybinding = true; 278 } 279 gtk_util::SetLabelWidth(info_label, kTextColumnWidth); 280 gtk_box_pack_start(GTK_BOX(text_column), info_label, FALSE, FALSE, 0); 281 } 282 283 // Page action label. 284 if (type_ == PAGE_ACTION) { 285 extensions::CommandService* command_service = 286 extensions::CommandService::Get(browser_->profile()); 287 extensions::Command page_action_command; 288 GtkWidget* info_label; 289 if (!command_service->GetPageActionCommand( 290 extension_->id(), 291 extensions::CommandService::ACTIVE_ONLY, 292 &page_action_command, 293 NULL)) { 294 info_label = gtk_label_new(l10n_util::GetStringUTF8( 295 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO).c_str()); 296 } else { 297 info_label = gtk_label_new(l10n_util::GetStringFUTF8( 298 IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO_WITH_SHORTCUT, 299 page_action_command.accelerator().GetShortcutText()).c_str()); 300 has_keybinding = true; 301 } 302 gtk_util::SetLabelWidth(info_label, kTextColumnWidth); 303 gtk_box_pack_start(GTK_BOX(text_column), info_label, FALSE, FALSE, 0); 304 } 305 306 // Omnibox keyword label. 307 if (type_ == OMNIBOX_KEYWORD) { 308 GtkWidget* info_label = gtk_label_new(l10n_util::GetStringFUTF8( 309 IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO, 310 UTF8ToUTF16(extensions::OmniboxInfo::GetKeyword(extension_))).c_str()); 311 gtk_util::SetLabelWidth(info_label, kTextColumnWidth); 312 gtk_box_pack_start(GTK_BOX(text_column), info_label, FALSE, FALSE, 0); 313 } 314 315 if (has_keybinding) { 316 GtkWidget* manage_link = theme_provider->BuildChromeLinkButton( 317 l10n_util::GetStringUTF8(IDS_EXTENSION_INSTALLED_MANAGE_SHORTCUTS)); 318 GtkWidget* link_hbox = gtk_hbox_new(FALSE, 0); 319 // Stick it in an hbox so it doesn't expand to the whole width. 320 gtk_box_pack_end(GTK_BOX(link_hbox), manage_link, FALSE, FALSE, 0); 321 gtk_box_pack_start(GTK_BOX(text_column), link_hbox, FALSE, FALSE, 0); 322 g_signal_connect(manage_link, "clicked", 323 G_CALLBACK(OnLinkClickedThunk), this); 324 } else { 325 // Manage label. 326 GtkWidget* manage_label = gtk_label_new( 327 l10n_util::GetStringUTF8(IDS_EXTENSION_INSTALLED_MANAGE_INFO).c_str()); 328 gtk_util::SetLabelWidth(manage_label, kTextColumnWidth); 329 gtk_box_pack_start(GTK_BOX(text_column), manage_label, FALSE, FALSE, 0); 330 } 331 332 // Create and pack the close button. 333 GtkWidget* close_column = gtk_vbox_new(FALSE, 0); 334 gtk_box_pack_start(GTK_BOX(bubble_content), close_column, FALSE, FALSE, 0); 335 close_button_.reset(CustomDrawButton::CloseButtonBubble(theme_provider)); 336 g_signal_connect(close_button_->widget(), "clicked", 337 G_CALLBACK(OnButtonClick), this); 338 gtk_box_pack_start(GTK_BOX(close_column), close_button_->widget(), 339 FALSE, FALSE, 0); 340 341 BubbleGtk::FrameStyle frame_style = BubbleGtk::ANCHOR_TOP_RIGHT; 342 343 gfx::Rect bounds = gtk_util::WidgetBounds(reference_widget); 344 if (type_ == OMNIBOX_KEYWORD) { 345 // Reverse the arrow for omnibox keywords, since the bubble will be on the 346 // other side of the window. We also clear the width to avoid centering 347 // the popup on the URL bar. 348 frame_style = BubbleGtk::ANCHOR_TOP_LEFT; 349 if (base::i18n::IsRTL()) 350 bounds.Offset(bounds.width(), 0); 351 bounds.set_width(0); 352 } 353 354 bubble_ = BubbleGtk::Show(reference_widget, 355 &bounds, 356 bubble_content, 357 frame_style, 358 BubbleGtk::MATCH_SYSTEM_THEME | 359 BubbleGtk::POPUP_WINDOW | 360 BubbleGtk::GRAB_INPUT, 361 theme_provider, 362 this); 363 g_signal_connect(bubble_content, "destroy", 364 G_CALLBACK(&OnDestroyThunk), this); 365 } 366 367 // static 368 void ExtensionInstalledBubbleGtk::OnButtonClick(GtkWidget* button, 369 ExtensionInstalledBubbleGtk* bubble) { 370 if (button == bubble->close_button_->widget()) { 371 bubble->bubble_->Close(); 372 } else { 373 NOTREACHED(); 374 } 375 } 376 377 void ExtensionInstalledBubbleGtk::OnLinkClicked(GtkWidget* widget) { 378 bubble_->Close(); 379 380 std::string configure_url = chrome::kChromeUIExtensionsURL; 381 configure_url += chrome::kExtensionConfigureCommandsSubPage; 382 chrome::NavigateParams params( 383 chrome::GetSingletonTabNavigateParams( 384 browser_, GURL(configure_url.c_str()))); 385 chrome::Navigate(¶ms); 386 } 387 388 void ExtensionInstalledBubbleGtk::BubbleClosing(BubbleGtk* bubble, 389 bool closed_by_escape) { 390 if (extension_ && type_ == PAGE_ACTION) { 391 // Turn the page action preview off. 392 BrowserWindowGtk* browser_window = 393 BrowserWindowGtk::GetBrowserWindowForNativeWindow( 394 browser_->window()->GetNativeWindow()); 395 LocationBarViewGtk* location_bar_view = 396 browser_window->GetToolbar()->GetLocationBarView(); 397 location_bar_view->SetPreviewEnabledPageAction( 398 ExtensionActionManager::Get(browser_->profile())-> 399 GetPageAction(*extension_), 400 false); // preview_enabled 401 } 402 } 403