1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
10 #include "base/file_util.h"
11 #include "base/logging.h"
12 #include "base/memory/scoped_ptr.h"
13 #include "base/message_loop/message_loop.h"
14 #include "base/strings/string_util.h"
15 #include "base/strings/sys_string_conversions.h"
16 #include "base/strings/utf_string_conversions.h"
17 #include "base/threading/thread.h"
18 #include "base/threading/thread_restrictions.h"
19 #include "grit/ui_strings.h"
20 #include "ui/base/gtk/gtk_signal.h"
21 #include "ui/base/l10n/l10n_util.h"
22 #include "ui/shell_dialogs/gtk/select_file_dialog_impl.h"
23 #include "ui/shell_dialogs/select_file_dialog.h"
27 // Makes sure that .jpg also shows .JPG.
28 gboolean FileFilterCaseInsensitive(const GtkFileFilterInfo* file_info,
29 std::string* file_extension) {
30 return EndsWith(file_info->filename, *file_extension, false);
33 // Deletes |data| when gtk_file_filter_add_custom() is done with it.
34 void OnFileFilterDataDestroyed(std::string* file_extension) {
35 delete file_extension;
38 // Implementation of SelectFileDialog that shows a Gtk common dialog for
39 // choosing a file or folder. This acts as a modal dialog.
40 class SelectFileDialogImplGTK : public ui::SelectFileDialogImpl {
42 explicit SelectFileDialogImplGTK(Listener* listener,
43 ui::SelectFilePolicy* policy);
46 virtual ~SelectFileDialogImplGTK();
48 // SelectFileDialog implementation.
49 // |params| is user data we pass back via the Listener interface.
50 virtual void SelectFileImpl(
52 const base::string16& title,
53 const base::FilePath& default_path,
54 const FileTypeInfo* file_types,
56 const base::FilePath::StringType& default_extension,
57 gfx::NativeWindow owning_window,
58 void* params) OVERRIDE;
61 virtual bool HasMultipleFileTypeChoicesImpl() OVERRIDE;
63 // Add the filters from |file_types_| to |chooser|.
64 void AddFilters(GtkFileChooser* chooser);
66 // Notifies the listener that a single file was chosen.
67 void FileSelected(GtkWidget* dialog, const base::FilePath& path);
69 // Notifies the listener that multiple files were chosen.
70 void MultiFilesSelected(GtkWidget* dialog,
71 const std::vector<base::FilePath>& files);
73 // Notifies the listener that no file was chosen (the action was canceled).
74 // Dialog is passed so we can find that |params| pointer that was passed to
75 // us when we were told to show the dialog.
76 void FileNotSelected(GtkWidget* dialog);
78 GtkWidget* CreateSelectFolderDialog(
80 const std::string& title,
81 const base::FilePath& default_path,
82 gfx::NativeWindow parent);
84 GtkWidget* CreateFileOpenDialog(const std::string& title,
85 const base::FilePath& default_path, gfx::NativeWindow parent);
87 GtkWidget* CreateMultiFileOpenDialog(const std::string& title,
88 const base::FilePath& default_path, gfx::NativeWindow parent);
90 GtkWidget* CreateSaveAsDialog(const std::string& title,
91 const base::FilePath& default_path, gfx::NativeWindow parent);
93 // Removes and returns the |params| associated with |dialog| from
95 void* PopParamsForDialog(GtkWidget* dialog);
97 // Take care of internal data structures when a file dialog is destroyed.
98 void FileDialogDestroyed(GtkWidget* dialog);
100 // Check whether response_id corresponds to the user cancelling/closing the
101 // dialog. Used as a helper for the below callbacks.
102 bool IsCancelResponse(gint response_id);
104 // Common function for OnSelectSingleFileDialogResponse and
105 // OnSelectSingleFolderDialogResponse.
106 void SelectSingleFileHelper(GtkWidget* dialog,
110 // Common function for CreateFileOpenDialog and CreateMultiFileOpenDialog.
111 GtkWidget* CreateFileOpenHelper(const std::string& title,
112 const base::FilePath& default_path,
113 gfx::NativeWindow parent);
115 // Callback for when the user responds to a Save As or Open File dialog.
116 CHROMEGTK_CALLBACK_1(SelectFileDialogImplGTK, void,
117 OnSelectSingleFileDialogResponse, int);
119 // Callback for when the user responds to a Select Folder dialog.
120 CHROMEGTK_CALLBACK_1(SelectFileDialogImplGTK, void,
121 OnSelectSingleFolderDialogResponse, int);
123 // Callback for when the user responds to a Open Multiple Files dialog.
124 CHROMEGTK_CALLBACK_1(SelectFileDialogImplGTK, void,
125 OnSelectMultiFileDialogResponse, int);
127 // Callback for when the file chooser gets destroyed.
128 CHROMEGTK_CALLBACK_0(SelectFileDialogImplGTK, void, OnFileChooserDestroy);
130 // Callback for when we update the preview for the selection.
131 CHROMEGTK_CALLBACK_0(SelectFileDialogImplGTK, void, OnUpdatePreview);
133 // A map from dialog windows to the |params| user data associated with them.
134 std::map<GtkWidget*, void*> params_map_;
136 // The GtkImage widget for showing previews of selected images.
140 std::set<GtkWidget*> dialogs_;
142 DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImplGTK);
145 // The size of the preview we display for selected image files. We set height
146 // larger than width because generally there is more free space vertically
147 // than horiztonally (setting the preview image will alway expand the width of
148 // the dialog, but usually not the height). The image's aspect ratio will always
150 static const int kPreviewWidth = 256;
151 static const int kPreviewHeight = 512;
153 SelectFileDialogImplGTK::SelectFileDialogImplGTK(Listener* listener,
154 ui::SelectFilePolicy* policy)
155 : SelectFileDialogImpl(listener, policy),
159 SelectFileDialogImplGTK::~SelectFileDialogImplGTK() {
160 while (dialogs_.begin() != dialogs_.end()) {
161 gtk_widget_destroy(*(dialogs_.begin()));
165 bool SelectFileDialogImplGTK::HasMultipleFileTypeChoicesImpl() {
166 return file_types_.extensions.size() > 1;
169 // We ignore |default_extension|.
170 void SelectFileDialogImplGTK::SelectFileImpl(
172 const base::string16& title,
173 const base::FilePath& default_path,
174 const FileTypeInfo* file_types,
176 const base::FilePath::StringType& default_extension,
177 gfx::NativeWindow owning_window,
180 // |owning_window| can be null when user right-clicks on a downloadable item
181 // and chooses 'Open Link in New Tab' when 'Ask where to save each file
182 // before downloading.' preference is turned on. (http://crbug.com/29213)
184 parents_.insert(owning_window);
186 std::string title_string = UTF16ToUTF8(title);
188 file_type_index_ = file_type_index;
190 file_types_ = *file_types;
192 file_types_.include_all_files = true;
194 GtkWidget* dialog = NULL;
197 case SELECT_UPLOAD_FOLDER:
198 dialog = CreateSelectFolderDialog(type, title_string, default_path,
201 case SELECT_OPEN_FILE:
202 dialog = CreateFileOpenDialog(title_string, default_path, owning_window);
204 case SELECT_OPEN_MULTI_FILE:
205 dialog = CreateMultiFileOpenDialog(title_string, default_path,
208 case SELECT_SAVEAS_FILE:
209 dialog = CreateSaveAsDialog(title_string, default_path, owning_window);
215 g_signal_connect(dialog, "delete-event",
216 G_CALLBACK(gtk_widget_hide_on_delete), NULL);
217 dialogs_.insert(dialog);
219 preview_ = gtk_image_new();
220 g_signal_connect(dialog, "destroy",
221 G_CALLBACK(OnFileChooserDestroyThunk), this);
222 g_signal_connect(dialog, "update-preview",
223 G_CALLBACK(OnUpdatePreviewThunk), this);
224 gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog), preview_);
226 params_map_[dialog] = params;
228 // Set window-to-parent modality by adding the dialog to the same window
229 // group as the parent.
230 gtk_window_group_add_window(gtk_window_get_group(owning_window),
232 gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
234 gtk_widget_show_all(dialog);
237 void SelectFileDialogImplGTK::AddFilters(GtkFileChooser* chooser) {
238 for (size_t i = 0; i < file_types_.extensions.size(); ++i) {
239 GtkFileFilter* filter = NULL;
240 std::set<std::string> fallback_labels;
242 for (size_t j = 0; j < file_types_.extensions[i].size(); ++j) {
243 const std::string& current_extension = file_types_.extensions[i][j];
244 if (!current_extension.empty()) {
246 filter = gtk_file_filter_new();
247 scoped_ptr<std::string> file_extension(
248 new std::string("." + current_extension));
249 fallback_labels.insert(std::string("*").append(*file_extension));
250 gtk_file_filter_add_custom(
252 GTK_FILE_FILTER_FILENAME,
253 reinterpret_cast<GtkFileFilterFunc>(FileFilterCaseInsensitive),
254 file_extension.release(),
255 reinterpret_cast<GDestroyNotify>(OnFileFilterDataDestroyed));
258 // We didn't find any non-empty extensions to filter on.
262 // The description vector may be blank, in which case we are supposed to
263 // use some sort of default description based on the filter.
264 if (i < file_types_.extension_description_overrides.size()) {
265 gtk_file_filter_set_name(filter, UTF16ToUTF8(
266 file_types_.extension_description_overrides[i]).c_str());
268 // There is no system default filter description so we use
269 // the extensions themselves if the description is blank.
270 std::vector<std::string> fallback_labels_vector(fallback_labels.begin(),
271 fallback_labels.end());
272 std::string fallback_label = JoinString(fallback_labels_vector, ',');
273 gtk_file_filter_set_name(filter, fallback_label.c_str());
276 gtk_file_chooser_add_filter(chooser, filter);
277 if (i == file_type_index_ - 1)
278 gtk_file_chooser_set_filter(chooser, filter);
281 // Add the *.* filter, but only if we have added other filters (otherwise it
283 if (file_types_.include_all_files && !file_types_.extensions.empty()) {
284 GtkFileFilter* filter = gtk_file_filter_new();
285 gtk_file_filter_add_pattern(filter, "*");
286 gtk_file_filter_set_name(filter,
287 l10n_util::GetStringUTF8(IDS_SAVEAS_ALL_FILES).c_str());
288 gtk_file_chooser_add_filter(chooser, filter);
292 void SelectFileDialogImplGTK::FileSelected(GtkWidget* dialog,
293 const base::FilePath& path) {
294 if (type_ == SELECT_SAVEAS_FILE)
295 *last_saved_path_ = path.DirName();
296 else if (type_ == SELECT_OPEN_FILE || type_ == SELECT_FOLDER)
297 *last_opened_path_ = path.DirName();
302 GtkFileFilter* selected_filter =
303 gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(dialog));
304 GSList* filters = gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(dialog));
305 int idx = g_slist_index(filters, selected_filter);
306 g_slist_free(filters);
307 listener_->FileSelected(path, idx + 1, PopParamsForDialog(dialog));
309 gtk_widget_destroy(dialog);
312 void SelectFileDialogImplGTK::MultiFilesSelected(GtkWidget* dialog,
313 const std::vector<base::FilePath>& files) {
314 *last_opened_path_ = files[0].DirName();
317 listener_->MultiFilesSelected(files, PopParamsForDialog(dialog));
318 gtk_widget_destroy(dialog);
321 void SelectFileDialogImplGTK::FileNotSelected(GtkWidget* dialog) {
322 void* params = PopParamsForDialog(dialog);
324 listener_->FileSelectionCanceled(params);
325 gtk_widget_destroy(dialog);
328 GtkWidget* SelectFileDialogImplGTK::CreateFileOpenHelper(
329 const std::string& title,
330 const base::FilePath& default_path,
331 gfx::NativeWindow parent) {
333 gtk_file_chooser_dialog_new(title.c_str(), parent,
334 GTK_FILE_CHOOSER_ACTION_OPEN,
335 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
336 GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
338 AddFilters(GTK_FILE_CHOOSER(dialog));
340 if (!default_path.empty()) {
341 if (CallDirectoryExistsOnUIThread(default_path)) {
342 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
343 default_path.value().c_str());
345 // If the file doesn't exist, this will just switch to the correct
346 // directory. That's good enough.
347 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),
348 default_path.value().c_str());
350 } else if (!last_opened_path_->empty()) {
351 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
352 last_opened_path_->value().c_str());
357 GtkWidget* SelectFileDialogImplGTK::CreateSelectFolderDialog(
359 const std::string& title,
360 const base::FilePath& default_path,
361 gfx::NativeWindow parent) {
362 std::string title_string = title;
363 if (title_string.empty()) {
364 title_string = (type == SELECT_UPLOAD_FOLDER) ?
365 l10n_util::GetStringUTF8(IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE) :
366 l10n_util::GetStringUTF8(IDS_SELECT_FOLDER_DIALOG_TITLE);
368 std::string accept_button_label = (type == SELECT_UPLOAD_FOLDER) ?
369 l10n_util::GetStringUTF8(IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON) :
373 gtk_file_chooser_dialog_new(title_string.c_str(), parent,
374 GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
375 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
376 accept_button_label.c_str(),
380 if (!default_path.empty()) {
381 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),
382 default_path.value().c_str());
383 } else if (!last_opened_path_->empty()) {
384 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
385 last_opened_path_->value().c_str());
387 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
388 g_signal_connect(dialog, "response",
389 G_CALLBACK(OnSelectSingleFolderDialogResponseThunk), this);
393 GtkWidget* SelectFileDialogImplGTK::CreateFileOpenDialog(
394 const std::string& title,
395 const base::FilePath& default_path,
396 gfx::NativeWindow parent) {
397 std::string title_string = !title.empty() ? title :
398 l10n_util::GetStringUTF8(IDS_OPEN_FILE_DIALOG_TITLE);
400 GtkWidget* dialog = CreateFileOpenHelper(title_string, default_path, parent);
401 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
402 g_signal_connect(dialog, "response",
403 G_CALLBACK(OnSelectSingleFileDialogResponseThunk), this);
407 GtkWidget* SelectFileDialogImplGTK::CreateMultiFileOpenDialog(
408 const std::string& title,
409 const base::FilePath& default_path,
410 gfx::NativeWindow parent) {
411 std::string title_string = !title.empty() ? title :
412 l10n_util::GetStringUTF8(IDS_OPEN_FILES_DIALOG_TITLE);
414 GtkWidget* dialog = CreateFileOpenHelper(title_string, default_path, parent);
415 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE);
416 g_signal_connect(dialog, "response",
417 G_CALLBACK(OnSelectMultiFileDialogResponseThunk), this);
421 GtkWidget* SelectFileDialogImplGTK::CreateSaveAsDialog(const std::string& title,
422 const base::FilePath& default_path, gfx::NativeWindow parent) {
423 std::string title_string = !title.empty() ? title :
424 l10n_util::GetStringUTF8(IDS_SAVE_AS_DIALOG_TITLE);
427 gtk_file_chooser_dialog_new(title_string.c_str(), parent,
428 GTK_FILE_CHOOSER_ACTION_SAVE,
429 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
430 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
433 AddFilters(GTK_FILE_CHOOSER(dialog));
434 if (!default_path.empty()) {
435 // Since the file may not already exist, we use
436 // set_current_folder() followed by set_current_name(), as per the
437 // recommendation of the GTK docs.
438 if (CallDirectoryExistsOnUIThread(default_path)) {
439 gtk_file_chooser_set_current_folder(
440 GTK_FILE_CHOOSER(dialog), default_path.value().c_str());
441 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "");
443 gtk_file_chooser_set_current_folder(
444 GTK_FILE_CHOOSER(dialog), default_path.DirName().value().c_str());
445 gtk_file_chooser_set_current_name(
446 GTK_FILE_CHOOSER(dialog), default_path.BaseName().value().c_str());
448 } else if (!last_saved_path_->empty()) {
449 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
450 last_saved_path_->value().c_str());
452 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
453 gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),
455 g_signal_connect(dialog, "response",
456 G_CALLBACK(OnSelectSingleFileDialogResponseThunk), this);
460 void* SelectFileDialogImplGTK::PopParamsForDialog(GtkWidget* dialog) {
461 std::map<GtkWidget*, void*>::iterator iter = params_map_.find(dialog);
462 DCHECK(iter != params_map_.end());
463 void* params = iter->second;
464 params_map_.erase(iter);
468 void SelectFileDialogImplGTK::FileDialogDestroyed(GtkWidget* dialog) {
469 dialogs_.erase(dialog);
471 // Parent may be NULL in a few cases: 1) on shutdown when
472 // AllBrowsersClosed() trigger this handler after all the browser
473 // windows got destroyed, or 2) when the parent tab has been opened by
474 // 'Open Link in New Tab' context menu on a downloadable item and
475 // the tab has no content (see the comment in SelectFile as well).
476 GtkWindow* parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
479 std::set<GtkWindow*>::iterator iter = parents_.find(parent);
480 if (iter != parents_.end())
481 parents_.erase(iter);
486 bool SelectFileDialogImplGTK::IsCancelResponse(gint response_id) {
487 bool is_cancel = response_id == GTK_RESPONSE_CANCEL ||
488 response_id == GTK_RESPONSE_DELETE_EVENT;
492 DCHECK(response_id == GTK_RESPONSE_ACCEPT);
496 void SelectFileDialogImplGTK::SelectSingleFileHelper(GtkWidget* dialog,
499 if (IsCancelResponse(response_id)) {
500 FileNotSelected(dialog);
504 gchar* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
506 FileNotSelected(dialog);
510 base::FilePath path(filename);
514 FileSelected(dialog, path);
518 if (CallDirectoryExistsOnUIThread(path))
519 FileNotSelected(dialog);
521 FileSelected(dialog, path);
524 void SelectFileDialogImplGTK::OnSelectSingleFileDialogResponse(
525 GtkWidget* dialog, int response_id) {
526 SelectSingleFileHelper(dialog, response_id, false);
529 void SelectFileDialogImplGTK::OnSelectSingleFolderDialogResponse(
530 GtkWidget* dialog, int response_id) {
531 SelectSingleFileHelper(dialog, response_id, true);
534 void SelectFileDialogImplGTK::OnSelectMultiFileDialogResponse(GtkWidget* dialog,
536 if (IsCancelResponse(response_id)) {
537 FileNotSelected(dialog);
541 GSList* filenames = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
543 FileNotSelected(dialog);
547 std::vector<base::FilePath> filenames_fp;
548 for (GSList* iter = filenames; iter != NULL; iter = g_slist_next(iter)) {
549 base::FilePath path(static_cast<char*>(iter->data));
551 if (CallDirectoryExistsOnUIThread(path))
553 filenames_fp.push_back(path);
555 g_slist_free(filenames);
557 if (filenames_fp.empty()) {
558 FileNotSelected(dialog);
561 MultiFilesSelected(dialog, filenames_fp);
564 void SelectFileDialogImplGTK::OnFileChooserDestroy(GtkWidget* dialog) {
565 FileDialogDestroyed(dialog);
568 void SelectFileDialogImplGTK::OnUpdatePreview(GtkWidget* chooser) {
569 gchar* filename = gtk_file_chooser_get_preview_filename(
570 GTK_FILE_CHOOSER(chooser));
573 // This will preserve the image's aspect ratio.
574 GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file_at_size(filename, kPreviewWidth,
575 kPreviewHeight, NULL);
578 gtk_image_set_from_pixbuf(GTK_IMAGE(preview_), pixbuf);
579 g_object_unref(pixbuf);
581 gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser),
582 pixbuf ? TRUE : FALSE);
589 SelectFileDialogImpl* SelectFileDialogImpl::NewSelectFileDialogImplGTK(
590 Listener* listener, ui::SelectFilePolicy* policy) {
591 return new SelectFileDialogImplGTK(listener, policy);