Home | History | Annotate | Download | only in libgtk2ui
      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 
     29 using content::BrowserThread;
     30 
     31 namespace {
     32 
     33 std::string GetTitle(const std::string& title, int message_id) {
     34   return title.empty() ? l10n_util::GetStringUTF8(message_id) : title;
     35 }
     36 
     37 const char kKdialogBinary[] = "kdialog";
     38 
     39 }  // namespace
     40 
     41 namespace 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.
     45 class 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
    152 bool 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
    166 SelectFileDialogImpl* SelectFileDialogImpl::NewSelectFileDialogImplKDE(
    167     Listener* listener,
    168     ui::SelectFilePolicy* policy,
    169     base::nix::DesktopEnvironment desktop) {
    170   return new SelectFileDialogImplKDE(listener, policy, desktop);
    171 }
    172 
    173 SelectFileDialogImplKDE::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 
    183 SelectFileDialogImplKDE::~SelectFileDialogImplKDE() {
    184 }
    185 
    186 // We ignore |default_extension|.
    187 void 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 
    235 bool SelectFileDialogImplKDE::HasMultipleFileTypeChoicesImpl() {
    236   return file_types_.extensions.size() > 1;
    237 }
    238 
    239 std::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 
    266 void 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 
    294 void 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 
    327 void 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 
    343 void 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 
    350 void SelectFileDialogImplKDE::FileNotSelected(void* params) {
    351   if (listener_)
    352     listener_->FileSelectionCanceled(params);
    353 }
    354 
    355 void 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 
    374 void 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 
    390 void 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 
    406 void 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 
    422 void 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 
    442 void 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 
    448 void 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 
    454 void 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