1 // Copyright (c) 2013 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 <set> 6 7 #include "base/bind.h" 8 #include "base/bind_helpers.h" 9 #include "base/command_line.h" 10 #include "base/logging.h" 11 #include "base/message_loop/message_loop.h" 12 #include "base/message_loop/message_loop_proxy.h" 13 #include "base/nix/mime_util_xdg.h" 14 #include "base/nix/xdg_util.h" 15 #include "base/process/launch.h" 16 #include "base/strings/string_number_conversions.h" 17 #include "base/strings/string_util.h" 18 #include "base/strings/utf_string_conversions.h" 19 #include "base/threading/thread_restrictions.h" 20 #include "base/threading/worker_pool.h" 21 #include "grit/ui_strings.h" 22 #include "ui/base/l10n/l10n_util.h" 23 #include "ui/shell_dialogs/gtk/select_file_dialog_impl.h" 24 25 // These conflict with base/tracked_objects.h, so need to come last. 26 #include <gdk/gdkx.h> 27 #include <gtk/gtk.h> 28 29 namespace { 30 31 std::string GetTitle(const std::string& title, int message_id) { 32 return title.empty() ? l10n_util::GetStringUTF8(message_id) : title; 33 } 34 35 const char kKdialogBinary[] = "kdialog"; 36 37 // Implementation of SelectFileDialog that shows a KDE common dialog for 38 // choosing a file or folder. This acts as a modal dialog. 39 class SelectFileDialogImplKDE : public ui::SelectFileDialogImpl { 40 public: 41 SelectFileDialogImplKDE(Listener* listener, 42 ui::SelectFilePolicy* policy, 43 base::nix::DesktopEnvironment desktop); 44 45 protected: 46 virtual ~SelectFileDialogImplKDE(); 47 48 // SelectFileDialog implementation. 49 // |params| is user data we pass back via the Listener interface. 50 virtual void SelectFileImpl( 51 Type type, 52 const base::string16& title, 53 const base::FilePath& default_path, 54 const FileTypeInfo* file_types, 55 int file_type_index, 56 const base::FilePath::StringType& default_extension, 57 gfx::NativeWindow owning_window, 58 void* params) OVERRIDE; 59 60 private: 61 virtual bool HasMultipleFileTypeChoicesImpl() OVERRIDE; 62 63 struct KDialogParams { 64 // This constructor can only be run from the UI thread. 65 KDialogParams(const std::string& type, 66 const std::string& title, 67 const base::FilePath& default_path, 68 gfx::NativeWindow parent, 69 bool file_operation, 70 bool multiple_selection, 71 void* kdialog_params, 72 void(SelectFileDialogImplKDE::* callback)(const std::string&, 73 int, 74 void*)) 75 : type(type), 76 title(title), 77 default_path(default_path), 78 parent(parent), 79 file_operation(file_operation), 80 multiple_selection(multiple_selection), 81 kdialog_params(kdialog_params), 82 ui_loop_proxy( 83 base::MessageLoopForUI::current()->message_loop_proxy()), 84 callback(callback) {} 85 86 std::string type; 87 std::string title; 88 base::FilePath default_path; 89 gfx::NativeWindow parent; 90 bool file_operation; 91 bool multiple_selection; 92 void* kdialog_params; 93 scoped_refptr<base::MessageLoopProxy> ui_loop_proxy; 94 95 void (SelectFileDialogImplKDE::*callback)(const std::string&, int, void*); 96 }; 97 98 // Get the filters from |file_types_| and concatenate them into 99 // |filter_string|. 100 std::string GetMimeTypeFilterString(); 101 102 // Get KDialog command line representing the Argv array for KDialog. 103 void GetKDialogCommandLine(const std::string& type, const std::string& title, 104 const base::FilePath& default_path, gfx::NativeWindow parent, 105 bool file_operation, bool multiple_selection, CommandLine* command_line); 106 107 // Call KDialog on a worker thread and post results back to the caller 108 // thread. 109 void CallKDialogOutput(const KDialogParams& params); 110 111 // Notifies the listener that a single file was chosen. 112 void FileSelected(const base::FilePath& path, void* params); 113 114 // Notifies the listener that multiple files were chosen. 115 void MultiFilesSelected(const std::vector<base::FilePath>& files, 116 void* params); 117 118 // Notifies the listener that no file was chosen (the action was canceled). 119 // Dialog is passed so we can find that |params| pointer that was passed to 120 // us when we were told to show the dialog. 121 void FileNotSelected(void *params); 122 123 void CreateSelectFolderDialog(Type type, 124 const std::string& title, 125 const base::FilePath& default_path, 126 gfx::NativeWindow parent, void* params); 127 128 void CreateFileOpenDialog(const std::string& title, 129 const base::FilePath& default_path, 130 gfx::NativeWindow parent, void* params); 131 132 void CreateMultiFileOpenDialog(const std::string& title, 133 const base::FilePath& default_path, 134 gfx::NativeWindow parent, void* params); 135 136 void CreateSaveAsDialog(const std::string& title, 137 const base::FilePath& default_path, 138 gfx::NativeWindow parent, void* params); 139 140 // Common function for OnSelectSingleFileDialogResponse and 141 // OnSelectSingleFolderDialogResponse. 142 void SelectSingleFileHelper(const std::string& output, int exit_code, 143 void* params, bool allow_folder); 144 145 void OnSelectSingleFileDialogResponse(const std::string& output, 146 int exit_code, void* params); 147 void OnSelectMultiFileDialogResponse(const std::string& output, 148 int exit_code, void* params); 149 void OnSelectSingleFolderDialogResponse(const std::string& output, 150 int exit_code, void* params); 151 152 // Should be either DESKTOP_ENVIRONMENT_KDE3 or DESKTOP_ENVIRONMENT_KDE4. 153 base::nix::DesktopEnvironment desktop_; 154 155 DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImplKDE); 156 }; 157 158 SelectFileDialogImplKDE::SelectFileDialogImplKDE( 159 Listener* listener, 160 ui::SelectFilePolicy* policy, 161 base::nix::DesktopEnvironment desktop) 162 : SelectFileDialogImpl(listener, policy), 163 desktop_(desktop) { 164 DCHECK(desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 || 165 desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE4); 166 } 167 168 SelectFileDialogImplKDE::~SelectFileDialogImplKDE() { 169 } 170 171 // We ignore |default_extension|. 172 void SelectFileDialogImplKDE::SelectFileImpl( 173 Type type, 174 const base::string16& title, 175 const base::FilePath& default_path, 176 const FileTypeInfo* file_types, 177 int file_type_index, 178 const base::FilePath::StringType& default_extension, 179 gfx::NativeWindow owning_window, 180 void* params) { 181 type_ = type; 182 // |owning_window| can be null when user right-clicks on a downloadable item 183 // and chooses 'Open Link in New Tab' when 'Ask where to save each file 184 // before downloading.' preference is turned on. (http://crbug.com/29213) 185 if (owning_window) 186 parents_.insert(owning_window); 187 188 std::string title_string = UTF16ToUTF8(title); 189 190 file_type_index_ = file_type_index; 191 if (file_types) 192 file_types_ = *file_types; 193 else 194 file_types_.include_all_files = true; 195 196 switch (type) { 197 case SELECT_FOLDER: 198 case SELECT_UPLOAD_FOLDER: 199 CreateSelectFolderDialog(type, title_string, default_path, 200 owning_window, params); 201 return; 202 case SELECT_OPEN_FILE: 203 CreateFileOpenDialog(title_string, default_path, owning_window, 204 params); 205 return; 206 case SELECT_OPEN_MULTI_FILE: 207 CreateMultiFileOpenDialog(title_string, default_path, 208 owning_window, params); 209 return; 210 case SELECT_SAVEAS_FILE: 211 CreateSaveAsDialog(title_string, default_path, owning_window, 212 params); 213 return; 214 default: 215 NOTREACHED(); 216 return; 217 } 218 } 219 220 bool SelectFileDialogImplKDE::HasMultipleFileTypeChoicesImpl() { 221 return file_types_.extensions.size() > 1; 222 } 223 224 std::string SelectFileDialogImplKDE::GetMimeTypeFilterString() { 225 std::string filter_string; 226 // We need a filter set because the same mime type can appear multiple times. 227 std::set<std::string> filter_set; 228 for (size_t i = 0; i < file_types_.extensions.size(); ++i) { 229 for (size_t j = 0; j < file_types_.extensions[i].size(); ++j) { 230 if (!file_types_.extensions[i][j].empty()) { 231 std::string mime_type = base::nix::GetFileMimeType( 232 base::FilePath("name").ReplaceExtension( 233 file_types_.extensions[i][j])); 234 filter_set.insert(mime_type); 235 } 236 } 237 } 238 // Add the *.* filter, but only if we have added other filters (otherwise it 239 // is implied). 240 if (file_types_.include_all_files && !file_types_.extensions.empty()) 241 filter_set.insert("application/octet-stream"); 242 // Create the final output string. 243 filter_string.clear(); 244 for (std::set<std::string>::iterator it = filter_set.begin(); 245 it != filter_set.end(); ++it) { 246 filter_string.append(*it + " "); 247 } 248 return filter_string; 249 } 250 251 void SelectFileDialogImplKDE::CallKDialogOutput(const KDialogParams& params) { 252 CommandLine::StringVector cmd_vector; 253 cmd_vector.push_back(kKdialogBinary); 254 CommandLine command_line(cmd_vector); 255 GetKDialogCommandLine(params.type, params.title, params.default_path, 256 params.parent, params.file_operation, 257 params.multiple_selection, &command_line); 258 std::string output; 259 int exit_code; 260 // Get output from KDialog 261 base::GetAppOutputWithExitCode(command_line, &output, &exit_code); 262 if (!output.empty()) 263 output.erase(output.size() - 1); 264 // Now the dialog is no longer showing. We can erase its parent from the 265 // parent set. 266 std::set<GtkWindow*>::iterator iter = parents_.find(params.parent); 267 if (iter != parents_.end()) 268 parents_.erase(iter); 269 params.ui_loop_proxy->PostTask(FROM_HERE, 270 base::Bind(params.callback, this, output, exit_code, 271 params.kdialog_params)); 272 } 273 274 void SelectFileDialogImplKDE::GetKDialogCommandLine(const std::string& type, 275 const std::string& title, const base::FilePath& path, 276 gfx::NativeWindow parent, bool file_operation, bool multiple_selection, 277 CommandLine* command_line) { 278 CHECK(command_line); 279 280 // Attach to the current Chrome window. 281 GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET((parent))); 282 int window_id = GDK_DRAWABLE_XID(gdk_window); 283 command_line->AppendSwitchNative( 284 desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 ? "--embed" : "--attach", 285 base::IntToString(window_id)); 286 // Set the correct title for the dialog. 287 if (!title.empty()) 288 command_line->AppendSwitchNative("--title", title); 289 // Enable multiple file selection if we need to. 290 if (multiple_selection) { 291 command_line->AppendSwitch("--multiple"); 292 command_line->AppendSwitch("--separate-output"); 293 } 294 command_line->AppendSwitch(type); 295 // The path should never be empty. If it is, set it to PWD. 296 if (path.empty()) 297 command_line->AppendArgPath(base::FilePath(".")); 298 else 299 command_line->AppendArgPath(path); 300 // Depending on the type of the operation we need, get the path to the 301 // file/folder and set up mime type filters. 302 if (file_operation) 303 command_line->AppendArg(GetMimeTypeFilterString()); 304 VLOG(1) << "KDialog command line: " << command_line->GetCommandLineString(); 305 } 306 307 void SelectFileDialogImplKDE::FileSelected(const base::FilePath& path, 308 void* params) { 309 if (type_ == SELECT_SAVEAS_FILE) 310 *last_saved_path_ = path.DirName(); 311 else if (type_ == SELECT_OPEN_FILE) 312 *last_opened_path_ = path.DirName(); 313 else if (type_ == SELECT_FOLDER) 314 *last_opened_path_ = path; 315 else 316 NOTREACHED(); 317 if (listener_) { // What does the filter index actually do? 318 // TODO(dfilimon): Get a reasonable index value from somewhere. 319 listener_->FileSelected(path, 1, params); 320 } 321 } 322 323 void SelectFileDialogImplKDE::MultiFilesSelected( 324 const std::vector<base::FilePath>& files, void* params) { 325 *last_opened_path_ = files[0].DirName(); 326 if (listener_) 327 listener_->MultiFilesSelected(files, params); 328 } 329 330 void SelectFileDialogImplKDE::FileNotSelected(void* params) { 331 if (listener_) 332 listener_->FileSelectionCanceled(params); 333 } 334 335 void SelectFileDialogImplKDE::CreateSelectFolderDialog( 336 Type type, const std::string& title, const base::FilePath& default_path, 337 gfx::NativeWindow parent, void *params) { 338 int title_message_id = (type == SELECT_UPLOAD_FOLDER) ? 339 IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE : 340 IDS_SELECT_FOLDER_DIALOG_TITLE; 341 base::WorkerPool::PostTask(FROM_HERE, 342 base::Bind( 343 &SelectFileDialogImplKDE::CallKDialogOutput, 344 this, 345 KDialogParams( 346 "--getexistingdirectory", 347 GetTitle(title, title_message_id), 348 default_path.empty() ? *last_opened_path_ : default_path, 349 parent, false, false, params, 350 &SelectFileDialogImplKDE::OnSelectSingleFolderDialogResponse)), 351 true); 352 } 353 354 void SelectFileDialogImplKDE::CreateFileOpenDialog( 355 const std::string& title, const base::FilePath& default_path, 356 gfx::NativeWindow parent, void* params) { 357 base::WorkerPool::PostTask(FROM_HERE, 358 base::Bind( 359 &SelectFileDialogImplKDE::CallKDialogOutput, 360 this, 361 KDialogParams( 362 "--getopenfilename", 363 GetTitle(title, IDS_OPEN_FILE_DIALOG_TITLE), 364 default_path.empty() ? *last_opened_path_ : default_path, 365 parent, true, false, params, 366 &SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse)), 367 true); 368 } 369 370 void SelectFileDialogImplKDE::CreateMultiFileOpenDialog( 371 const std::string& title, const base::FilePath& default_path, 372 gfx::NativeWindow parent, void* params) { 373 base::WorkerPool::PostTask(FROM_HERE, 374 base::Bind( 375 &SelectFileDialogImplKDE::CallKDialogOutput, 376 this, 377 KDialogParams( 378 "--getopenfilename", 379 GetTitle(title, IDS_OPEN_FILES_DIALOG_TITLE), 380 default_path.empty() ? *last_opened_path_ : default_path, 381 parent, true, true, params, 382 &SelectFileDialogImplKDE::OnSelectMultiFileDialogResponse)), 383 true); 384 } 385 386 void SelectFileDialogImplKDE::CreateSaveAsDialog( 387 const std::string& title, const base::FilePath& default_path, 388 gfx::NativeWindow parent, void* params) { 389 base::WorkerPool::PostTask(FROM_HERE, 390 base::Bind( 391 &SelectFileDialogImplKDE::CallKDialogOutput, 392 this, 393 KDialogParams( 394 "--getsavefilename", 395 GetTitle(title, IDS_SAVE_AS_DIALOG_TITLE), 396 default_path.empty() ? *last_saved_path_ : default_path, 397 parent, true, false, params, 398 &SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse)), 399 true); 400 } 401 402 void SelectFileDialogImplKDE::SelectSingleFileHelper(const std::string& output, 403 int exit_code, void* params, bool allow_folder) { 404 VLOG(1) << "[kdialog] SingleFileResponse: " << output; 405 if (exit_code != 0 || output.empty()) { 406 FileNotSelected(params); 407 return; 408 } 409 410 base::FilePath path(output); 411 if (allow_folder) { 412 FileSelected(path, params); 413 return; 414 } 415 416 if (CallDirectoryExistsOnUIThread(path)) 417 FileNotSelected(params); 418 else 419 FileSelected(path, params); 420 } 421 422 void SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse( 423 const std::string& output, int exit_code, void* params) { 424 SelectSingleFileHelper(output, exit_code, params, false); 425 } 426 427 void SelectFileDialogImplKDE::OnSelectSingleFolderDialogResponse( 428 const std::string& output, int exit_code, void* params) { 429 SelectSingleFileHelper(output, exit_code, params, true); 430 } 431 432 void SelectFileDialogImplKDE::OnSelectMultiFileDialogResponse( 433 const std::string& output, int exit_code, void* params) { 434 VLOG(1) << "[kdialog] MultiFileResponse: " << output; 435 436 if (exit_code != 0 || output.empty()) { 437 FileNotSelected(params); 438 return; 439 } 440 441 std::vector<std::string> filenames; 442 Tokenize(output, "\n", &filenames); 443 std::vector<base::FilePath> filenames_fp; 444 for (std::vector<std::string>::iterator iter = filenames.begin(); 445 iter != filenames.end(); ++iter) { 446 base::FilePath path(*iter); 447 if (CallDirectoryExistsOnUIThread(path)) 448 continue; 449 filenames_fp.push_back(path); 450 } 451 452 if (filenames_fp.empty()) { 453 FileNotSelected(params); 454 return; 455 } 456 MultiFilesSelected(filenames_fp, params); 457 } 458 459 } // namespace 460 461 namespace ui { 462 463 // static 464 bool SelectFileDialogImpl::CheckKDEDialogWorksOnUIThread() { 465 // No choice. UI thread can't continue without an answer here. Fortunately we 466 // only do this once, the first time a file dialog is displayed. 467 base::ThreadRestrictions::ScopedAllowIO allow_io; 468 469 CommandLine::StringVector cmd_vector; 470 cmd_vector.push_back(kKdialogBinary); 471 cmd_vector.push_back("--version"); 472 CommandLine command_line(cmd_vector); 473 std::string dummy; 474 return base::GetAppOutput(command_line, &dummy); 475 } 476 477 // static 478 SelectFileDialogImpl* SelectFileDialogImpl::NewSelectFileDialogImplKDE( 479 Listener* listener, 480 ui::SelectFilePolicy* policy, 481 base::nix::DesktopEnvironment desktop) { 482 return new SelectFileDialogImplKDE(listener, policy, desktop); 483 } 484 485 } // namespace ui 486