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/. */
5 
6 #include <dlfcn.h>
7 #include <gtk/gtk.h>
8 #include <sys/types.h>
9 #include <sys/stat.h>
10 #include <unistd.h>
11 
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"
20 
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"
28 
29 #include "nsFilePicker.h"
30 
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 */
41 
42 using namespace mozilla;
43 
44 #define MAX_PREVIEW_SIZE 180
45 // bug 1184009
46 #define MAX_PREVIEW_SOURCE_SIZE 4096
47 
48 nsIFile* nsFilePicker::mPrevDisplayDirectory = nullptr;
49 
Shutdown()50 void nsFilePicker::Shutdown() { NS_IF_RELEASE(mPrevDisplayDirectory); }
51 
GetGtkFileChooserAction(int16_t aMode)52 static GtkFileChooserAction GetGtkFileChooserAction(int16_t aMode) {
53   GtkFileChooserAction action;
54 
55   switch (aMode) {
56     case nsIFilePicker::modeSave:
57       action = GTK_FILE_CHOOSER_ACTION_SAVE;
58       break;
59 
60     case nsIFilePicker::modeGetFolder:
61       action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
62       break;
63 
64     case nsIFilePicker::modeOpen:
65     case nsIFilePicker::modeOpenMultiple:
66       action = GTK_FILE_CHOOSER_ACTION_OPEN;
67       break;
68 
69     default:
70       NS_WARNING("Unknown nsIFilePicker mode");
71       action = GTK_FILE_CHOOSER_ACTION_OPEN;
72       break;
73   }
74 
75   return action;
76 }
77 
UpdateFilePreviewWidget(GtkFileChooser * file_chooser,gpointer preview_widget_voidptr)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;
83 
84   if (!image_filename) {
85     gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
86     return;
87   }
88 
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 */
99   }
100 
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;
109   }
110 
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);
118   }
119 
120   g_free(image_filename);
121 
122   if (!preview_pixbuf) {
123     gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
124     return;
125   }
126 
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);
130 
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);
137 
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);
141 }
142 
MakeCaseInsensitiveShellGlob(const char * aPattern)143 static nsAutoCString MakeCaseInsensitiveShellGlob(const char* aPattern) {
144   // aPattern is UTF8
145   nsAutoCString result;
146   unsigned int len = strlen(aPattern);
147 
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;
154     }
155 
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(']');
162   }
163 
164   return result;
165 }
166 
NS_IMPL_ISUPPORTS(nsFilePicker,nsIFilePicker)167 NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
168 
169 nsFilePicker::nsFilePicker()
170     : mSelectedType(0),
171       mRunning(false),
172       mAllowURLs(false),
173       mFileChooserDelegate(nullptr) {
174   mUseNativeFileChooser =
175       widget::ShouldUsePortal(widget::PortalKind::FilePicker);
176 }
177 
178 nsFilePicker::~nsFilePicker() = default;
179 
ReadMultipleFiles(gpointer filename,gpointer array)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);
188   }
189 
190   g_free(filename);
191 }
192 
ReadValuesFromFileChooser(void * file_chooser)193 void nsFilePicker::ReadValuesFromFileChooser(void* file_chooser) {
194   mFiles.Clear();
195 
196   if (mMode == nsIFilePicker::modeOpenMultiple) {
197     mFileURL.Truncate();
198 
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);
207   }
208 
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));
213 
214   mSelectedType = static_cast<int16_t>(g_slist_index(filter_list, filter));
215   g_slist_free(filter_list);
216 
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);
225     }
226   }
227 }
228 
InitNative(nsIWidget * aParent,const nsAString & aTitle)229 void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
230   mParentWidget = aParent;
231   mTitle.Assign(aTitle);
232 }
233 
234 NS_IMETHODIMP
AppendFilters(int32_t aFilterMask)235 nsFilePicker::AppendFilters(int32_t aFilterMask) {
236   mAllowURLs = !!(aFilterMask & filterAllowURLs);
237   return nsBaseFilePicker::AppendFilters(aFilterMask);
238 }
239 
240 NS_IMETHODIMP
AppendFilter(const nsAString & aTitle,const nsAString & aFilter)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;
245   }
246 
247   nsAutoCString filter, name;
248   CopyUTF16toUTF8(aFilter, filter);
249   CopyUTF16toUTF8(aTitle, name);
250 
251   mFilters.AppendElement(filter);
252   mFilterNames.AppendElement(name);
253 
254   return NS_OK;
255 }
256 
257 NS_IMETHODIMP
SetDefaultString(const nsAString & aString)258 nsFilePicker::SetDefaultString(const nsAString& aString) {
259   mDefault = aString;
260 
261   return NS_OK;
262 }
263 
264 NS_IMETHODIMP
GetDefaultString(nsAString & aString)265 nsFilePicker::GetDefaultString(nsAString& aString) {
266   // Per API...
267   return NS_ERROR_FAILURE;
268 }
269 
270 NS_IMETHODIMP
SetDefaultExtension(const nsAString & aExtension)271 nsFilePicker::SetDefaultExtension(const nsAString& aExtension) {
272   mDefaultExtension = aExtension;
273 
274   return NS_OK;
275 }
276 
277 NS_IMETHODIMP
GetDefaultExtension(nsAString & aExtension)278 nsFilePicker::GetDefaultExtension(nsAString& aExtension) {
279   aExtension = mDefaultExtension;
280 
281   return NS_OK;
282 }
283 
284 NS_IMETHODIMP
GetFilterIndex(int32_t * aFilterIndex)285 nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
286   *aFilterIndex = mSelectedType;
287 
288   return NS_OK;
289 }
290 
291 NS_IMETHODIMP
SetFilterIndex(int32_t aFilterIndex)292 nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
293   mSelectedType = aFilterIndex;
294 
295   return NS_OK;
296 }
297 
298 NS_IMETHODIMP
GetFile(nsIFile ** aFile)299 nsFilePicker::GetFile(nsIFile** aFile) {
300   NS_ENSURE_ARG_POINTER(aFile);
301 
302   *aFile = nullptr;
303   nsCOMPtr<nsIURI> uri;
304   nsresult rv = GetFileURL(getter_AddRefs(uri));
305   if (!uri) return rv;
306 
307   nsCOMPtr<nsIFileURL> fileURL(do_QueryInterface(uri, &rv));
308   NS_ENSURE_SUCCESS(rv, rv);
309 
310   nsCOMPtr<nsIFile> file;
311   rv = fileURL->GetFile(getter_AddRefs(file));
312   NS_ENSURE_SUCCESS(rv, rv);
313 
314   file.forget(aFile);
315   return NS_OK;
316 }
317 
318 NS_IMETHODIMP
GetFileURL(nsIURI ** aFileURL)319 nsFilePicker::GetFileURL(nsIURI** aFileURL) {
320   *aFileURL = nullptr;
321   return NS_NewURI(aFileURL, mFileURL);
322 }
323 
324 NS_IMETHODIMP
GetFiles(nsISimpleEnumerator ** aFiles)325 nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
326   NS_ENSURE_ARG_POINTER(aFiles);
327 
328   if (mMode == nsIFilePicker::modeOpenMultiple) {
329     return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
330   }
331 
332   return NS_ERROR_FAILURE;
333 }
334 
Show(int16_t * aReturn)335 nsresult nsFilePicker::Show(int16_t* aReturn) {
336   NS_ENSURE_ARG_POINTER(aReturn);
337 
338   nsresult rv = Open(nullptr);
339   if (NS_FAILED(rv)) return rv;
340 
341   while (mRunning) {
342     g_main_context_iteration(nullptr, TRUE);
343   }
344 
345   *aReturn = mResult;
346   return NS_OK;
347 }
348 
349 NS_IMETHODIMP
Open(nsIFilePickerShownCallback * aCallback)350 nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) {
351   // Can't show two dialogs concurrently with the same filepicker
352   if (mRunning) return NS_ERROR_NOT_AVAILABLE;
353 
354   NS_ConvertUTF16toUTF8 title(mTitle);
355 
356   GtkWindow* parent_widget =
357       GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET));
358 
359   GtkFileChooserAction action = GetGtkFileChooserAction(mMode);
360 
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;
367   }
368 
369   void* file_chooser =
370       GtkFileChooserNew(title.get(), parent_widget, action, accept_button);
371 
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);
377   }
378 #endif
379 
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);
387   }
388 
389   GtkFileChooserSetModal(file_chooser, parent_widget, TRUE);
390 
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;
401   }
402 
403   nsCOMPtr<nsIFile> defaultPath;
404   if (mDisplayDirectory) {
405     mDisplayDirectory->Clone(getter_AddRefs(defaultPath));
406   } else if (mPrevDisplayDirectory) {
407     mPrevDisplayDirectory->Clone(getter_AddRefs(defaultPath));
408   }
409 
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);
421 
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);
436               }
437             },
438             &mFileChooserDelegate);
439 
440         if (mFileChooserDelegate != nullptr) {
441           g_object_ref(mFileChooserDelegate);
442         }
443       }
444       gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(file_chooser),
445                                           directory.get());
446     }
447   }
448 
449   if (GTK_IS_DIALOG(file_chooser)) {
450     gtk_dialog_set_default_response(GTK_DIALOG(file_chooser),
451                                     GTK_RESPONSE_ACCEPT);
452   }
453 
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.
458 
459     char** patterns = g_strsplit(mFilters[i].get(), ";", -1);
460     if (!patterns) {
461       return NS_ERROR_OUT_OF_MEMORY;
462     }
463 
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());
469     }
470 
471     g_strfreev(patterns);
472 
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);
481     }
482 
483     gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter);
484 
485     // Set the initially selected filter
486     if (mSelectedType == i) {
487       gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(file_chooser), filter);
488     }
489   }
490 
491   gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(file_chooser),
492                                                  TRUE);
493 
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);
499 
500   return NS_OK;
501 }
502 
503 /* static */
OnResponse(void * file_chooser,gint response_id,gpointer user_data)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);
507 }
508 
509 /* static */
OnDestroy(GtkWidget * file_chooser,gpointer user_data)510 void nsFilePicker::OnDestroy(GtkWidget* file_chooser, gpointer user_data) {
511   static_cast<nsFilePicker*>(user_data)->Done(file_chooser,
512                                               GTK_RESPONSE_CANCEL);
513 }
514 
Done(void * file_chooser,gint response)515 void nsFilePicker::Done(void* file_chooser, gint response) {
516   mRunning = false;
517 
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;
531         }
532       }
533       break;
534 
535     case GTK_RESPONSE_CANCEL:
536     case GTK_RESPONSE_CLOSE:
537     case GTK_RESPONSE_DELETE_EVENT:
538       result = nsIFilePicker::returnCancel;
539       break;
540 
541     default:
542       NS_WARNING("Unexpected response");
543       result = nsIFilePicker::returnCancel;
544       break;
545   }
546 
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);
550 
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);
558 
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;
569         },
570         mFileChooserDelegate);
571     mFileChooserDelegate = nullptr;
572   }
573 
574   if (mCallback) {
575     mCallback->Done(result);
576     mCallback = nullptr;
577   } else {
578     mResult = result;
579   }
580   NS_RELEASE_THIS();
581 }
582 
583 // All below functions available as of GTK 3.20+
GtkFileChooserNew(const gchar * title,GtkWindow * parent,GtkFileChooserAction action,const gchar * accept_label)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);
594   }
595   if (accept_label == nullptr) {
596     accept_label = (action == GTK_FILE_CHOOSER_ACTION_SAVE) ? GTK_STOCK_SAVE
597                                                             : GTK_STOCK_OPEN;
598   }
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;
605 }
606 
GtkFileChooserShow(void * 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);
616     }
617     (*sGtkNativeDialogShowPtr)(file_chooser);
618     if (setPortalEnv) {
619       unsetenv("GTK_USE_PORTAL");
620     }
621   } else {
622     g_signal_connect(file_chooser, "destroy", G_CALLBACK(OnDestroy), this);
623     gtk_widget_show(GTK_WIDGET(file_chooser));
624   }
625 }
626 
GtkFileChooserDestroy(void * 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));
634   }
635 }
636 
GtkFileChooserSetModal(void * file_chooser,GtkWindow * parent_widget,gboolean modal)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);
649     }
650   }
651 }
652