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