Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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/. */
6 #include "mozilla/Types.h"
8 #include <gtk/gtk.h>
10 #include "nsGtkUtils.h"
11 #include "nsIFileURL.h"
12 #include "nsIURI.h"
13 #include "nsIWidget.h"
14 #include "nsIFile.h"
15 #include "nsIStringBundle.h"
17 #include "nsArrayEnumerator.h"
18 #include "nsMemory.h"
19 #include "nsEnumeratorUtils.h"
20 #include "nsNetUtil.h"
21 #include "nsReadableUtils.h"
22 #include "mozcontainer.h"
24 #include "nsFilePicker.h"
26 using namespace mozilla;
28 #define MAX_PREVIEW_SIZE 180
30 nsIFile *nsFilePicker::mPrevDisplayDirectory = nullptr;
32 void
33 nsFilePicker::Shutdown()
34 {
35 NS_IF_RELEASE(mPrevDisplayDirectory);
36 }
38 static GtkFileChooserAction
39 GetGtkFileChooserAction(int16_t aMode)
40 {
41 GtkFileChooserAction action;
43 switch (aMode) {
44 case nsIFilePicker::modeSave:
45 action = GTK_FILE_CHOOSER_ACTION_SAVE;
46 break;
48 case nsIFilePicker::modeGetFolder:
49 action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
50 break;
52 case nsIFilePicker::modeOpen:
53 case nsIFilePicker::modeOpenMultiple:
54 action = GTK_FILE_CHOOSER_ACTION_OPEN;
55 break;
57 default:
58 NS_WARNING("Unknown nsIFilePicker mode");
59 action = GTK_FILE_CHOOSER_ACTION_OPEN;
60 break;
61 }
63 return action;
64 }
67 static void
68 UpdateFilePreviewWidget(GtkFileChooser *file_chooser,
69 gpointer preview_widget_voidptr)
70 {
71 GtkImage *preview_widget = GTK_IMAGE(preview_widget_voidptr);
72 char *image_filename = gtk_file_chooser_get_preview_filename(file_chooser);
74 if (!image_filename) {
75 gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
76 return;
77 }
79 gint preview_width = 0;
80 gint preview_height = 0;
81 GdkPixbufFormat *preview_format = gdk_pixbuf_get_file_info(image_filename,
82 &preview_width,
83 &preview_height);
84 if (!preview_format) {
85 g_free(image_filename);
86 gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
87 return;
88 }
90 GdkPixbuf *preview_pixbuf;
91 // Only scale down images that are too big
92 if (preview_width > MAX_PREVIEW_SIZE || preview_height > MAX_PREVIEW_SIZE) {
93 preview_pixbuf = gdk_pixbuf_new_from_file_at_size(image_filename,
94 MAX_PREVIEW_SIZE,
95 MAX_PREVIEW_SIZE,
96 nullptr);
97 }
98 else {
99 preview_pixbuf = gdk_pixbuf_new_from_file(image_filename, nullptr);
100 }
102 g_free(image_filename);
104 if (!preview_pixbuf) {
105 gtk_file_chooser_set_preview_widget_active(file_chooser, FALSE);
106 return;
107 }
109 #if GTK_CHECK_VERSION(2,12,0)
110 GdkPixbuf *preview_pixbuf_temp = preview_pixbuf;
111 preview_pixbuf = gdk_pixbuf_apply_embedded_orientation(preview_pixbuf_temp);
112 g_object_unref(preview_pixbuf_temp);
113 #endif
115 // This is the easiest way to do center alignment without worrying about containers
116 // Minimum 3px padding each side (hence the 6) just to make things nice
117 gint x_padding = (MAX_PREVIEW_SIZE + 6 - gdk_pixbuf_get_width(preview_pixbuf)) / 2;
118 gtk_misc_set_padding(GTK_MISC(preview_widget), x_padding, 0);
120 gtk_image_set_from_pixbuf(preview_widget, preview_pixbuf);
121 g_object_unref(preview_pixbuf);
122 gtk_file_chooser_set_preview_widget_active(file_chooser, TRUE);
123 }
125 static nsAutoCString
126 MakeCaseInsensitiveShellGlob(const char* aPattern) {
127 // aPattern is UTF8
128 nsAutoCString result;
129 unsigned int len = strlen(aPattern);
131 for (unsigned int i = 0; i < len; i++) {
132 if (!g_ascii_isalpha(aPattern[i])) {
133 // non-ASCII characters will also trigger this path, so unicode
134 // is safely handled albeit case-sensitively
135 result.Append(aPattern[i]);
136 continue;
137 }
139 // add the lowercase and uppercase version of a character to a bracket
140 // match, so it matches either the lowercase or uppercase char.
141 result.Append('[');
142 result.Append(g_ascii_tolower(aPattern[i]));
143 result.Append(g_ascii_toupper(aPattern[i]));
144 result.Append(']');
146 }
148 return result;
149 }
151 NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
153 nsFilePicker::nsFilePicker()
154 : mSelectedType(0),
155 mRunning(false),
156 mAllowURLs(false)
157 {
158 }
160 nsFilePicker::~nsFilePicker()
161 {
162 }
164 void
165 ReadMultipleFiles(gpointer filename, gpointer array)
166 {
167 nsCOMPtr<nsIFile> localfile;
168 nsresult rv = NS_NewNativeLocalFile(nsDependentCString(static_cast<char*>(filename)),
169 false,
170 getter_AddRefs(localfile));
171 if (NS_SUCCEEDED(rv)) {
172 nsCOMArray<nsIFile>& files = *static_cast<nsCOMArray<nsIFile>*>(array);
173 files.AppendObject(localfile);
174 }
176 g_free(filename);
177 }
179 void
180 nsFilePicker::ReadValuesFromFileChooser(GtkWidget *file_chooser)
181 {
182 mFiles.Clear();
184 if (mMode == nsIFilePicker::modeOpenMultiple) {
185 mFileURL.Truncate();
187 GSList *list = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(file_chooser));
188 g_slist_foreach(list, ReadMultipleFiles, static_cast<gpointer>(&mFiles));
189 g_slist_free(list);
190 } else {
191 gchar *filename = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(file_chooser));
192 mFileURL.Assign(filename);
193 g_free(filename);
194 }
196 GtkFileFilter *filter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(file_chooser));
197 GSList *filter_list = gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(file_chooser));
199 mSelectedType = static_cast<int16_t>(g_slist_index(filter_list, filter));
200 g_slist_free(filter_list);
202 // Remember last used directory.
203 nsCOMPtr<nsIFile> file;
204 GetFile(getter_AddRefs(file));
205 if (file) {
206 nsCOMPtr<nsIFile> dir;
207 file->GetParent(getter_AddRefs(dir));
208 if (dir) {
209 dir.swap(mPrevDisplayDirectory);
210 }
211 }
212 }
214 void
215 nsFilePicker::InitNative(nsIWidget *aParent,
216 const nsAString& aTitle)
217 {
218 mParentWidget = aParent;
219 mTitle.Assign(aTitle);
220 }
222 NS_IMETHODIMP
223 nsFilePicker::AppendFilters(int32_t aFilterMask)
224 {
225 mAllowURLs = !!(aFilterMask & filterAllowURLs);
226 return nsBaseFilePicker::AppendFilters(aFilterMask);
227 }
229 NS_IMETHODIMP
230 nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter)
231 {
232 if (aFilter.EqualsLiteral("..apps")) {
233 // No platform specific thing we can do here, really....
234 return NS_OK;
235 }
237 nsAutoCString filter, name;
238 CopyUTF16toUTF8(aFilter, filter);
239 CopyUTF16toUTF8(aTitle, name);
241 mFilters.AppendElement(filter);
242 mFilterNames.AppendElement(name);
244 return NS_OK;
245 }
247 NS_IMETHODIMP
248 nsFilePicker::SetDefaultString(const nsAString& aString)
249 {
250 mDefault = aString;
252 return NS_OK;
253 }
255 NS_IMETHODIMP
256 nsFilePicker::GetDefaultString(nsAString& aString)
257 {
258 // Per API...
259 return NS_ERROR_FAILURE;
260 }
262 NS_IMETHODIMP
263 nsFilePicker::SetDefaultExtension(const nsAString& aExtension)
264 {
265 mDefaultExtension = aExtension;
267 return NS_OK;
268 }
270 NS_IMETHODIMP
271 nsFilePicker::GetDefaultExtension(nsAString& aExtension)
272 {
273 aExtension = mDefaultExtension;
275 return NS_OK;
276 }
278 NS_IMETHODIMP
279 nsFilePicker::GetFilterIndex(int32_t *aFilterIndex)
280 {
281 *aFilterIndex = mSelectedType;
283 return NS_OK;
284 }
286 NS_IMETHODIMP
287 nsFilePicker::SetFilterIndex(int32_t aFilterIndex)
288 {
289 mSelectedType = aFilterIndex;
291 return NS_OK;
292 }
294 NS_IMETHODIMP
295 nsFilePicker::GetFile(nsIFile **aFile)
296 {
297 NS_ENSURE_ARG_POINTER(aFile);
299 *aFile = nullptr;
300 nsCOMPtr<nsIURI> uri;
301 nsresult rv = GetFileURL(getter_AddRefs(uri));
302 if (!uri)
303 return rv;
305 nsCOMPtr<nsIFileURL> fileURL(do_QueryInterface(uri, &rv));
306 NS_ENSURE_SUCCESS(rv, rv);
308 nsCOMPtr<nsIFile> file;
309 rv = fileURL->GetFile(getter_AddRefs(file));
310 NS_ENSURE_SUCCESS(rv, rv);
312 file.forget(aFile);
313 return NS_OK;
314 }
316 NS_IMETHODIMP
317 nsFilePicker::GetFileURL(nsIURI **aFileURL)
318 {
319 *aFileURL = nullptr;
320 return NS_NewURI(aFileURL, mFileURL);
321 }
323 NS_IMETHODIMP
324 nsFilePicker::GetFiles(nsISimpleEnumerator **aFiles)
325 {
326 NS_ENSURE_ARG_POINTER(aFiles);
328 if (mMode == nsIFilePicker::modeOpenMultiple) {
329 return NS_NewArrayEnumerator(aFiles, mFiles);
330 }
332 return NS_ERROR_FAILURE;
333 }
335 NS_IMETHODIMP
336 nsFilePicker::Show(int16_t *aReturn)
337 {
338 NS_ENSURE_ARG_POINTER(aReturn);
340 nsresult rv = Open(nullptr);
341 if (NS_FAILED(rv))
342 return rv;
344 while (mRunning) {
345 g_main_context_iteration(nullptr, TRUE);
346 }
348 *aReturn = mResult;
349 return NS_OK;
350 }
352 NS_IMETHODIMP
353 nsFilePicker::Open(nsIFilePickerShownCallback *aCallback)
354 {
355 // Can't show two dialogs concurrently with the same filepicker
356 if (mRunning)
357 return NS_ERROR_NOT_AVAILABLE;
359 nsXPIDLCString title;
360 title.Adopt(ToNewUTF8String(mTitle));
362 GtkWindow *parent_widget =
363 GTK_WINDOW(mParentWidget->GetNativeData(NS_NATIVE_SHELLWIDGET));
365 GtkFileChooserAction action = GetGtkFileChooserAction(mMode);
366 const gchar *accept_button = (action == GTK_FILE_CHOOSER_ACTION_SAVE)
367 ? GTK_STOCK_SAVE : GTK_STOCK_OPEN;
368 GtkWidget *file_chooser =
369 gtk_file_chooser_dialog_new(title, parent_widget, action,
370 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
371 accept_button, GTK_RESPONSE_ACCEPT,
372 nullptr);
373 gtk_dialog_set_alternative_button_order(GTK_DIALOG(file_chooser),
374 GTK_RESPONSE_ACCEPT,
375 GTK_RESPONSE_CANCEL,
376 -1);
377 if (mAllowURLs) {
378 gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(file_chooser), FALSE);
379 }
381 if (action == GTK_FILE_CHOOSER_ACTION_OPEN || 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), img_preview);
384 g_signal_connect(file_chooser, "update-preview", G_CALLBACK(UpdateFilePreviewWidget), img_preview);
385 }
387 GtkWindow *window = GTK_WINDOW(file_chooser);
388 gtk_window_set_modal(window, TRUE);
389 if (parent_widget) {
390 gtk_window_set_destroy_with_parent(window, TRUE);
391 }
393 NS_ConvertUTF16toUTF8 defaultName(mDefault);
394 switch (mMode) {
395 case nsIFilePicker::modeOpenMultiple:
396 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(file_chooser), TRUE);
397 break;
398 case nsIFilePicker::modeSave:
399 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser),
400 defaultName.get());
401 break;
402 }
404 nsCOMPtr<nsIFile> defaultPath;
405 if (mDisplayDirectory) {
406 mDisplayDirectory->Clone(getter_AddRefs(defaultPath));
407 } else if (mPrevDisplayDirectory) {
408 mPrevDisplayDirectory->Clone(getter_AddRefs(defaultPath));
409 }
411 if (defaultPath) {
412 if (!defaultName.IsEmpty() && mMode != nsIFilePicker::modeSave) {
413 // Try to select the intended file. Even if it doesn't exist, GTK still switches
414 // directories.
415 defaultPath->AppendNative(defaultName);
416 nsAutoCString path;
417 defaultPath->GetNativePath(path);
418 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(file_chooser), path.get());
419 } else {
420 nsAutoCString directory;
421 defaultPath->GetNativePath(directory);
422 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(file_chooser),
423 directory.get());
424 }
425 }
427 gtk_dialog_set_default_response(GTK_DIALOG(file_chooser), GTK_RESPONSE_ACCEPT);
429 int32_t count = mFilters.Length();
430 for (int32_t i = 0; i < count; ++i) {
431 // This is fun... the GTK file picker does not accept a list of filters
432 // so we need to split out each string, and add it manually.
434 char **patterns = g_strsplit(mFilters[i].get(), ";", -1);
435 if (!patterns) {
436 return NS_ERROR_OUT_OF_MEMORY;
437 }
439 GtkFileFilter *filter = gtk_file_filter_new();
440 for (int j = 0; patterns[j] != nullptr; ++j) {
441 nsAutoCString caseInsensitiveFilter = MakeCaseInsensitiveShellGlob(g_strstrip(patterns[j]));
442 gtk_file_filter_add_pattern(filter, caseInsensitiveFilter.get());
443 }
445 g_strfreev(patterns);
447 if (!mFilterNames[i].IsEmpty()) {
448 // If we have a name for our filter, let's use that.
449 const char *filter_name = mFilterNames[i].get();
450 gtk_file_filter_set_name(filter, filter_name);
451 } else {
452 // If we don't have a name, let's just use the filter pattern.
453 const char *filter_pattern = mFilters[i].get();
454 gtk_file_filter_set_name(filter, filter_pattern);
455 }
457 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(file_chooser), filter);
459 // Set the initially selected filter
460 if (mSelectedType == i) {
461 gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(file_chooser), filter);
462 }
463 }
465 gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(file_chooser), TRUE);
467 mRunning = true;
468 mCallback = aCallback;
469 NS_ADDREF_THIS();
470 g_signal_connect(file_chooser, "response", G_CALLBACK(OnResponse), this);
471 g_signal_connect(file_chooser, "destroy", G_CALLBACK(OnDestroy), this);
472 gtk_widget_show(file_chooser);
474 return NS_OK;
475 }
477 /* static */ void
478 nsFilePicker::OnResponse(GtkWidget* file_chooser, gint response_id,
479 gpointer user_data)
480 {
481 static_cast<nsFilePicker*>(user_data)->
482 Done(file_chooser, response_id);
483 }
485 /* static */ void
486 nsFilePicker::OnDestroy(GtkWidget* file_chooser, gpointer user_data)
487 {
488 static_cast<nsFilePicker*>(user_data)->
489 Done(file_chooser, GTK_RESPONSE_CANCEL);
490 }
492 void
493 nsFilePicker::Done(GtkWidget* file_chooser, gint response)
494 {
495 mRunning = false;
497 int16_t result;
498 switch (response) {
499 case GTK_RESPONSE_OK:
500 case GTK_RESPONSE_ACCEPT:
501 ReadValuesFromFileChooser(file_chooser);
502 result = nsIFilePicker::returnOK;
503 if (mMode == nsIFilePicker::modeSave) {
504 nsCOMPtr<nsIFile> file;
505 GetFile(getter_AddRefs(file));
506 if (file) {
507 bool exists = false;
508 file->Exists(&exists);
509 if (exists)
510 result = nsIFilePicker::returnReplace;
511 }
512 }
513 break;
515 case GTK_RESPONSE_CANCEL:
516 case GTK_RESPONSE_CLOSE:
517 case GTK_RESPONSE_DELETE_EVENT:
518 result = nsIFilePicker::returnCancel;
519 break;
521 default:
522 NS_WARNING("Unexpected response");
523 result = nsIFilePicker::returnCancel;
524 break;
525 }
527 // A "response" signal won't be sent again but "destroy" will be.
528 g_signal_handlers_disconnect_by_func(file_chooser,
529 FuncToGpointer(OnDestroy), this);
531 // When response_id is GTK_RESPONSE_DELETE_EVENT or when called from
532 // OnDestroy, the widget would be destroyed anyway but it is fine if
533 // gtk_widget_destroy is called more than once. gtk_widget_destroy has
534 // requests that any remaining references be released, but the reference
535 // count will not be decremented again if GtkWindow's reference has already
536 // been released.
537 gtk_widget_destroy(file_chooser);
539 if (mCallback) {
540 mCallback->Done(result);
541 mCallback = nullptr;
542 } else {
543 mResult = result;
544 }
545 NS_RELEASE_THIS();
546 }