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