Bug 1751217 Part 3: Make HDR-capable macOS screens report 30 pixelDepth. r=mstange
[gecko.git] / widget / gtk / nsFilePicker.cpp
blob00a5caa288ff736766aeec051ea52c008c5943b6
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include <dlfcn.h>
7 #include <gtk/gtk.h>
8 #include <sys/types.h>
9 #include <sys/stat.h>
10 #include <unistd.h>
12 #include "mozilla/Types.h"
13 #include "nsGtkUtils.h"
14 #include "nsIFileURL.h"
15 #include "nsIGIOService.h"
16 #include "nsIURI.h"
17 #include "nsIWidget.h"
18 #include "nsIFile.h"
19 #include "mozilla/Preferences.h"
21 #include "nsArrayEnumerator.h"
22 #include "nsMemory.h"
23 #include "nsEnumeratorUtils.h"
24 #include "nsNetUtil.h"
25 #include "nsReadableUtils.h"
26 #include "MozContainer.h"
27 #include "WidgetUtilsGtk.h"
29 #include "nsFilePicker.h"
31 #undef LOG
32 #ifdef MOZ_LOGGING
33 # include "mozilla/Logging.h"
34 # include "nsTArray.h"
35 # include "Units.h"
36 extern mozilla::LazyLogModule gWidgetLog;
37 # define LOG(args) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, args)
38 #else
39 # define LOG(args)
40 #endif /* MOZ_LOGGING */
42 using namespace mozilla;
44 #define MAX_PREVIEW_SIZE 180
45 // bug 1184009
46 #define MAX_PREVIEW_SOURCE_SIZE 4096
48 nsIFile* nsFilePicker::mPrevDisplayDirectory = nullptr;
50 void nsFilePicker::Shutdown() { NS_IF_RELEASE(mPrevDisplayDirectory); }
52 static GtkFileChooserAction GetGtkFileChooserAction(int16_t aMode) {
53 GtkFileChooserAction action;
55 switch (aMode) {
56 case nsIFilePicker::modeSave:
57 action = GTK_FILE_CHOOSER_ACTION_SAVE;
58 break;
60 case nsIFilePicker::modeGetFolder:
61 action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
62 break;
64 case nsIFilePicker::modeOpen:
65 case nsIFilePicker::modeOpenMultiple:
66 action = GTK_FILE_CHOOSER_ACTION_OPEN;
67 break;
69 default:
70 NS_WARNING("Unknown nsIFilePicker mode");
71 action = GTK_FILE_CHOOSER_ACTION_OPEN;
72 break;
75 return action;
78 static void UpdateFilePreviewWidget(GtkFileChooser* file_chooser,
79 gpointer preview_widget_voidptr) {
80 GtkImage* preview_widget = GTK_IMAGE(preview_widget_voidptr);
81 char* image_filename = gtk_file_chooser_get_preview_filename(file_chooser);
82 struct stat st_buf;
84 if (!image_filename) {
85 gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
86 return;
89 gint preview_width = 0;
90 gint preview_height = 0;
91 /* check type of file
92 * if file is named pipe, Open is blocking which may lead to UI
93 * nonresponsiveness; if file is directory/socket, it also isn't
94 * likely to get preview */
95 if (stat(image_filename, &st_buf) || (!S_ISREG(st_buf.st_mode))) {
96 g_free(image_filename);
97 gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
98 return; /* stat failed or file is not regular */
101 GdkPixbufFormat* preview_format =
102 gdk_pixbuf_get_file_info(image_filename, &preview_width, &preview_height);
103 if (!preview_format || preview_width <= 0 || preview_height <= 0 ||
104 preview_width > MAX_PREVIEW_SOURCE_SIZE ||
105 preview_height > MAX_PREVIEW_SOURCE_SIZE) {
106 g_free(image_filename);
107 gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
108 return;
111 GdkPixbuf* preview_pixbuf = nullptr;
112 // Only scale down images that are too big
113 if (preview_width > MAX_PREVIEW_SIZE || preview_height > MAX_PREVIEW_SIZE) {
114 preview_pixbuf = gdk_pixbuf_new_from_file_at_size(
115 image_filename, MAX_PREVIEW_SIZE, MAX_PREVIEW_SIZE, nullptr);
116 } else {
117 preview_pixbuf = gdk_pixbuf_new_from_file(image_filename, nullptr);
120 g_free(image_filename);
122 if (!preview_pixbuf) {
123 gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
124 return;
127 GdkPixbuf* preview_pixbuf_temp = preview_pixbuf;
128 preview_pixbuf = gdk_pixbuf_apply_embedded_orientation(preview_pixbuf_temp);
129 g_object_unref(preview_pixbuf_temp);
131 // This is the easiest way to do center alignment without worrying about
132 // containers Minimum 3px padding each side (hence the 6) just to make things
133 // nice
134 gint x_padding =
135 (MAX_PREVIEW_SIZE + 6 - gdk_pixbuf_get_width(preview_pixbuf)) / 2;
136 gtk_misc_set_padding(GTK_MISC(preview_widget), x_padding, 0);
138 gtk_image_set_from_pixbuf(preview_widget, preview_pixbuf);
139 g_object_unref(preview_pixbuf);
140 gtk_file_chooser_set_preview_widget_active(file_chooser, TRUE);
143 static nsAutoCString MakeCaseInsensitiveShellGlob(const char* aPattern) {
144 // aPattern is UTF8
145 nsAutoCString result;
146 unsigned int len = strlen(aPattern);
148 for (unsigned int i = 0; i < len; i++) {
149 if (!g_ascii_isalpha(aPattern[i])) {
150 // non-ASCII characters will also trigger this path, so unicode
151 // is safely handled albeit case-sensitively
152 result.Append(aPattern[i]);
153 continue;
156 // add the lowercase and uppercase version of a character to a bracket
157 // match, so it matches either the lowercase or uppercase char.
158 result.Append('[');
159 result.Append(g_ascii_tolower(aPattern[i]));
160 result.Append(g_ascii_toupper(aPattern[i]));
161 result.Append(']');
164 return result;
167 NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
169 nsFilePicker::nsFilePicker()
170 : mSelectedType(0),
171 mRunning(false),
172 mAllowURLs(false),
173 mFileChooserDelegate(nullptr) {
174 mUseNativeFileChooser =
175 widget::ShouldUsePortal(widget::PortalKind::FilePicker);
178 nsFilePicker::~nsFilePicker() = default;
180 void ReadMultipleFiles(gpointer filename, gpointer array) {
181 nsCOMPtr<nsIFile> localfile;
182 nsresult rv =
183 NS_NewNativeLocalFile(nsDependentCString(static_cast<char*>(filename)),
184 false, getter_AddRefs(localfile));
185 if (NS_SUCCEEDED(rv)) {
186 nsCOMArray<nsIFile>& files = *static_cast<nsCOMArray<nsIFile>*>(array);
187 files.AppendObject(localfile);
190 g_free(filename);
193 void nsFilePicker::ReadValuesFromFileChooser(void* file_chooser) {
194 mFiles.Clear();
196 if (mMode == nsIFilePicker::modeOpenMultiple) {
197 mFileURL.Truncate();
199 GSList* list =
200 gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(file_chooser));
201 g_slist_foreach(list, ReadMultipleFiles, static_cast<gpointer>(&mFiles));
202 g_slist_free(list);
203 } else {
204 gchar* filename = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(file_chooser));
205 mFileURL.Assign(filename);
206 g_free(filename);
209 GtkFileFilter* filter =
210 gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(file_chooser));
211 GSList* filter_list =
212 gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(file_chooser));
214 mSelectedType = static_cast<int16_t>(g_slist_index(filter_list, filter));
215 g_slist_free(filter_list);
217 // Remember last used directory.
218 nsCOMPtr<nsIFile> file;
219 GetFile(getter_AddRefs(file));
220 if (file) {
221 nsCOMPtr<nsIFile> dir;
222 file->GetParent(getter_AddRefs(dir));
223 if (dir) {
224 dir.swap(mPrevDisplayDirectory);
229 void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
230 mParentWidget = aParent;
231 mTitle.Assign(aTitle);
234 NS_IMETHODIMP
235 nsFilePicker::AppendFilters(int32_t aFilterMask) {
236 mAllowURLs = !!(aFilterMask & filterAllowURLs);
237 return nsBaseFilePicker::AppendFilters(aFilterMask);
240 NS_IMETHODIMP
241 nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) {
242 if (aFilter.EqualsLiteral("..apps")) {
243 // No platform specific thing we can do here, really....
244 return NS_OK;
247 nsAutoCString filter, name;
248 CopyUTF16toUTF8(aFilter, filter);
249 CopyUTF16toUTF8(aTitle, name);
251 mFilters.AppendElement(filter);
252 mFilterNames.AppendElement(name);
254 return NS_OK;
257 NS_IMETHODIMP
258 nsFilePicker::SetDefaultString(const nsAString& aString) {
259 mDefault = aString;
261 return NS_OK;
264 NS_IMETHODIMP
265 nsFilePicker::GetDefaultString(nsAString& aString) {
266 // Per API...
267 return NS_ERROR_FAILURE;
270 NS_IMETHODIMP
271 nsFilePicker::SetDefaultExtension(const nsAString& aExtension) {
272 mDefaultExtension = aExtension;
274 return NS_OK;
277 NS_IMETHODIMP
278 nsFilePicker::GetDefaultExtension(nsAString& aExtension) {
279 aExtension = mDefaultExtension;
281 return NS_OK;
284 NS_IMETHODIMP
285 nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
286 *aFilterIndex = mSelectedType;
288 return NS_OK;
291 NS_IMETHODIMP
292 nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
293 mSelectedType = aFilterIndex;
295 return NS_OK;
298 NS_IMETHODIMP
299 nsFilePicker::GetFile(nsIFile** aFile) {
300 NS_ENSURE_ARG_POINTER(aFile);
302 *aFile = nullptr;
303 nsCOMPtr<nsIURI> uri;
304 nsresult rv = GetFileURL(getter_AddRefs(uri));
305 if (!uri) return rv;
307 nsCOMPtr<nsIFileURL> fileURL(do_QueryInterface(uri, &rv));
308 NS_ENSURE_SUCCESS(rv, rv);
310 nsCOMPtr<nsIFile> file;
311 rv = fileURL->GetFile(getter_AddRefs(file));
312 NS_ENSURE_SUCCESS(rv, rv);
314 file.forget(aFile);
315 return NS_OK;
318 NS_IMETHODIMP
319 nsFilePicker::GetFileURL(nsIURI** aFileURL) {
320 *aFileURL = nullptr;
321 return NS_NewURI(aFileURL, mFileURL);
324 NS_IMETHODIMP
325 nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
326 NS_ENSURE_ARG_POINTER(aFiles);
328 if (mMode == nsIFilePicker::modeOpenMultiple) {
329 return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
332 return NS_ERROR_FAILURE;
335 nsresult nsFilePicker::Show(int16_t* aReturn) {
336 NS_ENSURE_ARG_POINTER(aReturn);
338 nsresult rv = Open(nullptr);
339 if (NS_FAILED(rv)) return rv;
341 while (mRunning) {
342 g_main_context_iteration(nullptr, TRUE);
345 *aReturn = mResult;
346 return NS_OK;
349 NS_IMETHODIMP
350 nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) {
351 // Can't show two dialogs concurrently with the same filepicker
352 if (mRunning) return NS_ERROR_NOT_AVAILABLE;
354 NS_ConvertUTF16toUTF8 title(mTitle);
356 GtkWindow* parent_widget =
357 GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET));
359 GtkFileChooserAction action = GetGtkFileChooserAction(mMode);
361 const gchar* accept_button;
362 NS_ConvertUTF16toUTF8 buttonLabel(mOkButtonLabel);
363 if (!mOkButtonLabel.IsEmpty()) {
364 accept_button = buttonLabel.get();
365 } else {
366 accept_button = nullptr;
369 void* file_chooser =
370 GtkFileChooserNew(title.get(), parent_widget, action, accept_button);
372 // If we have --enable-proxy-bypass-protection, then don't allow
373 // remote URLs to be used.
374 #ifndef MOZ_PROXY_BYPASS_PROTECTION
375 if (mAllowURLs) {
376 gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(file_chooser), FALSE);
378 #endif
380 if (action == GTK_FILE_CHOOSER_ACTION_OPEN ||
381 action == GTK_FILE_CHOOSER_ACTION_SAVE) {
382 GtkWidget* img_preview = gtk_image_new();
383 gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(file_chooser),
384 img_preview);
385 g_signal_connect(file_chooser, "update-preview",
386 G_CALLBACK(UpdateFilePreviewWidget), img_preview);
389 GtkFileChooserSetModal(file_chooser, parent_widget, TRUE);
391 NS_ConvertUTF16toUTF8 defaultName(mDefault);
392 switch (mMode) {
393 case nsIFilePicker::modeOpenMultiple:
394 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(file_chooser),
395 TRUE);
396 break;
397 case nsIFilePicker::modeSave:
398 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser),
399 defaultName.get());
400 break;
403 nsCOMPtr<nsIFile> defaultPath;
404 if (mDisplayDirectory) {
405 mDisplayDirectory->Clone(getter_AddRefs(defaultPath));
406 } else if (mPrevDisplayDirectory) {
407 mPrevDisplayDirectory->Clone(getter_AddRefs(defaultPath));
410 if (defaultPath) {
411 if (!defaultName.IsEmpty() && mMode != nsIFilePicker::modeSave) {
412 // Try to select the intended file. Even if it doesn't exist, GTK still
413 // switches directories.
414 defaultPath->AppendNative(defaultName);
415 nsAutoCString path;
416 defaultPath->GetNativePath(path);
417 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(file_chooser), path.get());
418 } else {
419 nsAutoCString directory;
420 defaultPath->GetNativePath(directory);
422 // Workaround for problematic refcounting in GTK3 before 3.16.
423 // We need to keep a reference to the dialog's internal delegate.
424 // Otherwise, if our dialog gets destroyed, we'll lose the dialog's
425 // delegate by the time this gets processed in the event loop.
426 // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1166741
427 if (GTK_IS_DIALOG(file_chooser)) {
428 GtkDialog* dialog = GTK_DIALOG(file_chooser);
429 GtkContainer* area = GTK_CONTAINER(gtk_dialog_get_content_area(dialog));
430 gtk_container_forall(
431 area,
432 [](GtkWidget* widget, gpointer data) {
433 if (GTK_IS_FILE_CHOOSER_WIDGET(widget)) {
434 auto result = static_cast<GtkFileChooserWidget**>(data);
435 *result = GTK_FILE_CHOOSER_WIDGET(widget);
438 &mFileChooserDelegate);
440 if (mFileChooserDelegate != nullptr) {
441 g_object_ref(mFileChooserDelegate);
444 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(file_chooser),
445 directory.get());
449 if (GTK_IS_DIALOG(file_chooser)) {
450 gtk_dialog_set_default_response(GTK_DIALOG(file_chooser),
451 GTK_RESPONSE_ACCEPT);
454 int32_t count = mFilters.Length();
455 for (int32_t i = 0; i < count; ++i) {
456 // This is fun... the GTK file picker does not accept a list of filters
457 // so we need to split out each string, and add it manually.
459 char** patterns = g_strsplit(mFilters[i].get(), ";", -1);
460 if (!patterns) {
461 return NS_ERROR_OUT_OF_MEMORY;
464 GtkFileFilter* filter = gtk_file_filter_new();
465 for (int j = 0; patterns[j] != nullptr; ++j) {
466 nsAutoCString caseInsensitiveFilter =
467 MakeCaseInsensitiveShellGlob(g_strstrip(patterns[j]));
468 gtk_file_filter_add_pattern(filter, caseInsensitiveFilter.get());
471 g_strfreev(patterns);
473 if (!mFilterNames[i].IsEmpty()) {
474 // If we have a name for our filter, let's use that.
475 const char* filter_name = mFilterNames[i].get();
476 gtk_file_filter_set_name(filter, filter_name);
477 } else {
478 // If we don't have a name, let's just use the filter pattern.
479 const char* filter_pattern = mFilters[i].get();
480 gtk_file_filter_set_name(filter, filter_pattern);
483 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter);
485 // Set the initially selected filter
486 if (mSelectedType == i) {
487 gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(file_chooser), filter);
491 gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(file_chooser),
492 TRUE);
494 mRunning = true;
495 mCallback = aCallback;
496 NS_ADDREF_THIS();
497 g_signal_connect(file_chooser, "response", G_CALLBACK(OnResponse), this);
498 GtkFileChooserShow(file_chooser);
500 return NS_OK;
503 /* static */
504 void nsFilePicker::OnResponse(void* file_chooser, gint response_id,
505 gpointer user_data) {
506 static_cast<nsFilePicker*>(user_data)->Done(file_chooser, response_id);
509 /* static */
510 void nsFilePicker::OnDestroy(GtkWidget* file_chooser, gpointer user_data) {
511 static_cast<nsFilePicker*>(user_data)->Done(file_chooser,
512 GTK_RESPONSE_CANCEL);
515 void nsFilePicker::Done(void* file_chooser, gint response) {
516 mRunning = false;
518 int16_t result;
519 switch (response) {
520 case GTK_RESPONSE_OK:
521 case GTK_RESPONSE_ACCEPT:
522 ReadValuesFromFileChooser(file_chooser);
523 result = nsIFilePicker::returnOK;
524 if (mMode == nsIFilePicker::modeSave) {
525 nsCOMPtr<nsIFile> file;
526 GetFile(getter_AddRefs(file));
527 if (file) {
528 bool exists = false;
529 file->Exists(&exists);
530 if (exists) result = nsIFilePicker::returnReplace;
533 break;
535 case GTK_RESPONSE_CANCEL:
536 case GTK_RESPONSE_CLOSE:
537 case GTK_RESPONSE_DELETE_EVENT:
538 result = nsIFilePicker::returnCancel;
539 break;
541 default:
542 NS_WARNING("Unexpected response");
543 result = nsIFilePicker::returnCancel;
544 break;
547 // A "response" signal won't be sent again but "destroy" will be.
548 g_signal_handlers_disconnect_by_func(file_chooser, FuncToGpointer(OnDestroy),
549 this);
551 // When response_id is GTK_RESPONSE_DELETE_EVENT or when called from
552 // OnDestroy, the widget would be destroyed anyway but it is fine if
553 // gtk_widget_destroy is called more than once. gtk_widget_destroy has
554 // requests that any remaining references be released, but the reference
555 // count will not be decremented again if GtkWindow's reference has already
556 // been released.
557 GtkFileChooserDestroy(file_chooser);
559 if (mFileChooserDelegate) {
560 // Properly deref our acquired reference. We call this after
561 // gtk_widget_destroy() to try and ensure that pending file info
562 // queries caused by updating the current folder have been cancelled.
563 // However, we do not know for certain when the callback will run after
564 // cancelled.
565 g_idle_add(
566 [](gpointer data) -> gboolean {
567 g_object_unref(data);
568 return G_SOURCE_REMOVE;
570 mFileChooserDelegate);
571 mFileChooserDelegate = nullptr;
574 if (mCallback) {
575 mCallback->Done(result);
576 mCallback = nullptr;
577 } else {
578 mResult = result;
580 NS_RELEASE_THIS();
583 // All below functions available as of GTK 3.20+
584 void* nsFilePicker::GtkFileChooserNew(const gchar* title, GtkWindow* parent,
585 GtkFileChooserAction action,
586 const gchar* accept_label) {
587 static auto sGtkFileChooserNativeNewPtr =
588 (void* (*)(const gchar*, GtkWindow*, GtkFileChooserAction, const gchar*,
589 const gchar*))dlsym(RTLD_DEFAULT,
590 "gtk_file_chooser_native_new");
591 if (mUseNativeFileChooser && sGtkFileChooserNativeNewPtr != nullptr) {
592 return (*sGtkFileChooserNativeNewPtr)(title, parent, action, accept_label,
593 nullptr);
595 if (accept_label == nullptr) {
596 accept_label = (action == GTK_FILE_CHOOSER_ACTION_SAVE) ? GTK_STOCK_SAVE
597 : GTK_STOCK_OPEN;
599 GtkWidget* file_chooser = gtk_file_chooser_dialog_new(
600 title, parent, action, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
601 accept_label, GTK_RESPONSE_ACCEPT, nullptr);
602 gtk_dialog_set_alternative_button_order(
603 GTK_DIALOG(file_chooser), GTK_RESPONSE_ACCEPT, GTK_RESPONSE_CANCEL, -1);
604 return file_chooser;
607 void nsFilePicker::GtkFileChooserShow(void* file_chooser) {
608 static auto sGtkNativeDialogShowPtr =
609 (void (*)(void*))dlsym(RTLD_DEFAULT, "gtk_native_dialog_show");
610 if (mUseNativeFileChooser && sGtkNativeDialogShowPtr != nullptr) {
611 const char* portalEnvString = g_getenv("GTK_USE_PORTAL");
612 bool setPortalEnv =
613 (portalEnvString && *portalEnvString == '0') || !portalEnvString;
614 if (setPortalEnv) {
615 setenv("GTK_USE_PORTAL", "1", true);
617 (*sGtkNativeDialogShowPtr)(file_chooser);
618 if (setPortalEnv) {
619 unsetenv("GTK_USE_PORTAL");
621 } else {
622 g_signal_connect(file_chooser, "destroy", G_CALLBACK(OnDestroy), this);
623 gtk_widget_show(GTK_WIDGET(file_chooser));
627 void nsFilePicker::GtkFileChooserDestroy(void* file_chooser) {
628 static auto sGtkNativeDialogDestroyPtr =
629 (void (*)(void*))dlsym(RTLD_DEFAULT, "gtk_native_dialog_destroy");
630 if (mUseNativeFileChooser && sGtkNativeDialogDestroyPtr != nullptr) {
631 (*sGtkNativeDialogDestroyPtr)(file_chooser);
632 } else {
633 gtk_widget_destroy(GTK_WIDGET(file_chooser));
637 void nsFilePicker::GtkFileChooserSetModal(void* file_chooser,
638 GtkWindow* parent_widget,
639 gboolean modal) {
640 static auto sGtkNativeDialogSetModalPtr = (void (*)(void*, gboolean))dlsym(
641 RTLD_DEFAULT, "gtk_native_dialog_set_modal");
642 if (mUseNativeFileChooser && sGtkNativeDialogSetModalPtr != nullptr) {
643 (*sGtkNativeDialogSetModalPtr)(file_chooser, modal);
644 } else {
645 GtkWindow* window = GTK_WINDOW(file_chooser);
646 gtk_window_set_modal(window, modal);
647 if (parent_widget != nullptr) {
648 gtk_window_set_destroy_with_parent(window, modal);