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 <gtk/gtk.h>
      6 
      7 #include "base/i18n/rtl.h"
      8 #include "base/strings/string_util.h"
      9 #include "base/strings/utf_string_conversions.h"
     10 #include "chrome/browser/extensions/bundle_installer.h"
     11 #include "chrome/browser/extensions/extension_install_prompt.h"
     12 #include "chrome/browser/profiles/profile.h"
     13 #include "chrome/browser/ui/gtk/browser_window_gtk.h"
     14 #include "chrome/browser/ui/gtk/gtk_chrome_link_button.h"
     15 #include "chrome/browser/ui/gtk/gtk_util.h"
     16 #include "chrome/common/extensions/extension.h"
     17 #include "content/public/browser/page_navigator.h"
     18 #include "content/public/browser/web_contents.h"
     19 #include "content/public/browser/web_contents_view.h"
     20 #include "grit/generated_resources.h"
     21 #include "skia/ext/image_operations.h"
     22 #include "ui/base/gtk/gtk_hig_constants.h"
     23 #include "ui/base/l10n/l10n_util.h"
     24 #include "ui/base/resource/resource_bundle.h"
     25 #include "ui/gfx/gtk_util.h"
     26 
     27 using content::OpenURLParams;
     28 using extensions::BundleInstaller;
     29 
     30 namespace {
     31 
     32 const int kLeftColumnMinWidth = 250;
     33 // External installs have more text, so use a wider dialog.
     34 const int kExternalInstallLeftColumnWidth = 350;
     35 const int kImageSize = 69;
     36 const int kDetailIndent = 20;
     37 const int kMaxRetainedFilesHeight = 100;
     38 
     39 // Additional padding (beyond on ui::kControlSpacing) all sides of each
     40 // permission in the permissions list.
     41 const int kPermissionsPadding = 2;
     42 const int kExtensionsPadding = kPermissionsPadding;
     43 
     44 const double kRatingTextSize = 12.1;  // 12.1px = 9pt @ 96dpi
     45 
     46 // Adds a Skia image as an icon control to the given container.
     47 void AddResourceIcon(const gfx::ImageSkia* icon, void* data) {
     48   GtkWidget* container = static_cast<GtkWidget*>(data);
     49   GdkPixbuf* icon_pixbuf = gfx::GdkPixbufFromSkBitmap(*icon->bitmap());
     50   GtkWidget* icon_widget = gtk_image_new_from_pixbuf(icon_pixbuf);
     51   g_object_unref(icon_pixbuf);
     52   gtk_box_pack_start(GTK_BOX(container), icon_widget, FALSE, FALSE, 0);
     53 }
     54 
     55 void OnZippyButtonRealize(GtkWidget* event_box, gpointer unused) {
     56   gdk_window_set_cursor(event_box->window, gfx::GetCursor(GDK_HAND2));
     57 }
     58 
     59 gboolean OnZippyButtonRelease(GtkWidget* event_box,
     60                               GdkEvent* event,
     61                               GtkWidget* detail_box) {
     62   if (event->button.button != 1)
     63     return FALSE;
     64 
     65   GtkWidget* arrow =
     66       GTK_WIDGET(gtk_object_get_data(GTK_OBJECT(event_box), "arrow"));
     67 
     68   if (gtk_widget_get_visible(detail_box)) {
     69     gtk_widget_hide(detail_box);
     70     gtk_arrow_set(GTK_ARROW(arrow), GTK_ARROW_RIGHT, GTK_SHADOW_OUT);
     71   } else {
     72     gtk_widget_set_no_show_all(detail_box, FALSE);
     73     gtk_widget_show_all(detail_box);
     74     gtk_widget_set_no_show_all(detail_box, TRUE);
     75     gtk_arrow_set(GTK_ARROW(arrow), GTK_ARROW_DOWN, GTK_SHADOW_OUT);
     76   }
     77 
     78   return TRUE;
     79 }
     80 
     81 }  // namespace
     82 
     83 namespace chrome {
     84 
     85 // Displays the dialog when constructed, deletes itself when dialog is
     86 // dismissed. Success/failure is passed back through the
     87 // ExtensionInstallPrompt::Delegate instance.
     88 class ExtensionInstallDialog {
     89  public:
     90   ExtensionInstallDialog(const ExtensionInstallPrompt::ShowParams& show_params,
     91                          ExtensionInstallPrompt::Delegate* delegate,
     92                          const ExtensionInstallPrompt::Prompt& prompt);
     93  private:
     94   ~ExtensionInstallDialog();
     95 
     96   CHROMEGTK_CALLBACK_1(ExtensionInstallDialog, void, OnResponse, int);
     97   CHROMEGTK_CALLBACK_0(ExtensionInstallDialog, void, OnStoreLinkClick);
     98 
     99   GtkWidget* CreateWidgetForIssueAdvice(
    100       const IssueAdviceInfoEntry& issue_advice, int pixel_width);
    101 
    102   content::PageNavigator* navigator_;
    103   ExtensionInstallPrompt::Delegate* delegate_;
    104   std::string extension_id_;  // Set for INLINE_INSTALL_PROMPT.
    105   GtkWidget* dialog_;
    106 };
    107 
    108 ExtensionInstallDialog::ExtensionInstallDialog(
    109     const ExtensionInstallPrompt::ShowParams& show_params,
    110     ExtensionInstallPrompt::Delegate *delegate,
    111     const ExtensionInstallPrompt::Prompt& prompt)
    112     : navigator_(show_params.navigator),
    113       delegate_(delegate),
    114       dialog_(NULL) {
    115   bool show_permissions = prompt.ShouldShowPermissions();
    116   bool show_oauth_issues = prompt.GetOAuthIssueCount() > 0;
    117   bool show_retained_files = prompt.GetRetainedFileCount() > 0;
    118   bool is_inline_install =
    119       prompt.type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT;
    120   bool is_bundle_install =
    121       prompt.type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT;
    122   bool is_external_install =
    123       prompt.type() == ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT;
    124 
    125   if (is_inline_install)
    126     extension_id_ = prompt.extension()->id();
    127 
    128   // Build the dialog.
    129   gfx::NativeWindow parent = show_params.parent_window;
    130   dialog_ = gtk_dialog_new_with_buttons(
    131       UTF16ToUTF8(prompt.GetDialogTitle()).c_str(),
    132       parent,
    133       GTK_DIALOG_MODAL,
    134       NULL);
    135   GtkWidget* close_button = gtk_dialog_add_button(
    136       GTK_DIALOG(dialog_),
    137       prompt.HasAbortButtonLabel() ?
    138           UTF16ToUTF8(prompt.GetAbortButtonLabel()).c_str() : GTK_STOCK_CANCEL,
    139       GTK_RESPONSE_CLOSE);
    140   if (prompt.HasAcceptButtonLabel()) {
    141     gtk_dialog_add_button(
    142         GTK_DIALOG(dialog_),
    143         UTF16ToUTF8(prompt.GetAcceptButtonLabel()).c_str(),
    144         GTK_RESPONSE_ACCEPT);
    145   }
    146 #if !GTK_CHECK_VERSION(2, 22, 0)
    147   gtk_dialog_set_has_separator(GTK_DIALOG(dialog_), FALSE);
    148 #endif
    149 
    150   GtkWidget* content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog_));
    151   gtk_box_set_spacing(GTK_BOX(content_area), ui::kContentAreaSpacing);
    152 
    153   // Divide the dialog vertically (item data and icon on the top, permissions
    154   // on the bottom).
    155   GtkWidget* content_vbox = gtk_vbox_new(FALSE, ui::kControlSpacing);
    156   gtk_box_pack_start(GTK_BOX(content_area), content_vbox, TRUE, TRUE, 0);
    157 
    158   // Create a two column layout for the top (item data on the left, icon on
    159   // the right).
    160   GtkWidget* top_content_hbox = gtk_hbox_new(FALSE, ui::kContentAreaSpacing);
    161   gtk_box_pack_start(GTK_BOX(content_vbox), top_content_hbox, TRUE, TRUE, 0);
    162 
    163   // We don't show the image for bundle installs, so let the left column take
    164   // up that space.
    165   int left_column_min_width = kLeftColumnMinWidth;
    166   if (is_bundle_install)
    167     left_column_min_width += kImageSize;
    168   if (is_external_install)
    169     left_column_min_width = kExternalInstallLeftColumnWidth;
    170 
    171   // Create a new vbox for the left column.
    172   GtkWidget* left_column_area = gtk_vbox_new(FALSE, ui::kControlSpacing);
    173   gtk_box_pack_start(GTK_BOX(top_content_hbox), left_column_area,
    174                      TRUE, TRUE, 0);
    175   gtk_widget_set_size_request(left_column_area, left_column_min_width, -1);
    176 
    177   GtkWidget* heading_vbox = gtk_vbox_new(FALSE, 0);
    178   // If we are not going to show anything else, vertically center the title.
    179   bool center_heading = !show_permissions && !show_oauth_issues &&
    180                         !is_inline_install && !show_retained_files;
    181   gtk_box_pack_start(GTK_BOX(left_column_area), heading_vbox, center_heading,
    182                      center_heading, 0);
    183 
    184   // Heading
    185   GtkWidget* heading_label = gtk_util::CreateBoldLabel(
    186       UTF16ToUTF8(prompt.GetHeading().c_str()));
    187   gtk_util::SetLabelWidth(heading_label, left_column_min_width);
    188   gtk_box_pack_start(GTK_BOX(heading_vbox), heading_label, center_heading,
    189                      center_heading, 0);
    190 
    191   if (is_inline_install) {
    192     // Average rating (as stars) and number of ratings.
    193     GtkWidget* stars_hbox = gtk_hbox_new(FALSE, 0);
    194     gtk_box_pack_start(GTK_BOX(heading_vbox), stars_hbox, FALSE, FALSE, 0);
    195     prompt.AppendRatingStars(AddResourceIcon, stars_hbox);
    196     GtkWidget* rating_label = gtk_label_new(UTF16ToUTF8(
    197         prompt.GetRatingCount()).c_str());
    198     gtk_util::ForceFontSizePixels(rating_label, kRatingTextSize);
    199     gtk_box_pack_start(GTK_BOX(stars_hbox), rating_label,
    200                        FALSE, FALSE, 3);
    201 
    202     // User count.
    203     GtkWidget* users_label = gtk_label_new(UTF16ToUTF8(
    204         prompt.GetUserCount()).c_str());
    205     gtk_util::SetLabelWidth(users_label, left_column_min_width);
    206     gtk_util::SetLabelColor(users_label, &ui::kGdkGray);
    207     gtk_util::ForceFontSizePixels(rating_label, kRatingTextSize);
    208     gtk_box_pack_start(GTK_BOX(heading_vbox), users_label,
    209                        FALSE, FALSE, 0);
    210 
    211     // Store link.
    212     GtkWidget* store_link = gtk_chrome_link_button_new(
    213         l10n_util::GetStringUTF8(IDS_EXTENSION_PROMPT_STORE_LINK).c_str());
    214     gtk_util::ForceFontSizePixels(store_link, kRatingTextSize);
    215     GtkWidget* store_link_hbox = gtk_hbox_new(FALSE, 0);
    216     // Stick it in an hbox so it doesn't expand to the whole width.
    217     gtk_box_pack_start(GTK_BOX(store_link_hbox), store_link, FALSE, FALSE, 0);
    218     gtk_box_pack_start(GTK_BOX(heading_vbox), store_link_hbox, FALSE, FALSE, 0);
    219     g_signal_connect(store_link, "clicked",
    220                      G_CALLBACK(OnStoreLinkClickThunk), this);
    221   }
    222 
    223   if (is_bundle_install) {
    224     // Add the list of extensions to be installed.
    225     GtkWidget* extensions_vbox = gtk_vbox_new(FALSE, ui::kControlSpacing);
    226     gtk_box_pack_start(GTK_BOX(heading_vbox), extensions_vbox, FALSE, FALSE,
    227                        ui::kControlSpacing);
    228 
    229     BundleInstaller::ItemList items = prompt.bundle()->GetItemsWithState(
    230         BundleInstaller::Item::STATE_PENDING);
    231     for (size_t i = 0; i < items.size(); ++i) {
    232       GtkWidget* extension_label = gtk_label_new(UTF16ToUTF8(
    233           items[i].GetNameForDisplay()).c_str());
    234       gtk_util::SetLabelWidth(extension_label, left_column_min_width);
    235       gtk_box_pack_start(GTK_BOX(extensions_vbox), extension_label,
    236                          FALSE, FALSE, kExtensionsPadding);
    237     }
    238   } else {
    239     // Resize the icon if necessary.
    240     SkBitmap scaled_icon = *prompt.icon().ToSkBitmap();
    241     if (scaled_icon.width() > kImageSize || scaled_icon.height() > kImageSize) {
    242       scaled_icon = skia::ImageOperations::Resize(
    243           scaled_icon, skia::ImageOperations::RESIZE_LANCZOS3,
    244           kImageSize, kImageSize);
    245     }
    246 
    247     // Put icon in the right column.
    248     GdkPixbuf* pixbuf = gfx::GdkPixbufFromSkBitmap(scaled_icon);
    249     GtkWidget* icon = gtk_image_new_from_pixbuf(pixbuf);
    250     g_object_unref(pixbuf);
    251     gtk_box_pack_start(GTK_BOX(top_content_hbox), icon, FALSE, FALSE, 0);
    252     // Top justify the image.
    253     gtk_misc_set_alignment(GTK_MISC(icon), 0.5, 0.0);
    254   }
    255 
    256   // Permissions are shown separated by a divider for inline installs, or
    257   // directly under the heading for regular installs (where we don't have
    258   // the store data)
    259   if (show_permissions) {
    260     GtkWidget* permissions_container;
    261     if (is_inline_install) {
    262       permissions_container = content_vbox;
    263       gtk_box_pack_start(GTK_BOX(content_vbox), gtk_hseparator_new(),
    264                          FALSE, FALSE, ui::kControlSpacing);
    265     } else {
    266       permissions_container = left_column_area;
    267     }
    268 
    269     if (prompt.GetPermissionCount() > 0) {
    270       GtkWidget* permissions_header = gtk_util::CreateBoldLabel(
    271           UTF16ToUTF8(prompt.GetPermissionsHeading()).c_str());
    272       gtk_util::SetLabelWidth(permissions_header, left_column_min_width);
    273       gtk_box_pack_start(GTK_BOX(permissions_container), permissions_header,
    274                          FALSE, FALSE, 0);
    275 
    276       for (size_t i = 0; i < prompt.GetPermissionCount(); ++i) {
    277         std::string permission = l10n_util::GetStringFUTF8(
    278             IDS_EXTENSION_PERMISSION_LINE, prompt.GetPermission(i));
    279         GtkWidget* permission_label = gtk_label_new(permission.c_str());
    280         gtk_util::SetLabelWidth(permission_label, left_column_min_width);
    281         gtk_box_pack_start(GTK_BOX(permissions_container), permission_label,
    282                            FALSE, FALSE, kPermissionsPadding);
    283       }
    284     } else {
    285       GtkWidget* permission_label = gtk_label_new(l10n_util::GetStringUTF8(
    286           IDS_EXTENSION_NO_SPECIAL_PERMISSIONS).c_str());
    287       gtk_util::SetLabelWidth(permission_label, left_column_min_width);
    288       gtk_box_pack_start(GTK_BOX(permissions_container), permission_label,
    289                          FALSE, FALSE, kPermissionsPadding);
    290     }
    291   }
    292 
    293   if (show_oauth_issues) {
    294     // If permissions are shown, then the scopes will go below them and take
    295     // up the entire width of the dialog. Otherwise the scopes will go where
    296     // the permissions usually go.
    297     GtkWidget* oauth_issues_container =
    298         show_permissions ? content_vbox : left_column_area;
    299     int pixel_width = left_column_min_width +
    300         (show_permissions ? kImageSize : 0);
    301 
    302     GtkWidget* oauth_issues_header = gtk_util::CreateBoldLabel(
    303         UTF16ToUTF8(prompt.GetOAuthHeading()).c_str());
    304     gtk_util::SetLabelWidth(oauth_issues_header, pixel_width);
    305     gtk_box_pack_start(GTK_BOX(oauth_issues_container), oauth_issues_header,
    306                        FALSE, FALSE, 0);
    307 
    308     for (size_t i = 0; i < prompt.GetOAuthIssueCount(); ++i) {
    309       GtkWidget* issue_advice_widget =
    310           CreateWidgetForIssueAdvice(prompt.GetOAuthIssue(i), pixel_width);
    311       gtk_box_pack_start(GTK_BOX(oauth_issues_container), issue_advice_widget,
    312                          FALSE, FALSE, kPermissionsPadding);
    313     }
    314   }
    315 
    316   if (show_retained_files) {
    317     GtkWidget* retained_files_container =
    318         (show_permissions || show_oauth_issues) ? content_vbox
    319                                                 : left_column_area;
    320     int pixel_width =
    321         left_column_min_width +
    322         ((show_permissions || show_oauth_issues) ? kImageSize : 0);
    323 
    324     GtkWidget* retained_files_header = gtk_util::CreateBoldLabel(
    325         UTF16ToUTF8(prompt.GetRetainedFilesHeading()).c_str());
    326     gtk_util::SetLabelWidth(retained_files_header, pixel_width);
    327     gtk_box_pack_start(GTK_BOX(retained_files_container), retained_files_header,
    328                        FALSE, FALSE, 0);
    329 
    330     GtkWidget* paths_vbox = gtk_vbox_new(FALSE, kPermissionsPadding);
    331     for (size_t i = 0; i < prompt.GetRetainedFileCount(); ++i) {
    332       std::string path = base::UTF16ToUTF8(prompt.GetRetainedFile(i));
    333       GtkWidget* path_label = gtk_label_new(path.c_str());
    334       gtk_util::LeftAlignMisc(path_label);
    335       gtk_box_pack_start(GTK_BOX(paths_vbox), path_label, FALSE, FALSE, 0);
    336     }
    337     GtkWidget* paths_scrolled_window = gtk_scrolled_window_new(NULL, NULL);
    338     gtk_widget_set_size_request(
    339         paths_scrolled_window, pixel_width, kMaxRetainedFilesHeight);
    340     gtk_scrolled_window_add_with_viewport(
    341         GTK_SCROLLED_WINDOW(paths_scrolled_window), paths_vbox);
    342     gtk_box_pack_start(GTK_BOX(retained_files_container), paths_scrolled_window,
    343                        FALSE, FALSE, kPermissionsPadding);
    344   }
    345 
    346   g_signal_connect(dialog_, "response", G_CALLBACK(OnResponseThunk), this);
    347   gtk_window_set_resizable(GTK_WINDOW(dialog_), FALSE);
    348 
    349   gtk_dialog_set_default_response(GTK_DIALOG(dialog_), GTK_RESPONSE_CLOSE);
    350   gtk_widget_show_all(dialog_);
    351   gtk_widget_grab_focus(close_button);
    352 }
    353 
    354 ExtensionInstallDialog::~ExtensionInstallDialog() {
    355 }
    356 
    357 void ExtensionInstallDialog::OnResponse(GtkWidget* dialog, int response_id) {
    358   if (response_id == GTK_RESPONSE_ACCEPT)
    359     delegate_->InstallUIProceed();
    360   else
    361     delegate_->InstallUIAbort(true);
    362 
    363   gtk_widget_destroy(dialog_);
    364   delete this;
    365 }
    366 
    367 void ExtensionInstallDialog::OnStoreLinkClick(GtkWidget* sender) {
    368   GURL store_url(
    369       extension_urls::GetWebstoreItemDetailURLPrefix() + extension_id_);
    370   navigator_->OpenURL(OpenURLParams(
    371       store_url, content::Referrer(), NEW_FOREGROUND_TAB,
    372       content::PAGE_TRANSITION_LINK, false));
    373 
    374   OnResponse(dialog_, GTK_RESPONSE_CLOSE);
    375 }
    376 
    377 GtkWidget* ExtensionInstallDialog::CreateWidgetForIssueAdvice(
    378     const IssueAdviceInfoEntry& issue_advice, int pixel_width) {
    379   GtkWidget* box = gtk_vbox_new(FALSE, ui::kControlSpacing);
    380   GtkWidget* header = gtk_hbox_new(FALSE, 0);
    381   GtkWidget* event_box = gtk_event_box_new();
    382   gtk_container_add(GTK_CONTAINER(event_box), header);
    383   gtk_box_pack_start(GTK_BOX(box), event_box, FALSE, FALSE,
    384                      kPermissionsPadding);
    385 
    386   GtkWidget* arrow = NULL;
    387   GtkWidget* label = NULL;
    388   int label_pixel_width = pixel_width;
    389 
    390   if (issue_advice.details.empty()) {
    391     label = gtk_label_new(l10n_util::GetStringFUTF8(
    392         IDS_EXTENSION_PERMISSION_LINE,
    393         issue_advice.description).c_str());
    394   } else {
    395     arrow = gtk_arrow_new(GTK_ARROW_RIGHT, GTK_SHADOW_OUT);
    396     GtkRequisition req;
    397     gtk_widget_size_request(arrow, &req);
    398     label_pixel_width -= req.width;
    399 
    400     label = gtk_label_new(UTF16ToUTF8(issue_advice.description).c_str());
    401 
    402     GtkWidget* detail_box = gtk_vbox_new(FALSE, ui::kControlSpacing);
    403     gtk_box_pack_start(GTK_BOX(box), detail_box, FALSE, FALSE, 0);
    404     gtk_widget_set_no_show_all(detail_box, TRUE);
    405     gtk_object_set_data(GTK_OBJECT(event_box), "arrow", arrow);
    406 
    407     for (size_t i = 0; i < issue_advice.details.size(); ++i) {
    408       std::string text = l10n_util::GetStringFUTF8(
    409           IDS_EXTENSION_PERMISSION_LINE, issue_advice.details[i]);
    410       GtkWidget* label = gtk_label_new(text.c_str());
    411       gtk_util::SetLabelWidth(label, pixel_width - kDetailIndent);
    412 
    413       GtkWidget* align = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
    414       gtk_alignment_set_padding(GTK_ALIGNMENT(align), 0, 0, kDetailIndent, 0);
    415       gtk_container_add(GTK_CONTAINER(align), label);
    416       gtk_box_pack_start(GTK_BOX(detail_box), align, FALSE, FALSE,
    417                          kPermissionsPadding);
    418     }
    419 
    420     g_signal_connect(event_box, "realize",
    421                      G_CALLBACK(OnZippyButtonRealize), NULL);
    422     g_signal_connect(event_box, "button-release-event",
    423                      G_CALLBACK(OnZippyButtonRelease), detail_box);
    424   }
    425 
    426   gtk_util::SetLabelWidth(label, label_pixel_width);
    427   if (arrow)
    428     gtk_box_pack_start(GTK_BOX(header), arrow, FALSE, FALSE, 0);
    429   gtk_box_pack_start(GTK_BOX(header), label, TRUE, TRUE, 0);
    430 
    431   return box;
    432 }
    433 
    434 }  // namespace chrome
    435 
    436 namespace {
    437 
    438 void ShowExtensionInstallDialogImpl(
    439     const ExtensionInstallPrompt::ShowParams& show_params,
    440     ExtensionInstallPrompt::Delegate* delegate,
    441     const ExtensionInstallPrompt::Prompt& prompt) {
    442   new chrome::ExtensionInstallDialog(show_params, delegate, prompt);
    443 }
    444 
    445 }  // namespace
    446 
    447 // static
    448 ExtensionInstallPrompt::ShowDialogCallback
    449 ExtensionInstallPrompt::GetDefaultShowDialogCallback() {
    450   return base::Bind(&ShowExtensionInstallDialogImpl);
    451 }
    452