1// Copyright (c) 2011 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.
4
5#include <gtk/gtk.h>
6#include <map>
7#include <set>
8
9#include "base/file_util.h"
10#include "base/logging.h"
11#include "base/message_loop.h"
12#include "base/mime_util.h"
13#include "base/sys_string_conversions.h"
14#include "base/threading/thread.h"
15#include "base/threading/thread_restrictions.h"
16#include "base/utf_string_conversions.h"
17#include "chrome/browser/ui/shell_dialogs.h"
18#include "content/browser/browser_thread.h"
19#include "grit/generated_resources.h"
20#include "ui/base/gtk/gtk_signal.h"
21#include "ui/base/l10n/l10n_util.h"
22
23// The size of the preview we display for selected image files. We set height
24// larger than width because generally there is more free space vertically
25// than horiztonally (setting the preview image will alway expand the width of
26// the dialog, but usually not the height). The image's aspect ratio will always
27// be preserved.
28static const int kPreviewWidth = 256;
29static const int kPreviewHeight = 512;
30
31// Implementation of SelectFileDialog that shows a Gtk common dialog for
32// choosing a file or folder. This acts as a modal dialog.
33class SelectFileDialogImpl : public SelectFileDialog {
34 public:
35  explicit SelectFileDialogImpl(Listener* listener);
36
37  // BaseShellDialog implementation.
38  virtual bool IsRunning(gfx::NativeWindow parent_window) const;
39  virtual void ListenerDestroyed();
40
41 protected:
42  // SelectFileDialog implementation.
43  // |params| is user data we pass back via the Listener interface.
44  virtual void SelectFileImpl(Type type,
45                              const string16& title,
46                              const FilePath& default_path,
47                              const FileTypeInfo* file_types,
48                              int file_type_index,
49                              const FilePath::StringType& default_extension,
50                              gfx::NativeWindow owning_window,
51                              void* params);
52
53 private:
54  virtual ~SelectFileDialogImpl();
55
56  // Add the filters from |file_types_| to |chooser|.
57  void AddFilters(GtkFileChooser* chooser);
58
59  void FileSelected(GtkWidget* dialog, const FilePath& path);
60
61  // Notifies the listener that multiple files were chosen.
62  void MultiFilesSelected(GtkWidget* dialog,
63                          const std::vector<FilePath>& files);
64
65  // Notifies the listener that no file was chosen (the action was canceled).
66  // Dialog is passed so we can find that |params| pointer that was passed to
67  // us when we were told to show the dialog.
68  void FileNotSelected(GtkWidget* dialog);
69
70  GtkWidget* CreateSelectFolderDialog(const std::string& title,
71      const FilePath& default_path, gfx::NativeWindow parent);
72
73  GtkWidget* CreateFileOpenDialog(const std::string& title,
74      const FilePath& default_path, gfx::NativeWindow parent);
75
76  GtkWidget* CreateMultiFileOpenDialog(const std::string& title,
77      const FilePath& default_path, gfx::NativeWindow parent);
78
79  GtkWidget* CreateSaveAsDialog(const std::string& title,
80      const FilePath& default_path, gfx::NativeWindow parent);
81
82  // Removes and returns the |params| associated with |dialog| from
83  // |params_map_|.
84  void* PopParamsForDialog(GtkWidget* dialog);
85
86  // Take care of internal data structures when a file dialog is destroyed.
87  void FileDialogDestroyed(GtkWidget* dialog);
88
89  // Check whether response_id corresponds to the user cancelling/closing the
90  // dialog. Used as a helper for the below callbacks.
91  bool IsCancelResponse(gint response_id);
92
93  // Common function for OnSelectSingleFileDialogResponse and
94  // OnSelectSingleFolderDialogResponse.
95  void SelectSingleFileHelper(GtkWidget* dialog,
96                              gint response_id,
97                              bool allow_folder);
98
99  // Common function for CreateFileOpenDialog and CreateMultiFileOpenDialog.
100  GtkWidget* CreateFileOpenHelper(const std::string& title,
101                                  const FilePath& default_path,
102                                  gfx::NativeWindow parent);
103
104  // Wrapper for file_util::DirectoryExists() that allow access on the UI
105  // thread. Use this only in the file dialog functions, where it's ok
106  // because the file dialog has to do many stats anyway. One more won't
107  // hurt too badly and it's likely already cached.
108  bool CallDirectoryExistsOnUIThread(const FilePath& path);
109
110  // Callback for when the user responds to a Save As or Open File dialog.
111  CHROMEGTK_CALLBACK_1(SelectFileDialogImpl, void,
112                       OnSelectSingleFileDialogResponse, int);
113
114  // Callback for when the user responds to a Select Folder dialog.
115  CHROMEGTK_CALLBACK_1(SelectFileDialogImpl, void,
116                       OnSelectSingleFolderDialogResponse, int);
117
118  // Callback for when the user responds to a Open Multiple Files dialog.
119  CHROMEGTK_CALLBACK_1(SelectFileDialogImpl, void,
120                       OnSelectMultiFileDialogResponse, int);
121
122  // Callback for when the file chooser gets destroyed.
123  CHROMEGTK_CALLBACK_0(SelectFileDialogImpl, void, OnFileChooserDestroy);
124
125  // Callback for when we update the preview for the selection.
126  CHROMEGTK_CALLBACK_0(SelectFileDialogImpl, void, OnUpdatePreview);
127
128  // A map from dialog windows to the |params| user data associated with them.
129  std::map<GtkWidget*, void*> params_map_;
130
131  // The file filters.
132  FileTypeInfo file_types_;
133
134  // The index of the default selected file filter.
135  // Note: This starts from 1, not 0.
136  size_t file_type_index_;
137
138  // The set of all parent windows for which we are currently running dialogs.
139  std::set<GtkWindow*> parents_;
140
141  // The type of dialog we are showing the user.
142  Type type_;
143
144  // These two variables track where the user last saved a file or opened a
145  // file so that we can display future dialogs with the same starting path.
146  static FilePath* last_saved_path_;
147  static FilePath* last_opened_path_;
148
149  // The GtkImage widget for showing previews of selected images.
150  GtkWidget* preview_;
151
152  // All our dialogs.
153  std::set<GtkWidget*> dialogs_;
154
155  DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl);
156};
157
158FilePath* SelectFileDialogImpl::last_saved_path_ = NULL;
159FilePath* SelectFileDialogImpl::last_opened_path_ = NULL;
160
161// static
162SelectFileDialog* SelectFileDialog::Create(Listener* listener) {
163  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
164  return new SelectFileDialogImpl(listener);
165}
166
167SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener)
168    : SelectFileDialog(listener),
169      file_type_index_(0),
170      type_(SELECT_NONE),
171      preview_(NULL) {
172  if (!last_saved_path_) {
173    last_saved_path_ = new FilePath();
174    last_opened_path_ = new FilePath();
175  }
176}
177
178SelectFileDialogImpl::~SelectFileDialogImpl() {
179  while (dialogs_.begin() != dialogs_.end()) {
180    gtk_widget_destroy(*(dialogs_.begin()));
181  }
182}
183
184bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const {
185  return parents_.find(parent_window) != parents_.end();
186}
187
188void SelectFileDialogImpl::ListenerDestroyed() {
189  listener_ = NULL;
190}
191
192// We ignore |default_extension|.
193void SelectFileDialogImpl::SelectFileImpl(
194    Type type,
195    const string16& title,
196    const FilePath& default_path,
197    const FileTypeInfo* file_types,
198    int file_type_index,
199    const FilePath::StringType& default_extension,
200    gfx::NativeWindow owning_window,
201    void* params) {
202  type_ = type;
203  // |owning_window| can be null when user right-clicks on a downloadable item
204  // and chooses 'Open Link in New Tab' when 'Ask where to save each file
205  // before downloading.' preference is turned on. (http://crbug.com/29213)
206  if (owning_window)
207    parents_.insert(owning_window);
208
209  std::string title_string = UTF16ToUTF8(title);
210
211  file_type_index_ = file_type_index;
212  if (file_types)
213    file_types_ = *file_types;
214  else
215    file_types_.include_all_files = true;
216
217  GtkWidget* dialog = NULL;
218  switch (type) {
219    case SELECT_FOLDER:
220      dialog = CreateSelectFolderDialog(title_string, default_path,
221                                        owning_window);
222      break;
223    case SELECT_OPEN_FILE:
224      dialog = CreateFileOpenDialog(title_string, default_path, owning_window);
225      break;
226    case SELECT_OPEN_MULTI_FILE:
227      dialog = CreateMultiFileOpenDialog(title_string, default_path,
228                                         owning_window);
229      break;
230    case SELECT_SAVEAS_FILE:
231      dialog = CreateSaveAsDialog(title_string, default_path, owning_window);
232      break;
233    default:
234      NOTREACHED();
235      return;
236  }
237  dialogs_.insert(dialog);
238
239  preview_ = gtk_image_new();
240  g_signal_connect(dialog, "destroy",
241                   G_CALLBACK(OnFileChooserDestroyThunk), this);
242  g_signal_connect(dialog, "update-preview",
243                   G_CALLBACK(OnUpdatePreviewThunk), this);
244  gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog), preview_);
245
246  params_map_[dialog] = params;
247
248  // Set window-to-parent modality by adding the dialog to the same window
249  // group as the parent.
250  gtk_window_group_add_window(gtk_window_get_group(owning_window),
251                              GTK_WINDOW(dialog));
252  gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
253
254  gtk_widget_show_all(dialog);
255}
256
257void SelectFileDialogImpl::AddFilters(GtkFileChooser* chooser) {
258  for (size_t i = 0; i < file_types_.extensions.size(); ++i) {
259    GtkFileFilter* filter = NULL;
260    for (size_t j = 0; j < file_types_.extensions[i].size(); ++j) {
261      if (!file_types_.extensions[i][j].empty()) {
262        if (!filter)
263          filter = gtk_file_filter_new();
264
265        // Allow IO in the file dialog. http://crbug.com/72637
266        base::ThreadRestrictions::ScopedAllowIO allow_io;
267        std::string mime_type = mime_util::GetFileMimeType(
268            FilePath("name").ReplaceExtension(file_types_.extensions[i][j]));
269        gtk_file_filter_add_mime_type(filter, mime_type.c_str());
270      }
271    }
272    // We didn't find any non-empty extensions to filter on.
273    if (!filter)
274      continue;
275
276    // The description vector may be blank, in which case we are supposed to
277    // use some sort of default description based on the filter.
278    if (i < file_types_.extension_description_overrides.size()) {
279      gtk_file_filter_set_name(filter, UTF16ToUTF8(
280          file_types_.extension_description_overrides[i]).c_str());
281    } else {
282      // Allow IO in the file dialog. http://crbug.com/72637
283      base::ThreadRestrictions::ScopedAllowIO allow_io;
284      // There is no system default filter description so we use
285      // the MIME type itself if the description is blank.
286      std::string mime_type = mime_util::GetFileMimeType(
287          FilePath("name").ReplaceExtension(file_types_.extensions[i][0]));
288      gtk_file_filter_set_name(filter, mime_type.c_str());
289    }
290
291    gtk_file_chooser_add_filter(chooser, filter);
292    if (i == file_type_index_ - 1)
293      gtk_file_chooser_set_filter(chooser, filter);
294  }
295
296  // Add the *.* filter, but only if we have added other filters (otherwise it
297  // is implied).
298  if (file_types_.include_all_files && !file_types_.extensions.empty()) {
299    GtkFileFilter* filter = gtk_file_filter_new();
300    gtk_file_filter_add_pattern(filter, "*");
301    gtk_file_filter_set_name(filter,
302        l10n_util::GetStringUTF8(IDS_SAVEAS_ALL_FILES).c_str());
303    gtk_file_chooser_add_filter(chooser, filter);
304  }
305}
306
307void SelectFileDialogImpl::FileSelected(GtkWidget* dialog,
308                                        const FilePath& path) {
309  if (type_ == SELECT_SAVEAS_FILE)
310    *last_saved_path_ = path.DirName();
311  else if (type_ == SELECT_OPEN_FILE || type_ == SELECT_FOLDER)
312    *last_opened_path_ = path.DirName();
313  else
314    NOTREACHED();
315
316  if (listener_) {
317    GtkFileFilter* selected_filter =
318        gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(dialog));
319    GSList* filters = gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(dialog));
320    int idx = g_slist_index(filters, selected_filter);
321    g_slist_free(filters);
322    listener_->FileSelected(path, idx + 1, PopParamsForDialog(dialog));
323  }
324  gtk_widget_destroy(dialog);
325}
326
327void SelectFileDialogImpl::MultiFilesSelected(GtkWidget* dialog,
328    const std::vector<FilePath>& files) {
329  *last_opened_path_ = files[0].DirName();
330
331  if (listener_)
332    listener_->MultiFilesSelected(files, PopParamsForDialog(dialog));
333  gtk_widget_destroy(dialog);
334}
335
336void SelectFileDialogImpl::FileNotSelected(GtkWidget* dialog) {
337  void* params = PopParamsForDialog(dialog);
338  if (listener_)
339    listener_->FileSelectionCanceled(params);
340  gtk_widget_destroy(dialog);
341}
342
343bool SelectFileDialogImpl::CallDirectoryExistsOnUIThread(const FilePath& path) {
344  base::ThreadRestrictions::ScopedAllowIO allow_io;
345  return file_util::DirectoryExists(path);
346}
347
348GtkWidget* SelectFileDialogImpl::CreateFileOpenHelper(
349    const std::string& title,
350    const FilePath& default_path,
351    gfx::NativeWindow parent) {
352  GtkWidget* dialog =
353      gtk_file_chooser_dialog_new(title.c_str(), parent,
354                                  GTK_FILE_CHOOSER_ACTION_OPEN,
355                                  GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
356                                  GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
357                                  NULL);
358  AddFilters(GTK_FILE_CHOOSER(dialog));
359
360  if (!default_path.empty()) {
361    if (CallDirectoryExistsOnUIThread(default_path)) {
362      gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
363                                          default_path.value().c_str());
364    } else {
365      // If the file doesn't exist, this will just switch to the correct
366      // directory. That's good enough.
367      gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),
368                                    default_path.value().c_str());
369    }
370  } else if (!last_opened_path_->empty()) {
371    gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
372                                        last_opened_path_->value().c_str());
373  }
374  return dialog;
375}
376
377GtkWidget* SelectFileDialogImpl::CreateSelectFolderDialog(
378    const std::string& title,
379    const FilePath& default_path,
380    gfx::NativeWindow parent) {
381  std::string title_string = !title.empty() ? title :
382        l10n_util::GetStringUTF8(IDS_SELECT_FOLDER_DIALOG_TITLE);
383
384  GtkWidget* dialog =
385      gtk_file_chooser_dialog_new(title_string.c_str(), parent,
386                                  GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
387                                  GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
388                                  GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
389                                  NULL);
390
391  if (!default_path.empty()) {
392    gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),
393                                  default_path.value().c_str());
394  } else if (!last_opened_path_->empty()) {
395    gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
396                                        last_opened_path_->value().c_str());
397  }
398  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
399  g_signal_connect(dialog, "response",
400                   G_CALLBACK(OnSelectSingleFolderDialogResponseThunk), this);
401  return dialog;
402}
403
404GtkWidget* SelectFileDialogImpl::CreateFileOpenDialog(
405    const std::string& title,
406    const FilePath& default_path,
407    gfx::NativeWindow parent) {
408  std::string title_string = !title.empty() ? title :
409        l10n_util::GetStringUTF8(IDS_OPEN_FILE_DIALOG_TITLE);
410  GtkWidget* dialog = CreateFileOpenHelper(title_string, default_path, parent);
411  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
412  g_signal_connect(dialog, "response",
413                   G_CALLBACK(OnSelectSingleFileDialogResponseThunk), this);
414  return dialog;
415}
416
417GtkWidget* SelectFileDialogImpl::CreateMultiFileOpenDialog(
418    const std::string& title,
419    const FilePath& default_path,
420    gfx::NativeWindow parent) {
421  std::string title_string = !title.empty() ? title :
422        l10n_util::GetStringUTF8(IDS_OPEN_FILES_DIALOG_TITLE);
423  GtkWidget* dialog = CreateFileOpenHelper(title_string, default_path, parent);
424  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE);
425  g_signal_connect(dialog, "response",
426                   G_CALLBACK(OnSelectMultiFileDialogResponseThunk), this);
427  return dialog;
428}
429
430GtkWidget* SelectFileDialogImpl::CreateSaveAsDialog(const std::string& title,
431    const FilePath& default_path, gfx::NativeWindow parent) {
432  std::string title_string = !title.empty() ? title :
433        l10n_util::GetStringUTF8(IDS_SAVE_AS_DIALOG_TITLE);
434
435  GtkWidget* dialog =
436      gtk_file_chooser_dialog_new(title_string.c_str(), parent,
437                                  GTK_FILE_CHOOSER_ACTION_SAVE,
438                                  GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
439                                  GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
440                                  NULL);
441
442  AddFilters(GTK_FILE_CHOOSER(dialog));
443  if (!default_path.empty()) {
444    // Since the file may not already exist, we use
445    // set_current_folder() followed by set_current_name(), as per the
446    // recommendation of the GTK docs.
447    gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
448        default_path.DirName().value().c_str());
449    gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),
450        default_path.BaseName().value().c_str());
451  } else if (!last_saved_path_->empty()) {
452    gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
453                                        last_saved_path_->value().c_str());
454  }
455  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
456  gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),
457                                                 TRUE);
458  g_signal_connect(dialog, "response",
459                   G_CALLBACK(OnSelectSingleFileDialogResponseThunk), this);
460  return dialog;
461}
462
463void* SelectFileDialogImpl::PopParamsForDialog(GtkWidget* dialog) {
464  std::map<GtkWidget*, void*>::iterator iter = params_map_.find(dialog);
465  DCHECK(iter != params_map_.end());
466  void* params = iter->second;
467  params_map_.erase(iter);
468  return params;
469}
470
471void SelectFileDialogImpl::FileDialogDestroyed(GtkWidget* dialog) {
472  dialogs_.erase(dialog);
473
474  // Parent may be NULL in a few cases: 1) on shutdown when
475  // AllBrowsersClosed() trigger this handler after all the browser
476  // windows got destroyed, or 2) when the parent tab has been opened by
477  // 'Open Link in New Tab' context menu on a downloadable item and
478  // the tab has no content (see the comment in SelectFile as well).
479  GtkWindow* parent = gtk_window_get_transient_for(GTK_WINDOW(dialog));
480  if (!parent)
481    return;
482  std::set<GtkWindow*>::iterator iter = parents_.find(parent);
483  if (iter != parents_.end())
484    parents_.erase(iter);
485  else
486    NOTREACHED();
487}
488
489bool SelectFileDialogImpl::IsCancelResponse(gint response_id) {
490  bool is_cancel = response_id == GTK_RESPONSE_CANCEL ||
491                   response_id == GTK_RESPONSE_DELETE_EVENT;
492  if (is_cancel)
493    return true;
494
495  DCHECK(response_id == GTK_RESPONSE_ACCEPT);
496  return false;
497}
498
499void SelectFileDialogImpl::SelectSingleFileHelper(GtkWidget* dialog,
500    gint response_id,
501    bool allow_folder) {
502  if (IsCancelResponse(response_id)) {
503    FileNotSelected(dialog);
504    return;
505  }
506
507  gchar* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
508  if (!filename) {
509    FileNotSelected(dialog);
510    return;
511  }
512
513  FilePath path(filename);
514  g_free(filename);
515
516  if (allow_folder) {
517    FileSelected(dialog, path);
518    return;
519  }
520
521  if (CallDirectoryExistsOnUIThread(path))
522    FileNotSelected(dialog);
523  else
524    FileSelected(dialog, path);
525}
526
527void SelectFileDialogImpl::OnSelectSingleFileDialogResponse(GtkWidget* dialog,
528                                                            int response_id) {
529  SelectSingleFileHelper(dialog, response_id, false);
530}
531
532void SelectFileDialogImpl::OnSelectSingleFolderDialogResponse(GtkWidget* dialog,
533                                                              int response_id) {
534  SelectSingleFileHelper(dialog, response_id, true);
535}
536
537void SelectFileDialogImpl::OnSelectMultiFileDialogResponse(GtkWidget* dialog,
538                                                           int response_id) {
539  if (IsCancelResponse(response_id)) {
540    FileNotSelected(dialog);
541    return;
542  }
543
544  GSList* filenames = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
545  if (!filenames) {
546    FileNotSelected(dialog);
547    return;
548  }
549
550  std::vector<FilePath> filenames_fp;
551  for (GSList* iter = filenames; iter != NULL; iter = g_slist_next(iter)) {
552    FilePath path(static_cast<char*>(iter->data));
553    g_free(iter->data);
554    if (CallDirectoryExistsOnUIThread(path))
555      continue;
556    filenames_fp.push_back(path);
557  }
558  g_slist_free(filenames);
559
560  if (filenames_fp.empty()) {
561    FileNotSelected(dialog);
562    return;
563  }
564  MultiFilesSelected(dialog, filenames_fp);
565}
566
567void SelectFileDialogImpl::OnFileChooserDestroy(GtkWidget* dialog) {
568  FileDialogDestroyed(dialog);
569}
570
571void SelectFileDialogImpl::OnUpdatePreview(GtkWidget* chooser) {
572  gchar* filename = gtk_file_chooser_get_preview_filename(
573      GTK_FILE_CHOOSER(chooser));
574  if (!filename)
575    return;
576  // This will preserve the image's aspect ratio.
577  GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file_at_size(filename, kPreviewWidth,
578                                                       kPreviewHeight, NULL);
579  g_free(filename);
580  if (pixbuf) {
581    gtk_image_set_from_pixbuf(GTK_IMAGE(preview_), pixbuf);
582    g_object_unref(pixbuf);
583  }
584  gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser),
585                                             pixbuf ? TRUE : FALSE);
586}
587