| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/shell_dialogs/select_file_dialog_win.h" |
| |
| #include <algorithm> |
| #include <memory> |
| |
| #include "base/check_op.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/i18n/case_conversion.h" |
| #include "base/notreached.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/win/registry.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_event_dispatcher.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/native_widget_types.h" |
| #include "ui/shell_dialogs/base_shell_dialog_win.h" |
| #include "ui/shell_dialogs/execute_select_file_win.h" |
| #include "ui/shell_dialogs/select_file_policy.h" |
| #include "ui/shell_dialogs/select_file_utils_win.h" |
| #include "ui/shell_dialogs/selected_file_info.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "url/gurl.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| // Get the file type description from the registry. This will be "Text Document" |
| // for .txt files, "JPEG Image" for .jpg files, etc. If the registry doesn't |
| // have an entry for the file type, we return false, true if the description was |
| // found. 'file_ext' must be in form ".txt". |
| bool GetRegistryDescriptionFromExtension(const std::u16string& file_ext, |
| std::u16string* reg_description) { |
| DCHECK(reg_description); |
| base::win::RegKey reg_ext(HKEY_CLASSES_ROOT, base::as_wcstr(file_ext), |
| KEY_READ); |
| std::wstring reg_app; |
| if (reg_ext.ReadValue(nullptr, ®_app) == ERROR_SUCCESS && |
| !reg_app.empty()) { |
| base::win::RegKey reg_link(HKEY_CLASSES_ROOT, reg_app.c_str(), KEY_READ); |
| std::wstring description; |
| if (reg_link.ReadValue(nullptr, &description) == ERROR_SUCCESS) { |
| *reg_description = base::WideToUTF16(description); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Set up a filter for a Save/Open dialog, |ext_desc| as the text descriptions |
| // of the |file_ext| types (optional), and (optionally) the default 'All Files' |
| // view. The purpose of the filter is to show only files of a particular type in |
| // a Windows Save/Open dialog box. The resulting filter is returned. The filter |
| // created here are: |
| // 1. only files that have 'file_ext' as their extension |
| // 2. all files (only added if 'include_all_files' is true) |
| // If a description is not provided for a file extension, it will be retrieved |
| // from the registry. If the file extension does not exist in the registry, a |
| // default description will be created (e.g. "qqq" yields "QQQ File"). |
| std::vector<FileFilterSpec> FormatFilterForExtensions( |
| const std::vector<std::u16string>& file_ext, |
| const std::vector<std::u16string>& ext_desc, |
| bool include_all_files, |
| bool keep_extension_visible) { |
| const std::u16string all_ext = u"*.*"; |
| const std::u16string all_desc = |
| l10n_util::GetStringUTF16(IDS_APP_SAVEAS_ALL_FILES); |
| |
| DCHECK(file_ext.size() >= ext_desc.size()); |
| |
| if (file_ext.empty()) |
| include_all_files = true; |
| |
| std::vector<FileFilterSpec> result; |
| result.reserve(file_ext.size() + 1); |
| |
| for (size_t i = 0; i < file_ext.size(); ++i) { |
| std::u16string ext = |
| RemoveEnvVarFromFileName<char16_t>(file_ext[i], std::u16string(u"%")); |
| std::u16string desc; |
| if (i < ext_desc.size()) |
| desc = ext_desc[i]; |
| |
| if (ext.empty()) { |
| // Force something reasonable to appear in the dialog box if there is no |
| // extension provided. |
| include_all_files = true; |
| continue; |
| } |
| |
| if (desc.empty()) { |
| DCHECK(ext.find(u'.') != std::u16string::npos); |
| std::u16string first_extension = ext.substr(ext.find(u'.')); |
| size_t first_separator_index = first_extension.find(u';'); |
| if (first_separator_index != std::u16string::npos) |
| first_extension = first_extension.substr(0, first_separator_index); |
| |
| // Find the extension name without the preceeding '.' character. |
| std::u16string ext_name = first_extension; |
| size_t ext_index = ext_name.find_first_not_of(u'.'); |
| if (ext_index != std::u16string::npos) |
| ext_name = ext_name.substr(ext_index); |
| |
| if (!GetRegistryDescriptionFromExtension(first_extension, &desc)) { |
| // The extension doesn't exist in the registry. Create a description |
| // based on the unknown extension type (i.e. if the extension is .qqq, |
| // then we create a description "QQQ File"). |
| desc = l10n_util::GetStringFUTF16(IDS_APP_SAVEAS_EXTENSION_FORMAT, |
| base::i18n::ToUpper(ext_name)); |
| include_all_files = true; |
| } |
| if (desc.empty()) |
| desc = u"*." + ext_name; |
| } else if (keep_extension_visible) { |
| // Having '*' in the description could cause the windows file dialog to |
| // not include the file extension in the file dialog. So strip out any '*' |
| // characters if `keep_extension_visible` is set. |
| base::ReplaceChars(desc, u"*", base::StringPiece16(), &desc); |
| } |
| |
| result.push_back({desc, ext}); |
| } |
| |
| if (include_all_files) |
| result.push_back({all_desc, all_ext}); |
| |
| return result; |
| } |
| |
| // Forwards the result from a select file operation to the SelectFileDialog |
| // object on the UI thread. |
| void OnSelectFileExecutedOnDialogTaskRunner( |
| scoped_refptr<base::SequencedTaskRunner> ui_task_runner, |
| OnSelectFileExecutedCallback on_select_file_executed_callback, |
| const std::vector<base::FilePath>& paths, |
| int index) { |
| ui_task_runner->PostTask( |
| FROM_HERE, base::BindOnce(std::move(on_select_file_executed_callback), |
| paths, index)); |
| } |
| |
| // Implementation of SelectFileDialog that shows a Windows common dialog for |
| // choosing a file or folder. |
| class SelectFileDialogImpl : public ui::SelectFileDialog, |
| public ui::BaseShellDialogImpl { |
| public: |
| SelectFileDialogImpl( |
| Listener* listener, |
| std::unique_ptr<ui::SelectFilePolicy> policy, |
| const ExecuteSelectFileCallback& execute_select_file_callback); |
| |
| SelectFileDialogImpl(const SelectFileDialogImpl&) = delete; |
| SelectFileDialogImpl& operator=(const SelectFileDialogImpl&) = delete; |
| |
| // BaseShellDialog implementation: |
| bool IsRunning(gfx::NativeWindow owning_window) const override; |
| void ListenerDestroyed() override; |
| |
| protected: |
| // SelectFileDialog implementation: |
| void SelectFileImpl(Type type, |
| const std::u16string& 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, |
| const GURL* caller) override; |
| |
| private: |
| ~SelectFileDialogImpl() override; |
| |
| struct SelectFolderDialogOptions { |
| const wchar_t* default_path; |
| bool is_upload; |
| }; |
| |
| // Returns the result of the select file operation to the listener. |
| void OnSelectFileExecuted(Type type, |
| std::unique_ptr<RunState> run_state, |
| void* params, |
| const std::vector<base::FilePath>& paths, |
| int index); |
| |
| bool HasMultipleFileTypeChoicesImpl() override; |
| |
| // Returns the filter to be used while displaying the open/save file dialog. |
| // This is computed from the extensions for the file types being opened. |
| // |file_types| can be NULL in which case the returned filter will be empty. |
| static std::vector<FileFilterSpec> GetFilterForFileTypes( |
| const FileTypeInfo* file_types); |
| |
| bool has_multiple_file_type_choices_; |
| ExecuteSelectFileCallback execute_select_file_callback_; |
| }; |
| |
| SelectFileDialogImpl::SelectFileDialogImpl( |
| Listener* listener, |
| std::unique_ptr<ui::SelectFilePolicy> policy, |
| const ExecuteSelectFileCallback& execute_select_file_callback) |
| : SelectFileDialog(listener, std::move(policy)), |
| BaseShellDialogImpl(), |
| has_multiple_file_type_choices_(false), |
| execute_select_file_callback_(execute_select_file_callback) {} |
| |
| SelectFileDialogImpl::~SelectFileDialogImpl() = default; |
| |
| // Invokes the |execute_select_file_callback| and returns the result to |
| void DoSelectFileOnDialogTaskRunner( |
| const ExecuteSelectFileCallback& execute_select_file_callback, |
| SelectFileDialog::Type type, |
| const std::u16string& title, |
| const base::FilePath& default_path, |
| const std::vector<ui::FileFilterSpec>& filter, |
| int file_type_index, |
| const std::wstring& default_extension, |
| HWND owner, |
| scoped_refptr<base::SequencedTaskRunner> ui_task_runner, |
| OnSelectFileExecutedCallback on_select_file_executed_callback) { |
| execute_select_file_callback.Run( |
| type, title, default_path, filter, file_type_index, default_extension, |
| owner, |
| base::BindOnce(&OnSelectFileExecutedOnDialogTaskRunner, |
| std::move(ui_task_runner), |
| std::move(on_select_file_executed_callback))); |
| } |
| |
| void SelectFileDialogImpl::SelectFileImpl( |
| Type type, |
| const std::u16string& 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, |
| const GURL* caller) { |
| has_multiple_file_type_choices_ = |
| file_types ? file_types->extensions.size() > 1 : true; |
| |
| std::vector<FileFilterSpec> filter = GetFilterForFileTypes(file_types); |
| HWND owner = owning_window && owning_window->GetRootWindow() |
| ? owning_window->GetHost()->GetAcceleratedWidget() |
| : nullptr; |
| |
| std::unique_ptr<RunState> run_state = BeginRun(owner); |
| |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner = |
| run_state->dialog_task_runner; |
| task_runner->PostTask( |
| FROM_HERE, |
| base::BindOnce(&DoSelectFileOnDialogTaskRunner, |
| execute_select_file_callback_, type, title, default_path, |
| filter, file_type_index, default_extension, owner, |
| base::SingleThreadTaskRunner::GetCurrentDefault(), |
| base::BindOnce(&SelectFileDialogImpl::OnSelectFileExecuted, |
| this, type, std::move(run_state), params))); |
| } |
| |
| bool SelectFileDialogImpl::HasMultipleFileTypeChoicesImpl() { |
| return has_multiple_file_type_choices_; |
| } |
| |
| bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow owning_window) const { |
| if (!owning_window->GetRootWindow()) |
| return false; |
| HWND owner = owning_window->GetHost()->GetAcceleratedWidget(); |
| return listener_ && IsRunningDialogForOwner(owner); |
| } |
| |
| void SelectFileDialogImpl::ListenerDestroyed() { |
| // Our associated listener has gone away, so we shouldn't call back to it if |
| // our worker thread returns after the listener is dead. |
| listener_ = nullptr; |
| } |
| |
| void SelectFileDialogImpl::OnSelectFileExecuted( |
| Type type, |
| std::unique_ptr<RunState> run_state, |
| void* params, |
| const std::vector<base::FilePath>& paths, |
| int index) { |
| if (listener_) { |
| // The paths vector is empty when the user cancels the dialog. |
| if (paths.empty()) { |
| listener_->FileSelectionCanceled(params); |
| } else { |
| switch (type) { |
| case SELECT_FOLDER: |
| case SELECT_UPLOAD_FOLDER: |
| case SELECT_EXISTING_FOLDER: |
| case SELECT_SAVEAS_FILE: |
| case SELECT_OPEN_FILE: |
| DCHECK_EQ(paths.size(), 1u); |
| listener_->FileSelected(SelectedFileInfo(paths[0]), index, params); |
| break; |
| case SELECT_OPEN_MULTI_FILE: |
| listener_->MultiFilesSelected( |
| FilePathListToSelectedFileInfoList(paths), params); |
| break; |
| case SELECT_NONE: |
| NOTREACHED(); |
| } |
| } |
| } |
| |
| EndRun(std::move(run_state)); |
| } |
| |
| // static |
| std::vector<FileFilterSpec> SelectFileDialogImpl::GetFilterForFileTypes( |
| const FileTypeInfo* file_types) { |
| if (!file_types) |
| return std::vector<FileFilterSpec>(); |
| |
| std::vector<std::u16string> exts; |
| for (size_t i = 0; i < file_types->extensions.size(); ++i) { |
| const std::vector<std::wstring>& inner_exts = file_types->extensions[i]; |
| std::u16string ext_string; |
| for (size_t j = 0; j < inner_exts.size(); ++j) { |
| if (!ext_string.empty()) |
| ext_string.push_back(u';'); |
| ext_string.append(u"*."); |
| ext_string.append(base::WideToUTF16(inner_exts[j])); |
| } |
| exts.push_back(ext_string); |
| } |
| return FormatFilterForExtensions( |
| exts, file_types->extension_description_overrides, |
| file_types->include_all_files, file_types->keep_extension_visible); |
| } |
| |
| } // namespace |
| |
| SelectFileDialog* CreateWinSelectFileDialog( |
| SelectFileDialog::Listener* listener, |
| std::unique_ptr<SelectFilePolicy> policy, |
| const ExecuteSelectFileCallback& execute_select_file_callback) { |
| return new SelectFileDialogImpl(listener, std::move(policy), |
| execute_select_file_callback); |
| } |
| |
| SelectFileDialog* CreateSelectFileDialog( |
| SelectFileDialog::Listener* listener, |
| std::unique_ptr<SelectFilePolicy> policy) { |
| return CreateWinSelectFileDialog(listener, std::move(policy), |
| base::BindRepeating(&ui::ExecuteSelectFile)); |
| } |
| |
| } // namespace ui |