From 72d85e90ace86475a11be3b7889e29c45d51d8e0 Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 11:31:48 +0200 Subject: [PATCH] refactor: use `//ui/shell_dialogs` on Linux (#42109) --- BUILD.gn | 15 +- filenames.gni | 2 +- patches/chromium/.patches | 1 + ...ing_dialog_features_to_shell_dialogs.patch | 243 +++++++++++ .../chromium/make_gtk_getlibgtk_public.patch | 36 +- shell/browser/electron_browser_main_parts.cc | 6 - shell/browser/ui/file_dialog_gtk.cc | 395 ------------------ shell/browser/ui/file_dialog_linux.cc | 258 ++++++++++++ 8 files changed, 514 insertions(+), 442 deletions(-) create mode 100644 patches/chromium/feat_add_support_for_missing_dialog_features_to_shell_dialogs.patch delete mode 100644 shell/browser/ui/file_dialog_gtk.cc create mode 100644 shell/browser/ui/file_dialog_linux.cc diff --git a/BUILD.gn b/BUILD.gn index 8873185b6aa9..5cfa6d959d5a 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -81,18 +81,11 @@ if (is_linux) { ] } - # Generates electron_gtk_stubs.h header which contains - # stubs for extracting function ptrs from the gtk library. - # Function signatures for which stubs are required should be - # declared in electron_gtk.sigs, currently this file contains - # signatures for the functions used with native file chooser - # implementation. In future, this file can be extended to contain - # gtk4 stubs to switch gtk version in runtime. + # Generates headers which contain stubs for extracting function ptrs + # from the gtk library. Function signatures for which stubs are + # required should be declared in the sig files. generate_stubs("electron_gtk_stubs") { - sigs = [ - "shell/browser/ui/electron_gdk_pixbuf.sigs", - "shell/browser/ui/electron_gtk.sigs", - ] + sigs = [ "shell/browser/ui/electron_gdk_pixbuf.sigs" ] extra_header = "shell/browser/ui/electron_gtk.fragment" output_name = "electron_gtk_stubs" public_deps = [ "//ui/gtk:gtk_config" ] diff --git a/filenames.gni b/filenames.gni index e25ed8ea4899..a8fce610abac 100644 --- a/filenames.gni +++ b/filenames.gni @@ -34,7 +34,7 @@ filenames = { "shell/browser/notifications/linux/notification_presenter_linux.h", "shell/browser/relauncher_linux.cc", "shell/browser/ui/electron_desktop_window_tree_host_linux.cc", - "shell/browser/ui/file_dialog_gtk.cc", + "shell/browser/ui/file_dialog_linux.cc", "shell/browser/ui/gtk/menu_gtk.cc", "shell/browser/ui/gtk/menu_gtk.h", "shell/browser/ui/gtk/menu_util.cc", diff --git a/patches/chromium/.patches b/patches/chromium/.patches index a4883d404985..ecf9f6fa92d1 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -129,3 +129,4 @@ fix_add_support_for_skipping_first_2_no-op_refreshes_in_thumb_cap.patch refactor_expose_file_system_access_blocklist.patch revert_power_update_trace_counter_in_power_monitor.patch cherry-pick-b2cc7b7ac538.patch +feat_add_support_for_missing_dialog_features_to_shell_dialogs.patch diff --git a/patches/chromium/feat_add_support_for_missing_dialog_features_to_shell_dialogs.patch b/patches/chromium/feat_add_support_for_missing_dialog_features_to_shell_dialogs.patch new file mode 100644 index 000000000000..860e930fe1fe --- /dev/null +++ b/patches/chromium/feat_add_support_for_missing_dialog_features_to_shell_dialogs.patch @@ -0,0 +1,243 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shelley Vohr +Date: Sun, 5 May 2024 09:17:17 +0000 +Subject: feat: add support for missing dialog features to //shell_dialogs + +This CL adds support for the following features to //shell_dialogs: +* buttonLabel - Custom label for the confirmation button. +* showHiddenFiles - Show hidden files in dialog. +* showOverwriteConfirmation - Whether the user will be presented a confirmation dialog if the user types a file name that already exists. + +This may be partially upstreamed to Chromium in the future. + +diff --git a/ui/gtk/select_file_dialog_linux_gtk.cc b/ui/gtk/select_file_dialog_linux_gtk.cc +index 698f97b23f851c02af037880585c82d4494da63f..0a3b701a701289a2124214e7421a6383ff7f0320 100644 +--- a/ui/gtk/select_file_dialog_linux_gtk.cc ++++ b/ui/gtk/select_file_dialog_linux_gtk.cc +@@ -243,6 +243,10 @@ void SelectFileDialogLinuxGtk::SelectFileImpl( + + std::string title_string = base::UTF16ToUTF8(title); + ++ ExtraSettings extra_settings; ++ if (params) ++ extra_settings = *(static_cast(params)); ++ + set_file_type_index(file_type_index); + if (file_types) + set_file_types(*file_types); +@@ -261,23 +265,23 @@ void SelectFileDialogLinuxGtk::SelectFileImpl( + case SELECT_UPLOAD_FOLDER: + case SELECT_EXISTING_FOLDER: + dialog = CreateSelectFolderDialog(type, title_string, default_path, +- owning_window); ++ owning_window, extra_settings); + connect("response", + &SelectFileDialogLinuxGtk::OnSelectSingleFolderDialogResponse); + break; + case SELECT_OPEN_FILE: +- dialog = CreateFileOpenDialog(title_string, default_path, owning_window); ++ dialog = CreateFileOpenDialog(title_string, default_path, owning_window, extra_settings); + connect("response", + &SelectFileDialogLinuxGtk::OnSelectSingleFileDialogResponse); + break; + case SELECT_OPEN_MULTI_FILE: + dialog = +- CreateMultiFileOpenDialog(title_string, default_path, owning_window); ++ CreateMultiFileOpenDialog(title_string, default_path, owning_window, extra_settings); + connect("response", + &SelectFileDialogLinuxGtk::OnSelectMultiFileDialogResponse); + break; + case SELECT_SAVEAS_FILE: +- dialog = CreateSaveAsDialog(title_string, default_path, owning_window); ++ dialog = CreateSaveAsDialog(title_string, default_path, owning_window, extra_settings); + connect("response", + &SelectFileDialogLinuxGtk::OnSelectSingleFileDialogResponse); + break; +@@ -412,10 +416,14 @@ void SelectFileDialogLinuxGtk::FileNotSelected(GtkWidget* dialog) { + GtkWidget* SelectFileDialogLinuxGtk::CreateFileOpenHelper( + const std::string& title, + const base::FilePath& default_path, +- gfx::NativeWindow parent) { ++ gfx::NativeWindow parent, ++ const ExtraSettings& settings) { ++ const char* button_label = settings.button_label.empty() ++ ? GetOpenLabel() ++ : settings.button_label.c_str(); + GtkWidget* dialog = GtkFileChooserDialogNew( + title.c_str(), nullptr, GTK_FILE_CHOOSER_ACTION_OPEN, GetCancelLabel(), +- GTK_RESPONSE_CANCEL, GetOpenLabel(), GTK_RESPONSE_ACCEPT); ++ GTK_RESPONSE_CANCEL, button_label, GTK_RESPONSE_ACCEPT); + SetGtkTransientForAura(dialog, parent); + AddFilters(GTK_FILE_CHOOSER(dialog)); + +@@ -431,6 +439,8 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateFileOpenHelper( + GtkFileChooserSetCurrentFolder(GTK_FILE_CHOOSER(dialog), + *last_opened_path()); + } ++ gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog), ++ settings.show_hidden); + return dialog; + } + +@@ -438,7 +448,8 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSelectFolderDialog( + Type type, + const std::string& title, + const base::FilePath& default_path, +- gfx::NativeWindow parent) { ++ gfx::NativeWindow parent, ++ const ExtraSettings& settings) { + std::string title_string = title; + if (title_string.empty()) { + title_string = +@@ -446,11 +457,14 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSelectFolderDialog( + ? l10n_util::GetStringUTF8(IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE) + : l10n_util::GetStringUTF8(IDS_SELECT_FOLDER_DIALOG_TITLE); + } +- std::string accept_button_label = +- (type == SELECT_UPLOAD_FOLDER) +- ? l10n_util::GetStringUTF8( +- IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON) +- : GetOpenLabel(); ++ ++ std::string accept_button_label = settings.button_label; ++ if (accept_button_label.empty()) { ++ accept_button_label = (type == SELECT_UPLOAD_FOLDER) ++ ? l10n_util::GetStringUTF8( ++ IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON) ++ : GetOpenLabel(); ++ } + + GtkWidget* dialog = GtkFileChooserDialogNew( + title_string.c_str(), nullptr, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, +@@ -472,19 +486,21 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSelectFolderDialog( + gtk_file_filter_add_mime_type(only_folders, "inode/directory"); + gtk_file_filter_add_mime_type(only_folders, "text/directory"); + gtk_file_chooser_add_filter(chooser, only_folders); +- gtk_file_chooser_set_select_multiple(chooser, FALSE); ++ gtk_file_chooser_set_select_multiple(chooser, settings.allow_multiple_selection); ++ gtk_file_chooser_set_show_hidden(chooser, settings.show_hidden); + return dialog; + } + + GtkWidget* SelectFileDialogLinuxGtk::CreateFileOpenDialog( + const std::string& title, + const base::FilePath& default_path, +- gfx::NativeWindow parent) { ++ gfx::NativeWindow parent, ++ const ExtraSettings& settings) { + std::string title_string = + !title.empty() ? title + : l10n_util::GetStringUTF8(IDS_OPEN_FILE_DIALOG_TITLE); + +- GtkWidget* dialog = CreateFileOpenHelper(title_string, default_path, parent); ++ GtkWidget* dialog = CreateFileOpenHelper(title_string, default_path, parent, settings); + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE); + return dialog; + } +@@ -492,12 +508,14 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateFileOpenDialog( + GtkWidget* SelectFileDialogLinuxGtk::CreateMultiFileOpenDialog( + const std::string& title, + const base::FilePath& default_path, +- gfx::NativeWindow parent) { ++ gfx::NativeWindow parent, ++ const ExtraSettings& settings) { + std::string title_string = + !title.empty() ? title + : l10n_util::GetStringUTF8(IDS_OPEN_FILES_DIALOG_TITLE); + +- GtkWidget* dialog = CreateFileOpenHelper(title_string, default_path, parent); ++ GtkWidget* dialog = ++ CreateFileOpenHelper(title_string, default_path, parent, settings); + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE); + return dialog; + } +@@ -505,14 +523,17 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateMultiFileOpenDialog( + GtkWidget* SelectFileDialogLinuxGtk::CreateSaveAsDialog( + const std::string& title, + const base::FilePath& default_path, +- gfx::NativeWindow parent) { ++ gfx::NativeWindow parent, ++ const ExtraSettings& settings) { + std::string title_string = + !title.empty() ? title + : l10n_util::GetStringUTF8(IDS_SAVE_AS_DIALOG_TITLE); +- ++ const char* button_label = settings.button_label.empty() ++ ? GetSaveLabel() ++ : settings.button_label.c_str(); + GtkWidget* dialog = GtkFileChooserDialogNew( + title_string.c_str(), nullptr, GTK_FILE_CHOOSER_ACTION_SAVE, +- GetCancelLabel(), GTK_RESPONSE_CANCEL, GetSaveLabel(), ++ GetCancelLabel(), GTK_RESPONSE_CANCEL, button_label, + GTK_RESPONSE_ACCEPT); + SetGtkTransientForAura(dialog, parent); + +@@ -538,9 +559,10 @@ GtkWidget* SelectFileDialogLinuxGtk::CreateSaveAsDialog( + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE); + // Overwrite confirmation is always enabled in GTK4. + if (!GtkCheckVersion(4)) { +- gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), +- TRUE); ++ gtk_file_chooser_set_do_overwrite_confirmation( ++ GTK_FILE_CHOOSER(dialog), settings.show_overwrite_confirmation); + } ++ gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog), settings.show_hidden); + return dialog; + } + +diff --git a/ui/gtk/select_file_dialog_linux_gtk.h b/ui/gtk/select_file_dialog_linux_gtk.h +index 53ae15f14c45ee72abdae172fc4555c9e4b3ff9a..af181afd9db1351cd886ba24dd651c7bf2f8a716 100644 +--- a/ui/gtk/select_file_dialog_linux_gtk.h ++++ b/ui/gtk/select_file_dialog_linux_gtk.h +@@ -15,6 +15,13 @@ + + namespace gtk { + ++struct ExtraSettings { ++ std::string button_label; ++ bool show_overwrite_confirmation = true; ++ bool show_hidden = false; ++ bool allow_multiple_selection = false; ++}; ++ + // Implementation of SelectFileDialog that shows a Gtk common dialog for + // choosing a file or folder. This acts as a modal dialog. + class SelectFileDialogLinuxGtk : public ui::SelectFileDialogLinux, +@@ -90,19 +97,23 @@ class SelectFileDialogLinuxGtk : public ui::SelectFileDialogLinux, + GtkWidget* CreateSelectFolderDialog(Type type, + const std::string& title, + const base::FilePath& default_path, +- gfx::NativeWindow parent); ++ gfx::NativeWindow parent, ++ const ExtraSettings& settings); + + GtkWidget* CreateFileOpenDialog(const std::string& title, + const base::FilePath& default_path, +- gfx::NativeWindow parent); ++ gfx::NativeWindow parent, ++ const ExtraSettings& settings); + + GtkWidget* CreateMultiFileOpenDialog(const std::string& title, + const base::FilePath& default_path, +- gfx::NativeWindow parent); ++ gfx::NativeWindow parent, ++ const ExtraSettings& settings); + + GtkWidget* CreateSaveAsDialog(const std::string& title, + const base::FilePath& default_path, +- gfx::NativeWindow parent); ++ gfx::NativeWindow parent, ++ const ExtraSettings& settings); + + // Removes and returns the |params| associated with |dialog| from + // |params_map_|. +@@ -121,7 +132,8 @@ class SelectFileDialogLinuxGtk : public ui::SelectFileDialogLinux, + // Common function for CreateFileOpenDialog and CreateMultiFileOpenDialog. + GtkWidget* CreateFileOpenHelper(const std::string& title, + const base::FilePath& default_path, +- gfx::NativeWindow parent); ++ gfx::NativeWindow parent, ++ const ExtraSettings& settings); + + // Callback for when the user responds to a Save As or Open File dialog. + void OnSelectSingleFileDialogResponse(GtkWidget* dialog, int response_id); diff --git a/patches/chromium/make_gtk_getlibgtk_public.patch b/patches/chromium/make_gtk_getlibgtk_public.patch index 3bd5bad15391..a63d5783f1ed 100644 --- a/patches/chromium/make_gtk_getlibgtk_public.patch +++ b/patches/chromium/make_gtk_getlibgtk_public.patch @@ -1,13 +1,13 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Thu, 7 Apr 2022 20:30:16 +0900 -Subject: Make gtk::GetLibGtk and gtk::GetLibGdkPixbuf public +Subject: Make gtk::GetLibGdkPixbuf public -Allows embedders to get a handle to the gtk and -gdk_pixbuf libraries already loaded in the process. +Allows embedders to get a handle to the gdk_pixbuf +library already loaded in the process. diff --git a/ui/gtk/gtk_compat.cc b/ui/gtk/gtk_compat.cc -index 3a4b856ec5c2f5c3ede6e8f6db7858498b737702..a308249c66c01856df84e64bda0411dbcbd96457 100644 +index 3a4b856ec5c2f5c3ede6e8f6db7858498b737702..e410998c4197c98947d2e1ad8bebe12c70379358 100644 --- a/ui/gtk/gtk_compat.cc +++ b/ui/gtk/gtk_compat.cc @@ -66,11 +66,6 @@ void* GetLibGio() { @@ -22,20 +22,7 @@ index 3a4b856ec5c2f5c3ede6e8f6db7858498b737702..a308249c66c01856df84e64bda0411db void* GetLibGdk3() { static void* libgdk3 = DlOpen("libgdk-3.so.0"); return libgdk3; -@@ -86,12 +81,6 @@ void* GetLibGtk4(bool check = true) { - return libgtk4; - } - --void* GetLibGtk() { -- if (GtkCheckVersion(4)) -- return GetLibGtk4(); -- return GetLibGtk3(); --} -- - bool LoadGtk3() { - if (!GetLibGtk3(false)) - return false; -@@ -134,6 +123,17 @@ gfx::Insets InsetsFromGtkBorder(const GtkBorder& border) { +@@ -134,6 +129,11 @@ gfx::Insets InsetsFromGtkBorder(const GtkBorder& border) { } // namespace @@ -43,29 +30,20 @@ index 3a4b856ec5c2f5c3ede6e8f6db7858498b737702..a308249c66c01856df84e64bda0411db + static void* libgdk_pixbuf = DlOpen("libgdk_pixbuf-2.0.so.0"); + return libgdk_pixbuf; +} -+ -+void* GetLibGtk() { -+ if (GtkCheckVersion(4)) -+ return GetLibGtk4(); -+ return GetLibGtk3(); -+} + bool LoadGtk() { static bool loaded = LoadGtkImpl(); return loaded; diff --git a/ui/gtk/gtk_compat.h b/ui/gtk/gtk_compat.h -index 19f73cc179d82a3729c5fe37883460ac05f4d0c3..cdb67c98291f145f89f76a50b31bf00318648518 100644 +index 19f73cc179d82a3729c5fe37883460ac05f4d0c3..17aa0b95bd6158ed02c03095c1687185a057fe62 100644 --- a/ui/gtk/gtk_compat.h +++ b/ui/gtk/gtk_compat.h -@@ -41,6 +41,12 @@ using SkColor = uint32_t; +@@ -41,6 +41,9 @@ using SkColor = uint32_t; namespace gtk { +// Get handle to the currently loaded gdk_pixbuf library in the process. +void* GetLibGdkPixbuf(); -+ -+// Get handle to the currently loaded gtk library in the process. -+void* GetLibGtk(); + // Loads libgtk and related libraries and returns true on success. bool LoadGtk(); diff --git a/shell/browser/electron_browser_main_parts.cc b/shell/browser/electron_browser_main_parts.cc index c843ecbbce05..842d730839b1 100644 --- a/shell/browser/electron_browser_main_parts.cc +++ b/shell/browser/electron_browser_main_parts.cc @@ -398,12 +398,6 @@ void ElectronBrowserMainParts::ToolkitInitialized() { CHECK(linux_ui); linux_ui_getter_ = std::make_unique(); - // Try loading gtk symbols used by Electron. - electron::InitializeElectron_gtk(gtk::GetLibGtk()); - if (!electron::IsElectron_gtkInitialized()) { - electron::UninitializeElectron_gtk(); - } - electron::InitializeElectron_gdk_pixbuf(gtk::GetLibGdkPixbuf()); CHECK(electron::IsElectron_gdk_pixbufInitialized()) << "Failed to initialize libgdk_pixbuf-2.0.so.0"; diff --git a/shell/browser/ui/file_dialog_gtk.cc b/shell/browser/ui/file_dialog_gtk.cc deleted file mode 100644 index ffe2e7a86f78..000000000000 --- a/shell/browser/ui/file_dialog_gtk.cc +++ /dev/null @@ -1,395 +0,0 @@ -// 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 -#include - -#include "base/files/file_util.h" -#include "base/functional/bind.h" -#include "base/functional/callback.h" -#include "base/memory/raw_ptr.h" -#include "base/memory/raw_ptr_exclusion.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/scoped_gsignal.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(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(), nullptr, 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(), nullptr, action, gtk_util::GetCancelLabel(), - GTK_RESPONSE_CANCEL, confirm_text, GTK_RESPONSE_ACCEPT, nullptr)); - } - - 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(); - signals_.emplace_back( - dialog_, "update-preview", - base::BindRepeating(&FileChooserDialog::OnUpdatePreview, - base::Unretained(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() { - signals_.emplace_back( - GTK_WIDGET(dialog_), "response", - base::BindRepeating(&FileChooserDialog::OnFileDialogResponse, - base::Unretained(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 promise) { - save_promise_ = - std::make_unique>( - std::move(promise)); - RunAsynchronous(); - } - - void RunOpenAsynchronous( - gin_helper::Promise promise) { - open_promise_ = - std::make_unique>( - 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 GetFileNames() const { - std::vector 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(iter->data); - paths.emplace_back(filename); - g_free(filename); - } - g_slist_free(filenames); - return paths; - } - - void OnFileDialogResponse(GtkWidget* widget, int response); - - GtkFileChooser* dialog() const { return dialog_; } - - private: - void AddFilters(const Filters& filters); - - raw_ptr parent_; - - RAW_PTR_EXCLUSION GtkFileChooser* dialog_; - RAW_PTR_EXCLUSION GtkWidget* preview_; - - Filters filters_; - std::unique_ptr> save_promise_; - std::unique_ptr> open_promise_; - - // Callback for when we update the preview for the selection. - void OnUpdatePreview(GtkFileChooser* chooser); - - std::vector signals_; -}; - -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_) { - auto dict = gin_helper::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_) { - auto dict = gin_helper::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()); - } - 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* 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 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 promise) { - FileChooserDialog* save_dialog = - new FileChooserDialog(GTK_FILE_CHOOSER_ACTION_SAVE, settings); - save_dialog->RunSaveAsynchronous(std::move(promise)); -} - -} // namespace file_dialog diff --git a/shell/browser/ui/file_dialog_linux.cc b/shell/browser/ui/file_dialog_linux.cc new file mode 100644 index 000000000000..336825a8660b --- /dev/null +++ b/shell/browser/ui/file_dialog_linux.cc @@ -0,0 +1,258 @@ +// Copyright (c) 2024 Microsoft, GmbH. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include +#include + +#include "base/files/file_util.h" +#include "base/functional/bind.h" +#include "base/functional/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/raw_ptr_exclusion.h" +#include "base/run_loop.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "shell/browser/javascript_environment.h" +#include "shell/browser/native_window_views.h" +#include "shell/browser/ui/file_dialog.h" +#include "shell/common/gin_converters/callback_converter.h" +#include "shell/common/gin_converters/file_path_converter.h" +#include "ui/gtk/select_file_dialog_linux_gtk.h" // nogncheck +#include "ui/shell_dialogs/select_file_dialog.h" +#include "ui/shell_dialogs/selected_file_info.h" + +namespace file_dialog { + +DialogSettings::DialogSettings() = default; +DialogSettings::DialogSettings(const DialogSettings&) = default; +DialogSettings::~DialogSettings() = default; + +namespace { + +ui::SelectFileDialog::Type GetDialogType(int properties) { + if (properties & OPEN_DIALOG_OPEN_DIRECTORY) + return ui::SelectFileDialog::SELECT_FOLDER; + + if (properties & OPEN_DIALOG_MULTI_SELECTIONS) + return ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE; + + return ui::SelectFileDialog::SELECT_OPEN_FILE; +} + +ui::SelectFileDialog::FileTypeInfo GetFilterInfo(const Filters& filters) { + ui::SelectFileDialog::FileTypeInfo file_type_info; + + for (const auto& [name, extension_group] : filters) { + file_type_info.extension_description_overrides.push_back( + base::UTF8ToUTF16(name)); + + const bool has_all_files_wildcard = base::ranges::any_of( + extension_group, [](const auto& ext) { return ext == "*"; }); + if (has_all_files_wildcard) { + file_type_info.include_all_files = true; + } else { + file_type_info.extensions.emplace_back(extension_group); + } + } + + return file_type_info; +} + +class FileChooserDialog : public ui::SelectFileDialog::Listener { + public: + enum class DialogType { OPEN, SAVE }; + + FileChooserDialog() { dialog_ = ui::SelectFileDialog::Create(this, nullptr); } + + ~FileChooserDialog() override = default; + + gtk::ExtraSettings GetExtraSettings(const DialogSettings& settings) { + gtk::ExtraSettings extra; + extra.button_label = settings.button_label; + extra.show_overwrite_confirmation = + settings.properties & SAVE_DIALOG_SHOW_OVERWRITE_CONFIRMATION; + extra.allow_multiple_selection = + settings.properties & OPEN_DIALOG_MULTI_SELECTIONS; + if (type_ == DialogType::SAVE) { + extra.show_hidden = settings.properties & SAVE_DIALOG_SHOW_HIDDEN_FILES; + } else { + extra.show_hidden = settings.properties & OPEN_DIALOG_SHOW_HIDDEN_FILES; + } + + return extra; + } + + void RunSaveDialogImpl(const DialogSettings& settings) { + type_ = DialogType::SAVE; + ui::SelectFileDialog::FileTypeInfo file_info = + GetFilterInfo(settings.filters); + auto extra_settings = GetExtraSettings(settings); + dialog_->SelectFile( + ui::SelectFileDialog::SELECT_SAVEAS_FILE, + base::UTF8ToUTF16(settings.title), settings.default_path, + &file_info /* file_types */, 0 /* file_type_index */, + base::FilePath::StringType() /* default_extension */, + settings.parent_window ? settings.parent_window->GetNativeWindow() + : nullptr, + static_cast(&extra_settings)); + } + + void RunSaveDialog(gin_helper::Promise promise, + const DialogSettings& settings) { + promise_ = std::move(promise); + RunSaveDialogImpl(settings); + } + + void RunSaveDialog(base::OnceCallback callback, + const DialogSettings& settings) { + callback_ = std::move(callback); + RunSaveDialogImpl(settings); + } + + void RunOpenDialogImpl(const DialogSettings& settings) { + type_ = DialogType::OPEN; + ui::SelectFileDialog::FileTypeInfo file_info = + GetFilterInfo(settings.filters); + auto extra_settings = GetExtraSettings(settings); + dialog_->SelectFile( + GetDialogType(settings.properties), base::UTF8ToUTF16(settings.title), + settings.default_path, &file_info, 0 /* file_type_index */, + base::FilePath::StringType() /* default_extension */, + settings.parent_window ? settings.parent_window->GetNativeWindow() + : nullptr, + static_cast(&extra_settings)); + } + + void RunOpenDialog(gin_helper::Promise promise, + const DialogSettings& settings) { + promise_ = std::move(promise); + RunOpenDialogImpl(settings); + } + + void RunOpenDialog(base::OnceCallback callback, + const DialogSettings& settings) { + callback_ = std::move(callback); + RunOpenDialogImpl(settings); + } + + void FileSelected(const ui::SelectedFileInfo& file, + int index, + void* params) override { + v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); + v8::HandleScope scope(isolate); + auto dict = gin_helper::Dictionary::CreateEmpty(isolate); + dict.Set("canceled", false); + if (type_ == DialogType::SAVE) { + dict.Set("filePath", file.file_path); + } else { + dict.Set("filePaths", std::vector{file.file_path}); + } + + if (callback_) { + std::move(callback_).Run(dict); + } else { + promise_.Resolve(dict); + } + + delete this; + } + + void MultiFilesSelected(const std::vector& files, + void* params) override { + v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); + v8::HandleScope scope(isolate); + auto dict = gin_helper::Dictionary::CreateEmpty(isolate); + dict.Set("canceled", false); + dict.Set("filePaths", ui::SelectedFileInfoListToFilePathList(files)); + + if (callback_) { + std::move(callback_).Run(dict); + } else { + promise_.Resolve(dict); + } + + delete this; + } + + void FileSelectionCanceled(void* params) override { + v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); + v8::HandleScope scope(isolate); + auto dict = gin_helper::Dictionary::CreateEmpty(isolate); + dict.Set("canceled", true); + if (type_ == DialogType::SAVE) { + dict.Set("filePath", base::FilePath()); + } else { + dict.Set("filePaths", std::vector()); + } + + if (callback_) { + std::move(callback_).Run(dict); + } else { + promise_.Resolve(dict); + } + + delete this; + } + + private: + DialogType type_; + scoped_refptr dialog_; + base::OnceCallback callback_; + gin_helper::Promise promise_; +}; + +} // namespace + +bool ShowOpenDialogSync(const DialogSettings& settings, + std::vector* paths) { + v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); + gin_helper::Promise promise(isolate); + + base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed); + auto cb = base::BindOnce( + [](base::RepeatingClosure cb, std::vector* file_paths, + gin_helper::Dictionary result) { + result.Get("filePaths", file_paths); + std::move(cb).Run(); + }, + run_loop.QuitClosure(), paths); + + FileChooserDialog* dialog = new FileChooserDialog(); + dialog->RunOpenDialog(std::move(cb), settings); + + run_loop.Run(); + return !paths->empty(); +} + +void ShowOpenDialog(const DialogSettings& settings, + gin_helper::Promise promise) { + FileChooserDialog* dialog = new FileChooserDialog(); + dialog->RunOpenDialog(std::move(promise), settings); +} + +bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) { + base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed); + v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); + gin_helper::Promise promise(isolate); + auto cb = base::BindOnce( + [](base::RepeatingClosure cb, base::FilePath* file_path, + gin_helper::Dictionary result) { + result.Get("filePath", file_path); + std::move(cb).Run(); + }, + run_loop.QuitClosure(), path); + + FileChooserDialog* dialog = new FileChooserDialog(); + dialog->RunSaveDialog(std::move(promise), settings); + run_loop.Run(); + return !path->empty(); +} + +void ShowSaveDialog(const DialogSettings& settings, + gin_helper::Promise promise) { + FileChooserDialog* dialog = new FileChooserDialog(); + dialog->RunSaveDialog(std::move(promise), settings); +} + +} // namespace file_dialog