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