| // Copyright 2018 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/execute_select_file_win.h" |
| |
| #include <shlobj.h> |
| #include <wrl/client.h> |
| |
| #include <memory> |
| |
| #include "base/check.h" |
| #include "base/feature_list.h" |
| #include "base/files/file.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/callback.h" |
| #include "base/strings/string_util.h" |
| #include "base/threading/hang_watcher.h" |
| #include "base/win/com_init_util.h" |
| #include "base/win/registry.h" |
| #include "base/win/scoped_co_mem.h" |
| #include "base/win/shortcut.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/shell_dialogs/auto_close_dialog_event_handler_win.h" |
| #include "ui/shell_dialogs/base_shell_dialog_win.h" |
| #include "ui/shell_dialogs/select_file_utils_win.h" |
| #include "ui/strings/grit/ui_strings.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| // Stop switch for the AutoCloseDialogEventHandler. |
| BASE_FEATURE(kAutoCloseFileDialogs, |
| "AutoCloseFileDialogs", |
| base::FEATURE_ENABLED_BY_DEFAULT); |
| |
| // RAII wrapper around AutoCloseDialogEventHandler. |
| class ScopedAutoCloseDialogEventHandler { |
| public: |
| ScopedAutoCloseDialogEventHandler(HWND owner_window, IFileDialog* file_dialog) |
| : file_dialog_(file_dialog) { |
| CHECK(file_dialog_); |
| |
| if (!owner_window) { |
| return; |
| } |
| |
| if (!base::FeatureList::IsEnabled(kAutoCloseFileDialogs)) { |
| return; |
| } |
| |
| Microsoft::WRL::ComPtr<IFileDialogEvents> dialog_event_handler = |
| Microsoft::WRL::Make<AutoCloseDialogEventHandler>(owner_window); |
| if (!dialog_event_handler) { |
| return; |
| } |
| |
| file_dialog_->Advise(dialog_event_handler.Get(), &cookie_); |
| } |
| |
| ~ScopedAutoCloseDialogEventHandler() { |
| if (cookie_) { |
| file_dialog_->Unadvise(cookie_); |
| } |
| } |
| |
| private: |
| Microsoft::WRL::ComPtr<IFileDialog> file_dialog_; |
| DWORD cookie_ = 0; |
| }; |
| |
| // Distinguish directories from regular files. |
| bool IsDirectory(const base::FilePath& path) { |
| base::File::Info file_info; |
| return base::GetFileInfo(path, &file_info) ? file_info.is_directory |
| : path.EndsWithSeparator(); |
| } |
| |
| // Sets which path is going to be open when the dialog will be shown. If |
| // |default_path| is not only a directory, also sets the contents of the text |
| // box equals to the basename of the path. |
| bool SetDefaultPath(IFileDialog* file_dialog, |
| const base::FilePath& default_path) { |
| if (default_path.empty()) |
| return true; |
| |
| base::FilePath default_folder; |
| base::FilePath default_file_name; |
| if (IsDirectory(default_path)) { |
| default_folder = default_path; |
| } else { |
| default_folder = default_path.DirName(); |
| std::wstring sanitized = RemoveEnvVarFromFileName<wchar_t>( |
| default_path.BaseName().value(), std::wstring(L"%")); |
| default_file_name = base::FilePath(sanitized); |
| } |
| |
| // Do not fail the file dialog operation if the specified folder is invalid. |
| Microsoft::WRL::ComPtr<IShellItem> default_folder_shell_item; |
| if (SUCCEEDED(SHCreateItemFromParsingName( |
| default_folder.value().c_str(), nullptr, |
| IID_PPV_ARGS(&default_folder_shell_item)))) { |
| if (FAILED(file_dialog->SetFolder(default_folder_shell_item.Get()))) |
| return false; |
| } |
| |
| return SUCCEEDED(file_dialog->SetFileName(default_file_name.value().c_str())); |
| } |
| |
| // Sets the file extension filters on the dialog. |
| bool SetFilters(IFileDialog* file_dialog, |
| const std::vector<FileFilterSpec>& filter, |
| int filter_index) { |
| if (filter.empty()) |
| return true; |
| |
| // A COMDLG_FILTERSPEC instance does not own any memory. |filter| must still |
| // be alive at the time the dialog is shown. |
| std::vector<COMDLG_FILTERSPEC> comdlg_filterspec(filter.size()); |
| |
| for (size_t i = 0; i < filter.size(); ++i) { |
| comdlg_filterspec[i].pszName = base::as_wcstr(filter[i].description); |
| comdlg_filterspec[i].pszSpec = base::as_wcstr(filter[i].extension_spec); |
| } |
| |
| return SUCCEEDED(file_dialog->SetFileTypes(comdlg_filterspec.size(), |
| comdlg_filterspec.data())) && |
| SUCCEEDED(file_dialog->SetFileTypeIndex(filter_index)); |
| } |
| |
| // Sets the requested |dialog_options|, making sure to keep the default values |
| // when not overwritten. |
| bool SetOptions(IFileDialog* file_dialog, DWORD dialog_options) { |
| // First retrieve the default options for a file dialog. |
| DWORD options; |
| if (FAILED(file_dialog->GetOptions(&options))) |
| return false; |
| |
| options |= dialog_options; |
| |
| return SUCCEEDED(file_dialog->SetOptions(options)); |
| } |
| |
| // Configures a |file_dialog| object given the specified parameters. |
| bool ConfigureDialog(IFileDialog* file_dialog, |
| const std::u16string& title, |
| const std::u16string& ok_button_label, |
| const base::FilePath& default_path, |
| const std::vector<FileFilterSpec>& filter, |
| int filter_index, |
| DWORD dialog_options) { |
| // Set title. |
| if (!title.empty()) { |
| if (FAILED(file_dialog->SetTitle(base::as_wcstr(title)))) |
| return false; |
| } |
| |
| if (!ok_button_label.empty()) { |
| if (FAILED(file_dialog->SetOkButtonLabel(base::as_wcstr(ok_button_label)))) |
| return false; |
| } |
| |
| return SetDefaultPath(file_dialog, default_path) && |
| SetOptions(file_dialog, dialog_options) && |
| SetFilters(file_dialog, filter, filter_index); |
| } |
| |
| // Prompt the user for location to save a file. |
| // Callers should provide the filter string, and also a filter index. |
| // The parameter |index| indicates the initial index of filter description and |
| // filter pattern for the dialog box. If |index| is zero or greater than the |
| // number of total filter types, the system uses the first filter in the |
| // |filter| buffer. |index| is used to specify the initial selected extension, |
| // and when done contains the extension the user chose. The parameter |path| |
| // returns the file name which contains the drive designator, path, file name, |
| // and extension of the user selected file name. |def_ext| is the default |
| // extension to give to the file if the user did not enter an extension. |
| bool RunSaveFileDialog(HWND owner, |
| const std::u16string& title, |
| const base::FilePath& default_path, |
| const std::vector<FileFilterSpec>& filter, |
| DWORD dialog_options, |
| const std::wstring& def_ext, |
| int* filter_index, |
| base::FilePath* path) { |
| Microsoft::WRL::ComPtr<IFileSaveDialog> file_save_dialog; |
| if (FAILED(::CoCreateInstance(CLSID_FileSaveDialog, nullptr, |
| CLSCTX_INPROC_SERVER, |
| IID_PPV_ARGS(&file_save_dialog)))) { |
| return false; |
| } |
| |
| if (!ConfigureDialog(file_save_dialog.Get(), title, std::u16string(), |
| default_path, filter, *filter_index, dialog_options)) { |
| return false; |
| } |
| |
| file_save_dialog->SetDefaultExtension(def_ext.c_str()); |
| |
| // This handler auto-closes the file dialog if its owner window is closed. |
| auto auto_close_dialog_event_handler = |
| std::make_unique<ScopedAutoCloseDialogEventHandler>( |
| owner, file_save_dialog.Get()); |
| |
| // Never consider the current scope as hung. The hang watching deadline (if |
| // any) is not valid since the user can take unbounded time to choose the |
| // file. |
| base::HangWatcher::InvalidateActiveExpectations(); |
| |
| HRESULT hr = file_save_dialog->Show(owner); |
| BaseShellDialogImpl::DisableOwner(owner); |
| |
| // Remove the event handler regardless of the return value of Show(). |
| auto_close_dialog_event_handler = nullptr; |
| |
| if (FAILED(hr)) |
| return false; |
| |
| UINT file_type_index; |
| if (FAILED(file_save_dialog->GetFileTypeIndex(&file_type_index))) |
| return false; |
| |
| *filter_index = static_cast<int>(file_type_index); |
| |
| Microsoft::WRL::ComPtr<IShellItem> result; |
| if (FAILED(file_save_dialog->GetResult(&result))) |
| return false; |
| |
| base::win::ScopedCoMem<wchar_t> display_name; |
| if (FAILED(result->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, |
| &display_name))) { |
| return false; |
| } |
| |
| *path = base::FilePath(display_name.get()); |
| return true; |
| } |
| |
| // Runs an Open file dialog box, with similar semantics for input parameters as |
| // RunSaveFileDialog. |
| bool RunOpenFileDialog(HWND owner, |
| const std::u16string& title, |
| const std::u16string& ok_button_label, |
| const base::FilePath& default_path, |
| const std::vector<FileFilterSpec>& filter, |
| DWORD dialog_options, |
| int* filter_index, |
| std::vector<base::FilePath>* paths) { |
| Microsoft::WRL::ComPtr<IFileOpenDialog> file_open_dialog; |
| if (FAILED(::CoCreateInstance(CLSID_FileOpenDialog, nullptr, |
| CLSCTX_INPROC_SERVER, |
| IID_PPV_ARGS(&file_open_dialog)))) { |
| return false; |
| } |
| |
| // The FOS_FORCEFILESYSTEM option ensures that if the user enters a URL in the |
| // "File name" box, it will be downloaded locally and its new file path will |
| // be returned by the dialog. This was a default option in the deprecated |
| // GetOpenFileName API. |
| dialog_options |= FOS_FORCEFILESYSTEM; |
| |
| if (!ConfigureDialog(file_open_dialog.Get(), title, ok_button_label, |
| default_path, filter, *filter_index, dialog_options)) { |
| return false; |
| } |
| |
| // This handler auto-closes the file dialog if its owner window is closed. |
| auto auto_close_dialog_event_handler = |
| std::make_unique<ScopedAutoCloseDialogEventHandler>( |
| owner, file_open_dialog.Get()); |
| |
| // Never consider the current scope as hung. The hang watching deadline (if |
| // any) is not valid since the user can take unbounded time to choose the |
| // file. |
| base::HangWatcher::InvalidateActiveExpectations(); |
| |
| HRESULT hr = file_open_dialog->Show(owner); |
| BaseShellDialogImpl::DisableOwner(owner); |
| |
| // Remove the event handler regardless of the return value of Show(). |
| auto_close_dialog_event_handler = nullptr; |
| |
| if (FAILED(hr)) |
| return false; |
| |
| UINT file_type_index; |
| if (FAILED(file_open_dialog->GetFileTypeIndex(&file_type_index))) |
| return false; |
| |
| *filter_index = static_cast<int>(file_type_index); |
| |
| Microsoft::WRL::ComPtr<IShellItemArray> selected_items; |
| if (FAILED(file_open_dialog->GetResults(&selected_items))) |
| return false; |
| |
| DWORD result_count; |
| if (FAILED(selected_items->GetCount(&result_count))) |
| return false; |
| |
| DCHECK(result_count == 1 || (dialog_options & FOS_ALLOWMULTISELECT)); |
| |
| std::vector<base::FilePath> result(result_count); |
| for (DWORD i = 0; i < result_count; ++i) { |
| Microsoft::WRL::ComPtr<IShellItem> shell_item; |
| if (FAILED(selected_items->GetItemAt(i, &shell_item))) |
| return false; |
| |
| base::win::ScopedCoMem<wchar_t> display_name; |
| if (FAILED(shell_item->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, |
| &display_name))) { |
| return false; |
| } |
| |
| result[i] = base::FilePath(display_name.get()); |
| } |
| |
| // Only modify the out parameter if the enumeration didn't fail. |
| *paths = std::move(result); |
| return !paths->empty(); |
| } |
| |
| // Runs a Folder selection dialog box, passes back the selected folder in |path| |
| // and returns true if the user clicks OK. If the user cancels the dialog box |
| // the value in |path| is not modified and returns false. Run on the dialog |
| // thread. |
| bool ExecuteSelectFolder(HWND owner, |
| SelectFileDialog::Type type, |
| const std::u16string& title, |
| const base::FilePath& default_path, |
| std::vector<base::FilePath>* paths) { |
| DCHECK(paths); |
| |
| std::u16string new_title = title; |
| if (new_title.empty() && type == SelectFileDialog::SELECT_UPLOAD_FOLDER) { |
| // If it's for uploading don't use default dialog title to |
| // make sure we clearly tell it's for uploading. |
| new_title = |
| l10n_util::GetStringUTF16(IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE); |
| } |
| |
| std::u16string ok_button_label; |
| if (type == SelectFileDialog::SELECT_UPLOAD_FOLDER) { |
| ok_button_label = l10n_util::GetStringUTF16( |
| IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON); |
| } |
| |
| DWORD dialog_options = FOS_PICKFOLDERS; |
| |
| std::vector<FileFilterSpec> no_filter; |
| int filter_index = 0; |
| |
| return RunOpenFileDialog(owner, new_title, ok_button_label, default_path, |
| no_filter, dialog_options, &filter_index, paths); |
| } |
| |
| bool ExecuteSelectSingleFile(HWND owner, |
| const std::u16string& title, |
| const base::FilePath& default_path, |
| const std::vector<FileFilterSpec>& filter, |
| int* filter_index, |
| std::vector<base::FilePath>* paths) { |
| // Note: The title is not passed down for historical reasons. |
| // TODO(pmonette): Figure out if it's a worthwhile improvement. |
| return RunOpenFileDialog(owner, std::u16string(), std::u16string(), |
| default_path, filter, 0, filter_index, paths); |
| } |
| |
| bool ExecuteSelectMultipleFile(HWND owner, |
| const std::u16string& title, |
| const base::FilePath& default_path, |
| const std::vector<FileFilterSpec>& filter, |
| int* filter_index, |
| std::vector<base::FilePath>* paths) { |
| DWORD dialog_options = FOS_ALLOWMULTISELECT; |
| |
| // Note: The title is not passed down for historical reasons. |
| // TODO(pmonette): Figure out if it's a worthwhile improvement. |
| return RunOpenFileDialog(owner, std::u16string(), std::u16string(), |
| default_path, filter, dialog_options, filter_index, |
| paths); |
| } |
| |
| bool ExecuteSaveFile(HWND owner, |
| const base::FilePath& default_path, |
| const std::vector<FileFilterSpec>& filter, |
| const std::wstring& def_ext, |
| int* filter_index, |
| base::FilePath* path) { |
| DCHECK(path); |
| // Having an empty filter for a bad user experience. We should always |
| // specify a filter when saving. |
| DCHECK(!filter.empty()); |
| |
| DWORD dialog_options = FOS_OVERWRITEPROMPT; |
| |
| // Note: The title is not passed down for historical reasons. |
| // TODO(pmonette): Figure out if it's a worthwhile improvement. |
| return RunSaveFileDialog(owner, std::u16string(), default_path, filter, |
| dialog_options, def_ext, filter_index, path); |
| } |
| |
| } // namespace |
| |
| void ExecuteSelectFile( |
| SelectFileDialog::Type type, |
| const std::u16string& title, |
| const base::FilePath& default_path, |
| const std::vector<FileFilterSpec>& filter, |
| int file_type_index, |
| const std::wstring& default_extension, |
| HWND owner, |
| OnSelectFileExecutedCallback on_select_file_executed_callback) { |
| base::win::AssertComInitialized(); |
| std::vector<base::FilePath> paths; |
| switch (type) { |
| case SelectFileDialog::SELECT_FOLDER: |
| case SelectFileDialog::SELECT_UPLOAD_FOLDER: |
| case SelectFileDialog::SELECT_EXISTING_FOLDER: |
| ExecuteSelectFolder(owner, type, title, default_path, &paths); |
| break; |
| case SelectFileDialog::SELECT_SAVEAS_FILE: { |
| base::FilePath path; |
| if (ExecuteSaveFile(owner, default_path, filter, default_extension, |
| &file_type_index, &path)) { |
| paths.push_back(std::move(path)); |
| } |
| break; |
| } |
| case SelectFileDialog::SELECT_OPEN_FILE: |
| ExecuteSelectSingleFile(owner, title, default_path, filter, |
| &file_type_index, &paths); |
| break; |
| case SelectFileDialog::SELECT_OPEN_MULTI_FILE: |
| ExecuteSelectMultipleFile(owner, title, default_path, filter, |
| &file_type_index, &paths); |
| break; |
| case SelectFileDialog::SELECT_NONE: |
| NOTREACHED(); |
| } |
| |
| std::move(on_select_file_executed_callback).Run(paths, file_type_index); |
| } |
| |
| } // namespace ui |