Home | History | Annotate | Download | only in extensions
      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(&params);
    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