1 // Copyright (c) 2012 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.
12 // Xlib defines RootWindow
15 #include "base/file_util.h"
16 #include "base/logging.h"
17 #include "base/message_loop.h"
18 #include "base/string_util.h"
19 #include "base/sys_string_conversions.h"
20 #include "base/threading/thread.h"
21 #include "base/threading/thread_restrictions.h"
22 #include "base/utf_string_conversions.h"
23 #include "chrome/browser/ui/libgtk2ui/gtk2_signal.h"
24 #include "chrome/browser/ui/libgtk2ui/select_file_dialog_impl.h"
25 #include "grit/ui_strings.h"
26 #include "ui/aura/root_window.h"
27 #include "ui/aura/window_observer.h"
28 #include "ui/base/dialogs/select_file_dialog.h"
29 #include "ui/base/l10n/l10n_util.h"
33 const char kAuraTransientParent
[] = "aura-transient-parent";
35 // Set |dialog| as transient for |parent|, which will keep it on top and center
37 void SetGtkTransientForAura(GtkWidget
* dialog
, aura::Window
* parent
) {
38 gtk_widget_realize(dialog
);
39 GdkWindow
* gdk_window
= gtk_widget_get_window(dialog
);
41 // TODO(erg): Check to make sure we're using X11 if wayland or some other
42 // display server ever happens. Otherwise, this will crash.
43 XSetTransientForHint(GDK_WINDOW_XDISPLAY(gdk_window
),
44 GDK_WINDOW_XID(gdk_window
),
45 parent
->GetRootWindow()->GetAcceleratedWidget());
47 // We also set the |parent| as a property of |dialog|, so that we can unlink
49 g_object_set_data(G_OBJECT(dialog
), kAuraTransientParent
, parent
);
56 // Implementation of SelectFileDialog that shows a Gtk common dialog for
57 // choosing a file or folder. This acts as a modal dialog.
58 class SelectFileDialogImplGTK
: public SelectFileDialogImpl
,
59 public aura::WindowObserver
{
61 explicit SelectFileDialogImplGTK(Listener
* listener
,
62 ui::SelectFilePolicy
* policy
);
65 virtual ~SelectFileDialogImplGTK();
67 // SelectFileDialog implementation.
68 // |params| is user data we pass back via the Listener interface.
69 virtual void SelectFileImpl(Type type
,
70 const string16
& title
,
71 const FilePath
& default_path
,
72 const FileTypeInfo
* file_types
,
74 const FilePath::StringType
& default_extension
,
75 gfx::NativeWindow owning_window
,
76 void* params
) OVERRIDE
;
79 virtual bool HasMultipleFileTypeChoicesImpl() OVERRIDE
;
81 // Overridden from aura::WindowObserver:
82 virtual void OnWindowDestroying(aura::Window
* window
) OVERRIDE
;
84 // Add the filters from |file_types_| to |chooser|.
85 void AddFilters(GtkFileChooser
* chooser
);
87 // Notifies the listener that a single file was chosen.
88 void FileSelected(GtkWidget
* dialog
, const FilePath
& path
);
90 // Notifies the listener that multiple files were chosen.
91 void MultiFilesSelected(GtkWidget
* dialog
,
92 const std::vector
<FilePath
>& files
);
94 // Notifies the listener that no file was chosen (the action was canceled).
95 // Dialog is passed so we can find that |params| pointer that was passed to
96 // us when we were told to show the dialog.
97 void FileNotSelected(GtkWidget
* dialog
);
99 GtkWidget
* CreateSelectFolderDialog(const std::string
& title
,
100 const FilePath
& default_path
, gfx::NativeWindow parent
);
102 GtkWidget
* CreateFileOpenDialog(const std::string
& title
,
103 const FilePath
& default_path
, gfx::NativeWindow parent
);
105 GtkWidget
* CreateMultiFileOpenDialog(const std::string
& title
,
106 const FilePath
& default_path
, gfx::NativeWindow parent
);
108 GtkWidget
* CreateSaveAsDialog(const std::string
& title
,
109 const FilePath
& default_path
, gfx::NativeWindow parent
);
111 // Removes and returns the |params| associated with |dialog| from
113 void* PopParamsForDialog(GtkWidget
* dialog
);
115 // Take care of internal data structures when a file dialog is destroyed.
116 void FileDialogDestroyed(GtkWidget
* dialog
);
118 // Check whether response_id corresponds to the user cancelling/closing the
119 // dialog. Used as a helper for the below callbacks.
120 bool IsCancelResponse(gint response_id
);
122 // Common function for OnSelectSingleFileDialogResponse and
123 // OnSelectSingleFolderDialogResponse.
124 void SelectSingleFileHelper(GtkWidget
* dialog
,
128 // Common function for CreateFileOpenDialog and CreateMultiFileOpenDialog.
129 GtkWidget
* CreateFileOpenHelper(const std::string
& title
,
130 const FilePath
& default_path
,
131 gfx::NativeWindow parent
);
133 // Callback for when the user responds to a Save As or Open File dialog.
134 CHROMEGTK_CALLBACK_1(SelectFileDialogImplGTK
, void,
135 OnSelectSingleFileDialogResponse
, int);
137 // Callback for when the user responds to a Select Folder dialog.
138 CHROMEGTK_CALLBACK_1(SelectFileDialogImplGTK
, void,
139 OnSelectSingleFolderDialogResponse
, int);
141 // Callback for when the user responds to a Open Multiple Files dialog.
142 CHROMEGTK_CALLBACK_1(SelectFileDialogImplGTK
, void,
143 OnSelectMultiFileDialogResponse
, int);
145 // Callback for when the file chooser gets destroyed.
146 CHROMEGTK_CALLBACK_0(SelectFileDialogImplGTK
, void, OnFileChooserDestroy
);
148 // Callback for when we update the preview for the selection.
149 CHROMEGTK_CALLBACK_0(SelectFileDialogImplGTK
, void, OnUpdatePreview
);
151 // A map from dialog windows to the |params| user data associated with them.
152 std::map
<GtkWidget
*, void*> params_map_
;
154 // The GtkImage widget for showing previews of selected images.
158 std::set
<GtkWidget
*> dialogs_
;
160 DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImplGTK
);
163 // The size of the preview we display for selected image files. We set height
164 // larger than width because generally there is more free space vertically
165 // than horiztonally (setting the preview image will alway expand the width of
166 // the dialog, but usually not the height). The image's aspect ratio will always
168 static const int kPreviewWidth
= 256;
169 static const int kPreviewHeight
= 512;
171 SelectFileDialogImpl
* SelectFileDialogImpl::NewSelectFileDialogImplGTK(
172 Listener
* listener
, ui::SelectFilePolicy
* policy
) {
173 return new SelectFileDialogImplGTK(listener
, policy
);
176 SelectFileDialogImplGTK::SelectFileDialogImplGTK(Listener
* listener
,
177 ui::SelectFilePolicy
* policy
)
178 : SelectFileDialogImpl(listener
, policy
),
182 SelectFileDialogImplGTK::~SelectFileDialogImplGTK() {
183 while (dialogs_
.begin() != dialogs_
.end()) {
184 gtk_widget_destroy(*(dialogs_
.begin()));
188 bool SelectFileDialogImplGTK::HasMultipleFileTypeChoicesImpl() {
189 return file_types_
.extensions
.size() > 1;
192 void SelectFileDialogImplGTK::OnWindowDestroying(aura::Window
* window
) {
193 std::set
<aura::Window
*>::iterator iter
= parents_
.find(window
);
194 if (iter
!= parents_
.end()) {
195 (*iter
)->RemoveObserver(this);
196 parents_
.erase(iter
);
200 // We ignore |default_extension|.
201 void SelectFileDialogImplGTK::SelectFileImpl(
203 const string16
& title
,
204 const FilePath
& default_path
,
205 const FileTypeInfo
* file_types
,
207 const FilePath::StringType
& default_extension
,
208 gfx::NativeWindow owning_window
,
211 // |owning_window| can be null when user right-clicks on a downloadable item
212 // and chooses 'Open Link in New Tab' when 'Ask where to save each file
213 // before downloading.' preference is turned on. (http://crbug.com/29213)
215 owning_window
->AddObserver(this);
216 parents_
.insert(owning_window
);
219 std::string title_string
= UTF16ToUTF8(title
);
221 file_type_index_
= file_type_index
;
223 file_types_
= *file_types
;
225 GtkWidget
* dialog
= NULL
;
228 dialog
= CreateSelectFolderDialog(title_string
, default_path
,
231 case SELECT_OPEN_FILE
:
232 dialog
= CreateFileOpenDialog(title_string
, default_path
, owning_window
);
234 case SELECT_OPEN_MULTI_FILE
:
235 dialog
= CreateMultiFileOpenDialog(title_string
, default_path
,
238 case SELECT_SAVEAS_FILE
:
239 dialog
= CreateSaveAsDialog(title_string
, default_path
, owning_window
);
245 g_signal_connect(dialog
, "delete-event",
246 G_CALLBACK(gtk_widget_hide_on_delete
), NULL
);
247 dialogs_
.insert(dialog
);
249 preview_
= gtk_image_new();
250 g_signal_connect(dialog
, "destroy",
251 G_CALLBACK(OnFileChooserDestroyThunk
), this);
252 g_signal_connect(dialog
, "update-preview",
253 G_CALLBACK(OnUpdatePreviewThunk
), this);
254 gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog
), preview_
);
256 params_map_
[dialog
] = params
;
258 // TODO(erg): Figure out how to fake GTK window-to-parent modality without
259 // having the parent be a real GtkWindow.
260 gtk_window_set_modal(GTK_WINDOW(dialog
), TRUE
);
262 gtk_widget_show_all(dialog
);
265 void SelectFileDialogImplGTK::AddFilters(GtkFileChooser
* chooser
) {
266 for (size_t i
= 0; i
< file_types_
.extensions
.size(); ++i
) {
267 GtkFileFilter
* filter
= NULL
;
268 std::set
<std::string
> fallback_labels
;
270 for (size_t j
= 0; j
< file_types_
.extensions
[i
].size(); ++j
) {
271 const std::string
& current_extension
= file_types_
.extensions
[i
][j
];
272 if (!current_extension
.empty()) {
274 filter
= gtk_file_filter_new();
275 std::string pattern
= "*." + current_extension
;
276 gtk_file_filter_add_pattern(filter
, pattern
.c_str());
277 fallback_labels
.insert(pattern
);
280 // We didn't find any non-empty extensions to filter on.
284 // The description vector may be blank, in which case we are supposed to
285 // use some sort of default description based on the filter.
286 if (i
< file_types_
.extension_description_overrides
.size()) {
287 gtk_file_filter_set_name(filter
, UTF16ToUTF8(
288 file_types_
.extension_description_overrides
[i
]).c_str());
290 // There is no system default filter description so we use
291 // the extensions themselves if the description is blank.
292 std::vector
<std::string
> fallback_labels_vector(fallback_labels
.begin(),
293 fallback_labels
.end());
294 std::string fallback_label
= JoinString(fallback_labels_vector
, ',');
295 gtk_file_filter_set_name(filter
, fallback_label
.c_str());
298 gtk_file_chooser_add_filter(chooser
, filter
);
299 if (i
== file_type_index_
- 1)
300 gtk_file_chooser_set_filter(chooser
, filter
);
303 // Add the *.* filter, but only if we have added other filters (otherwise it
305 if (file_types_
.include_all_files
&& !file_types_
.extensions
.empty()) {
306 GtkFileFilter
* filter
= gtk_file_filter_new();
307 gtk_file_filter_add_pattern(filter
, "*");
308 gtk_file_filter_set_name(filter
,
309 l10n_util::GetStringUTF8(IDS_SAVEAS_ALL_FILES
).c_str());
310 gtk_file_chooser_add_filter(chooser
, filter
);
314 void SelectFileDialogImplGTK::FileSelected(GtkWidget
* dialog
,
315 const FilePath
& path
) {
316 if (type_
== SELECT_SAVEAS_FILE
)
317 *last_saved_path_
= path
.DirName();
318 else if (type_
== SELECT_OPEN_FILE
|| type_
== SELECT_FOLDER
)
319 *last_opened_path_
= path
.DirName();
324 GtkFileFilter
* selected_filter
=
325 gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(dialog
));
326 GSList
* filters
= gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(dialog
));
327 int idx
= g_slist_index(filters
, selected_filter
);
328 g_slist_free(filters
);
329 listener_
->FileSelected(path
, idx
+ 1, PopParamsForDialog(dialog
));
331 gtk_widget_destroy(dialog
);
334 void SelectFileDialogImplGTK::MultiFilesSelected(GtkWidget
* dialog
,
335 const std::vector
<FilePath
>& files
) {
336 *last_opened_path_
= files
[0].DirName();
339 listener_
->MultiFilesSelected(files
, PopParamsForDialog(dialog
));
340 gtk_widget_destroy(dialog
);
343 void SelectFileDialogImplGTK::FileNotSelected(GtkWidget
* dialog
) {
344 void* params
= PopParamsForDialog(dialog
);
346 listener_
->FileSelectionCanceled(params
);
347 gtk_widget_destroy(dialog
);
350 GtkWidget
* SelectFileDialogImplGTK::CreateFileOpenHelper(
351 const std::string
& title
,
352 const FilePath
& default_path
,
353 gfx::NativeWindow parent
) {
355 gtk_file_chooser_dialog_new(title
.c_str(), NULL
,
356 GTK_FILE_CHOOSER_ACTION_OPEN
,
357 GTK_STOCK_CANCEL
, GTK_RESPONSE_CANCEL
,
358 GTK_STOCK_OPEN
, GTK_RESPONSE_ACCEPT
,
360 SetGtkTransientForAura(dialog
, parent
);
361 AddFilters(GTK_FILE_CHOOSER(dialog
));
363 if (!default_path
.empty()) {
364 if (CallDirectoryExistsOnUIThread(default_path
)) {
365 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog
),
366 default_path
.value().c_str());
368 // If the file doesn't exist, this will just switch to the correct
369 // directory. That's good enough.
370 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog
),
371 default_path
.value().c_str());
373 } else if (!last_opened_path_
->empty()) {
374 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog
),
375 last_opened_path_
->value().c_str());
380 GtkWidget
* SelectFileDialogImplGTK::CreateSelectFolderDialog(
381 const std::string
& title
,
382 const FilePath
& default_path
,
383 gfx::NativeWindow parent
) {
384 std::string title_string
= !title
.empty() ? title
:
385 l10n_util::GetStringUTF8(IDS_SELECT_FOLDER_DIALOG_TITLE
);
388 gtk_file_chooser_dialog_new(title_string
.c_str(), NULL
,
389 GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER
,
390 GTK_STOCK_CANCEL
, GTK_RESPONSE_CANCEL
,
391 GTK_STOCK_OPEN
, GTK_RESPONSE_ACCEPT
,
393 SetGtkTransientForAura(dialog
, parent
);
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());
402 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog
), FALSE
);
403 g_signal_connect(dialog
, "response",
404 G_CALLBACK(OnSelectSingleFolderDialogResponseThunk
), this);
408 GtkWidget
* SelectFileDialogImplGTK::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
);
415 GtkWidget
* dialog
= CreateFileOpenHelper(title_string
, default_path
, parent
);
416 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog
), FALSE
);
417 g_signal_connect(dialog
, "response",
418 G_CALLBACK(OnSelectSingleFileDialogResponseThunk
), this);
422 GtkWidget
* SelectFileDialogImplGTK::CreateMultiFileOpenDialog(
423 const std::string
& title
,
424 const FilePath
& default_path
,
425 gfx::NativeWindow parent
) {
426 std::string title_string
= !title
.empty() ? title
:
427 l10n_util::GetStringUTF8(IDS_OPEN_FILES_DIALOG_TITLE
);
429 GtkWidget
* dialog
= CreateFileOpenHelper(title_string
, default_path
, parent
);
430 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog
), TRUE
);
431 g_signal_connect(dialog
, "response",
432 G_CALLBACK(OnSelectMultiFileDialogResponseThunk
), this);
436 GtkWidget
* SelectFileDialogImplGTK::CreateSaveAsDialog(const std::string
& title
,
437 const FilePath
& default_path
, gfx::NativeWindow parent
) {
438 std::string title_string
= !title
.empty() ? title
:
439 l10n_util::GetStringUTF8(IDS_SAVE_AS_DIALOG_TITLE
);
442 gtk_file_chooser_dialog_new(title_string
.c_str(), NULL
,
443 GTK_FILE_CHOOSER_ACTION_SAVE
,
444 GTK_STOCK_CANCEL
, GTK_RESPONSE_CANCEL
,
445 GTK_STOCK_SAVE
, GTK_RESPONSE_ACCEPT
,
447 SetGtkTransientForAura(dialog
, parent
);
449 AddFilters(GTK_FILE_CHOOSER(dialog
));
450 if (!default_path
.empty()) {
451 // Since the file may not already exist, we use
452 // set_current_folder() followed by set_current_name(), as per the
453 // recommendation of the GTK docs.
454 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog
),
455 default_path
.DirName().value().c_str());
456 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog
),
457 default_path
.BaseName().value().c_str());
458 } else if (!last_saved_path_
->empty()) {
459 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog
),
460 last_saved_path_
->value().c_str());
462 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog
), FALSE
);
463 gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog
),
465 g_signal_connect(dialog
, "response",
466 G_CALLBACK(OnSelectSingleFileDialogResponseThunk
), this);
470 void* SelectFileDialogImplGTK::PopParamsForDialog(GtkWidget
* dialog
) {
471 std::map
<GtkWidget
*, void*>::iterator iter
= params_map_
.find(dialog
);
472 DCHECK(iter
!= params_map_
.end());
473 void* params
= iter
->second
;
474 params_map_
.erase(iter
);
478 void SelectFileDialogImplGTK::FileDialogDestroyed(GtkWidget
* dialog
) {
479 dialogs_
.erase(dialog
);
481 // Parent may be NULL in a few cases: 1) on shutdown when
482 // AllBrowsersClosed() trigger this handler after all the browser
483 // windows got destroyed, or 2) when the parent tab has been opened by
484 // 'Open Link in New Tab' context menu on a downloadable item and
485 // the tab has no content (see the comment in SelectFile as well).
486 aura::Window
* parent
= reinterpret_cast<aura::Window
*>(
487 g_object_get_data(G_OBJECT(dialog
), kAuraTransientParent
));
490 std::set
<aura::Window
*>::iterator iter
= parents_
.find(parent
);
491 if (iter
!= parents_
.end()) {
492 (*iter
)->RemoveObserver(this);
493 parents_
.erase(iter
);
499 bool SelectFileDialogImplGTK::IsCancelResponse(gint response_id
) {
500 bool is_cancel
= response_id
== GTK_RESPONSE_CANCEL
||
501 response_id
== GTK_RESPONSE_DELETE_EVENT
;
505 DCHECK(response_id
== GTK_RESPONSE_ACCEPT
);
509 void SelectFileDialogImplGTK::SelectSingleFileHelper(GtkWidget
* dialog
,
512 if (IsCancelResponse(response_id
)) {
513 FileNotSelected(dialog
);
517 gchar
* filename
= gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog
));
519 FileNotSelected(dialog
);
523 FilePath
path(filename
);
527 FileSelected(dialog
, path
);
531 if (CallDirectoryExistsOnUIThread(path
))
532 FileNotSelected(dialog
);
534 FileSelected(dialog
, path
);
537 void SelectFileDialogImplGTK::OnSelectSingleFileDialogResponse(
538 GtkWidget
* dialog
, int response_id
) {
539 SelectSingleFileHelper(dialog
, response_id
, false);
542 void SelectFileDialogImplGTK::OnSelectSingleFolderDialogResponse(
543 GtkWidget
* dialog
, int response_id
) {
544 SelectSingleFileHelper(dialog
, response_id
, true);
547 void SelectFileDialogImplGTK::OnSelectMultiFileDialogResponse(GtkWidget
* dialog
,
549 if (IsCancelResponse(response_id
)) {
550 FileNotSelected(dialog
);
554 GSList
* filenames
= gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog
));
556 FileNotSelected(dialog
);
560 std::vector
<FilePath
> filenames_fp
;
561 for (GSList
* iter
= filenames
; iter
!= NULL
; iter
= g_slist_next(iter
)) {
562 FilePath
path(static_cast<char*>(iter
->data
));
564 if (CallDirectoryExistsOnUIThread(path
))
566 filenames_fp
.push_back(path
);
568 g_slist_free(filenames
);
570 if (filenames_fp
.empty()) {
571 FileNotSelected(dialog
);
574 MultiFilesSelected(dialog
, filenames_fp
);
577 void SelectFileDialogImplGTK::OnFileChooserDestroy(GtkWidget
* dialog
) {
578 FileDialogDestroyed(dialog
);
581 void SelectFileDialogImplGTK::OnUpdatePreview(GtkWidget
* chooser
) {
582 gchar
* filename
= gtk_file_chooser_get_preview_filename(
583 GTK_FILE_CHOOSER(chooser
));
586 // This will preserve the image's aspect ratio.
587 GdkPixbuf
* pixbuf
= gdk_pixbuf_new_from_file_at_size(filename
, kPreviewWidth
,
588 kPreviewHeight
, NULL
);
591 gtk_image_set_from_pixbuf(GTK_IMAGE(preview_
), pixbuf
);
592 g_object_unref(pixbuf
);
594 gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser
),
595 pixbuf
? TRUE
: FALSE
);
598 } // namespace libgtk2ui