123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- // Copyright (c) 2014 GitHub, Inc.
- // Use of this source code is governed by the MIT license that can be
- // found in the LICENSE file.
- #include <memory>
- #include <string>
- #include "base/files/file_util.h"
- #include "base/functional/callback.h"
- #include "base/strings/string_util.h"
- #include "electron/electron_gtk_stubs.h"
- #include "shell/browser/javascript_environment.h"
- #include "shell/browser/native_window_views.h"
- #include "shell/browser/ui/file_dialog.h"
- #include "shell/browser/ui/gtk_util.h"
- #include "shell/common/gin_converters/file_path_converter.h"
- #include "shell/common/thread_restrictions.h"
- #include "ui/base/glib/glib_signal.h"
- #include "ui/gtk/gtk_ui.h" // nogncheck
- #include "ui/gtk/gtk_util.h" // nogncheck
- namespace file_dialog {
- DialogSettings::DialogSettings() = default;
- DialogSettings::DialogSettings(const DialogSettings&) = default;
- DialogSettings::~DialogSettings() = default;
- namespace {
- static const int kPreviewWidth = 256;
- static const int kPreviewHeight = 512;
- std::string MakeCaseInsensitivePattern(const std::string& extension) {
- // If the extension is the "all files" extension, no change needed.
- if (extension == "*")
- return extension;
- std::string pattern("*.");
- for (char ch : extension) {
- if (!base::IsAsciiAlpha(ch)) {
- pattern.push_back(ch);
- continue;
- }
- pattern.push_back('[');
- pattern.push_back(base::ToLowerASCII(ch));
- pattern.push_back(base::ToUpperASCII(ch));
- pattern.push_back(']');
- }
- return pattern;
- }
- class FileChooserDialog {
- public:
- FileChooserDialog(GtkFileChooserAction action, const DialogSettings& settings)
- : parent_(
- static_cast<electron::NativeWindowViews*>(settings.parent_window)),
- filters_(settings.filters) {
- auto label = settings.button_label;
- if (electron::IsElectron_gtkInitialized()) {
- dialog_ = GTK_FILE_CHOOSER(gtk_file_chooser_native_new(
- settings.title.c_str(), NULL, action,
- label.empty() ? nullptr : label.c_str(), nullptr));
- } else {
- const char* confirm_text = gtk_util::GetOkLabel();
- if (!label.empty())
- confirm_text = label.c_str();
- else if (action == GTK_FILE_CHOOSER_ACTION_SAVE)
- confirm_text = gtk_util::GetSaveLabel();
- else if (action == GTK_FILE_CHOOSER_ACTION_OPEN)
- confirm_text = gtk_util::GetOpenLabel();
- dialog_ = GTK_FILE_CHOOSER(gtk_file_chooser_dialog_new(
- settings.title.c_str(), NULL, action, gtk_util::GetCancelLabel(),
- GTK_RESPONSE_CANCEL, confirm_text, GTK_RESPONSE_ACCEPT, NULL));
- }
- if (parent_) {
- parent_->SetEnabled(false);
- if (electron::IsElectron_gtkInitialized()) {
- gtk_native_dialog_set_modal(GTK_NATIVE_DIALOG(dialog_), TRUE);
- } else {
- gtk::SetGtkTransientForAura(GTK_WIDGET(dialog_),
- parent_->GetNativeWindow());
- gtk_window_set_modal(GTK_WINDOW(dialog_), TRUE);
- }
- }
- if (action == GTK_FILE_CHOOSER_ACTION_SAVE)
- gtk_file_chooser_set_do_overwrite_confirmation(dialog_, TRUE);
- if (action != GTK_FILE_CHOOSER_ACTION_OPEN)
- gtk_file_chooser_set_create_folders(dialog_, TRUE);
- if (!settings.default_path.empty()) {
- electron::ScopedAllowBlockingForElectron allow_blocking;
- if (base::DirectoryExists(settings.default_path)) {
- gtk_file_chooser_set_current_folder(
- dialog_, settings.default_path.value().c_str());
- } else {
- if (settings.default_path.IsAbsolute()) {
- gtk_file_chooser_set_current_folder(
- dialog_, settings.default_path.DirName().value().c_str());
- }
- gtk_file_chooser_set_current_name(
- GTK_FILE_CHOOSER(dialog_),
- settings.default_path.BaseName().value().c_str());
- }
- }
- if (!settings.filters.empty())
- AddFilters(settings.filters);
- // GtkFileChooserNative does not support preview widgets through the
- // org.freedesktop.portal.FileChooser portal. In the case of running through
- // the org.freedesktop.portal.FileChooser portal, anything having to do with
- // the update-preview signal or the preview widget will just be ignored.
- if (!electron::IsElectron_gtkInitialized()) {
- preview_ = gtk_image_new();
- g_signal_connect(dialog_, "update-preview",
- G_CALLBACK(OnUpdatePreviewThunk), this);
- gtk_file_chooser_set_preview_widget(dialog_, preview_);
- }
- }
- ~FileChooserDialog() {
- if (electron::IsElectron_gtkInitialized()) {
- gtk_native_dialog_destroy(GTK_NATIVE_DIALOG(dialog_));
- } else {
- gtk_widget_destroy(GTK_WIDGET(dialog_));
- }
- if (parent_)
- parent_->SetEnabled(true);
- }
- // disable copy
- FileChooserDialog(const FileChooserDialog&) = delete;
- FileChooserDialog& operator=(const FileChooserDialog&) = delete;
- void SetupOpenProperties(int properties) {
- const auto hasProp = [properties](OpenFileDialogProperty prop) {
- return gboolean((properties & prop) != 0);
- };
- auto* file_chooser = dialog();
- gtk_file_chooser_set_select_multiple(file_chooser,
- hasProp(OPEN_DIALOG_MULTI_SELECTIONS));
- gtk_file_chooser_set_show_hidden(file_chooser,
- hasProp(OPEN_DIALOG_SHOW_HIDDEN_FILES));
- }
- void SetupSaveProperties(int properties) {
- const auto hasProp = [properties](SaveFileDialogProperty prop) {
- return gboolean((properties & prop) != 0);
- };
- auto* file_chooser = dialog();
- gtk_file_chooser_set_show_hidden(file_chooser,
- hasProp(SAVE_DIALOG_SHOW_HIDDEN_FILES));
- gtk_file_chooser_set_do_overwrite_confirmation(
- file_chooser, hasProp(SAVE_DIALOG_SHOW_OVERWRITE_CONFIRMATION));
- }
- void RunAsynchronous() {
- g_signal_connect(dialog_, "response", G_CALLBACK(OnFileDialogResponseThunk),
- this);
- if (electron::IsElectron_gtkInitialized()) {
- gtk_native_dialog_show(GTK_NATIVE_DIALOG(dialog_));
- } else {
- gtk_widget_show_all(GTK_WIDGET(dialog_));
- gtk::GtkUi::GetPlatform()->ShowGtkWindow(GTK_WINDOW(dialog_));
- }
- }
- void RunSaveAsynchronous(
- gin_helper::Promise<gin_helper::Dictionary> promise) {
- save_promise_ =
- std::make_unique<gin_helper::Promise<gin_helper::Dictionary>>(
- std::move(promise));
- RunAsynchronous();
- }
- void RunOpenAsynchronous(
- gin_helper::Promise<gin_helper::Dictionary> promise) {
- open_promise_ =
- std::make_unique<gin_helper::Promise<gin_helper::Dictionary>>(
- std::move(promise));
- RunAsynchronous();
- }
- base::FilePath GetFileName() const {
- gchar* filename = gtk_file_chooser_get_filename(dialog_);
- const base::FilePath path(filename);
- g_free(filename);
- return path;
- }
- std::vector<base::FilePath> GetFileNames() const {
- std::vector<base::FilePath> paths;
- auto* filenames = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog_));
- for (auto* iter = filenames; iter != nullptr; iter = iter->next) {
- auto* filename = static_cast<char*>(iter->data);
- paths.emplace_back(filename);
- g_free(filename);
- }
- g_slist_free(filenames);
- return paths;
- }
- CHROMEG_CALLBACK_1(FileChooserDialog,
- void,
- OnFileDialogResponse,
- GtkWidget*,
- int);
- GtkFileChooser* dialog() const { return dialog_; }
- private:
- void AddFilters(const Filters& filters);
- electron::NativeWindowViews* parent_;
- GtkFileChooser* dialog_;
- GtkWidget* preview_;
- Filters filters_;
- std::unique_ptr<gin_helper::Promise<gin_helper::Dictionary>> save_promise_;
- std::unique_ptr<gin_helper::Promise<gin_helper::Dictionary>> open_promise_;
- // Callback for when we update the preview for the selection.
- CHROMEG_CALLBACK_0(FileChooserDialog, void, OnUpdatePreview, GtkFileChooser*);
- };
- void FileChooserDialog::OnFileDialogResponse(GtkWidget* widget, int response) {
- if (electron::IsElectron_gtkInitialized()) {
- gtk_native_dialog_hide(GTK_NATIVE_DIALOG(dialog_));
- } else {
- gtk_widget_hide(GTK_WIDGET(dialog_));
- }
- v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
- v8::HandleScope scope(isolate);
- if (save_promise_) {
- gin_helper::Dictionary dict =
- gin::Dictionary::CreateEmpty(save_promise_->isolate());
- if (response == GTK_RESPONSE_ACCEPT) {
- dict.Set("canceled", false);
- dict.Set("filePath", GetFileName());
- } else {
- dict.Set("canceled", true);
- dict.Set("filePath", base::FilePath());
- }
- save_promise_->Resolve(dict);
- } else if (open_promise_) {
- gin_helper::Dictionary dict =
- gin::Dictionary::CreateEmpty(open_promise_->isolate());
- if (response == GTK_RESPONSE_ACCEPT) {
- dict.Set("canceled", false);
- dict.Set("filePaths", GetFileNames());
- } else {
- dict.Set("canceled", true);
- dict.Set("filePaths", std::vector<base::FilePath>());
- }
- open_promise_->Resolve(dict);
- }
- delete this;
- }
- void FileChooserDialog::AddFilters(const Filters& filters) {
- for (const auto& filter : filters) {
- GtkFileFilter* gtk_filter = gtk_file_filter_new();
- for (const auto& extension : filter.second) {
- std::string pattern = MakeCaseInsensitivePattern(extension);
- gtk_file_filter_add_pattern(gtk_filter, pattern.c_str());
- }
- gtk_file_filter_set_name(gtk_filter, filter.first.c_str());
- gtk_file_chooser_add_filter(dialog_, gtk_filter);
- }
- }
- bool CanPreview(const struct stat& st) {
- // Only preview regular files; pipes may hang.
- // See https://crbug.com/534754.
- if (!S_ISREG(st.st_mode)) {
- return false;
- }
- // Don't preview huge files; they may crash.
- // https://github.com/electron/electron/issues/31630
- // Setting an arbitrary filesize max t at 100 MB here.
- constexpr off_t ArbitraryMax = 100000000ULL;
- return st.st_size < ArbitraryMax;
- }
- void FileChooserDialog::OnUpdatePreview(GtkFileChooser* chooser) {
- CHECK(!electron::IsElectron_gtkInitialized());
- gchar* filename = gtk_file_chooser_get_preview_filename(chooser);
- if (!filename) {
- gtk_file_chooser_set_preview_widget_active(chooser, FALSE);
- return;
- }
- struct stat sb;
- if (stat(filename, &sb) != 0 || !CanPreview(sb)) {
- g_free(filename);
- gtk_file_chooser_set_preview_widget_active(chooser, FALSE);
- return;
- }
- // This will preserve the image's aspect ratio.
- GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file_at_size(filename, kPreviewWidth,
- kPreviewHeight, nullptr);
- g_free(filename);
- if (pixbuf) {
- gtk_image_set_from_pixbuf(GTK_IMAGE(preview_), pixbuf);
- g_object_unref(pixbuf);
- }
- gtk_file_chooser_set_preview_widget_active(chooser, pixbuf ? TRUE : FALSE);
- }
- } // namespace
- void ShowFileDialog(const FileChooserDialog& dialog) {
- // gtk_native_dialog_run() will call gtk_native_dialog_show() for us.
- if (!electron::IsElectron_gtkInitialized()) {
- gtk_widget_show_all(GTK_WIDGET(dialog.dialog()));
- }
- }
- int RunFileDialog(const FileChooserDialog& dialog) {
- int response = 0;
- if (electron::IsElectron_gtkInitialized()) {
- response = gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog.dialog()));
- } else {
- response = gtk_dialog_run(GTK_DIALOG(dialog.dialog()));
- }
- return response;
- }
- bool ShowOpenDialogSync(const DialogSettings& settings,
- std::vector<base::FilePath>* paths) {
- GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
- if (settings.properties & OPEN_DIALOG_OPEN_DIRECTORY)
- action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
- FileChooserDialog open_dialog(action, settings);
- open_dialog.SetupOpenProperties(settings.properties);
- ShowFileDialog(open_dialog);
- const int response = RunFileDialog(open_dialog);
- if (response == GTK_RESPONSE_ACCEPT) {
- *paths = open_dialog.GetFileNames();
- return true;
- }
- return false;
- }
- void ShowOpenDialog(const DialogSettings& settings,
- gin_helper::Promise<gin_helper::Dictionary> promise) {
- GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
- if (settings.properties & OPEN_DIALOG_OPEN_DIRECTORY)
- action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
- FileChooserDialog* open_dialog = new FileChooserDialog(action, settings);
- open_dialog->SetupOpenProperties(settings.properties);
- open_dialog->RunOpenAsynchronous(std::move(promise));
- }
- bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) {
- FileChooserDialog save_dialog(GTK_FILE_CHOOSER_ACTION_SAVE, settings);
- save_dialog.SetupSaveProperties(settings.properties);
- ShowFileDialog(save_dialog);
- const int response = RunFileDialog(save_dialog);
- if (response == GTK_RESPONSE_ACCEPT) {
- *path = save_dialog.GetFileName();
- return true;
- }
- return false;
- }
- void ShowSaveDialog(const DialogSettings& settings,
- gin_helper::Promise<gin_helper::Dictionary> promise) {
- FileChooserDialog* save_dialog =
- new FileChooserDialog(GTK_FILE_CHOOSER_ACTION_SAVE, settings);
- save_dialog->RunSaveAsynchronous(std::move(promise));
- }
- } // namespace file_dialog
|