Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use GtkFileChooserNative to support the XDG Desktop Portal specification #19159

Merged
merged 50 commits into from
Apr 1, 2021
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7018d60
feat: Use GtkFileChooserNative if available to support XDG portals
tristan957 Jul 8, 2019
43b6de1
fix: namespace
tristan957 Feb 28, 2020
cad379b
fix: labels were reversed
tristan957 Mar 6, 2020
d192d26
fix: lint issue
tristan957 Mar 6, 2020
fbbc100
fix: clean up some implementation
tristan957 Mar 8, 2020
5e06ecc
fix: remove deprecation branch
tristan957 Mar 8, 2020
c09eb5d
fix: remove unused header
tristan957 Mar 8, 2020
6584d8e
fix: remove unused gi18n.h include
tristan957 Mar 8, 2020
22a0d76
fix: add the set_data call into the mirrored SetGtkTransientForAura func
tristan957 Jul 14, 2020
ce211f6
fix: remove gmodule support and use native for the dialog regardless
tristan957 Jul 14, 2020
e938c06
fix: undo yarn.lock changes
tristan957 Jul 14, 2020
26e2caa
fix: lint
tristan957 Jul 14, 2020
b46f9a0
fix: remove x11 unncessary x11 include
tristan957 Oct 29, 2020
a60c7d3
fix: lint
tristan957 Oct 29, 2020
e3b9b83
fix: remove SetGtkTransientForAura
tristan957 Oct 29, 2020
309f1a7
Revert "fix: remove gmodule support and use native for the dialog reg…
tristan957 Oct 30, 2020
29b0f2b
fix: add support in a backwards compatible way
tristan957 Oct 30, 2020
d70f84e
fix: lint
tristan957 Oct 30, 2020
22151f6
docs: update comment
tristan957 Oct 30, 2020
301e4b3
Revert "fix: remove x11 unncessary x11 include"
tristan957 Oct 30, 2020
fef6ac3
fix: compiler errors
tristan957 Oct 30, 2020
80aeb99
fix: int -> x11::time
tristan957 Oct 30, 2020
cf779fa
fix: move GtkNativeDialog static data to global state
tristan957 Oct 30, 2020
8b482e1
fix: revert yarn.lock change
tristan957 Dec 18, 2020
47a15d5
update: for code review comments
tristan957 Dec 18, 2020
da52c8b
fix: remove functional header
tristan957 Dec 18, 2020
e0668a3
fix: variable name
tristan957 Dec 18, 2020
a6490d8
fix: rename GTK native initalization func
tristan957 Dec 18, 2020
116232c
Help out the compiler
tristan957 Dec 18, 2020
013d052
Help out the compiler
tristan957 Dec 18, 2020
9035eb3
Help out the compiler
tristan957 Dec 18, 2020
039e1e7
Fix function signature
tristan957 Dec 18, 2020
4c1c674
Remove unused header
tristan957 Dec 18, 2020
187b207
Rename optional boolean for GtkFileChooserNative support
tristan957 Dec 18, 2020
e4229a1
Add back in USE_X11 check
tristan957 Dec 18, 2020
79b5aa6
Satisfy linter
tristan957 Dec 18, 2020
55ec914
Resatisfy linter
tristan957 Dec 18, 2020
bbd2609
Fix alignment of if
tristan957 Dec 18, 2020
d991097
Fix alignment of arguments
tristan957 Dec 18, 2020
9e2d7e0
linting...
tristan957 Dec 18, 2020
1c8b384
fix: add back in the i18n hack
tristan957 Feb 10, 2021
0e70859
fix: lint
tristan957 Feb 10, 2021
20bfff4
Respond to some review comments
tristan957 Mar 25, 2021
9b0aa96
fix: lint
tristan957 Mar 25, 2021
02a07e3
Make adding filter agnostic
tristan957 Mar 25, 2021
ce5f94a
fix: transform is in place
tristan957 Mar 25, 2021
b4221c7
fix: remove std::transform because not c++17
tristan957 Mar 25, 2021
b4aba74
Remove unused include
tristan957 Mar 25, 2021
2d4f82e
fix: address Cheng's review
tristan957 Mar 30, 2021
25b5401
fix: Remove unused header
tristan957 Mar 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
232 changes: 182 additions & 50 deletions shell/browser/ui/file_dialog_gtk.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#include <memory>
#include <gmodule.h>

#include "shell/browser/ui/file_dialog.h"
#include "shell/browser/ui/gtk_util.h"
#include <memory>

#include "base/callback.h"
#include "base/files/file_util.h"
#include "base/strings/string_util.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/browser/unresponsive_suppressor.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "ui/base/glib/glib_signal.h"
Expand All @@ -27,6 +28,27 @@

