1 // Copyright (c) 2015 The Chromium Embedded Framework Authors. All rights
2 // reserved. Use of this source code is governed by a BSD-style license that
3 // can be found in the LICENSE file.
4
5 #include "tests/cefclient/browser/dialog_handler_gtk.h"
6
7 #include <libgen.h>
8 #include <sys/stat.h>
9
10 #include "include/cef_browser.h"
11 #include "include/cef_parser.h"
12 #include "include/wrapper/cef_helpers.h"
13 #include "tests/cefclient/browser/root_window.h"
14 #include "tests/cefclient/browser/util_gtk.h"
15
16 namespace client {
17
18 namespace {
19
20 const char kPromptTextId[] = "cef_prompt_text";
21
22 // If there's a text entry in the dialog, get the text from the first one and
23 // return it.
GetPromptText(GtkDialog * dialog)24 std::string GetPromptText(GtkDialog* dialog) {
25 GtkWidget* widget = static_cast<GtkWidget*>(
26 g_object_get_data(G_OBJECT(dialog), kPromptTextId));
27 if (widget)
28 return gtk_entry_get_text(GTK_ENTRY(widget));
29 return std::string();
30 }
31
GetDescriptionFromMimeType(const std::string & mime_type)32 std::string GetDescriptionFromMimeType(const std::string& mime_type) {
33 // Check for wild card mime types and return an appropriate description.
34 static const struct {
35 const char* mime_type;
36 const char* label;
37 } kWildCardMimeTypes[] = {
38 {"audio", "Audio Files"},
39 {"image", "Image Files"},
40 {"text", "Text Files"},
41 {"video", "Video Files"},
42 };
43
44 for (size_t i = 0;
45 i < sizeof(kWildCardMimeTypes) / sizeof(kWildCardMimeTypes[0]); ++i) {
46 if (mime_type == std::string(kWildCardMimeTypes[i].mime_type) + "/*")
47 return std::string(kWildCardMimeTypes[i].label);
48 }
49
50 return std::string();
51 }
52
AddFilters(GtkFileChooser * chooser,const std::vector<CefString> & accept_filters,bool include_all_files,std::vector<GtkFileFilter * > * filters)53 void AddFilters(GtkFileChooser* chooser,
54 const std::vector<CefString>& accept_filters,
55 bool include_all_files,
56 std::vector<GtkFileFilter*>* filters) {
57 bool has_filter = false;
58
59 for (size_t j = 0; j < accept_filters.size(); ++j) {
60 const std::string& filter = accept_filters[j];
61 if (filter.empty())
62 continue;
63
64 std::vector<std::string> extensions;
65 std::string description;
66
67 size_t sep_index = filter.find('|');
68 if (sep_index != std::string::npos) {
69 // Treat as a filter of the form "Filter Name|.ext1;.ext2;.ext3".
70 description = filter.substr(0, sep_index);
71
72 const std::string& exts = filter.substr(sep_index + 1);
73 size_t last = 0;
74 size_t size = exts.size();
75 for (size_t i = 0; i <= size; ++i) {
76 if (i == size || exts[i] == ';') {
77 std::string ext(exts, last, i - last);
78 if (!ext.empty() && ext[0] == '.')
79 extensions.push_back(ext);
80 last = i + 1;
81 }
82 }
83 } else if (filter[0] == '.') {
84 // Treat as an extension beginning with the '.' character.
85 extensions.push_back(filter);
86 } else {
87 // Otherwise convert mime type to one or more extensions.
88 description = GetDescriptionFromMimeType(filter);
89
90 std::vector<CefString> ext;
91 CefGetExtensionsForMimeType(filter, ext);
92 for (size_t x = 0; x < ext.size(); ++x)
93 extensions.push_back("." + ext[x].ToString());
94 }
95
96 if (extensions.empty())
97 continue;
98
99 GtkFileFilter* gtk_filter = gtk_file_filter_new();
100
101 std::string ext_str;
102 for (size_t x = 0; x < extensions.size(); ++x) {
103 const std::string& pattern = "*" + extensions[x];
104 if (x != 0)
105 ext_str += ";";
106 ext_str += pattern;
107 gtk_file_filter_add_pattern(gtk_filter, pattern.c_str());
108 }
109
110 if (description.empty())
111 description = ext_str;
112 else
113 description += " (" + ext_str + ")";
114
115 gtk_file_filter_set_name(gtk_filter, description.c_str());
116 gtk_file_chooser_add_filter(chooser, gtk_filter);
117 if (!has_filter)
118 has_filter = true;
119
120 filters->push_back(gtk_filter);
121 }
122
123 // Add the *.* filter, but only if we have added other filters (otherwise it
124 // is implied).
125 if (include_all_files && has_filter) {
126 GtkFileFilter* filter = gtk_file_filter_new();
127 gtk_file_filter_add_pattern(filter, "*");
128 gtk_file_filter_set_name(filter, "All Files (*)");
129 gtk_file_chooser_add_filter(chooser, filter);
130 }
131 }
132
GetWindow(CefRefPtr<CefBrowser> browser)133 GtkWindow* GetWindow(CefRefPtr<CefBrowser> browser) {
134 REQUIRE_MAIN_THREAD();
135 scoped_refptr<RootWindow> root_window =
136 RootWindow::GetForBrowser(browser->GetIdentifier());
137 if (root_window) {
138 GtkWidget* window = root_window->GetWindowHandle();
139 if (!window)
140 LOG(ERROR) << "No GtkWindow for browser";
141 return GTK_WINDOW(window);
142 }
143 return nullptr;
144 }
145
146 } // namespace
147
ClientDialogHandlerGtk()148 ClientDialogHandlerGtk::ClientDialogHandlerGtk() : gtk_dialog_(nullptr) {}
149
OnFileDialog(CefRefPtr<CefBrowser> browser,FileDialogMode mode,const CefString & title,const CefString & default_file_path,const std::vector<CefString> & accept_filters,int selected_accept_filter,CefRefPtr<CefFileDialogCallback> callback)150 bool ClientDialogHandlerGtk::OnFileDialog(
151 CefRefPtr<CefBrowser> browser,
152 FileDialogMode mode,
153 const CefString& title,
154 const CefString& default_file_path,
155 const std::vector<CefString>& accept_filters,
156 int selected_accept_filter,
157 CefRefPtr<CefFileDialogCallback> callback) {
158 CEF_REQUIRE_UI_THREAD();
159
160 OnFileDialogParams params;
161 params.browser = browser;
162 params.mode = mode;
163 params.title = title;
164 params.default_file_path = default_file_path;
165 params.accept_filters = accept_filters;
166 params.selected_accept_filter = selected_accept_filter;
167 params.callback = callback;
168
169 GetWindowAndContinue(
170 browser, base::BindOnce(&ClientDialogHandlerGtk::OnFileDialogContinue,
171 this, params));
172 return true;
173 }
174
OnJSDialog(CefRefPtr<CefBrowser> browser,const CefString & origin_url,JSDialogType dialog_type,const CefString & message_text,const CefString & default_prompt_text,CefRefPtr<CefJSDialogCallback> callback,bool & suppress_message)175 bool ClientDialogHandlerGtk::OnJSDialog(CefRefPtr<CefBrowser> browser,
176 const CefString& origin_url,
177 JSDialogType dialog_type,
178 const CefString& message_text,
179 const CefString& default_prompt_text,
180 CefRefPtr<CefJSDialogCallback> callback,
181 bool& suppress_message) {
182 CEF_REQUIRE_UI_THREAD();
183
184 OnJSDialogParams params;
185 params.browser = browser;
186 params.origin_url = origin_url;
187 params.dialog_type = dialog_type;
188 params.message_text = message_text;
189 params.default_prompt_text = default_prompt_text;
190 params.callback = callback;
191
192 GetWindowAndContinue(
193 browser, base::BindOnce(&ClientDialogHandlerGtk::OnJSDialogContinue, this,
194 params));
195 return true;
196 }
197
OnBeforeUnloadDialog(CefRefPtr<CefBrowser> browser,const CefString & message_text,bool is_reload,CefRefPtr<CefJSDialogCallback> callback)198 bool ClientDialogHandlerGtk::OnBeforeUnloadDialog(
199 CefRefPtr<CefBrowser> browser,
200 const CefString& message_text,
201 bool is_reload,
202 CefRefPtr<CefJSDialogCallback> callback) {
203 CEF_REQUIRE_UI_THREAD();
204
205 const std::string& new_message_text =
206 message_text.ToString() + "\n\nIs it OK to leave/reload this page?";
207 bool suppress_message = false;
208
209 return OnJSDialog(browser, CefString(), JSDIALOGTYPE_CONFIRM,
210 new_message_text, CefString(), callback, suppress_message);
211 }
212
OnResetDialogState(CefRefPtr<CefBrowser> browser)213 void ClientDialogHandlerGtk::OnResetDialogState(CefRefPtr<CefBrowser> browser) {
214 CEF_REQUIRE_UI_THREAD();
215
216 if (!gtk_dialog_)
217 return;
218
219 gtk_widget_destroy(gtk_dialog_);
220 gtk_dialog_ = nullptr;
221 js_dialog_callback_ = nullptr;
222 }
223
OnFileDialogContinue(OnFileDialogParams params,GtkWindow * window)224 void ClientDialogHandlerGtk::OnFileDialogContinue(OnFileDialogParams params,
225 GtkWindow* window) {
226 REQUIRE_MAIN_THREAD();
227
228 ScopedGdkThreadsEnter scoped_gdk_threads;
229
230 std::vector<CefString> files;
231
232 GtkFileChooserAction action;
233 const gchar* accept_button;
234
235 // Remove any modifier flags.
236 FileDialogMode mode_type =
237 static_cast<FileDialogMode>(params.mode & FILE_DIALOG_TYPE_MASK);
238
239 if (mode_type == FILE_DIALOG_OPEN || mode_type == FILE_DIALOG_OPEN_MULTIPLE) {
240 action = GTK_FILE_CHOOSER_ACTION_OPEN;
241 accept_button = "_Open";
242 } else if (mode_type == FILE_DIALOG_OPEN_FOLDER) {
243 action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
244 accept_button = "_Open";
245 } else if (mode_type == FILE_DIALOG_SAVE) {
246 action = GTK_FILE_CHOOSER_ACTION_SAVE;
247 accept_button = "_Save";
248 } else {
249 NOTREACHED();
250 params.callback->Cancel();
251 return;
252 }
253
254 std::string title_str;
255 if (!params.title.empty()) {
256 title_str = params.title;
257 } else {
258 switch (mode_type) {
259 case FILE_DIALOG_OPEN:
260 title_str = "Open File";
261 break;
262 case FILE_DIALOG_OPEN_MULTIPLE:
263 title_str = "Open Files";
264 break;
265 case FILE_DIALOG_OPEN_FOLDER:
266 title_str = "Open Folder";
267 break;
268 case FILE_DIALOG_SAVE:
269 title_str = "Save File";
270 break;
271 default:
272 break;
273 }
274 }
275
276 GtkWidget* dialog = gtk_file_chooser_dialog_new(
277 title_str.c_str(), GTK_WINDOW(window), action, "_Cancel",
278 GTK_RESPONSE_CANCEL, accept_button, GTK_RESPONSE_ACCEPT, nullptr);
279
280 if (mode_type == FILE_DIALOG_OPEN_MULTIPLE)
281 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE);
282
283 if (mode_type == FILE_DIALOG_SAVE) {
284 gtk_file_chooser_set_do_overwrite_confirmation(
285 GTK_FILE_CHOOSER(dialog),
286 !!(params.mode & FILE_DIALOG_OVERWRITEPROMPT_FLAG));
287 }
288
289 gtk_file_chooser_set_show_hidden(
290 GTK_FILE_CHOOSER(dialog), !(params.mode & FILE_DIALOG_HIDEREADONLY_FLAG));
291
292 if (!params.default_file_path.empty() && mode_type == FILE_DIALOG_SAVE) {
293 const std::string& file_path = params.default_file_path;
294 bool exists = false;
295
296 struct stat sb;
297 if (stat(file_path.c_str(), &sb) == 0 && S_ISREG(sb.st_mode)) {
298 // Use the directory and name of the existing file.
299 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog), file_path.data());
300 exists = true;
301 }
302
303 if (!exists) {
304 // Set the current file name but let the user choose the directory.
305 std::string file_name_str = file_path;
306 const char* file_name = basename(const_cast<char*>(file_name_str.data()));
307 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), file_name);
308 }
309 }
310
311 std::vector<GtkFileFilter*> filters;
312 AddFilters(GTK_FILE_CHOOSER(dialog), params.accept_filters, true, &filters);
313 if (params.selected_accept_filter < static_cast<int>(filters.size())) {
314 gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog),
315 filters[params.selected_accept_filter]);
316 }
317
318 bool success = false;
319
320 if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
321 if (mode_type == FILE_DIALOG_OPEN || mode_type == FILE_DIALOG_OPEN_FOLDER ||
322 mode_type == FILE_DIALOG_SAVE) {
323 char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
324 files.push_back(std::string(filename));
325 success = true;
326 } else if (mode_type == FILE_DIALOG_OPEN_MULTIPLE) {
327 GSList* filenames =
328 gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
329 if (filenames) {
330 for (GSList* iter = filenames; iter != nullptr;
331 iter = g_slist_next(iter)) {
332 std::string path(static_cast<char*>(iter->data));
333 g_free(iter->data);
334 files.push_back(path);
335 }
336 g_slist_free(filenames);
337 success = true;
338 }
339 }
340 }
341
342 int filter_index = params.selected_accept_filter;
343 if (success) {
344 GtkFileFilter* selected_filter =
345 gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(dialog));
346 if (selected_filter != nullptr) {
347 for (size_t x = 0; x < filters.size(); ++x) {
348 if (filters[x] == selected_filter) {
349 filter_index = x;
350 break;
351 }
352 }
353 }
354 }
355
356 gtk_widget_destroy(dialog);
357
358 if (success)
359 params.callback->Continue(filter_index, files);
360 else
361 params.callback->Cancel();
362 }
363
OnJSDialogContinue(OnJSDialogParams params,GtkWindow * window)364 void ClientDialogHandlerGtk::OnJSDialogContinue(OnJSDialogParams params,
365 GtkWindow* window) {
366 REQUIRE_MAIN_THREAD();
367
368 ScopedGdkThreadsEnter scoped_gdk_threads;
369
370 GtkButtonsType buttons = GTK_BUTTONS_NONE;
371 GtkMessageType gtk_message_type = GTK_MESSAGE_OTHER;
372 std::string title;
373
374 switch (params.dialog_type) {
375 case JSDIALOGTYPE_ALERT:
376 buttons = GTK_BUTTONS_NONE;
377 gtk_message_type = GTK_MESSAGE_WARNING;
378 title = "JavaScript Alert";
379 break;
380
381 case JSDIALOGTYPE_CONFIRM:
382 buttons = GTK_BUTTONS_CANCEL;
383 gtk_message_type = GTK_MESSAGE_QUESTION;
384 title = "JavaScript Confirm";
385 break;
386
387 case JSDIALOGTYPE_PROMPT:
388 buttons = GTK_BUTTONS_CANCEL;
389 gtk_message_type = GTK_MESSAGE_QUESTION;
390 title = "JavaScript Prompt";
391 break;
392 }
393
394 js_dialog_callback_ = params.callback;
395
396 if (!params.origin_url.empty()) {
397 title += " - ";
398 title += CefFormatUrlForSecurityDisplay(params.origin_url).ToString();
399 }
400
401 gtk_dialog_ = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL,
402 gtk_message_type, buttons, "%s",
403 params.message_text.ToString().c_str());
404 g_signal_connect(gtk_dialog_, "delete-event",
405 G_CALLBACK(gtk_widget_hide_on_delete), nullptr);
406
407 gtk_window_set_title(GTK_WINDOW(gtk_dialog_), title.c_str());
408
409 GtkWidget* ok_button =
410 gtk_dialog_add_button(GTK_DIALOG(gtk_dialog_), "_OK", GTK_RESPONSE_OK);
411
412 if (params.dialog_type != JSDIALOGTYPE_PROMPT)
413 gtk_widget_grab_focus(ok_button);
414
415 if (params.dialog_type == JSDIALOGTYPE_PROMPT) {
416 GtkWidget* content_area =
417 gtk_dialog_get_content_area(GTK_DIALOG(gtk_dialog_));
418 GtkWidget* text_box = gtk_entry_new();
419 gtk_entry_set_text(GTK_ENTRY(text_box),
420 params.default_prompt_text.ToString().c_str());
421 gtk_box_pack_start(GTK_BOX(content_area), text_box, TRUE, TRUE, 0);
422 g_object_set_data(G_OBJECT(gtk_dialog_), kPromptTextId, text_box);
423 gtk_entry_set_activates_default(GTK_ENTRY(text_box), TRUE);
424 }
425
426 gtk_dialog_set_default_response(GTK_DIALOG(gtk_dialog_), GTK_RESPONSE_OK);
427 g_signal_connect(gtk_dialog_, "response", G_CALLBACK(OnDialogResponse), this);
428 gtk_widget_show_all(GTK_WIDGET(gtk_dialog_));
429 }
430
GetWindowAndContinue(CefRefPtr<CefBrowser> browser,base::OnceCallback<void (GtkWindow *)> callback)431 void ClientDialogHandlerGtk::GetWindowAndContinue(
432 CefRefPtr<CefBrowser> browser,
433 base::OnceCallback<void(GtkWindow*)> callback) {
434 if (!CURRENTLY_ON_MAIN_THREAD()) {
435 MAIN_POST_CLOSURE(
436 base::BindOnce(&ClientDialogHandlerGtk::GetWindowAndContinue, this,
437 browser, std::move(callback)));
438 return;
439 }
440
441 GtkWindow* window = GetWindow(browser);
442 if (window) {
443 std::move(callback).Run(window);
444 }
445 }
446
447 // static
OnDialogResponse(GtkDialog * dialog,gint response_id,ClientDialogHandlerGtk * handler)448 void ClientDialogHandlerGtk::OnDialogResponse(GtkDialog* dialog,
449 gint response_id,
450 ClientDialogHandlerGtk* handler) {
451 REQUIRE_MAIN_THREAD();
452
453 DCHECK_EQ(dialog, GTK_DIALOG(handler->gtk_dialog_));
454 switch (response_id) {
455 case GTK_RESPONSE_OK:
456 handler->js_dialog_callback_->Continue(true, GetPromptText(dialog));
457 break;
458 case GTK_RESPONSE_CANCEL:
459 case GTK_RESPONSE_DELETE_EVENT:
460 handler->js_dialog_callback_->Continue(false, CefString());
461 break;
462 default:
463 NOTREACHED();
464 }
465
466 CefPostTask(TID_UI,
467 base::BindOnce(&ClientDialogHandlerGtk::OnResetDialogState,
468 handler, nullptr));
469 }
470
471 } // namespace client
472