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. 28 static const int kPreviewWidth = 256; 29 static 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. 33 class 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 158 FilePath* SelectFileDialogImpl::last_saved_path_ = NULL; 159 FilePath* SelectFileDialogImpl::last_opened_path_ = NULL; 160 161 // static 162 SelectFileDialog* SelectFileDialog::Create(Listener* listener) { 163 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 164 return new SelectFileDialogImpl(listener); 165 } 166 167 SelectFileDialogImpl::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 178 SelectFileDialogImpl::~SelectFileDialogImpl() { 179 while (dialogs_.begin() != dialogs_.end()) { 180 gtk_widget_destroy(*(dialogs_.begin())); 181 } 182 } 183 184 bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const { 185 return parents_.find(parent_window) != parents_.end(); 186 } 187 188 void SelectFileDialogImpl::ListenerDestroyed() { 189 listener_ = NULL; 190 } 191 192 // We ignore |default_extension|. 193 void 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 257 void 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 307 void 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 327 void 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 336 void SelectFileDialogImpl::FileNotSelected(GtkWidget* dialog) { 337 void* params = PopParamsForDialog(dialog); 338 if (listener_) 339 listener_->FileSelectionCanceled(params); 340 gtk_widget_destroy(dialog); 341 } 342 343 bool SelectFileDialogImpl::CallDirectoryExistsOnUIThread(const FilePath& path) { 344 base::ThreadRestrictions::ScopedAllowIO allow_io; 345 return file_util::DirectoryExists(path); 346 } 347 348 GtkWidget* 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 377 GtkWidget* 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 404 GtkWidget* 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 417 GtkWidget* 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 430 GtkWidget* 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 463 void* 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 471 void 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 489 bool 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 499 void 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 527 void SelectFileDialogImpl::OnSelectSingleFileDialogResponse(GtkWidget* dialog, 528 int response_id) { 529 SelectSingleFileHelper(dialog, response_id, false); 530 } 531 532 void SelectFileDialogImpl::OnSelectSingleFolderDialogResponse(GtkWidget* dialog, 533 int response_id) { 534 SelectSingleFileHelper(dialog, response_id, true); 535 } 536 537 void 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 567 void SelectFileDialogImpl::OnFileChooserDestroy(GtkWidget* dialog) { 568 FileDialogDestroyed(dialog); 569 } 570 571 void 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