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
29namespace {
30
31std::string GetTitle(const std::string& title, int message_id) {
32  return title.empty() ? l10n_util::GetStringUTF8(message_id) : title;
33}
34
35const 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.
39class 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
158SelectFileDialogImplKDE::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
168SelectFileDialogImplKDE::~SelectFileDialogImplKDE() {
169}
170
171// We ignore |default_extension|.
172void 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
220bool SelectFileDialogImplKDE::HasMultipleFileTypeChoicesImpl() {
221  return file_types_.extensions.size() > 1;
222}
223
224std::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
251void 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
274void 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
307void 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
323void 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
330void SelectFileDialogImplKDE::FileNotSelected(void* params) {
331  if (listener_)
332    listener_->FileSelectionCanceled(params);
333}
334
335void 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
354void 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
370void 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
386void 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
402void 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
422void SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse(
423    const std::string& output, int exit_code, void* params) {
424  SelectSingleFileHelper(output, exit_code, params, false);
425}
426
427void SelectFileDialogImplKDE::OnSelectSingleFolderDialogResponse(
428    const std::string& output, int exit_code, void* params) {
429  SelectSingleFileHelper(output, exit_code, params, true);
430}
431
432void 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
461namespace ui {
462
463// static
464bool 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
478SelectFileDialogImpl* 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