Home | History | Annotate | Download | only in gtk
      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