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.
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_constants.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 "extensions/common/extension.h"
21 #include "grit/generated_resources.h"
22 #include "skia/ext/image_operations.h"
23 #include "ui/base/gtk/gtk_hig_constants.h"
24 #include "ui/base/l10n/l10n_util.h"
25 #include "ui/base/resource/resource_bundle.h"
26 #include "ui/gfx/gtk_util.h"
28 using content::OpenURLParams
;
29 using extensions::BundleInstaller
;
33 const int kLeftColumnMinWidth
= 250;
34 // External installs have more text, so use a wider dialog.
35 const int kExternalInstallLeftColumnWidth
= 350;
36 const int kImageSize
= 69;
37 const int kDetailIndent
= 20;
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
;
44 const double kRatingTextSize
= 12.1; // 12.1px = 9pt @ 96dpi
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);
55 // Returns an expander with the lines in |details|.
56 GtkWidget
* CreateDetailsWidget(const std::vector
<base::string16
>& details
,
59 GtkWidget
* expander
= gtk_expander_new(
60 l10n_util::GetStringUTF8(IDS_EXTENSIONS_DETAILS
).c_str());
61 GtkWidget
* align
= gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
62 gtk_container_add(GTK_CONTAINER(expander
), align
);
63 GtkWidget
* details_vbox
= gtk_vbox_new(FALSE
, kPermissionsPadding
);
64 gtk_alignment_set_padding(GTK_ALIGNMENT(align
), 0, 0, kDetailIndent
, 0);
65 gtk_container_add(GTK_CONTAINER(align
), details_vbox
);
67 for (size_t i
= 0; i
< details
.size(); ++i
) {
68 std::string detail
= show_bullets
?
69 l10n_util::GetStringFUTF8(IDS_EXTENSION_PERMISSION_LINE
, details
[0]) :
70 base::UTF16ToUTF8(details
[i
]);
71 GtkWidget
* detail_label
= gtk_label_new(detail
.c_str());
72 gtk_label_set_line_wrap(GTK_LABEL(detail_label
), true);
73 gtk_util::SetLabelWidth(detail_label
, width
- kDetailIndent
);
75 GTK_BOX(details_vbox
), detail_label
, FALSE
, FALSE
, kPermissionsPadding
);
84 // Displays the dialog when constructed, deletes itself when dialog is
85 // dismissed. Success/failure is passed back through the
86 // ExtensionInstallPrompt::Delegate instance.
87 class ExtensionInstallDialog
{
89 ExtensionInstallDialog(const ExtensionInstallPrompt::ShowParams
& show_params
,
90 ExtensionInstallPrompt::Delegate
* delegate
,
91 const ExtensionInstallPrompt::Prompt
& prompt
);
93 ~ExtensionInstallDialog();
95 CHROMEGTK_CALLBACK_1(ExtensionInstallDialog
, void, OnResponse
, int);
96 CHROMEGTK_CALLBACK_0(ExtensionInstallDialog
, void, OnStoreLinkClick
);
98 GtkWidget
* CreateWidgetForIssueAdvice(
99 const IssueAdviceInfoEntry
& issue_advice
, int pixel_width
);
101 content::PageNavigator
* navigator_
;
102 ExtensionInstallPrompt::Delegate
* delegate_
;
103 std::string extension_id_
; // Set for INLINE_INSTALL_PROMPT.
107 ExtensionInstallDialog::ExtensionInstallDialog(
108 const ExtensionInstallPrompt::ShowParams
& show_params
,
109 ExtensionInstallPrompt::Delegate
*delegate
,
110 const ExtensionInstallPrompt::Prompt
& prompt
)
111 : navigator_(show_params
.navigator
),
114 bool show_permissions
= prompt
.ShouldShowPermissions();
115 bool show_oauth_issues
= prompt
.GetOAuthIssueCount() > 0;
116 bool show_retained_files
= prompt
.GetRetainedFileCount() > 0;
117 bool is_inline_install
=
118 prompt
.type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT
;
119 bool has_webstore_data
= prompt
.has_webstore_data();
120 bool is_bundle_install
=
121 prompt
.type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT
;
122 bool is_external_install
=
123 prompt
.type() == ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT
;
125 if (is_inline_install
)
126 extension_id_
= prompt
.extension()->id();
129 gfx::NativeWindow parent
= show_params
.parent_window
;
130 dialog_
= gtk_dialog_new_with_buttons(
131 base::UTF16ToUTF8(prompt
.GetDialogTitle()).c_str(),
135 GtkWidget
* close_button
= gtk_dialog_add_button(
137 prompt
.HasAbortButtonLabel() ?
138 base::UTF16ToUTF8(prompt
.GetAbortButtonLabel()).c_str() :
141 if (prompt
.HasAcceptButtonLabel()) {
142 gtk_dialog_add_button(
144 base::UTF16ToUTF8(prompt
.GetAcceptButtonLabel()).c_str(),
145 GTK_RESPONSE_ACCEPT
);
147 #if !GTK_CHECK_VERSION(2, 22, 0)
148 gtk_dialog_set_has_separator(GTK_DIALOG(dialog_
), FALSE
);
151 GtkWidget
* scrolled_window
= gtk_scrolled_window_new(NULL
, NULL
);
152 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window
),
154 GTK_POLICY_AUTOMATIC
);
155 GtkWidget
* content_area
= gtk_dialog_get_content_area(GTK_DIALOG(dialog_
));
156 gtk_box_set_spacing(GTK_BOX(content_area
), ui::kContentAreaSpacing
);
158 // Divide the dialog vertically (item data and icon on the top, permissions
160 GtkWidget
* content_vbox
= gtk_vbox_new(FALSE
, ui::kControlSpacing
);
161 gtk_container_set_border_width(GTK_CONTAINER(content_vbox
),
162 ui::kContentAreaBorder
);
163 gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrolled_window
),
165 GtkWidget
* viewport
= gtk_bin_get_child(GTK_BIN(scrolled_window
));
166 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport
), GTK_SHADOW_NONE
);
167 gtk_box_pack_start(GTK_BOX(content_area
), scrolled_window
, TRUE
, TRUE
, 0);
169 // Create a two column layout for the top (item data on the left, icon on
171 GtkWidget
* top_content_hbox
= gtk_hbox_new(FALSE
, ui::kContentAreaSpacing
);
172 gtk_box_pack_start(GTK_BOX(content_vbox
), top_content_hbox
, TRUE
, TRUE
, 0);
174 // We don't show the image for bundle installs, so let the left column take
176 int left_column_min_width
= kLeftColumnMinWidth
;
177 if (is_bundle_install
)
178 left_column_min_width
+= kImageSize
;
179 if (is_external_install
)
180 left_column_min_width
= kExternalInstallLeftColumnWidth
;
182 // Create a new vbox for the left column.
183 GtkWidget
* left_column_area
= gtk_vbox_new(FALSE
, ui::kControlSpacing
);
184 gtk_box_pack_start(GTK_BOX(top_content_hbox
), left_column_area
,
186 gtk_widget_set_size_request(left_column_area
, left_column_min_width
, -1);
188 GtkWidget
* heading_vbox
= gtk_vbox_new(FALSE
, 0);
189 // If we are not going to show anything else, vertically center the title.
190 bool center_heading
= !show_permissions
&& !show_oauth_issues
&&
191 !is_inline_install
&& !show_retained_files
;
192 gtk_box_pack_start(GTK_BOX(left_column_area
), heading_vbox
, center_heading
,
196 GtkWidget
* heading_label
= gtk_util::CreateBoldLabel(
197 base::UTF16ToUTF8(prompt
.GetHeading().c_str()));
198 gtk_util::SetLabelWidth(heading_label
, left_column_min_width
);
199 gtk_box_pack_start(GTK_BOX(heading_vbox
), heading_label
, center_heading
,
202 if (has_webstore_data
) {
203 // Average rating (as stars) and number of ratings.
204 GtkWidget
* stars_hbox
= gtk_hbox_new(FALSE
, 0);
205 gtk_box_pack_start(GTK_BOX(heading_vbox
), stars_hbox
, FALSE
, FALSE
, 0);
206 prompt
.AppendRatingStars(AddResourceIcon
, stars_hbox
);
207 GtkWidget
* rating_label
= gtk_label_new(base::UTF16ToUTF8(
208 prompt
.GetRatingCount()).c_str());
209 gtk_util::ForceFontSizePixels(rating_label
, kRatingTextSize
);
210 gtk_box_pack_start(GTK_BOX(stars_hbox
), rating_label
,
214 GtkWidget
* users_label
= gtk_label_new(base::UTF16ToUTF8(
215 prompt
.GetUserCount()).c_str());
216 gtk_util::SetLabelWidth(users_label
, left_column_min_width
);
217 gtk_util::SetLabelColor(users_label
, &ui::kGdkGray
);
218 gtk_util::ForceFontSizePixels(rating_label
, kRatingTextSize
);
219 gtk_box_pack_start(GTK_BOX(heading_vbox
), users_label
,
223 GtkWidget
* store_link
= gtk_chrome_link_button_new(
224 l10n_util::GetStringUTF8(IDS_EXTENSION_PROMPT_STORE_LINK
).c_str());
225 gtk_util::ForceFontSizePixels(store_link
, kRatingTextSize
);
226 GtkWidget
* store_link_hbox
= gtk_hbox_new(FALSE
, 0);
227 // Stick it in an hbox so it doesn't expand to the whole width.
228 gtk_box_pack_start(GTK_BOX(store_link_hbox
), store_link
, FALSE
, FALSE
, 0);
229 gtk_box_pack_start(GTK_BOX(heading_vbox
), store_link_hbox
, FALSE
, FALSE
, 0);
230 g_signal_connect(store_link
, "clicked",
231 G_CALLBACK(OnStoreLinkClickThunk
), this);
234 if (is_bundle_install
) {
235 // Add the list of extensions to be installed.
236 GtkWidget
* extensions_vbox
= gtk_vbox_new(FALSE
, ui::kControlSpacing
);
237 gtk_box_pack_start(GTK_BOX(heading_vbox
), extensions_vbox
, FALSE
, FALSE
,
238 ui::kControlSpacing
);
240 BundleInstaller::ItemList items
= prompt
.bundle()->GetItemsWithState(
241 BundleInstaller::Item::STATE_PENDING
);
242 for (size_t i
= 0; i
< items
.size(); ++i
) {
243 GtkWidget
* extension_label
= gtk_label_new(base::UTF16ToUTF8(
244 items
[i
].GetNameForDisplay()).c_str());
245 gtk_util::SetLabelWidth(extension_label
, left_column_min_width
);
246 gtk_box_pack_start(GTK_BOX(extensions_vbox
), extension_label
,
247 FALSE
, FALSE
, kExtensionsPadding
);
250 // Resize the icon if necessary.
251 SkBitmap scaled_icon
= *prompt
.icon().ToSkBitmap();
252 if (scaled_icon
.width() > kImageSize
|| scaled_icon
.height() > kImageSize
) {
253 scaled_icon
= skia::ImageOperations::Resize(
254 scaled_icon
, skia::ImageOperations::RESIZE_LANCZOS3
,
255 kImageSize
, kImageSize
);
258 // Put icon in the right column.
259 GdkPixbuf
* pixbuf
= gfx::GdkPixbufFromSkBitmap(scaled_icon
);
260 GtkWidget
* icon
= gtk_image_new_from_pixbuf(pixbuf
);
261 g_object_unref(pixbuf
);
262 gtk_box_pack_start(GTK_BOX(top_content_hbox
), icon
, FALSE
, FALSE
, 0);
263 // Top justify the image.
264 gtk_misc_set_alignment(GTK_MISC(icon
), 0.5, 0.0);
267 // Permissions are shown separated by a divider for inline installs, or
268 // directly under the heading for regular installs (where we don't have
270 if (show_permissions
) {
271 GtkWidget
* permissions_container
;
272 if (is_inline_install
) {
273 permissions_container
= content_vbox
;
274 gtk_box_pack_start(GTK_BOX(content_vbox
), gtk_hseparator_new(),
275 FALSE
, FALSE
, ui::kControlSpacing
);
277 permissions_container
= left_column_area
;
280 if (prompt
.GetPermissionCount() > 0) {
281 GtkWidget
* permissions_header
= gtk_util::CreateBoldLabel(
282 base::UTF16ToUTF8(prompt
.GetPermissionsHeading()).c_str());
283 gtk_util::SetLabelWidth(permissions_header
, left_column_min_width
);
284 gtk_box_pack_start(GTK_BOX(permissions_container
), permissions_header
,
287 for (size_t i
= 0; i
< prompt
.GetPermissionCount(); ++i
) {
288 GtkWidget
* permission_vbox
= gtk_vbox_new(FALSE
, 0);
289 std::string permission
= l10n_util::GetStringFUTF8(
290 IDS_EXTENSION_PERMISSION_LINE
, prompt
.GetPermission(i
));
291 GtkWidget
* permission_label
= gtk_label_new(permission
.c_str());
292 gtk_util::SetLabelWidth(permission_label
, left_column_min_width
);
293 gtk_box_pack_start(GTK_BOX(permission_vbox
), permission_label
,
295 if (!prompt
.GetPermissionsDetails(i
).empty()) {
296 std::vector
<base::string16
> details
;
297 details
.push_back(prompt
.GetPermissionsDetails(i
));
299 GTK_BOX(permission_vbox
),
300 CreateDetailsWidget(details
, left_column_min_width
, false),
305 gtk_box_pack_start(GTK_BOX(permissions_container
), permission_vbox
,
306 FALSE
, FALSE
, kPermissionsPadding
);
309 GtkWidget
* permission_label
= gtk_label_new(l10n_util::GetStringUTF8(
310 IDS_EXTENSION_NO_SPECIAL_PERMISSIONS
).c_str());
311 gtk_util::SetLabelWidth(permission_label
, left_column_min_width
);
312 gtk_box_pack_start(GTK_BOX(permissions_container
), permission_label
,
313 FALSE
, FALSE
, kPermissionsPadding
);
317 if (show_oauth_issues
) {
318 // If permissions are shown, then the scopes will go below them and take
319 // up the entire width of the dialog. Otherwise the scopes will go where
320 // the permissions usually go.
321 GtkWidget
* oauth_issues_container
=
322 show_permissions
? content_vbox
: left_column_area
;
323 int pixel_width
= left_column_min_width
+
324 (show_permissions
? kImageSize
: 0);
326 GtkWidget
* oauth_issues_header
= gtk_util::CreateBoldLabel(
327 base::UTF16ToUTF8(prompt
.GetOAuthHeading()).c_str());
328 gtk_util::SetLabelWidth(oauth_issues_header
, pixel_width
);
329 gtk_box_pack_start(GTK_BOX(oauth_issues_container
), oauth_issues_header
,
332 for (size_t i
= 0; i
< prompt
.GetOAuthIssueCount(); ++i
) {
333 GtkWidget
* issue_advice_widget
=
334 CreateWidgetForIssueAdvice(prompt
.GetOAuthIssue(i
), pixel_width
);
335 gtk_box_pack_start(GTK_BOX(oauth_issues_container
), issue_advice_widget
,
336 FALSE
, FALSE
, kPermissionsPadding
);
340 if (show_retained_files
) {
341 GtkWidget
* retained_files_container
=
342 (show_permissions
|| show_oauth_issues
) ? content_vbox
345 left_column_min_width
+
346 ((show_permissions
|| show_oauth_issues
) ? kImageSize
: 0);
348 GtkWidget
* retained_files_header
= gtk_util::CreateBoldLabel(
349 base::UTF16ToUTF8(prompt
.GetRetainedFilesHeading()).c_str());
350 gtk_util::SetLabelWidth(retained_files_header
, pixel_width
);
351 gtk_box_pack_start(GTK_BOX(retained_files_container
), retained_files_header
,
354 std::vector
<base::string16
> paths
;
355 for (size_t i
= 0; i
< prompt
.GetRetainedFileCount(); ++i
) {
356 paths
.push_back(prompt
.GetRetainedFile(i
));
358 gtk_box_pack_start(GTK_BOX(retained_files_container
),
359 CreateDetailsWidget(paths
, pixel_width
, false),
362 kPermissionsPadding
);
365 g_signal_connect(dialog_
, "response", G_CALLBACK(OnResponseThunk
), this);
366 gtk_window_set_resizable(GTK_WINDOW(dialog_
), FALSE
);
368 gtk_dialog_set_default_response(GTK_DIALOG(dialog_
), GTK_RESPONSE_CLOSE
);
369 gtk_widget_show_all(dialog_
);
371 gtk_container_set_border_width(GTK_CONTAINER(content_area
), 0);
372 gtk_container_set_border_width(
373 GTK_CONTAINER(gtk_dialog_get_action_area(GTK_DIALOG(dialog_
))),
374 ui::kContentAreaBorder
);
375 gtk_box_set_spacing(GTK_BOX(gtk_bin_get_child(GTK_BIN(dialog_
))), 0);
376 GtkRequisition requisition
;
377 gtk_widget_size_request(content_vbox
, &requisition
);
378 gtk_widget_set_size_request(
379 scrolled_window
, requisition
.width
, requisition
.height
);
380 gtk_widget_grab_focus(close_button
);
383 ExtensionInstallDialog::~ExtensionInstallDialog() {
386 void ExtensionInstallDialog::OnResponse(GtkWidget
* dialog
, int response_id
) {
387 if (response_id
== GTK_RESPONSE_ACCEPT
)
388 delegate_
->InstallUIProceed();
390 delegate_
->InstallUIAbort(true);
392 gtk_widget_destroy(dialog_
);
396 void ExtensionInstallDialog::OnStoreLinkClick(GtkWidget
* sender
) {
398 extension_urls::GetWebstoreItemDetailURLPrefix() + extension_id_
);
399 navigator_
->OpenURL(OpenURLParams(
400 store_url
, content::Referrer(), NEW_FOREGROUND_TAB
,
401 content::PAGE_TRANSITION_LINK
, false));
403 OnResponse(dialog_
, GTK_RESPONSE_CLOSE
);
406 GtkWidget
* ExtensionInstallDialog::CreateWidgetForIssueAdvice(
407 const IssueAdviceInfoEntry
& issue_advice
, int pixel_width
) {
408 GtkWidget
* box
= gtk_vbox_new(FALSE
, 0);
409 GtkWidget
* label
= gtk_label_new(l10n_util::GetStringFUTF8(
410 IDS_EXTENSION_PERMISSION_LINE
, issue_advice
.description
).c_str());
411 gtk_util::SetLabelWidth(label
, pixel_width
);
412 gtk_box_pack_start(GTK_BOX(box
), label
, TRUE
, TRUE
, 0);
414 if (!issue_advice
.details
.empty()) {
417 CreateDetailsWidget(issue_advice
.details
, pixel_width
, true),
425 } // namespace chrome
429 void ShowExtensionInstallDialogImpl(
430 const ExtensionInstallPrompt::ShowParams
& show_params
,
431 ExtensionInstallPrompt::Delegate
* delegate
,
432 const ExtensionInstallPrompt::Prompt
& prompt
) {
433 new chrome::ExtensionInstallDialog(show_params
, delegate
, prompt
);
439 ExtensionInstallPrompt::ShowDialogCallback
440 ExtensionInstallPrompt::GetDefaultShowDialogCallback() {
441 return base::Bind(&ShowExtensionInstallDialogImpl
);