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 <gdk/gdkx.h>
6 #include <gtk/gtk.h>
7 #include <stddef.h>
8
9 #include <memory>
10 #include <set>
11
12 #include "base/bind.h"
13 #include "base/bind_helpers.h"
14 #include "base/command_line.h"
15 #include "base/logging.h"
16 #include "base/macros.h"
17 #include "base/nix/mime_util_xdg.h"
18 #include "base/nix/xdg_util.h"
19 #include "base/process/launch.h"
20 #include "base/strings/string_number_conversions.h"
21 #include "base/strings/string_split.h"
22 #include "base/strings/string_util.h"
23 #include "base/strings/utf_string_conversions.h"
24 #include "base/task/post_task.h"
25 #include "base/task/task_traits.h"
26 #include "base/task/thread_pool.h"
27 #include "base/threading/thread_restrictions.h"
28 #include "ui/aura/window_tree_host.h"
29 #include "ui/base/l10n/l10n_util.h"
30 #include "ui/gfx/x/x11.h"
31 #include "ui/gtk/select_file_dialog_impl.h"
32 #include "ui/strings/grit/ui_strings.h"
33
34 namespace {
35
GetTitle(const std::string & title,int message_id)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 gtk {
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 std::unique_ptr<ui::SelectFilePolicy> policy,
52 base::nix::DesktopEnvironment desktop);
53
54 protected:
55 ~SelectFileDialogImplKDE() override;
56
57 // BaseShellDialog implementation:
58 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 void SelectFileImpl(Type type,
63 const base::string16& title,
64 const base::FilePath& default_path,
65 const FileTypeInfo* file_types,
66 int file_type_index,
67 const base::FilePath::StringType& default_extension,
68 gfx::NativeWindow owning_window,
69 void* params) override;
70
71 private:
72 bool HasMultipleFileTypeChoicesImpl() override;
73
74 struct KDialogParams {
KDialogParamsgtk::SelectFileDialogImplKDE::KDialogParams75 KDialogParams(const std::string& type,
76 const std::string& title,
77 const base::FilePath& default_path,
78 XID parent,
79 bool file_operation,
80 bool multiple_selection)
81 : type(type),
82 title(title),
83 default_path(default_path),
84 parent(parent),
85 file_operation(file_operation),
86 multiple_selection(multiple_selection) {}
87
88 std::string type;
89 std::string title;
90 base::FilePath default_path;
91 XID parent;
92 bool file_operation;
93 bool multiple_selection;
94 };
95
96 struct KDialogOutputParams {
97 std::string output;
98 int exit_code;
99 };
100
101 // Get the filters from |file_types_| and concatenate them into
102 // |filter_string|.
103 std::string GetMimeTypeFilterString();
104
105 // Get KDialog command line representing the Argv array for KDialog.
106 void GetKDialogCommandLine(const std::string& type,
107 const std::string& title,
108 const base::FilePath& default_path,
109 XID parent,
110 bool file_operation,
111 bool multiple_selection,
112 base::CommandLine* command_line);
113
114 // Call KDialog on the FILE thread and return the results.
115 std::unique_ptr<KDialogOutputParams> CallKDialogOutput(
116 const KDialogParams& params);
117
118 // Notifies the listener that a single file was chosen.
119 void FileSelected(const base::FilePath& path, void* params);
120
121 // Notifies the listener that multiple files were chosen.
122 void MultiFilesSelected(const std::vector<base::FilePath>& files,
123 void* params);
124
125 // Notifies the listener that no file was chosen (the action was canceled).
126 // Dialog is passed so we can find that |params| pointer that was passed to
127 // us when we were told to show the dialog.
128 void FileNotSelected(void* params);
129
130 void CreateSelectFolderDialog(Type type,
131 const std::string& title,
132 const base::FilePath& default_path,
133 XID parent,
134 void* params);
135
136 void CreateFileOpenDialog(const std::string& title,
137 const base::FilePath& default_path,
138 XID parent,
139 void* params);
140
141 void CreateMultiFileOpenDialog(const std::string& title,
142 const base::FilePath& default_path,
143 XID parent,
144 void* params);
145
146 void CreateSaveAsDialog(const std::string& title,
147 const base::FilePath& default_path,
148 XID parent,
149 void* params);
150
151 // Common function for OnSelectSingleFileDialogResponse and
152 // OnSelectSingleFolderDialogResponse.
153 void SelectSingleFileHelper(void* params,
154 bool allow_folder,
155 std::unique_ptr<KDialogOutputParams> results);
156
157 void OnSelectSingleFileDialogResponse(
158 XID parent,
159 void* params,
160 std::unique_ptr<KDialogOutputParams> results);
161 void OnSelectMultiFileDialogResponse(
162 XID parent,
163 void* params,
164 std::unique_ptr<KDialogOutputParams> results);
165 void OnSelectSingleFolderDialogResponse(
166 XID parent,
167 void* params,
168 std::unique_ptr<KDialogOutputParams> results);
169
170 // Should be either DESKTOP_ENVIRONMENT_KDE3, KDE4, or KDE5.
171 base::nix::DesktopEnvironment desktop_;
172
173 // The set of all parent windows for which we are currently running
174 // dialogs. This should only be accessed on the UI thread.
175 std::set<XID> parents_;
176
177 // A task runner for blocking pipe reads.
178 scoped_refptr<base::SequencedTaskRunner> pipe_task_runner_;
179
180 SEQUENCE_CHECKER(sequence_checker_);
181
182 DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImplKDE);
183 };
184
185 // static
CheckKDEDialogWorksOnUIThread()186 bool SelectFileDialogImpl::CheckKDEDialogWorksOnUIThread() {
187 // No choice. UI thread can't continue without an answer here. Fortunately we
188 // only do this once, the first time a file dialog is displayed.
189 base::ThreadRestrictions::ScopedAllowIO allow_io;
190
191 base::CommandLine::StringVector cmd_vector;
192 cmd_vector.push_back(kKdialogBinary);
193 cmd_vector.push_back("--version");
194 base::CommandLine command_line(cmd_vector);
195 std::string dummy;
196 return base::GetAppOutput(command_line, &dummy);
197 }
198
199 // static
NewSelectFileDialogImplKDE(Listener * listener,std::unique_ptr<ui::SelectFilePolicy> policy,base::nix::DesktopEnvironment desktop)200 SelectFileDialogImpl* SelectFileDialogImpl::NewSelectFileDialogImplKDE(
201 Listener* listener,
202 std::unique_ptr<ui::SelectFilePolicy> policy,
203 base::nix::DesktopEnvironment desktop) {
204 return new SelectFileDialogImplKDE(listener, std::move(policy), desktop);
205 }
206
SelectFileDialogImplKDE(Listener * listener,std::unique_ptr<ui::SelectFilePolicy> policy,base::nix::DesktopEnvironment desktop)207 SelectFileDialogImplKDE::SelectFileDialogImplKDE(
208 Listener* listener,
209 std::unique_ptr<ui::SelectFilePolicy> policy,
210 base::nix::DesktopEnvironment desktop)
211 : SelectFileDialogImpl(listener, std::move(policy)),
212 desktop_(desktop),
213 pipe_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
214 {base::MayBlock(), base::TaskPriority::USER_BLOCKING,
215 base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {
216 DCHECK(desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 ||
217 desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE4 ||
218 desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE5);
219 }
220
~SelectFileDialogImplKDE()221 SelectFileDialogImplKDE::~SelectFileDialogImplKDE() {}
222
IsRunning(gfx::NativeWindow parent_window) const223 bool SelectFileDialogImplKDE::IsRunning(gfx::NativeWindow parent_window) const {
224 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
225 if (parent_window && parent_window->GetHost()) {
226 XID xid = parent_window->GetHost()->GetAcceleratedWidget();
227 return parents_.find(xid) != parents_.end();
228 }
229
230 return false;
231 }
232
233 // We ignore |default_extension|.
SelectFileImpl(Type type,const base::string16 & title,const base::FilePath & default_path,const FileTypeInfo * file_types,int file_type_index,const base::FilePath::StringType & default_extension,gfx::NativeWindow owning_window,void * params)234 void SelectFileDialogImplKDE::SelectFileImpl(
235 Type type,
236 const base::string16& title,
237 const base::FilePath& default_path,
238 const FileTypeInfo* file_types,
239 int file_type_index,
240 const base::FilePath::StringType& default_extension,
241 gfx::NativeWindow owning_window,
242 void* params) {
243 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
244 type_ = type;
245
246 XID window_xid = x11::None;
247 if (owning_window && owning_window->GetHost()) {
248 // |owning_window| can be null when user right-clicks on a downloadable item
249 // and chooses 'Open Link in New Tab' when 'Ask where to save each file
250 // before downloading.' preference is turned on. (http://crbug.com/29213)
251 window_xid = owning_window->GetHost()->GetAcceleratedWidget();
252 parents_.insert(window_xid);
253 }
254
255 std::string title_string = base::UTF16ToUTF8(title);
256
257 file_type_index_ = file_type_index;
258 if (file_types)
259 file_types_ = *file_types;
260 else
261 file_types_.include_all_files = true;
262
263 switch (type) {
264 case SELECT_FOLDER:
265 case SELECT_UPLOAD_FOLDER:
266 case SELECT_EXISTING_FOLDER:
267 CreateSelectFolderDialog(type, title_string, default_path, window_xid,
268 params);
269 return;
270 case SELECT_OPEN_FILE:
271 CreateFileOpenDialog(title_string, default_path, window_xid, params);
272 return;
273 case SELECT_OPEN_MULTI_FILE:
274 CreateMultiFileOpenDialog(title_string, default_path, window_xid, params);
275 return;
276 case SELECT_SAVEAS_FILE:
277 CreateSaveAsDialog(title_string, default_path, window_xid, params);
278 return;
279 case SELECT_NONE:
280 NOTREACHED();
281 return;
282 }
283 }
284
HasMultipleFileTypeChoicesImpl()285 bool SelectFileDialogImplKDE::HasMultipleFileTypeChoicesImpl() {
286 return file_types_.extensions.size() > 1;
287 }
288
GetMimeTypeFilterString()289 std::string SelectFileDialogImplKDE::GetMimeTypeFilterString() {
290 DCHECK(pipe_task_runner_->RunsTasksInCurrentSequence());
291 // We need a filter set because the same mime type can appear multiple times.
292 std::set<std::string> filter_set;
293 for (size_t i = 0; i < file_types_.extensions.size(); ++i) {
294 for (size_t j = 0; j < file_types_.extensions[i].size(); ++j) {
295 if (!file_types_.extensions[i][j].empty()) {
296 std::string mime_type =
297 base::nix::GetFileMimeType(base::FilePath("name").ReplaceExtension(
298 file_types_.extensions[i][j]));
299 filter_set.insert(mime_type);
300 }
301 }
302 }
303 std::vector<std::string> filter_vector(filter_set.cbegin(),
304 filter_set.cend());
305 // Add the *.* filter, but only if we have added other filters (otherwise it
306 // is implied). It needs to be added last to avoid being picked as the default
307 // filter.
308 if (file_types_.include_all_files && !file_types_.extensions.empty()) {
309 DCHECK(filter_set.find("application/octet-stream") == filter_set.end());
310 filter_vector.push_back("application/octet-stream");
311 }
312 return base::JoinString(filter_vector, " ");
313 }
314
315 std::unique_ptr<SelectFileDialogImplKDE::KDialogOutputParams>
CallKDialogOutput(const KDialogParams & params)316 SelectFileDialogImplKDE::CallKDialogOutput(const KDialogParams& params) {
317 DCHECK(pipe_task_runner_->RunsTasksInCurrentSequence());
318 base::CommandLine::StringVector cmd_vector;
319 cmd_vector.push_back(kKdialogBinary);
320 base::CommandLine command_line(cmd_vector);
321 GetKDialogCommandLine(params.type, params.title, params.default_path,
322 params.parent, params.file_operation,
323 params.multiple_selection, &command_line);
324
325 auto results = std::make_unique<KDialogOutputParams>();
326 // Get output from KDialog
327 base::GetAppOutputWithExitCode(command_line, &results->output,
328 &results->exit_code);
329 if (!results->output.empty())
330 results->output.erase(results->output.size() - 1);
331 return results;
332 }
333
GetKDialogCommandLine(const std::string & type,const std::string & title,const base::FilePath & path,XID parent,bool file_operation,bool multiple_selection,base::CommandLine * command_line)334 void SelectFileDialogImplKDE::GetKDialogCommandLine(
335 const std::string& type,
336 const std::string& title,
337 const base::FilePath& path,
338 XID parent,
339 bool file_operation,
340 bool multiple_selection,
341 base::CommandLine* command_line) {
342 CHECK(command_line);
343
344 // Attach to the current Chrome window.
345 if (parent != x11::None) {
346 command_line->AppendSwitchNative(
347 desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 ? "--embed"
348 : "--attach",
349 base::NumberToString(parent));
350 }
351
352 // Set the correct title for the dialog.
353 if (!title.empty())
354 command_line->AppendSwitchNative("--title", title);
355 // Enable multiple file selection if we need to.
356 if (multiple_selection) {
357 command_line->AppendSwitch("--multiple");
358 command_line->AppendSwitch("--separate-output");
359 }
360 command_line->AppendSwitch(type);
361 // The path should never be empty. If it is, set it to PWD.
362 if (path.empty())
363 command_line->AppendArgPath(base::FilePath("."));
364 else
365 command_line->AppendArgPath(path);
366 // Depending on the type of the operation we need, get the path to the
367 // file/folder and set up mime type filters.
368 if (file_operation)
369 command_line->AppendArg(GetMimeTypeFilterString());
370 VLOG(1) << "KDialog command line: " << command_line->GetCommandLineString();
371 }
372
FileSelected(const base::FilePath & path,void * params)373 void SelectFileDialogImplKDE::FileSelected(const base::FilePath& path,
374 void* params) {
375 if (type_ == SELECT_SAVEAS_FILE)
376 *last_saved_path_ = path.DirName();
377 else if (type_ == SELECT_OPEN_FILE)
378 *last_opened_path_ = path.DirName();
379 else if (type_ == SELECT_FOLDER || type_ == SELECT_UPLOAD_FOLDER ||
380 type_ == SELECT_EXISTING_FOLDER)
381 *last_opened_path_ = path;
382 else
383 NOTREACHED();
384 if (listener_) { // What does the filter index actually do?
385 // TODO(dfilimon): Get a reasonable index value from somewhere.
386 listener_->FileSelected(path, 1, params);
387 }
388 }
389
MultiFilesSelected(const std::vector<base::FilePath> & files,void * params)390 void SelectFileDialogImplKDE::MultiFilesSelected(
391 const std::vector<base::FilePath>& files,
392 void* params) {
393 *last_opened_path_ = files[0].DirName();
394 if (listener_)
395 listener_->MultiFilesSelected(files, params);
396 }
397
FileNotSelected(void * params)398 void SelectFileDialogImplKDE::FileNotSelected(void* params) {
399 if (listener_)
400 listener_->FileSelectionCanceled(params);
401 }
402
CreateSelectFolderDialog(Type type,const std::string & title,const base::FilePath & default_path,XID parent,void * params)403 void SelectFileDialogImplKDE::CreateSelectFolderDialog(
404 Type type,
405 const std::string& title,
406 const base::FilePath& default_path,
407 XID parent,
408 void* params) {
409 int title_message_id = (type == SELECT_UPLOAD_FOLDER)
410 ? IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE
411 : IDS_SELECT_FOLDER_DIALOG_TITLE;
412 pipe_task_runner_->PostTaskAndReplyWithResult(
413 FROM_HERE,
414 base::BindOnce(
415 &SelectFileDialogImplKDE::CallKDialogOutput, this,
416 KDialogParams(
417 "--getexistingdirectory", GetTitle(title, title_message_id),
418 default_path.empty() ? *last_opened_path_ : default_path, parent,
419 false, false)),
420 base::BindOnce(
421 &SelectFileDialogImplKDE::OnSelectSingleFolderDialogResponse, this,
422 parent, params));
423 }
424
CreateFileOpenDialog(const std::string & title,const base::FilePath & default_path,XID parent,void * params)425 void SelectFileDialogImplKDE::CreateFileOpenDialog(
426 const std::string& title,
427 const base::FilePath& default_path,
428 XID parent,
429 void* params) {
430 pipe_task_runner_->PostTaskAndReplyWithResult(
431 FROM_HERE,
432 base::BindOnce(
433 &SelectFileDialogImplKDE::CallKDialogOutput, this,
434 KDialogParams(
435 "--getopenfilename", GetTitle(title, IDS_OPEN_FILE_DIALOG_TITLE),
436 default_path.empty() ? *last_opened_path_ : default_path, parent,
437 true, false)),
438 base::BindOnce(&SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse,
439 this, parent, params));
440 }
441
CreateMultiFileOpenDialog(const std::string & title,const base::FilePath & default_path,XID parent,void * params)442 void SelectFileDialogImplKDE::CreateMultiFileOpenDialog(
443 const std::string& title,
444 const base::FilePath& default_path,
445 XID parent,
446 void* params) {
447 pipe_task_runner_->PostTaskAndReplyWithResult(
448 FROM_HERE,
449 base::BindOnce(
450 &SelectFileDialogImplKDE::CallKDialogOutput, this,
451 KDialogParams(
452 "--getopenfilename", GetTitle(title, IDS_OPEN_FILES_DIALOG_TITLE),
453 default_path.empty() ? *last_opened_path_ : default_path, parent,
454 true, true)),
455 base::BindOnce(&SelectFileDialogImplKDE::OnSelectMultiFileDialogResponse,
456 this, parent, params));
457 }
458
CreateSaveAsDialog(const std::string & title,const base::FilePath & default_path,XID parent,void * params)459 void SelectFileDialogImplKDE::CreateSaveAsDialog(
460 const std::string& title,
461 const base::FilePath& default_path,
462 XID parent,
463 void* params) {
464 pipe_task_runner_->PostTaskAndReplyWithResult(
465 FROM_HERE,
466 base::BindOnce(
467 &SelectFileDialogImplKDE::CallKDialogOutput, this,
468 KDialogParams("--getsavefilename",
469 GetTitle(title, IDS_SAVE_AS_DIALOG_TITLE),
470 default_path.empty() ? *last_saved_path_ : default_path,
471 parent, true, false)),
472 base::BindOnce(&SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse,
473 this, parent, params));
474 }
475
SelectSingleFileHelper(void * params,bool allow_folder,std::unique_ptr<KDialogOutputParams> results)476 void SelectFileDialogImplKDE::SelectSingleFileHelper(
477 void* params,
478 bool allow_folder,
479 std::unique_ptr<KDialogOutputParams> results) {
480 VLOG(1) << "[kdialog] SingleFileResponse: " << results->output;
481 if (results->exit_code || results->output.empty()) {
482 FileNotSelected(params);
483 return;
484 }
485
486 base::FilePath path(results->output);
487 if (allow_folder) {
488 FileSelected(path, params);
489 return;
490 }
491
492 if (CallDirectoryExistsOnUIThread(path))
493 FileNotSelected(params);
494 else
495 FileSelected(path, params);
496 }
497
OnSelectSingleFileDialogResponse(XID parent,void * params,std::unique_ptr<KDialogOutputParams> results)498 void SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse(
499 XID parent,
500 void* params,
501 std::unique_ptr<KDialogOutputParams> results) {
502 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
503 parents_.erase(parent);
504 SelectSingleFileHelper(params, false, std::move(results));
505 }
506
OnSelectSingleFolderDialogResponse(XID parent,void * params,std::unique_ptr<KDialogOutputParams> results)507 void SelectFileDialogImplKDE::OnSelectSingleFolderDialogResponse(
508 XID parent,
509 void* params,
510 std::unique_ptr<KDialogOutputParams> results) {
511 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
512 parents_.erase(parent);
513 SelectSingleFileHelper(params, true, std::move(results));
514 }
515
OnSelectMultiFileDialogResponse(XID parent,void * params,std::unique_ptr<KDialogOutputParams> results)516 void SelectFileDialogImplKDE::OnSelectMultiFileDialogResponse(
517 XID parent,
518 void* params,
519 std::unique_ptr<KDialogOutputParams> results) {
520 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
521 VLOG(1) << "[kdialog] MultiFileResponse: " << results->output;
522
523 parents_.erase(parent);
524
525 if (results->exit_code || results->output.empty()) {
526 FileNotSelected(params);
527 return;
528 }
529
530 std::vector<base::FilePath> filenames_fp;
531 for (const base::StringPiece& line :
532 base::SplitStringPiece(results->output, "\n", base::KEEP_WHITESPACE,
533 base::SPLIT_WANT_NONEMPTY)) {
534 base::FilePath path(line);
535 if (CallDirectoryExistsOnUIThread(path))
536 continue;
537 filenames_fp.push_back(path);
538 }
539
540 if (filenames_fp.empty()) {
541 FileNotSelected(params);
542 return;
543 }
544 MultiFilesSelected(filenames_fp, params);
545 }
546
547 } // namespace gtk
548