select_file_dialog_impl_kde.cc revision f2477e01787aa58f445919b809d89e252beef54f
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/aura/root_window.h"
24#include "ui/base/l10n/l10n_util.h"
25
26// These conflict with base/tracked_objects.h, so need to come last.
27#include <gdk/gdkx.h>
28#include <gtk/gtk.h>
29
30using content::BrowserThread;
31
32namespace {
33
34std::string GetTitle(const std::string& title, int message_id) {
35  return title.empty() ? l10n_util::GetStringUTF8(message_id) : title;
36}
37
38const char kKdialogBinary[] = "kdialog";
39
40}  // namespace
41
42namespace libgtk2ui {
43
44// Implementation of SelectFileDialog that shows a KDE common dialog for
45// choosing a file or folder. This acts as a modal dialog.
46class SelectFileDialogImplKDE : public SelectFileDialogImpl {
47 public:
48  SelectFileDialogImplKDE(Listener* listener,
49                          ui::SelectFilePolicy* policy,
50                          base::nix::DesktopEnvironment desktop);
51
52 protected:
53  virtual ~SelectFileDialogImplKDE();
54
55  // SelectFileDialog implementation.
56  // |params| is user data we pass back via the Listener interface.
57  virtual void SelectFileImpl(
58      Type type,
59      const string16& title,
60      const base::FilePath& default_path,
61      const FileTypeInfo* file_types,
62      int file_type_index,
63      const base::FilePath::StringType& default_extension,
64      gfx::NativeWindow owning_window,
65      void* params) OVERRIDE;
66
67 private:
68  virtual bool HasMultipleFileTypeChoicesImpl() OVERRIDE;
69
70  struct KDialogParams {
71    KDialogParams(const std::string& type, const std::string& title,
72                  const base::FilePath& default_path, gfx::NativeWindow parent,
73                  bool file_operation, bool multiple_selection,
74                  void* kdialog_params,
75                  void (SelectFileDialogImplKDE::*callback)(const std::string&,
76                                                            int, void*))
77        : type(type), title(title), default_path(default_path), parent(parent),
78          file_operation(file_operation),
79          multiple_selection(multiple_selection),
80          kdialog_params(kdialog_params), callback(callback) {
81    }
82
83    std::string type;
84    std::string title;
85    base::FilePath default_path;
86    gfx::NativeWindow parent;
87    bool file_operation;
88    bool multiple_selection;
89    void* kdialog_params;
90    void (SelectFileDialogImplKDE::*callback)(const std::string&, int, void*);
91  };
92
93  // Get the filters from |file_types_| and concatenate them into
94  // |filter_string|.
95  std::string GetMimeTypeFilterString();
96
97  // Get KDialog command line representing the Argv array for KDialog.
98  void GetKDialogCommandLine(const std::string& type, const std::string& title,
99      const base::FilePath& default_path, gfx::NativeWindow parent,
100      bool file_operation, bool multiple_selection, CommandLine* command_line);
101
102  // Call KDialog on the FILE thread and post results back to the UI thread.
103  void CallKDialogOutput(const KDialogParams& params);
104
105  // Notifies the listener that a single file was chosen.
106  void FileSelected(const base::FilePath& path, void* params);
107
108  // Notifies the listener that multiple files were chosen.
109  void MultiFilesSelected(const std::vector<base::FilePath>& files,
110                          void* params);
111
112  // Notifies the listener that no file was chosen (the action was canceled).
113  // Dialog is passed so we can find that |params| pointer that was passed to
114  // us when we were told to show the dialog.
115  void FileNotSelected(void *params);
116
117  void CreateSelectFolderDialog(Type type,
118                                const std::string& title,
119                                const base::FilePath& default_path,
120                                gfx::NativeWindow parent, void* params);
121
122  void CreateFileOpenDialog(const std::string& title,
123                                  const base::FilePath& default_path,
124                                  gfx::NativeWindow parent, void* params);
125
126  void CreateMultiFileOpenDialog(const std::string& title,
127                                 const base::FilePath& default_path,
128                                 gfx::NativeWindow parent, void* params);
129
130  void CreateSaveAsDialog(const std::string& title,
131                          const base::FilePath& default_path,
132                          gfx::NativeWindow parent, void* params);
133
134  // Common function for OnSelectSingleFileDialogResponse and
135  // OnSelectSingleFolderDialogResponse.
136  void SelectSingleFileHelper(const std::string& output, int exit_code,
137                              void* params, bool allow_folder);
138
139  void OnSelectSingleFileDialogResponse(const std::string& output,
140                                        int exit_code, void* params);
141  void OnSelectMultiFileDialogResponse(const std::string& output,
142                                       int exit_code, void* params);
143  void OnSelectSingleFolderDialogResponse(const std::string& output,
144                                          int exit_code, void* params);
145
146  // Should be either DESKTOP_ENVIRONMENT_KDE3 or DESKTOP_ENVIRONMENT_KDE4.
147  base::nix::DesktopEnvironment desktop_;
148
149  DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImplKDE);
150};
151
152// static
153bool SelectFileDialogImpl::CheckKDEDialogWorksOnUIThread() {
154  // No choice. UI thread can't continue without an answer here. Fortunately we
155  // only do this once, the first time a file dialog is displayed.
156  base::ThreadRestrictions::ScopedAllowIO allow_io;
157
158  CommandLine::StringVector cmd_vector;
159  cmd_vector.push_back(kKdialogBinary);
160  cmd_vector.push_back("--version");
161  CommandLine command_line(cmd_vector);
162  std::string dummy;
163  return base::GetAppOutput(command_line, &dummy);
164}
165
166// static
167SelectFileDialogImpl* SelectFileDialogImpl::NewSelectFileDialogImplKDE(
168    Listener* listener,
169    ui::SelectFilePolicy* policy,
170    base::nix::DesktopEnvironment desktop) {
171  return new SelectFileDialogImplKDE(listener, policy, desktop);
172}
173
174SelectFileDialogImplKDE::SelectFileDialogImplKDE(
175    Listener* listener,
176    ui::SelectFilePolicy* policy,
177    base::nix::DesktopEnvironment desktop)
178    : SelectFileDialogImpl(listener, policy),
179      desktop_(desktop) {
180  DCHECK(desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 ||
181         desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE4);
182}
183
184SelectFileDialogImplKDE::~SelectFileDialogImplKDE() {
185}
186
187// We ignore |default_extension|.
188void SelectFileDialogImplKDE::SelectFileImpl(
189    Type type,
190    const string16& title,
191    const base::FilePath& default_path,
192    const FileTypeInfo* file_types,
193    int file_type_index,
194    const base::FilePath::StringType& default_extension,
195    gfx::NativeWindow owning_window,
196    void* params) {
197  type_ = type;
198  // |owning_window| can be null when user right-clicks on a downloadable item
199  // and chooses 'Open Link in New Tab' when 'Ask where to save each file
200  // before downloading.' preference is turned on. (http://crbug.com/29213)
201  if (owning_window)
202    parents_.insert(owning_window);
203
204  std::string title_string = UTF16ToUTF8(title);
205
206  file_type_index_ = file_type_index;
207  if (file_types)
208    file_types_ = *file_types;
209  else
210    file_types_.include_all_files = true;
211
212  switch (type) {
213    case SELECT_FOLDER:
214    case SELECT_UPLOAD_FOLDER:
215      CreateSelectFolderDialog(type, title_string, default_path,
216                               owning_window, params);
217      return;
218    case SELECT_OPEN_FILE:
219      CreateFileOpenDialog(title_string, default_path, owning_window,
220                           params);
221      return;
222    case SELECT_OPEN_MULTI_FILE:
223      CreateMultiFileOpenDialog(title_string, default_path,
224                                owning_window, params);
225      return;
226    case SELECT_SAVEAS_FILE:
227      CreateSaveAsDialog(title_string, default_path, owning_window,
228                         params);
229      return;
230    default:
231      NOTREACHED();
232      return;
233  }
234}
235
236bool SelectFileDialogImplKDE::HasMultipleFileTypeChoicesImpl() {
237  return file_types_.extensions.size() > 1;
238}
239
240std::string SelectFileDialogImplKDE::GetMimeTypeFilterString() {
241  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
242  std::string filter_string;
243  // We need a filter set because the same mime type can appear multiple times.
244  std::set<std::string> filter_set;
245  for (size_t i = 0; i < file_types_.extensions.size(); ++i) {
246    for (size_t j = 0; j < file_types_.extensions[i].size(); ++j) {
247      if (!file_types_.extensions[i][j].empty()) {
248        std::string mime_type = base::nix::GetFileMimeType(base::FilePath(
249            "name").ReplaceExtension(file_types_.extensions[i][j]));
250        filter_set.insert(mime_type);
251      }
252    }
253  }
254  // Add the *.* filter, but only if we have added other filters (otherwise it
255  // is implied).
256  if (file_types_.include_all_files && !file_types_.extensions.empty())
257    filter_set.insert("application/octet-stream");
258  // Create the final output string.
259  filter_string.clear();
260  for (std::set<std::string>::iterator it = filter_set.begin();
261       it != filter_set.end(); ++it) {
262    filter_string.append(*it + " ");
263  }
264  return filter_string;
265}
266
267void SelectFileDialogImplKDE::CallKDialogOutput(const KDialogParams& params) {
268  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
269  CommandLine::StringVector cmd_vector;
270  cmd_vector.push_back(kKdialogBinary);
271  CommandLine command_line(cmd_vector);
272  GetKDialogCommandLine(params.type, params.title, params.default_path,
273                        params.parent, params.file_operation,
274                        params.multiple_selection, &command_line);
275  std::string output;
276  int exit_code;
277  // Get output from KDialog
278  base::GetAppOutputWithExitCode(command_line, &output, &exit_code);
279  if (!output.empty())
280    output.erase(output.size() - 1);
281
282  // Now the dialog is no longer showing. We can erase its parent from the
283  // parent set.
284  // TODO(erg): FIX THIS.
285  // std::set<GtkWindow*>::iterator iter = parents_.find(params.parent);
286  // if (iter != parents_.end())
287  //   parents_.erase(iter);
288
289  BrowserThread::PostTask(
290      BrowserThread::UI, FROM_HERE,
291      base::Bind(params.callback, this, output, exit_code,
292                 params.kdialog_params));
293}
294
295void SelectFileDialogImplKDE::GetKDialogCommandLine(const std::string& type,
296    const std::string& title, const base::FilePath& path,
297    gfx::NativeWindow parent, bool file_operation, bool multiple_selection,
298    CommandLine* command_line) {
299  CHECK(command_line);
300
301  // Attach to the current Chrome window.
302  int window_id = parent->GetDispatcher()->host()->GetAcceleratedWidget();
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