namespace file_dialog {

static GModule* gtk_module;
static base::Optional<bool> supports_gtk_file_chooser_native;

using dl_gtk_native_dialog_show_t = void (*)(void*);
using dl_gtk_native_dialog_destroy_t = void (*)(void*);
using dl_gtk_native_dialog_set_modal_t = void (*)(void*, gboolean);
using dl_gtk_native_dialog_run_t = int (*)(void*);
using dl_gtk_native_dialog_hide_t = void (*)(void*);
using dl_gtk_file_chooser_native_new_t = void* (*)(const char*,
GtkWindow*,
GtkFileChooserAction,
const char*,
const char*);

static dl_gtk_native_dialog_show_t dl_gtk_native_dialog_show;
static dl_gtk_native_dialog_destroy_t dl_gtk_native_dialog_destroy;
static dl_gtk_native_dialog_set_modal_t dl_gtk_native_dialog_set_modal;
static dl_gtk_native_dialog_run_t dl_gtk_native_dialog_run;
static dl_gtk_native_dialog_hide_t dl_gtk_native_dialog_hide;
static dl_gtk_file_chooser_native_new_t dl_gtk_file_chooser_native_new;

DialogSettings::DialogSettings() = default;
DialogSettings::DialogSettings(const DialogSettings&) = default;
DialogSettings::~DialogSettings() = default;
Expand All @@ -51,6 +73,73 @@ void OnFileFilterDataDestroyed(std::string* file_extension) {
delete file_extension;
}

void InitGtkFileChooserNativeSupport() {
// Return early if we have already setup the native functions or we have tried
// once before and failed. Avoid running expensive dynamic library operations.
if (supports_gtk_file_chooser_native) {
return;
}
tristan957 marked this conversation as resolved.
Show resolved Hide resolved

// Mark that we have attempted to initialize support at least once
supports_gtk_file_chooser_native = false;

if (!g_module_supported()) {
return;
}

gtk_module = g_module_open("libgtk-3.so", G_MODULE_BIND_LAZY);
if (!gtk_module) {
return;
}

// Will never be unloaded
g_module_make_resident(gtk_module);

bool found = g_module_symbol(
gtk_module, "gtk_file_chooser_native_new",
reinterpret_cast<void**>(&dl_gtk_file_chooser_native_new));
if (!found) {
return;
}

found = g_module_symbol(
gtk_module, "gtk_native_dialog_set_modal",
reinterpret_cast<void**>(&dl_gtk_native_dialog_set_modal));
if (!found) {
return;
}

found =
g_module_symbol(gtk_module, "gtk_native_dialog_destroy",
reinterpret_cast<void**>(&dl_gtk_native_dialog_destroy));
if (!found) {
return;
}

found = g_module_symbol(gtk_module, "gtk_native_dialog_show",
reinterpret_cast<void**>(&dl_gtk_native_dialog_show));
if (!found) {
return;
}

found = g_module_symbol(gtk_module, "gtk_native_dialog_hide",
reinterpret_cast<void**>(&dl_gtk_native_dialog_hide));
if (!found) {
return;
}

found = g_module_symbol(gtk_module, "gtk_native_dialog_run",
reinterpret_cast<void**>(&dl_gtk_native_dialog_run));
if (!found) {
return;
}

supports_gtk_file_chooser_native =
dl_gtk_file_chooser_native_new && dl_gtk_native_dialog_set_modal &&
dl_gtk_native_dialog_destroy && dl_gtk_native_dialog_run &&
dl_gtk_native_dialog_show && dl_gtk_native_dialog_hide;
}

class FileChooserDialog {
public:
FileChooserDialog(GtkFileChooserAction action, const DialogSettings& settings)
Expand All @@ -66,30 +155,42 @@ class FileChooserDialog {
else if (action == GTK_FILE_CHOOSER_ACTION_OPEN)
confirm_text = gtk_util::kOpenLabel;

dialog_ = gtk_file_chooser_dialog_new(
settings.title.c_str(), nullptr, action, gtk_util::kCancelLabel,
GTK_RESPONSE_CANCEL, confirm_text, GTK_RESPONSE_ACCEPT, NULL);
InitGtkFileChooserNativeSupport();

if (supports_gtk_file_chooser_native.value_or(false)) {
tristan957 marked this conversation as resolved.
Show resolved Hide resolved
dialog_ = GTK_FILE_CHOOSER(
dl_gtk_file_chooser_native_new(settings.title.c_str(), NULL, action,
confirm_text, gtk_util::kCancelLabel));
} else {
dialog_ = GTK_FILE_CHOOSER(gtk_file_chooser_dialog_new(
settings.title.c_str(), NULL, action, gtk_util::kCancelLabel,
GTK_RESPONSE_CANCEL, confirm_text, GTK_RESPONSE_ACCEPT, NULL));
}

if (parent_) {
parent_->SetEnabled(false);
gtk::SetGtkTransientForAura(dialog_, parent_->GetNativeWindow());
gtk_window_set_modal(GTK_WINDOW(dialog_), TRUE);
if (supports_gtk_file_chooser_native.value_or(false)) {
dl_gtk_native_dialog_set_modal(static_cast<void*>(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(GTK_FILE_CHOOSER(dialog_),
TRUE);
gtk_file_chooser_set_do_overwrite_confirmation(dialog_, TRUE);
if (action != GTK_FILE_CHOOSER_ACTION_OPEN)
gtk_file_chooser_set_create_folders(GTK_FILE_CHOOSER(dialog_), TRUE);
gtk_file_chooser_set_create_folders(dialog_, TRUE);

if (!settings.default_path.empty()) {
if (base::DirectoryExists(settings.default_path)) {
gtk_file_chooser_set_current_folder(
GTK_FILE_CHOOSER(dialog_), settings.default_path.value().c_str());
dialog_, settings.default_path.value().c_str());
} else {
if (settings.default_path.IsAbsolute()) {
gtk_file_chooser_set_current_folder(
GTK_FILE_CHOOSER(dialog_),
settings.default_path.DirName().value().c_str());
dialog_, settings.default_path.DirName().value().c_str());
}

gtk_file_chooser_set_current_name(
Expand All @@ -101,14 +202,23 @@ class FileChooserDialog {
if (!settings.filters.empty())
AddFilters(settings.filters);
tristan957 marked this conversation as resolved.
Show resolved Hide resolved

// 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.
tristan957 marked this conversation as resolved.
Show resolved Hide resolved
preview_ = gtk_image_new();
g_signal_connect(dialog_, "update-preview",
G_CALLBACK(OnUpdatePreviewThunk), this);
gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog_), preview_);
gtk_file_chooser_set_preview_widget(dialog_, preview_);
}

~FileChooserDialog() {
gtk_widget_destroy(dialog_);
if (supports_gtk_file_chooser_native.value_or(false)) {
dl_gtk_native_dialog_destroy(static_cast<void*>(dialog_));
} else {
gtk_widget_destroy(GTK_WIDGET(dialog_));
}

if (parent_)
parent_->SetEnabled(true);
}
Expand All @@ -117,7 +227,7 @@ class FileChooserDialog {
const auto hasProp = [properties](OpenFileDialogProperty prop) {
return gboolean((properties & prop) != 0);
};
auto* file_chooser = GTK_FILE_CHOOSER(dialog());
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,
Expand All @@ -128,29 +238,31 @@ class FileChooserDialog {
const auto hasProp = [properties](SaveFileDialogProperty prop) {
return gboolean((properties & prop) != 0);
};
auto* file_chooser = GTK_FILE_CHOOSER(dialog());
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_, "delete-event",
G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect(dialog_, "response", G_CALLBACK(OnFileDialogResponseThunk),
this);
gtk_widget_show_all(dialog_);
if (supports_gtk_file_chooser_native.value_or(false)) {
dl_gtk_native_dialog_show(static_cast<void*>(dialog_));
} else {
gtk_widget_show_all(GTK_WIDGET(dialog_));

#if defined(USE_X11)
if (!features::IsUsingOzonePlatform()) {
// We need to call gtk_window_present after making the widgets visible to
// make sure window gets correctly raised and gets focus.
x11::Time time = ui::X11EventSource::GetInstance()->GetTimestamp();
gtk_window_present_with_time(GTK_WINDOW(dialog_),
static_cast<uint32_t>(time));
}
if (!features::IsUsingOzonePlatform()) {
// We need to call gtk_window_present after making the widgets visible
// to make sure window gets correctly raised and gets focus.
x11::Time time = ui::X11EventSource::GetInstance()->GetTimestamp();
gtk_window_present_with_time(GTK_WINDOW(dialog_),
static_cast<uint32_t>(time));
}
#endif
}
tristan957 marked this conversation as resolved.
Show resolved Hide resolved
tristan957 marked this conversation as resolved.
Show resolved Hide resolved
}

void RunSaveAsynchronous(
Expand All @@ -170,7 +282,7 @@ class FileChooserDialog {
}

base::FilePath GetFileName() const {
gchar* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog_));
gchar* filename = gtk_file_chooser_get_filename(dialog_);
const base::FilePath path(filename);
g_free(filename);
return path;
Expand All @@ -194,29 +306,33 @@ class FileChooserDialog {
GtkWidget*,
int);

GtkWidget* dialog() const { return dialog_; }
GtkFileChooser* dialog() const { return dialog_; }

private:
void AddFilters(const Filters& filters);

electron::NativeWindowViews* parent_;
electron::UnresponsiveSuppressor unresponsive_suppressor_;

GtkWidget* dialog_;
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, GtkWidget*);
CHROMEG_CALLBACK_0(FileChooserDialog, void, OnUpdatePreview, GtkFileChooser*);

DISALLOW_COPY_AND_ASSIGN(FileChooserDialog);
};

void FileChooserDialog::OnFileDialogResponse(GtkWidget* widget, int response) {
gtk_widget_hide(dialog_);
if (supports_gtk_file_chooser_native.value_or(false)) {
dl_gtk_native_dialog_hide(static_cast<void*>(dialog_));
} else {
gtk_widget_hide(GTK_WIDGET(dialog_));
}
tristan957 marked this conversation as resolved.
Show resolved Hide resolved
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
if (save_promise_) {
Expand Down Expand Up @@ -259,26 +375,23 @@ void FileChooserDialog::AddFilters(const Filters& filters) {
}

gtk_file_filter_set_name(gtk_filter, filter.first.c_str());
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog_), gtk_filter);
gtk_file_chooser_add_filter(dialog_, gtk_filter);
}
}

void FileChooserDialog::OnUpdatePreview(GtkWidget* chooser) {
gchar* filename =
gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(chooser));
void FileChooserDialog::OnUpdatePreview(GtkFileChooser* chooser) {
gchar* filename = gtk_file_chooser_get_preview_filename(chooser);
if (!filename) {
gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser),
FALSE);
gtk_file_chooser_set_preview_widget_active(chooser, FALSE);
return;
}

// Don't attempt to open anything which isn't a regular file. If a named pipe,
// this may hang. See https://crbug.com/534754.
// Don't attempt to open anything which isn't a regular file. If a named
// pipe, this may hang. See https://crbug.com/534754.
struct stat stat_buf;
if (stat(filename, &stat_buf) != 0 || !S_ISREG(stat_buf.st_mode)) {
g_free(filename);
gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser),
FALSE);
gtk_file_chooser_set_preview_widget_active(chooser, FALSE);
return;
}

