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 <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