file_dialog_gtk.cc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. // Copyright (c) 2014 GitHub, Inc.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include <memory>
  5. #include <string>
  6. #include "base/files/file_util.h"
  7. #include "base/functional/callback.h"
  8. #include "base/strings/string_util.h"
  9. #include "electron/electron_gtk_stubs.h"
  10. #include "shell/browser/javascript_environment.h"
  11. #include "shell/browser/native_window_views.h"
  12. #include "shell/browser/ui/file_dialog.h"
  13. #include "shell/browser/ui/gtk_util.h"
  14. #include "shell/common/gin_converters/file_path_converter.h"
  15. #include "shell/common/thread_restrictions.h"
  16. #include "ui/base/glib/glib_signal.h"
  17. #include "ui/gtk/gtk_ui.h" // nogncheck
  18. #include "ui/gtk/gtk_util.h" // nogncheck
  19. namespace file_dialog {
  20. DialogSettings::DialogSettings() = default;
  21. DialogSettings::DialogSettings(const DialogSettings&) = default;
  22. DialogSettings::~DialogSettings() = default;
  23. namespace {
  24. static const int kPreviewWidth = 256;
  25. static const int kPreviewHeight = 512;
  26. std::string MakeCaseInsensitivePattern(const std::string& extension) {
  27. // If the extension is the "all files" extension, no change needed.
  28. if (extension == "*")
  29. return extension;
  30. std::string pattern("*.");
  31. for (char ch : extension) {
  32. if (!base::IsAsciiAlpha(ch)) {
  33. pattern.push_back(ch);
  34. continue;
  35. }
  36. pattern.push_back('[');
  37. pattern.push_back(base::ToLowerASCII(ch));
  38. pattern.push_back(base::ToUpperASCII(ch));
  39. pattern.push_back(']');
  40. }
  41. return pattern;
  42. }
  43. class FileChooserDialog {
  44. public:
  45. FileChooserDialog(GtkFileChooserAction action, const DialogSettings& settings)
  46. : parent_(
  47. static_cast<electron::NativeWindowViews*>(settings.parent_window)),
  48. filters_(settings.filters) {
  49. auto label = settings.button_label;
  50. if (electron::IsElectron_gtkInitialized()) {
  51. dialog_ = GTK_FILE_CHOOSER(gtk_file_chooser_native_new(
  52. settings.title.c_str(), NULL, action,
  53. label.empty() ? nullptr : label.c_str(), nullptr));
  54. } else {
  55. const char* confirm_text = gtk_util::GetOkLabel();
  56. if (!label.empty())
  57. confirm_text = label.c_str();
  58. else if (action == GTK_FILE_CHOOSER_ACTION_SAVE)
  59. confirm_text = gtk_util::GetSaveLabel();
  60. else if (action == GTK_FILE_CHOOSER_ACTION_OPEN)
  61. confirm_text = gtk_util::GetOpenLabel();
  62. dialog_ = GTK_FILE_CHOOSER(gtk_file_chooser_dialog_new(
  63. settings.title.c_str(), NULL, action, gtk_util::GetCancelLabel(),
  64. GTK_RESPONSE_CANCEL, confirm_text, GTK_RESPONSE_ACCEPT, NULL));
  65. }
  66. if (parent_) {
  67. parent_->SetEnabled(false);
  68. if (electron::IsElectron_gtkInitialized()) {
  69. gtk_native_dialog_set_modal(GTK_NATIVE_DIALOG(dialog_), TRUE);
  70. } else {
  71. gtk::SetGtkTransientForAura(GTK_WIDGET(dialog_),
  72. parent_->GetNativeWindow());
  73. gtk_window_set_modal(GTK_WINDOW(dialog_), TRUE);
  74. }
  75. }
  76. if (action == GTK_FILE_CHOOSER_ACTION_SAVE)
  77. gtk_file_chooser_set_do_overwrite_confirmation(dialog_, TRUE);
  78. if (action != GTK_FILE_CHOOSER_ACTION_OPEN)
  79. gtk_file_chooser_set_create_folders(dialog_, TRUE);
  80. if (!settings.default_path.empty()) {
  81. electron::ScopedAllowBlockingForElectron allow_blocking;
  82. if (base::DirectoryExists(settings.default_path)) {
  83. gtk_file_chooser_set_current_folder(
  84. dialog_, settings.default_path.value().c_str());
  85. } else {
  86. if (settings.default_path.IsAbsolute()) {
  87. gtk_file_chooser_set_current_folder(
  88. dialog_, settings.default_path.DirName().value().c_str());
  89. }
  90. gtk_file_chooser_set_current_name(
  91. GTK_FILE_CHOOSER(dialog_),
  92. settings.default_path.BaseName().value().c_str());
  93. }
  94. }
  95. if (!settings.filters.empty())
  96. AddFilters(settings.filters);
  97. // GtkFileChooserNative does not support preview widgets through the
  98. // org.freedesktop.portal.FileChooser portal. In the case of running through
  99. // the org.freedesktop.portal.FileChooser portal, anything having to do with
  100. // the update-preview signal or the preview widget will just be ignored.
  101. if (!electron::IsElectron_gtkInitialized()) {
  102. preview_ = gtk_image_new();
  103. g_signal_connect(dialog_, "update-preview",
  104. G_CALLBACK(OnUpdatePreviewThunk), this);
  105. gtk_file_chooser_set_preview_widget(dialog_, preview_);
  106. }
  107. }
  108. ~FileChooserDialog() {
  109. if (electron::IsElectron_gtkInitialized()) {
  110. gtk_native_dialog_destroy(GTK_NATIVE_DIALOG(dialog_));
  111. } else {
  112. gtk_widget_destroy(GTK_WIDGET(dialog_));
  113. }
  114. if (parent_)
  115. parent_->SetEnabled(true);
  116. }
  117. // disable copy
  118. FileChooserDialog(const FileChooserDialog&) = delete;
  119. FileChooserDialog& operator=(const FileChooserDialog&) = delete;
  120. void SetupOpenProperties(int properties) {
  121. const auto hasProp = [properties](OpenFileDialogProperty prop) {
  122. return gboolean((properties & prop) != 0);
  123. };
  124. auto* file_chooser = dialog();
  125. gtk_file_chooser_set_select_multiple(file_chooser,
  126. hasProp(OPEN_DIALOG_MULTI_SELECTIONS));
  127. gtk_file_chooser_set_show_hidden(file_chooser,
  128. hasProp(OPEN_DIALOG_SHOW_HIDDEN_FILES));
  129. }
  130. void SetupSaveProperties(int properties) {
  131. const auto hasProp = [properties](SaveFileDialogProperty prop) {
  132. return gboolean((properties & prop) != 0);
  133. };
  134. auto* file_chooser = dialog();
  135. gtk_file_chooser_set_show_hidden(file_chooser,
  136. hasProp(SAVE_DIALOG_SHOW_HIDDEN_FILES));
  137. gtk_file_chooser_set_do_overwrite_confirmation(
  138. file_chooser, hasProp(SAVE_DIALOG_SHOW_OVERWRITE_CONFIRMATION));
  139. }
  140. void RunAsynchronous() {
  141. g_signal_connect(dialog_, "response", G_CALLBACK(OnFileDialogResponseThunk),
  142. this);
  143. if (electron::IsElectron_gtkInitialized()) {
  144. gtk_native_dialog_show(GTK_NATIVE_DIALOG(dialog_));
  145. } else {
  146. gtk_widget_show_all(GTK_WIDGET(dialog_));
  147. gtk::GtkUi::GetPlatform()->ShowGtkWindow(GTK_WINDOW(dialog_));
  148. }
  149. }
  150. void RunSaveAsynchronous(
  151. gin_helper::Promise<gin_helper::Dictionary> promise) {
  152. save_promise_ =
  153. std::make_unique<gin_helper::Promise<gin_helper::Dictionary>>(
  154. std::move(promise));
  155. RunAsynchronous();
  156. }
  157. void RunOpenAsynchronous(
  158. gin_helper::Promise<gin_helper::Dictionary> promise) {
  159. open_promise_ =
  160. std::make_unique<gin_helper::Promise<gin_helper::Dictionary>>(
  161. std::move(promise));
  162. RunAsynchronous();
  163. }
  164. base::FilePath GetFileName() const {
  165. gchar* filename = gtk_file_chooser_get_filename(dialog_);
  166. const base::FilePath path(filename);
  167. g_free(filename);
  168. return path;
  169. }
  170. std::vector<base::FilePath> GetFileNames() const {
  171. std::vector<base::FilePath> paths;
  172. auto* filenames = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog_));
  173. for (auto* iter = filenames; iter != nullptr; iter = iter->next) {
  174. auto* filename = static_cast<char*>(iter->data);
  175. paths.emplace_back(filename);
  176. g_free(filename);
  177. }
  178. g_slist_free(filenames);
  179. return paths;
  180. }
  181. CHROMEG_CALLBACK_1(FileChooserDialog,
  182. void,
  183. OnFileDialogResponse,
  184. GtkWidget*,
  185. int);
  186. GtkFileChooser* dialog() const { return dialog_; }
  187. private:
  188. void AddFilters(const Filters& filters);
  189. electron::NativeWindowViews* parent_;
  190. GtkFileChooser* dialog_;
  191. GtkWidget* preview_;
  192. Filters filters_;
  193. std::unique_ptr<gin_helper::Promise<gin_helper::Dictionary>> save_promise_;
  194. std::unique_ptr<gin_helper::Promise<gin_helper::Dictionary>> open_promise_;
  195. // Callback for when we update the preview for the selection.
  196. CHROMEG_CALLBACK_0(FileChooserDialog, void, OnUpdatePreview, GtkFileChooser*);
  197. };
  198. void FileChooserDialog::OnFileDialogResponse(GtkWidget* widget, int response) {
  199. if (electron::IsElectron_gtkInitialized()) {
  200. gtk_native_dialog_hide(GTK_NATIVE_DIALOG(dialog_));
  201. } else {
  202. gtk_widget_hide(GTK_WIDGET(dialog_));
  203. }
  204. v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
  205. v8::HandleScope scope(isolate);
  206. if (save_promise_) {
  207. gin_helper::Dictionary dict =
  208. gin::Dictionary::CreateEmpty(save_promise_->isolate());
  209. if (response == GTK_RESPONSE_ACCEPT) {
  210. dict.Set("canceled", false);
  211. dict.Set("filePath", GetFileName());
  212. } else {
  213. dict.Set("canceled", true);
  214. dict.Set("filePath", base::FilePath());
  215. }
  216. save_promise_->Resolve(dict);
  217. } else if (open_promise_) {
  218. gin_helper::Dictionary dict =
  219. gin::Dictionary::CreateEmpty(open_promise_->isolate());
  220. if (response == GTK_RESPONSE_ACCEPT) {
  221. dict.Set("canceled", false);
  222. dict.Set("filePaths", GetFileNames());
  223. } else {
  224. dict.Set("canceled", true);
  225. dict.Set("filePaths", std::vector<base::FilePath>());
  226. }
  227. open_promise_->Resolve(dict);
  228. }
  229. delete this;
  230. }
  231. void FileChooserDialog::AddFilters(const Filters& filters) {
  232. for (const auto& filter : filters) {
  233. GtkFileFilter* gtk_filter = gtk_file_filter_new();
  234. for (const auto& extension : filter.second) {
  235. std::string pattern = MakeCaseInsensitivePattern(extension);
  236. gtk_file_filter_add_pattern(gtk_filter, pattern.c_str());
  237. }
  238. gtk_file_filter_set_name(gtk_filter, filter.first.c_str());
  239. gtk_file_chooser_add_filter(dialog_, gtk_filter);
  240. }
  241. }
  242. bool CanPreview(const struct stat& st) {
  243. // Only preview regular files; pipes may hang.
  244. // See https://crbug.com/534754.
  245. if (!S_ISREG(st.st_mode)) {
  246. return false;
  247. }
  248. // Don't preview huge files; they may crash.
  249. // https://github.com/electron/electron/issues/31630
  250. // Setting an arbitrary filesize max t at 100 MB here.
  251. constexpr off_t ArbitraryMax = 100000000ULL;
  252. return st.st_size < ArbitraryMax;
  253. }
  254. void FileChooserDialog::OnUpdatePreview(GtkFileChooser* chooser) {
  255. CHECK(!electron::IsElectron_gtkInitialized());
  256. gchar* filename = gtk_file_chooser_get_preview_filename(chooser);
  257. if (!filename) {
  258. gtk_file_chooser_set_preview_widget_active(chooser, FALSE);
  259. return;
  260. }
  261. struct stat sb;
  262. if (stat(filename, &sb) != 0 || !CanPreview(sb)) {
  263. g_free(filename);
  264. gtk_file_chooser_set_preview_widget_active(chooser, FALSE);
  265. return;
  266. }
  267. // This will preserve the image's aspect ratio.
  268. GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file_at_size(filename, kPreviewWidth,
  269. kPreviewHeight, nullptr);
  270. g_free(filename);
  271. if (pixbuf) {
  272. gtk_image_set_from_pixbuf(GTK_IMAGE(preview_), pixbuf);
  273. g_object_unref(pixbuf);
  274. }
  275. gtk_file_chooser_set_preview_widget_active(chooser, pixbuf ? TRUE : FALSE);
  276. }
  277. } // namespace
  278. void ShowFileDialog(const FileChooserDialog& dialog) {
  279. // gtk_native_dialog_run() will call gtk_native_dialog_show() for us.
  280. if (!electron::IsElectron_gtkInitialized()) {
  281. gtk_widget_show_all(GTK_WIDGET(dialog.dialog()));
  282. }
  283. }
  284. int RunFileDialog(const FileChooserDialog& dialog) {
  285. int response = 0;
  286. if (electron::IsElectron_gtkInitialized()) {
  287. response = gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog.dialog()));
  288. } else {
  289. response = gtk_dialog_run(GTK_DIALOG(dialog.dialog()));
  290. }
  291. return response;
  292. }
  293. bool ShowOpenDialogSync(const DialogSettings& settings,
  294. std::vector<base::FilePath>* paths) {
  295. GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
  296. if (settings.properties & OPEN_DIALOG_OPEN_DIRECTORY)
  297. action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
  298. FileChooserDialog open_dialog(action, settings);
  299. open_dialog.SetupOpenProperties(settings.properties);
  300. ShowFileDialog(open_dialog);
  301. const int response = RunFileDialog(open_dialog);
  302. if (response == GTK_RESPONSE_ACCEPT) {
  303. *paths = open_dialog.GetFileNames();
  304. return true;
  305. }
  306. return false;
  307. }
  308. void ShowOpenDialog(const DialogSettings& settings,
  309. gin_helper::Promise<gin_helper::Dictionary> promise) {
  310. GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
  311. if (settings.properties & OPEN_DIALOG_OPEN_DIRECTORY)
  312. action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
  313. FileChooserDialog* open_dialog = new FileChooserDialog(action, settings);
  314. open_dialog->SetupOpenProperties(settings.properties);
  315. open_dialog->RunOpenAsynchronous(std::move(promise));
  316. }
  317. bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) {
  318. FileChooserDialog save_dialog(GTK_FILE_CHOOSER_ACTION_SAVE, settings);
  319. save_dialog.SetupSaveProperties(settings.properties);
  320. ShowFileDialog(save_dialog);
  321. const int response = RunFileDialog(save_dialog);
  322. if (response == GTK_RESPONSE_ACCEPT) {
  323. *path = save_dialog.GetFileName();
  324. return true;
  325. }
  326. return false;
  327. }
  328. void ShowSaveDialog(const DialogSettings& settings,
  329. gin_helper::Promise<gin_helper::Dictionary> promise) {
  330. FileChooserDialog* save_dialog =
  331. new FileChooserDialog(GTK_FILE_CHOOSER_ACTION_SAVE, settings);
  332. save_dialog->RunSaveAsynchronous(std::move(promise));
  333. }
  334. } // namespace file_dialog