Expand All @@ -290,12 +403,30 @@ void FileChooserDialog::OnUpdatePreview(GtkWidget* chooser) {
gtk_image_set_from_pixbuf(GTK_IMAGE(preview_), pixbuf);
g_object_unref(pixbuf);
}
gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser),
pixbuf ? TRUE : FALSE);
gtk_file_chooser_set_preview_widget_active(chooser, pixbuf ? TRUE : FALSE);
}

} // namespace

void ShowFileDialog(const FileChooserDialog& dialog) {
if (supports_gtk_file_chooser_native.value_or(false)) {
dl_gtk_native_dialog_show(static_cast<void*>(dialog.dialog()));
} else {
gtk_widget_show_all(GTK_WIDGET(dialog.dialog()));
}
tristan957 marked this conversation as resolved.
Show resolved Hide resolved
}

int RunFileDialog(const FileChooserDialog& dialog) {
int response = 0;
if (supports_gtk_file_chooser_native.value_or(false)) {
response = dl_gtk_native_dialog_run(static_cast<void*>(dialog.dialog()));
} else {
response = gtk_dialog_run(GTK_DIALOG(dialog.dialog()));
}
ckerr marked this conversation as resolved.
Show resolved Hide resolved

return response;
}

bool ShowOpenDialogSync(const DialogSettings& settings,
std::vector<base::FilePath>* paths) {
GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
Expand All @@ -304,8 +435,9 @@ bool ShowOpenDialogSync(const DialogSettings& settings,
FileChooserDialog open_dialog(action, settings);
open_dialog.SetupOpenProperties(settings.properties);

gtk_widget_show_all(open_dialog.dialog());
int response = gtk_dialog_run(GTK_DIALOG(open_dialog.dialog()));
ShowFileDialog(open_dialog);

const int response = RunFileDialog(open_dialog);
if (response == GTK_RESPONSE_ACCEPT) {
*paths = open_dialog.GetFileNames();
return true;
Expand All @@ -325,10 +457,10 @@ void ShowOpenDialog(const DialogSettings& settings,

bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) {
FileChooserDialog save_dialog(GTK_FILE_CHOOSER_ACTION_SAVE, settings);
save_dialog.SetupSaveProperties(settings.properties);
tristan957 marked this conversation as resolved.
Show resolved Hide resolved

gtk_widget_show_all(save_dialog.dialog());
int response = gtk_dialog_run(GTK_DIALOG(save_dialog.dialog()));
ShowFileDialog(save_dialog);

const int response = RunFileDialog(save_dialog);
if (response == GTK_RESPONSE_ACCEPT) {
*path = save_dialog.GetFileName();
return true;
Expand Down