Home | History | Annotate | Download | only in extensions
      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