michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "mozilla/Types.h" michael@0: michael@0: #include michael@0: michael@0: #include "nsGtkUtils.h" michael@0: #include "nsIFileURL.h" michael@0: #include "nsIURI.h" michael@0: #include "nsIWidget.h" michael@0: #include "nsIFile.h" michael@0: #include "nsIStringBundle.h" michael@0: michael@0: #include "nsArrayEnumerator.h" michael@0: #include "nsMemory.h" michael@0: #include "nsEnumeratorUtils.h" michael@0: #include "nsNetUtil.h" michael@0: #include "nsReadableUtils.h" michael@0: #include "mozcontainer.h" michael@0: michael@0: #include "nsFilePicker.h" michael@0: michael@0: using namespace mozilla; michael@0: michael@0: #define MAX_PREVIEW_SIZE 180 michael@0: michael@0: nsIFile *nsFilePicker::mPrevDisplayDirectory = nullptr; michael@0: michael@0: void michael@0: nsFilePicker::Shutdown() michael@0: { michael@0: NS_IF_RELEASE(mPrevDisplayDirectory); michael@0: } michael@0: michael@0: static GtkFileChooserAction michael@0: GetGtkFileChooserAction(int16_t aMode) michael@0: { michael@0: GtkFileChooserAction action; michael@0: michael@0: switch (aMode) { michael@0: case nsIFilePicker::modeSave: michael@0: action = GTK_FILE_CHOOSER_ACTION_SAVE; michael@0: break; michael@0: michael@0: case nsIFilePicker::modeGetFolder: michael@0: action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; michael@0: break; michael@0: michael@0: case nsIFilePicker::modeOpen: michael@0: case nsIFilePicker::modeOpenMultiple: michael@0: action = GTK_FILE_CHOOSER_ACTION_OPEN; michael@0: break; michael@0: michael@0: default: michael@0: NS_WARNING("Unknown nsIFilePicker mode"); michael@0: action = GTK_FILE_CHOOSER_ACTION_OPEN; michael@0: break; michael@0: } michael@0: michael@0: return action; michael@0: } michael@0: michael@0: michael@0: static void michael@0: UpdateFilePreviewWidget(GtkFileChooser *file_chooser, michael@0: gpointer preview_widget_voidptr) michael@0: { michael@0: GtkImage *preview_widget = GTK_IMAGE(preview_widget_voidptr); michael@0: char *image_filename = gtk_file_chooser_get_preview_filename(file_chooser); michael@0: michael@0: if (!image_filename) { michael@0: gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE); michael@0: return; michael@0: } michael@0: michael@0: gint preview_width = 0; michael@0: gint preview_height = 0; michael@0: GdkPixbufFormat *preview_format = gdk_pixbuf_get_file_info(image_filename, michael@0: &preview_width, michael@0: &preview_height); michael@0: if (!preview_format) { michael@0: g_free(image_filename); michael@0: gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE); michael@0: return; michael@0: } michael@0: michael@0: GdkPixbuf *preview_pixbuf; michael@0: // Only scale down images that are too big michael@0: if (preview_width > MAX_PREVIEW_SIZE || preview_height > MAX_PREVIEW_SIZE) { michael@0: preview_pixbuf = gdk_pixbuf_new_from_file_at_size(image_filename, michael@0: MAX_PREVIEW_SIZE, michael@0: MAX_PREVIEW_SIZE, michael@0: nullptr); michael@0: } michael@0: else { michael@0: preview_pixbuf = gdk_pixbuf_new_from_file(image_filename, nullptr); michael@0: } michael@0: michael@0: g_free(image_filename); michael@0: michael@0: if (!preview_pixbuf) { michael@0: gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE); michael@0: return; michael@0: } michael@0: michael@0: #if GTK_CHECK_VERSION(2,12,0) michael@0: GdkPixbuf *preview_pixbuf_temp = preview_pixbuf; michael@0: preview_pixbuf = gdk_pixbuf_apply_embedded_orientation(preview_pixbuf_temp); michael@0: g_object_unref(preview_pixbuf_temp); michael@0: #endif michael@0: michael@0: // This is the easiest way to do center alignment without worrying about containers michael@0: // Minimum 3px padding each side (hence the 6) just to make things nice michael@0: gint x_padding = (MAX_PREVIEW_SIZE + 6 - gdk_pixbuf_get_width(preview_pixbuf)) / 2; michael@0: gtk_misc_set_padding(GTK_MISC(preview_widget), x_padding, 0); michael@0: michael@0: gtk_image_set_from_pixbuf(preview_widget, preview_pixbuf); michael@0: g_object_unref(preview_pixbuf); michael@0: gtk_file_chooser_set_preview_widget_active(file_chooser, TRUE); michael@0: } michael@0: michael@0: static nsAutoCString michael@0: MakeCaseInsensitiveShellGlob(const char* aPattern) { michael@0: // aPattern is UTF8 michael@0: nsAutoCString result; michael@0: unsigned int len = strlen(aPattern); michael@0: michael@0: for (unsigned int i = 0; i < len; i++) { michael@0: if (!g_ascii_isalpha(aPattern[i])) { michael@0: // non-ASCII characters will also trigger this path, so unicode michael@0: // is safely handled albeit case-sensitively michael@0: result.Append(aPattern[i]); michael@0: continue; michael@0: } michael@0: michael@0: // add the lowercase and uppercase version of a character to a bracket michael@0: // match, so it matches either the lowercase or uppercase char. michael@0: result.Append('['); michael@0: result.Append(g_ascii_tolower(aPattern[i])); michael@0: result.Append(g_ascii_toupper(aPattern[i])); michael@0: result.Append(']'); michael@0: michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: michael@0: NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker) michael@0: michael@0: nsFilePicker::nsFilePicker() michael@0: : mSelectedType(0), michael@0: mRunning(false), michael@0: mAllowURLs(false) michael@0: { michael@0: } michael@0: michael@0: nsFilePicker::~nsFilePicker() michael@0: { michael@0: } michael@0: michael@0: void michael@0: ReadMultipleFiles(gpointer filename, gpointer array) michael@0: { michael@0: nsCOMPtr localfile; michael@0: nsresult rv = NS_NewNativeLocalFile(nsDependentCString(static_cast(filename)), michael@0: false, michael@0: getter_AddRefs(localfile)); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: nsCOMArray& files = *static_cast*>(array); michael@0: files.AppendObject(localfile); michael@0: } michael@0: michael@0: g_free(filename); michael@0: } michael@0: michael@0: void michael@0: nsFilePicker::ReadValuesFromFileChooser(GtkWidget *file_chooser) michael@0: { michael@0: mFiles.Clear(); michael@0: michael@0: if (mMode == nsIFilePicker::modeOpenMultiple) { michael@0: mFileURL.Truncate(); michael@0: michael@0: GSList *list = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(file_chooser)); michael@0: g_slist_foreach(list, ReadMultipleFiles, static_cast(&mFiles)); michael@0: g_slist_free(list); michael@0: } else { michael@0: gchar *filename = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(file_chooser)); michael@0: mFileURL.Assign(filename); michael@0: g_free(filename); michael@0: } michael@0: michael@0: GtkFileFilter *filter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(file_chooser)); michael@0: GSList *filter_list = gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(file_chooser)); michael@0: michael@0: mSelectedType = static_cast(g_slist_index(filter_list, filter)); michael@0: g_slist_free(filter_list); michael@0: michael@0: // Remember last used directory. michael@0: nsCOMPtr file; michael@0: GetFile(getter_AddRefs(file)); michael@0: if (file) { michael@0: nsCOMPtr dir; michael@0: file->GetParent(getter_AddRefs(dir)); michael@0: if (dir) { michael@0: dir.swap(mPrevDisplayDirectory); michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: nsFilePicker::InitNative(nsIWidget *aParent, michael@0: const nsAString& aTitle) michael@0: { michael@0: mParentWidget = aParent; michael@0: mTitle.Assign(aTitle); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::AppendFilters(int32_t aFilterMask) michael@0: { michael@0: mAllowURLs = !!(aFilterMask & filterAllowURLs); michael@0: return nsBaseFilePicker::AppendFilters(aFilterMask); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) michael@0: { michael@0: if (aFilter.EqualsLiteral("..apps")) { michael@0: // No platform specific thing we can do here, really.... michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsAutoCString filter, name; michael@0: CopyUTF16toUTF8(aFilter, filter); michael@0: CopyUTF16toUTF8(aTitle, name); michael@0: michael@0: mFilters.AppendElement(filter); michael@0: mFilterNames.AppendElement(name); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::SetDefaultString(const nsAString& aString) michael@0: { michael@0: mDefault = aString; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::GetDefaultString(nsAString& aString) michael@0: { michael@0: // Per API... michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::SetDefaultExtension(const nsAString& aExtension) michael@0: { michael@0: mDefaultExtension = aExtension; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::GetDefaultExtension(nsAString& aExtension) michael@0: { michael@0: aExtension = mDefaultExtension; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::GetFilterIndex(int32_t *aFilterIndex) michael@0: { michael@0: *aFilterIndex = mSelectedType; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::SetFilterIndex(int32_t aFilterIndex) michael@0: { michael@0: mSelectedType = aFilterIndex; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::GetFile(nsIFile **aFile) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aFile); michael@0: michael@0: *aFile = nullptr; michael@0: nsCOMPtr uri; michael@0: nsresult rv = GetFileURL(getter_AddRefs(uri)); michael@0: if (!uri) michael@0: return rv; michael@0: michael@0: nsCOMPtr fileURL(do_QueryInterface(uri, &rv)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCOMPtr file; michael@0: rv = fileURL->GetFile(getter_AddRefs(file)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: file.forget(aFile); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::GetFileURL(nsIURI **aFileURL) michael@0: { michael@0: *aFileURL = nullptr; michael@0: return NS_NewURI(aFileURL, mFileURL); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::GetFiles(nsISimpleEnumerator **aFiles) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aFiles); michael@0: michael@0: if (mMode == nsIFilePicker::modeOpenMultiple) { michael@0: return NS_NewArrayEnumerator(aFiles, mFiles); michael@0: } michael@0: michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::Show(int16_t *aReturn) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aReturn); michael@0: michael@0: nsresult rv = Open(nullptr); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: while (mRunning) { michael@0: g_main_context_iteration(nullptr, TRUE); michael@0: } michael@0: michael@0: *aReturn = mResult; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsFilePicker::Open(nsIFilePickerShownCallback *aCallback) michael@0: { michael@0: // Can't show two dialogs concurrently with the same filepicker michael@0: if (mRunning) michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: michael@0: nsXPIDLCString title; michael@0: title.Adopt(ToNewUTF8String(mTitle)); michael@0: michael@0: GtkWindow *parent_widget = michael@0: GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET)); michael@0: michael@0: GtkFileChooserAction action = GetGtkFileChooserAction(mMode); michael@0: const gchar *accept_button = (action == GTK_FILE_CHOOSER_ACTION_SAVE) michael@0: ? GTK_STOCK_SAVE : GTK_STOCK_OPEN; michael@0: GtkWidget *file_chooser = michael@0: gtk_file_chooser_dialog_new(title, parent_widget, action, michael@0: GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, michael@0: accept_button, GTK_RESPONSE_ACCEPT, michael@0: nullptr); michael@0: gtk_dialog_set_alternative_button_order(GTK_DIALOG(file_chooser), michael@0: GTK_RESPONSE_ACCEPT, michael@0: GTK_RESPONSE_CANCEL, michael@0: -1); michael@0: if (mAllowURLs) { michael@0: gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(file_chooser), FALSE); michael@0: } michael@0: michael@0: if (action == GTK_FILE_CHOOSER_ACTION_OPEN || action == GTK_FILE_CHOOSER_ACTION_SAVE) { michael@0: GtkWidget *img_preview = gtk_image_new(); michael@0: gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(file_chooser), img_preview); michael@0: g_signal_connect(file_chooser, "update-preview", G_CALLBACK(UpdateFilePreviewWidget), img_preview); michael@0: } michael@0: michael@0: GtkWindow *window = GTK_WINDOW(file_chooser); michael@0: gtk_window_set_modal(window, TRUE); michael@0: if (parent_widget) { michael@0: gtk_window_set_destroy_with_parent(window, TRUE); michael@0: } michael@0: michael@0: NS_ConvertUTF16toUTF8 defaultName(mDefault); michael@0: switch (mMode) { michael@0: case nsIFilePicker::modeOpenMultiple: michael@0: gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(file_chooser), TRUE); michael@0: break; michael@0: case nsIFilePicker::modeSave: michael@0: gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser), michael@0: defaultName.get()); michael@0: break; michael@0: } michael@0: michael@0: nsCOMPtr defaultPath; michael@0: if (mDisplayDirectory) { michael@0: mDisplayDirectory->Clone(getter_AddRefs(defaultPath)); michael@0: } else if (mPrevDisplayDirectory) { michael@0: mPrevDisplayDirectory->Clone(getter_AddRefs(defaultPath)); michael@0: } michael@0: michael@0: if (defaultPath) { michael@0: if (!defaultName.IsEmpty() && mMode != nsIFilePicker::modeSave) { michael@0: // Try to select the intended file. Even if it doesn't exist, GTK still switches michael@0: // directories. michael@0: defaultPath->AppendNative(defaultName); michael@0: nsAutoCString path; michael@0: defaultPath->GetNativePath(path); michael@0: gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(file_chooser), path.get()); michael@0: } else { michael@0: nsAutoCString directory; michael@0: defaultPath->GetNativePath(directory); michael@0: gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(file_chooser), michael@0: directory.get()); michael@0: } michael@0: } michael@0: michael@0: gtk_dialog_set_default_response(GTK_DIALOG(file_chooser), GTK_RESPONSE_ACCEPT); michael@0: michael@0: int32_t count = mFilters.Length(); michael@0: for (int32_t i = 0; i < count; ++i) { michael@0: // This is fun... the GTK file picker does not accept a list of filters michael@0: // so we need to split out each string, and add it manually. michael@0: michael@0: char **patterns = g_strsplit(mFilters[i].get(), ";", -1); michael@0: if (!patterns) { michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: michael@0: GtkFileFilter *filter = gtk_file_filter_new(); michael@0: for (int j = 0; patterns[j] != nullptr; ++j) { michael@0: nsAutoCString caseInsensitiveFilter = MakeCaseInsensitiveShellGlob(g_strstrip(patterns[j])); michael@0: gtk_file_filter_add_pattern(filter, caseInsensitiveFilter.get()); michael@0: } michael@0: michael@0: g_strfreev(patterns); michael@0: michael@0: if (!mFilterNames[i].IsEmpty()) { michael@0: // If we have a name for our filter, let's use that. michael@0: const char *filter_name = mFilterNames[i].get(); michael@0: gtk_file_filter_set_name(filter, filter_name); michael@0: } else { michael@0: // If we don't have a name, let's just use the filter pattern. michael@0: const char *filter_pattern = mFilters[i].get(); michael@0: gtk_file_filter_set_name(filter, filter_pattern); michael@0: } michael@0: michael@0: gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter); michael@0: michael@0: // Set the initially selected filter michael@0: if (mSelectedType == i) { michael@0: gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(file_chooser), filter); michael@0: } michael@0: } michael@0: michael@0: gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(file_chooser), TRUE); michael@0: michael@0: mRunning = true; michael@0: mCallback = aCallback; michael@0: NS_ADDREF_THIS(); michael@0: g_signal_connect(file_chooser, "response", G_CALLBACK(OnResponse), this); michael@0: g_signal_connect(file_chooser, "destroy", G_CALLBACK(OnDestroy), this); michael@0: gtk_widget_show(file_chooser); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: /* static */ void michael@0: nsFilePicker::OnResponse(GtkWidget* file_chooser, gint response_id, michael@0: gpointer user_data) michael@0: { michael@0: static_cast(user_data)-> michael@0: Done(file_chooser, response_id); michael@0: } michael@0: michael@0: /* static */ void michael@0: nsFilePicker::OnDestroy(GtkWidget* file_chooser, gpointer user_data) michael@0: { michael@0: static_cast(user_data)-> michael@0: Done(file_chooser, GTK_RESPONSE_CANCEL); michael@0: } michael@0: michael@0: void michael@0: nsFilePicker::Done(GtkWidget* file_chooser, gint response) michael@0: { michael@0: mRunning = false; michael@0: michael@0: int16_t result; michael@0: switch (response) { michael@0: case GTK_RESPONSE_OK: michael@0: case GTK_RESPONSE_ACCEPT: michael@0: ReadValuesFromFileChooser(file_chooser); michael@0: result = nsIFilePicker::returnOK; michael@0: if (mMode == nsIFilePicker::modeSave) { michael@0: nsCOMPtr file; michael@0: GetFile(getter_AddRefs(file)); michael@0: if (file) { michael@0: bool exists = false; michael@0: file->Exists(&exists); michael@0: if (exists) michael@0: result = nsIFilePicker::returnReplace; michael@0: } michael@0: } michael@0: break; michael@0: michael@0: case GTK_RESPONSE_CANCEL: michael@0: case GTK_RESPONSE_CLOSE: michael@0: case GTK_RESPONSE_DELETE_EVENT: michael@0: result = nsIFilePicker::returnCancel; michael@0: break; michael@0: michael@0: default: michael@0: NS_WARNING("Unexpected response"); michael@0: result = nsIFilePicker::returnCancel; michael@0: break; michael@0: } michael@0: michael@0: // A "response" signal won't be sent again but "destroy" will be. michael@0: g_signal_handlers_disconnect_by_func(file_chooser, michael@0: FuncToGpointer(OnDestroy), this); michael@0: michael@0: // When response_id is GTK_RESPONSE_DELETE_EVENT or when called from michael@0: // OnDestroy, the widget would be destroyed anyway but it is fine if michael@0: // gtk_widget_destroy is called more than once. gtk_widget_destroy has michael@0: // requests that any remaining references be released, but the reference michael@0: // count will not be decremented again if GtkWindow's reference has already michael@0: // been released. michael@0: gtk_widget_destroy(file_chooser); michael@0: michael@0: if (mCallback) { michael@0: mCallback->Done(result); michael@0: mCallback = nullptr; michael@0: } else { michael@0: mResult = result; michael@0: } michael@0: NS_RELEASE_THIS(); michael@0: }