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