Source code

Revision control

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <dlfcn.h>
#include <gtk/gtk.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "mozilla/Types.h"
#include "nsGtkUtils.h"
#include "nsIFileURL.h"
#include "nsIGIOService.h"
#include "nsIURI.h"
#include "nsIWidget.h"
#include "nsIFile.h"
#include "mozilla/Preferences.h"
#include "nsArrayEnumerator.h"
#include "nsMemory.h"
#include "nsEnumeratorUtils.h"
#include "nsNetUtil.h"
#include "nsReadableUtils.h"
#include "MozContainer.h"
#include "WidgetUtilsGtk.h"
#include "nsFilePicker.h"
#undef LOG
#ifdef MOZ_LOGGING
# include "mozilla/Logging.h"
# include "nsTArray.h"
# include "Units.h"
extern mozilla::LazyLogModule gWidgetLog;
# define LOG(args) MOZ_LOG(gWidgetLog, mozilla::LogLevel::Debug, args)
#else
# define LOG(args)
#endif /* MOZ_LOGGING */
using namespace mozilla;
#define MAX_PREVIEW_SIZE 180
#define MAX_PREVIEW_SOURCE_SIZE 4096
nsIFile* nsFilePicker::mPrevDisplayDirectory = nullptr;
void nsFilePicker::Shutdown() { NS_IF_RELEASE(mPrevDisplayDirectory); }
static GtkFileChooserAction GetGtkFileChooserAction(int16_t aMode) {
GtkFileChooserAction action;
switch (aMode) {
case nsIFilePicker::modeSave:
action = GTK_FILE_CHOOSER_ACTION_SAVE;
break;
case nsIFilePicker::modeGetFolder:
action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
break;
case nsIFilePicker::modeOpen:
case nsIFilePicker::modeOpenMultiple:
action = GTK_FILE_CHOOSER_ACTION_OPEN;
break;
default:
NS_WARNING("Unknown nsIFilePicker mode");
action = GTK_FILE_CHOOSER_ACTION_OPEN;
break;
}
return action;
}
static void UpdateFilePreviewWidget(GtkFileChooser* file_chooser,
gpointer preview_widget_voidptr) {
GtkImage* preview_widget = GTK_IMAGE(preview_widget_voidptr);
char* image_filename = gtk_file_chooser_get_preview_filename(file_chooser);
struct stat st_buf;
if (!image_filename) {
gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
return;
}
gint preview_width = 0;
gint preview_height = 0;
/* check type of file
* if file is named pipe, Open is blocking which may lead to UI
* nonresponsiveness; if file is directory/socket, it also isn't
* likely to get preview */
if (stat(image_filename, &st_buf) || (!S_ISREG(st_buf.st_mode))) {
g_free(image_filename);
gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
return; /* stat failed or file is not regular */
}
GdkPixbufFormat* preview_format =
gdk_pixbuf_get_file_info(image_filename, &preview_width, &preview_height);
if (!preview_format || preview_width <= 0 || preview_height <= 0 ||
preview_width > MAX_PREVIEW_SOURCE_SIZE ||
preview_height > MAX_PREVIEW_SOURCE_SIZE) {
g_free(image_filename);
gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
return;
}
GdkPixbuf* preview_pixbuf = nullptr;
// Only scale down images that are too big
if (preview_width > MAX_PREVIEW_SIZE || preview_height > MAX_PREVIEW_SIZE) {
preview_pixbuf = gdk_pixbuf_new_from_file_at_size(
image_filename, MAX_PREVIEW_SIZE, MAX_PREVIEW_SIZE, nullptr);
} else {
preview_pixbuf = gdk_pixbuf_new_from_file(image_filename, nullptr);
}
g_free(image_filename);
if (!preview_pixbuf) {
gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
return;
}
GdkPixbuf* preview_pixbuf_temp = preview_pixbuf;
preview_pixbuf = gdk_pixbuf_apply_embedded_orientation(preview_pixbuf_temp);
g_object_unref(preview_pixbuf_temp);
// This is the easiest way to do center alignment without worrying about
// containers Minimum 3px padding each side (hence the 6) just to make things
// nice
gint x_padding =
(MAX_PREVIEW_SIZE + 6 - gdk_pixbuf_get_width(preview_pixbuf)) / 2;
gtk_misc_set_padding(GTK_MISC(preview_widget), x_padding, 0);
gtk_image_set_from_pixbuf(preview_widget, preview_pixbuf);
g_object_unref(preview_pixbuf);
gtk_file_chooser_set_preview_widget_active(file_chooser, TRUE);
}
static nsAutoCString MakeCaseInsensitiveShellGlob(const char* aPattern) {
// aPattern is UTF8
nsAutoCString result;
unsigned int len = strlen(aPattern);
for (unsigned int i = 0; i < len; i++) {
if (!g_ascii_isalpha(aPattern[i])) {
// non-ASCII characters will also trigger this path, so unicode
// is safely handled albeit case-sensitively
result.Append(aPattern[i]);
continue;
}
// add the lowercase and uppercase version of a character to a bracket
// match, so it matches either the lowercase or uppercase char.
result.Append('[');
result.Append(g_ascii_tolower(aPattern[i]));
result.Append(g_ascii_toupper(aPattern[i]));
result.Append(']');
}
return result;
}
NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
nsFilePicker::nsFilePicker()
: mSelectedType(0),
mRunning(false),
mAllowURLs(false),
mFileChooserDelegate(nullptr) {
nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
// Due to Bug 1635718 always use portal for file dialog on Wayland.
if (widget::GdkIsWaylandDisplay()) {
mUseNativeFileChooser =
Preferences::GetBool("widget.use-xdg-desktop-portal", true);
} else {
giovfs->ShouldUseFlatpakPortal(&mUseNativeFileChooser);
}
}
nsFilePicker::~nsFilePicker() = default;
void ReadMultipleFiles(gpointer filename, gpointer array) {
nsCOMPtr<nsIFile> localfile;
nsresult rv =
NS_NewNativeLocalFile(nsDependentCString(static_cast<char*>(filename)),
false, getter_AddRefs(localfile));
if (NS_SUCCEEDED(rv)) {
nsCOMArray<nsIFile>& files = *static_cast<nsCOMArray<nsIFile>*>(array);
files.AppendObject(localfile);
}
g_free(filename);
}
void nsFilePicker::ReadValuesFromFileChooser(void* file_chooser) {
mFiles.Clear();
if (mMode == nsIFilePicker::modeOpenMultiple) {
mFileURL.Truncate();
GSList* list =
gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(file_chooser));
g_slist_foreach(list, ReadMultipleFiles, static_cast<gpointer>(&mFiles));
g_slist_free(list);
} else {
gchar* filename = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(file_chooser));
mFileURL.Assign(filename);
g_free(filename);
}
GtkFileFilter* filter =
gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(file_chooser));
GSList* filter_list =
gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(file_chooser));
mSelectedType = static_cast<int16_t>(g_slist_index(filter_list, filter));
g_slist_free(filter_list);
// Remember last used directory.
nsCOMPtr<nsIFile> file;
GetFile(getter_AddRefs(file));
if (file) {
nsCOMPtr<nsIFile> dir;
file->GetParent(getter_AddRefs(dir));
if (dir) {
dir.swap(mPrevDisplayDirectory);
}
}
}
void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
mParentWidget = aParent;
mTitle.Assign(aTitle);
}
NS_IMETHODIMP
nsFilePicker::AppendFilters(int32_t aFilterMask) {
mAllowURLs = !!(aFilterMask & filterAllowURLs);
return nsBaseFilePicker::AppendFilters(aFilterMask);
}
NS_IMETHODIMP
nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) {
if (aFilter.EqualsLiteral("..apps")) {
// No platform specific thing we can do here, really....
return NS_OK;
}
nsAutoCString filter, name;
CopyUTF16toUTF8(aFilter, filter);
CopyUTF16toUTF8(aTitle, name);
mFilters.AppendElement(filter);
mFilterNames.AppendElement(name);
return NS_OK;
}
NS_IMETHODIMP
nsFilePicker::SetDefaultString(const nsAString& aString) {
mDefault = aString;
return NS_OK;
}
NS_IMETHODIMP
nsFilePicker::GetDefaultString(nsAString& aString) {
// Per API...
return NS_ERROR_FAILURE;
}
NS_IMETHODIMP
nsFilePicker::SetDefaultExtension(const nsAString& aExtension) {
mDefaultExtension = aExtension;
return NS_OK;
}
NS_IMETHODIMP
nsFilePicker::GetDefaultExtension(nsAString& aExtension) {
aExtension = mDefaultExtension;
return NS_OK;
}
NS_IMETHODIMP
nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
*aFilterIndex = mSelectedType;
return NS_OK;
}
NS_IMETHODIMP
nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
mSelectedType = aFilterIndex;
return NS_OK;
}
NS_IMETHODIMP
nsFilePicker::GetFile(nsIFile** aFile) {
NS_ENSURE_ARG_POINTER(aFile);
*aFile = nullptr;
nsCOMPtr<nsIURI> uri;
nsresult rv = GetFileURL(getter_AddRefs(uri));
if (!uri) return rv;
nsCOMPtr<nsIFileURL> fileURL(do_QueryInterface(uri, &rv));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> file;
rv = fileURL->GetFile(getter_AddRefs(file));
NS_ENSURE_SUCCESS(rv, rv);
file.forget(aFile);
return NS_OK;
}
NS_IMETHODIMP
nsFilePicker::GetFileURL(nsIURI** aFileURL) {
*aFileURL = nullptr;
return NS_NewURI(aFileURL, mFileURL);
}
NS_IMETHODIMP
nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
NS_ENSURE_ARG_POINTER(aFiles);
if (mMode == nsIFilePicker::modeOpenMultiple) {
return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
}
return NS_ERROR_FAILURE;
}
nsresult nsFilePicker::Show(int16_t* aReturn) {
NS_ENSURE_ARG_POINTER(aReturn);
nsresult rv = Open(nullptr);
if (NS_FAILED(rv)) return rv;
while (mRunning) {
g_main_context_iteration(nullptr, TRUE);
}
*aReturn = mResult;
return NS_OK;
}
NS_IMETHODIMP
nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) {
// Can't show two dialogs concurrently with the same filepicker
if (mRunning) return NS_ERROR_NOT_AVAILABLE;
NS_ConvertUTF16toUTF8 title(mTitle);
GtkWindow* parent_widget =
GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET));
GtkFileChooserAction action = GetGtkFileChooserAction(mMode);
const gchar* accept_button;
NS_ConvertUTF16toUTF8 buttonLabel(mOkButtonLabel);
if (!mOkButtonLabel.IsEmpty()) {
accept_button = buttonLabel.get();
} else {
accept_button = nullptr;
}
void* file_chooser =
GtkFileChooserNew(title.get(), parent_widget, action, accept_button);
// If we have --enable-proxy-bypass-protection, then don't allow
// remote URLs to be used.
#ifndef MOZ_PROXY_BYPASS_PROTECTION
if (mAllowURLs) {
gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(file_chooser), FALSE);
}
#endif
if (action == GTK_FILE_CHOOSER_ACTION_OPEN ||
action == GTK_FILE_CHOOSER_ACTION_SAVE) {
GtkWidget* img_preview = gtk_image_new();
gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(file_chooser),
img_preview);
g_signal_connect(file_chooser, "update-preview",
G_CALLBACK(UpdateFilePreviewWidget), img_preview);
}
GtkFileChooserSetModal(file_chooser, parent_widget, TRUE);
NS_ConvertUTF16toUTF8 defaultName(mDefault);
switch (mMode) {
case nsIFilePicker::modeOpenMultiple:
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(file_chooser),
TRUE);
break;
case nsIFilePicker::modeSave:
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser),
defaultName.get());
break;
}
nsCOMPtr<nsIFile> defaultPath;
if (mDisplayDirectory) {
mDisplayDirectory->Clone(getter_AddRefs(defaultPath));
} else if (mPrevDisplayDirectory) {
mPrevDisplayDirectory->Clone(getter_AddRefs(defaultPath));
}
if (defaultPath) {
if (!defaultName.IsEmpty() && mMode != nsIFilePicker::modeSave) {
// Try to select the intended file. Even if it doesn't exist, GTK still
// switches directories.
defaultPath->AppendNative(defaultName);
nsAutoCString path;
defaultPath->GetNativePath(path);
gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(file_chooser), path.get());
} else {
nsAutoCString directory;
defaultPath->GetNativePath(directory);
// Workaround for problematic refcounting in GTK3 before 3.16.
// We need to keep a reference to the dialog's internal delegate.
// Otherwise, if our dialog gets destroyed, we'll lose the dialog's
// delegate by the time this gets processed in the event loop.
if (GTK_IS_DIALOG(file_chooser)) {
GtkDialog* dialog = GTK_DIALOG(file_chooser);
GtkContainer* area = GTK_CONTAINER(gtk_dialog_get_content_area(dialog));
gtk_container_forall(
area,
[](GtkWidget* widget, gpointer data) {
if (GTK_IS_FILE_CHOOSER_WIDGET(widget)) {
auto result = static_cast<GtkFileChooserWidget**>(data);
*result = GTK_FILE_CHOOSER_WIDGET(widget);
}
},
&mFileChooserDelegate);
if (mFileChooserDelegate != nullptr) {
g_object_ref(mFileChooserDelegate);
}
}
gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(file_chooser),
directory.get());
}
}
if (GTK_IS_DIALOG(file_chooser)) {
gtk_dialog_set_default_response(GTK_DIALOG(file_chooser),
GTK_RESPONSE_ACCEPT);
}
int32_t count = mFilters.Length();
for (int32_t i = 0; i < count; ++i) {
// This is fun... the GTK file picker does not accept a list of filters
// so we need to split out each string, and add it manually.
char** patterns = g_strsplit(mFilters[i].get(), ";", -1);
if (!patterns) {
return NS_ERROR_OUT_OF_MEMORY;
}
GtkFileFilter* filter = gtk_file_filter_new();
for (int j = 0; patterns[j] != nullptr; ++j) {
nsAutoCString caseInsensitiveFilter =
MakeCaseInsensitiveShellGlob(g_strstrip(patterns[j]));
gtk_file_filter_add_pattern(filter, caseInsensitiveFilter.get());
}
g_strfreev(patterns);
if (!mFilterNames[i].IsEmpty()) {
// If we have a name for our filter, let's use that.
const char* filter_name = mFilterNames[i].get();
gtk_file_filter_set_name(filter, filter_name);
} else {
// If we don't have a name, let's just use the filter pattern.
const char* filter_pattern = mFilters[i].get();
gtk_file_filter_set_name(filter, filter_pattern);
}
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter);
// Set the initially selected filter
if (mSelectedType == i) {
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(file_chooser), filter);
}
}
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(file_chooser),
TRUE);
mRunning = true;
mCallback = aCallback;
NS_ADDREF_THIS();
g_signal_connect(file_chooser, "response", G_CALLBACK(OnResponse), this);
GtkFileChooserShow(file_chooser);
return NS_OK;
}
/* static */
void nsFilePicker::OnResponse(void* file_chooser, gint response_id,
gpointer user_data) {
static_cast<nsFilePicker*>(user_data)->Done(file_chooser, response_id);
}
/* static */
void nsFilePicker::OnDestroy(GtkWidget* file_chooser, gpointer user_data) {
static_cast<nsFilePicker*>(user_data)->Done(file_chooser,
GTK_RESPONSE_CANCEL);
}
void nsFilePicker::Done(void* file_chooser, gint response) {
mRunning = false;
int16_t result;
switch (response) {
case GTK_RESPONSE_OK:
case GTK_RESPONSE_ACCEPT:
ReadValuesFromFileChooser(file_chooser);
result = nsIFilePicker::returnOK;
if (mMode == nsIFilePicker::modeSave) {
nsCOMPtr<nsIFile> file;
GetFile(getter_AddRefs(file));
if (file) {
bool exists = false;
file->Exists(&exists);
if (exists) result = nsIFilePicker::returnReplace;
}
}
break;
case GTK_RESPONSE_CANCEL:
case GTK_RESPONSE_CLOSE:
case GTK_RESPONSE_DELETE_EVENT:
result = nsIFilePicker::returnCancel;
break;
default:
NS_WARNING("Unexpected response");
result = nsIFilePicker::returnCancel;
break;
}
// A "response" signal won't be sent again but "destroy" will be.
g_signal_handlers_disconnect_by_func(file_chooser, FuncToGpointer(OnDestroy),
this);
// When response_id is GTK_RESPONSE_DELETE_EVENT or when called from
// OnDestroy, the widget would be destroyed anyway but it is fine if
// gtk_widget_destroy is called more than once. gtk_widget_destroy has
// requests that any remaining references be released, but the reference
// count will not be decremented again if GtkWindow's reference has already
// been released.
GtkFileChooserDestroy(file_chooser);
if (mFileChooserDelegate) {
// Properly deref our acquired reference. We call this after
// gtk_widget_destroy() to try and ensure that pending file info
// queries caused by updating the current folder have been cancelled.
// However, we do not know for certain when the callback will run after
// cancelled.
g_idle_add(
[](gpointer data) -> gboolean {
g_object_unref(data);
return G_SOURCE_REMOVE;
},
mFileChooserDelegate);
mFileChooserDelegate = nullptr;
}
if (mCallback) {
mCallback->Done(result);
mCallback = nullptr;
} else {
mResult = result;
}
NS_RELEASE_THIS();
}
// All below functions available as of GTK 3.20+
void* nsFilePicker::GtkFileChooserNew(const gchar* title, GtkWindow* parent,
GtkFileChooserAction action,
const gchar* accept_label) {
static auto sGtkFileChooserNativeNewPtr =
(void* (*)(const gchar*, GtkWindow*, GtkFileChooserAction, const gchar*,
const gchar*))dlsym(RTLD_DEFAULT,
"gtk_file_chooser_native_new");
if (mUseNativeFileChooser && sGtkFileChooserNativeNewPtr != nullptr) {
return (*sGtkFileChooserNativeNewPtr)(title, parent, action, accept_label,
nullptr);
}
if (accept_label == nullptr) {
accept_label = (action == GTK_FILE_CHOOSER_ACTION_SAVE) ? GTK_STOCK_SAVE
: GTK_STOCK_OPEN;
}
GtkWidget* file_chooser = gtk_file_chooser_dialog_new(
title, parent, action, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
accept_label, GTK_RESPONSE_ACCEPT, nullptr);
gtk_dialog_set_alternative_button_order(
GTK_DIALOG(file_chooser), GTK_RESPONSE_ACCEPT, GTK_RESPONSE_CANCEL, -1);
return file_chooser;
}
void nsFilePicker::GtkFileChooserShow(void* file_chooser) {
static auto sGtkNativeDialogShowPtr =
(void (*)(void*))dlsym(RTLD_DEFAULT, "gtk_native_dialog_show");
if (mUseNativeFileChooser && sGtkNativeDialogShowPtr != nullptr) {
const char* portalEnvString = g_getenv("GTK_USE_PORTAL");
bool setPortalEnv =
(portalEnvString && *portalEnvString == '0') || !portalEnvString;
if (setPortalEnv) {
setenv("GTK_USE_PORTAL", "1", true);
}
(*sGtkNativeDialogShowPtr)(file_chooser);
if (setPortalEnv) {
unsetenv("GTK_USE_PORTAL");
}
} else {
g_signal_connect(file_chooser, "destroy", G_CALLBACK(OnDestroy), this);
gtk_widget_show(GTK_WIDGET(file_chooser));
}
}
void nsFilePicker::GtkFileChooserDestroy(void* file_chooser) {
static auto sGtkNativeDialogDestroyPtr =
(void (*)(void*))dlsym(RTLD_DEFAULT, "gtk_native_dialog_destroy");
if (mUseNativeFileChooser && sGtkNativeDialogDestroyPtr != nullptr) {
(*sGtkNativeDialogDestroyPtr)(file_chooser);
} else {
gtk_widget_destroy(GTK_WIDGET(file_chooser));
}
}
void nsFilePicker::GtkFileChooserSetModal(void* file_chooser,
GtkWindow* parent_widget,
gboolean modal) {
static auto sGtkNativeDialogSetModalPtr = (void (*)(void*, gboolean))dlsym(
RTLD_DEFAULT, "gtk_native_dialog_set_modal");
if (mUseNativeFileChooser && sGtkNativeDialogSetModalPtr != nullptr) {
(*sGtkNativeDialogSetModalPtr)(file_chooser, modal);
} else {
GtkWindow* window = GTK_WINDOW(file_chooser);
gtk_window_set_modal(window, modal);
if (parent_widget != nullptr) {
gtk_window_set_destroy_with_parent(window, modal);
}
}
}