// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <set> #include "base/bind.h" #include "base/bind_helpers.h" #include "base/command_line.h" #include "base/logging.h" #include "base/nix/mime_util_xdg.h" #include "base/nix/xdg_util.h" #include "base/process_util.h" #include "base/string_number_conversions.h" #include "base/string_util.h" #include "base/threading/thread_restrictions.h" #include "base/utf_string_conversions.h" #include "chrome/browser/ui/gtk/select_file_dialog_impl.h" #include "content/public/browser/browser_thread.h" #include "grit/generated_resources.h" #include "ui/base/l10n/l10n_util.h" // These conflict with base/tracked_objects.h, so need to come last. #include <gdk/gdkx.h> #include <gtk/gtk.h> using content::BrowserThread; namespace { std::string GetTitle(const std::string& title, int message_id) { return title.empty() ? l10n_util::GetStringUTF8(message_id) : title; } const char kKdialogBinary[] = "kdialog"; } // namespace // Implementation of SelectFileDialog that shows a KDE common dialog for // choosing a file or folder. This acts as a modal dialog. class SelectFileDialogImplKDE : public SelectFileDialogImpl { public: SelectFileDialogImplKDE(Listener* listener, base::nix::DesktopEnvironment desktop); protected: virtual ~SelectFileDialogImplKDE(); // SelectFileDialog implementation. // |params| is user data we pass back via the Listener interface. virtual void SelectFileImpl(Type type, const string16& title, const FilePath& default_path, const FileTypeInfo* file_types, int file_type_index, const FilePath::StringType& default_extension, gfx::NativeWindow owning_window, void* params) OVERRIDE; private: virtual bool HasMultipleFileTypeChoicesImpl() OVERRIDE; struct KDialogParams { KDialogParams(const std::string& type, const std::string& title, const FilePath& default_path, gfx::NativeWindow parent, bool file_operation, bool multiple_selection, void* kdialog_params, void (SelectFileDialogImplKDE::*callback)(const std::string&, int, void*)) : type(type), title(title), default_path(default_path), parent(parent), file_operation(file_operation), multiple_selection(multiple_selection), kdialog_params(kdialog_params), callback(callback) { } std::string type; std::string title; FilePath default_path; gfx::NativeWindow parent; bool file_operation; bool multiple_selection; void* kdialog_params; void (SelectFileDialogImplKDE::*callback)(const std::string&, int, void*); }; // Get the filters from |file_types_| and concatenate them into // |filter_string|. std::string GetMimeTypeFilterString(); // Get KDialog command line representing the Argv array for KDialog. void GetKDialogCommandLine(const std::string& type, const std::string& title, const FilePath& default_path, gfx::NativeWindow parent, bool file_operation, bool multiple_selection, CommandLine* command_line); // Call KDialog on the FILE thread and post results back to the UI thread. void CallKDialogOutput(const KDialogParams& params); // Notifies the listener that a single file was chosen. void FileSelected(const FilePath& path, void* params); // Notifies the listener that multiple files were chosen. void MultiFilesSelected(const std::vector<FilePath>& files, void* params); // Notifies the listener that no file was chosen (the action was canceled). // Dialog is passed so we can find that |params| pointer that was passed to // us when we were told to show the dialog. void FileNotSelected(void *params); void CreateSelectFolderDialog(const std::string& title, const FilePath& default_path, gfx::NativeWindow parent, void* params); void CreateFileOpenDialog(const std::string& title, const FilePath& default_path, gfx::NativeWindow parent, void* params); void CreateMultiFileOpenDialog(const std::string& title, const FilePath& default_path, gfx::NativeWindow parent, void* params); void CreateSaveAsDialog(const std::string& title, const FilePath& default_path, gfx::NativeWindow parent, void* params); // Common function for OnSelectSingleFileDialogResponse and // OnSelectSingleFolderDialogResponse. void SelectSingleFileHelper(const std::string& output, int exit_code, void* params, bool allow_folder); void OnSelectSingleFileDialogResponse(const std::string& output, int exit_code, void* params); void OnSelectMultiFileDialogResponse(const std::string& output, int exit_code, void* params); void OnSelectSingleFolderDialogResponse(const std::string& output, int exit_code, void* params); // Should be either DESKTOP_ENVIRONMENT_KDE3 or DESKTOP_ENVIRONMENT_KDE4. base::nix::DesktopEnvironment desktop_; DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImplKDE); }; // static bool SelectFileDialogImpl::CheckKDEDialogWorksOnUIThread() { // No choice. UI thread can't continue without an answer here. Fortunately we // only do this once, the first time a file dialog is displayed. base::ThreadRestrictions::ScopedAllowIO allow_io; CommandLine::StringVector cmd_vector; cmd_vector.push_back(kKdialogBinary); cmd_vector.push_back("--version"); CommandLine command_line(cmd_vector); std::string dummy; return base::GetAppOutput(command_line, &dummy); } // static SelectFileDialogImpl* SelectFileDialogImpl::NewSelectFileDialogImplKDE( Listener* listener, base::nix::DesktopEnvironment desktop) { return new SelectFileDialogImplKDE(listener, desktop); } SelectFileDialogImplKDE::SelectFileDialogImplKDE( Listener* listener, base::nix::DesktopEnvironment desktop) : SelectFileDialogImpl(listener), desktop_(desktop) { DCHECK(desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 || desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE4); } SelectFileDialogImplKDE::~SelectFileDialogImplKDE() { } // We ignore |default_extension|. void SelectFileDialogImplKDE::SelectFileImpl( Type type, const string16& title, const FilePath& default_path, const FileTypeInfo* file_types, int file_type_index, const FilePath::StringType& default_extension, gfx::NativeWindow owning_window, void* params) { type_ = type; // |owning_window| can be null when user right-clicks on a downloadable item // and chooses 'Open Link in New Tab' when 'Ask where to save each file // before downloading.' preference is turned on. (http://crbug.com/29213) if (owning_window) parents_.insert(owning_window); std::string title_string = UTF16ToUTF8(title); file_type_index_ = file_type_index; if (file_types) file_types_ = *file_types; else file_types_.include_all_files = true; switch (type) { case SELECT_FOLDER: CreateSelectFolderDialog(title_string, default_path, owning_window, params); return; case SELECT_OPEN_FILE: CreateFileOpenDialog(title_string, default_path, owning_window, params); return; case SELECT_OPEN_MULTI_FILE: CreateMultiFileOpenDialog(title_string, default_path, owning_window, params); return; case SELECT_SAVEAS_FILE: CreateSaveAsDialog(title_string, default_path, owning_window, params); return; default: NOTREACHED(); return; } } bool SelectFileDialogImplKDE::HasMultipleFileTypeChoicesImpl() { return file_types_.extensions.size() > 1; } std::string SelectFileDialogImplKDE::GetMimeTypeFilterString() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); std::string filter_string; // We need a filter set because the same mime type can appear multiple times. std::set<std::string> filter_set; for (size_t i = 0; i < file_types_.extensions.size(); ++i) { for (size_t j = 0; j < file_types_.extensions[i].size(); ++j) { if (!file_types_.extensions[i][j].empty()) { std::string mime_type = base::nix::GetFileMimeType( FilePath("name").ReplaceExtension(file_types_.extensions[i][j])); filter_set.insert(mime_type); } } } // Add the *.* filter, but only if we have added other filters (otherwise it // is implied). if (file_types_.include_all_files && !file_types_.extensions.empty()) filter_set.insert("application/octet-stream"); // Create the final output string. filter_string.clear(); for (std::set<std::string>::iterator it = filter_set.begin(); it != filter_set.end(); ++it) { filter_string.append(*it + " "); } return filter_string; } void SelectFileDialogImplKDE::CallKDialogOutput(const KDialogParams& params) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); CommandLine::StringVector cmd_vector; cmd_vector.push_back(kKdialogBinary); CommandLine command_line(cmd_vector); GetKDialogCommandLine(params.type, params.title, params.default_path, params.parent, params.file_operation, params.multiple_selection, &command_line); std::string output; int exit_code; // Get output from KDialog base::GetAppOutputWithExitCode(command_line, &output, &exit_code); if (!output.empty()) output.erase(output.size() - 1); // Now the dialog is no longer showing. We can erase its parent from the // parent set. std::set<GtkWindow*>::iterator iter = parents_.find(params.parent); if (iter != parents_.end()) parents_.erase(iter); BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(params.callback, this, output, exit_code, params.kdialog_params)); } void SelectFileDialogImplKDE::GetKDialogCommandLine(const std::string& type, const std::string& title, const FilePath& path, gfx::NativeWindow parent, bool file_operation, bool multiple_selection, CommandLine* command_line) { CHECK(command_line); // Attach to the current Chrome window. GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET((parent))); int window_id = GDK_DRAWABLE_XID(gdk_window); command_line->AppendSwitchNative( desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 ? "--embed" : "--attach", base::IntToString(window_id)); // Set the correct title for the dialog. if (!title.empty()) command_line->AppendSwitchNative("--title", title); // Enable multiple file selection if we need to. if (multiple_selection) { command_line->AppendSwitch("--multiple"); command_line->AppendSwitch("--separate-output"); } command_line->AppendSwitch(type); // The path should never be empty. If it is, set it to PWD. if (path.empty()) command_line->AppendArgPath(FilePath(".")); else command_line->AppendArgPath(path); // Depending on the type of the operation we need, get the path to the // file/folder and set up mime type filters. if (file_operation) command_line->AppendArg(GetMimeTypeFilterString()); VLOG(1) << "KDialog command line: " << command_line->GetCommandLineString(); } void SelectFileDialogImplKDE::FileSelected(const FilePath& path, void* params) { if (type_ == SELECT_SAVEAS_FILE) *last_saved_path_ = path.DirName(); else if (type_ == SELECT_OPEN_FILE) *last_opened_path_ = path.DirName(); else if (type_ == SELECT_FOLDER) *last_opened_path_ = path; else NOTREACHED(); if (listener_) { // What does the filter index actually do? // TODO(dfilimon): Get a reasonable index value from somewhere. listener_->FileSelected(path, 1, params); } } void SelectFileDialogImplKDE::MultiFilesSelected( const std::vector<FilePath>& files, void* params) { *last_opened_path_ = files[0].DirName(); if (listener_) listener_->MultiFilesSelected(files, params); } void SelectFileDialogImplKDE::FileNotSelected(void* params) { if (listener_) listener_->FileSelectionCanceled(params); } void SelectFileDialogImplKDE::CreateSelectFolderDialog( const std::string& title, const FilePath& default_path, gfx::NativeWindow parent, void *params) { BrowserThread::PostTask( BrowserThread::FILE, FROM_HERE, base::Bind( &SelectFileDialogImplKDE::CallKDialogOutput, this, KDialogParams( "--getexistingdirectory", GetTitle(title, IDS_SELECT_FOLDER_DIALOG_TITLE), default_path.empty() ? *last_opened_path_ : default_path, parent, false, false, params, &SelectFileDialogImplKDE::OnSelectSingleFolderDialogResponse))); } void SelectFileDialogImplKDE::CreateFileOpenDialog( const std::string& title, const FilePath& default_path, gfx::NativeWindow parent, void* params) { BrowserThread::PostTask( BrowserThread::FILE, FROM_HERE, base::Bind( &SelectFileDialogImplKDE::CallKDialogOutput, this, KDialogParams( "--getopenfilename", GetTitle(title, IDS_OPEN_FILE_DIALOG_TITLE), default_path.empty() ? *last_opened_path_ : default_path, parent, true, false, params, &SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse))); } void SelectFileDialogImplKDE::CreateMultiFileOpenDialog( const std::string& title, const FilePath& default_path, gfx::NativeWindow parent, void* params) { BrowserThread::PostTask( BrowserThread::FILE, FROM_HERE, base::Bind( &SelectFileDialogImplKDE::CallKDialogOutput, this, KDialogParams( "--getopenfilename", GetTitle(title, IDS_OPEN_FILES_DIALOG_TITLE), default_path.empty() ? *last_opened_path_ : default_path, parent, true, true, params, &SelectFileDialogImplKDE::OnSelectMultiFileDialogResponse))); } void SelectFileDialogImplKDE::CreateSaveAsDialog( const std::string& title, const FilePath& default_path, gfx::NativeWindow parent, void* params) { BrowserThread::PostTask( BrowserThread::FILE, FROM_HERE, base::Bind( &SelectFileDialogImplKDE::CallKDialogOutput, this, KDialogParams( "--getsavefilename", GetTitle(title, IDS_SAVE_AS_DIALOG_TITLE), default_path.empty() ? *last_saved_path_ : default_path, parent, true, false, params, &SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse))); } void SelectFileDialogImplKDE::SelectSingleFileHelper(const std::string& output, int exit_code, void* params, bool allow_folder) { VLOG(1) << "[kdialog] SingleFileResponse: " << output; if (exit_code != 0 || output.empty()) { FileNotSelected(params); return; } FilePath path(output); if (allow_folder) { FileSelected(path, params); return; } if (CallDirectoryExistsOnUIThread(path)) FileNotSelected(params); else FileSelected(path, params); } void SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse( const std::string& output, int exit_code, void* params) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); SelectSingleFileHelper(output, exit_code, params, false); } void SelectFileDialogImplKDE::OnSelectSingleFolderDialogResponse( const std::string& output, int exit_code, void* params) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); SelectSingleFileHelper(output, exit_code, params, true); } void SelectFileDialogImplKDE::OnSelectMultiFileDialogResponse( const std::string& output, int exit_code, void* params) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); VLOG(1) << "[kdialog] MultiFileResponse: " << output; if (exit_code != 0 || output.empty()) { FileNotSelected(params); return; } std::vector<std::string> filenames; Tokenize(output, "\n", &filenames); std::vector<FilePath> filenames_fp; for (std::vector<std::string>::iterator iter = filenames.begin(); iter != filenames.end(); ++iter) { FilePath path(*iter); if (CallDirectoryExistsOnUIThread(path)) continue; filenames_fp.push_back(path); } if (filenames_fp.empty()) { FileNotSelected(params); return; } MultiFilesSelected(filenames_fp, params); }