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