Files
cef/tests/cefclient/browser/dialog_handler_gtk.cc
Marshall Greenblatt 20a659fa66 Pass mime type values as file dialog accept filters (see #3314)
File dialogs that specify mime type (e.g. "image/*") accept filters will pass
those values unchanged to the OnFileDialog |accept_filters| parameter. The
default dialog implementation will show those filters in addition to a combined
"Custom Files" filter. This is a change from preexisting Google Chrome
behavior where only the combined "Custom Files" filter is displayed, and
restores CEF behavior that existed prior to 2ea7459a89.

Document the fact that OnFileDialog may be called twice, once before MIME type
expansion and once afterwards.

Add new OnFileDialog |accept_extensions| and |accept_descriptions| parameters
for MIME type extensions and descriptions.

Details: This change adds a SelectFileDialog::FileTypeInfo::extension_mimetypes
member and improves the logic in FileSelectHelper::GetFileTypesFromAcceptType
and file_dialog_manager.cc SelectFileToFileChooserParams to support recall of
the source mime type when populating the FileChooserParams structure.

To test:
- Run `ceftests --gtest_filter=DialogTest.*`
- Run `cefclient --url=https://tests/dialogs`
2024-05-30 15:46:19 -04:00

477 lines
14 KiB
C++

// Copyright (c) 2015 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
#include "tests/cefclient/browser/dialog_handler_gtk.h"
#include <libgen.h>
#include <sys/stat.h>
#include "include/base/cef_logging.h"
#include "include/cef_browser.h"
#include "include/cef_parser.h"
#include "include/wrapper/cef_helpers.h"
#include "tests/cefclient/browser/root_window.h"
#include "tests/cefclient/browser/util_gtk.h"
#include "tests/shared/common/string_util.h"
namespace client {
namespace {
const char kPromptTextId[] = "cef_prompt_text";
// If there's a text entry in the dialog, get the text from the first one and
// return it.
std::string GetPromptText(GtkDialog* dialog) {
GtkWidget* widget = static_cast<GtkWidget*>(
g_object_get_data(G_OBJECT(dialog), kPromptTextId));
if (widget) {
return gtk_entry_get_text(GTK_ENTRY(widget));
}
return std::string();
}
std::string GetDescriptionFromMimeType(const std::string& mime_type) {
// Check for wild card mime types and return an appropriate description.
static const struct {
const char* mime_type;
const char* label;
} kWildCardMimeTypes[] = {
{"audio", "Audio Files"},
{"image", "Image Files"},
{"text", "Text Files"},
{"video", "Video Files"},
};
for (size_t i = 0;
i < sizeof(kWildCardMimeTypes) / sizeof(kWildCardMimeTypes[0]); ++i) {
if (mime_type == std::string(kWildCardMimeTypes[i].mime_type) + "/*") {
return std::string(kWildCardMimeTypes[i].label);
}
}
LOG(WARNING) << "Unrecognized mime type: " << mime_type;
return std::string();
}
void AddFilters(GtkFileChooser* chooser,
const std::vector<CefString>& accept_filters,
const std::vector<CefString>& accept_extensions,
const std::vector<CefString>& accept_descriptions,
bool include_all_files,
std::vector<GtkFileFilter*>* filters) {
bool has_filter = false;
for (size_t j = 0; j < accept_filters.size(); ++j) {
const std::string& filter = accept_filters[j];
if (filter.empty()) {
continue;
}
// Extensions and descriptions may be provided.
std::vector<std::string> extensions =
AsciiStrSplit(accept_extensions[j], ';');
std::string description = accept_descriptions[j];
if (extensions.empty()) {
// Try to convert mime type to one or more extensions.
std::vector<CefString> ext;
CefGetExtensionsForMimeType(filter, ext);
for (size_t x = 0; x < ext.size(); ++x) {
extensions.push_back("." + ext[x].ToString());
}
}
if (extensions.empty()) {
continue;
}
if (description.empty() && filter[0] != '.') {
description = GetDescriptionFromMimeType(filter);
}
GtkFileFilter* gtk_filter = gtk_file_filter_new();
std::string ext_str;
for (size_t x = 0; x < extensions.size(); ++x) {
const std::string& pattern = "*" + extensions[x];
if (x != 0) {
ext_str += ";";
}
ext_str += pattern;
gtk_file_filter_add_pattern(gtk_filter, pattern.c_str());
}
if (description.empty()) {
description = ext_str;
} else {
description += " (" + ext_str + ")";
}
gtk_file_filter_set_name(gtk_filter, description.c_str());
gtk_file_chooser_add_filter(chooser, gtk_filter);
if (!has_filter) {
has_filter = true;
}
filters->push_back(gtk_filter);
}
// Add the *.* filter, but only if we have added other filters (otherwise it
// is implied).
if (include_all_files && has_filter) {
GtkFileFilter* filter = gtk_file_filter_new();
gtk_file_filter_add_pattern(filter, "*");
gtk_file_filter_set_name(filter, "All Files (*)");
gtk_file_chooser_add_filter(chooser, filter);
}
}
GtkWindow* GetWindow(CefRefPtr<CefBrowser> browser) {
REQUIRE_MAIN_THREAD();
scoped_refptr<RootWindow> root_window =
RootWindow::GetForBrowser(browser->GetIdentifier());
if (root_window) {
GtkWidget* window = root_window->GetWindowHandle();
DCHECK(window);
if (!window) {
LOG(ERROR) << "No GtkWindow for browser";
}
return GTK_WINDOW(window);
}
return nullptr;
}
// Returns true if |accept_filters| contains a MIME type value without matching
// extensions in |accept_extensions|.
bool MissingMimeTypeData(const std::vector<CefString>& accept_filters,
const std::vector<CefString>& accept_extensions) {
if (accept_filters.empty()) {
return false;
}
for (size_t i = 0; i < accept_filters.size(); ++i) {
const std::string& filter = accept_filters[i];
if (filter[0] != '.' && accept_extensions[i].empty()) {
return true;
}
}
return false;
}
} // namespace
ClientDialogHandlerGtk::ClientDialogHandlerGtk() : gtk_dialog_(nullptr) {}
bool ClientDialogHandlerGtk::OnFileDialog(
CefRefPtr<CefBrowser> browser,
FileDialogMode mode,
const CefString& title,
const CefString& default_file_path,
const std::vector<CefString>& accept_filters,
const std::vector<CefString>& accept_extensions,
const std::vector<CefString>& accept_descriptions,
CefRefPtr<CefFileDialogCallback> callback) {
CEF_REQUIRE_UI_THREAD();
DCHECK(accept_filters.size() == accept_extensions.size() ==
accept_descriptions.size());
if (MissingMimeTypeData(accept_filters, accept_extensions)) {
// Wait for the 2nd call that provides MIME type data.
return false;
}
OnFileDialogParams params;
params.browser = browser;
params.mode = mode;
params.title = title;
params.default_file_path = default_file_path;
params.accept_filters = accept_filters;
params.accept_extensions = accept_extensions;
params.accept_descriptions = accept_descriptions;
params.callback = callback;
GetWindowAndContinue(
browser, base::BindOnce(&ClientDialogHandlerGtk::OnFileDialogContinue,
this, params));
return true;
}
bool ClientDialogHandlerGtk::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) {
CEF_REQUIRE_UI_THREAD();
OnJSDialogParams params;
params.browser = browser;
params.origin_url = origin_url;
params.dialog_type = dialog_type;
params.message_text = message_text;
params.default_prompt_text = default_prompt_text;
params.callback = callback;
GetWindowAndContinue(
browser, base::BindOnce(&ClientDialogHandlerGtk::OnJSDialogContinue, this,
params));
return true;
}
bool ClientDialogHandlerGtk::OnBeforeUnloadDialog(
CefRefPtr<CefBrowser> browser,
const CefString& message_text,
bool is_reload,
CefRefPtr<CefJSDialogCallback> callback) {
CEF_REQUIRE_UI_THREAD();
const std::string& new_message_text =
message_text.ToString() + "\n\nIs it OK to leave/reload this page?";
bool suppress_message = false;
return OnJSDialog(browser, CefString(), JSDIALOGTYPE_CONFIRM,
new_message_text, CefString(), callback, suppress_message);
}
void ClientDialogHandlerGtk::OnResetDialogState(CefRefPtr<CefBrowser> browser) {
CEF_REQUIRE_UI_THREAD();
if (!gtk_dialog_) {
return;
}
gtk_widget_destroy(gtk_dialog_);
gtk_dialog_ = nullptr;
js_dialog_callback_ = nullptr;
}
void ClientDialogHandlerGtk::OnFileDialogContinue(
const OnFileDialogParams& params,
GtkWindow* window) {
REQUIRE_MAIN_THREAD();
ScopedGdkThreadsEnter scoped_gdk_threads;
std::vector<CefString> files;
GtkFileChooserAction action;
const gchar* accept_button;
if (params.mode == FILE_DIALOG_OPEN ||
params.mode == FILE_DIALOG_OPEN_MULTIPLE) {
action = GTK_FILE_CHOOSER_ACTION_OPEN;
accept_button = "_Open";
} else if (params.mode == FILE_DIALOG_OPEN_FOLDER) {
action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
accept_button = "_Open";
} else if (params.mode == FILE_DIALOG_SAVE) {
action = GTK_FILE_CHOOSER_ACTION_SAVE;
accept_button = "_Save";
} else {
NOTREACHED();
params.callback->Cancel();
return;
}
std::string title_str;
if (!params.title.empty()) {
title_str = params.title;
} else {
switch (params.mode) {
case FILE_DIALOG_OPEN:
title_str = "Open File";
break;
case FILE_DIALOG_OPEN_MULTIPLE:
title_str = "Open Files";
break;
case FILE_DIALOG_OPEN_FOLDER:
title_str = "Open Folder";
break;
case FILE_DIALOG_SAVE:
title_str = "Save File";
break;
default:
break;
}
}
GtkWidget* dialog = gtk_file_chooser_dialog_new(
title_str.c_str(), GTK_WINDOW(window), action, "_Cancel",
GTK_RESPONSE_CANCEL, accept_button, GTK_RESPONSE_ACCEPT, nullptr);
if (params.mode == FILE_DIALOG_OPEN_MULTIPLE) {
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE);
}
if (!params.default_file_path.empty() && params.mode == FILE_DIALOG_SAVE) {
const std::string& file_path = params.default_file_path;
bool exists = false;
struct stat sb;
if (stat(file_path.c_str(), &sb) == 0 && S_ISREG(sb.st_mode)) {
// Use the directory and name of the existing file.
gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog), file_path.data());
exists = true;
}
if (!exists) {
// Set the current file name but let the user choose the directory.
std::string file_name_str = file_path;
const char* file_name = basename(const_cast<char*>(file_name_str.data()));
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), file_name);
}
}
std::vector<GtkFileFilter*> filters;
AddFilters(GTK_FILE_CHOOSER(dialog), params.accept_filters,
params.accept_extensions, params.accept_descriptions, true,
&filters);
bool success = false;
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
if (params.mode == FILE_DIALOG_OPEN ||
params.mode == FILE_DIALOG_OPEN_FOLDER ||
params.mode == FILE_DIALOG_SAVE) {
char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
files.push_back(std::string(filename));
success = true;
} else if (params.mode == FILE_DIALOG_OPEN_MULTIPLE) {
GSList* filenames =
gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
if (filenames) {
for (GSList* iter = filenames; iter != nullptr;
iter = g_slist_next(iter)) {
std::string path(static_cast<char*>(iter->data));
g_free(iter->data);
files.push_back(path);
}
g_slist_free(filenames);
success = true;
}
}
}
gtk_widget_destroy(dialog);
if (success) {
params.callback->Continue(files);
} else {
params.callback->Cancel();
}
}
void ClientDialogHandlerGtk::OnJSDialogContinue(const OnJSDialogParams& params,
GtkWindow* window) {
REQUIRE_MAIN_THREAD();
ScopedGdkThreadsEnter scoped_gdk_threads;
GtkButtonsType buttons = GTK_BUTTONS_NONE;
GtkMessageType gtk_message_type = GTK_MESSAGE_OTHER;
std::string title;
switch (params.dialog_type) {
case JSDIALOGTYPE_ALERT:
buttons = GTK_BUTTONS_NONE;
gtk_message_type = GTK_MESSAGE_WARNING;
title = "JavaScript Alert";
break;
case JSDIALOGTYPE_CONFIRM:
buttons = GTK_BUTTONS_CANCEL;
gtk_message_type = GTK_MESSAGE_QUESTION;
title = "JavaScript Confirm";
break;
case JSDIALOGTYPE_PROMPT:
buttons = GTK_BUTTONS_CANCEL;
gtk_message_type = GTK_MESSAGE_QUESTION;
title = "JavaScript Prompt";
break;
}
js_dialog_callback_ = params.callback;
if (!params.origin_url.empty()) {
title += " - ";
title += CefFormatUrlForSecurityDisplay(params.origin_url).ToString();
}
gtk_dialog_ = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL,
gtk_message_type, buttons, "%s",
params.message_text.ToString().c_str());
g_signal_connect(gtk_dialog_, "delete-event",
G_CALLBACK(gtk_widget_hide_on_delete), nullptr);
gtk_window_set_title(GTK_WINDOW(gtk_dialog_), title.c_str());
GtkWidget* ok_button =
gtk_dialog_add_button(GTK_DIALOG(gtk_dialog_), "_OK", GTK_RESPONSE_OK);
if (params.dialog_type != JSDIALOGTYPE_PROMPT) {
gtk_widget_grab_focus(ok_button);
}
if (params.dialog_type == JSDIALOGTYPE_PROMPT) {
GtkWidget* content_area =
gtk_dialog_get_content_area(GTK_DIALOG(gtk_dialog_));
GtkWidget* text_box = gtk_entry_new();
gtk_entry_set_text(GTK_ENTRY(text_box),
params.default_prompt_text.ToString().c_str());
gtk_box_pack_start(GTK_BOX(content_area), text_box, TRUE, TRUE, 0);
g_object_set_data(G_OBJECT(gtk_dialog_), kPromptTextId, text_box);
gtk_entry_set_activates_default(GTK_ENTRY(text_box), TRUE);
}
gtk_dialog_set_default_response(GTK_DIALOG(gtk_dialog_), GTK_RESPONSE_OK);
g_signal_connect(gtk_dialog_, "response", G_CALLBACK(OnDialogResponse), this);
gtk_widget_show_all(GTK_WIDGET(gtk_dialog_));
}
void ClientDialogHandlerGtk::GetWindowAndContinue(
CefRefPtr<CefBrowser> browser,
base::OnceCallback<void(GtkWindow*)> callback) {
if (!CURRENTLY_ON_MAIN_THREAD()) {
MAIN_POST_CLOSURE(
base::BindOnce(&ClientDialogHandlerGtk::GetWindowAndContinue, this,
browser, std::move(callback)));
return;
}
GtkWindow* window = GetWindow(browser);
if (window) {
std::move(callback).Run(window);
}
}
// static
void ClientDialogHandlerGtk::OnDialogResponse(GtkDialog* dialog,
gint response_id,
ClientDialogHandlerGtk* handler) {
REQUIRE_MAIN_THREAD();
DCHECK_EQ(dialog, GTK_DIALOG(handler->gtk_dialog_));
switch (response_id) {
case GTK_RESPONSE_OK:
handler->js_dialog_callback_->Continue(true, GetPromptText(dialog));
break;
case GTK_RESPONSE_CANCEL:
case GTK_RESPONSE_DELETE_EVENT:
handler->js_dialog_callback_->Continue(false, CefString());
break;
default:
NOTREACHED();
}
CefPostTask(TID_UI,
base::BindOnce(&ClientDialogHandlerGtk::OnResetDialogState,
handler, nullptr));
}
} // namespace client