1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/widget/gtk/nsFilePicker.cpp Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,546 @@ 1.4 +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +#include "mozilla/Types.h" 1.10 + 1.11 +#include <gtk/gtk.h> 1.12 + 1.13 +#include "nsGtkUtils.h" 1.14 +#include "nsIFileURL.h" 1.15 +#include "nsIURI.h" 1.16 +#include "nsIWidget.h" 1.17 +#include "nsIFile.h" 1.18 +#include "nsIStringBundle.h" 1.19 + 1.20 +#include "nsArrayEnumerator.h" 1.21 +#include "nsMemory.h" 1.22 +#include "nsEnumeratorUtils.h" 1.23 +#include "nsNetUtil.h" 1.24 +#include "nsReadableUtils.h" 1.25 +#include "mozcontainer.h" 1.26 + 1.27 +#include "nsFilePicker.h" 1.28 + 1.29 +using namespace mozilla; 1.30 + 1.31 +#define MAX_PREVIEW_SIZE 180 1.32 + 1.33 +nsIFile *nsFilePicker::mPrevDisplayDirectory = nullptr; 1.34 + 1.35 +void 1.36 +nsFilePicker::Shutdown() 1.37 +{ 1.38 + NS_IF_RELEASE(mPrevDisplayDirectory); 1.39 +} 1.40 + 1.41 +static GtkFileChooserAction 1.42 +GetGtkFileChooserAction(int16_t aMode) 1.43 +{ 1.44 + GtkFileChooserAction action; 1.45 + 1.46 + switch (aMode) { 1.47 + case nsIFilePicker::modeSave: 1.48 + action = GTK_FILE_CHOOSER_ACTION_SAVE; 1.49 + break; 1.50 + 1.51 + case nsIFilePicker::modeGetFolder: 1.52 + action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; 1.53 + break; 1.54 + 1.55 + case nsIFilePicker::modeOpen: 1.56 + case nsIFilePicker::modeOpenMultiple: 1.57 + action = GTK_FILE_CHOOSER_ACTION_OPEN; 1.58 + break; 1.59 + 1.60 + default: 1.61 + NS_WARNING("Unknown nsIFilePicker mode"); 1.62 + action = GTK_FILE_CHOOSER_ACTION_OPEN; 1.63 + break; 1.64 + } 1.65 + 1.66 + return action; 1.67 +} 1.68 + 1.69 + 1.70 +static void 1.71 +UpdateFilePreviewWidget(GtkFileChooser *file_chooser, 1.72 + gpointer preview_widget_voidptr) 1.73 +{ 1.74 + GtkImage *preview_widget = GTK_IMAGE(preview_widget_voidptr); 1.75 + char *image_filename = gtk_file_chooser_get_preview_filename(file_chooser); 1.76 + 1.77 + if (!image_filename) { 1.78 + gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE); 1.79 + return; 1.80 + } 1.81 + 1.82 + gint preview_width = 0; 1.83 + gint preview_height = 0; 1.84 + GdkPixbufFormat *preview_format = gdk_pixbuf_get_file_info(image_filename, 1.85 + &preview_width, 1.86 + &preview_height); 1.87 + if (!preview_format) { 1.88 + g_free(image_filename); 1.89 + gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE); 1.90 + return; 1.91 + } 1.92 + 1.93 + GdkPixbuf *preview_pixbuf; 1.94 + // Only scale down images that are too big 1.95 + if (preview_width > MAX_PREVIEW_SIZE || preview_height > MAX_PREVIEW_SIZE) { 1.96 + preview_pixbuf = gdk_pixbuf_new_from_file_at_size(image_filename, 1.97 + MAX_PREVIEW_SIZE, 1.98 + MAX_PREVIEW_SIZE, 1.99 + nullptr); 1.100 + } 1.101 + else { 1.102 + preview_pixbuf = gdk_pixbuf_new_from_file(image_filename, nullptr); 1.103 + } 1.104 + 1.105 + g_free(image_filename); 1.106 + 1.107 + if (!preview_pixbuf) { 1.108 + gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE); 1.109 + return; 1.110 + } 1.111 + 1.112 +#if GTK_CHECK_VERSION(2,12,0) 1.113 + GdkPixbuf *preview_pixbuf_temp = preview_pixbuf; 1.114 + preview_pixbuf = gdk_pixbuf_apply_embedded_orientation(preview_pixbuf_temp); 1.115 + g_object_unref(preview_pixbuf_temp); 1.116 +#endif 1.117 + 1.118 + // This is the easiest way to do center alignment without worrying about containers 1.119 + // Minimum 3px padding each side (hence the 6) just to make things nice 1.120 + gint x_padding = (MAX_PREVIEW_SIZE + 6 - gdk_pixbuf_get_width(preview_pixbuf)) / 2; 1.121 + gtk_misc_set_padding(GTK_MISC(preview_widget), x_padding, 0); 1.122 + 1.123 + gtk_image_set_from_pixbuf(preview_widget, preview_pixbuf); 1.124 + g_object_unref(preview_pixbuf); 1.125 + gtk_file_chooser_set_preview_widget_active(file_chooser, TRUE); 1.126 +} 1.127 + 1.128 +static nsAutoCString 1.129 +MakeCaseInsensitiveShellGlob(const char* aPattern) { 1.130 + // aPattern is UTF8 1.131 + nsAutoCString result; 1.132 + unsigned int len = strlen(aPattern); 1.133 + 1.134 + for (unsigned int i = 0; i < len; i++) { 1.135 + if (!g_ascii_isalpha(aPattern[i])) { 1.136 + // non-ASCII characters will also trigger this path, so unicode 1.137 + // is safely handled albeit case-sensitively 1.138 + result.Append(aPattern[i]); 1.139 + continue; 1.140 + } 1.141 + 1.142 + // add the lowercase and uppercase version of a character to a bracket 1.143 + // match, so it matches either the lowercase or uppercase char. 1.144 + result.Append('['); 1.145 + result.Append(g_ascii_tolower(aPattern[i])); 1.146 + result.Append(g_ascii_toupper(aPattern[i])); 1.147 + result.Append(']'); 1.148 + 1.149 + } 1.150 + 1.151 + return result; 1.152 +} 1.153 + 1.154 +NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker) 1.155 + 1.156 +nsFilePicker::nsFilePicker() 1.157 + : mSelectedType(0), 1.158 + mRunning(false), 1.159 + mAllowURLs(false) 1.160 +{ 1.161 +} 1.162 + 1.163 +nsFilePicker::~nsFilePicker() 1.164 +{ 1.165 +} 1.166 + 1.167 +void 1.168 +ReadMultipleFiles(gpointer filename, gpointer array) 1.169 +{ 1.170 + nsCOMPtr<nsIFile> localfile; 1.171 + nsresult rv = NS_NewNativeLocalFile(nsDependentCString(static_cast<char*>(filename)), 1.172 + false, 1.173 + getter_AddRefs(localfile)); 1.174 + if (NS_SUCCEEDED(rv)) { 1.175 + nsCOMArray<nsIFile>& files = *static_cast<nsCOMArray<nsIFile>*>(array); 1.176 + files.AppendObject(localfile); 1.177 + } 1.178 + 1.179 + g_free(filename); 1.180 +} 1.181 + 1.182 +void 1.183 +nsFilePicker::ReadValuesFromFileChooser(GtkWidget *file_chooser) 1.184 +{ 1.185 + mFiles.Clear(); 1.186 + 1.187 + if (mMode == nsIFilePicker::modeOpenMultiple) { 1.188 + mFileURL.Truncate(); 1.189 + 1.190 + GSList *list = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(file_chooser)); 1.191 + g_slist_foreach(list, ReadMultipleFiles, static_cast<gpointer>(&mFiles)); 1.192 + g_slist_free(list); 1.193 + } else { 1.194 + gchar *filename = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(file_chooser)); 1.195 + mFileURL.Assign(filename); 1.196 + g_free(filename); 1.197 + } 1.198 + 1.199 + GtkFileFilter *filter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(file_chooser)); 1.200 + GSList *filter_list = gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(file_chooser)); 1.201 + 1.202 + mSelectedType = static_cast<int16_t>(g_slist_index(filter_list, filter)); 1.203 + g_slist_free(filter_list); 1.204 + 1.205 + // Remember last used directory. 1.206 + nsCOMPtr<nsIFile> file; 1.207 + GetFile(getter_AddRefs(file)); 1.208 + if (file) { 1.209 + nsCOMPtr<nsIFile> dir; 1.210 + file->GetParent(getter_AddRefs(dir)); 1.211 + if (dir) { 1.212 + dir.swap(mPrevDisplayDirectory); 1.213 + } 1.214 + } 1.215 +} 1.216 + 1.217 +void 1.218 +nsFilePicker::InitNative(nsIWidget *aParent, 1.219 + const nsAString& aTitle) 1.220 +{ 1.221 + mParentWidget = aParent; 1.222 + mTitle.Assign(aTitle); 1.223 +} 1.224 + 1.225 +NS_IMETHODIMP 1.226 +nsFilePicker::AppendFilters(int32_t aFilterMask) 1.227 +{ 1.228 + mAllowURLs = !!(aFilterMask & filterAllowURLs); 1.229 + return nsBaseFilePicker::AppendFilters(aFilterMask); 1.230 +} 1.231 + 1.232 +NS_IMETHODIMP 1.233 +nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) 1.234 +{ 1.235 + if (aFilter.EqualsLiteral("..apps")) { 1.236 + // No platform specific thing we can do here, really.... 1.237 + return NS_OK; 1.238 + } 1.239 + 1.240 + nsAutoCString filter, name; 1.241 + CopyUTF16toUTF8(aFilter, filter); 1.242 + CopyUTF16toUTF8(aTitle, name); 1.243 + 1.244 + mFilters.AppendElement(filter); 1.245 + mFilterNames.AppendElement(name); 1.246 + 1.247 + return NS_OK; 1.248 +} 1.249 + 1.250 +NS_IMETHODIMP 1.251 +nsFilePicker::SetDefaultString(const nsAString& aString) 1.252 +{ 1.253 + mDefault = aString; 1.254 + 1.255 + return NS_OK; 1.256 +} 1.257 + 1.258 +NS_IMETHODIMP 1.259 +nsFilePicker::GetDefaultString(nsAString& aString) 1.260 +{ 1.261 + // Per API... 1.262 + return NS_ERROR_FAILURE; 1.263 +} 1.264 + 1.265 +NS_IMETHODIMP 1.266 +nsFilePicker::SetDefaultExtension(const nsAString& aExtension) 1.267 +{ 1.268 + mDefaultExtension = aExtension; 1.269 + 1.270 + return NS_OK; 1.271 +} 1.272 + 1.273 +NS_IMETHODIMP 1.274 +nsFilePicker::GetDefaultExtension(nsAString& aExtension) 1.275 +{ 1.276 + aExtension = mDefaultExtension; 1.277 + 1.278 + return NS_OK; 1.279 +} 1.280 + 1.281 +NS_IMETHODIMP 1.282 +nsFilePicker::GetFilterIndex(int32_t *aFilterIndex) 1.283 +{ 1.284 + *aFilterIndex = mSelectedType; 1.285 + 1.286 + return NS_OK; 1.287 +} 1.288 + 1.289 +NS_IMETHODIMP 1.290 +nsFilePicker::SetFilterIndex(int32_t aFilterIndex) 1.291 +{ 1.292 + mSelectedType = aFilterIndex; 1.293 + 1.294 + return NS_OK; 1.295 +} 1.296 + 1.297 +NS_IMETHODIMP 1.298 +nsFilePicker::GetFile(nsIFile **aFile) 1.299 +{ 1.300 + NS_ENSURE_ARG_POINTER(aFile); 1.301 + 1.302 + *aFile = nullptr; 1.303 + nsCOMPtr<nsIURI> uri; 1.304 + nsresult rv = GetFileURL(getter_AddRefs(uri)); 1.305 + if (!uri) 1.306 + return rv; 1.307 + 1.308 + nsCOMPtr<nsIFileURL> fileURL(do_QueryInterface(uri, &rv)); 1.309 + NS_ENSURE_SUCCESS(rv, rv); 1.310 + 1.311 + nsCOMPtr<nsIFile> file; 1.312 + rv = fileURL->GetFile(getter_AddRefs(file)); 1.313 + NS_ENSURE_SUCCESS(rv, rv); 1.314 + 1.315 + file.forget(aFile); 1.316 + return NS_OK; 1.317 +} 1.318 + 1.319 +NS_IMETHODIMP 1.320 +nsFilePicker::GetFileURL(nsIURI **aFileURL) 1.321 +{ 1.322 + *aFileURL = nullptr; 1.323 + return NS_NewURI(aFileURL, mFileURL); 1.324 +} 1.325 + 1.326 +NS_IMETHODIMP 1.327 +nsFilePicker::GetFiles(nsISimpleEnumerator **aFiles) 1.328 +{ 1.329 + NS_ENSURE_ARG_POINTER(aFiles); 1.330 + 1.331 + if (mMode == nsIFilePicker::modeOpenMultiple) { 1.332 + return NS_NewArrayEnumerator(aFiles, mFiles); 1.333 + } 1.334 + 1.335 + return NS_ERROR_FAILURE; 1.336 +} 1.337 + 1.338 +NS_IMETHODIMP 1.339 +nsFilePicker::Show(int16_t *aReturn) 1.340 +{ 1.341 + NS_ENSURE_ARG_POINTER(aReturn); 1.342 + 1.343 + nsresult rv = Open(nullptr); 1.344 + if (NS_FAILED(rv)) 1.345 + return rv; 1.346 + 1.347 + while (mRunning) { 1.348 + g_main_context_iteration(nullptr, TRUE); 1.349 + } 1.350 + 1.351 + *aReturn = mResult; 1.352 + return NS_OK; 1.353 +} 1.354 + 1.355 +NS_IMETHODIMP 1.356 +nsFilePicker::Open(nsIFilePickerShownCallback *aCallback) 1.357 +{ 1.358 + // Can't show two dialogs concurrently with the same filepicker 1.359 + if (mRunning) 1.360 + return NS_ERROR_NOT_AVAILABLE; 1.361 + 1.362 + nsXPIDLCString title; 1.363 + title.Adopt(ToNewUTF8String(mTitle)); 1.364 + 1.365 + GtkWindow *parent_widget = 1.366 + GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET)); 1.367 + 1.368 + GtkFileChooserAction action = GetGtkFileChooserAction(mMode); 1.369 + const gchar *accept_button = (action == GTK_FILE_CHOOSER_ACTION_SAVE) 1.370 + ? GTK_STOCK_SAVE : GTK_STOCK_OPEN; 1.371 + GtkWidget *file_chooser = 1.372 + gtk_file_chooser_dialog_new(title, parent_widget, action, 1.373 + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, 1.374 + accept_button, GTK_RESPONSE_ACCEPT, 1.375 + nullptr); 1.376 + gtk_dialog_set_alternative_button_order(GTK_DIALOG(file_chooser), 1.377 + GTK_RESPONSE_ACCEPT, 1.378 + GTK_RESPONSE_CANCEL, 1.379 + -1); 1.380 + if (mAllowURLs) { 1.381 + gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(file_chooser), FALSE); 1.382 + } 1.383 + 1.384 + if (action == GTK_FILE_CHOOSER_ACTION_OPEN || action == GTK_FILE_CHOOSER_ACTION_SAVE) { 1.385 + GtkWidget *img_preview = gtk_image_new(); 1.386 + gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(file_chooser), img_preview); 1.387 + g_signal_connect(file_chooser, "update-preview", G_CALLBACK(UpdateFilePreviewWidget), img_preview); 1.388 + } 1.389 + 1.390 + GtkWindow *window = GTK_WINDOW(file_chooser); 1.391 + gtk_window_set_modal(window, TRUE); 1.392 + if (parent_widget) { 1.393 + gtk_window_set_destroy_with_parent(window, TRUE); 1.394 + } 1.395 + 1.396 + NS_ConvertUTF16toUTF8 defaultName(mDefault); 1.397 + switch (mMode) { 1.398 + case nsIFilePicker::modeOpenMultiple: 1.399 + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(file_chooser), TRUE); 1.400 + break; 1.401 + case nsIFilePicker::modeSave: 1.402 + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser), 1.403 + defaultName.get()); 1.404 + break; 1.405 + } 1.406 + 1.407 + nsCOMPtr<nsIFile> defaultPath; 1.408 + if (mDisplayDirectory) { 1.409 + mDisplayDirectory->Clone(getter_AddRefs(defaultPath)); 1.410 + } else if (mPrevDisplayDirectory) { 1.411 + mPrevDisplayDirectory->Clone(getter_AddRefs(defaultPath)); 1.412 + } 1.413 + 1.414 + if (defaultPath) { 1.415 + if (!defaultName.IsEmpty() && mMode != nsIFilePicker::modeSave) { 1.416 + // Try to select the intended file. Even if it doesn't exist, GTK still switches 1.417 + // directories. 1.418 + defaultPath->AppendNative(defaultName); 1.419 + nsAutoCString path; 1.420 + defaultPath->GetNativePath(path); 1.421 + gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(file_chooser), path.get()); 1.422 + } else { 1.423 + nsAutoCString directory; 1.424 + defaultPath->GetNativePath(directory); 1.425 + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(file_chooser), 1.426 + directory.get()); 1.427 + } 1.428 + } 1.429 + 1.430 + gtk_dialog_set_default_response(GTK_DIALOG(file_chooser), GTK_RESPONSE_ACCEPT); 1.431 + 1.432 + int32_t count = mFilters.Length(); 1.433 + for (int32_t i = 0; i < count; ++i) { 1.434 + // This is fun... the GTK file picker does not accept a list of filters 1.435 + // so we need to split out each string, and add it manually. 1.436 + 1.437 + char **patterns = g_strsplit(mFilters[i].get(), ";", -1); 1.438 + if (!patterns) { 1.439 + return NS_ERROR_OUT_OF_MEMORY; 1.440 + } 1.441 + 1.442 + GtkFileFilter *filter = gtk_file_filter_new(); 1.443 + for (int j = 0; patterns[j] != nullptr; ++j) { 1.444 + nsAutoCString caseInsensitiveFilter = MakeCaseInsensitiveShellGlob(g_strstrip(patterns[j])); 1.445 + gtk_file_filter_add_pattern(filter, caseInsensitiveFilter.get()); 1.446 + } 1.447 + 1.448 + g_strfreev(patterns); 1.449 + 1.450 + if (!mFilterNames[i].IsEmpty()) { 1.451 + // If we have a name for our filter, let's use that. 1.452 + const char *filter_name = mFilterNames[i].get(); 1.453 + gtk_file_filter_set_name(filter, filter_name); 1.454 + } else { 1.455 + // If we don't have a name, let's just use the filter pattern. 1.456 + const char *filter_pattern = mFilters[i].get(); 1.457 + gtk_file_filter_set_name(filter, filter_pattern); 1.458 + } 1.459 + 1.460 + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter); 1.461 + 1.462 + // Set the initially selected filter 1.463 + if (mSelectedType == i) { 1.464 + gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(file_chooser), filter); 1.465 + } 1.466 + } 1.467 + 1.468 + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(file_chooser), TRUE); 1.469 + 1.470 + mRunning = true; 1.471 + mCallback = aCallback; 1.472 + NS_ADDREF_THIS(); 1.473 + g_signal_connect(file_chooser, "response", G_CALLBACK(OnResponse), this); 1.474 + g_signal_connect(file_chooser, "destroy", G_CALLBACK(OnDestroy), this); 1.475 + gtk_widget_show(file_chooser); 1.476 + 1.477 + return NS_OK; 1.478 +} 1.479 + 1.480 +/* static */ void 1.481 +nsFilePicker::OnResponse(GtkWidget* file_chooser, gint response_id, 1.482 + gpointer user_data) 1.483 +{ 1.484 + static_cast<nsFilePicker*>(user_data)-> 1.485 + Done(file_chooser, response_id); 1.486 +} 1.487 + 1.488 +/* static */ void 1.489 +nsFilePicker::OnDestroy(GtkWidget* file_chooser, gpointer user_data) 1.490 +{ 1.491 + static_cast<nsFilePicker*>(user_data)-> 1.492 + Done(file_chooser, GTK_RESPONSE_CANCEL); 1.493 +} 1.494 + 1.495 +void 1.496 +nsFilePicker::Done(GtkWidget* file_chooser, gint response) 1.497 +{ 1.498 + mRunning = false; 1.499 + 1.500 + int16_t result; 1.501 + switch (response) { 1.502 + case GTK_RESPONSE_OK: 1.503 + case GTK_RESPONSE_ACCEPT: 1.504 + ReadValuesFromFileChooser(file_chooser); 1.505 + result = nsIFilePicker::returnOK; 1.506 + if (mMode == nsIFilePicker::modeSave) { 1.507 + nsCOMPtr<nsIFile> file; 1.508 + GetFile(getter_AddRefs(file)); 1.509 + if (file) { 1.510 + bool exists = false; 1.511 + file->Exists(&exists); 1.512 + if (exists) 1.513 + result = nsIFilePicker::returnReplace; 1.514 + } 1.515 + } 1.516 + break; 1.517 + 1.518 + case GTK_RESPONSE_CANCEL: 1.519 + case GTK_RESPONSE_CLOSE: 1.520 + case GTK_RESPONSE_DELETE_EVENT: 1.521 + result = nsIFilePicker::returnCancel; 1.522 + break; 1.523 + 1.524 + default: 1.525 + NS_WARNING("Unexpected response"); 1.526 + result = nsIFilePicker::returnCancel; 1.527 + break; 1.528 + } 1.529 + 1.530 + // A "response" signal won't be sent again but "destroy" will be. 1.531 + g_signal_handlers_disconnect_by_func(file_chooser, 1.532 + FuncToGpointer(OnDestroy), this); 1.533 + 1.534 + // When response_id is GTK_RESPONSE_DELETE_EVENT or when called from 1.535 + // OnDestroy, the widget would be destroyed anyway but it is fine if 1.536 + // gtk_widget_destroy is called more than once. gtk_widget_destroy has 1.537 + // requests that any remaining references be released, but the reference 1.538 + // count will not be decremented again if GtkWindow's reference has already 1.539 + // been released. 1.540 + gtk_widget_destroy(file_chooser); 1.541 + 1.542 + if (mCallback) { 1.543 + mCallback->Done(result); 1.544 + mCallback = nullptr; 1.545 + } else { 1.546 + mResult = result; 1.547 + } 1.548 + NS_RELEASE_THIS(); 1.549 +}