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/. */
12 #include "mozilla/Types.h"
13 #include "nsGtkUtils.h"
14 #include "nsIFileURL.h"
15 #include "nsIGIOService.h"
17 #include "nsIWidget.h"
19 #include "mozilla/Preferences.h"
21 #include "nsArrayEnumerator.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"
33 # include "mozilla/Logging.h"
34 # include "nsTArray.h"
36 extern mozilla::LazyLogModule gWidgetLog
;
37 # define LOG(args) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, args)
40 #endif /* MOZ_LOGGING */
42 using namespace mozilla
;
44 #define MAX_PREVIEW_SIZE 180
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
;
56 case nsIFilePicker::modeSave
:
57 action
= GTK_FILE_CHOOSER_ACTION_SAVE
;
60 case nsIFilePicker::modeGetFolder
:
61 action
= GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER
;
64 case nsIFilePicker::modeOpen
:
65 case nsIFilePicker::modeOpenMultiple
:
66 action
= GTK_FILE_CHOOSER_ACTION_OPEN
;
70 NS_WARNING("Unknown nsIFilePicker mode");
71 action
= GTK_FILE_CHOOSER_ACTION_OPEN
;
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
);
84 if (!image_filename
) {
85 gtk_file_chooser_set_preview_widget_active(file_chooser
, FALSE
);
89 gint preview_width
= 0;
90 gint preview_height
= 0;
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
);
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);
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
);
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
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
) {
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
]);
156 // add the lowercase and uppercase version of a character to a bracket
157 // match, so it matches either the lowercase or uppercase char.
159 result
.Append(g_ascii_tolower(aPattern
[i
]));
160 result
.Append(g_ascii_toupper(aPattern
[i
]));
167 NS_IMPL_ISUPPORTS(nsFilePicker
, nsIFilePicker
)
169 nsFilePicker::nsFilePicker()
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
;
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
);
193 void nsFilePicker::ReadValuesFromFileChooser(void* file_chooser
) {
196 if (mMode
== nsIFilePicker::modeOpenMultiple
) {
200 gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(file_chooser
));
201 g_slist_foreach(list
, ReadMultipleFiles
, static_cast<gpointer
>(&mFiles
));
204 gchar
* filename
= gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(file_chooser
));
205 mFileURL
.Assign(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
));
221 nsCOMPtr
<nsIFile
> dir
;
222 file
->GetParent(getter_AddRefs(dir
));
224 dir
.swap(mPrevDisplayDirectory
);
229 void nsFilePicker::InitNative(nsIWidget
* aParent
, const nsAString
& aTitle
) {
230 mParentWidget
= aParent
;
231 mTitle
.Assign(aTitle
);
235 nsFilePicker::AppendFilters(int32_t aFilterMask
) {
236 mAllowURLs
= !!(aFilterMask
& filterAllowURLs
);
237 return nsBaseFilePicker::AppendFilters(aFilterMask
);
241 nsFilePicker::AppendFilter(const nsAString
& aTitle
, const nsAString
& aFilter
) {
242 if (aFilter
.EqualsLiteral("..apps")) {
243 // No platform specific thing we can do here, really....
247 nsAutoCString filter
, name
;
248 CopyUTF16toUTF8(aFilter
, filter
);
249 CopyUTF16toUTF8(aTitle
, name
);
251 mFilters
.AppendElement(filter
);
252 mFilterNames
.AppendElement(name
);
258 nsFilePicker::SetDefaultString(const nsAString
& aString
) {
265 nsFilePicker::GetDefaultString(nsAString
& aString
) {
267 return NS_ERROR_FAILURE
;
271 nsFilePicker::SetDefaultExtension(const nsAString
& aExtension
) {
272 mDefaultExtension
= aExtension
;
278 nsFilePicker::GetDefaultExtension(nsAString
& aExtension
) {
279 aExtension
= mDefaultExtension
;
285 nsFilePicker::GetFilterIndex(int32_t* aFilterIndex
) {
286 *aFilterIndex
= mSelectedType
;
292 nsFilePicker::SetFilterIndex(int32_t aFilterIndex
) {
293 mSelectedType
= aFilterIndex
;
299 nsFilePicker::GetFile(nsIFile
** aFile
) {
300 NS_ENSURE_ARG_POINTER(aFile
);
303 nsCOMPtr
<nsIURI
> uri
;
304 nsresult rv
= GetFileURL(getter_AddRefs(uri
));
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
);
319 nsFilePicker::GetFileURL(nsIURI
** aFileURL
) {
321 return NS_NewURI(aFileURL
, mFileURL
);
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
;
342 g_main_context_iteration(nullptr, TRUE
);
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();
366 accept_button
= nullptr;
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
376 gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(file_chooser
), FALSE
);
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
),
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
);
393 case nsIFilePicker::modeOpenMultiple
:
394 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(file_chooser
),
397 case nsIFilePicker::modeSave
:
398 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser
),
403 nsCOMPtr
<nsIFile
> defaultPath
;
404 if (mDisplayDirectory
) {
405 mDisplayDirectory
->Clone(getter_AddRefs(defaultPath
));
406 } else if (mPrevDisplayDirectory
) {
407 mPrevDisplayDirectory
->Clone(getter_AddRefs(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
);
416 defaultPath
->GetNativePath(path
);
417 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(file_chooser
), path
.get());
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(
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
),
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);
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
);
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
),
495 mCallback
= aCallback
;
497 g_signal_connect(file_chooser
, "response", G_CALLBACK(OnResponse
), this);
498 GtkFileChooserShow(file_chooser
);
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
);
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
) {
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
));
529 file
->Exists(&exists
);
530 if (exists
) result
= nsIFilePicker::returnReplace
;
535 case GTK_RESPONSE_CANCEL
:
536 case GTK_RESPONSE_CLOSE
:
537 case GTK_RESPONSE_DELETE_EVENT
:
538 result
= nsIFilePicker::returnCancel
;
542 NS_WARNING("Unexpected response");
543 result
= nsIFilePicker::returnCancel
;
547 // A "response" signal won't be sent again but "destroy" will be.
548 g_signal_handlers_disconnect_by_func(file_chooser
, FuncToGpointer(OnDestroy
),
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
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
566 [](gpointer data
) -> gboolean
{
567 g_object_unref(data
);
568 return G_SOURCE_REMOVE
;
570 mFileChooserDelegate
);
571 mFileChooserDelegate
= nullptr;
575 mCallback
->Done(result
);
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
,
595 if (accept_label
== nullptr) {
596 accept_label
= (action
== GTK_FILE_CHOOSER_ACTION_SAVE
) ? GTK_STOCK_SAVE
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);
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");
613 (portalEnvString
&& *portalEnvString
== '0') || !portalEnvString
;
615 setenv("GTK_USE_PORTAL", "1", true);
617 (*sGtkNativeDialogShowPtr
)(file_chooser
);
619 unsetenv("GTK_USE_PORTAL");
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
);
633 gtk_widget_destroy(GTK_WIDGET(file_chooser
));
637 void nsFilePicker::GtkFileChooserSetModal(void* file_chooser
,
638 GtkWindow
* parent_widget
,
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
);
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
);